引子
有这样一个问题:题目链接
给出一个长度为N的正整数数组,不改变数组元素的顺序,将这N个数分为K组。各组中元素的和分别为S1,S2…Sk。如何分组,使得S1至Sk中的最大值最小?
例如:1 2 3 4 5 6分为3组,{1 2 3} {4 5} {6},元素和为6, 9, 6,最大值为9。也可以分为{1 2 3 4} {5} {6}。元素和为:10 5 6,最大值为10。因此第一种方案更优。并且第一种方案的最大值是所有方案中最小的。输出这个最小的最大值。
- 读过问题发现这是一个最值问题,而且直接求解感觉无从下手,分组的方式多种多样,遍历所有情况显然是不合理的。
- 要解决这个问题,可以利用一种正难则反的思想,直接求取困难,那么能不能转换成从后往前推呢?如果给我们一个答案,比如说我说最大值是10,那我这个是不是答案呢?事实上还可以找到最大值为9的情况,所以像这样一点点的逼近正确答案就是二分答案的思想。
概念
- 为什么要叫二分答案?,因为就是求取答案,通过二分找答案,把合法的答案再二分寻找最优的合法。所以往往问题中会有类似最大的最小值或者最小的最大值之类的字眼,那么这问题大概率就是二分答案了。
- 二分答案大概的模板如下:
while(MIN<=MAX){
int num = 0;
int now = 0;
mid = MIN + ((MAX - MIN) >> 1);//防溢出
if(check(mid)){
MAX = mid-1;
ans = mid;//合法的解
}else{
MIN = mid+1;
}
}
- 找到一个合法的解就把他记录在ans中,在循环结束时ans即为最优解。
- 二分答案思路看起来比较简单,但是这其中的细节实难控制,比如为什么while循环中是<=不是小于?有些网上代码写的是小于啊。为什么MAX = mid - 1,MIN = mid + 1 而不是MAX = mid等等这类问题。对于上述相关问题,这里不做细微讨论,这里有一篇文章讲的很详细文章链接
练习
练习一
- 上面的如果完全掌握了,那么可以做相关的问题,回到最开始的问题
- 题目中讲不改变元素的顺序,那么我们就可以从头开始一个一个计算,如果和超过mid,说明这个地方需要分,如果分组总数超过k说明不合法,否则合法。
- 注意分组是从1开始分的。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 2e5+100;
ll Data[MAXN];
ll low = 0,high = 5e14,mid;
ll ans;
int n,k;
bool check(ll m){
ll sum = 0;
int num = 1;
for(int i=0;i<n;i++){
if(sum + Data[i] > m){
num++;
sum = 0;
}
if(num > k) return false;
sum += Data[i];
}
return true;
}
int main(){
cin>>n>>k;
for(int i=0;i<n;i++) cin>>Data[i];
while(low<=high){
mid = low + ((high - low) >> 1);
if(check(mid)){//合法,是不是最小呢
ans = mid;
high = mid -1;
}else low = mid + 1;
}
cout<<ans;
return 0;
}
附加
- 突然发现这里有一道一样的问题,按照上面的程序交上去第四个点WA了,震惊!不得不说luogu上面题目数据还是很强的
自造样例:
Input:
2 2
1 5
Output:
5
实际输出:1 - 问题在于如果有个数特别大,甚至大于mid,就可能出问题,可以加个特判
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
int main(){
int n, m;
ll l, r, mid, ans;
l = 0;
r = 1e13;
cin>>n>>m;
for(int i=0;i<n;i++) cin>>Data[i];
while(l <= r){
mid = l + ((r - l) >> 1);
ll sum = 0;
int num = 1;
for(int i=0;i<n;i++){
if(Data[i] > mid){
l = Data[i];
goto here;
}
if(sum + Data[i] <= mid){
sum += Data[i];
}else{
num++;
sum = Data[i];
}
}if(num > m) l = mid + 1;
else{
ans = mid;
r = mid - 1;
}
here:;
}
cout<<ans;
return 0;
}
- 但是最好的方法是直接让l等于数组中最大的那一个,这样肯定不会出现那样的情况
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
const double eps = 1e-6;
int Data[MAXN];
bool check(int n, int m, ll mid){
ll sum = 0;
int num = 1;
for(int i=0;i<n;i++){
if(sum + Data[i] <= mid){
sum += Data[i];
}else{
num++;
sum = Data[i];
if(num > m) return false;
}
}
return true;
}
int main(){
int n, m;
ll l, r, mid, ans;
l = 0;
r = 1e13;
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>Data[i];
l = max(l, Data[i] * 1LL);
}
while(l <= r){
mid = l + ((r - l) >> 1);
if(check(n, m, mid)){
ans = mid;
r = mid - 1;
}else l = mid + 1;
}
cout<<ans;
return 0;
}
练习二
这道题是洛谷上的P2678题目链接:跳石头
- 这道题求得是最短跳跃距离的最大值,想到二分答案,需要注意的是我们需要从0跳到终点,所以数组元素应该是从0到终点之间所有石头坐标再加上终点坐标,用一个now指针记录当前的位置,如果发现跳的距离小于mid,说明石头需要移动,贪心的思路,显然我们需要移除右边石头,这样一直移动下去最后和最大移动石头数进行比较,如果大于,说明移动的多了,这说明mid取得太大,需要二分区间取左边;如果小于等于,说明这是合法的解,那么我们取右边,看看有没有更优解,因为问题求的是最短距离的最大值。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <stack>
using namespace std;
const int MAXN = 2e5+100;
int Data[MAXN];
int main(){
int L,N,M;
cin>>L>>N>>M;
for(int i=1;i<=N;i++) cin>>Data[i];
Data[0] = 0;
Data[N+1] = L;
int low,high;
int ans;
low = 0;
high = L;
while(low <= high){
int mid = low + ((high - low) >> 1);
int sum = 0;
int now = 0;
for(int i=1;i<=N+1;i++){
if(Data[i] - Data[now] < mid){
sum++;
}else now = i;
if(sum > M) break;
}
if(sum > M){
high = mid - 1;
}else{
ans = mid;
low = mid + 1;
}
}
cout<<ans;
return 0;
}
练习三
- 这道题思路上和前面两道题都是类似的,二分答案,不断寻找合法的最优解,所不同的是这里面涉及到了double类型的二分答案,这里面就涉及到了精度的问题,如何去控制精度,精度太高可能会超时,精度低可能会错误,这道题在洛谷上对精度没有太高的要求,但是在poj上就不同了。
- 下面是这道题的一般性写法,需要注意的是最终输出结果要求略去小数点两位之后的数而不是四舍五入。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <stack>
using namespace std;
const int MAXN = 2e5+100;
double eps = 1e-8;
double Data[MAXN];
int main(){
int n,k;
cin>>n>>k;
double MAX = 0;
for(int i=0;i<n;i++){
cin>>Data[i];
MAX = max(MAX,Data[i]);
}
double low = 0;
double high = 1e9;
double ans;
double mid;
int T = 100;
while(high - low >eps){
int num = 0;
mid = (low + high) / 2;
for(int i=0;i<n;i++){
num += (int) (Data[i] / mid);
}
if(num < k){
high = mid;
}else low = mid;
}
printf("%.2f",floor(mid*100)/100.0);
return 0;
}
同样的程序在POJ上却不能通过,但是如果用一个for循环来代替while就可以消除high-low的精度问题。
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
#include <stack>
using namespace std;
const int MAXN = 2e5+100;
double eps = 1e-8;
double Data[MAXN];
int main(){
int n,k;
scanf("%d%d",&n,&k);
double MAX = 0;
for(int i=0;i<n;i++){
scanf("%lf",&Data[i]);
MAX = max(MAX,Data[i]);
}
double low = 0;
double high = MAX;
double ans;
double mid;
for(int j=0;j<100;j++){
int num = 0;
mid = (low + high) / 2;
for(int i=0;i<n;i++){
num += floor(Data[i] / mid);
}
if(num < k){
high = mid;
}else low = mid;
}
printf("%.2f",floor(mid*100)/100.0);
return 0;
}
循环一百次,相当于2的100次方,大概是10的-30次方的精度,实测50次循环达到10的-15次方精度也无法通过这道题,更不要说设置的10的-8次方的精度了。
精度问题又是一个新的课题,对于一般的题目来说选取合适的精度就可以达到目的了。
练习四
洛谷P3853
这是一道比较正常的二分答案,练习此题目的在于熟悉自己的二分模板
- 关于while带不带等号。由于区间是闭区间,所以带等号
- 关于mid加减1的问题。闭区间,直接l=mid+1,r=mid-1防止循环出不来
- 需要注意一个地方,如果出现这种情况:开始路标坐标为0,100,空旷指数为50,那么只需要加一个路标就可以了,而不是(100-0)/50=2,所以坐标差整除空旷指数的时候num还要减一
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 2e5+100;
int Data[MAXN];
int main(){
int L,N,K;
cin>>L>>N>>K;
for(int i=0;i<N;i++) {
cin>>Data[i];
}
int l = 0;
int r = L;
int ans = 0;
while(l <= r){
int mid = l + ((r - l) >> 1);
int num = 0;
for(int i=1;i<N;i++){
int tmp = Data[i] - Data[i-1];
if(tmp > mid) {
num += tmp / mid;
if(tmp % mid == 0) num--;
}
}
if(num <= K){
ans = mid;
r = mid - 1;
}else l = mid + 1;
}
cout<<ans;
return 0;
}
练习五
- 这道题需要仔细理解题意,首先需要两个数组a和b,a数组表示设备每秒消耗多少能量,b数组表示设备总共有多少能量,且能量匀速消耗,现在有一个充电器,它每秒钟能充电p,也是匀速充电,将这些电器一起使用,问最多能使用多久才不会有电器电量减为0
- 一个关键点是充电时间任意,这个条件使得问题大大简化,那么可以这样考虑,如果电器能够无限使用,那么每秒钟电器消耗的总电能应该要小于等于充电器每秒钟能充电的电能,这是输出-1的情况;如果不是无限使用,那么需要二分答案,给我一个答案ans,我来判断是否成立,这就需要遍历a数组,如果说在ans时间内,当前电器用电总量不到它开始时的总电量,那么不需要处理,否则需要计算它需要充电的电量,把所有电器需要充电的电量累加起来,与充电器能充电的电量比较,如果大于说明使用时间长了,否则说明使用时间短了
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2e5 + 100;
double eps = 1e-6;
double a[MAXN];
double b[MAXN];
bool check(double ans, int n, double p){
double tot = 0;
for(int i=0;i<n;i++){
if(ans * a[i] <= b[i]) continue;
tot += ans * a[i] - b[i];
}
if(tot < p * ans) return true;//ans合法,且可以继续加大
return false;
}
int main(){
int n;
double p;
double tot = 0;
scanf("%d%lf",&n,&p);
for(int i=0;i<n;i++){
scanf("%lf%lf",a+i,b+i);
tot += a[i];
}
if(tot <= p) printf("-1");
else{
double l = 0;
double r = 1e10;
double mid;
while(r - l > eps){
mid = (l + r) / 2;
if(check(mid, n, p)){
l = mid;
}else r = mid;
}
printf("%.6f",mid);
}
return 0;
}
- 精度控制一下,太小可能TLE