二分推进算法

序:
二分搜索是很常见的一种算法,用来在有序序列中寻找某数(或者小于大于它的最*值的某数)。
二分答案也比较常见,估算出答案范围后二分缩小范围逼近答案
二分推进与二分答案很像,不同之处是二分推进并非得到答案的范围,而是通过缩小答案对应的数据的范围,逼近答案对应的数据(通常求最值)…

举个例子:
平面上有n个点,已知每个点的座标。求一个点m(x,y),使得m与其他所有点的距离和最短。(x,y的精度为0.1,最终的距离和 < 最小值+0.01即可)

思路一:暴力枚举。
由于精度是0.1,所以我们可以从minx -> maxx, miny -> maxy枚举与所有点的距离,取最小。复杂度O(100n^2),n大一点即使忽略常数100(1/0.1*1/0.1)也得TLE。

思路二:二分推进。
尽然我们想到了枚举,而且我们知道最小值的座标是唯一的。(不知道怎么证,凭直觉这应该是显然的…)
既然如此,假设这个点是m(x,y),对于m周边的所有点一定距离和都要大于它的距离和(否则就不是最小了)。
还有一个隐含条件:minx<=x<=maxx; miny<=y<=maxy;
最重要的一点,越接近m的点的距离和应该越接近最小值(答案)。
知道范围,又知道答案的特殊性与数据范围的单调性,那么就可以使用二分推进算法逼近了

思路是这样的:
1.选取范围内的任意一个点。
2.计算它的距离和以及它上下左右±r,也就是(x+r,y),(x,y+r)…四个座标的距离和,如果出现ans < minn,则相当于出现更优解,意味着答案应该更靠近那个点,所以我们将当前点更新为那个点继续操作。
3.可能会出现上下左右4个点都比现在座标大,那看来是幅度太大了,r = r/2重复上一步操作。

伪代码是这样的:

while(步长大于误差)
    {
        flag = true;
        while(flag)
        {
            先不允许下次以当前步长搜索
            for(四个方向)
            {
                计算当前座标的距离和;
                if(当前的答案更小)
                {
                    允许下次继续以当前步长搜索
                    更新当前座标与最小答案
                }
            }
        }
        缩小步长
    }

最终的座标就是答案。

代码实现:

/* About: T1 二分推进 Auther: kongse_qi Date:2017/04/29 */

#include <cstdio>
#include <iostream> 
#include <cmath>
#define maxn 10005
#define INF 0x7fffff
#define eps 0.01
using namespace std;

int n, a[maxn][2];
double maxx, maxy, minx, miny;
double ans[2], cur_ans = INF;

void Read()
{
    double x, y;
    scanf("%d", &n);
    for(int i = 0; i != n; ++i)
    {
        scanf("%d%d", &a[i][0], &a[i][1]);
        x = double(a[i][0]);
        y = double(a[i][1]);
        maxx = max(maxx, x);
        minx = min(minx, x);
        maxy = max(maxy, y);
        miny = min(miny, y);
    }
    return ; 
}

double Cal(double x, double y)
{
    double ans = 0, x1, y1;
    if(x > maxx || y > maxy || x < minx || y < miny)    return cur_ans+1;
    for(int i = 0; i != n; ++i)
    {
        x1 = a[i][0];
        y1 = a[i][1];
        ans += sqrt((x-x1)*(x-x1)+(y-y1)*(y-y1));
    }
    return ans;
}

void Solve()
{
    int ti;
    double r = max(maxx-minx, maxy-miny);
    bool flag;
    double curx = (minx+maxx)/2, cury = (miny+maxy)/2;
    int dicx[] = {0, 1, 0, -1}, dicy[] = {1, 0, -1, 0};
    double ne_ans;
    cur_ans = Cal(curx, cury);
    while(r >= eps)
    {
        flag = true;
        while(flag)
        {
            flag = false;
            for(int i = 0; i != 4; ++i)
            {
                ne_ans = Cal(curx+r*dicx[i], cury+r*dicy[i]);
                if(ne_ans < cur_ans)
                {
                    flag = true;
                    curx += r*dicx[i];
                    cury += r*dicy[i];
                    cur_ans = ne_ans;
                }
            }
        }
        r *= 0.5;
    }
    ans[0] = curx;
    ans[1] = cury;
    return ;
}

int main()
{
    freopen("test.in", "r", stdin);
    Read();
    Solve();
    printf("%.1f %.1f", ans[0], ans[1]);
    return 0;
}

这与启发式搜索很像,但是还没有搞懂怎个启发式。作为与二分答案法的姊妹算法,暴力代码后也可以看看能否通过二分优化。

至此结束。
箜瑟_qi 2017.04.29 23:49

点赞