基础算法 02_二分法、三分法
更多二分题目戳我
二分是一种思想,而非模板
在什么情况下使用二分?需要同时满足两个条件:上下界[L,R]确定,函数在[L,R]内是单调的。
二分法模板:
int left = ?, right = ?;//给L和R确定边界 while (left < right) { int ans;//ans记录答案 int mid = (left + right) >> 1; if (check(mid)) { ans = mid;//记录答案 ...//移动left(或者right) } else ...//移动right(或者left) }
所以,二分的难点就在于如何建立模型(寻找单调性作为mid)和check()条件(如何缩小边界),在写二分的时候,可能会用上其他的算法或者数据结构,比如在DP中,或者图论中。
二分法的典型应用有:最大值最小化,最小值最大化。
最大值最小化(让最大值尽可能的小)
https://www.luogu.com.cn/problem/P1462
题目:
题目背景 在艾泽拉斯大陆上有一位名叫歪嘴哦的神奇术士,他是部落的中坚力量 有一天他醒来后发现自己居然到了联盟的主城暴风城 在被众多联盟的士兵攻击后,他决定逃回自己的家乡奥格瑞玛 题目描述 在艾泽拉斯,有n个城市。编号为1,2,3,...,n。 城市之间有m条双向的公路,连接着两个城市,从某个城市到另一个城市,会遭到联盟的攻击,进而损失一定的血量。 每次经过一个城市,都会被收取一定的过路费(包括起点和终点)。路上并没有收费站。 假设1为暴风城,n为奥格瑞玛,而他的血量最多为b,出发时他的血量是满的。 歪嘴哦不希望花很多钱,他想知道,在可以到达奥格瑞玛的情况下,他所经过的所有城市中最多的一次收取的费用的最小值是多少。 输入格式 第一行3个正整数,n,m,b。分别表示有n个城市,m条公路,歪嘴哦的血量为b。 接下来有n行,每行1个正整数,fi。表示经过城市i,需要交费fi元。 再接下来有m行,每行3个正整数,ai,bi,ci(1<=ai,bi<=n)。表示城市ai和城市bi之间有一条公路,如果从城市ai到城市bi,
或者从城市bi到城市ai,会损失ci的血量。 输出格式 仅一个整数,表示歪嘴哦交费最多的一次的最小值。 如果他无法到达奥格瑞玛,输出AFK。 输入输出样例 输入 #1复制 4 4 8 8 5 6 10 2 1 2 2 4 1 1 3 4 3 4 3 输出 #1复制 10 说明/提示 对于60%的数据,满足n≤200,m≤10000,b≤200 对于100%的数据,满足n≤10000,m≤50000,b≤1000000000 对于100%的数据,满足ci≤1000000000,fi≤1000000000,可能有两条边连接着相同的城市。
分析:
题目说让求交费最多一次的最小值,是最大值最小化问题,我们二分这个费用。
check函数怎么写呢?
如果有比他大的,就换一条路走,走不通返回false
如果血量消耗完,返回false
但是这样显然是不对的,血量问题没有得到解决,血量够用不够用,
和路费的二分没有与之相匹配的单调性。
----------------------------------------------
我们换种思路来求,还是以路费作为二分的依据。
在所有的路径中,总有一个点的点权最大为x[]
我们要找到x[]中最小的那个x
我们将点权按照从小到大的顺序排个序,然后二分点权
check函数怎么写呢。
我们在二分到某个点权为x的点v的时候,将点权大于x的点删掉
在剩下的点中求一边最短路,如果不能从1走到n,返回false
如果求得的最短路的总边权大于等于我们的血量,返回false
然后都满足,返回true
小计:
这个题,我做了大概有三个小时,分析这个题的时候,并没有多花时间(当然,也是因为看了其他人的博客,找了思路)。思路是对的,但是自己的编码能力确实不行,中间一直在错。 一开始,我想的是将每个点的点权从小到大排个序,然后枚举下标,l=0,r=n+1 。然后如果r在1到n之间的话,就是有解,否则就是没解。但是我忽略了在dijkstra算法的时候筛去哪些大于mid的值的时候,cost的下标早就已经变化了。 还有就是dijkstra用的居然如此的生疏,最开始明明不需要将st标记为true的,我给标记为true了,这里的st数组的作用是如果这个点已经扩展过了其他所有点,那就给他标记上。并不是spfa算法中的标记是否在队列中。 还有一点就是,记录图的时候,用strust还是不熟悉啊,用错了都。
代码:
#include <cstdio> #include <cstring> #include <algorithm> #include <queue> #include <vector> using namespace std; typedef pair<int, int> PII; const int N = 100010, inf = 0x3f3f3f3f; int n, m, blood; int val[N]; int h[N], e[N], ne[N], w[N], tot; void add(int a, int b, int c) { e[++ tot] = b, w[tot] = c, ne[tot] = h[a], h[a] = tot; } int dist[N]; bool st[N]; bool check(int mid) { // printf("%d\n", mid); memset(st, 0, sizeof st); memset(dist, 0x3f, sizeof dist); dist[1] = 0; priority_queue<PII, vector<PII>, greater<PII>> q; q.push({0, 1}); while (q.size()) { PII t = q.top(); q.pop(); int x = t.second; if (st[x]) continue; st[x] = true; if (val[x] > mid) continue; // printf("%d && %d && %d\n", mid, x, val[x]); for (int i = h[x]; ~i; i = ne[i]) { int y = e[i], z = w[i]; if (val[y] > mid) continue; if (dist[y] > dist[x] + z) { dist[y] = dist[x] + z; q.push({dist[y], y}); } } } return dist[n] < blood; } int main() { memset(h, -1, sizeof h); scanf("%d%d%d", &n, &m, &blood); for (int i = 1; i <= n; i ++ ) { int x; scanf("%d", &x); val[i] = x; } for (int i = 1; i <= m; i ++ ) { int x, y, z; scanf("%d%d%d", &x, &y, &z); add(x, y, z), add(y, x, z); } int l = 0, r = inf; if (!check(r)) { puts("AFK"); return 0; } while (l < r) { int mid = l + r >> 1; // puts("--------------------------"); if (check(mid)) r = mid; else l = mid + 1; } printf("%d\n", r); return 0; }
最小值最大化(让最小值尽量大)
题目:
题目描述 Farmer John建造了一个有N(2<=N<=100,000)个隔间的牛棚,这些隔间分布在一条直线上,坐标是x1,...,xN (0<=xi<=1,000,000,000)。 他的C(2<=C<=N)头牛不满于隔间的位置分布,它们为牛棚里其他的牛的存在而愤怒。为了防止牛之间的互相打斗,Farmer John想把这些牛安置在指定的
隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢? 输入格式 第1行:两个用空格隔开的数字N和C。 第2~N+1行:每行一个整数,表示每个隔间的坐标。 输出格式 输出只有一行,即相邻两头牛最大的最近距离。 输入输出样例 输入 #1复制 5 3 1 2 8 4 9 输出 #1复制 3
分析:
要让最小值最大化,我们二分的就是这个最小值,如果所有牛的间距都大于等于mid,就是合法的(那mid就是最小值)
我们的目标就是让这个mid更大,二分就好
代码:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 5 using namespace std; 6 7 const int N = 100010, inf = 0x3f3f3f3f; 8 9 int n, c; 10 int a[N], b[N]; 11 12 bool check(int mid) 13 { 14 int cnt = 1, sum = 0; 15 for (int i = 1; i <= n - 1; i ++ ) 16 { 17 sum += b[i]; 18 if (sum >= mid) 19 { 20 cnt ++; 21 sum = 0; 22 } 23 } 24 25 return cnt >= c; 26 } 27 28 int main() 29 { 30 scanf("%d%d", &n, &c); 31 for (int i = 1; i <= n; i ++ ) 32 { 33 scanf("%d", &a[i]); 34 } 35 36 sort(a + 1, a + 1 + n); 37 for (int i = 1; i < n; i ++ ) 38 { 39 b[i] = a[i + 1] - a[i]; 40 } 41 42 int l = 0, r = inf; 43 while (l < r) 44 { 45 int mid = l + r + 1>> 1; 46 // printf("%d %d %d\n", mid, l, r); 47 if (check(mid)) l = mid; 48 else r = mid - 1; 49 } 50 51 printf("%d\n", r); 52 53 return 0; 54 }
三分法:
三分法常用来求解单峰函数或单谷函数的极值问题。
三分法通过将区间三等分(r - l) / 3,然后让mid1=l + (r - l) / 3, 让mid2 = r - (r - l) / 3;
如果这个函数是先增后减的,那么存在单峰。
如果f(mid1)<f(mid2)说明极值点v一定在mid1的右侧,如果f(mid1)>f(mid2)说明极值点一定在mid2的左侧。
三分法的时间复杂度是O(log(3))的。
https://www.luogu.com.cn/problem/P3382
题目:
题目描述 如题,给出一个 N 次函数,保证在范围 [l,r] 内存在一点 x,使得 [l,x] 上单调增,[x,r] 上单调减。试求出 x 的值。 输入格式 第一行一次包含一个正整数 N 和两个实数 l,r,含义如题目描述所示。 第二行包含 N + 1 个实数,从高到低依次表示该 N 次函数各项的系数。 输出格式 输出为一行,包含一个实数,即为 x 的值。若你的答案与标准答案的相对或绝对误差不超过 10^{-5} 则算正确。 输入输出样例 输入 #1复制 3 -0.9981 0.5 1 -3 -3 1 输出 #1复制 -0.41421 说明/提示 对于 100% 的数据,6<= N <=13,函数系数均在 [−100,100] 内且至多 15 位小数,∣l∣,∣r∣≤10 且至多 15 位小数.l <= r 【样例解释】 如图所示,红色段即为该函数 f(x) = x^3 - 3 x^2 - 3x + 1在区间 [-0.9981, 0.5]上的图像。 当 x = -0.41421 时图像位于最高点,故此时函数在 [l,x] 上单调增,[x,r] 上单调减,故 x=−0.41421,输出 −0.41421。
分析:
实数三分模板题。
代码:
#include <cstdio> using namespace std; int n; double l, r; double a[55]; double f(double mid) { double ans = 0; for (int i = n; i >= 0; i -- ) { ans = ans * mid + a[i]; } return ans; } int main() { scanf("%d%lf%lf", &n, &l, &r); for (int i = n; i >= 0; i -- ) scanf("%lf", &a[i]); while (r - l > 1e-6) { double mid1 = l + (r - l) / 3, mid2 = r - (r - l) / 3; if (f(mid1) > f(mid2)) r = mid2; else l = mid1; } printf("%.5lf\n", r); return 0; }
三分:
[曲线](https://ac.nowcoder.com/acm/contest/951/C)
题目:
链接:https://ac.nowcoder.com/acm/contest/951/C
来源:牛客网题目描述
明明做作业的时候遇到了n个二次函数Si(x)=ax2+bx+c,他突发奇想设计了一个新的函数F(x)=max{Si(x)},i=1…n明明现在想求这个函数在[0,1000]的最小值,要求精确到小数点后四位,四舍五入。
输入描述:
输入包含T组数据,每组第一行一个整数n;
接下来n行,每行3个整数a,b,c,用来表示每个二次函数的3个系数。注意:二次函数有可能退化成一次。输出描述:
每组数据输出一行,表示新函数F(x)的在区间[0,1000]上的最小值。精确到小数点后四位,四舍五入。示例1输入
2 1 2 0 0 2 2 0 0 2 -4 2输出
0.0000 0.5000备注:
对于50%的数据,1≤n≤100;对于100%的数据,1≤T≤10,1≤n≤100000,0≤a≤100,0≤∣b∣≤5000,0≤∣c∣≤5000。
分析:
最大值最小问题
唯一明白的就是F函数就是一个单谷函数。。。明白了这一点,就OK了
代码:
#include <cstdio> #include <algorithm> using namespace std; const int N = 100010, inf = 1 << 30; const double eps = 1e-6; int n; double a[N], b[N], c[N]; double f(double x) { double ans = -inf; for (int i = 1; i <= n; i ++ ) ans = max(ans, a[i] * x * x + b[i] * x + c[i]); return ans; } void work() { scanf("%d", &n); for (int i = 1; i <= n; i ++ ) { scanf("%lf%lf%lf", &a[i], &b[i], &c[i]); } double l = 0, r = 1000; while (r - l > eps) { double d = (r - l) / 3; double mid1 = l + d, mid2 = r - d; if (f(mid1) > f(mid2)) l = mid1; else r = mid2; } printf("%.4lf\n", f(r)); } int main() { int T; scanf("%d", &T); while (T -- ) { work(); } return 0; }