二分算法进阶之三分法

二分法

二分算法一般用于在具有单调性的数组中寻点,它利用了数组中元素的单调性每次使查找范围缩小一半。一般遍历数组寻点的时间复杂度是O(n),而用二分法可以降到O(logn)

例如在下图中存在一些满足单调性的离散点,后一个点一定不小于前一个点
假设A、B、C、D、E、F的值分别为-1、3、3、5、7、9
也就是数组arr[6] = {-1,3,3,5,7,9}
这个时候假如说我要在其中找一个值为7的点,就可以使用二分算法
在这里插入图片描述
具体步骤如下:
设一个左标记left指向数组左端点,也就是left = 0
设一个右标记right指向数组右端点,也就是right = 5
设mid = (left+right)/2,也就是mid = 2
比较arr[mid]与7的大小,发现arr[mid],也就是3比7小,说明在数组中mid左边的点都小于7,不可能存在答案
所以left = mid,把左边标记往右移一半,缩小范围
重复mid = (left+right)/2,也就是mid = 3
比较arr[mid]与7的大小,发现arr[mid],也就是5比7小,说明在数组中mid左边的点都小于7,不可能存在答案。
所以left = mid,把左边标记往右移一半,缩小范围
重复mid = (left+right)/2,也就是mid = 4
比较arr[mid]与7的大小,发现arr[mid]正好等于7,这时候就找到答案了,返回7的数组下标mid作为结果

三分法

三分法一般用于解决凸性函数求极值的问题,它的复杂度同样是O(logn)。
与二分法不同,我们每次取left与right中间的两个三等分点lmid和rmid,然后对比一下lmid对应的点与rmid对应的点的值的大小,每次让边界向值较小的三等分点移动。

也就是说:
如果lmid对应的点大于等于rmid对应的点,那么right = rmid
如果lmid对应的点小于rmid对应的点,那么left = lmid

为什么这样的方法可行呢?
由于凸性函数在最大值(最小值)任意一侧都具有单调性,因此,lmid与rmid中,更大(小)的那个数自然更为靠近最值。所以我们远离最值的那个区间不可能包含最值,可以舍弃。
例如下图中,无论lmid是C点还是D点,EF之间都不可能存在极值点,因为已经有点lmid比他们之间的点大了,所以right = rmid
在这里插入图片描述

下面以例子详细展示三分算法具体操作
假设A、B、C、D、E、F的值分别为1、3、5、7、4、0
也就是数组arr[6] = {1,3,5,7,4,0}
在这里插入图片描述
具体步骤如下:
设一个左标记left指向数组左端点,也就是left = 0
设一个右标记right指向数组右端点,也就是right = 5
设rmid = (left+right)*2/3,也就是rmid = 3
设lmid = (left+right)/3,也就是lmid = 1
arr[lmid] = 3,arr[rmid] = 7
所以left = lmid+1 = 2,接下来
rmid = (left+right)*2/3,也就是rmid = 4
lmid = (left+right)/3,也就是lmid = 2
arr[lmid] = 5,arr[rmid] = 4
所以right = rmid-1 = 3,接下来
rmid = (left+right)*2/3,也就是rmid = 3
lmid = (left+right)/3,也就是lmid = 1
arr[lmid] = 3,arr[rmid] = 7
所以left = lmid+1 = 3
此时发现left = right = 3,所以找到极值点arr[3] = 7

下面我们用一道算法题来巩固一下三分法的学习
题目来源于牛客网

题目描述

众所周知,立体机动装置可以用腰部的发射机射出固定器,固定在巨人身上或是建筑物上后,再利用高压瓦斯将固定器的缆绳给卷回来使身体高速移动,并通过身体的摆动利用这立体的高速机动力来战斗。也就是说它可以让人在墙壁或大地的表面运动。
现在艾伦要使用立体机动装置从多边形城墙上一个顶点运动到墙角另一个顶点, 求最短距离。
给出平面上一个n个点的凸多边形,表示城墙围城的区域。
将这个多边形当成一个直棱柱的底面。给定直棱柱的高h(表示城墙的高度,城墙厚度忽略不计)、上表面上的一个顶点s和下表面上的一个顶点t。
问在不经过直棱柱上表面的情况下,沿直棱柱的表面从s走到的最短距离。

输入描述:

第一行两个整数n(3 ≤ n≤ 10^5),h(1 ≤h≤ 10^6)。
接下来n行,第1行两个实数xi, yi(-10^5 ≤ xi,yi ≤ 10^5),不超过6位小数),表示第i个点的坐标。保证按逆时针顺序给出凸多边形上的点。
最后一行两个整数s,t(1≤ s ≤ t ≤n),表示从上底面的第s个点到下底面的第t个点(按照输入顺序从1到n给点标号)。

输出描述:

一行一个实数,表示最小距离(绝对或相对误差不超过10^-2即会被判为正确)。

题目思路:

因为下表面是可以走的,所以不能直接算s点和t点的直线距离。这个时候就要考虑枚举经过下表面的每一条边,再在每条边上三分求出最短距离。注意最短距离的初始值要设为不经过下表面任何一条边的最短距离。复杂度O(nlogn)。
slen记录每个顶点到s点的距离,getdis是获得上表面或下表面的两点距离,finddis是获得如果经过下表面某个点的路程距离,tridiv三分。因为我的下标是从0开始的,所以s点是p[s-1]。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int s,t,n;
double ans,slen[100001],h;
pair<double,double>p[100001];
inline double getdis(pair<double,double>p1,pair<double,double>p2)
{
    return sqrt((p1.first-p2.first)*(p1.first-p2.first)+(p1.second-p2.second)*(p1.second-p2.second));
}
inline double finddis(pair<double,double>now,int l,int r)
{
    double len = min(slen[l]+getdis(p[l],now),slen[r]+getdis(p[r],now));
    return sqrt(h*h+len*len)+getdis(now,p[t-1]);
}
inline double tridiv(int l,int r)
{
    double minn = 0x3f3f3f3f;
    pair<double,double> left = p[l];
    pair<double,double> right = p[r];
    while(getdis(left,right)>1e-3)
    {
        pair<double,double> first = make_pair((2*left.first+right.first)/3,(2*left.second+right.second)/3);
        pair<double,double> second = make_pair((left.first+2*right.first)/3,(left.second+2*right.second)/3);
        double firdis = finddis(first,l,r);
        double secdis = finddis(second,l,r);
        if(firdis>secdis)
        {
            left = first;
            minn = min(minn,secdis);
        }
        else
        {
            right = second;
            minn = min(minn,firdis);
        }
    }
    return minn;
}
int main()
{
    scanf("%d%lf",&n,&h);
    for(int i = 0;i<n;i++)
        scanf("%lf%lf",&p[i].first,&p[i].second);
    scanf("%d%d",&s,&t);
    double s1 = 0,s2 = 0,sum = 0;
    int last = s-1;
    slen[last] = 0;
    for(int i = s%n;i!=s-1;i++,i%=n)
    {
        double len = getdis(p[last],p[i]);
        sum+=len;
        slen[i] = slen[last]+len;
        last = i;
    }
    sum+=getdis(p[last],p[s-1]);
    for(int i = 0;i<n;i++)
        slen[i] = min(slen[i],sum-slen[i]);
    ans = sqrt(slen[t-1]*slen[t-1]+h*h);
    last = n-1;
    for(int i = 0;i<n;i++,last++,last%=n)
        ans = min(ans,tridiv(last,i));
    printf("%.6lf",ans);
    return 0;
}

欢迎关注微信公众号:Java后台开发

致力于分享原创计算机与软件开发知识及SSM、Spring cloud、Redis、微服务等Java后端开发技术
公众号里还有很多开发工具及学习资料

在这里插入图片描述

原创文章 7 获赞 4 访问量 299

猜你喜欢

转载自blog.csdn.net/zxc375499877/article/details/105767222
今日推荐