二分总结

第一部分思考过程

一般写二分的思考顺序是这样的:首先通过题目背景和check(mid)函数的逻辑,判断答案落在左半区间还是右半区间。
左右半区间的划分方式一共有两种:

中点mid属于左半区间,则左半区间是[l, mid],右半区间是[mid+1, r],更新方式是r = mid;或者 l = mid + 1;,此时用第一个模板;
中点mid属于右半区间,则左半区间是[l, mid-1],右半区间是[mid, r],更新方式是r = mid - 1;或者 l = mid;,此时用第二个模板;

二分模板

二分浮点数

#include<bits/stdc++.h>
using namespace std;
int main()
{
    double x;
   cin>>x;
    double l=0;
    double r=x;
    while(r-l>1e-8)
    {
        double mid=(l+r)/2;
        if(mid*mid>=x)
            r=mid;
        else l=mid;
    }
    cout<<l<<endl;
}

有一个经验之谈就是,题目让你保留几位小数,我们比题目多保留两位小数

第三部分应用

二分的基础用法是在单调序列或单调函数中进行查找,因此当问题的答案具有单调性的时候,我们就可以通过二分把求解转化为判定,根据复杂度理论(判定的时间复杂度小于求解)

如果题目说序列单调不减,那么我们很容易就想到二分;

最方便的就是使用stl自带的二分函数

upper_bound和lower_bound

这两个函数的作用是二分查找一个数在数组中出现的位置,区别是upper_bound返回的是数组中第一个大于搜索数的位置而lower_bound返回的是数组中第一个不小于搜素数的位置

函数的用法lower_bound(a.begin(),a.end(),x)-a;

x为要查找的数;
注意在函数的后面要-a
也就是减去地址

在这一题中函数的写法lower_bound(a+1,a+1+n,x);

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
inline int read()
{
    int x=0;
    char c;
    int f=1;
    x=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-')
         f=-1;
        c=getchar();
    }
    while(x>='0'&&x<='9')
    {
        x=x*10,x=x+c-'0';
        c=getchar();
    }
    return x*f;
}
int a[maxn];
int main()
{
 int n=read();
 int m=read();
 for(int i=1;i<=n;i++)
     a[i]=read();
    while(m--)
    {
        int x=read();
        int ans=lower_bound(a+1,a+1+n,x);
        if(x!=a[ans])
            cout<<-1<<" ";
        else cout<<ans<<" ";
    }
    return 0;
}

分析

这一天看了看二分,发现不是特别难。

在学习一个算法的时候我们要明白我们为什么要学习它

因为二分可以使复杂度降低到\(log(n)\),在大数据的情况下,比\(O(n)\)朴素的暴力要好。

什么时候使用

在题目中说到最大值最小,或者说最小值最大,就可以使用二分进行求解

要想使用二分搜索法来解这种「最大值最小化」的题目,需要满足以下三个条件:

  1. 答案在一个固定区间内;

  2. eg:

    比如我们进行抄书,那就暗示着我们二分出来的答案所对应的人数要小于等于总人数,否则就是不合法的,即check函数return tot<=m(tot是当前二分出来的答案所对应的人数,m是总人数)

  3. 可能查找一个符合条件的值不是很容易,但是要求能比较容易地判断某个值是否是符合条件的;

  4. 可行解对于区间满足一定的单调性。换言之,如果\(x\) 是符合条件的,那么有 \(x+1\)或者 \(x-1\)也符合条件。(这样下来就满足了上面提到的单调性)

原理

二分法把一个寻找极值的问题转化成一个判定的问题(用二分搜索来找这个极值)。类比枚举法,我们

当时是枚举答案的可能情况,现在由于单调性,我们不再需要一个个枚举,利用二分的思路,就可以用

更优的方法解决「最大值最小」、「最小值最大」。这种解法也成为是「二分答案」,常见于解题报告

中。

心得

我觉得二分中比较重要的就是check函数

这个函数帮助我们不断逼近答案,

check函数中进行模拟

判断当前$check(int k) $$\ k$是否符合

在主函数中的\(while\)循环里面的条件,根据题面的不同进行修改,比如有的是浮点数,就需要让\(r-l<=0.000001\),记得初始化\(l=0,r=1e9\)(正无穷)

例题

题目背景

大多数人的错误原因:尽可能让前面的人少抄写,如果前几个人可以不写则不写,对应的人输出 0 0

不过,已经修改数据,保证每个人都有活可干。

题目描述

现在要把 m 本有顺序的书分给 k个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。

现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。

输入格式

第一行两个整数 m,k

第二行 m 个整数,第 i个整数表示第 i 本书的页数。

输出格式

共 k行,每行两个整数,第 i 行表示第 i个人抄写的书的起始编号和终止编号。 k 行的起始编号应该从小到大排列,如果有多解,则尽可能让前面的人少抄写。

输入输出样例

输入

9 3

1 2 3 4 5 6 7 8 9

输出

1 5

6 7

8 9

这道题典型的二分题,但是它要求尽可能让前面的人少抄写

这怎么办,我们可以倒着枚举,这样就可以使后面的人抄尽量多的书。

再注意一下细节,这道题就可以A掉了

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=600;
int m,k;
int a[maxn];
bool check(int fuzhi)
{
	int tot=1;
	int cnt=0;
	for(int i=1;i<=m;i++)
	{
		if(a[i]>fuzhi)
		return false;
		if(tot>k)
		return false;
		if(cnt+a[i]<=fuzhi)
		cnt+=a[i];
		else cnt=a[i],tot++;
	}
	return tot<=k;
}
int main(){
	cin>>m>>k;
	for(int i=1;i<=m;i++)
	cin>>a[i];
	int l=0;
	int r=1e9;
	while(l<=r)
	{
		int mid=(l+r)/2;
		if(check(mid))
		r=mid-1;
		else l=mid+1;
	}
	int tail[maxn];
	int head[maxn];
	int cnt=0;
	int tot=k;
	tail[tot]=m;
	head[1]=1;
	for(int i=m;i>=1;i--)
	{
	
	
		if(cnt+a[i]>l)
		{
			cnt=a[i];
			head[tot]=i+1;
			tail[--tot]=i;
		}
		else cnt=cnt+a[i];
	}
	for(int i=1;i<=k;i++)
	cout<<head[i]<<" "<<tail[i]<<endl;
	return 0;
} 
posted @ 2020-08-29 15:07  邦的轩辕  阅读(57)  评论(0)    收藏  举报