1. 程式人生 > >淺談二分—— by hyl天夢

淺談二分—— by hyl天夢

二分

解決範圍

二分法可以用來解決這一系列具有單調性質的題,例如求單調函式的零點
其實在小學奧數中就用到了二分法
例如手動開根號,再比如猜數遊戲
二分的具體過程就是先取一箇中間值,判定一下正確答案在哪邊,然後接著再二分,直到找到答案為止
二分法的本質是把求解問題轉化成判定問題

優勢

二分相對於暴力列舉來講,判定次數會顯著變少
具體來說,如果暴力列舉期望是O(N)次
那麼二分只需要O(logN)次就可以得出答案

模板

//整數版
while(l<r)
{
    mid=(l+r)/2;
    if(check(mid)) r=mid;
    else l=mid+1;
}

while(l<r)
{
    mid=(l+r+1)/2;//注意+1
    if(check(mid)) l=mid;
    else r=mid-1;
}
//小數版

while(r-l>eps)
{
    mid=(l+r)/2;
    if(check(mid)>0) l=mid;
    else r=mid-eps;
}
//其中eps=1e-6或1e-8;依照題而定

例題

題目1

我的生日要到了!根據習俗,我需要將一些派分給大家。我有N個不同口味、不同大小的派。有F個朋友會來參加我的派對,每個人會拿到一塊派(必須一個派的一塊,不能由幾個派的小塊拼成;可以是一整個派)。
我的朋友們都特別小氣,如果有人拿到更大的一塊,就會開始抱怨。因此所有人拿到的派是同樣大小的(但不需要是同樣形狀的),雖然這樣有些派會被浪費,但總比搞砸整個派對好。當然,我也要給自己留一塊,而這一塊也要和其他人的同樣大小。
請問我們每個人拿到的派最大是多少?每個派都是一個高為1,半徑不等的圓柱體。

程式碼1
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<vector>
#define N 100010
#define ll long long
#define dd double
using namespace std;

int n,f;
dd a[N];
const dd pie=3.1415926535;

bool check(dd mid)
{
    ll sum=0;
    for(int i=1;i<=n;i++)
    {
        sum+=a[i]/mid;
    }
    if(sum>=f) return 1;
    else return 0;
}

int main()
{
    cin>>n>>f;
    dd l=0,r=0;
    for(int i=1;i<=n;i++)
    {
        dd x;
        cin>>x;
        a[i]=x*x*pie*1;
        r=max(a[i],r);
    }
    dd eps=0.001;
    while(r-l>eps)
    {
        dd mid=(l+r)/2.0;
        if(check(mid)) l=mid;
        else r=mid-eps;
    }
    printf("%0.3lf",l);
    return 0;
}
題目2

把一個包含n個正整數的序列劃分為m個連續的子序列(每個正整數恰好屬於一個序列)。設第i個序列的各數之和為S(i),你的任務是讓所有S(i)的最大值儘量小。
例如序列1 2 3 2 5 4劃分成3個序列的最優方案為1 2 3|2 5 |4,其中S(1)、S(2)、S(3)分別為6、7、4,最大值為7;如果劃分成1 2|3 2|5 4,則最大值為9,不如剛才的好。
n<=10^6,所有數之和不超過10^9。

程式碼2
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<vector>
#define N 100010
#define ll long long
using namespace std;

ll n,k,a[N];

bool check(ll mid)
{
    int sum=0,j=1;
    for(int i=1;i<=n;i++)
    {
        sum+=a[i];
        if(a[i]>mid) return 0;
        if(sum>mid)
        {
            j++;
            sum=a[i];
        }
    }
    if(j>k) return 0;
    else return 1;
}

int main()
{
    scanf("%d%d",&n,&k);
    ll l,r=0;
    l=-100;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        l=max(l,a[i]);
        r+=a[i];
    } 
    while(l<r)
    {
        ll mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<l<<endl;
    return 0;
}
題目3

公園裡有n個水塘,需要把這n個水塘中的水排幹,水塘中的水在自然條件下1個單位的時間可以蒸發A升水。現在買了1臺抽水機,使用抽水機可以讓你用1個單位的時間使每個水塘除開自然蒸發的A升水外,還可抽B升水,但在1個單位的時間內只能對1個水塘使用。
要你求出排幹所有水塘的最少時間(水塘中的水為0時為排幹)。

程式碼3
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<vector>
#define N 100010
#define ll long long
using namespace std;

ll n,a[N],A,B;

bool check(ll mid)
{
    int sum=0;
    int jian=mid*A;
    for(int i=1;i<=n;i++)
    {
        int ai=a[i]-jian;
        if(ai>0)
        {
            sum+=ai/B;
            if(ai%B!=0) sum++;
        }
    }
    if(sum>mid) return 0;
    else return 1;
} 

int main()
{
    cin>>n>>A>>B;
    ll l=0,r=0;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        r+=a[i];
    }
    while(l<r)
    {
        ll mid=l+r>>1;
        if(check(mid)) r=mid;
        else l=mid+1;
    }
    cout<<l<<endl;
    return 0;
}
/*
5 3 4
19 12 15 23 7
答案:5
*/ 
題目4

給出兩個長度為n的正整數有序陣列A和B, 在A和B中各任取一個, 可以得到n×n個積. 求第n小的元素。
n<=100000

思路4

判定有多少乘積小於這個答案就可以繼續二分

但是怎麼判定呢?
由於兩個陣列都是有序的,所以A陣列中可行的乘積對應B陣列一定是從頭開始的一段序列,並且範圍逐漸變小

這樣我們O(N)掃一遍,用一個指標維護一下B數組合法位置就可以了

程式碼4
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<vector>
#define N 100010
#define ll long long
#define dd double
using namespace std;

ll n/*,m*/,a[N],b[N];

bool check(ll mid)
{
    int sum=0,j=1,i=n;
    while(i>=1&&j<=n)
    {
        if(a[i]*b[j]<mid)
        {
            sum+=i;
            j++;
        }
        else i--;
    }
    //cout<<sum+1<<endl;
    sum++;
    if(sum<=n) return 1;
    else return 0;
}

int main()
{
    cin>>n;
    ll l=0,r=0;
    ll max1;
    for(int i=1;i<=n;i++) cin>>a[i],r=max(r,a[i]);
    for(int i=1;i<=n;i++) cin>>b[i],max1=max(max1,b[i]);
    r=max1*r;
    //cin>>m;check(m);
    while(l<r)
    {
        ll mid=l+r+1>>1;
        if(check(mid)) l=mid;
        else r=mid-1;
    }
    cout<<l<<endl;
    return 0;
}

/*
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<vector>
#define N 100010
#define ll long long
#define dd double
using namespace std;
ll n,a[N],b[N],c[N],tail;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++) cin>>b[i];
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++)
        c[++tail]=a[i]*b[j];
    sort(c+1,c+tail+1);
    cout<<c[n];
}
*/

/*
5
12 23 112 231 345
23 123 423 2390 8492
答案:2829
5
1 2 3 4 5
2 3 4 5 6
答案:5 
*/
題目5

一年一度的“跳石頭”比賽又要開始了!
這項比賽將在一條筆直的河道中進行,河道中分佈著一些巨大岩石。組委會已經選擇好了兩塊岩石作為比賽起點和終點。在起點和終點之間,有 N 塊岩石(不含起點和終點的岩石)。在比賽過程中,選手們將從起點出發,每一步跳向相鄰的岩石,直至到達終點。
為了提高比賽難度,組委會計劃移走一些岩石,使得選手們在比賽過程中的最短跳躍距離儘可能長。由於預算限制,組委會至多從起點和終點之間移走 M 塊岩石(不能移走起點和終點的岩石)。
求最短跳躍距離的最大值
N,M<=50000

程式碼5
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#define dd double
#define ll long long
#define N 10000100
using namespace std;
ll n,m,a[N];
ll l;

bool check(ll mid)
{
    int now=0;
    int sum=0;
    for(int i=1;i<=n+1;i++)
    {
        if(a[i]-a[now]<mid) sum++;
        else now=i;
    }
    //cout<<mid<<" "<<sum<<endl;
    if(sum<=m) return 1;
    else return 0;
}

int main()
{
    cin>>l>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    a[n+1]=l;
    ll r=l;
    l=0;
    while(l<r)
    {
        ll mid=l+r+1>>1;
        if(check(mid)) l=mid;
        else r=mid-1;
    }
    cout<<l<<endl;
    return 0;
} 

三分

與二分法類似,三分法可以用來解決具有單峰性質的題
三分的具體過程就是先取兩個中間值,分別位於1/3和2/3處,根據單峰性判定一下正確答案在前2/3還是後2/3,然後接著再三分,直到找到答案或答案的近似值為止
二分法每次把答案範圍縮小一半,三分法每次把答案範圍變為原來的2/3,他們的時間複雜度都是O(log(n))的

題目1

在一個2維平面上有兩條傳送帶,每一條傳送帶可以看成是一條線段。兩條傳送帶分別為線段AB和線段CD。小y在AB上的移動速度為P,在CD上的移動速度為Q,在平面上的移動速度R。現在小y想從A點走到D點,他想知道最少需要走多長時間?

思路1

小y走的路徑一定是三條線段組成的折線
如果把離開AB線段的點設為x,到達CD的點設為y,總時間設為z,那麼z是關於x,y的二元函式
可以證明這個函式形如一個山丘,也就是說可以先三分x再三分y求出z的最值

程式碼1

詳見https://www.cnblogs.com/TianMeng-hyl/p/12309214.html