前言
当问题是求某一个最值时,可以考虑用二分来枚举答案。可以用二分的前提是答案具有二段性。以求满足条件的最小答案为例,首先最小答案一定是满足条件的,如果对于任何大于最小答案的值也满足条件,任何小于最小答案的值不满足条件,那么就称所求答案具有二段性。通过最小答案这个值,可以把所有的数划分为两个区间,一个区间的所有数都满足题目的条件,另一个区间的所有数都不满足题目的条件,这样就可以通过二分来找到最小答案。如果是求最大答案同理。
机器人跳跃问题
机器人正在玩一个古老的基于 $DOS$ 的游戏。
游戏中有 $N+1$ 座建筑——从 $0$ 到 $N$ 编号,从左到右排列。
编号为 $0$ 的建筑高度为 $0$ 个单位,编号为 $i$ 的建筑高度为 $H \left( i \right)$ 个单位。
起初,机器人在编号为 $0$ 的建筑处。
每一步,它跳到下一个(右边)建筑。
假设机器人在第 $k$ 个建筑,且它现在的能量值是 $E$,下一步它将跳到第 $k+1$ 个建筑。
如果 $H \left( {k+1} \right)>E$,那么机器人就失去 $H \left( {k+1} \right)−E$ 的能量值,否则它将得到 $E−H \left( {k+1} \right)$ 的能量值。
游戏目标是到达第 $N$ 个建筑,在这个过程中能量值不能为负数个单位。
现在的问题是机器人至少以多少能量值开始游戏,才可以保证成功完成游戏?
输入格式
第一行输入整数 $N$。
第二行是 $N$ 个空格分隔的整数,$H \left( 1 \right),H \left( 2 \right),…,H \left( N \right)$ 代表建筑物的高度。
输出格式
输出一个整数,表示所需的最少单位的初始能量值上取整后的结果。
数据范围
$1 \leq N,H \left( i \right) \leq 10^{5}$
输入样例1:
5 3 4 3 2 4
输出样例1:
4
输入样例2:
3 4 4 4
输出样例2:
4
输入样例3:
3 1 6 4
输出样例3:
3
解题思路
设$E$表示跳到第$k$座塔后剩余的能量。题目要求对于跳到任何一座塔,$E$都要是一个非零的整数,也就是说在整个跳跃的过程中$E_{k}$都要大于等于$0$,跳到最后一座塔时$E$可以等于$0$。
若此时已经跳到第$k$座塔,剩余的能量为$E$,第$k+1$座塔的高度为$h_{k+1}$:
- 若$h_{k+1} > E$,则$E = E - \left( h_{k+1} - E \right) = 2 \cdot E - h_{k+1}$。
- 若$h_{k+1} \leq E$,则$E = E + \left( E - h_{k+1} \right) = 2 \cdot E - h_{k+1}$。
所以可以发现,无论后一座塔的高度与当前剩余能量的大小如何,都可以用$E = 2 \cdot E - h_{k+1}$计算得到跳跃后的能量。
我们来看看是否可以用二分来做。如果一开始的能量为$E_{0}$且满足条件(可以跳到最后一座塔),那么对于任何大于$E_{0}$的数都是满足条件的。
证明:假设$E_{0}$为跳到第$0$座塔后剩余的能量,$E_{1}$为跳到第$1$座塔后剩余的能量,以此类推,$E_{n}$为跳到第$n$座塔后剩余的能量,如果$E_{0}$是满足条件的,那么对于任何的$E_{i}$都是满足$E_{i} \geq 0$。如果我们取一个${E'}_{0} \geq E_{0}$,则有${E'}_{1} = 2 \cdot {E'}_{0} - h_{1} \geq 2 \cdot E_{0} - h_{1} = E_{1} \geq 0$,用数学归纳法可以证明对于任何一项${E'}_{i}$都是大于$E_{i}$的,所以对于任何大于$E_{0}$的数都是满足条件的。
所以一定存在一个最小的数$E_{0}$,当$E \geq E_{0}$时,满足条件,当$E < E_{0}$,不满足条件,因此答案具有二段性,可以使用二分。
这个还有一个细节就是,我们每跳一次$E$都要乘一次$2$,如果每一座塔的高度都很小(比如都是$1$),那么我们的答案肯定会爆掉。但我们又可以发现,在跳跃过程中当$E$达到某一个数值时,那么就可以判定一定可以跳到最后一座塔。这个值就是所有塔中最高的塔的高度。设最大高度为$h_{max}$,则当$E \geq h_{max}$,此时对于任何一座塔,$2 \cdot E - h_{i} = E + E - h_{i} \geq E + h_{max} - h_{i} \geq E$,也就是说,如果在某一时刻$E \geq h_{max}$,那么之后的跳跃$E$都会严格递增的,同时也是大于等于$0$的。所有只要发生这种情况,我们就可以直接判定满足条件,返回$true$。
AC代码如下:
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 const int N = 1e5 + 10; 6 7 int n; 8 int h[N]; 9 10 bool check(int x) { 11 for (int i = 0; i < n; i++) { 12 x = x * 2 - h[i]; 13 if (x >= N) return true; // 发现能量大于最大值,可以判定一定满足条件 14 else if (x < 0) return false; 15 } 16 17 return true; 18 } 19 20 int main() { 21 scanf("%d", &n); 22 for (int i = 0; i < n; i++) { 23 scanf("%d", h + i); 24 } 25 26 int left = 1, right = N; 27 while (left < right) { 28 int mid = left + right >> 1; 29 if (check(mid)) right = mid; // mid满足条件,说明最小答案小于等于mid,在mid的左边(包括mid) 30 else left = mid + 1; // mid不满足条件,说明答案一定大于mid,在mid的右边 31 } 32 printf("%d", left); 33 34 return 0; 35 }
这道题还有另一种做法,就是递推。我们可以发现,当跳到最后一座塔时,如果剩余的答案为$0$,那么初始能量一定是最小的。同时根据前面的公式$E = 2 \cdot E - h_{k+1}$,如果知道跳到当前这座塔的剩余能量,那么就可以求出跳到前一座塔的剩余能量,又因为我们已经知道跳到最后一座塔的能量为$0$,所以就可以递推出初始能量了。
AC代码如下:
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 const int N = 1e5 + 10; 6 7 int h[N]; 8 9 int main() { 10 int n; 11 scanf("%d", &n); 12 for (int i = 0; i < n; i++) { 13 scanf("%d", h + i); 14 } 15 16 int ret = 0; 17 for (int i = n - 1; i >= 0; i--) { 18 ret = (ret + h[i] + 1) >> 1; // 这里要上取整,防止出现小数 19 } 20 printf("%d", ret); 21 22 return 0; 23 }
分巧克力
儿童节那天有 $K$ 位小朋友到小明家做客。
小明拿出了珍藏的巧克力招待小朋友们。
小明一共有 $N$ 块巧克力,其中第 $i$ 块是 $H_{i} \times W_{i}$ 的方格组成的长方形。
为了公平起见,小明需要从这 $N$ 块巧克力中切出 $K$ 块巧克力分给小朋友们。
切出的巧克力需要满足:
- 形状是正方形,边长是整数
- 大小相同
例如一块 $6 \times 5$ 的巧克力可以切出 $6$ 块 $2 \times 2$ 的巧克力或者 $2$ 块 $3 \times 3$ 的巧克力。
当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?
输入格式
第一行包含两个整数 $N$ 和 $K$。
以下 $N$ 行每行包含两个整数 $H_{i}$ 和 $W_{i}$。
输入保证每位小朋友至少能获得一块 $1\times 1$ 的巧克力。
输出格式
输出切出的正方形巧克力最大可能的边长。
数据范围
$1\leq N,K \leq 105$,
$1\leq H_{i},W_{i} \leq 10^{5}$
输入样例:
2 10 6 5 5 6
输出样例:
2
解题思路
这题让我们求的是最大边长,就是最大值。我们来看一下如果边长小于等于最大边长是否满足题目的条件,大于最大边长是否不满足题目的条件,如果都是的话,就意味着满足二段性,可以用二分来做。
如果边长为$x$,那么一块大小为$H_{i} \times W_{i}$的巧克力可以切的块数为$\left\lfloor \frac{H_{i}}{x} \right\rfloor \times \left\lfloor \frac{W_{i}}{x} \right\rfloor$。
我们知道,随着边长的增加,能够切出来的块数就越小,这是一个单调的性质。所以我们应该找到最大的那个边,满足切出的块数要大于等于$k$。
如果所求的最大边长为$x_{m}$,那么小于等于$x_{m}$的边长一定是满足条件的(切出的块一定比$k$大),而大于$x_{m}$的边长一定不满足条件(切出的块一定比$k$小)。因此答案满足二段性,可以用二分。
AC代码如下:
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 const int N = 1e5 + 10; 6 7 int n, k; 8 int h[N], w[N]; 9 10 bool cheak(int len) { 11 long long cnt = 0; 12 for (int i = 0; i < n; i++) { 13 cnt += (long long)(h[i] / len) * (w[i] / len); 14 if (cnt >= k) return true; 15 } 16 17 return false; 18 } 19 20 int main() { 21 scanf("%d %d", &n, &k); 22 for (int i = 0; i < n; i++) { 23 scanf("%d %d", h + i, w + i); 24 } 25 26 int left = 1, right = N; 27 while (left < right) { 28 int mid = left + right + 1 >> 1; 29 if (cheak(mid)) left = mid; // 如果mid满足条件,说明最大答案大于等于mid,在mid的右边(包括mid) 30 else right = mid - 1; // 如果mid不满足条件,说明最大答案一定小于mid,在mid的左边 31 } 32 printf("%d", left); 33 34 return 0; 35 }
我在哪?
农夫约翰出门沿着马路散步,但是他现在发现自己可能迷路了!
沿路有一排共 $N$ 个农场。
不幸的是农场并没有编号,这使得约翰难以分辨他在这条路上所处的位置。
然而,每个农场都沿路设有一个彩色的邮箱,所以约翰希望能够通过查看最近的几个邮箱的颜色来唯一确定他所在的位置。
每个邮箱的颜色用 $A..Z$ 之间的一个字母来指定,所以沿着道路的 $N$ 个邮箱的序列可以用一个长为 $N$ 的由字母 $A..Z$ 组成的字符串来表示。
某些邮箱可能会有相同的颜色。
约翰想要知道最小的 $K$ 的值,使得他查看任意连续 $K$ 个邮箱序列,他都可以唯一确定这一序列在道路上的位置。
例如,假设沿路的邮箱序列为 $ABCDABC$ 。
约翰不能令 $K=3$,因为如果他看到了 $ABC$,则沿路有两个这一连续颜色序列可能所在的位置。
最小可行的 $K$ 的值为 $K=4$,因为如果他查看任意连续 $4$ 个邮箱,那么可得到的连续颜色序列可以唯一确定他在道路上的位置。
输入格式
输入的第一行包含 $N$,第二行包含一个由 $N$ 个字符组成的字符串,每个字符均在 $A..Z$ 之内。
输出格式
输出一行,包含一个整数,为可以解决农夫约翰的问题的最小 $K$ 值。
数据范围
$1 \leq N \leq 100$
输入样例:
7 ABCDABC
输出样例:
4
解题思路
如果满足条件的最小值为$k$(当长度为$k$时,字符串中任何连续的长度为$k$的子串两两不同),如果有$k' > k$,那么$k'$是否还满足条件呢?我们任意取两段长度为$k'$的子串,对于这两段子串中长度为$k$的前缀,这两段前缀一定是不同的,这是因为当长度为$k$时,字符串中任何连续的长度为$k$的子串两两不同,两段前缀不同,那么这两个字串一定不同。因此当$k' \geq k$,都是满足条件的(长度为$k'$的字串两两不同)。
由于$k$是可以取到最小值的,所以当长度小于$k$时,不满足条件。因此答案具有二段性,可以用二分。
为了可以快速判断两段字符串是否相同,这里用字符串哈希,因为任何一段子串都可以看作是字符串都某个区间,判断的时间可以达到$O \left( 1 \right)$。
AC代码如下:
1 #include <cstdio> 2 #include <unordered_set> 3 #include <algorithm> 4 using namespace std; 5 6 typedef unsigned long long ULL; 7 8 const int N = 110, P = 131; 9 10 int n; 11 char str[N]; 12 ULL h[N], p[N]; 13 14 bool is_overlap(int len) { 15 unordered_set<ULL> st; 16 for (int i = len; i <= n; i++) { 17 ULL t = h[i] - h[i - len] * p[len]; 18 if (st.count(t)) return true; 19 st.insert(t); 20 } 21 22 return false; 23 } 24 25 int main() { 26 scanf("%d %s", &n, str + 1); 27 28 p[0] = 1; 29 for (int i = 1; i <= n; i++) { 30 p[i] = p[i - 1] * P; 31 h[i] = h[i - 1] * P + str[i]; 32 } 33 34 int left = 1, right = n; 35 while (left < right) { 36 int mid = left + right >> 1; 37 if (!is_overlap(mid)) right = mid; 38 else left = mid + 1; 39 } 40 printf("%d", left); 41 42 return 0; 43 }
参考资料
2.2 二分与前缀和——习题课:https://www.acwing.com/video/617/
AcWing 1460. 我在哪?(寒假每日一题2022):https://www.acwing.com/video/3710/