二分法学习总结(5.22)

Posted 未定_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分法学习总结(5.22)相关的知识,希望对你有一定的参考价值。

二分学习心得:
二分法,简单说就是不断缩小解存在的范围,从而求得最优解.。一般通过递归方式查找,查找前一般要先进行排序。
基本模板:

int l=0,r=n;// l为区间下限,r为区间上限
while(l<=r)//根据下面l,r的加一减一情况可能限制l<r
//是否取等取决于区间的变化,只要能让循环终止即可
{
    mid=(l+r)>>1;//取中间值
    if(judge(mid))//判断区间取大取小
        l=mid+1;//是否加一看while限制以及题目要求
    else
        r=mid-1;//是否减一看while限制以及题目要求
}

不做题真不知道自己哪里不会,做题时老是会被一些细节卡住,不是死循环就是超时,还有关于浮点型数的二分,被精度卡死,(=_=),发现一个特点,如果上面的whilr(l<=r),下面应该需要l=mid+1,r=mid-1;如果上面的while(l<r),下面可能有l=mid,r=mid,不一定适用所有情况,具体再因题而定。判断(judge)函数里的返回语句是否有等号也要注意。时间限制上,除2变成>>1,能用变量就不用数组,cin,cout换成scanf,printf等,加上以下语句

ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);

或者自己写输入语句:

inline int read() {
    register char ch=getchar();
    register int x=0;
    for(; ch<'0'||ch>'9'; ch=getchar());
    for(; ch>='0'&&ch<='9'; ch=getchar()) x=(x<<3)+(x<<1)+ch-'0';
    return x;
}

虽然自己写的输入语句较快,但这不是绝对的,有时可能不如scanf快,还取决于代码的写法。

另外:学到一种简单输出变量值的方式,可以用在检查程序运行过程中变量的变化,更简便的输出这个变量的值。

#define dbg(x) cerr<<#x" = "<<x<<endl;

二分做题总结:
D
题意:a,b,c,d四个数组,从每一个数组中挑一个形成一个组合,求最多有多少个组合数为0.

做的第一道二分题,还不太会用,这个题有点类似a+b+c=s那个题。类似方法比较容易想到a+b存一个数组sum1,-(c+d)存一个数组sum2,然后用二分法找sum1的每个数与sum2里的每个数相同的个数。代码还不太会写,写了好久没运行出来T_T。学习了一些其他人的代码实现方法。
法1:先固定sum1,每次从sum1里取一个数,然后在sum2里二分查找是否存在这个数以及存在几个,累加求和。二分函数实现形式。

void find(int a)
{   int l=0,h=k-1,mid;
    while(l<h)
    {   mid=(l+h)/2;
        if(sum2[mid]<a)   
        //必须是小于a不能等于,如果等于的话,此时mid是找到的a值,然后l=mid+1再重新计算区间中值,区间变了,少了一部分a解。
            l=mid+1;//为了找到从小到大排第一个a值
        else h=mid;
        //更新h,即使h=mid=a时,l一定会移动到h处,此时h=l=a。
    }
    while(sum2[l]==a&&l<k)
    //如果第一个l不是a,那么后面的一定都不是,反之,如果第一个是,找后面是否有相等的数。
    {
        ans++;
        l++;
    }
}

详细代码:

#include<iostream>
#include<algorithm>
using namespace std;
long long int a[4005],b[4005],c[4005],d[4005];
long long int sum1[4005*4005],sum2[4005*4005];
int ans=0,k=0;
void find(int a)
{   int l=0,h=k-1,mid;
    while(l<h)
    {   mid=(l+h)/2;
        if(sum2[mid]<a)
            l=mid+1;
        else h=mid;
    }
    while(sum2[l]==a&&l<k)
    {
        ans++;
        l++;
    }
}
int main()
{
    int n,i,j,l=0;
    cin>>n;
    for(i=0; i<n; i++)
    {
        cin>>a[i]>>b[i]>>c[i]>>d[i];
    }
    for(i=0; i<n; i++)
    {   for(j=0; j<n; j++)
        {   sum1[k]=a[i]+b[j];
            sum2[k]=-c[i]-d[j];
            k++;
        }
    }
    sort(sum1,sum1+k);//必须要先排序
    sort(sum2,sum2+k);
    for(i=0; i<k; i++)
    {
        find(sum1[i]);
    }
    cout<<ans<<endl;
    return 0;
}

法2
主函数部分和上面一样,find函数换了一种写法,这里的处理方式和法一不同,在找mid的时候找到的a不一定是数组中出现的第一个a,因为处理时只要遇到a循环就停,a不一定是第一个a,而上一个方法通过l,不断更新找到第一个a,所以法2在处理的时候需要mid两边都判断。

void find(int a)
{   int l=0,h=k-1,mid,p=0,i;
    while(l<=h)//必须有等号,否则会落掉l=h=mid且sum2[mid]=a这种情况。
    {   mid=(l+h)>>1;//移位操作比除以2运行更快
        if(sum2[mid]==a)
        {
            p=1;
            break;
        }
        else if(sum2[mid]<a)
        {   l=mid+1;
        }
        else
        {   h=mid-1;
        }
    }
    if(p)
    {
        for(i=mid-1; i>=0; i--)//判断左边有几个等于a的
        {
            if(sum2[i]==a)
                ans++;
            else break;
        }
        for(i=mid+1; i<k; i++)//判断右边有几个等于a的
        {
            if(sum2[i]==a)
                ans++;
            else break;
        }
        ans++;//加上本身
    }
}

法3:(最简单的方法)
参考博客:二分 折半枚举
用upper_bound 和 lower_bound:

upper_bound(begin, end, value); 
返回>value的元素的第一个位置。
lower_bound(begin, end, value);
返回>=value的元素的第一个位置。

在这里插入图片描述
这样只需要累加个数就行了

#include<iostream>
#include<algorithm>
using namespace std;
long long int a[4005],b[4005],c[4005],d[4005];
long long int sum1[4005*4005],sum2[4005*4005];
int ans=0,k=0;
int main()
{
    int n,i,j,l=0;
    cin>>n;
    for(i=0; i<n; i++)
    {
        cin>>a[i]>>b[i]>>c[i]>>d[i];
    }
    for(i=0; i<n; i++)
    {   for(j=0; j<n; j++)
        {   sum1[k]=a[i]+b[j];
            sum2[k]=-c[i]-d[j];
            k++;
        }
    }
    sort(sum1,sum1+k);
    sort(sum2,sum2+k);
    for(i=0; i<k; i++)
    {
        ans+=(upper_bound(sum1,sum1+k,sum2[i])-lower_bound(sum1,sum1+k,sum2[i]));
    }
    cout<<ans<<endl;
    return 0;
}

H
题意:给了河的长度,石头数量,可删除的石头数量,求解删除指定数量的石头后石头间的最小距离的最大值是多少

二分查找,(一开始我还以为是贪心)
先假设一个距离,这个距离的假设通过二分确定,然后找这个最小距离下可删除的石头个数,不断更新距离直到找到最小距离的最大值。代码解释见下(建议先看代码再看图片)
在这里插入图片描述
关于代码几个细节问题:
•find函数里for循环里的判断语句
if(d[i]-p<now)这里必须是小于不能等于,因为等于最小距离时石头必须保留,留下的石头最小之差才是now
•find函数里的return语句
return res<=m;这里必须小于等于,因为最多删m个,很好理解,另外res的值还可能包括删除河岸L处的石子
•主函数while(l<=r),这里必须有等号,l=r=mid时的情况必须要讨论,其可能是最优解。
ans=mid放在右二分的语句里,因为小于等于m的情况都是合理的,从这里选最优解,所以ans要在右二分(l=mid+1)语句里更新。

#include<iostream>
#include<algorithm>
using namespace std;
long long int L,i,j,n,m,d[50005];
long long int l=0,r=0,mid=0,ans;
bool find(int now)
{
    int res=0,p=0;//res记录删除的个数,p记录指向石头的前一个石头的位置
    for(i=0; i<n; i++)
    {
        if(d[i]-p<now)//计算指向石头与前一个石头位置的差值,如果小于假设的最小位置,就删石头
            res++;
        else p=d[i];//如果大于等于假设的最小位置就保留该石头,并更新p,便于计算下一个石头到该石头的距离
    }
    return res<=m;//最多删m个石头,这里做了一个判断,便于假设距离通过二分更新
}
int main()
{
    cin>>L>>n>>m;
    for(i=0; i<n; i++)
    {   cin>>d[i];
    }
    d[n]=L;
    sort(d,d+n+1);
    r=L;
    while(l<=r)
    {
        mid=(l+r)>>1;
        if(find(mid))//说明假设距离小了,删的石头少,扩大距离l=mid+1
        {   ans=mid;//这时的答案是对的,但不是最优,所以先记录下来,不断更新找到最优解
            l=mid+1;
        }
        else r=mid-1;//假设距离大了,删的石头多了,缩小距离r=mid-1
    }
    cout<<ans<<endl;
    return 0;
}

N
题意:给定一组数据,表示在第几个月存进多少钱,然后给出第 k (1<=k<=12)个月后钱的总数,要求根据这些条件算出月利率(假设月利率是固定不变的)

简单的数学应用题,列出表达式求方程的解即可。本来以为能一次就过的,结果错了好几遍都是时间限制T_T。
时间限制原因:忘记给m数组赋0导致第二个样例不运行;最后一个月,即给出总和的月份存入数组,自定义函数的时候返回值中有带数组的运算,好像挺费时间的,把最后一个月用普通变量就过了。
部分图片正误对比
在这里插入图片描述
正确代码

#include<iostream>
#include<iomanip>
#include<cmath>
#include<cstring>
using namespace std;
int y[13],i,n,p=1,mm,c[10000];
double m[13]= {0},h;
double l,r,mid;
double f(double a)
{   double sum=0.0;
    for(i=y[0]; i<=mm; i++)
    {
        if(m[i]==0)
            sum*=1+a;
        else sum=(sum+m[i])*(1+a);
    }
    return sum-h;
}
int main()
{
    while(cin>>n&&n!=-1)
    {   
        memset(m,0,sizeof(m));
        for(i=0; i<n; i++)
        {   cin>>y[i];
            cin>>m[y[i]];
        }
        cin>>mm>>h;
        l=0.0;
        r=1.0;
        while(1)
        {
            mid=(l+r)/2.0;
            if(fabs(f(mid))<1e-6)
                break;
            else if(f(mid)>0)
            {
                r=mid;
            }
            else
            {
                l=mid;
            }
        }
        if(p>1)
            cout<<endl;
        cout<<"Case "<<p++<<": ";
        cout<<fixed<<setprecision(5)<<mid<<endl;
    }
    return 0;
}

X
题意:给你一个数,找前后最近素数的差。

用到了素数表,没看出来怎么用的二分(゚o゚;
练习一下素数判断方法,还有怎么打印素数表,这个代码还有一个地方挺疑惑的,两个自定义的函数for循环里的i如果放在全局变量里,就是函数里不再重新int不知道为啥就runtime,就像下图(部分图片),两个自定义函数的for循环里的i一个int i,一个直接用全局的i,其余的代码都一样,欢迎大神解答疑惑
在这里插入图片描述
最后附上正确代码

#include <iostream>
#include <cmath>
using namespace std;

int p[1000001],i,l=0;
bool ss(int n)
{
    for(int i=2; i<=sqrt(double(n)); i++)
        if(n%i==0)
            return 0;
    return 1;
}
void b()
{   
    for(int i=2; i<=1299709; i++)
        if(ss(i))
            p[l++]=i;
}
int main()
{
    b();
    int n;
    while(cin>>n&&n)
    {
        for(i=0; i<10000000; i++)
            if(p[i]>=n)
                break;
        if(p[i]==n)
            cout<<0<<endl;
        else
            cout<<p[i]-p[i-1]<<endl;
    }
    return 0;
}

以上是关于二分法学习总结(5.22)的主要内容,如果未能解决你的问题,请参考以下文章

Python学习总结

5.22

线程学习知识点总结

5.22 下午

《算法竞赛进阶指南》学习总结 二分与三分

5.22 3.1