二分算法~~~大综合

Posted tech-chen

tags:

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

二分:一个非常神奇的算法:永远记住二分,分的是答案,直接在答案在的区间范围中二分,分出一个值,就判断是不是           答案,并进行转移

二分答案: 

如果已知候选答案的范围[min,max],有时候我们不必通过计算得到答案,只需在此范围内应用“二分”的过程,逐渐靠近答案(最后,得到答案)!


一、何时可以使用“二分答案”

    不是任何题目都适合使用“二分答案”的,我Sam观察到一般有以下的一些特征:


    A. 候选答案必须是离散的 ,且已知答案的范围是:[最小值min, 最大值max] (连续区间上不能进行二分操作)

 

        例如,在题目“Kth Largest 第k大的数”中 ==> 答案是闭区间[a[1]b[1], a[n]b[n]]上的正整数
        例如,在题目“Drying 烘干衣服”中 ==> 烘干时间t∈[0,maxT], maxT=max{a[i]}


    B. 候选答案在区间[min,max]上某种属性一类一类的排列 (这样就能在此属性上进行二分操作 ),各个类别不交错混杂

 

        例如,在题目“Kth Largest 第k大的数”中 ==>

                 (候选答案)第k大的数的值:              a[1]b[1],  ... , a[n]b[n]

                 (属性分类)>这个乘积的数有多少:      n^2-1     ...      0

 

        例如,在题目“Drying 烘干衣服”中 ==>

                 (候选答案)烘干时间:  t=0,  t=1,  t=2,  t=3,  ...,  t=maxT-1,  t=maxT

                 (属性分类)能否烘干:  不能  不能   不能   能     ...    能               能


    C. 容易判断某个点 是否为答案(即二分过程中,mid指向的点是否为答案)
        例如,在题目“Kth Largest 第k大的数”中 ==> λ∈[ a[1]b[1], a[n]b[n] ]

                 对于某个候选答案,如果“>λ的乘积个数"<k   && “>λ-1的乘积个数”≥k ,则答案为λ

 

        例如,在题目“Drying 烘干衣服”中 ==>

                 需要寻找第一个出现的“能”(true),即如果check(mid-1)==false && check(mid)==true ,则答案为mid.

 

1.codevs 1696 奇怪的函数

 时间限制: 1 s
 空间限制: 128000 KB
 题目等级 : 黄金 Gold
题目描述 Description

    自从得到上次的教训后,John的上课态度认真多了,也变得更爱动脑筋了。今天他又学习了一个新的知识:关于 xk 的位数。

    如果x大于0小于l,那么位数=1+小数部分×k,

    如果x≥l,那么位数=trunc(ln(x)/ln(10)×k)+1+小数部分×k。

    根据这些函数知识,他学会了求xk的位数了。但他又想到了另外一个问题,如果已知位数N,能不能求出使得 xk 达到或超过N位数字的最小正整数x是多少?

输入描述 Input Description

    输入一个正整数n(n≤2000000000)。

输出描述 Output Description

    输出使得 xk 达到n位数字的最小正整数x。

样例输入 Sample Input

    11

样例输出 Sample Output

    10

 1 #include<iostream>
 2 using namespace std;
 3 #include<cstdio>
 4 #include<cmath>
 5 #include<cstdlib>
 6 #define inf 2000000000
 7 #define ll long long
 8 inline ll f(ll x)
 9 {
10     return x*(log(x)/log(10))+1;/*求位数的公式,n进制,n就做log的底数,别忘了加1*/
11 }
12 int main()
13 {
14     ll l=1,r=inf;
15     ll n;
16     cin>>n;
17     while(1)
18     {
19         ll mid=(l+r)>>1;
20         if(f(mid)>=n) r=mid;/*确定好二分的边界*/
21         else l=mid+1;
22         if(l==r) 
23         {
24             cout<<l<<endl;
25             break;
26         }
27     }
28     return 0;
29 }

 2.ccsu 1487 数列归并

Description

给定长度为N的序列A,其中1≤N≤100000,1≤A[i] ≤100000。现在要将A分成M段(1≤M≤N),每段有A中的1个或相邻的多个元素构成。例如A={1,3,4,6,7,8}分成3段的一种情况为B={1,(3,4),(6,7,8)}。 
由于将A分成M段的情况有多种,现在要求最大子段和最小的情况。例如上述中B的子段和分别为{1,7,21}. 

Input

每组输入的第一行为N和M,然后N行是序列A,为N个正整数。

Output

输出一个整数,占一行。求最小的情况下的最大子段和。

Sample Input

 

7 5
100
400
300
100
500
101
400

Sample Output

500

Hint

Sample中的分法为:100+400, 300+100, 500, 101, 400. 其中最大子段和为500,且此种情况为多种情况下最小的。 

网上的答案:

 1 #include<iostream>
 2 #include<algorithm>
 3 #define size 100010
 4 #include<cstdio>
 5 using namespace std;
 6 int n,m;
 7 int a[size];
 8 int ok(int x)
 9 {
10     int sum = 0 , t = 0 , i = 0 , flag = 0 ,len;
11     while (i < n)
12     {
13         sum = a[i++];
14         int j=i;
15         while (j < n && sum < x )
16         {
17               sum+=a[j++];
18         }
19         t++;
20         if(sum == x) {flag =1; len = j - i + 1; }/*恰好可以取到最大子段和*/
21         else if( sum > x )  {j --; sum-=a[j];} /*如果加多了*/
22         if( t > m ) return -1;/*当分组数超过m的时候,返回-1*/
23         i=j;
24     }
25     if(flag == 1 && n - len >= m -1 ) return  1;
26     if(flag == 1 && t <= m)  return 1;/*分组数不够,因为我们求的是最大子段和,如果可以求到相应的值,其余的部分够分为m-1份就可以了*/
27     return 0;
28 }
29 int find_ans(int low , int heigh)
30 {
31     int ans = 0 , mid;
32     while ( low <= heigh)
33     {
34         mid = (low + heigh)/2;
35         int temp = ok(mid);
36         if(temp == -1){ low = mid +1 ;}/*分组分多了,说明这个最大值取小了*/
37         else if(temp )   {heigh = mid - 1; ans = mid;}/*分组数刚好够或者不够,因为我们要求的是最大子段和的最小值,所以当恰好分为m份的时候,应该再把区间向左取小才可以*/
38         else  heigh = mid -1 ;/*而且只有当可以取到恰好的子段和时候,才可以更新ans,else 只能判断区间*/
39     }
40     return ans;
41 }
42 int main()
43 {
44     int min,max;
45     while (scanf("%d%d",&n,&m)!=EOF)
46     {
47           min = max = 0;
48           for(int i=0;i<n;i++)  
49           {
50                   scanf("%d",&a[i]);
51                   if(a[i] >  min)  min = a[i];/*确定查询区间*/
52                   max+=a[i];
53           }
54          
55           printf("%d\n",find_ans(min,max));
56     }
57     return 0;
58 }

 我的:

 1 #include<iostream>
 2 using namespace std;
 3 #include<cstdio>
 4 #define N 100000+10
 5 int n,m;
 6 int a[N];
 7 int read()
 8 {
 9     int ans=0;
10     char s;
11     s=getchar();
12     while(s<0||s>9) s=getchar();
13     while(s>=0&&s<=9)
14     {ans=ans*10+s-0;s=getchar();}
15     return ans;
16 }
17 int check(int x)
18 {
19     int sum=0,t=0,i=1,len;
20     bool flag=false;
21     while(i<=n)
22     {
23         sum=a[i];
24         i++;
25         int j=i;
26         while(j<=n&&sum<x)
27         {
28             sum+=a[j];/*分组,一超过最大字段和,就分为一组,并检验是>还是==,因为只有我们把分组尽量的平均才可能实现最大子段和最小*/
29             j++;
30         }
31         t++;
32         if(sum==x){flag=true;len=j-i+1;}
33         else if(sum>x) {j--;sum-=a[j];}
34         if(t>m) return -1;
35         i=j; /*i表示下一个开始加的位置*/
36     }
37     if(flag&&n-len+1>=m) return 1;/*可以取到mid,就回去更新,即使分组不够*/
38     if(flag&&t<=m) return 1;
39     return 0;/*取不到,仅仅取小*/
40 }
41 int search_answer(int l,int r)
42 {
43     int ans=0,mid;
44     while(l<r)
45     {
46         mid=(l+r)>>1;/*取出中点*/
47         int flag=check(mid);
48         if(flag==-1) l=mid+1;/*检验中点,==-1,说明分组数多了,所以要把最大字段和扩大,就把l=mid+1*/
49         else if(flag==1) {r=mid;ans=mid;}/*这表示当前是可以凑出mid,并且分组数<=m的,分组数少了,和可以取到一个最大子段和的时候,因为我们要求最小字段和,所以这种情况下,我们仍要取左区间*/
50         else r=mid;/*取不到mid,是不能来更新答的*/
51     }
52     return ans;
53 }
54 void input()
55 {
56     int minn=(1<<31)-1,maxx=0;
57     for(int i=1;i<=n;++i)
58     {
59           a[i]=read();
60           minn=min(minn,a[i]);
61           maxx+=a[i];/*确定二分查找的范围,从最小的元素到sum*/
62     }
63     printf("%d\n",search_answer(minn,maxx));
64       
65 }
66 int main()
67 {
68     while(scanf("%d%d",&n,&m)==2)
69     {
70         input();
71     }
72     return 0;
73 }

3.codevs 4768 跳石头

 

 时间限制: 1 s
 空间限制: 128000 KB
 题目等级 : 黄金 Gold
 
题目描述 Description

一年一度的“跳石头”比赛又要开始了! 

这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有N块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。 

为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走M块岩石(不能移走起点和终点的岩石)。

 

输入描述 Input Description

输入文件名为 stone.in。 

输入文件第一行包含三个整数L,N,M,分别表示起点到终点的距离,起点和终点之间的岩石数,以及组委会至多移走的岩石数。 

接下来N行,每行一个整数,第i行的整数Di(0 < Di < L)表示第i块岩石与起点的距离。这些岩石按与起点距离从小到大的顺序给出,且不会有两个岩石出现在同一个位置。 

 

输出描述 Output Description

输出文件名为stone.out。 

输出文件只包含一个整数,即最短跳跃距离的最大值。

 

样例输入 Sample Input

25 5 2

11 

14 

17

21

样例输出 Sample Output

4

数据范围及提示 Data Size & Hint

对于20%的数据,0≤M≤N≤10。 对于50%的数据,0≤M≤N≤100。 

对于50%的数据,0≤M≤N≤100。

对于100%的数据,0≤M≤N≤50,000,1≤L≤1,000,000,000。

 


 1 /*注意数据中有n==0&&m==的特殊情况,所以要特判
 2 基本思路:与上面那道题目一致,上面是n个数分为m分,
 3             这个题目是把n+1个数,分为n+1-m份而已;
 4 */
 5 #include<iostream>
 6 using namespace std;
 7 #include<cstdio>
 8 #include<cstring>
 9 #define N 50101
10 int con[N],zum[N];
11 int L,n,m,minn=(1<<31)-1;
12 inline int read()
13 {
14     int ans=0;char s;
15     s=getchar();
16     while(s<0||s>9) s=getchar();
17     while(s>=0&&s<=9)
18     {ans=ans*10+s-0;
19     s=getchar();
20     }
21     return ans;
22 }
23 void input()
24 {
25     L=read();n=read();m=read();
26     int x;
27     con[0]=0;
28     for(int i=1;i<=n;++i)
29     {
30         zum[i]=read();
31         con[i]=zum[i]-zum[i-1];
32         minn=min(con[i],minn);
33     }
34     con[n+1]=L-zum[n];
35     m=n+1-m;
36     n++;
37 }
38 int check(int x)/*二分一个最小值*/
39 {
40     int sum=0,i=1,j;
41     bool flag=false;
42     int fz=0;
43     while(i<=n)
44     {
45         sum=0;
46         j=i;
47         while(sum<x)
48         {
49             sum+=con[j];
50             j++;
51             if(j>n&&sum<x)/*这表明最后一组分的不够了,说明最小值取大了*/
52               return 1; 
53         }
54         if(sum==x)
55         {
56             flag=true;
57         }
58         i=j;
59         fz++;
60         if(flag&&fz>=m) 
61           return 0;
62         if(!flag&&fz>=m) 
63           return -1;
64             /*在能找到x的时候,为了让最小值尽量大,我们要取右区间,同时当我们分组分多了的时候,也要分右区间才可以*/
65     }
66     
67     return 1;
68 }
69 int find_ans(int l,int r)
70 {
71     int mid,ans;
72     while(l<=r)/*注意二分的边界要这么写,l<=r,内部的循环每次是l=mid+1,或者r=mid-1,其他情况可能会死循环*/
73     {
74         mid=(l+r)>>1;
75         int temp=check(mid);
76         if(temp==-1) 
77               l=mid+1;
78         if(temp==0)
79           l=mid+1,ans=mid;
80         if(temp==1) 
81           r=mid-1;
82     }
83     return ans;
84 }
85 int main()
86 {
87 
88     input();
89     if(n==1&&m==1) 
90       printf("%d\n",L);
91     else 
92     printf("%d\n",find_ans(minn,L));
93     
94     return 0;
95 }

4.codevs 1069 关押罪犯

2010年NOIP全国联赛提高组

 时间限制: 1 s
 空间限制: 128000 KB
 题目等级 : 钻石 Diamond
题目描述 Description

S 城现有两座监狱,一共关押着N 名罪犯,编号分别为1~N。他们之间的关系自然也极

不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨

气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之

间的积怨越多。如果两名怨气值为c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并

造成影响力为c 的冲突事件。

每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,

然后上报到S 城Z 市长那里。公务繁忙的Z 市长只会去看列表中的第一个事件的影响力,

如果影响很坏,他就会考虑撤换警察局长。

在详细考察了N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在

两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只

要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。那

么,应如何分配罪犯,才能使Z 市长看到的那个冲突事件的影响力最小?这个最小值是少?

输入描述 Input Description

第一行为两个正整数N 和M,分别表示罪犯的数目以及存在仇恨的罪犯对数。

接下来的M 行每行为三个正整数aj,bj,cj,表示aj 号和bj 号罪犯之间存在仇恨,其怨气值为cj。数据保证且每对罪犯组合只出现一次。

输出描述 Output Description

共1 行,为Z 市长看到的那个冲突事件的影响力。如果本年内监狱

中未发生任何冲突事件,请输出0。

样例输入 Sample Input

4 6

1 4 2534

2 3 3512

1 2 28351

1 3 6618

2 4 1805

3 4 12884

样例输出 Sample Output

3512

数据范围及提示 Data Size & Hint

罪犯之间的怨气值如下面左图所示,右图所示为罪犯的分配方法,市长看到的冲突事件

影响力是3512(由2 号和3 号罪犯引发)。其他任何分法都不会比这个分法更优。

【数据范围】

对于30%的数据有N≤ 15。

对于70%的数据有N≤ 2000,M≤ 50000。

对于100%的数据有N≤ 20000,M≤ 100000。

分类标签 Tags 

二分法 并查集 树结构 大陆地区 NOIP全国联赛提高组 2010年
  1 /*解法:二分+扩展并查集(加权并查集),二分一个最大值,先处理关系坏的,如果推出不可避免的要大于当前的最大值,说明最大值不够了,就二分一个更大的数。*/
  2  /*关于”扩展并查集“:
  3  读了飘过的小牛的“扩展并查集”,对于加权并查集又有了新的理解:
  4 http://blog.csdn.net/niushuai666/article/details/6981689
  5 特别是"关系域更新":
  6 当然,这道题理解到这里思路已经基本明确了,剩下的就是如何实现,在实现过程中,我们发现,更新关系域是一个很头疼的操作,网上各种分析都有,但是都是直接给出个公式,至于怎么推出来的都是一笔带过,让我着实头疼了很久,经过不断的看discuss,终于明白了更新操作是通过什么来实现的。下面讲解一下
  7 仔细再想想,rootx-x 、x-y、y-rooty,是不是很像向量形式?于是我们可以大胆的从向量入手:
  8 tx       ty
  9 |          |
 10 x   ~    y
 11 对于集合里的任意两个元素x,y而言,它们之间必定存在着某种联系,因为并查集中的元素均是有联系的(这点是并查集的实质,要深刻理解),否则也不会被合并到当前集合中。那么我们就把这2个元素之间的关系量转化为一个偏移量(大牛不愧为大牛!~YM)。
 12 由上面可知:
 13 x->y 偏移量0时 x和y同类
 14 x->y 偏移量1时 x被y吃
 15 x->y 偏移量2时 x吃y
 16 有了这个假设,我们就可以在并查集中完成任意两个元素之间的关系转换了。
 17 不妨继续假设,x的当前集合根节点rootx,y的当前集合根节点rooty,x->y的偏移值为d-1(题中给出的询问已知条件)
 18 (1)如果rootx和rooty不相同,那么我们把rooty合并到rootx上,并且更新relation关系域的值(注意:p[i].relation表示i的根结点到i的偏移量!!!!(向量方向性一定不能搞错))
 19     此时 rootx->rooty = rootx->x + x->y + y->rooty,这一步就是大牛独创的向量思维模式
 20     上式进一步转化为:rootx->rooty = (relation[x]+d-1+3-relation[y])%3 = relation[rooty],(模3是保证偏移量取值始终在[0,2]间)
 21 (2)如果rootx和rooty相同(即x和y在已经在一个集合中,不需要合并操作了,根结点相同),那么我们就验证x->y之间的偏移量是否与题中给出的d-1一致
 22     此时 x->y = x->rootx + rootx->y
 23     上式进一步转化为:x->y = (3-relation[x]+relation[y])%3,
 24     若一致则为真,否则为假。
 25 
 26 */
 27 #include<iostream>
 28 using namespace std;
 29 #include<cstdio>
 30 #include<vector>
 31 #include<cstring>
 32 #include<algorithm> 
 33 #define N 20100
 34 #define M 100100
 35 struct Edge{
 36     int u,v,w;
 37 }edge[M];
 38 vector<int>relat[N];
 39 int n,m,maxx=0;
 40 int father[N],val[N];
 41 int read()
 42 {
 43     int ans=0;char s;
 44     s=getchar();
 45     while(s<0||s>9) s=getchar();
 46     while(s>=0&&s<=9) 
 47     {
 48         ans=ans*10+s-0;
 49         s=getchar();
 50     }
 51     return ans;
 52 }
 53 int cmp(Edge p,Edge q)
 54 {
 55     return p.w>q.w;
 56 }
 57 void input()
 58 {
 59     n=read();m=read();
 60     for(int i=1;i<=m;++i)
 61     {
 62         edge[i].u=read();
 63         edge[i].v=read();
 64         edge[i].w=read();
 65         relat[edge[i].u].push_back(edge[i].v);
 66         relat[edge[i].u].push_back(edge[i].w);
 67         relat[edge[i].v].push_back(edge[i].u);
 68         relat[edge[i].v].push_back(edge[i].w);
 69         maxx=max(maxx,edge[i].w);
 70     }
 71     sort(edge+1,edge+m+1,cmp);/*必须先对已有的关系排序,因为这道题目符合贪心策略,先处理关系坏的一定优,防止关系略差的顶掉关系差的*/
 72 以上是关于二分算法~~~大综合的主要内容,如果未能解决你的问题,请参考以下文章

排序算法之归并排序

二分算法~~~大综合

C语言实现一些算法或者函数

归并排序详解

白话经典算法系列之五 归并排序的实现

分治算法之快排和归并