【基礎演算法】第三章 二分演算法
例一 數列分段
題目描述
對於給定的一個長度為N的正整數數列A,現在將其分成M段,並要求每段連續,且每段和的最大值最小。
輸入格式
第1行包含兩個正整數N,M。
第2行包含N個空格隔開的非負整數A。
輸出格式
僅包含一個正整數,即每段和最大值最小為多少。
樣例輸入
5 3
4 2 4 5 1
樣例輸出
6
分析
題中出現類似“最大值最小”的含義,這是答案具有單調性的最常見、最典型 的特徵之一。設最優解為S,因為S的最優性如果要求每段和可以>S,那麼一定存在一種劃分方案使得總段數不超過M。因此答案就處於分段可行性的分界點上。
樣例程式碼
bool check(int limit){
int cnt=1;sum=0;
for(int i=1;i<=n;i++){
if(sum+a[i]<=n;i++){
sum+=a[i];
}
else cnt++,sum=a[i];
}
return cnt<=m;
}
Code
#include<bits/stdc++.h>
using namespace std;
int n,m,a[100005],l,r,mid,ans;
bool check(int x){
int sum=0,num=0;
for(int i=1;i<=n;i++)
{
if(sum+a[i]<=x)
sum+=a[i];
else sum=a[i],num++;
}
return num>=m;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i],l=max(l,a[i]),r+=a[i];
while(l<=r){
mid=l+r>>1;
if(check(mid))
l=mid+1;
else
r=mid-1;
}
cout<<l;
return 0;
}
例二 防具佈置
題目描述
現在有N組防具。 我們可以認為防線是一維的,那麼每一組防具都分佈在防線的某一段上,並且同一組防具是等距離排列的。 也就是說,我們可以用三個整數 和 來描述一組防具,即這一組防具佈置在防線的S,S+D,S+2D...S+KD位置上。 若一個位置上的防具數量為奇數,則我們稱這個位置有破綻,但是整個防線上有且僅有一個位置有破綻或根本沒有破綻。請你求出破綻的位置,或是確定防線沒有破綻。
輸入格式
第一行是一個整數T,表示有T組互相獨立的測試資料。
每組資料的第一行是一個整數N。
之後N行,每行三個整數S,E,D,代表第i組防具的三個引數,資料用空格隔開。
輸出格式
對於每組測試資料,如果防線沒有破綻,輸出一行 There's no weakness.。
否則在一行內輸出兩個空格分隔的整數P和C,表示在P位置 有C個防具。當然C應該是奇數。
樣例輸入
3
2
1 10 1
2 10 1
2
1 10 1
1 10 1
4
1 10 1
4 4 1
1 5 1
6 10 1
樣例輸出
1 1
There's no weakness.
4 3
分析
首先,若S(2^(31)-1)為偶數,則整道防線沒有破綻。
否則,設破綻的位置為P,故只有P上有奇數個防具,其他位置上都有偶數個,則對於x<p,S(x)均為偶數,對於x>=P,S(x)均為奇數。
Code
#include <bits/stdc++.h>
#define N 200010
using namespace std;
int T, n;
long long s[N], e[N], d[N], l, r, mid, ans;
long long f(long long x){
long long sum=0;
for (int i=1;i<=n;i++)
if (s[i]<=x)
sum+=(min(x,e[i])-s[i])/d[i]+1;
return sum;
}
void work ()
{
cin>>n;
for (int i = 1; i <= n; i++)
cin>>s[i]>>e[i]>>d[i];
if (f((long long)2147483647)%2==0){
printf("There's no weakness.\n");
return;
}
l=ans=1,r=(long long)2147483647;
while (l < r)
{
mid = (l + r) / 2;
if (f(mid) % 2 == 1) r = mid;
else l=mid+1;
}
cout<<r<<" "<<f(r)-f(r-1)<<endl;
}
int main()
{
cin>>T;
while(T--)
work ();
return 0;
}
例三 最大均值
題目描述
給定正整數序列A,求一個平均數最大的,長度不小於L的(連續的)子段。
輸入格式
第一行兩個整數N和L。
接下來N行,每行輸入一個正整數A。
輸出格式
輸出一個整數,表示平均值的最大值乘以1000再向下取整之後得到的結果。
樣例輸入
10 6
6
4
2
10
3
8
5
9
4
1
樣例輸出
6500
分析
注意到答案具有單調性,考慮二分答案,判定“是否存在一個長度不小於L的子段,平均值不小於mid”。
如果把序列裡每個數都減去二分的值,就進一步轉化為“是否存在一個長度不小於L的子段,子段和非負”。
然後只需要檢查一下最大子段和是否為非負數,就可以確定二分上下界的變化範圍了。
Code
#include <bits/stdc++.h>
using namespace std;
const double N=1e-5;
int n,L;
double a[100004],b[100004],sum[100004];
bool check(double num){
for(int i=1;i<=n;i++)
b[i]=a[i]-num;
for(int i=1;i<=n;i++)
sum[i]=sum[i-1]+b[i];
double ans=-1e10;
double v=1e10;
for(int i=L;i<=n;i++){
v=min(v,sum[i-L]);
ans=max(ans,sum[i]-v);
}
return ans>=0;
}
int main(){
double l=-1e6,r=1e6;
cin>>n>>L;
for(int i=1;i<=n;i++)
cin>>a[i];
while(l+N<r){
double mid=(l+r)/2;
if(check(mid))
l=mid;
else r=mid;
}
cout<<int (r*1000)<<endl;
}