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;
}

还要继续加油呀少年~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值