8.2 暑期集訓—— 二分法

一共10道題,除了最後一題沒想到怎麼用二分,最後用遞歸寫出來之外,其他題目都可以歸爲幾類經典的二分題,這次將題目按類分出
具體按類分比較基礎的講解可以參考《挑戰程序設計》二分法一章
有兩題有點不太熟的放在的最後
可以跳到最後看 primary X-subfactor series *********
和Exams *******

類型一:二分答案驗證是否可行

Pie (poj3122)

題意: 一共n個披薩,加上我(f+1) 個人,喫披薩~
每個人只能從一個披薩(陷阱)中取一塊,已知每個披薩的半徑,每個人取的披薩面積一樣,求可以取到的最大面積
誤差要求不超過10^-3
思路: 二分面積,判斷 求每個披薩能貢獻多少塊,總和大於人數則可行

注意: 精度的問題啊…… WA了好多次,有 兩處 細節看代碼註釋

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
int n,f;
double s[10010];
const double pi=acos(-1);   //用acos(-1) 不能用3.1415926 否則精度不夠WA

bool check(double mid){
    int ans=0;
    for(int i=n-1;i>=0;i--){
        ans+=int(s[i]/mid); //少了個int也wa T T
    }
    return ans>=f;
}


int main(){
    int t;
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&n,&f);
        f++;
        memset(s,0,sizeof(s));
        double l=0;
        double r=0;
        for(int i=0;i<n;i++){
            int rr;
            scanf("%d",&rr);
            s[i]=pi*rr*rr;
            r+=s[i];
        }
        while(fabs(r-l)>0.000001){ //注意精度嗯
            double mid=(l+r)/2.0;
            if(check(mid))
                l=mid;
            else
                r=mid;
           // printf("%lf\n",mid);
        }
        printf("%.4lf\n",l);
    }
    return  0;
}

String game 二分區間

題意: 已知字符串s和字符串t,告訴你數組 a[1],a[2],……,a[n],每個數字對應字符串中一個位置,求最多按數組的順序刪多少個s中的字符,s 仍可以生成 t
思路: 二分位置,如果可行則二分後一段,否則前一段

想清楚二分比不二分優化在哪裏
我不是很理解這有什麼優化的地方…… 而且這個複雜度好像也不太會算啊 ORZ

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
using namespace std;
char str[200100];
int mark[200100];
char estr[200100];
int per[200100];
int n,n2;
int mid2;

bool check(int mid){
    if(mid2<mid) //在上一個二分的基礎上記錄每個位置的字符有沒有被拿走,節約點時間……
    for(int i=mid2;i<=mid;i++){
        int t=per[i]-1;
        mark[t]=0;
    }
    else
    for(int i=mid+1;i<=mid2;i++){
        int t=per[i]-1;
        mark[t]=-1;
    }
    int cnt=0;
    for(int i=0;i<n;i++){
        if(mark[i]){
            if(str[i]==estr[cnt]){ //判斷estr中每個字符在str中能不能按順序得到
                cnt++;
                if(cnt==n2) return true; //判斷完畢
            }
        }
    }
    return false;
}

int main(){
   // freopen("1.txt","r",stdin);
    while(~scanf("%s%s",str,estr)){ //str爲給初始的字符串,estr爲你要生成的字符串
       n=strlen(str);
       n2=strlen(estr);
       memset(mark,-1,sizeof(mark));
       memset(per,0,sizeof(per));
       for(int i=0;i<n;i++) scanf("%d",per+i);
       int l=0,r=n-1;
       int mid=n-1;
        mid2=0;
       while(r-l>1||mid!=l){ //考慮r-l==1的特殊情況,其實應該有更好的寫法,但做的時候還不熟練,先這麼寫了
            mid=(l+r)/2;
            if(check(mid))
                l=mid;
            else
                r=mid;
            mid2=mid;
       }
       if(r==0) printf("0\n");
       else
            printf("%d\n",l+1);
    }
    return 0;
}

Median *****

題意: 給一個n個數的數列,計算數列裏各個數之間的差值的絕對值,形成一個新數列,求新數列的中位數
思路: 二分+貪心, 將初始序列從小到大排序,每次判斷大於mid的值是否大於n*(n-1)個,細節見代碼

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
#include <vector>
#include <set>
using namespace std;
vector<double> s;
int n;
int base;

bool check(double mid){
    int ans=0;
    vector<double>::iterator it1,it2;
    for(it1=s.begin();it1!=s.end();it1++){
        double l=(*it1)+mid;
        it2=lower_bound(s.begin(),s.end(),l); //找到差值大於mid的第一個數的位置,則這個數之後的所有數差值都大於mid
        if(it2!=s.end()); //如果有差值大於mid的數
            ans+=n-(it2-s.begin());
    }
    return ans>=base+1;
}

int main(){
    //freopen("1.txt","r",stdin);
    double l,r;
    while(~scanf("%d",&n)){
        s.clear();
        l=r=0;
        for(int i=0;i<n;i++){
            int t;
            scanf("%d",&t);
            s.push_back(t);
            r=max(r,(double)t);
        }
        sort(s.begin(),s.end());
        base=n*(n-1)/4; //判斷mid的時候,大於mid的數的個數應該爲 base個,這樣mid才爲中間的數
        double mid;
        while(r-l>0.5){
            mid=(l+r)/2;
            if(check(mid))
                l=mid;
            else
                r=mid;
        }
        printf("%d\n",(int)r);
    }
    return 0;
}

Gourmet and Banquet

題意:有N份菜,分別在[ai,bi]時間段內有供應,一位美食家想喫到每樣菜,並且喫每樣菜的時間要相同(喫每道菜的次數不限,比如可在a1-a2時間喫A菜,a3-a4時間再喫一次A菜,這樣喫A菜的總時間爲a4-a3+a2-a1)。求美食家能享受菜品的最大總時間。
思路: 與上面兩題基本沒啥差別,設mid 貪心判斷是否可行即可,代碼在學校的vjudge上

類型二:求最大最小值,最小最大值

River Hopscotch (最小值最大化)

題意: 河中有n+1個石頭,已知它們到起點的距離,第n+1個是終點(不能動)。怎樣去掉這n個石頭中的m個,其間距的最小值可以達到最大。
思路: 二分答案+貪心
每一次貪心 距離前一個石頭小於mid的石頭都需要被拿走,若終點的石頭不能滿足,或者一共需要拿走的石頭超過m個,則false

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <algorithm>
#include <string>
using namespace std;
int rock[50010];
int L,m,n;

bool check(double mid){
    int l=0;
    int cnt=0;
    for(int i=0;i<=n;i++){
        if(rock[i]<l+mid){
            cnt++;
            if(cnt>m||i==n) return false; //需要拿走的超過m個,或者這個需要拿走的石頭在終點,不能被拿走
        }
        else
            l=rock[i];
    }
    return true;
}

int main(){
    while(~scanf("%d%d%d",&L,&n,&m)){
        memset(rock,0,sizeof(rock));
        for(int i=0;i<n;i++)
            scanf("%d",rock+i);
        sort(rock,rock+n);
        rock[n]=L;
        double l=0,r=L;
        double mid=r;
        while(r-l>0.001){ //莫名喜歡用double 型的精度,注意check的時候怎麼對mid的數取整
            mid=(l+r)/2;
            if(check(mid))
                l=mid;
            else
                r=mid;
        }
        printf("%d\n",(int)r);
    }
    return 0;
}

Monthly expense (最大值最小化)

題意: 給你一段數列,將其劃分成連續的m段,求怎樣劃分使m段中最大的數列和 最小
思路: 同上一題 二分+貪心 每一次判斷 細節見代碼

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
int day[100100];
int n,m;

bool check(double mid){
    int l=0;
    int cnt=1;
    for(int i=0;i<n;i++){
        if(l+day[i]>mid){
            cnt++;
            l=day[i];
            if(day[i]>mid) return false;  // 如果單獨一天的資金就超過了mid 的話,false
            if(cnt>m) return false; //需要的劃分大於m
        }
        else
            l=l+day[i];
    }
    return true;
}

int main(){
    double l,r;
    while(~scanf("%d%d",&n,&m)){
        l=r=0;
        memset(day,0,sizeof(day));
        for(int i=0;i<n;i++){
            scanf("%d",&day[i]);
            r+=day[i];
        }
        double mid=r;
        while(r-l>0.001){  //哈哈哈對double 的狂熱愛好
            mid=(r+l)/2;
            if(check(mid))
                r=mid;
            else
                l=mid;
        }
        printf("%d\n",(int)r); //注意不能爲l
    }
    return 0;
}

類三:最大化平均值

Dropping tests (找公式)

題意: 給你數組a,b 從中拋掉k對數據使下列公式最大
這裏寫圖片描述
思路: 設mid爲最大值,將這個公式分解,按照100*a[i]-mid*b[i] 的值從大到小排序,取出前面n-k個值,判斷大於0,則true

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
long long a[1010],b[1010];
int n,k;
double c[1010];

bool check(double mid){
    for(int i=0;i<n;i++)
        c[i]=100.0*a[i]-mid*(double)b[i];
    sort(c,c+n);
    double ans=0;
    for(int i=k;i<n;i++) //這裏是從小到大排序,取最後n-k個數
        ans+=c[i];
    return ans>=0;
}


int main(){
    while(~scanf("%d%d",&n,&k)&&n){
        memset(a,0,sizeof(a));
        memset(b,0,sizeof(b));
        for(int i=0;i<n;i++) scanf("%lld",&a[i]);
        for(int i=0;i<n;i++) scanf("%lld",&b[i]);
        double l=0;
        double r=100;
        double mid=r;
        while(r-l>0.0001){
            mid=(l+r)/2;
            if(check(mid))
                l=mid;
            else
                r=mid;
        }
        printf("%d\n",(int)(r+0.5)); //注意答案要求四捨五入
    }
    return 0;
}

複雜點的?

Exams

題意: 給你n個考試科目編號1~n以及他們所需要的複習時間ai;(複習時間不一定要連續的,可以分開,只要複習夠ai天就行了) 然後再給你m天,每天有一個值di; 其中,di==0代表這一天沒有考試(所以是隻能拿來複習的); 若di不爲0,則di的值就代表第i天可以考編號爲di的科目 ;(當然這一天也可以不考而拿來複習) 。 問你最少能在第幾天考完所有科目,無解則輸出-1。
思路: 代碼即思路

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
#include <map>
using namespace std;
typedef long long LL;
int day[100100];
int a[100100];
int n,m;
map<int,int> mark;

bool check(int mid){
    LL need=0;
    int cnt=0;
    mark.clear();
    for(int i=mid-1;i>=0;i--){

        if(day[i]!=0&&!mark.count(day[i]-1)){
                cnt++;
                int exam=day[i]-1;
                need+=a[exam];
                mark[exam]=1;
            }
        else
                if(need>0) need--;
    }
    if(cnt==m&&need==0) return true;
    return false;
}


int main(){
   // freopen("1.txt","r",stdin);
    while(~scanf("%d%d",&n,&m)){
        memset(day,0,sizeof(day));
        memset(a,0,sizeof(a));
        int l=1;
        for(int i=0;i<n;i++) scanf("%d",&day[i]);
        for(int i=0;i<m;i++) {
                scanf("%d",&a[i]);
        }
        int r=n;
        //l--;

        int mid;
        bool res=false;
        while(r>l){
            mid=(l+r)/2.0;
            //printf("A  %d %d %d %d\n",r,l,mid,n);
            if(check(mid)){
               // printf("TT  %lf %lf %lf\n",l,r,mid);
                res=true;
                r=mid;
            }
            else
                l=mid+1;
        }
        if(check(l))
            printf("%d\n",l);
        else if(check(r))
            printf("%d\n",r);
        else
            printf("-1\n");
    }
    return 0;
}

Primary X-Subfactor Series **********

題意:
定義:subfactor:
1.v爲u的子串
1)不含前導0
2)不能亂序
3)不能自己添加數字
4)至少刪除一個數字
2.v爲u的因數:u%v==0
3.v > 1
給出一個數字n(不超過10億),每次刪去一個他的subfactor,直到沒有subfactor。
使得刪減次數最多。如果存在次數一樣則輸出字典序最小的那個序列

思路: 不知道怎麼二分,剛好學了紫皮上二進制表示子集那節,發現可以用遞歸+記憶花搜索做,就寫了試試看,居然過了= =

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <map>
using namespace std;
map<int,int> pre;  //記錄每一個數後面一個subfactor
map<int,int> cont; //記錄每個數對應的最長subfactor sequencn的長度

int calcu(int n){ //求n的位數
    int len=0;
    while(n){
        n/=10;
        len++;
    }
    return len;
}

int shift(int n,int k,int op){ //op爲1時表示需要考慮前導零,如在計算subfactor的時候,op爲0時 不需要考慮,直接輸出n的k排列時的值
    long long m;
    int ans;
    ans=0,m=1; //m初始化爲1,防止若ans最後有0的時候被跳過 ***
    while(k){
        if(k&1)
            m=m*10+n%10;
        k>>=1;
        n/=10;
    }
    int mm=m;
    while(m!=1){
        ans=ans*10+m%10;
        m/=10;
    }
    if(mm%10==0&&ans&&op) return -1;
    return ans;
}

void progress(int N,int K,int& n,int& k){ 
    n=k=0;
    n=shift(N,K,0);
    k=calcu(n);
    k=(1<<k)-1;
    return;
}

int solve(int N,int K){
    int n,k;
    progress(N,K,n,k); //將K排列的N  轉換成滿排列的n,k的各位都爲1
    if(cont[n]>0) return cont[n]; //記憶化,注意清零。 記憶化之後程序運行速度一下子從十幾秒提升至不到一秒…… orz
    pre[n]=-1; //初始化pre[n]爲-1 
    cont[n]=1;
    if(n==0) { //n爲0時特殊處理
        pre[0]=-1;
        return cont[0];
    }
    for(int i=1;i<k;i++){
        int m=shift(n,i,1); //求n取i排列時候的值
        if(m>0&&m!=1&&n%m==0){
            int t=solve(n,k^i); //注意 k^i 的意義
            if(cont[n]<t+1){
                cont[n]=t+1;
                pre[n]=shift(n,k^i,0); //pre 記錄下一個subfactor的位置,爲-1時表示到底啦
            }else if(cont[n]==t+1){
                pre[n]=min(pre[n],shift(n,k^i,0)); //pre[n]記錄字典序最小的subfactor的值
            }
        }
    }
    return cont[n];
}

int main(){
    //freopen("1.txt","r",stdin);
    int n;
    while(~scanf("%d",&n)&&n){
        pre.clear();
        cont.clear();
        int len=calcu(n); //計算 n的數位
        int k=(1<<len)-1;
        solve(n,k);
        while(1){
            printf("%d", n);
            if(pre[n]==-1) {printf("\n"); break;}
            else{
                printf(" ");
                n=pre[n];
            }
        }
    }
    return 0;
}

還要繼續加油呀少年~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章