分治算法

· · 个人记录

算法介绍

所谓分治就是指的分而治之,即将较大规模的问题分解成几个较小规模的问题,通过对较小规模问题的求解达到对整个问题的求解。当我们将问题分解成两个较小问题求解时的分治方法称之为二分法。

你们玩过猜数字的游戏吗?你的朋友心里想一个1000以内的正整数,你可以给出一个数字x,你朋友只要回答“比x大”或者“比x小”或者“猜中”,你能保证在10次以内猜中吗?运气好只要一次就猜中。

开始猜测是1到1000之间,你可以先猜500,运气好可以一次猜中,如果答案比500大,显然答案不可能在1到500之间,下一次猜测的区间变为501到1000,如果答案比500小,那答案不可能在500到1000之间,下一次猜测的区间变为1到499。只要每次都猜测区间的中间点,这样就可以把猜测区间缩小一半。因此不超过10次询问区间就可以缩小为1,答案就会猜中了,这就是二分的基本思想。

每一次使得可选的范围缩小一半,最终使得范围缩小为一个数,从而得出答案。假设问的范围是1到n,所以我们只需要问O(logn)次就能知道答案了。

需要注意的是使用二分法有一个重要的前提,就是有序性,下面通过几个例子来体会二分法的应用。

例题

1.找数

描述:

给一个长度为n的单调递增的正整数序列,即序列中每一个数都比前一个数大。有m个询问,每次询问一个x,问序列中最后一个小于等于x的数是什么?

输入:

第一行两个整数n,m。

接下来一行n个数,表示这个序列。

接下来m行每行一个数,表示一个询问。

输出:

输出共m行,表示序列中最后一个小于等于x的数是什么。假如没有输出-1。

样例输入:

5 3

1 2 3 4 6

5

1

3

样例输出:

4

1

3

分析

我们用Left表示询问区间的左边界,用Right表示询问区间的右边界,[Left,Right]组成询问区间。一开始Left=1,Right=n,我们可以把原始序列的左边想象成若干个无穷小的数,把序列的右边想象成无穷大的数,这样比较好理解。序列已经按照升序排好,保证了二分的有序性。

每一次二分,我们这样来做:

①取区间中间值Mid=(Left+Right)/2;

②判断Mid与x的关系,如果a[Mid]>x,由于序列是升序排列,所以区间[Mid,Right]都可以被排除,修改右边界Right=Mid-1;

③如果a[Mid]<=x,修改左边界Left=Mid+1; 重复执行二分操作直到Left>Right。

下面我们来分析答案的情况。循环结束示意图如下:

最终循环结束时一定是Left=Right+1,根据二分第②步的做法我们知道Right的右边一定都是大于x的,而根据第③步我们可以知道Left左边一定是小于等于x的。

所以,一目了然,最终答案是Right指向的数。Right=0就是题目中输出-1的情况。

参考代码

#include <bits/stdc++.h>
using namespace std;
int n,m,a[100005];
int main(){
    scanf("%d %d",&n,&m);
    for (int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    a[0]=-1;
    for(int i=1;i<=m;i++){
       int x;
        int left=1,right=n,mid;
        scanf("%d",&x);
        while(left<=right){//进行二分
            mid = (left + right) / 2;
            if (a[mid] <= x){
                left = mid + 1;
            } else {
                right = mid - 1;
            }           
        }          
        printf("%d\n",a[right]);
    }    
    return 0;
}