二分求解最值问题例题

前言

  当问题是求某一个最值时,可以考虑用二分来枚举答案。可以用二分的前提是答案具有二段性。以求满足条件的最小答案为例,首先最小答案一定是满足条件的,如果对于任何大于最小答案的值也满足条件,任何小于最小答案的值不满足条件,那么就称所求答案具有二段性。通过最小答案这个值,可以把所有的数划分为两个区间,一个区间的所有数都满足题目的条件,另一个区间的所有数都不满足题目的条件,这样就可以通过二分来找到最小答案。如果是求最大答案同理。

 

机器人跳跃问题

机器人正在玩一个古老的基于 DOS 的游戏。

游戏中有 N+1 座建筑——从 0N 编号,从左到右排列。

编号为 0 的建筑高度为 0 个单位,编号为 i 的建筑高度为 H(i) 个单位。

起初,机器人在编号为 0 的建筑处。

每一步,它跳到下一个(右边)建筑。

假设机器人在第 k 个建筑,且它现在的能量值是 E,下一步它将跳到第 k+1 个建筑。

如果 H(k+1)>E,那么机器人就失去 H(k+1)E 的能量值,否则它将得到 EH(k+1) 的能量值。

游戏目标是到达第 N 个建筑,在这个过程中能量值不能为负数个单位。

现在的问题是机器人至少以多少能量值开始游戏,才可以保证成功完成游戏?

输入格式

第一行输入整数 N

第二行是 N 个空格分隔的整数,H(1),H(2),,H(N) 代表建筑物的高度。

输出格式

输出一个整数,表示所需的最少单位的初始能量值上取整后的结果。

数据范围

1N,H(i)105

输入样例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都要是一个非零的整数,也就是说在整个跳跃的过程中Ek都要大于等于0,跳到最后一座塔时E可以等于0

  若此时已经跳到第k座塔,剩余的能量为E,第k+1座塔的高度为hk+1

  1. hk+1>E,则E=E(hk+1E)=2Ehk+1
  2. hk+1E,则E=E+(Ehk+1)=2Ehk+1

  所以可以发现,无论后一座塔的高度与当前剩余能量的大小如何,都可以用E=2Ehk+1计算得到跳跃后的能量。

  我们来看看是否可以用二分来做。如果一开始的能量为E0且满足条件(可以跳到最后一座塔),那么对于任何大于E0的数都是满足条件的。

  证明:假设E0为跳到第0座塔后剩余的能量,E1为跳到第1座塔后剩余的能量,以此类推,En为跳到第n座塔后剩余的能量,如果E0是满足条件的,那么对于任何的Ei都是满足Ei0。如果我们取一个E0E0,则有E1=2E0h12E0h1=E10,用数学归纳法可以证明对于任何一项Ei都是大于Ei的,所以对于任何大于E0的数都是满足条件的。

  所以一定存在一个最小的数E0,当EE0时,满足条件,当E<E0,不满足条件,因此答案具有二段性,可以使用二分。

  这个还有一个细节就是,我们每跳一次E都要乘一次2,如果每一座塔的高度都很小(比如都是1),那么我们的答案肯定会爆掉。但我们又可以发现,在跳跃过程中当E达到某一个数值时,那么就可以判定一定可以跳到最后一座塔。这个值就是所有塔中最高的塔的高度。设最大高度为hmax,则当Ehmax,此时对于任何一座塔,2Ehi=E+EhiE+hmaxhiE,也就是说,如果在某一时刻Ehmax,那么之后的跳跃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=2Ehk+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 块是 Hi×Wi 的方格组成的长方形。

为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。

切出的巧克力需要满足:

  1. 形状是正方形,边长是整数
  2. 大小相同

例如一块 6×5 的巧克力可以切出 62×2 的巧克力或者 23×3 的巧克力。

当然小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少么?

输入格式

第一行包含两个整数 NK

以下 N 行每行包含两个整数 HiWi

输入保证每位小朋友至少能获得一块 1×1 的巧克力。

输出格式

输出切出的正方形巧克力最大可能的边长。

数据范围

1N,K105,
1Hi,Wi105

输入样例:

2 10
6 5
5 6

输出样例:

2

 

解题思路

  这题让我们求的是最大边长,就是最大值。我们来看一下如果边长小于等于最大边长是否满足题目的条件,大于最大边长是否不满足题目的条件,如果都是的话,就意味着满足二段性,可以用二分来做。

  如果边长为x,那么一块大小为Hi×Wi的巧克力可以切的块数为Hix×Wix

  我们知道,随着边长的增加,能够切出来的块数就越小,这是一个单调的性质。所以我们应该找到最大的那个边,满足切出的块数要大于等于k

  如果所求的最大边长为xm,那么小于等于xm的边长一定是满足条件的(切出的块一定比k大),而大于xm的边长一定不满足条件(切出的块一定比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 值。

数据范围

1N100

输入样例:

7
ABCDABC

输出样例:

4

 

解题思路

  如果满足条件的最小值为k(当长度为k时,字符串中任何连续的长度为k的子串两两不同),如果有k>k,那么k是否还满足条件呢?我们任意取两段长度为k的子串,对于这两段子串中长度为k的前缀,这两段前缀一定是不同的,这是因为当长度为k时,字符串中任何连续的长度为k的子串两两不同,两段前缀不同,那么这两个字串一定不同。因此当kk,都是满足条件的(长度为k的字串两两不同)。

  由于k是可以取到最小值的,所以当长度小于k时,不满足条件。因此答案具有二段性,可以用二分。

  为了可以快速判断两段字符串是否相同,这里用字符串哈希,因为任何一段子串都可以看作是字符串都某个区间,判断的时间可以达到O(1)

  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/

 
posted @   onlyblues  阅读(192)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
Web Analytics
点击右上角即可分享
微信分享提示