二分法——二分答案

整理题前,我先整理一下关于二分的基础知识

所谓二分:根据mid值和target值的对比来不断缩小区间,最终确定所求值
朴素模板为

while(l<=r)
{
   if(mid>target)
    r=mid-1;
   else
   l=mid+1;
}

对于模板中我们需要注意的有三点
1.while的循环语句中为何这样写
2.为何r和l的更新值要这样写
3.if语句中的判断条件

接下来带着问题去看二分答案的做法

首先

二分答案最经典也是根基的两个问题
1.最小值最大情况
2.最大值最小情况
这两种情况是最为明显的二分答案标志,通常数据量会给到1e5以上就连DP也无法解决。

拿第一种情况来说
我们假设最小值的最大情况就是取得了x,那么对于x+1就不满足题意,而x-1虽然满足题意但不是大情况。显然,我们取得区间时(L,R],每次二分的中心为M=(L+R+1)/2
所以对应的模板也有
最大值最小化的二分区间是左闭右开,[L,R),每次二分的中心为M=(L+R)/2。

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

对应第二种情况,取值x则有x+1满足题意但是并非最小情况,x-1不满足题意,对应区间是[L,R),每次二分的中心为M=(L+R)/2。

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

对应的,如果我们的取值区间是闭区间,那么l=r就是我们还需要的一种情况,但是如果有一端是开口,则l<r作为while中的判断条件。

下面对应例题

1.最小值最大化经典例题
aggressive cows
对于这个check函数,我们是根据目前提供的候选x,看看是否能放置所有奶牛。
而二分答案的写法可以按照模板来,也可以这样

#include <bits/stdc++.h>

using namespace std;
long long n,c,a[100000+10];
bool check(long long mid)
{
    int t=c-1,pre=0;
    for(int i=1;i<=n;i++)
    {
        if(a[i]-a[pre]>=mid)
        {
            t--;
            pre=i;
        }
        if(!t)
            break;
    }
    if(!t)
        return 1;
    return 0;
}
int main()
{
    int t;
    for(cin>>t;t;t--)
    {
        cin>>n>>c;
        for(int i=0;i<n;i++)
            scanf("%lld",&a[i]);
        sort(a,a+n);
        int l=0,r=a[n-1];
        int mid=(l+r)/2;
        while(l<r)
        {
            if(check(mid))
                l=mid+1;
            else
                r=mid-1;
            mid=(l+r)/2;
        }
        cout<<r<<endl;
    }
}

区间最大值最小化
书的复制

思路:我们对于他要求的值,我们先找出来,然后对于这个值再去模拟从后往前分配书本。

#include <bits/stdc++.h>
#define ll long long
#define fastio ios::sync_with_stdio(0);cout.tie(0);cin.tie(0);
#define freopen freopen("in.txt","r",stdin);
#define YES cout<<"YES"<<endl;
#define NO cout<<"NO"<<endl;
using namespace std;

long long n,m;
long long a[505];
int x[505],y[505];				//记录输出

bool check(int s) {				//检查
	int num=1,t=0;
	for(int i=n;i>=1;i--) {		//倒序
		if(t+a[i]>s) t=0,num++;	//换下一人
		t+=a[i];
	}
	return num<=m;				//人数是否足够
}

int find(int low,int high) {	//二分
	int mid;
	while(low+1<high){
		mid=low+(high-low)/2;
		if(check(mid))
			high=mid;
		else
			low=mid;
	}
	return high;				//注意返回high
}

int main()
{
    freopen
	long long low=0,high=0;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
    {
		cin>>a[i];
		high+=a[i];				//上界为所有书的和
		low=max(low,a[i]);		//下界为最厚书的页数
	}
	int s=find(low,high);		//获取最优值
	int t=0,num=1;
	y[1]=n;
	for(int i=n;i>=1;i--)
    {
        if(t+a[i]>s)
        {
           t=0;
           x[num]=i+1;
           num++;
           y[num]=i;
        }
        t+=a[i];
    }
    x[num]=1;
    for(int i=m;i>=1;i--)
        cout<<x[i]<<" "<<y[i]<<endl;
    return 0;
}

二分答案确定固定花费下最大访问个数
地标访问

思路:对于所有给定的地标,我们必然是选定一个区间去访问,那么如何确定这个区间?
排序后,让初始最大值访问点为最后点,最小访问点为第一个点,二分查找。
check函数每次利用候选值枚举右端点,这样就能确定左端点,算出花费时间小于t则合格,注意算花费要分三种情况(因为分了正负方向)

#include <iostream>
#include <algorithm>
#include <stdio.h>
using namespace std;
int t,n,a[50000+10];
bool check(int x)
{
    for(int r=x;r<=n;r++)
    {
        int l=r-x+1;
        if(a[r]<=0)
        {
            if(-a[l]<=t)
                return 1;
        }
        if(a[l]>=0)
        {
            if(a[r]<=t)
                return 1;
        }
        if(a[l]<=0&&a[r]>=0)
        {
            if(min(a[r],-a[l])+a[r]-a[l]<=t)
                return 1;
        }
    }
     return 0;
}
int main()
{
    //freopen("in.txt","r",stdin);
    cin>>t>>n;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    sort(a+1,a+n+1);
    int l=-1,r=n+1;
    while(l+1<r)
    {
       int mid=l+(r-l)/2;
        if(check(mid))
            l=mid;
        else
            r=mid;
    }
    cout<<l<<endl;
    return 0;
}

二分答案优化
逛画展

思路:二分答案仍然超时,这时用到了巧妙地滚动数组的方式来解决
1.我们对于给定候选值可以选定1到mid作为第一个扫描区间,记录下每位画师的作品数,如果这个区间就满足包含所有画师作品的条件,那么直接返回这个方案
2.如果不满足,那么势必要更新,更新就是把1到mid,更新为2到mid+1,3到mid+2,一直到右边界小于n或者有满足条件的方案数出现为止

#include <bits/stdc++.h>
#define ll long long
#define fastio ios::sync_with_stdio(0);cout.tie(0);cin.tie(0);
#define freopen freopen("in.txt","r",stdin);
#define YES cout<<"YES"<<endl;
#define NO cout<<"NO"<<endl;
using namespace std;
int flag[1000000+10],n,m,a[2000+10],cnt;
int ansL,ansR;
bool check(int x)
{
    cnt=0;
    memset(flag,0,sizeof flag);
    for (int i=1;i<=x;i++)
    {
        if (!flag[a[i]])
            cnt++;
        flag[a[i]]++;
    }
    if (cnt==m)
    {
        ansL=1;ansR=x;
        return 1;
    }

    //先记录一个区间


    //滚动向前更新区间
    int y=1;
    while (x<n)
    {
        flag[a[y]]--;
        if (!flag[a[y]]) cnt--;
        y++;
        x++;
        flag[a[x]]++;
        if (flag[a[x]]==1) cnt++;
        if (cnt==m)
        {
            ansL=y;ansR=x;
            return 1;
        }
    }
    return 0;
}
int main()
{
    freopen
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%d",a+i);
    int l=1,r=n;
    while (l<=r)
    {
        int mid=(l+r)/2;
        if (check(mid)) 
        r=mid-1;
        else l=mid+1;
    }
    printf("%d %d\n",ansL,ansR);
    return 0;
}

木材加工
不是很难,,就没太仔细看,主要难点还是在check函数上

#include <bits/stdc++.h>
using namespace std;
long long n, k;
long long a[1000005];

bool f(long long x) {
	long long ans = 0;
	for (int i = 1; i <= n; i++) {
		ans += a[i] / x;
	}
	return ans >= k;
}

int main() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) cin >> a[i];

	long long l = 0, r = 100000001;
	long long mid;

	while (l + 1 < r) {
		mid = (l + r) / 2;
		if (f(mid)) l = mid;
		else r = mid;
	}
	cout << l << endl;
	return 0;
}

下内容我尚未搞得太明白
最长公共子序列
最长公共子序列,如果数据范围在1e4之内我们都可以用线性DP去做,但是这里明显数据卡到了1e5,题目标题还给我来个什么模板呵呵。
ans[i]代表构造长度为i时所需的a串的长度,line[i]代表a串中i这个数的位置。
每当读入第二个序列的数字m时,我们就在ans里用二分查找找到ans[r]<line[m]<ans[l],接着就可以将ans[l]的值降低为line[m]的值。当然,如果说ans中最大的数小于line[m],就可以给最长公共子序列长度加一

#include <bits/stdc++.h>

using namespace std;
int ans[100000+10],line[100000+10];
int main()
{
    int n,m;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&m);
        line[m]=i;
    }
    int total=0;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&m);
        int s=line[m];
        if(s>ans[total])
        {
            ans[++total]=s;
        }
        else
        {
            int l=0,r=total;
            while(l<=r)
            {
                int mid=ans[(l+r)/2];
                if(s>mid)
                    l=(l+r)/2+1;
                else
                    r=(l+r)/2-1;
            }
            ans[l]=s;
        }
    }
    cout<<total<<endl;
}

还有三个题,,都是类似的知识点不再赘述了

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值