阅读视图

发现新文章,点击刷新页面。

每日一题-最佳运动员的比拼回合🔴

n 名运动员参与一场锦标赛,所有运动员站成一排,并根据 最开始的 站位从 1n 编号(运动员 1 是这一排中的第一个运动员,运动员 2 是第二个运动员,依此类推)。

锦标赛由多个回合组成(从回合 1 开始)。每一回合中,这一排从前往后数的第 i 名运动员需要与从后往前数的第 i 名运动员比拼,获胜者将会进入下一回合。如果当前回合中运动员数目为奇数,那么中间那位运动员将轮空晋级下一回合。

  • 例如,当前回合中,运动员 1, 2, 4, 6, 7 站成一排
    • 运动员 1 需要和运动员 7 比拼
    • 运动员 2 需要和运动员 6 比拼
    • 运动员 4 轮空晋级下一回合

每回合结束后,获胜者将会基于最开始分配给他们的原始顺序(升序)重新排成一排。

编号为 firstPlayersecondPlayer 的运动员是本场锦标赛中的最佳运动员。在他们开始比拼之前,完全可以战胜任何其他运动员。而任意两个其他运动员进行比拼时,其中任意一个都有获胜的可能,因此你可以 裁定 谁是这一回合的获胜者。

给你三个整数 nfirstPlayersecondPlayer 。返回一个由两个值组成的整数数组,分别表示两位最佳运动员在本场锦标赛中比拼的 最早 回合数和 最晚 回合数。

 

示例 1:

输入:n = 11, firstPlayer = 2, secondPlayer = 4
输出:[3,4]
解释:
一种能够产生最早回合数的情景是:
回合 1:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
回合 2:2, 3, 4, 5, 6, 11
回合 3:2, 3, 4
一种能够产生最晚回合数的情景是:
回合 1:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
回合 2:1, 2, 3, 4, 5, 6
回合 3:1, 2, 4
回合 4:2, 4

示例 2:

输入:n = 5, firstPlayer = 1, secondPlayer = 5
输出:[1,1]
解释:两名最佳运动员 1 和 5 将会在回合 1 进行比拼。
不存在使他们在其他回合进行比拼的可能。

 

提示:

  • 2 <= n <= 28
  • 1 <= firstPlayer < secondPlayer <= n

最佳运动员的比拼回合

方法一:分析本质不同的站位情况 + 记忆化搜索

前言

本题思维难度较大。其中的有些技巧可能在其它的题目中很少出现。

读者在第一次阅读本题解时,可以多去思考「怎么做」,而尽量不要去思考「为什么要这么做」。

思路与算法

我们可以用 $F(n, f, s)$ 表示还剩余 $n$ 个人,并且两名最佳运动员分别是一排中从左往右数的第 $f$ 和 $s$ 名运动员时,他们比拼的最早回合数。

同理,我们用 $G(n, f, s)$ 表示他们比拼的最晚回合数。

那么如何进行状态转移呢?

只考虑本质不同的站位情况

如果我们单纯地用 $F(n, f, s)$ 来进行状态转移,会使得设计出的算法和编写出的代码都相当复杂。例如我们需要考虑 $f$ 是在左侧(即从前往后数)、中间(即轮空)还是右侧(即从后往前数),对于 $s$ 也需要考虑那么多情况,这样状态转移方程就相当麻烦。

我们可以考虑分析出本质不同的站位情况,得到下面的表格:

$s$ 在左侧 $s$ 在中间 $s$ 在右侧
$f$ 在左侧 保持不变 保持不变 保持不变
$f$ 在中间 等价于「$f$ 在左侧,$s$ 在中间」 不存在这种情况 等价于「$f$ 在左侧,$s$ 在中间」
$f$ 在右侧 等价于「$f$ 在左侧,$s$ 在右侧」 等价于「$f$ 在左侧,$s$ 在中间」 等价于「$f$ 在左侧,$s$ 在左侧」

其正确性在于:

  • $F(n, f, s) = F(n, s, f)$ 恒成立。即我们交换两名最佳运动员的位置,结果不会发生变化;

  • $F(n, f, s) = F(n, n+1-s, n+1-f)$ 恒成立。因为我们会让从前往后数的第 $i$ 运动员与从后往前数的第 $i$ 名运动员进行比拼,那么我们将所有的运动员看成一个整体,整体翻转一下,结果同样不会发生变化。

我们使用这两条变换规则,就可以保证在 $F(n, f, s)$ 中,$f$ 一定小于 $s$,那么 $f$ 一定在左侧,而 $s$ 可以在左侧、中间或者右侧。这样我们就将原本的 $8$ 种情况减少到了 $3$ 种情况。

对于 $G(n, f, s)$,其做法是完全相同的。

状态转移方程的设计

既然我们知道了 $f$ 一定在左侧,那么我们就可以根据 $s$ 在左侧、中间还是右侧,分别设计状态转移方程了。

fig1

如果 $s$ 在左侧,如上图所示:

  • $f$ 左侧有 $f-1$ 名运动员,它们会与右侧对应的运动员进行比拼,因此剩下 $[0, f-1]$ 名运动员;

  • $f$ 与 $s$ 中间有 $s-f-1$ 名运动员,它们会与右侧对应的运动员进行比拼,因此剩下 $[0, s-f-1]$ 名运动员。

如果 $f-1$ 名运动员中剩下了 $i$ 名,而 $s-f-1$ 名运动员中剩下了 $j$ 名,那么在下一回合中,两名最佳运动员分别位于位置 $i+1$ 和位置 $i+j+2$,而剩余的运动员总数为 $\lfloor \dfrac{n+1}{2} \rfloor$,其中 $\lfloor x \rfloor$ 表示对 $x$ 向下取整。因此我们可以得到状态转移方程:

$$
F(n, f, s) = \left( \min_{i\in [0,f-1], j \in [0, s-f-1]} F(\lfloor \frac{n+1}{2} \rfloor, i + 1, i + j + 2) \right) + 1
$$

fig2

如果 $s$ 在中间,如上图所示,我们可以发现状态转移方程与 $s$ 在左侧的情况是完全相同的。

如果 $s$ 在右侧,那么情况会较为麻烦,会有三种情况:

  • 最简单的情况就是 $f$ 和 $s$ 恰好比拼,即 $f+s=n+1$,那么 $F(n, f, s)=1$;

  • 此外,设这一回合与 $s$ 比拼的是 $s'=n+1-s$,那么 $f < s'$ 是一种情况,$f > s'$ 是另一种情况。

fig3

然而我们可以知道,根据类似上一节「本质不同的站位情况」的分析,我们将 $f$ 变为 $n+1-s$,$s$ 变为 $n+1-f$,这样 $f$ 仍然小于 $s$,并且 $f$ 也小于 $s'$ 了。因此我们只需要考虑 $f < s'$ 的情况,如上图所示:

  • $f$ 左侧有 $f-1$ 名运动员,它们会与右侧对应的运动员进行比拼,因此剩下 $[0, f-1]$ 名运动员;

  • $f$ 与 $s'$ 中间有 $s'-f-1$ 名运动员,它们会与右侧对应的运动员进行比拼,因此剩下 $[0, s'-f-1]$ 名运动员;

  • $s'$ 一定会输给 $s$;

  • $s'$ 与 $s$ 中间有 $n - 2s'$ 名运动员。如果 $n-2s'$ 是偶数,那么他们两两之间比拼,剩下 $\dfrac{n-2s'}{2}$ 名运动员;如果 $n-2s'$ 是奇数,那么其中一人轮空,剩余两两之间比拼,剩下 $\dfrac{n-2s'+1}{2}$ 名运动员。因此,无论 $n-2s'$ 是奇数还是偶数,$s'$ 与 $s$ 中间一定会有 $\lfloor \dfrac{n-2s'+1}{2} \rfloor$ 名运动员。

如果 $f-1$ 名运动员中剩下了 $i$ 名,而 $s'-f-1$ 名运动员中剩下了 $j$ 名,那么在下一回合中,两名最佳运动员分别位于位置 $i+1$ 和位置 $i+j+\lfloor \dfrac{n-2s'+1}{2} \rfloor+2$。因此我们可以得到状态转移方程:

$$
F(n, f, s) = \left( \min_{i\in [0,f-1], j \in [0, s'-f-1]} F(\lfloor \frac{n+1}{2} \rfloor, i + 1, i+j+\lfloor \dfrac{n-2s'+1}{2} \rfloor+2) \right) + 1
$$

这样我们就得到了所有关于 $F$ 的状态转移方程。而关于 $G$ 的状态转移方程,我们只需要把所有的 $\min$ 改为 $\max$ 即可。

细节

在「本质不同的站位情况」一节中,我们提到了两种变换规则。那么我们具体应当在 $n, f, s$ 满足什么关系(而不是抽象的「左侧」「中间」「右侧」)时使用其中的哪些规则呢?

这里有很多种设计方法,我们介绍一种较为简单的,题解代码中使用的方法:

  • 首先我们使用自顶向下的记忆化搜索代替动态规划进行状态转移,这样写更加简洁直观,并且无需考虑状态的求值顺序;

  • 记忆化搜索的入口为 $F(n, \textit{firstPlayer}, \textit{secondPlayer})$。我们在开始记忆化搜索之前,先通过变换规则 $F(n, f, s) = F(n, s, f)$ 使得 $\textit{firstPlayer}$ 一定小于 $\textit{secondPlayer}$,这样一来,由于另一条变换规则 $F(n, f, s) = F(n, n+1-s, n+1-f)$ 不会改变 $f$ 与 $s$ 间的大小关系,因此在接下来的记忆化搜索中,$f < s$ 是恒成立的,我们也就无需使用变换规则 $F(n, f, s) = F(n, s, f)$ 了;

  • 在之前表格中,我们需要变换的情况有 $5$ 种,分别是:「$f$ 在中间,$s$ 在左侧」「$f$ 在中间,$s$ 在右侧」「$f$ 在右侧,$s$ 在左侧」「$f$ 在右侧,$s$ 在中间」「$f$ 在右侧,$s$ 在右侧」。由于我们已经保证了 $f < s$ 恒成立,因此这 $5$ 种情况中只剩下 $2$ 种是需要处理的,即:「$f$ 在中间,$s$ 在右侧」和「$f$ 在右侧,$s$ 在右侧」。此外,我们在「状态转移方程的设计」一节中还发现了一种需要处理的情况,即「$f$ 在左侧,$s$ 在右侧,并且 $f>s'=n+1-s$」。

    那么这 $3$ 种情况是否可以统一呢?对于最后一种情况,我们有 $f+s > n+1$,而「$f$ 在中间,$s$ 在右侧」和「$f$ 在右侧,$s$ 在右侧」也恰好满足 $f+s > n+1$,并且所有不需要变换的情况都不满足 $f+s > n+1$。因此我们只需要在 $f+s > n+1$ 时,使用一次变换规则 $F(n, f, s) = F(n, n+1-s, n+1-f)$ 就行了。

代码

由于 $\texttt{Python}$ 中可以很方便地使用 $\texttt{@cache}$ 进行记忆化搜索,因此在下面 $\texttt{Python}$ 的代码中,我们无需显式地定义 $F$ 和 $G$,函数 $\textit{dp}(n, f, s)$ 返回的二元组即为 $F(n, f, s)$ 和 $G(n, f, s)$。

###C++

class Solution {
private:
    int F[30][30][30], G[30][30][30];

public:
    pair<int, int> dp(int n, int f, int s) {
        if (F[n][f][s]) {
            return {F[n][f][s], G[n][f][s]};
        }
        if (f + s == n + 1) {
            return {1, 1};
        }

        // F(n,f,s)=F(n,n+1-s,n+1-f)
        if (f + s > n + 1) {
            tie(F[n][f][s], G[n][f][s]) = dp(n, n + 1 - s, n + 1 - f);
            return {F[n][f][s], G[n][f][s]};
        }

        int earlist = INT_MAX, latest = INT_MIN;
        int n_half = (n + 1) / 2;

        if (s <= n_half) {
            // 在左侧或者中间
            for (int i = 0; i < f; ++i) {
                for (int j = 0; j < s - f; ++j) {
                    auto [x, y] = dp(n_half, i + 1, i + j + 2);
                    earlist = min(earlist, x);
                    latest = max(latest, y);
                }
            }
        }
        else {
            // s 在右侧
            // s'
            int s_prime = n + 1 - s;
            int mid = (n - 2 * s_prime + 1) / 2;
            for (int i = 0; i < f; ++i) {
                for (int j = 0; j < s_prime - f; ++j) {
                    auto [x, y] = dp(n_half, i + 1, i + j + mid + 2);
                    earlist = min(earlist, x);
                    latest = max(latest, y);
                }
            }
        }

        return {F[n][f][s] = earlist + 1, G[n][f][s] = latest + 1};
    }

    vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        memset(F, 0, sizeof(F));
        memset(G, 0, sizeof(G));

        // F(n,f,s) = F(n,s,f)
        if (firstPlayer > secondPlayer) {
            swap(firstPlayer, secondPlayer);
        }

        auto [earlist, latest] = dp(n, firstPlayer, secondPlayer);
        return {earlist, latest};
    }
};

###Python

class Solution:
    def earliestAndLatest(self, n: int, firstPlayer: int, secondPlayer: int) -> List[int]:
        @cache
        def dp(n: int, f: int, s: int) -> (int, int):
            if f + s == n + 1:
                return (1, 1)
            
            # F(n,f,s)=F(n,n+1-s,n+1-f)
            if f + s > n + 1:
                return dp(n, n + 1 - s, n + 1 - f)
            
            earliest, latest = float("inf"), float("-inf")
            n_half = (n + 1) // 2

            if s <= n_half:
                # s 在左侧或者中间
                for i in range(f):
                    for j in range(s - f):
                        x, y = dp(n_half, i + 1, i + j + 2)
                        earliest = min(earliest, x)
                        latest = max(latest, y)
            else:
                # s 在右侧
                # s'
                s_prime = n + 1 - s
                mid = (n - 2 * s_prime + 1) // 2
                for i in range(f):
                    for j in range(s_prime - f):
                        x, y = dp(n_half, i + 1, i + j + mid + 2)
                        earliest = min(earliest, x)
                        latest = max(latest, y)
            
            return (earliest + 1, latest + 1)

        # F(n,f,s) = F(n,s,f)
        if firstPlayer > secondPlayer:
            firstPlayer, secondPlayer = secondPlayer, firstPlayer
        
        earliest, latest = dp(n, firstPlayer, secondPlayer)
        dp.cache_clear()
        return [earliest, latest]

###Java

class Solution {
    private int[][][] F = new int[30][30][30];
    private int[][][] G = new int[30][30][30];

    private int[] dp(int n, int f, int s) {
        if (F[n][f][s] != 0) {
            return new int[]{F[n][f][s], G[n][f][s]};
        }
        if (f + s == n + 1) {
            return new int[]{1, 1};
        }
        // F(n,f,s) = F(n, n + 1 - s, n + 1 - f)
        if (f + s > n + 1) {
            int[] res = dp(n, n + 1 - s, n + 1 - f);
            F[n][f][s] = res[0];
            G[n][f][s] = res[1];
            return new int[]{F[n][f][s], G[n][f][s]};
        }

        int earlist = Integer.MAX_VALUE, latest = Integer.MIN_VALUE;
        int n_half = (n + 1) / 2;
        if (s <= n_half) {
            // 在左侧或者中间
            for (int i = 0; i < f; ++i) {
                for (int j = 0; j < s - f; ++j) {
                    int[] res = dp(n_half, i + 1, i + j + 2);
                    earlist = Math.min(earlist, res[0]);
                    latest = Math.max(latest, res[1]);
                }
            }
        } else {
            // s 在右侧
            int s_prime = n + 1 - s;
            int mid = (n - 2 * s_prime + 1) / 2;
            for (int i = 0; i < f; ++i) {
                for (int j = 0; j < s_prime - f; ++j) {
                    int[] res = dp(n_half, i + 1, i + j + mid + 2);
                    earlist = Math.min(earlist, res[0]);
                    latest = Math.max(latest, res[1]);
                }
            }
        }

        F[n][f][s] = earlist + 1;
        G[n][f][s] = latest + 1;
        return new int[]{F[n][f][s], G[n][f][s]};
    }

    public int[] earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        // F(n,f,s) = F(n,s,f)
        if (firstPlayer > secondPlayer) {
            int temp = firstPlayer;
            firstPlayer = secondPlayer;
            secondPlayer = temp;
        }

        int[] res = dp(n, firstPlayer, secondPlayer);
        return new int[]{res[0], res[1]};
    }
}

###C#

public class Solution {
    private int[,,] F = new int[30, 30, 30];
    private int[,,] G = new int[30, 30, 30];

    private int[] Dp(int n, int f, int s) {
        if (F[n, f, s] != 0) {
            return new int[]{F[n, f, s], G[n, f, s]};
        }
        if (f + s == n + 1) {
            return new int[]{1, 1};
        }

        // F(n,f,s) = F(n,n+1-s,n+1-f)
        if (f + s > n + 1) {
            int[] res = Dp(n, n + 1 - s, n + 1 - f);
            F[n, f, s] = res[0];
            G[n, f, s] = res[1];
            return new int[]{F[n, f, s], G[n, f, s]};
        }

        int earlist = int.MaxValue, latest = int.MinValue;
        int n_half = (n + 1) / 2;
        if (s <= n_half) {
            // 在左侧或者中间
            for (int i = 0; i < f; ++i) {
                for (int j = 0; j < s - f; ++j) {
                    int[] res = Dp(n_half, i + 1, i + j + 2);
                    earlist = Math.Min(earlist, res[0]);
                    latest = Math.Max(latest, res[1]);
                }
            }
        } else {
            // s 在右侧
            int s_prime = n + 1 - s;
            int mid = (n - 2 * s_prime + 1) / 2;
            for (int i = 0; i < f; ++i) {
                for (int j = 0; j < s_prime - f; ++j) {
                    int[] res = Dp(n_half, i + 1, i + j + mid + 2);
                    earlist = Math.Min(earlist, res[0]);
                    latest = Math.Max(latest, res[1]);
                }
            }
        }

        F[n, f, s] = earlist + 1;
        G[n, f, s] = latest + 1;
        return new int[]{F[n, f, s], G[n, f, s]};
    }

    public int[] EarliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        // F(n,f,s) = F(n,s,f)
        if (firstPlayer > secondPlayer) {
            int temp = firstPlayer;
            firstPlayer = secondPlayer;
            secondPlayer = temp;
        }

        int[] res = Dp(n, firstPlayer, secondPlayer);
        return new int[]{res[0], res[1]};
    }
}

###Go

func earliestAndLatest(n int, firstPlayer int, secondPlayer int) []int {
const maxN = 30
var F [maxN][maxN][maxN]int
var G [maxN][maxN][maxN]int
if firstPlayer > secondPlayer {
firstPlayer, secondPlayer = secondPlayer, firstPlayer
}

var dp func(n, f, s int) (int, int)
dp = func(n, f, s int) (int, int) {
if F[n][f][s] != 0 {
return F[n][f][s], G[n][f][s]
}
if f + s == n + 1 {
return 1, 1
}
// F(n,f,s) = F(n,n+1-s,n+1-f)
if f + s > n + 1 {
x, y := dp(n, n + 1 - s, n + 1 - f)
F[n][f][s] = x
G[n][f][s] = y
return x, y
}

earlist := math.MaxInt32
latest := math.MinInt32
n_half := (n + 1) / 2
if s <= n_half {
// 在左侧或者中间
for i := 0; i < f; i++ {
for j := 0; j < s - f; j++ {
x, y := dp(n_half, i + 1, i + j + 2)
earlist = min(earlist, x)
latest = max(latest, y)
}
}
} else {
// s 在右侧
s_prime := n + 1 - s
mid := (n - 2 * s_prime + 1) / 2
for i := 0; i < f; i++ {
for j := 0; j < s_prime-f; j++ {
x, y := dp(n_half, i + 1, i + j + mid + 2)
earlist = min(earlist, x)
latest = max(latest, y)
}
}
}

F[n][f][s] = earlist + 1
G[n][f][s] = latest + 1
return F[n][f][s], G[n][f][s]
}

earlist, latest := dp(n, firstPlayer, secondPlayer)
return []int{earlist, latest}
}

###C

int F[30][30][30] = {0};
int G[30][30][30] = {0};

void dp(int n, int f, int s, int* earlist, int* latest) {
    if (F[n][f][s]) {
        *earlist = F[n][f][s];
        *latest = G[n][f][s];
        return;
    }
    if (f + s == n + 1) {
        *earlist = 1;
        *latest = 1;
        return;
    }

    // F(n,f,s) = F(n,n+1-s,n+1-f)
    if (f + s > n + 1) {
        int x, y;
        dp(n, n + 1 - s, n + 1 - f, &x, &y);
        F[n][f][s] = x;
        G[n][f][s] = y;
        *earlist = x;
        *latest = y;
        return;
    }

    int min_earlist = INT_MAX;
    int max_latest = INT_MIN;
    int n_half = (n + 1) / 2;

    if (s <= n_half) {
        // 在左侧或者中间
        for (int i = 0; i < f; ++i) {
            for (int j = 0; j < s - f; ++j) {
                int x, y;
                dp(n_half, i + 1, i + j + 2, &x, &y);
                if (x < min_earlist) {
                    min_earlist = x;
                }
                if (y > max_latest) {
                    max_latest = y;
                }
            }
        }
    } else {
        // s 在右侧
        int s_prime = n + 1 - s;
        int mid = (n - 2 * s_prime + 1) / 2;
        for (int i = 0; i < f; ++i) {
            for (int j = 0; j < s_prime - f; ++j) {
                int x, y;
                dp(n_half, i + 1, i + j + mid + 2, &x, &y);
                if (x < min_earlist) {
                    min_earlist = x;
                }
                if (y > max_latest) {
                    max_latest = y;
                }
            }
        }
    }

    F[n][f][s] = min_earlist + 1;
    G[n][f][s] = max_latest + 1;
    *earlist = F[n][f][s];
    *latest = G[n][f][s];
}

int* earliestAndLatest(int n, int firstPlayer, int secondPlayer, int* returnSize) {
    *returnSize = 2;
    int* result = (int*)malloc(2 * sizeof(int));

    // F(n,f,s) = F(n,s,f)
    if (firstPlayer > secondPlayer) {
        int temp = firstPlayer;
        firstPlayer = secondPlayer;
        secondPlayer = temp;
    }

    int earlist, latest;
    dp(n, firstPlayer, secondPlayer, &earlist, &latest);
    result[0] = earlist;
    result[1] = latest;
    return result;
}

###JavaScript

var earliestAndLatest = function(n, firstPlayer, secondPlayer) {
    F = Array.from({ length: 30 }, () => 
            Array.from({ length: 30 }, () => 
                Array(30).fill(0)));
    G = Array.from({ length: 30 }, () => 
            Array.from({ length: 30 }, () => 
                Array(30).fill(0)));

    const dp = (n, f, s) => {
        if (this.F[n][f][s]) {
            return [F[n][f][s], G[n][f][s]];
        }
        if (f + s === n + 1) {
            return [1, 1];
        }
        // F(n,f,s)=F(n,n+1-s,n+1-f)
        if (f + s > n + 1) {
            const [x, y] = dp(n, n + 1 - s, n + 1 - f);
            F[n][f][s] = x;
            G[n][f][s] = y;
            return [x, y];
        }

        let earlist = Infinity;
        let latest = -Infinity;
        const n_half = Math.floor((n + 1) / 2);
        if (s <= n_half) {
            // 在左侧或者中间
            for (let i = 0; i < f; i++) {
                for (let j = 0; j < s - f; j++) {
                    const [x, y] = dp(n_half, i + 1, i + j + 2);
                    earlist = Math.min(earlist, x);
                    latest = Math.max(latest, y);
                }
            }
        } else {
            // s 在右侧
            const s_prime = n + 1 - s;
            const mid = Math.floor((n - 2 * s_prime + 1) / 2);
            for (let i = 0; i < f; i++) {
                for (let j = 0; j < s_prime - f; j++) {
                    const [x, y] = dp(n_half, i + 1, i + j + mid + 2);
                    earlist = Math.min(earlist, x);
                    latest = Math.max(latest, y);
                }
            }
        }

        F[n][f][s] = earlist + 1;
        G[n][f][s] = latest + 1;
        return [F[n][f][s], G[n][f][s]];
    };

    // F(n,f,s) = F(n,s,f)
    if (firstPlayer > secondPlayer) {
        [firstPlayer, secondPlayer] = [secondPlayer, firstPlayer];
    }
    const [earlist, latest] = dp(n, firstPlayer, secondPlayer);
    return [earlist, latest];
};

###TypeScript

function earliestAndLatest(n: number, firstPlayer: number, secondPlayer: number): number[] {
    const F = Array.from({ length: 30 }, () => 
        Array.from({ length: 30 }, () => 
            Array(30).fill(0)));
    const G = Array.from({ length: 30 }, () => 
        Array.from({ length: 30 }, () => 
            Array(30).fill(0)));
    
    function dp(n: number, f: number, s: number): [number, number] {
        if (F[n][f][s]) {
            return [F[n][f][s], G[n][f][s]];
        }
        if (f + s === n + 1) {
            return [1, 1];
        }

        // F(n,f,s)=F(n,n+1-s,n+1-f)
        if (f + s > n + 1) {
            const [x, y] = dp(n, n + 1 - s, n + 1 - f);
            F[n][f][s] = x;
            G[n][f][s] = y;
            return [x, y];
        }

        let earlist = Infinity;
        let latest = -Infinity;
        const n_half = Math.floor((n + 1) / 2);

        if (s <= n_half) {
            // 在左侧或者中间
            for (let i = 0; i < f; i++) {
                for (let j = 0; j < s - f; j++) {
                    const [x, y] = dp(n_half, i + 1, i + j + 2);
                    earlist = Math.min(earlist, x);
                    latest = Math.max(latest, y);
                }
            }
        } else {
            // s 在右侧
            const s_prime = n + 1 - s;
            const mid = Math.floor((n - 2 * s_prime + 1) / 2);
            for (let i = 0; i < f; i++) {
                for (let j = 0; j < s_prime - f; j++) {
                    const [x, y] = dp(n_half, i + 1, i + j + mid + 2);
                    earlist = Math.min(earlist, x);
                    latest = Math.max(latest, y);
                }
            }
        }

        F[n][f][s] = earlist + 1;
        G[n][f][s] = latest + 1;
        return [F[n][f][s], G[n][f][s]];
    };

    // F(n,f,s) = F(n,s,f)
    if (firstPlayer > secondPlayer) {
        [firstPlayer, secondPlayer] = [secondPlayer, firstPlayer];
    }

    const [earlist, latest] = dp(n, firstPlayer, secondPlayer);
    return [earlist, latest];
};

###Rust

use std::cmp::{min, max};

impl Solution {
    pub fn earliest_and_latest(n: i32, first_player: i32, second_player: i32) -> Vec<i32> {
        const MAX_N: usize = 30;
        let mut f = [[[0; MAX_N]; MAX_N]; MAX_N];
        let mut g = [[[0; MAX_N]; MAX_N]; MAX_N];
        
        let mut first = first_player as usize;
        let mut second = second_player as usize;
        if first > second {
            std::mem::swap(&mut first, &mut second);
        }
        let (earliest, latest) = Self::dp(n as usize, first, second, &mut f, &mut g);
        vec![earliest, latest]
    }

    fn dp(n: usize, first: usize, second: usize, f: &mut [[[i32; 30]; 30]; 30], g: &mut [[[i32; 30]; 30]; 30]) -> (i32, i32) {
        if f[n][first][second] != 0 {
            return (f[n][first][second], g[n][first][second]);
        }
        
        if first + second == n + 1 {
            return (1, 1);
        }
        
        // 对称情况处理
        if first + second > n + 1 {
            let (x, y) = Self::dp(n, n + 1 - second, n + 1 - first, f, g);
            f[n][first][second] = x;
            g[n][first][second] = y;
            return (x, y);
        }

        let mut earliest = i32::MAX;
        let mut latest = i32::MIN;
        let n_half = (n + 1) / 2;
        if second <= n_half {
            // 都在左侧或中间
            for i in 0..first {
                for j in 0..(second - first) {
                    let (x, y) = Self::dp(n_half, i + 1, i + j + 2, f, g);
                    earliest = min(earliest, x);
                    latest = max(latest, y);
                }
            }
        } else {
            // second在右侧
            let s_prime = n + 1 - second;
            let mid = (n - 2 * s_prime + 1) / 2;
            for i in 0..first {
                for j in 0..(s_prime - first) {
                    let (x, y) = Self::dp(n_half, i + 1, i + j + mid + 2, f, g);
                    earliest = min(earliest, x);
                    latest = max(latest, y);
                }
            }
        }
        f[n][first][second] = earliest + 1;
        g[n][first][second] = latest + 1;
        (f[n][first][second], g[n][first][second])
    }
}

复杂度分析

  • 时间复杂度:$O(n^4 \log n)$。在状态 $F(n, f, s)$ 中($G(n, f, s)$ 同理),每一维的范围都是 $O(n)$,而每一个状态需要 $O(n^2)$ 的时间枚举所有可以转移而来的状态,因此整个算法的时间复杂度为 $O(n^5)$。

    然而我们可以发现,$F(n, f, s)$ 中的 $n$ 的取值个数是有限的,在记忆化搜索的过程中,$n$ 会变成 $\lfloor \dfrac{n+1}{2} \rfloor$ 后继续向下递归,因此 $n$ 的取值个数只有 $O(\log n)$ 种,即总时间复杂度为 $O(n^4 \log n)$。

  • 空间复杂度:$O(n^2 \log n)$ 或 $O(n^3)$,即为存储所有状态需要的空间。在 $\texttt{C++}$ 代码中,我们使用数组存储所有状态,即使 $n$ 的取值个数只有 $O(\log n)$ 种,我们还是需要对第一维开辟 $O(n)$ 的空间。而在 $\texttt{Python}$ 代码中,$\texttt{@cache}$ 使用元组 $\texttt{tuple}$ 作为字典 $\texttt{dict}$ 的键来存储所有状态的值,状态的数量为 $O(n^2 \log n)$,那么使用的空间也为 $O(n^2 \log n)$。

此外,由于力扣平台上对于运行时间的计算是取所有测试数据的运行时间总和,所有的测试数据会在一次运行中全部测试完成,而本题中记忆化存储的结果在不同的测试数据之间是可以共享的。因此,代码中清空记忆化结果的命令(例如 $\texttt{C++}$ 中的 $\texttt{memset}$ 或者 $\texttt{Python}$ 中的 $\texttt{cache_clear}$)是可以省略的。

随机化模拟

为每一个运动员随机生成一个战斗力(保证firstPlayer和secondPlayer为最大和次大),当两个运动员进行比拼时战斗力较高的获胜。
不断模拟,统计firstPlayer和secondPlayer相遇的轮次的最大值和最小值即可。
由于力扣的计时似乎是看所有用例的总用时,所以需要根据n的大小来设置模拟次数。

注:此方法为比赛时走投无路试图水分的方法,日常刷题建议学习其他dp之类的稳定性做法,不建议为刷题而刷题。(厚着脸皮给自己的记忆化搜索题解引个流> <)

struct node
{
    int a , b;
    bool operator <(const node &p)
    {
        return b < p.b;
    }
}player[30];

class Solution {
public:
    vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) 
    {
        srand(time(NULL));

        // 根据n的大小设置模拟次数
        int N;
        if(n <= 10)
            N = 800;
        else if (n <= 20)
            N = 8000;
        else N = 38000;

        int ans1 = 9999 , ans2 = 0 , rest , now;
        
        while(N--)
        {
            // 剩余的运动员
            rest = n;

            // 初始化战斗力
            for(int i = 1 ; i <= n ; i++)
            {
                player[i].a = rand() % 1075943;
                player[i].b = i;
            }
            player[firstPlayer].a = 11000000;
            player[secondPlayer].a = 10999999;
            
            // 统计轮次
            now = 1;
         
            // 模拟比赛
            while(rest > 1)
            {
                for(int i = 1 ; i <= rest / 2 ; i++)
                {
                    if(player[i].a < player[rest + 1 - i].a)
                        player[i] = player[rest + 1 - i];

                    // 统计firstPlayer和secondPlayer相遇轮次的最大值和最小值
                    if(player[i].b == firstPlayer && player[rest + 1 - i].b == secondPlayer)
                        ans1 = min(ans1 , now) , ans2 = max(ans2 , now);
                }
                now++;
                rest = (rest + 1) / 2;
                sort(player + 1 , player + rest + 1);
            }
        }
        
        vector<int> ans = {ans1 , ans2};
        return ans;

    }
};

最后,在此题的数据范围内,应该模拟多少次才能稳定地求出准确值呢?
我把所有case都跑了一遍,在n=28时,随机模拟探测到该情况最小轮次的概率约为0.000235731(取探测到该情况200次时的频率作为概率)。故可以计算出当我们模拟88000次时,可以保证在1000个测试中有99.9999%的几率通过测试。。。我这里设置的38000次模拟,在200个用例的情况下,只能保证97.5%的通过率> <

【图解】从 O(n^4) 到 O(1):动态规划 / 贪心(Python/Java/C++/Go)

方法一:动态规划

lc1900-DP.png{:width=550}

不失一般性(减少分类讨论的情况),可以在枚举前处理一下两名选手的位置:如果 $A$ 左侧的人比 $B$ 右侧的人还多,则将 $A$ 的位置改为 $B$ 的对称位置 $B'$,$B$ 的位置改为 $A$ 的对称位置 $A'$。这样 $A$ 一定位于中轴线左侧,我们无需讨论 $A$ 在中轴线右侧的情况。

根据上图,定义 $\textit{dfs}(n,\textit{first},\textit{second})$ 表示在当前有 $n$ 人,$A$ 的位置为 $\textit{first}$,$B$ 的位置为 $\textit{second}$ 的情况下,最早回合数和最晚回合数($\textit{dfs}$ 返回两个数)。

枚举 $A$ 左侧保留 $\textit{left}=0,1,2,\ldots, \textit{first}-1$ 个人。

枚举 $AB$ 之间保留 $\textit{mid}$ 个人:

  • 如果 $B$ 在中轴线或中轴线左侧,那么 $\textit{mid}$ 最小是 $0$,最大是 $\textit{second}-\textit{first}-1$。
  • 如果 $B$ 在中轴线右侧。如下图所示,$\textit{mid}$ 最小为 $B$ 到中轴线(含)的人数 $\textit{second}-\left\lfloor\dfrac{n}{2}\right\rfloor-1$;最大为 $A$ 到中轴线(含)的人数 $\left\lceil\dfrac{n}{2}\right\rceil - \textit{first}-1$,其中 $-1$ 是去掉 $B'$。

lc1900-保留人数.png{:width=550}

现在问题变成:计算在当前有 $m = \left\lceil\dfrac{n}{2}\right\rceil$ 人,$A$ 的位置为 $\textit{left}+1$,$B$ 的位置为 $\textit{left}+\textit{mid} + 2$ 的情况下,最早回合数和最晚回合数,即 $\textit{dfs}(m, \textit{left}+1, \textit{left}+\textit{mid} + 2)$。用子问题的返回结果,更新最早回合数的最小值,以及最晚回合数的最大值。

递归边界:如果 $\textit{first}+\textit{second}=n+1$,两人相遇,返回 $(1,1)$。

递归入口:$\textit{dfs}(n,\textit{firstPlayer}, \textit{secondPlayer})$。

注:$\left\lceil\dfrac{n}{2}\right\rceil = \left\lfloor\dfrac{n+1}{2}\right\rfloor$。

关于记忆化搜索,见 动态规划入门:从记忆化搜索到递推【基础算法精讲 17】

class Solution:
    def earliestAndLatest(self, n: int, firstPlayer: int, secondPlayer: int) -> List[int]:
        @cache  # 缓存装饰器,避免重复计算 dfs(一行代码实现记忆化)
        def dfs(n: int, first: int, second: int) -> Tuple[int, int]:
            # AB 相遇
            if first + second == n + 1:
                return 1, 1

            # 保证 A 左边人数比 B 右边人数少
            # 注:题目已保证 first < second
            if first + second > n + 1:
                first, second = n + 1 - second, n + 1 - first

            m = (n + 1) // 2  # 下一回合人数
            # AB 之间保留 [min_mid, max_mid) 个人
            min_mid = 0 if second <= m else second - n // 2 - 1
            max_mid = second - first if second <= m else m - first
            earliest, latest = inf, 0

            for left in range(first):  # 枚举 A 左侧保留 left 个人
                for mid in range(min_mid, max_mid):  # 枚举 AB 之间保留 mid 个人
                    # 无需枚举 B 右侧保留多少个人,因为剩下的 m-2-left-mid 个人都在 B 右侧
                    e, l = dfs(m, left + 1, left + mid + 2)
                    earliest = min(earliest, e)
                    latest = max(latest, l)

            # 加上当前回合
            return earliest + 1, latest + 1

        return list(dfs(n, firstPlayer, secondPlayer))
@cache  # 缓存装饰器,避免重复计算 dfs(一行代码实现记忆化)
def dfs(n: int, first: int, second: int) -> Tuple[int, int]:
    # AB 相遇
    if first + second == n + 1:
        return 1, 1

    # 保证 A 左边人数比 B 右边人数少
    # 注:题目已保证 first < second
    if first + second > n + 1:
        first, second = n + 1 - second, n + 1 - first

    m = (n + 1) // 2  # 下一回合人数
    # AB 之间保留 [min_mid, max_mid) 个人
    min_mid = 0 if second <= m else second - n // 2 - 1
    max_mid = second - first if second <= m else m - first
    earliest, latest = inf, 0

    for left in range(first):  # 枚举 A 左侧保留 left 个人
        for mid in range(min_mid, max_mid):  # 枚举 AB 之间保留 mid 个人
            # 无需枚举 B 右侧保留多少个人,因为剩下的 m-2-left-mid 个人都在 B 右侧
            e, l = dfs(m, left + 1, left + mid + 2)
            earliest = min(earliest, e)
            latest = max(latest, l)

    # 加上当前回合
    return earliest + 1, latest + 1


class Solution:
    def earliestAndLatest(self, n: int, firstPlayer: int, secondPlayer: int) -> List[int]:
        return list(dfs(n, firstPlayer, secondPlayer))
class Solution {
    public int[] earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        int[][][][] memo = new int[n + 1][n + 1][n + 1][2];
        return dfs(n, firstPlayer, secondPlayer, memo);
    }

    private int[] dfs(int n, int first, int second, int[][][][] memo) {
        // AB 相遇
        if (first + second == n + 1) {
            return new int[]{1, 1};
        }

        // 保证 A 左边人数比 B 右边人数少
        // 注:题目已保证 first < second
        if (first + second > n + 1) {
            int tmp = first;
            first = n + 1 - second;
            second = n + 1 - tmp;
        }
        
        int[] mem = memo[n][first][second];
        if (mem[0] > 0) { // 之前计算过
            return mem;
        }

        int m = (n + 1) / 2; // 下一回合人数
        // AB 之间保留 [minMid, maxMid) 个人
        int minMid = second <= m ? 0 : second - n / 2 - 1;
        int maxMid = second <= m ? second - first : m - first;
        int earliest = Integer.MAX_VALUE;
        int latest = 0;

        for (int left = 0; left < first; left++) { // 枚举 A 左侧保留 left 个人
            for (int mid = minMid; mid < maxMid; mid++) { // 枚举 AB 之间保留 mid 个人
                // 无需枚举 B 右侧保留多少个人,因为剩下的 m-2-left-mid 个人都在 B 右侧
                int[] res = dfs(m, left + 1, left + mid + 2, memo);
                earliest = Math.min(earliest, res[0]);
                latest = Math.max(latest, res[1]);
            }
        }

        // 加上当前回合
        mem[0] = earliest + 1;
        mem[1] = latest + 1;
        return mem;
    }
}
class Solution {
    private static final int MX = 29;

    // 用 static 可以让 memo 记录的值在不同测试数据间共享
    private static final int[][][][] memo = new int[MX][MX][MX][2];

    public int[] earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        return dfs(n, firstPlayer, secondPlayer, memo);
    }

    private int[] dfs(int n, int first, int second, int[][][][] memo) {
        // AB 相遇
        if (first + second == n + 1) {
            return new int[]{1, 1};
        }

        // 保证 A 左边人数比 B 右边人数少
        // 注:题目已保证 first < second
        if (first + second > n + 1) {
            int tmp = first;
            first = n + 1 - second;
            second = n + 1 - tmp;
        }
        
        int[] mem = memo[n][first][second];
        if (mem[0] > 0) { // 之前计算过
            return mem;
        }

        int m = (n + 1) / 2; // 下一回合人数
        // AB 之间保留 [minMid, maxMid) 个人
        int minMid = second <= m ? 0 : second - n / 2 - 1;
        int maxMid = second <= m ? second - first : m - first;
        int earliest = Integer.MAX_VALUE;
        int latest = 0;

        for (int left = 0; left < first; left++) { // 枚举 A 左侧保留 left 个人
            for (int mid = minMid; mid < maxMid; mid++) { // 枚举 AB 之间保留 mid 个人
                // 无需枚举 B 右侧保留多少个人,因为剩下的 m-2-left-mid 个人都在 B 右侧
                int[] res = dfs(m, left + 1, left + mid + 2, memo);
                earliest = Math.min(earliest, res[0]);
                latest = Math.max(latest, res[1]);
            }
        }

        // 加上当前回合
        mem[0] = earliest + 1;
        mem[1] = latest + 1;
        return mem;
    }
}
class Solution {
public:
    vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        vector memo(n + 1, vector(n + 1, vector<pair<int, int>>(n + 1)));

        auto dfs = [&](this auto&& dfs, int n, int first, int second) -> pair<int, int> {
            // AB 相遇
            if (first + second == n + 1) {
                return {1, 1};
            }

            // 保证 A 左边人数比 B 右边人数少
            // 注:题目已保证 first < second
            if (first + second > n + 1) {
                tie(first, second) = pair(n + 1 - second, n + 1 - first);
            }

            auto& res = memo[n][first][second]; // 注意这里是引用
            if (res.first) { // 之前计算过
                return res;
            }

            int m = (n + 1) / 2; // 下一回合人数
            // AB 之间保留 [min_mid, max_mid) 个人
            int min_mid = second <= m ? 0 : second - n / 2 - 1;
            int max_mid = second <= m ? second - first : m - first;
            int earliest = INT_MAX;
            int latest = 0;

            for (int left = 0; left < first; left++) { // 枚举 A 左侧保留 left 个人
                for (int mid = min_mid; mid < max_mid; mid++) { // 枚举 AB 之间保留 mid 个人
                    // 无需枚举 B 右侧保留多少个人,因为剩下的 m-2-left-mid 个人都在 B 右侧
                    auto [e, l] = dfs(m, left + 1, left + mid + 2);
                    earliest = min(earliest, e);
                    latest = max(latest, l);
                }
            }

            // 加上当前回合
            return res = {earliest + 1, latest + 1};
        };

        auto [earliest, latest] = dfs(n, firstPlayer, secondPlayer);
        return {earliest, latest};
    }
};
func earliestAndLatest(n, firstPlayer, secondPlayer int) []int {
type pair struct{ earliest, latest int }
memo := make([][][]pair, n+1)
for i := range memo {
memo[i] = make([][]pair, n+1)
for j := range memo[i] {
memo[i][j] = make([]pair, n+1)
}
}

var dfs func(int, int, int) pair
dfs = func(n, first, second int) (res pair) {
// AB 相遇
if first+second == n+1 {
return pair{1, 1}
}

// 保证 A 左边人数比 B 右边人数少
// 注:题目已保证 first < second
if first+second > n+1 {
first, second = n+1-second, n+1-first
}

p := &memo[n][first][second]
if p.earliest > 0 { // 之前计算过
return *p
}
defer func() { *p = res }() // 记忆化

m := (n + 1) / 2 // 下一回合人数
// AB 之间保留 [minMid, maxMid) 个人
minMid, maxMid := 0, second-first
if second > m { // B 在中轴线右侧
minMid, maxMid = second-n/2-1, m-first
}
res.earliest = math.MaxInt

for left := range first { // 枚举 A 左侧保留 left 个人
for mid := minMid; mid < maxMid; mid++ { // 枚举 AB 之间保留 mid 个人
// 无需枚举 B 右侧保留多少个人,因为剩下的 m-2-left-mid 个人都在 B 右侧
r := dfs(m, left+1, left+mid+2)
res.earliest = min(res.earliest, r.earliest)
res.latest = max(res.latest, r.latest)
}
}

// 加上当前回合
res.earliest++
res.latest++
return res
}

ans := dfs(n, firstPlayer, secondPlayer)
return []int{ans.earliest, ans.latest}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^4)$。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。由于 $\textit{first}$ 和 $\textit{second}$ 都不超过当前回合的人数 $n$,状态数不超过等比数列之和 $n^2 + \left(\dfrac{n}{2}\right)^2 + \left(\dfrac{n}{4}\right)^2 + \cdots = \mathcal{O}(n^2)$,单个状态的计算时间为 $\mathcal{O}(n^2)$,所以总的时间复杂度为 $\mathcal{O}(n^4)$。
  • 空间复杂度:$\mathcal{O}(n^3)$ 或 $\mathcal{O}(n^2)$。如果用哈希表记忆化(Python)则空间复杂度为 $\mathcal{O}(n^2)$。数组写法也可以把第一维度优化为 $\mathcal{O}(\log n)$,第二三维度的大小根据当前回合数计算,也可以做到 $\mathcal{O}(n^2)$ 的空间。

:可以把 $\textit{memo}$ 写到类外,使记忆化的内容能够在不同测试数据间共享。

方法二:分类讨论

方法二可以解决 $n\le 10^{18}$ 的数据范围。

首先特判两人一开始就相遇的情况,返回 $[1,1]$。

下文假定两人一开始没有相遇。

lc1900-最晚回合数.png{:width=700}

最早回合数的情况很多,建议阅读时在纸上画画。

lc1900-最早回合数.png

理论最大回合数

根据 下取整恒等式及其应用 的证明过程,我们有

$$
\left\lceil\dfrac{\lceil {n/2} \rceil}{2}\right\rceil = \left\lceil\dfrac{n}{4}\right\rceil
$$

设理论最大回合数为 $k$。从 $1$ 到 $k$,会发生 $k-1$ 次除以 $2$ 上取整,剩余人数为

$$
\left\lceil\dfrac{n}{2^{k-1}}\right\rceil = 2
$$

由于

$$
\dfrac{n}{2^{k-1}} \le \left\lceil\dfrac{n}{2^{k-1}}\right\rceil = 2
$$

所以有

$$
2^k \ge n
$$

$$
k \ge \log_2 n
$$

取整数 $k=\left\lceil\log_2 n \right\rceil$,即为理论最大回合数。

:编程计算时,$\left\lceil\log_2 n \right\rceil$ 等价于 $n-1$ 的二进制长度。(规定 $0$ 的二进制长度为 $0$)

优化前

class Solution:
    def earliestAndLatest(self, n: int, first: int, second: int) -> List[int]:
        # AB 一开始就相遇
        if first + second == n + 1:
            return [1, 1]

        # 保证 A 左边人数比 B 右边人数少
        # 注:题目已保证 first < second
        if first + second > n + 1:
            first, second = n + 1 - second, n + 1 - first

        def calc_earliest_rounds(n: int) -> int:
            res = 1  # 初始回合

            # 情况 5:AB 太靠左了
            if first + second <= (n + 1) // 2:
                while first + second <= (n + 1) // 2:
                    res += 1
                    n = (n + 1) // 2

                # 情况 5a:AB 不相邻
                # 在上面循环的最后一回合,总是可以把局面调整为某些情况,使 AB 下回合就能相遇
                if second - first > 1:
                    return res + 1

                # 情况 5b:AB 相邻
                # 上面循环结束后,转化为情况 1

            # 情况 1:AB 相邻(由于 AB 不相遇,B 不可能在中轴线右侧。注意我们已保证 A 左边人数比 B 右边人数少)
            if second - first == 1:
                # 先过一回合
                res += 1
                n = (n + 1) // 2
                # 在 AB 相邻的情况下,当且仅当 n 是偶数的时候相遇(推导过程见图)
                while n % 2 > 0:
                    res += 1
                    n = (n + 1) // 2
                return res

            # 情况 2:B 在中轴线或中轴线左侧
            if second <= (n + 1) // 2:
                # 可以让 AB 左右人数一样多(构造方式见图),下回合就能相遇
                return res + 1

            # 情况 3:AB 之间恰有一个人
            if second - first == 2:
                # 下回合 AB 必定相邻,变成情况 1
                res += 1
                n = (n + 1) // 2
                while n % 2 > 0:
                    res += 1
                    n = (n + 1) // 2
                return res

            # 情况 4c:A 左侧有奇数个人,且 B 与 A' 相邻
            if first % 2 == 0 and first + second == n:
                # 一回合后,转化为情况 4a
                res += 1

            # 情况 4a:A 左侧有偶数个人
            # 情况 4b:A 左侧有奇数个人,且 B 与 A' 不相邻
            # 下回合就能相遇
            return res + 1

        # 计算最早回合数
        earliest = calc_earliest_rounds(n)

        # 计算最晚回合数
        latest = min((n - 1).bit_length(), n + 1 - second)

        return [earliest, latest]
class Solution {
    public int[] earliestAndLatest(int n, int first, int second) {
        // AB 一开始就相遇
        if (first + second == n + 1) {
            return new int[]{1, 1};
        }

        // 保证 A 左边人数比 B 右边人数少
        // 注:题目已保证 first < second
        if (first + second > n + 1) {
            int tmp = first;
            first = n + 1 - second;
            second = n + 1 - tmp;
        }

        // 计算最早回合数
        int earliest = calcEarliestRounds(n, first, second);

        // 计算最晚回合数
        int latest = Math.min(32 - Integer.numberOfLeadingZeros(n - 1), n + 1 - second);

        return new int[]{earliest, latest};
    }

    private int calcEarliestRounds(int n, int first, int second) {
        int res = 1; // 初始回合

        // 情况 5:AB 太靠左了
        if (first + second <= (n + 1) / 2) {
            while (first + second <= (n + 1) / 2) {
                res++;
                n = (n + 1) / 2;
            }

            // 情况 5a:AB 不相邻
            // 在上面循环的最后一回合,总是可以把局面调整为某些情况,使 AB 下回合就能相遇
            if (second - first > 1) {
                return res + 1;
            }

            // 情况 5b:AB 相邻
            // 上面循环结束后,转化为情况 1
        }

        // 情况 1:AB 相邻(由于 AB 不相遇,B 不可能在中轴线右侧。注意我们已保证 A 左边人数比 B 右边人数少)
        if (second - first == 1) {
            // 先过一回合
            res++;
            n = (n + 1) / 2;
            // 在 AB 相邻的情况下,当且仅当 n 是偶数的时候相遇(推导过程见图)
            while (n % 2 > 0) {
                res++;
                n = (n + 1) / 2;
            }
            return res;
        }

        // 情况 2:B 在中轴线或中轴线左侧
        if (second <= (n + 1) / 2) {
            // 可以让 AB 左右人数一样多(构造方式见图),下回合就能相遇
            return res + 1;
        }

        // 情况 3:AB 之间恰有一个人
        if (second - first == 2) {
            // 下回合 AB 必定相邻,变成情况 1
            res++;
            n = (n + 1) / 2;
            while (n % 2 > 0) {
                res++;
                n = (n + 1) / 2;
            }
            return res;
        }

        // 情况 4c:A 左侧有奇数个人,且 B 与 A' 相邻
        if (first % 2 == 0 && first + second == n) {
            // 一回合后,转化为情况 4a
            res++;
        }

        // 情况 4a:A 左侧有偶数个人
        // 情况 4b:A 左侧有奇数个人,且 B 与 A' 不相邻
        // 下回合就能相遇
        return res + 1;
    }
}
class Solution {
public:
    vector<int> earliestAndLatest(int n, int first, int second) {
        // AB 一开始就相遇
        if (first + second == n + 1) {
            return {1, 1};
        }

        // 保证 A 左边人数比 B 右边人数少
        // 注:题目已保证 first < second
        if (first + second > n + 1) {
            tie(first, second) = pair(n + 1 - second, n + 1 - first);
        }

        auto calc_earliest_rounds = [&](int n) -> int {
            int res = 1; // 初始回合

            // 情况 5:AB 太靠左了
            if (first + second <= (n + 1) / 2) {
                while (first + second <= (n + 1) / 2) {
                    res++;
                    n = (n + 1) / 2;
                }

                // 情况 5a:AB 不相邻
                // 在上面循环的最后一回合,总是可以把局面调整为某些情况,使 AB 下回合就能相遇
                if (second - first > 1) {
                    return res + 1;
                }

                // 情况 5b:AB 相邻
                // 上面循环结束后,转化为情况 1
            }

            // 情况 1:AB 相邻(由于 AB 不相遇,B 不可能在中轴线右侧。注意我们已保证 A 左边人数比 B 右边人数少)
            if (second - first == 1) {
                // 先过一回合
                res++;
                n = (n + 1) / 2;
                // 在 AB 相邻的情况下,当且仅当 n 是偶数的时候相遇(推导过程见图)
                while (n % 2) {
                    res++;
                    n = (n + 1) / 2;
                }
                return res;
            }

            // 情况 2:B 在中轴线或中轴线左侧
            if (second <= (n + 1) / 2) {
                // 可以让 AB 左右人数一样多(构造方式见图),下回合就能相遇
                return res + 1;
            }

            // 情况 3:AB 之间恰有一个人
            if (second - first == 2) {
                // 下回合 AB 必定相邻,变成情况 1
                res++;
                n = (n + 1) / 2;
                while (n % 2) {
                    res++;
                    n = (n + 1) / 2;
                }
                return res;
            }

            // 情况 4c:A 左侧有奇数个人,且 B 与 A' 相邻
            if (first % 2 == 0 && first + second == n) {
                // 一回合后,转化为情况 4a
                res++;
            }

            // 情况 4a:A 左侧有偶数个人
            // 情况 4b:A 左侧有奇数个人,且 B 与 A' 不相邻
            // 下回合就能相遇
            return res + 1;
        };

        // 计算最早回合数
        int earliest = calc_earliest_rounds(n);

        // 计算最晚回合数
        int latest = min(bit_width(n - 1u), n + 1 - second);

        return {earliest, latest};
    }
};
func earliestAndLatest(n, first, second int) []int {
// AB 一开始就相遇
if first+second == n+1 {
return []int{1, 1}
}

// 保证 A 左边人数比 B 右边人数少
// 注:题目已保证 first < second
if first+second > n+1 {
first, second = n+1-second, n+1-first
}

calcEarliestRounds := func(n int) int {
res := 1 // 初始回合

// 情况 5:AB 太靠左了
if first+second <= (n+1)/2 {
for first+second <= (n+1)/2 {
res++
n = (n + 1) / 2
}

// 情况 5a:AB 不相邻
// 在上面循环的最后一回合,总是可以把局面调整为某些情况,使 AB 下回合就能相遇
if second-first > 1 {
return res + 1
}

// 情况 5b:AB 相邻
// 上面循环结束后,转化为情况 1
}

// 情况 1:AB 相邻(由于 AB 不相遇,B 不可能在中轴线右侧。注意我们已保证 A 左边人数比 B 右边人数少)
if second-first == 1 {
// 先过一回合
res++
n = (n + 1) / 2
// 在 AB 相邻的情况下,当且仅当 n 是偶数的时候相遇(推导过程见图)
for n%2 > 0 {
res++
n = (n + 1) / 2
}
return res
}

// 情况 2:B 在中轴线或中轴线左侧
if second <= (n+1)/2 {
// 可以让 AB 左右人数一样多(构造方式见图),下回合就能相遇
return res + 1
}

// 情况 3:AB 之间恰有一个人
if second-first == 2 {
// 下回合 AB 必定相邻,变成情况 1
res++
n = (n + 1) / 2
for n%2 > 0 {
res++
n = (n + 1) / 2
}
return res
}

// 情况 4c:A 左侧有奇数个人,且 B 与 A' 相邻
if first%2 == 0 && first+second == n {
// 一回合后,转化为情况 4a
res++
}

// 情况 4a:A 左侧有偶数个人
// 情况 4b:A 左侧有奇数个人,且 B 与 A' 不相邻
// 下回合就能相遇
return res + 1
}

// 计算最早回合数
earliest := calcEarliestRounds(n)

// 计算最晚回合数
latest := min(bits.Len(uint(n-1)), n+1-second)

return []int{earliest, latest}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\log n)$。
  • 空间复杂度:$\mathcal{O}(1)$。

位运算优化

需要把情况 5 和情况 1 的循环优化成 $\mathcal{O}(1)$。(情况 3 合并到情况 1 中)

情况 5 的循环

设 $m = \textit{first} + \textit{second}$,我们需要计算最小的 $k$,满足

$$
m > \left\lceil\dfrac{n}{2^{k+1}}\right\rceil
$$

$$
m-1 \ge \left\lceil\dfrac{n}{2^{k+1}}\right\rceil \ge \dfrac{n}{2^{k+1}}
$$

$$
2^{k+1} \ge \dfrac{n}{m-1}
$$

由于 $2^{k+1}$ 是整数,上式等价于

$$
2^{k+1} \ge \left\lceil\dfrac{n}{m-1}\right\rceil = \left\lfloor\dfrac{n-1}{m-1}\right\rfloor + 1
$$

右边等号见 上取整下取整转换公式的证明

解得

$$
k\ge \log_2 \left(\left\lfloor\dfrac{n-1}{m-1}\right\rfloor + 1\right) - 1
$$

结论:$k$ 最小为 $\left\lfloor\dfrac{n-1}{m-1}\right\rfloor$ 的二进制长度减一。

我们还需要计算 $\left\lceil\dfrac{n}{2^k}\right\rceil$ 的值,由上取整下取整转换公式可得

$$
\left\lceil\dfrac{n}{2^k}\right\rceil = \left\lfloor\dfrac{n-1}{2^k}\right\rfloor + 1
$$

情况 1 的循环

不断地把 $n$ 变成 $\left\lceil\dfrac{n}{2}\right\rceil$,多少次循环后 $n$ 是偶数?

分类讨论:

  • 如果 $n$ 是偶数,循环 $0$ 次。
  • 如果 $n=4k+3$,即二进制末尾是 $11$,那么一轮循环后 $n=2k+2$,是偶数,所以只需循环 $1$ 次。
  • 如果 $n=4k+1$,即二进制末尾是 $01$,那么一轮循环后 $n=2k+1$,还是奇数,需要继续循环,直到二进制末尾是 $11$。比如二进制 $1001$,流程是 $1001\to 101\to 11\to 10$,循环 $3$ 次,相当于找倒数第二个比特 $1$ 的位置。这等于 $n-1$ 二进制尾零的个数。这一结论也适用于 $n$ 是偶数以及 $n=4k+3$ 的情况。

结论:循环次数为 $n-1$ 二进制尾零的个数。注意本题 $n\ge 2$。

注:Python 没有直接计算二进制尾零个数的库函数,可以计算 lowbit 的二进制长度,减一即为二进制尾零个数。什么是 lowbit?见【从集合论到位运算,常见位运算技巧分类总结】。

class Solution:
    def earliestAndLatest(self, n: int, first: int, second: int) -> List[int]:
        if first + second == n + 1:
            return [1, 1]

        if first + second > n + 1:
            first, second = n + 1 - second, n + 1 - first

        def calc_earliest_rounds(n: int) -> int:
            res = 1

            if first + second <= (n + 1) // 2:
                # 计算满足 first+second > ceil(n / 2^(k+1)) 的最小 k,推导过程见题解
                k = ((n - 1) // (first + second - 1)).bit_length() - 1
                n = ((n - 1) >> k) + 1  # n = ceil(n / 2^k)
                res += k

                if second - first > 1:
                    return res + 1

            # 情况 1 和情况 3 合并,情况 2 合并到最后的 return
            if second - first == 1 or second > (n + 1) // 2 and second - first == 2:
                # 先把 n 变成 ceil(n/2),然后计算需要多少次 ceil(n/2) 的操作才能把 n 变成偶数,推导过程见题解
                # 这里把 (n+1)/2 和 n-1 合并,得到 (n+1)/2-1 = (n-1)/2
                n = (n - 1) // 2
                # n 的二进制尾零个数等价于 lowbit(n) 的二进制长度减一,由于还要加一回合,加一减一抵消了
                return res + (n & -n).bit_length()

            if second > (n + 1) // 2 and first % 2 == 0 and first + second == n:
                res += 1

            return res + 1

        earliest = calc_earliest_rounds(n)
        latest = min((n - 1).bit_length(), n + 1 - second)
        return [earliest, latest]
class Solution {
    public int[] earliestAndLatest(int n, int first, int second) {
        if (first + second == n + 1) {
            return new int[]{1, 1};
        }

        if (first + second > n + 1) {
            int tmp = first;
            first = n + 1 - second;
            second = n + 1 - tmp;
        }

        int earliest = calcEarliestRounds(n, first, second);
        int latest = Math.min(32 - Integer.numberOfLeadingZeros(n - 1), n + 1 - second);
        return new int[]{earliest, latest};
    }

    private int calcEarliestRounds(int n, int first, int second) {
        int res = 1;

        if (first + second <= (n + 1) / 2) {
            // 计算满足 first+second > ceil(n / 2^(k+1)) 的最小 k,推导过程见题解
            int k = 32 - Integer.numberOfLeadingZeros((n - 1) / (first + second - 1)) - 1;
            n = ((n - 1) >> k) + 1; // n = ceil(n / 2^k)
            res += k;

            if (second - first > 1) {
                return res + 1;
            }
        }

        // 情况 1 和情况 3 合并,情况 2 合并到最后的 return
        if (second - first == 1 || second > (n + 1) / 2 && second - first == 2) {
            // 先把 n 变成 ceil(n/2),然后计算需要多少次 ceil(n/2) 的操作才能把 n 变成偶数,推导过程见题解
            // 这里把 (n+1)/2 和 n-1 合并,得到 (n+1)/2-1 = (n-1)/2
            return res + 1 + Integer.numberOfTrailingZeros((n - 1) / 2);
        }

        if (second > (n + 1) / 2 && first % 2 == 0 && first + second == n) {
            res++;
        }

        return res + 1;
    }
}
class Solution {
public:
    vector<int> earliestAndLatest(int n, int first, int second) {
        if (first + second == n + 1) {
            return {1, 1};
        }

        if (first + second > n + 1) {
            tie(first, second) = pair(n + 1 - second, n + 1 - first);
        }

        auto calc_earliest_rounds = [&](int n) -> int {
            int res = 1;

            if (first + second <= (n + 1) / 2) {
                // 计算满足 first+second > ceil(n / 2^(k+1)) 的最小 k,推导过程见题解
                int k = bit_width((n - 1u) / (first + second - 1)) - 1;
                n = ((n - 1) >> k) + 1; // n = ceil(n / 2^k)
                res += k;

                if (second - first > 1) {
                    return res + 1;
                }
            }

            // 情况 1 和情况 3 合并,情况 2 合并到最后的 return
            if (second - first == 1 || second > (n + 1) / 2 && second - first == 2) {
                // 先把 n 变成 ceil(n/2),然后计算需要多少次 ceil(n/2) 的操作才能把 n 变成偶数,推导过程见题解
                // 这里把 (n+1)/2 和 n-1 合并,得到 (n+1)/2-1 = (n-1)/2
                return res + 1 + countr_zero((n - 1u) / 2);
            }

            if (second > (n + 1) / 2 && first % 2 == 0 && first + second == n) {
                res++;
            }

            return res + 1;
        };

        int earliest = calc_earliest_rounds(n);
        int latest = min(bit_width(n - 1u), n + 1 - second);
        return {earliest, latest};
    }
};
func earliestAndLatest(n, first, second int) []int {
if first+second == n+1 {
return []int{1, 1}
}

if first+second > n+1 {
first, second = n+1-second, n+1-first
}

calcEarliestRounds := func(n int) int {
res := 1

if first+second <= (n+1)/2 {
// 计算满足 first+second > ceil(n / 2^(k+1)) 的最小 k,推导过程见题解
k := bits.Len(uint((n-1)/(first+second-1))) - 1
n = (n-1)>>k + 1 // n = ceil(n / 2^k)
res += k

if second-first > 1 {
return res + 1
}
}

// 情况 1 和情况 3 合并,情况 2 合并到最后的 return
if second-first == 1 || second > (n+1)/2 && second-first == 2 {
// 先把 n 变成 ceil(n/2),然后计算需要多少次 ceil(n/2) 的操作才能把 n 变成偶数,推导过程见题解
// 这里把 (n+1)/2 和 n-1 合并,得到 (n+1)/2-1 = (n-1)/2
return res + 1 + bits.TrailingZeros(uint((n-1)/2))
}

if second > (n+1)/2 && first%2 == 0 && first+second == n {
res++
}

return res + 1
}

earliest := calcEarliestRounds(n)
latest := min(bits.Len(uint(n-1)), n+1-second)
return []int{earliest, latest}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(1)$。
  • 空间复杂度:$\mathcal{O}(1)$。

相似题目

1040. 移动石子直到连续 II,这题也有我的 详细图解

另见动态规划题单的「§7.6 多维 DP」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

无需开会的工作日

方法一:区间合并

思路与算法

我们可以将每个会议看作一个闭区间,合并所有区间后将总天数减去每一段合并后的区间就是答案。关于合并区间的方法,可以参考「56. 合并区间」的题解。

在实现时,我们可以在合并区间的同时减去正在被合并的区间的长度。

代码

###C++

class Solution {
public:
    int countDays(int days, vector<vector<int>>& meetings) {
        sort(meetings.begin(), meetings.end());
        int l = 1, r = 0;
        for (auto m : meetings) {
            if (m[0] > r) {
                days -= (r - l + 1);
                l = m[0];
            }
            r = max(r, m[1]);
        }
        days -= (r - l + 1);
        return days;
    }
};

###Java

class Solution {
    public int countDays(int days, int[][] meetings) {
        Arrays.sort(meetings, Comparator.comparingInt(a -> a[0]));
        int l = 1, r = 0;
        for (int[] m : meetings) {
            if (m[0] > r) {
                days -= (r - l + 1);
                l = m[0];
            }
            r = Math.max(r, m[1]);
        }
        days -= (r - l + 1);
        return days;
    }
}

###Python

class Solution:
    def countDays(self, days: int, meetings: List[List[int]]) -> int:
        meetings.sort()
        l, r = 1, 0
        for m in meetings:
            if m[0] > r:
                days -= r - l + 1
                l = m[0]
            r = max(r, m[1])
        days -= r - l + 1
        return days

###C#

public class Solution {
    public int CountDays(int days, int[][] meetings) {
        Array.Sort(meetings, (a, b) => a[0].CompareTo(b[0]));

        int l = 1, r = 0;
        foreach (var m in meetings) {
            if (m[0] > r) {
                days -= (r - l + 1);
                l = m[0];
            }
            r = Math.Max(r, m[1]);
        }
        days -= (r - l + 1);
        return days;
    }
}

###Go

func countDays(days int, meetings [][]int) int {
    sort.Slice(meetings, func(i, j int) bool {
        return meetings[i][0] < meetings[j][0]
    })
    l, r := 1, 0
    for _, m := range meetings {
        if m[0] > r {
            days -= r - l + 1
            l = m[0]
        }
        if m[1] > r {
            r = m[1]
        }
    }
    days -= r - l + 1
    return days
}

###C

int compare(const void* a, const void* b) {
    return (*(int**)a)[0] - (*(int**)b)[0];
}

int countDays(int days, int** meetings, int meetingsSize,
              int* meetingsColSize) {

    qsort(meetings, meetingsSize, sizeof(int*), compare);
    int l = 1, r = 0;
    for (int i = 0; i < meetingsSize; ++i) {
        int start = meetings[i][0], end = meetings[i][1];
        if (start > r) {
            days -= (r - l + 1);
            l = start;
        }
        if (end > r) {
            r = end;
        }
    }
    days -= (r - l + 1);
    return days;
}

###JavaScript

var countDays = function(days, meetings) {
    meetings.sort((a, b) => a[0] - b[0]);
    let l = 1, r = 0;
    for (const [start, end] of meetings) {
        if (start > r) {
            days -= (r - l + 1);
            l = start;
        }
        r = Math.max(r, end);
    }
    days -= (r - l + 1);
    return days;
};

###TypeScript

function countDays(days: number, meetings: number[][]): number {
    meetings.sort((a, b) => a[0] - b[0]);
    let l = 1, r = 0;
    for (const [start, end] of meetings) {
        if (start > r) {
            days -= (r - l + 1);
            l = start;
        }
        r = Math.max(r, end);
    }
    days -= (r - l + 1);
    return days;
};

###Rust

impl Solution {
    pub fn count_days(mut days: i32, mut meetings: Vec<Vec<i32>>) -> i32 {
        meetings.sort_by_key(|m| m[0]);
        let (mut l, mut r) = (1, 0);
        for m in meetings {
            let (start, end) = (m[0], m[1]);
            if start > r {
                days -= r - l + 1;
                l = start;
            }
            r = r.max(end);
        }
        days -= r - l + 1;
        days
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是 $\textit{meetings}$ 数组的长度。对数组排序需要 $O(n \log n)$ 时间。
  • 空间复杂度:$O(S_n)$,即为对 $\textit{meetings}$ 排序所需的空间,其值取决于语言的具体实现,在 Java 和 C++ 中是 $O(\log n)$,在 Python 中是 $O(n)$。

[Python3/Java/C++/Go/TypeScript] 一题一解:排序(清晰题解)

方法一:排序

我们不妨将所有会议按照开始时间排序,用一个变量 $\textit{last}$ 记录此前会议的最晚结束时间。

然后我们遍历所有会议,对于每一个会议 $(\textit{st}, \textit{ed})$,如果 $\textit{last} < \textit{st}$,说明 $\textit{last}$ 到 $\textit{st}$ 之间的时间段是员工可以工作且没有安排会议的时间,我们将这段时间加入答案。然后我们更新 $\textit{last} = \max(\textit{last}, \textit{ed})$。

最后,如果 $\textit{last} < \textit{days}$,说明最后一次会议结束后还有时间段是员工可以工作且没有安排会议的时间,我们将这段时间加入答案。

###python

class Solution:
    def countDays(self, days: int, meetings: List[List[int]]) -> int:
        meetings.sort()
        ans = last = 0
        for st, ed in meetings:
            if last < st:
                ans += st - last - 1
            last = max(last, ed)
        ans += days - last
        return ans

###java

class Solution {
    public int countDays(int days, int[][] meetings) {
        Arrays.sort(meetings, (a, b) -> a[0] - b[0]);
        int ans = 0, last = 0;
        for (var e : meetings) {
            int st = e[0], ed = e[1];
            if (last < st) {
                ans += st - last - 1;
            }
            last = Math.max(last, ed);
        }
        ans += days - last;
        return ans;
    }
}

###cpp

class Solution {
public:
    int countDays(int days, vector<vector<int>>& meetings) {
        sort(meetings.begin(), meetings.end());
        int ans = 0, last = 0;
        for (auto& e : meetings) {
            int st = e[0], ed = e[1];
            if (last < st) {
                ans += st - last - 1;
            }
            last = max(last, ed);
        }
        ans += days - last;
        return ans;
    }
};

###go

func countDays(days int, meetings [][]int) (ans int) {
sort.Slice(meetings, func(i, j int) bool { return meetings[i][0] < meetings[j][0] })
last := 0
for _, e := range meetings {
st, ed := e[0], e[1]
if last < st {
ans += st - last - 1
}
last = max(last, ed)
}
ans += days - last
return
}

###ts

function countDays(days: number, meetings: number[][]): number {
    meetings.sort((a, b) => a[0] - b[0]);
    let [ans, last] = [0, 0];
    for (const [st, ed] of meetings) {
        if (last < st) {
            ans += st - last - 1;
        }
        last = Math.max(last, ed);
    }
    ans += days - last;
    return ans;
}

###rust

impl Solution {
    pub fn count_days(days: i32, mut meetings: Vec<Vec<i32>>) -> i32 {
        meetings.sort_by_key(|m| m[0]);
        let mut ans = 0;
        let mut last = 0;

        for e in meetings {
            let st = e[0];
            let ed = e[1];
            if last < st {
                ans += st - last - 1;
            }
            last = last.max(ed);
        }

        ans + (days - last)
    }
}

时间复杂度 $O(n \times \log n)$,空间复杂度 $O(\log n)$。其中 $n$ 为会议的数量。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-无需开会的工作日🟡

给你一个正整数 days,表示员工可工作的总天数(从第 1 天开始)。另给你一个二维数组 meetings,长度为 n,其中 meetings[i] = [start_i, end_i] 表示第 i 次会议的开始和结束天数(包含首尾)。

返回员工可工作且没有安排会议的天数。

注意:会议时间可能会有重叠。

 

示例 1:

输入:days = 10, meetings = [[5,7],[1,3],[9,10]]

输出:2

解释:

第 4 天和第 8 天没有安排会议。

示例 2:

输入:days = 5, meetings = [[2,4],[1,3]]

输出:1

解释:

第 5 天没有安排会议。

示例 3:

输入:days = 6, meetings = [[1,6]]

输出:0

解释:

所有工作日都安排了会议。

 

提示:

  • 1 <= days <= 109
  • 1 <= meetings.length <= 105
  • meetings[i].length == 2
  • 1 <= meetings[i][0] <= meetings[i][1] <= days

3169. 无需开会的工作日

解法

思路和算法

首先将数组 $\textit{meetings}$ 按照会议的开始日期升序排序,然后遍历排序后的数组 $\textit{meetings}$ 并计算没有会议的天数。

用 $\textit{prevMeeting}$ 表示已经遍历到的有会议的最大日期。由于员工可工作的总天数从 $1$ 开始计数,因此初始时 $\textit{prevMeeting} = 0$。遍历排序后的数组 $\textit{meetings}$,对于遍历到的每个会议 $\textit{meeting}$,执行如下操作。

  1. 所有的开始日期小于 $\textit{meeting}[0]$ 的会议已经遍历过且其中的有会议的最大日期是 $\textit{prevMeeting}$,因此区间 $[\textit{prevMeeting} + 1, \textit{meeting}[0] - 1]$ 中的所有日期没有会议,该区间的天数是 $\max(\textit{meeting}[0] - \textit{prevMeeting} - 1, 0)$,将该区间的天数加到没有会议的天数。

  2. 使用当前会议的结束日期 $\textit{meeting}[1]$ 更新 $\textit{prevMeeting}$。

遍历结束之后,区间 $[\textit{prevMeeting} + 1, \textit{days}]$ 中的所有日期没有会议,该区间的天数是 $\textit{days} - \textit{prevMeeting}$,将该区间的天数加到没有会议的天数。此时即可得到没有会议的天数。

由于每个会议前面的相邻没有会议的天数都恰好计算一次,且有会议的最大日期后面的没有会议的天数恰好计算一次,因此上述做法可以得到正确的结果。

代码

###Java

class Solution {
    public int countDays(int days, int[][] meetings) {
        Arrays.sort(meetings, (a, b) -> a[0] - b[0]);
        int available = 0, prevMeeting = 0;
        for (int[] meeting : meetings) {
            available += Math.max(meeting[0] - prevMeeting - 1, 0);
            prevMeeting = Math.max(prevMeeting, meeting[1]);
        }
        available += days - prevMeeting;
        return available;
    }
}

###C#

public class Solution {
    public int CountDays(int days, int[][] meetings) {
        Array.Sort(meetings, (a, b) => a[0] - b[0]);
        int available = 0, prevMeeting = 0;
        foreach (int[] meeting in meetings) {
            available += Math.Max(meeting[0] - prevMeeting - 1, 0);
            prevMeeting = Math.Max(prevMeeting, meeting[1]);
        }
        available += days - prevMeeting;
        return available;
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$,其中 $n$ 是数组 $\textit{meetings}$ 的长度。排序的时间是 $O(n \log n)$,排序之后需要遍历数组一次,因此时间复杂度是 $O(n \log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{meetings}$ 的长度。由于待排序的元素是数组,因此排序的空间是 $O(n)$。

排序

解法:排序

区间并模板题。答案就是 days 减去区间并的长度之和。区间并详见 leetcode 56. 合并区间

复杂度 $\mathcal{O}(n\log n)$,主要是给区间排序的复杂度。

参考代码(c++)

###cpp

class Solution {
public:
    int countDays(int days, vector<vector<int>>& meetings) {
        sort(meetings.begin(), meetings.end());
        int ans = 0;
        int R = 0;
        for (auto &meet : meetings) {
            if (meet[0] > R) ans += meet[0] - R - 1;
            R = max(R, meet[1]);
        }
        ans += days - R;
        return ans;
    }
};

正难则反+合并区间(Python/Java/C++/Go)

正难则反,答案等于 $\textit{days}$ 减「有会议安排的天数」。

要计算有会议安排的天数,做法同 56. 合并区间的题解。累加每个区间的长度,即为有会议安排的天数。

由于本题只需计算合并区间的长度,所以只需记录当前合并区间的左右端点。

###py

class Solution:
    def countDays(self, days: int, meetings: List[List[int]]) -> int:
        meetings.sort(key=lambda p: p[0])  # 按照左端点从小到大排序
        start, end = 1, 0  # 当前合并区间的左右端点
        for s, e in meetings:
            if s > end:  # 不相交
                days -= end - start + 1  # 当前合并区间的长度
                start = s  # 下一个合并区间的左端点
            end = max(end, e)
        days -= end - start + 1  # 最后一个合并区间的长度
        return days

###java

class Solution {
    public int countDays(int days, int[][] meetings) {
        Arrays.sort(meetings, (p, q) -> p[0] - q[0]); // 按照左端点从小到大排序
        int start = 1, end = 0; // 当前合并区间的左右端点
        for (int[] p : meetings) {
            if (p[0] > end) { // 不相交
                days -= end - start + 1; // 当前合并区间的长度
                start = p[0]; // 下一个合并区间的左端点
            }
            end = Math.max(end, p[1]);
        }
        days -= end - start + 1; // 最后一个合并区间的长度
        return days;
    }
}

###cpp

class Solution {
public:
    int countDays(int days, vector<vector<int>>& meetings) {
        ranges::sort(meetings); // 按照左端点从小到大排序
        int start = 1, end = 0; // 当前合并区间的左右端点
        for (auto& p : meetings) {
            if (p[0] > end) { // 不相交
                days -= end - start + 1; // 当前合并区间的长度
                start = p[0]; // 下一个合并区间的左端点
            }
            end = max(end, p[1]);
        }
        days -= end - start + 1; // 最后一个合并区间的长度
        return days;
    }
};

###go

func countDays(days int, meetings [][]int) int {
slices.SortFunc(meetings, func(p, q []int) int { return p[0] - q[0] }) // 按照左端点从小到大排序
start, end := 1, 0 // 当前合并区间的左右端点
for _, p := range meetings {
if p[0] > end { // 不相交
days -= end - start + 1 // 当前合并区间的长度
start = p[0] // 下一个合并区间的左端点
}
end = max(end, p[1])
}
days -= end - start + 1 // 最后一个合并区间的长度
return days
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{meetings}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。忽略排序的栈开销。

相似题目

见下面贪心题单的「§2.5 合并区间」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-重新安排会议得到最多空余时间 II🟡

给你一个整数 eventTime 表示一个活动的总时长,这个活动开始于 t = 0 ,结束于 t = eventTime 。

同时给你两个长度为 n 的整数数组 startTime 和 endTime 。它们表示这次活动中 n 个时间 没有重叠 的会议,其中第 i 个会议的时间为 [startTime[i], endTime[i]] 。

你可以重新安排 至多 一个会议,安排的规则是将会议时间平移,且保持原来的 会议时长 ,你的目的是移动会议后 最大化 相邻两个会议之间的 最长 连续空余时间。

请你返回重新安排会议以后,可以得到的 最大 空余时间。

注意,会议 不能 安排到整个活动的时间以外,且会议之间需要保持互不重叠。

注意:重新安排会议以后,会议之间的顺序可以发生改变。

 

示例 1:

输入:eventTime = 5, startTime = [1,3], endTime = [2,5]

输出:2

解释:

将 [1, 2] 的会议安排到 [2, 3] ,得到空余时间 [0, 2] 。

示例 2:

输入:eventTime = 10, startTime = [0,7,9], endTime = [1,8,10]

输出:7

解释:

将 [0, 1] 的会议安排到 [8, 9] ,得到空余时间 [0, 7] 。

示例 3:

输入:eventTime = 10, startTime = [0,3,7,9], endTime = [1,4,8,10]

输出:6

解释:

将 [3, 4] 的会议安排到 [8, 9] ,得到空余时间 [1, 7] 。

示例 4:

输入:eventTime = 5, startTime = [0,1,2,3,4], endTime = [1,2,3,4,5]

输出:0

解释:

活动中的所有时间都被会议安排满了。

 

提示:

  • 1 <= eventTime <= 109
  • n == startTime.length == endTime.length
  • 2 <= n <= 105
  • 0 <= startTime[i] < endTime[i] <= eventTime
  • endTime[i] <= startTime[i + 1] 其中 i 在范围 [0, n - 2] 之间。

重新安排会议得到最多空余时间 II

方法一:贪心

假设当前平移的会议为 $i$,那么平移的最优解有两种情况:

  1. 如果会议 $i$ 可以移动到某个空余时间段,且该空余时间段不是会议 $i$ 左右两侧相邻的两个空余时间段,那么平移会议 $i$ 可以得到一个新的空余时间段,时长为会议 $i$ 和其左右两侧相邻空余时间段的时长之和

  2. 否则,平移会议 $i$ 可以将其左右两侧相邻空间时间段进行合并

显然,重新安排会议后得到的最大空余时间必定为平移某个会议后,得到的新的空余时间段的最大值。

我们使用 $q[i]$ 记录会议 $i$ 是否适用于第一种情况,首先从左到右遍历会议,同时记录当前遍历到的会议 $i$ 左侧非相邻的空余时间段的最大时长 $t_1$,如果 $t_1 \ge \textit{endTime}[i] - \textit{startTime}[i]$,那么说明会议 $i$ 左侧有满足第一种情况的空余时间段,记 $q[i] = \text{true}$。同样地,我们从右到左遍历会议,记下会议 $i$ 右侧是否有满足第一种情况的空余时间段。

我们枚举平移的会议 $i$,同时令 $\textit{left}_i$ 表示左侧相邻会议的结束时间,$\textit{right}_i$ 表示右侧相邻会议的开始时间,然后根据 $q[i]$ 的值判断平移会议 $i$ 属于哪个情况:

  • 情况一:新的空余时间段的时长为 $\textit{right}_i - \textit{left}_i$

  • 情况二:新的空余时间段的时长为 $\textit{right}_i - \textit{left}_i - (\textit{endTime}[i] - \textit{startTime}[i])$

返回所有枚举结果的最大值。

###C++

class Solution {
public:
    int maxFreeTime(int eventTime, vector<int>& startTime, vector<int>& endTime) {
        int n = startTime.size();
        vector<bool> q(n);
        for (int i = 0, t1 = 0, t2 = 0; i < n; i++) {
            if (endTime[i] - startTime[i] <= t1) {
                q[i] = true;
            }
            t1 = max(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

            if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
                q[n - i - 1] = true;
            }
            t2 = max(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
        }

        int res = 0;
        for (int i = 0; i < n; i++) {
            int left = i == 0 ? 0 : endTime[i - 1];
            int right = i == n - 1 ? eventTime : startTime[i + 1];
            if (q[i]) {
                res = max(res, right - left);
            } else {
                res = max(res, right - left - (endTime[i] - startTime[i]));
            }
        }
        return res;
    }
};

###Go

func maxFreeTime(eventTime int, startTime []int, endTime []int) int {
n := len(startTime)
q := make([]bool, n)
t1, t2 := 0, 0
for i := 0; i < n; i++ {
if endTime[i] - startTime[i] <= t1 {
q[i] = true
}
if i == 0 {
t1 = max(t1, startTime[i])
} else {
t1 = max(t1, startTime[i] - endTime[i - 1])
}

if endTime[n - i - 1] - startTime[n - i - 1] <= t2 {
q[n - i - 1] = true
}
if i == 0 {
t2 = max(t2, eventTime - endTime[n - 1])
} else {
t2 = max(t2, startTime[n - i] - endTime[n - i - 1])
}
}

res := 0
for i := 0; i < n; i++ {
left := 0
if i != 0 {
left = endTime[i - 1]
}
right := eventTime
if i != n - 1 {
right = startTime[i + 1]
}
if q[i] {
res = max(res, right - left)
} else {
res = max(res, right - left - (endTime[i] - startTime[i]))
}
}
return res
}

###Python

class Solution:
    def maxFreeTime(self, eventTime: int, startTime: list[int], endTime: list[int]) -> int:
        n = len(startTime)
        q = [False] * n
        t1 = 0
        t2 = 0
        for i in range(n):
            if endTime[i] - startTime[i] <= t1:
                q[i] = True
            t1 = max(t1, startTime[i] - (0 if i == 0 else endTime[i - 1]))

            if endTime[n - i - 1] - startTime[n - i - 1] <= t2:
                q[n - i - 1] = True
            t2 = max(t2, (eventTime if i == 0 else startTime[n - i]) - endTime[n - i - 1])

        res = 0
        for i in range(n):
            left = 0 if i == 0 else endTime[i - 1]
            right = eventTime if i == n - 1 else startTime[i + 1]
            if q[i]:
                res = max(res, right - left)
            else:
                res = max(res, right - left - (endTime[i] - startTime[i]))
        return res

###Java

class Solution {
    public int maxFreeTime(int eventTime, int[] startTime, int[] endTime) {
        int n = startTime.length;
        boolean[] q = new boolean[n];
        for (int i = 0, t1 = 0, t2 = 0; i < n; i++) {
            if (endTime[i] - startTime[i] <= t1) {
                q[i] = true;
            }
            t1 = Math.max(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

            if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
                q[n - i - 1] = true;
            }
            t2 = Math.max(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
        }

        int res = 0;
        for (int i = 0; i < n; i++) {
            int left = i == 0 ? 0 : endTime[i - 1];
            int right = i == n - 1 ? eventTime : startTime[i + 1];
            if (q[i]) {
                res = Math.max(res, right - left);
            } else {
                res = Math.max(res, right - left - (endTime[i] - startTime[i]));
            }
        }
        return res;
    }
}

###TypeScript

function maxFreeTime(eventTime: number, startTime: number[], endTime: number[]): number {
    const n = startTime.length;
    const q: boolean[] = new Array(n).fill(false);
    let t1 = 0, t2 = 0;
    for (let i = 0; i < n; i++) {
        if (endTime[i] - startTime[i] <= t1) {
            q[i] = true;
        }
        t1 = Math.max(t1, startTime[i] - (i === 0 ? 0 : endTime[i - 1]));

        if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
            q[n - i - 1] = true;
        }
        t2 = Math.max(t2, (i === 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
    }

    let res = 0;
    for (let i = 0; i < n; i++) {
        const left = i === 0 ? 0 : endTime[i - 1];
        const right = i === n - 1 ? eventTime : startTime[i + 1];
        if (q[i]) {
            res = Math.max(res, right - left);
        } else {
            res = Math.max(res, right - left - (endTime[i] - startTime[i]));
        }
    }
    return res;
}

###JavaScript

var maxFreeTime = function(eventTime, startTime, endTime) {
    const n = startTime.length;
    const q = new Array(n).fill(false);
    let t1 = 0, t2 = 0;
    for (let i = 0; i < n; i++) {
        if (endTime[i] - startTime[i] <= t1) {
            q[i] = true;
        }
        t1 = Math.max(t1, startTime[i] - (i === 0 ? 0 : endTime[i - 1]));

        if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
            q[n - i - 1] = true;
        }
        t2 = Math.max(t2, (i === 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
    }

    let res = 0;
    for (let i = 0; i < n; i++) {
        const left = i === 0 ? 0 : endTime[i - 1];
        const right = i === n - 1 ? eventTime : startTime[i + 1];
        if (q[i]) {
            res = Math.max(res, right - left);
        } else {
            res = Math.max(res, right - left - (endTime[i] - startTime[i]));
        }
    }
    return res;
};

###C#

public class Solution {
    public int MaxFreeTime(int eventTime, int[] startTime, int[] endTime) {
        int n = startTime.Length;
        bool[] q = new bool[n];
        int t1 = 0, t2 = 0;
        for (int i = 0; i < n; i++) {
            if (endTime[i] - startTime[i] <= t1) {
                q[i] = true;
            }
            t1 = Math.Max(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

            if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
                q[n - i - 1] = true;
            }
            t2 = Math.Max(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
        }

        int res = 0;
        for (int i = 0; i < n; i++) {
            int left = i == 0 ? 0 : endTime[i - 1];
            int right = i == n - 1 ? eventTime : startTime[i + 1];
            if (q[i]) {
                res = Math.Max(res, right - left);
            } else {
                res = Math.Max(res, right - left - (endTime[i] - startTime[i]));
            }
        }
        return res;
    }
}

###C

int maxFreeTime(int eventTime, int* startTime, int startTimeSize, int* endTime, int endTimeSize) {
    int n = startTimeSize;
    bool* q = (bool*)calloc(n, sizeof(bool));
    int t1 = 0, t2 = 0;
    for (int i = 0; i < n; i++) {
        if (endTime[i] - startTime[i] <= t1) {
            q[i] = true;
        }
        t1 = fmax(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

        if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
            q[n - i - 1] = true;
        }
        t2 = fmax(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
    }

    int res = 0;
    for (int i = 0; i < n; i++) {
        int left = i == 0 ? 0 : endTime[i - 1];
        int right = i == n - 1 ? eventTime : startTime[i + 1];
        if (q[i]) {
            res = fmax(res, right - left);
        } else {
            res = fmax(res, right - left - (endTime[i] - startTime[i]));
        }
    }
    free(q);
    return res;
}

###Rust

impl Solution {
    pub fn max_free_time(event_time: i32, start_time: Vec<i32>, end_time: Vec<i32>) -> i32 {
        let n = start_time.len();
        let mut q = vec![false; n];
        let mut t1 = 0;
        let mut t2 = 0;
        for i in 0..n {
            if end_time[i] - start_time[i] <= t1 {
                q[i] = true;
            }
            t1 = t1.max(start_time[i] - if i == 0 { 0 } else { end_time[i - 1] });

            let j = n - i - 1;
            if end_time[j] - start_time[j] <= t2 {
                q[j] = true;
            }
            t2 = t2.max(if i == 0 { event_time } else { start_time[n - i] } - end_time[j]);
        }

        let mut res = 0;
        for i in 0..n {
            let left = if i == 0 { 0 } else { end_time[i - 1] };
            let right = if i == n - 1 { event_time } else { start_time[i + 1] };
            if q[i] {
                res = res.max(right - left);
            } else {
                res = res.max(right - left - (end_time[i] - start_time[i]));
            }
        }
        res
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是所有会议的数目。

  • 空间复杂度:$O(n)$。

方法二:贪心 + 优化

方法一中使用了 $q[i]$ 来判断会议 $i$ 平移的最优解,如果我们在枚举会议 $i$ 的过程中对会议 $i$ 平移的两种情况进行计算,那么就可以去掉数组 $q$。具体过程如下:

  • 枚举平移的会议,将该会议左右两侧相邻的空余时间段进行合并,计算得到的新的空余时间段的时长。

  • 从左到右枚举平移的会议,同时用 $t_1$ 记录当前遍历到的会议左侧非相邻的空余时间段的最大时长,如果当前会议时长小于等于 $t_1$,那么说明该会议可以平移到 $t_1$ 对应的空余时间段,计算平移后得到的新的空余时间段的时长。

  • 从右到左枚举平移的会议,同时用 $t_2$ 记录当前遍历到的会议右侧非相邻的空余时间段的最大时长,如果当前会议时长小于等于 $t_2$,那么说明该会议可以平移到 $t_2$ 对应的空余时间段,计算平移后得到的新的空余时间段的时长。

返回以上所有新的空余时间段时长的最大值。

###C++

class Solution {
public:
    int maxFreeTime(int eventTime, vector<int>& startTime, vector<int>& endTime) {
        int n = startTime.size(), res = 0;
        for (int i = 0, t1 = 0, t2 = 0; i < n; i++) {
            int left1 = i == 0 ? 0 : endTime[i - 1];
            int right1 = i == n - 1 ? eventTime : startTime[i + 1];
            if (endTime[i] - startTime[i] <= t1) {
                res = max(res, right1 - left1);
            }
            t1 = max(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

            res = max(res, right1 - left1 - (endTime[i] - startTime[i]));

            int left2 = i == n - 1 ? 0 : endTime[n - i - 2];
            int right2 = i == 0 ? eventTime : startTime[n - i];
            if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
                res = max(res, right2 - left2);
            }
            t2 = max(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
        }
        return res;
    }
};

###Go

func maxFreeTime(eventTime int, startTime []int, endTime []int) int {
n := len(startTime)
q := make([]bool, n)
t1, t2 := 0, 0
for i := 0; i < n; i++ {
if endTime[i] - startTime[i] <= t1 {
q[i] = true
}
if i == 0 {
t1 = max(t1, startTime[i])
} else {
t1 = max(t1, startTime[i] - endTime[i - 1])
}

if endTime[n - i - 1] - startTime[n - i - 1] <= t2 {
q[n - i - 1] = true
}
if i == 0 {
t2 = max(t2, eventTime - endTime[n - 1])
} else {
t2 = max(t2, startTime[n - i] - endTime[n - i - 1])
}
}

res := 0
for i := 0; i < n; i++ {
left := 0
if i != 0 {
left = endTime[i - 1]
}
right := eventTime
if i != n - 1 {
right = startTime[i + 1]
}
if q[i] {
res = max(res, right - left)
} else {
res = max(res, right - left - (endTime[i] - startTime[i]))
}
}
return res
}

###Python

class Solution:
    def maxFreeTime(self, eventTime: int, startTime: list[int], endTime: list[int]) -> int:
        n = len(startTime)
        q = [False] * n
        t1 = 0
        t2 = 0
        for i in range(n):
            if endTime[i] - startTime[i] <= t1:
                q[i] = True
            t1 = max(t1, startTime[i] - (0 if i == 0 else endTime[i - 1]))

            if endTime[n - i - 1] - startTime[n - i - 1] <= t2:
                q[n - i - 1] = True
            t2 = max(t2, (eventTime if i == 0 else startTime[n - i]) - endTime[n - i - 1])

        res = 0
        for i in range(n):
            left = 0 if i == 0 else endTime[i - 1]
            right = eventTime if i == n - 1 else startTime[i + 1]
            if q[i]:
                res = max(res, right - left)
            else:
                res = max(res, right - left - (endTime[i] - startTime[i]))
        return res

###Java

class Solution {
    public int maxFreeTime(int eventTime, int[] startTime, int[] endTime) {
        int n = startTime.length;
        boolean[] q = new boolean[n];
        for (int i = 0, t1 = 0, t2 = 0; i < n; i++) {
            if (endTime[i] - startTime[i] <= t1) {
                q[i] = true;
            }
            t1 = Math.max(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

            if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
                q[n - i - 1] = true;
            }
            t2 = Math.max(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
        }

        int res = 0;
        for (int i = 0; i < n; i++) {
            int left = i == 0 ? 0 : endTime[i - 1];
            int right = i == n - 1 ? eventTime : startTime[i + 1];
            if (q[i]) {
                res = Math.max(res, right - left);
            } else {
                res = Math.max(res, right - left - (endTime[i] - startTime[i]));
            }
        }
        return res;
    }
}

###TypeScript

function maxFreeTime(eventTime: number, startTime: number[], endTime: number[]): number {
    const n = startTime.length;
    const q: boolean[] = new Array(n).fill(false);
    let t1 = 0, t2 = 0;
    for (let i = 0; i < n; i++) {
        if (endTime[i] - startTime[i] <= t1) {
            q[i] = true;
        }
        t1 = Math.max(t1, startTime[i] - (i === 0 ? 0 : endTime[i - 1]));

        if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
            q[n - i - 1] = true;
        }
        t2 = Math.max(t2, (i === 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
    }

    let res = 0;
    for (let i = 0; i < n; i++) {
        const left = i === 0 ? 0 : endTime[i - 1];
        const right = i === n - 1 ? eventTime : startTime[i + 1];
        if (q[i]) {
            res = Math.max(res, right - left);
        } else {
            res = Math.max(res, right - left - (endTime[i] - startTime[i]));
        }
    }
    return res;
}

###JavaScript

var maxFreeTime = function(eventTime, startTime, endTime) {
    const n = startTime.length;
    const q = new Array(n).fill(false);
    let t1 = 0, t2 = 0;
    for (let i = 0; i < n; i++) {
        if (endTime[i] - startTime[i] <= t1) {
            q[i] = true;
        }
        t1 = Math.max(t1, startTime[i] - (i === 0 ? 0 : endTime[i - 1]));

        if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
            q[n - i - 1] = true;
        }
        t2 = Math.max(t2, (i === 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
    }

    let res = 0;
    for (let i = 0; i < n; i++) {
        const left = i === 0 ? 0 : endTime[i - 1];
        const right = i === n - 1 ? eventTime : startTime[i + 1];
        if (q[i]) {
            res = Math.max(res, right - left);
        } else {
            res = Math.max(res, right - left - (endTime[i] - startTime[i]));
        }
    }
    return res;
};

###C#

public class Solution {
    public int MaxFreeTime(int eventTime, int[] startTime, int[] endTime) {
        int n = startTime.Length;
        bool[] q = new bool[n];
        int t1 = 0, t2 = 0;
        for (int i = 0; i < n; i++) {
            if (endTime[i] - startTime[i] <= t1) {
                q[i] = true;
            }
            t1 = Math.Max(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

            if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
                q[n - i - 1] = true;
            }
            t2 = Math.Max(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
        }

        int res = 0;
        for (int i = 0; i < n; i++) {
            int left = i == 0 ? 0 : endTime[i - 1];
            int right = i == n - 1 ? eventTime : startTime[i + 1];
            if (q[i]) {
                res = Math.Max(res, right - left);
            } else {
                res = Math.Max(res, right - left - (endTime[i] - startTime[i]));
            }
        }
        return res;
    }
}

###C

int maxFreeTime(int eventTime, int* startTime, int startTimeSize, int* endTime, int endTimeSize) {
    int n = startTimeSize;
    bool* q = (bool*)calloc(n, sizeof(bool));
    int t1 = 0, t2 = 0;
    for (int i = 0; i < n; i++) {
        if (endTime[i] - startTime[i] <= t1) {
            q[i] = true;
        }
        t1 = fmax(t1, startTime[i] - (i == 0 ? 0 : endTime[i - 1]));

        if (endTime[n - i - 1] - startTime[n - i - 1] <= t2) {
            q[n - i - 1] = true;
        }
        t2 = fmax(t2, (i == 0 ? eventTime : startTime[n - i]) - endTime[n - i - 1]);
    }

    int res = 0;
    for (int i = 0; i < n; i++) {
        int left = i == 0 ? 0 : endTime[i - 1];
        int right = i == n - 1 ? eventTime : startTime[i + 1];
        if (q[i]) {
            res = fmax(res, right - left);
        } else {
            res = fmax(res, right - left - (endTime[i] - startTime[i]));
        }
    }
    free(q);
    return res;
}

###Rust

impl Solution {
    pub fn max_free_time(event_time: i32, start_time: Vec<i32>, end_time: Vec<i32>) -> i32 {
        let n = start_time.len();
        let mut q = vec![false; n];
        let mut t1 = 0;
        let mut t2 = 0;
        for i in 0..n {
            if end_time[i] - start_time[i] <= t1 {
                q[i] = true;
            }
            t1 = t1.max(start_time[i] - if i == 0 { 0 } else { end_time[i - 1] });

            let j = n - i - 1;
            if end_time[j] - start_time[j] <= t2 {
                q[j] = true;
            }
            t2 = t2.max(if i == 0 { event_time } else { start_time[n - i] } - end_time[j]);
        }

        let mut res = 0;
        for i in 0..n {
            let left = if i == 0 { 0 } else { end_time[i - 1] };
            let right = if i == n - 1 { event_time } else { start_time[i + 1] };
            if q[i] {
                res = res.max(right - left);
            } else {
                res = res.max(right - left - (end_time[i] - start_time[i]));
            }
        }
        res
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是所有会议的数目。

  • 空间复杂度:$O(1)$。

维护前三大的空位+枚举+分类讨论(Python/Java/C++/Go)

把会议区间视作桌子,空余时间视作空位,我们要把一个桌子移到别的空位中。

初步想法是枚举桌子,找一个长度大于等于桌子长度的空位移过去。看上去,找一个长度最长的空位就好了。

但万一最长空位与桌子相邻呢?这并没有把桌子彻底移出去。

找次长的空位?万一最长空位和次长空位都与桌子相邻呢?

那就再找第三长的。不可能有三个空位都与同一个桌子相邻。

核心思路:计算长度最长的三个空位在哪,其中一定有一个空位不在移出去的桌子的左右两侧。如果空位长度大于等于桌子的长度,我们把桌子移到这个空位中。

设最大的三个空位所在的位置(下标)分别是 $a,b,c$。

枚举桌子,分类讨论:

  • 如果桌子左右两侧的空位没有 $a$,那么把桌子移到 $a$。
  • 否则,如果桌子左右两侧的空位没有 $b$,那么把桌子移到 $b$。
  • 否则,把桌子移到 $c$。

继续分类讨论:

  • 如果能移(有足够长的空位),新的空位长度 = 桌子长度 + 桌子左右两侧的空位长度。
  • 如果不能移,那么只能移到左右相邻的桌子旁边,新的空位长度 = 桌子左右两侧的空位长度。

具体请看 视频讲解,欢迎点赞关注~

###py

class Solution:
    def maxFreeTime(self, eventTime: int, startTime: List[int], endTime: List[int]) -> int:
        n = len(startTime)

        # 计算空位长度
        def get(i: int) -> int:
            if i == 0:
                return startTime[0]
            if i == n:
                return eventTime - endTime[n - 1]
            return startTime[i] - endTime[i - 1]

        # 有 n+1 个空位,计算前三大的空位在哪
        a, b, c = 0, -1, -1
        for i in range(1, n + 1):
            sz = get(i)
            if sz > get(a):
                a, b, c = i, a, b
            elif b < 0 or sz > get(b):
                b, c = i, b
            elif c < 0 or sz > get(c):
                c = i

        ans = 0
        # 枚举桌子
        for i, (s, e) in enumerate(zip(startTime, endTime)):
            sz = e - s
            if i != a and i + 1 != a and sz <= get(a) or \
               i != b and i + 1 != b and sz <= get(b) or \
               sz <= get(c):  # 可以移出去
                ans = max(ans, get(i) + sz + get(i + 1))
            else:
                ans = max(ans, get(i) + get(i + 1))
        return ans

###py

class Solution:
    def maxFreeTime(self, eventTime: int, startTime: List[int], endTime: List[int]) -> int:
        free = [startTime[0]] + [s - e for s, e in zip(startTime[1:], endTime)] + [eventTime - endTime[-1]]

        a = b = c = -1
        for i, sz in enumerate(free):
            if a < 0 or sz > free[a]:
                a, b, c = i, a, b
            elif b < 0 or sz > free[b]:
                b, c = i, b
            elif c < 0 or sz > free[c]:
                c = i

        ans = 0
        for i, (s, e) in enumerate(zip(startTime, endTime)):
            sz = e - s
            if i != a and i + 1 != a and sz <= free[a] or \
               i != b and i + 1 != b and sz <= free[b] or \
               sz <= free[c]:
                ans = max(ans, free[i] + sz + free[i + 1])
            else:
                ans = max(ans, free[i] + free[i + 1])
        return ans

###java

class Solution {
    private int eventTime;
    private int[] startTime, endTime;

    public int maxFreeTime(int eventTime, int[] startTime, int[] endTime) {
        this.eventTime = eventTime;
        this.startTime = startTime;
        this.endTime = endTime;
        int n = startTime.length;

        // 有 n+1 个空位,计算前三大的空位在哪
        int a = 0, b = -1, c = -1;
        for (int i = 1; i <= n; i++) {
            int sz = get(i);
            if (sz > get(a)) {
                c = b; b = a; a = i;
            } else if (b < 0 || sz > get(b)) {
                c = b; b = i;
            } else if (c < 0 || sz > get(c)) {
                c = i;
            }
        }

        int ans = 0;
        // 枚举桌子
        for (int i = 0; i < n; i++) {
            int sz = endTime[i] - startTime[i];
            if (i != a && i + 1 != a && sz <= get(a) ||
                i != b && i + 1 != b && sz <= get(b) ||
                sz <= get(c)) { // 可以移出去
                ans = Math.max(ans, get(i) + sz + get(i + 1));
            } else {
                ans = Math.max(ans, get(i) + get(i + 1));
            }
        }
        return ans;
    }

    // 计算空位长度
    private int get(int i) {
        if (i == 0) {
            return startTime[0];
        }
        int n = startTime.length;
        if (i == n) {
            return eventTime - endTime[n - 1];
        }
        return startTime[i] - endTime[i - 1];
    }
}

###cpp

class Solution {
public:
    int maxFreeTime(int eventTime, vector<int>& startTime, vector<int>& endTime) {
        int n = startTime.size();

        // 计算空位长度
        auto get = [&](int i) -> int {
            if (i == 0) {
                return startTime[0];
            }
            if (i == n) {
                return eventTime - endTime[n - 1];
            }
            return startTime[i] - endTime[i - 1];
        };

        // 有 n+1 个空位,计算前三大的空位在哪
        int a = 0, b = -1, c = -1;
        for (int i = 1; i <= n; i++) {
            int sz = get(i);
            if (sz > get(a)) {
                c = b; b = a; a = i;
            } else if (b < 0 || sz > get(b)) {
                c = b; b = i;
            } else if (c < 0 || sz > get(c)) {
                c = i;
            }
        }

        int ans = 0;
        // 枚举桌子
        for (int i = 0; i < n; i++) {
            int sz = endTime[i] - startTime[i];
            if (i != a && i + 1 != a && sz <= get(a) ||
                i != b && i + 1 != b && sz <= get(b) ||
                sz <= get(c)) { // 可以移出去
                ans = max(ans, get(i) + sz + get(i + 1));
            } else {
                ans = max(ans, get(i) + get(i + 1));
            }
        }
        return ans;
    }
};

###go

func maxFreeTime(eventTime int, startTime, endTime []int) (ans int) {
    n := len(startTime)

    // 计算空位长度
    get := func(i int) int {
        if i == 0 {
            return startTime[0]
        }
        if i == n {
            return eventTime - endTime[n-1]
        }
        return startTime[i] - endTime[i-1]
    }

    // 有 n+1 个空位,计算前三大的空位在哪
    a, b, c := 0, -1, -1
    for i := 1; i <= n; i++ {
        sz := get(i)
        if sz > get(a) {
            a, b, c = i, a, b
        } else if b < 0 || sz > get(b) {
            b, c = i, b
        } else if c < 0 || sz > get(c) {
            c = i
        }
    }

    // 枚举桌子
    for i, e := range endTime {
        sz := e - startTime[i]
        if i != a && i+1 != a && sz <= get(a) ||
            i != b && i+1 != b && sz <= get(b) ||
            sz <= get(c) { // 可以移出去
            ans = max(ans, get(i)+sz+get(i+1))
        } else {
            ans = max(ans, get(i)+get(i+1))
        }
    }
    return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{startTime}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

枚举移动的会议

Problem: 3440. 重新安排会议得到最多空余时间 II

[TOC]

思路

  • 比 I 多出一种情况。

解题过程

  • 枚举移动的会议:
    1. 将会议平移到最左或最右;
    1. 将会议移动到左边或右边的一个空隙,如果存在足够大的空隙。
  • 各扫一遍可以处理左边或右边的最大空隙,从而判断是否能够选 2。最后取最大值即可。

复杂度

  • 时间复杂度: $O(n)$
  • 空间复杂度: 可以做到 $O(1)$,赛时使用了思维难度更低的 $O(n)$ 写法。

Code

###C++

class Solution {
public:
    int maxFreeTime(int eventTime, vector<int>& startTime,
                    vector<int>& endTime) {
        int n = startTime.size();
        vector<int> lRoom(n);
        vector<int> rRoom(n);
        lRoom[0] = startTime[0];
        for (int i = 1; i < n; i++) {
            lRoom[i] = max(lRoom[i - 1], startTime[i] - endTime[i - 1]);
        }
        rRoom[n - 1] = eventTime - endTime[n - 1];
        for (int i = n - 2; i > -1; i--) {
            rRoom[i] = max(rRoom[i + 1], startTime[i + 1] - endTime[i]);
        }
        int res = 0;
        for (int i = 0; i < n; i++) {
            int lTime = i == 0 ? 0 : endTime[i - 1];
            int rTime = i == n - 1 ? eventTime : startTime[i + 1];
            int len = endTime[i] - startTime[i];
            res = max(res, rTime - lTime - len);
            if (i > 0 && lRoom[i - 1] >= len) {
                res = max(res, rTime - lTime);
            }
            if (i < n - 1 && rRoom[i + 1] >= len) {
                res = max(res, rTime - lTime);
            }
        }
        return res;
    }
};

每日一题-重新安排会议得到最多空余时间 I🟡

给你一个整数 eventTime 表示一个活动的总时长,这个活动开始于 t = 0 ,结束于 t = eventTime 。

同时给你两个长度为 n 的整数数组 startTime 和 endTime 。它们表示这次活动中 n 个时间 没有重叠 的会议,其中第 i 个会议的时间为 [startTime[i], endTime[i]] 。

你可以重新安排 至多 k 个会议,安排的规则是将会议时间平移,且保持原来的 会议时长 ,你的目的是移动会议后 最大化 相邻两个会议之间的 最长 连续空余时间。

移动前后所有会议之间的 相对 顺序需要保持不变,而且会议时间也需要保持互不重叠。

请你返回重新安排会议以后,可以得到的 最大 空余时间。

注意,会议 不能 安排到整个活动的时间以外。

 

示例 1:

输入:eventTime = 5, k = 1, startTime = [1,3], endTime = [2,5]

输出:2

解释:

将 [1, 2] 的会议安排到 [2, 3] ,得到空余时间 [0, 2] 。

示例 2:

输入:eventTime = 10, k = 1, startTime = [0,2,9], endTime = [1,4,10]

输出:6

解释:

将 [2, 4] 的会议安排到 [1, 3] ,得到空余时间 [3, 9] 。

示例 3:

输入:eventTime = 5, k = 2, startTime = [0,1,2,3,4], endTime = [1,2,3,4,5]

输出:0

解释:

活动中的所有时间都被会议安排满了。

 

提示:

  • 1 <= eventTime <= 109
  • n == startTime.length == endTime.length
  • 2 <= n <= 105
  • 1 <= k <= n
  • 0 <= startTime[i] < endTime[i] <= eventTime
  • endTime[i] <= startTime[i + 1] 其中 i 在范围 [0, n - 2] 之间。

转化成定长滑动窗口(Python/Java/C++/Go)

lc3439.png

看示例 1,把会议区间 $[1,2]$ 移动到 $[0,1]$ 或者 $[2,3]$,会产生空余时间段 $[1,3]$ 或者 $[0,2]$,相当于把两个相邻的长为 $1$ 空余时间段 $[0,1]$ 和 $[2,3]$ 合并成一个更大的长为 $1+1=2$ 的空余时间段。

如果 $k=1$,那么我们可以合并 $2$ 个相邻的空余时间段。

如果 $k=2$,为了让答案尽量大,合并连续 $3$ 个空余时间段,相比其他方案是最优的。(注意题目要求会议之间的相对顺序必须保持不变)

一般地,最优做法是合并连续 $k+1$ 个空余时间段。

现在问题变成:

  • 给你 $n+1$ 个空余时间段,合并其中连续 $k+1$ 个空余时间段,得到的最大长度是多少?

注 1:最左边和最右边各有一个空余时间段,中间有 $n-1$ 个空余时间段夹在两个相邻会议之间,所以有 $n+1$ 个空余时间段。

注 2:空余时间段的长度可以是 $0$。

这可以用定长滑动窗口解决,窗口大小为 $k+1$。原理见【套路】教你解决定长滑窗!适用于所有定长滑窗题目!

本题视频讲解,欢迎点赞关注~

写法一

创建一个长为 $n+1$ 的数组,保存所有空余时间段的长度:

  • 最左边的空余时间段:长为 $\textit{startTime}[0]$。
  • 中间的空余时间段:长为 $\textit{startTime}[i] - \textit{endTime}[i-1]$。
  • 最右边的空余时间段:长为 $\textit{eventTime} - \textit{endTime}[n-1]$。

示例 1 的数组为 $[1, 1, 0]$。

示例 2 的数组为 $[0, 1, 5, 0]$。

示例 3 的数组为 $[0, 0, 0, 0, 0, 0]$。

###py

class Solution:
    def maxFreeTime(self, eventTime: int, k: int, startTime: List[int], endTime: List[int]) -> int:
        n = len(startTime)
        free = [0] * (n + 1)
        free[0] = startTime[0]  # 最左边的空余时间段
        for i in range(1, n):
            free[i] = startTime[i] - endTime[i - 1]  # 中间的空余时间段
        free[n] = eventTime - endTime[-1]  # 最右边的空余时间段

        # 套定长滑窗模板(窗口长为 k+1)
        ans = s = 0
        for i, f in enumerate(free):
            s += f
            if i < k:
                continue
            ans = max(ans, s)
            s -= free[i - k]
        return ans

###py

class Solution:
    def maxFreeTime(self, eventTime: int, k: int, startTime: List[int], endTime: List[int]) -> int:
        free = [startTime[0]] + [s - e for s, e in zip(startTime[1:], endTime)] + [eventTime - endTime[-1]]
        ans = s = 0
        for i, f in enumerate(free):
            s += f
            if i < k:
                continue
            ans = max(ans, s)
            s -= free[i - k]
        return ans

###java

class Solution {
    public int maxFreeTime(int eventTime, int k, int[] startTime, int[] endTime) {
        int n = startTime.length;
        int[] free = new int[n + 1];
        free[0] = startTime[0]; // 最左边的空余时间段
        for (int i = 1; i < n; i++) {
            free[i] = startTime[i] - endTime[i - 1]; // 中间的空余时间段
        }
        free[n] = eventTime - endTime[n - 1]; // 最右边的空余时间段

        // 套定长滑窗模板(窗口长为 k+1)
        int ans = 0;
        int s = 0;
        for (int i = 0; i <= n; i++) {
            s += free[i];
            if (i < k) {
                continue;
            }
            ans = Math.max(ans, s);
            s -= free[i - k];
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maxFreeTime(int eventTime, int k, vector<int>& startTime, vector<int>& endTime) {
        int n = startTime.size();
        vector<int> free(n + 1);
        free[0] = startTime[0]; // 最左边的空余时间段
        for (int i = 1; i < n; i++) {
            free[i] = startTime[i] - endTime[i - 1]; // 中间的空余时间段
        }
        free[n] = eventTime - endTime[n - 1]; // 最右边的空余时间段

        // 套定长滑窗模板(窗口长为 k+1)
        int ans = 0, s = 0;
        for (int i = 0; i <= n; i++) {
            s += free[i];
            if (i < k) {
                continue;
            }
            ans = max(ans, s);
            s -= free[i - k];
        }
        return ans;
    }
};

###go

func maxFreeTime(eventTime, k int, startTime, endTime []int) (ans int) {
n := len(startTime)
free := make([]int, n+1)
free[0] = startTime[0] // 最左边的空余时间段
for i := 1; i < n; i++ {
free[i] = startTime[i] - endTime[i-1] // 中间的空余时间段
}
free[n] = eventTime - endTime[n-1] // 最右边的空余时间段

// 套定长滑窗模板(窗口长为 k+1)
s := 0
for i, f := range free {
s += f
if i < k {
continue
}
ans = max(ans, s)
s -= free[i-k]
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{startTime}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

写法二:空间优化

改用一个函数计算第 $i$ 个空余时间段的长度,从而省去 $\textit{free}$ 数组。

###py

class Solution:
    def maxFreeTime(self, eventTime: int, k: int, startTime: List[int], endTime: List[int]) -> int:
        def get(i: int) -> int:
            if i == 0:
                return startTime[0]  # 最左边的空余时间段
            if i == n:
                return eventTime - endTime[-1]  # 最右边的空余时间段
            return startTime[i] - endTime[i - 1]  # 中间的空余时间段

        n = len(startTime)
        ans = s = 0
        for i in range(n + 1):
            s += get(i)
            if i < k:
                continue
            ans = max(ans, s)
            s -= get(i - k)
        return ans

###java

class Solution {
    public int maxFreeTime(int eventTime, int k, int[] startTime, int[] endTime) {
        int ans = 0;
        int s = 0;
        for (int i = 0; i <= startTime.length; i++) {
            s += get(i, eventTime, startTime, endTime);
            if (i < k) {
                continue;
            }
            ans = Math.max(ans, s);
            s -= get(i - k, eventTime, startTime, endTime);
        }
        return ans;
    }

    private int get(int i, int eventTime, int[] startTime, int[] endTime) {
        if (i == 0) {
            return startTime[0]; // 最左边的空余时间段
        }
        int n = startTime.length;
        if (i == n) {
            return eventTime - endTime[n - 1]; // 最右边的空余时间段
        }
        return startTime[i] - endTime[i - 1]; // 中间的空余时间段
    }
}

###cpp

class Solution {
public:
    int maxFreeTime(int eventTime, int k, vector<int>& startTime, vector<int>& endTime) {
        int n = startTime.size();
        auto get = [&](int i) -> int {
            if (i == 0) {
                return startTime[0]; // 最左边的空余时间段
            }
            if (i == n) {
                return eventTime - endTime[n - 1]; // 最右边的空余时间段
            }
            return startTime[i] - endTime[i - 1]; // 中间的空余时间段
        };

        int s = 0, ans = 0;
        for (int i = 0; i <= n; i++) {
            s += get(i);
            if (i < k) {
                continue;
            }
            ans = max(ans, s);
            s -= get(i - k);
        }
        return ans;
    }
};

###go

func maxFreeTime(eventTime, k int, startTime, endTime []int) (ans int) {
n := len(startTime)
get := func(i int) int {
if i == 0 {
return startTime[0] // 最左边的空余时间段
}
if i == n {
return eventTime - endTime[n-1] // 最右边的空余时间段
}
return startTime[i] - endTime[i-1] // 中间的空余时间段
}

s := 0
for i := range n + 1 {
s += get(i)
if i < k {
continue
}
ans = max(ans, s)
s -= get(i - k)
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{startTime}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌