二分搜索与三分搜索的应用

二分搜索与三分搜索的应用:


二分和三分都利用了分治的思想,都是通过不断缩小查找的范围,把问题分解为更小的子问题直到找到解为止,,二分的时间复杂度为log2(n),而三分的时间复杂度为3log3(n),两者都是非常高效的。

在解题时经常会遇到二分法与其他算法结合的题目,因此有必要总结一下。


一、二分搜索

(1)、应用二分最常见或者说最基础的的就是从有序序列中查找某个值:查找等于val的位置,大于等于val的第一个位置/值,者大于val的第一个值/位置,在STL中都有相对应的实现,binary_search,Lower_bound, .upper_bound等等。

(2)、通常我们会遇到一些问题,这个问题有单调性的特征,这时便可以使用二分了,举个例子,假设求满足某个条件的最小的x,如果对于x1>x都有x1满足条件的话,那么便可以二分求解x了。也就是说,如果问题具有的特征是,对于某个值x,x右边或者左边的值都满足或者不满足某个条件。二分常见于一些最优化问题的处理,比如说最小化最大值,最大化最小值,最大化平均值等等。

(3)、下面是一些常见的应用,总结不可能是全面的,目的是理解二分的思想。


1、 假定一个解并判断是否可行。          

Poj1064 - Cable master


题意:给出n条线段,以米的单位给出,小数点后两位(精确到厘米),要你对这些线段裁剪,裁剪出m条等长且尽量长的线段,并且让这些线段尽可能长另外线段的长度不能小于1厘米,如果筹不够m条,输出0.00

分析:和poj3122是一样的,每次假定一个解,判断是否符合要求.对于当前假定的长度len,如果以这种方式去切割时不能满足分出m条线段来则证明len太大,解存在于左区间,否则丢弃左边的区间继续去搜索更大的len

注意题目要求不能四舍五入,所以需要丢弃小数点两位以后的部分。

#include 
#include 
#include 
#include 

using namespace std;
double len[10010];
const double EPS= 1e-6;

int main()
{
    int n, k;
    scanf("%d %d", &n, &k);
    double sum = 0;
    for (int i = 0; i < n; i++) scanf("%lf", &len[i]), sum += len[i];
    double l = 0.0, r = sum/k;
    while (r - l >= EPS){
        double m = l + (r-l)/2;
        int num = 0;
        for (int i = 0; i < n; i++) num += (int)(len[i] / m);
        num >= k ? l = m : r = m;
    }
    printf("%.2f\n", floor(r*100) / 100);
    return 0;
}

2、 二分最大化最小值

Poj2456 - Aggressive cow


题意:有n个牛舍,第i个牛舌在xi的位置上面,但是m头牛会互相攻击,因此要使得最近的两头牛之间的距离尽量大,求这个距离。

分析:很明显,枚举所有可能的距离分别判断一下是否能放m头牛能得出解,但是这样复杂度太高,这时候就要思考如何高效的求解了。问题很明显具有的一个特征是:对于当前的距离x,如果x不满足条件,那么比x大的也不可能满足条件,如果满足的话,那么继续去找更大的x。所以我们就是要求出能使得两头牛之间的距离不小于d的最大的d,直接二分即可。判断函数C(x)为能够使得m头牛之间两两的距离都大于等于d。那么首先对牛舍位置排序,之后从第一个牛舍开始,贪心地放在刚好距离不小于d的另一个牛舍上,看能否放n头牛即可。

 

#include 
#include 
#include 
#include 

using namespace std;
int n, c;
int x[100010];

int cal(int dis)
{
    int k = 0, cnt = 1;
    for (int i = 0; i < n; i++)
        if (x[k] + dis <= x[i]) cnt++, k = i;
    return cnt >= c;
}
int main()
{
    scanf("%d %d", &n, &c);
    for (int i = 0; i < n; i++) scanf("%d", &x[i]);
    sort(x, x+n);
    int l = 0, r = x[n-1] - x[0];
    while (r - l > 1){
        int m = (r + l) / 2;
        cal(m) ? l = m : r = m;
    }
    printf("%d\n", l);
    return 0;
}


Poj3258 - River Hopscotch


题意:一条宽为L的河,有n个石头,然后河的左端为位置0点,右端为位置L点,给定n个石头每个石头的位置,现在要从左端跳至河的右端,只能从一个石头跳至另一个石头上面,然后我们可以去除河中最多m个石头,求去除石头之后一次跳跃的最短距离的最大值是多少?

分析:二分搜索距离,判断的明显就是去掉的石头的数量,跟上道题同样的思路,首先要使得每次跳跃的长度尽可能大的话,那么我们需要去掉尽可能去掉多的石头。那么采取贪心的策略,对于当前假定的距离d,从河的左端开始,每次把离当前的石头的距离小于d的全部去掉,然后跳至刚距离刚好大于d的那个石头,这样一直判断下去直到跳至河的右端,采取这样的贪心策略所跳的次数一定最多,然后去掉的石头数量当然也是最少的,如果此时去掉的石头数量仍然大于m的话,证明这个距离d不可行,而且比d大的也绝对不可行,所以缩小d,否则的话增大d。一直二分直到找到最大的可能的d为止。

可是对于这样的做法,我感觉有一个问题就是:对于当前的距离d,如果按这样的贪心策略来跳的话,会不会导致在要跳至河的右端的最后一次跳跃的时候距离小于d,那么这不就不满足大于等于d的条件了吗?现在还没有想明白这个问题表示有点难以理解,但是这样做可以A,是我想错了吗?

 

#include 
#include 
#include 
using namespace std;

int x[50010];
int n, l, m;
typedef long long LL;
const int INF = 0x3f3f3f3f;

int cal(int d)
{
    int cnt = 0, last = 0;
    for (int i = 1; i <= n; i++){
        if (x[i] - x[last] < d)  cnt++;
        else last = i;
    }
    return cnt <= m;
}
int main()
{
    scanf("%d %d %d", &l, &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", x+i);
    sort(x+1, x+n+1);
    x[0] = 0, x[++n] = l;
    int le = 0, ri = l+10;
    while (ri - le > 1){
        int m = ri- (ri-le)/2;
        if (cal(m))  le = m;
        else ri = m;
    }
    printf("%d\n", le);
    return 0;
}


3、 最大化平均值

Poj2976 - Dropping tests


题意:n场考试,i场考试有bi,然后得的分数是ai分,最后得的平均分是ai的和除以bi的和乘以100,最多可以去掉k场考试,求去掉k场考试之后平均分最大能多少?

分析:像这种题首先都会想到贪心法求解,但是局部最优不能保证整体最优,贪心策略是错误的,只能另寻他路,一般还需要是分析抓住题目的特征然后以此入手。

满足单调性特征的题目都可以尝试二分搜索,很明显这个题可以二分,二分的最主要一步是确定判断的函数C(x),这题中需要判断的是对于当前假定的平均分x,如何判断是否能达到呢?题目中的变量有两个,可以把两者变成一者来判断。那么求出每个ai-bi*x,如果最大的n-k个相加大于等于0的话证明可行,且继续搜索更大的平均分,否则不可能达到x的平均分,应缩小解的范围。根据上面的原则就可以进行求解了。

 

#include 
#include 
#include 
#include 
#define EPS 1e-5
using namespace std;

const int INF = 0x3f3f3f3f;
typedef long long LL;
const int N = 1010;
int n, k;
int a[N], b[N];
double t[N];

int main()
{
    while (scanf("%d %d", &n, &k), n || k){
        for (int i = 0; i < n; i++) scanf("%d", a+i);
        for (int i = 0; i < n; i++) scanf("%d", b+i);
        double r = 1.0, l = 0.0;
        while (r - l >= EPS){
            double m = r - (r-l)/2;
            for (int i = 0; i < n; i++) t[i] = a[i] - m*b[i];
            sort(t, t + n);
            if (accumulate(t + k, t + n, 0.0) >= 0.0) l = m;
            else r = m;
        }
        printf("%d\n", (int)(l*100 + 0.5));
    }
    return 0;
}



4、二分查找第k

Poj3579 – Median


题意:有n个数组成的一个序列,对于∣Xi - Xj (1≤ i  j ≤ N),这样的数总共C(N,2)个,求出值处在最中间的那个数。

分析:直接枚举所有的差值明显不可能,时间复杂度过高。二分搜索最基本的应用便是查找一个序列中的某个值,查找第k大只是一个拓展而已,判断的原则显而易见就是比差值比他大的有Cn,2/2个,为方便进行判断,首先对序列进行排序,对于当前假定的某个值x,因为数据量比较大,直接二分查找,也只要循环一遍,统计大于等于a[i]+x的元素有多少个,就可以进一步确定解的范围了。

 

#include 
#include 
#include 
#define INF 0x3f3f3f3f
#define LL long long
using namespace std;

int n;
int a[100010];
LL tot;

int cal(int k)
{
    LL cnt = 0;
    for (int i = 0; i < n; i++) cnt += a+n - lower_bound(a+i, a+n, a[i]+k);
    return cnt > tot;
}
int main()
{
    while (~scanf("%d", &n)){
        for (int i = 0; i < n; i++) scanf("%d", a+i);
        sort(a, a + n);
        int l = 0, r = a[n-1]-a[0]+10;
        tot = n*(n-1) / 4;
        while (r - l > 1){
            int m = r - (r-l)/2;
            if (cal(m)) l = m;
            else r = m;
        }
        printf("%d\n", l);
    }
    return 0;
}


5、二分最小化最大值

Poj3662 - Telephone Lines


题意:john需通过n根电线杆建立一个电话线系统把农庄连接到电信公司,这些电线杆从1n编号,1号已连接到电话公司,n号就是john的农庄,现在有p对电线杆之间是可以用电缆连接的,然后电信公司可以提供k条电缆,其他的由John提供,求john提供的电缆中最长的那根的长度最短是多少。

分析:直接枚举显然不可能。最小化最大值是一个常见的最值优化目标。问题具有的特征是单调性。长度越长,越可能达成,长度越短,越不可能达成。我们只需要通过二分不断来枚举当前的长度就行了。如何判断当前的长度是否可行呢?题目给定了一个明显的图,点数和边的情况都已输入,如果从1号连接到n号需要的最少的电缆的数量仍然大于k的话,证明当前的长度太短,不可能保证联通,否则继续去搜索尽量短的长度。那么我们只需要找出保证1n号联通的最少电缆数量就好了,如何判断呢?利用动态规划的思想求取最短路就好了,图中所有长度大于等于当前假定的长度的边定为边权1,否则定为边权0,这样就可以求取最少需要多少根电缆了。

 

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

typedef pair p;
const int INF = 0x3f3f3f3f;
typedef long long LL;
struct edge{
    int to, d;
    edge(int a = 0, int b = 0) : to(a), d(b){}
};
vector G[1010];
int dis[1010];

int dijstra(int n, int len)
{
    fill(dis + 2, dis+ n+1, INF);
    priority_queue, greater

> q; q.push(p(0, 1)); while (!q.empty()){ p t = q.top(); q.pop(); int x = t.second; if (dis[x] < t.first) continue; if (x == n) return dis[n]; for (int i = 0; i < G[x].size(); i++){ int to = G[x][i].to, d = G[x][i].d; d = d <= len ? 0 : 1; if (dis[to] > dis[x] + d){ dis[to] = dis[x] + d; q.push(p(dis[to], to)); } } } return INF; } int main() { int n, k, p, ma = 0; scanf("%d %d %d", &n, &p, &k); for (int i = 0; i < p; i++){ int f, t, d; scanf("%d %d %d", &f, &t, &d); ma = max(ma, d); G[f].push_back(edge(t, d)); G[t].push_back(edge(f, d)); } int l = -1, r = ma + 1; while (r - l > 1){ int m = (r+l) / 2; dijstra(n, m) <= k ? r = m : l = m; } r > ma? puts("-1") : printf("%d\n", r); return 0; }


(4)、二分搜索的总结:

1使用二分需要保证上下界包含解的所有范围,不管是最优化问题还是判定问题,最重要的一步都是向判定问题C(x)的转化,即判断当前假定的解是否符合条件。

2、对于整数的二分,循环条件while (r – l) > 1)或者while (r >= l)都可以,怎么方便怎么处理,只要保证那个不是解的端点在初始化时不取可能的解就行了。对于小数的二分,可以用eps来控制,但是精度的控制必须满足条件而且不能取得太小,否则容易进入死循环。可以循环一定的次数来二分,而不通过while (r – l) > EPS)来循环。一般循环100次就可以达到1e-30的精度了。而且最后得到的解就是其中的一个端点的值,是哪个端点就看二分的方式了。

正因为二分的高效和简单,所以二分常与其他算法结合起来,应用也十分普遍。掌握二分的思想是很重要的。

 



二、三分搜索


二分法适用于单调函数求解某点的值,而三分法适用于拟凸函数求解极值。

 

三分与二分的实现时的不同点在于每次会在lr之间选取两个点,分别是:m = l+(r-l)/3=(2l+r)/3 mm = r-(r-l)/3=(l+2r)/3,假设判定函数为c(x),c(m)>c(mm)时证明m更靠近最值点,则令r = mm,否则令l = mm。如果要求取的是极小值,则上面的改为小于即可。


下面是一些三分搜索的常见应用:

1、三分角度


题意:给定n个点,求最小的正方形能够覆盖这n个点,正方形不需要平行坐标轴。输出这个正方形的面积。

分析:一般随角度变化的值都会具有凸函数的性质,极值点总在某点取得。可以知道,不论正方形的角度如何,最小的正方形两条对边上面至少一定会经过一个点,否则总可以把正方形缩小而且也满足条件。

所以我们可以三分正方形的边与x轴正向的夹角a,然后求出旋转坐标系a角之后横坐标和纵坐标的最大值和最小值之差,然后要保证覆盖所有点,取两个坐标的差的较大值即为边长。


#include 
#include 
#include 
#include 
#define INF 0x3f3f3f3f
#define LL long long
#define EPS 1e-8
using namespace std;

int x[50], y[50];
int n;
double cal(double a)
{
    double maxx = -INF, maxy = -INF, minx = INF, miny = INF;
    for (int i = 0; i < n; i++){
        double xx = x[i]*cos(a) - y[i]*sin(a);
        double yy = y[i]*cos(a) + x[i]*sin(a);
        maxx = max(maxx, xx);
        maxy = max(maxy, yy);
        minx = min(minx, xx);
        miny = min(miny, yy);
    }
    return max(maxx - minx, maxy - miny);
}
int main()
{
    int t;
    scanf("%d", &t);
    while (t--){
        scanf("%d", &n);
        for (int i = 0; i < n; i++) scanf("%d %d", x+i, y+i);
        double l = 0, r = acos(-1.0);
        while (r - l >= EPS){
            double m = l + (r-l)/3, mm = r - (r-l)/3;
            if (cal(m) <= cal(mm)) r = mm;
            else l = m;
        }
        printf("%.2f\n", cal(l) * cal(l));
    }
    return 0;
}


2、三分坐标

Poj2420 - A Star not a Tree?

 

题意:二维坐标平面上有n个点,求出一个点使得到这n点的距离之和最小。

分析:只能尝试搜索的解法了。可以知道极值点一定位于这n点当中,怎么求解极值点呢?距离不满足单调性,可以尝试三分搜索极值点。对于两个量同时变化的情况,可以固定一者,枚举另一者求取最优解,那么在这里我们可以进行两次三分,首先三分横坐标x,然后对于每个假定的x,三分搜索当前横坐标下要取得最优值纵坐标应取的值。

#include 
#include 
#include 
#include 
#define INF 0x3f3f3f3f
#define LL long long
#define EPS 1e-2
using namespace std;

int n;
int x[110], y[110];
double maxx, maxy;

double dis(double xx, double yy)
{
    double sum = 0;
    for (int i = 0; i < n; i++) sum += sqrt((x[i]-xx)*(x[i]-xx) + (y[i]-yy)*(y[i]-yy));
    return sum;
}
double cal(double y)
{
    double l = 0, r = maxx;
    while (r - l >= EPS){
        double m = l + (r-l)/3, mm = r - (r-l)/3;
        if (dis(m, y) - dis(mm, y) <= -EPS) r = mm;
        else l = m;
    }
    return dis(l, y);
}
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d %d", x+i, y+i);
    maxx = *max_element(x, x+n), maxy = *max_element(y, y+n);
    double l = 0, r = maxy;
    while (r - l >= EPS){
        double m = l + (r-l)/3, mm = r - (r-l)/3;
        if (cal(m) - cal(mm) <= -EPS) r = mm;
        else l = m;
    }
    printf("%.0f\n", cal(l));
    return 0;
}


3、三分边长

Poj3737 – UmBasketella

 

题意:给出一个圆锥的表面积(侧面积+底面积),求圆锥的最大体积。

分析:三分的入门题,体积关于半径满足凸函数的性质,直接三分底面圆的半径长度就可以了。圆锥的体积公式根据S推导下就可以了。

 

#include 
#include 
#include 
#include 

using namespace std;
double S, r, V, h;
const int INF = 0x3f3f3f3f;
typedef long long LL;
const double PI = acos(-1);
const double EPS = 1e-4;

double cal(double r)
{
    h = sqrt(pow((S - PI*r*r) / (PI*r), 2) - r*r);
    return V = PI/3.0 * r*r*h;
}
int main()
{
    while(~scanf("%lf", &S)){
        double l = 0, r = sqrt(S / PI);
        for (int i = 0; i < 50; i++){
            double m = (2*l + r) / 3,  mm = (l + 2*r) / 3;
            cal(m) < cal(mm) ? l = m : r = mm;
        }
        printf("%.2f\n%.2f\n%.2f\n", V, h, l);
    }
    return 0;
}

4、三分距离

SGU 204/ZOJ2340 Little jumper

Andrew AndrewStankevich’sContest #2 @ACDream D


题意:一只青蛙从给定的ds点出发,首先越过第一面墙上的洞跳至两面墙的中间某一点,然后从该点出发继续越过第二个洞跳至给定的df点,问要完成这次任务两次起跳的速度中的较大值最小是多少?

分析:这题比前面之前几个题要难些了,感觉值得一做,结合了物理知识和数学分析能力还有三分搜索的方法。(感觉做了之后高中的物理平抛运动学公式全部复习了一遍….

首先:对于一次运动,由相应的运动学公式推导可知速度为45度时一定为最小的时候:设水平速度为Vx,竖直速度为Vy,从起跳点到落地点的水平距离为X,那么Vx*Vy=g*X/2,那么当Vx=Vy时速度最小,求得这个时候到达墙壁时的高度,若越过则速度为sqrt(g*x);

如果碰到墙壁,要使得速度尽量小,那么当高度大于洞上方高度则求得高度恰好为t时的速度,如果小于洞下方高度b的时候求得高度恰好为b时的速度。然后,对于两次运动,我们求得两个最小的速度并取其较大者就行了。但是因为速度关于落地点的函数属于凹性函数,我们可以三分查找逼近求解速度的值。

#include 
#include 
#include 
#include 
#include 
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;

const double EPS = 1e-6;
double b1, t1, b2, t2, l, ds, df, g;
double check(double b, double t, double x, double s)
{
    double h = s - s*s/x;
    if (b <= h && h <= t) return g*x;
    if (h > t) h = t;
    if (h < b) h = b;
    double Vx2 = g*s*(x-s) *0.5 / h;
    return Vx2 + g*g*x*x / (4*Vx2);
}
double MAX(double a)
{
    return max(check(b1, t1, a + ds, ds), check(b2, t2, l-a + df, l-a));
}
int main()
{
    while (~scanf("%lf %lf %lf %lf %lf %lf %lf %lf", &b1, &t1, &b2, &t2, &l, &ds, &df, &g)){
        double m, mm, le = 0, ri = l;
        int i = 0;
        while (++i <= 100){
            m = le + (ri-le)/3, mm = ri - (ri-le)/3;
            MAX(m) - MAX(mm) < -EPS? ri = mm : le = m;
        }
        printf("%.4f\n", sqrt(MAX(le)));
    }
    return 0;
}


关于三分搜索的总结:

1、三分应用于最优化问题的求解。在解题时没必要给出证明,只要知道问题不满足单调性,就可以尝试用三分搜索极值点,而且三分整数很少见,因为除非能够证明这种策略是正确的(即完全符合凸函数的性质,但是通常极值点不会在整点取得,如果三分整数,那么函数也不是连续的了),否则很可能会错误,而三分应用在小数中是最常见的,比如说三分角度,三分坐标等等。

2、三分搜索经常应用在数值计算的最优化问题中,对于凸函数极值的计算常常是行之有效的。注意取的两个点最好写成m = (2*l + r) / 3,  mm = (l + 2*r) / 3;而不要写成m = (r+l)/2, mm = (r+m)/2.上面的这题写成后面的那种就会错。同样的,二分最好写成m = l + (r-l)/2;


关于三分与二分的总结就到这里,上面所述的只是自己对于两种搜索查找方式的一点点理解而已,也算是一个总结,我相信凡事总结下总是好的,这样才能收获更多,真正要做到对各种算法运用自如还是需要我们勤加练习。

坚持总会有绽放的一天,加油~

你可能感兴趣的:(总结心得)