POJ3241 曼哈顿距离最小生成树

=.= 为什么莫队专题里面会有原理的题。。莫队是基于曼哈顿距离最小生成树提出的一种分块暴力(个人理解)算法,以后再说,先说这题。

因为不是图论选手,我图论非常菜,这题也是见都没见过,所以看了很多题解,来总结一下。

虽然说看了很多题解,但你搜搜就会发现,全部的讲解都是 一 模 一 样 的 !一点变动都没有的那种,所以我就直接写轮眼过来了。

http://poj.org/problem?id=3241

/*

曼哈顿距离最小生成树问题可以简述如下:

给定二维平面上的N个点,在两点之间连边的代价为其曼哈顿距离,求使所有点连通的最小代价。

朴素的算法可以用O(N2)的Prim,或者处理出所有边做Kruskal,但在这里总边数有O(N2)条,所以Kruskal的复杂度变成了O(N2logN)。

但是事实上,真正有用的边远没有O(N2)条。我们考虑每个点会和其他一些什么样的点连边。可以得出这样一个结论,以一个点为原点建立直角坐标系,在每45度内只会向距离该点最近的一个点连边。

这个结论可以证明如下:假设我们以点A为原点建系,考虑在y轴向右45度区域内的任意两点B(x1,y1)和C(x2,y2),不妨设|AB|≤|AC|(这里的距离为曼哈顿距离),如下图:

                                                    

|AB|=x1+y1,|AC|=x2+y2,|BC|=|x1-x2|+|y1-y2|。而由于B和C都在y轴向右45度的区域内,有y-x>0且x>0。下面我们分情况讨论:

1.      x1>x2且y1>y2。这与|AB|≤|AC|矛盾;

2.      x1≤x2且y1>y2。此时|BC|=x2-x1+y1-y2,|AC|-|BC|=x2+y2-x2+x1-y1+y2=x1-y1+2*y2。由前面各种关系可得y1>y2>x2>x1。假设|AC|<|BC|即y1>2*y2+x1,那么|AB|=x1+y1>2*x1+2*y2,|AC|=x2+y2<2*y2<|AB|与前提矛盾,故|AC|≥|BC|;

3.      x1>x2且y1≤y2。与2同理;

4.      x1≤x2且y1≤y2。此时显然有|AB|+|BC|=|AC|,即有|AC|>|BC|。

综上有|AC|≥|BC|,也即在这个区域内只需选择距离A最近的点向A连边。

这种连边方式可以保证边数是O(N)的,那么如果能高效处理出这些边,就可以用Kruskal在O(NlogN)的时间内解决问题。下面我们就考虑怎样高效处理边。

我们只需考虑在一块区域内的点,其他区域内的点可以通过坐标变换“移动”到这个区域内。为了方便处理,我们考虑在y轴向右45度的区域。在某个点A(x0,y0)的这个区域内的点B(x1,y1)满足x1≥x0且y1-x1>y0-x0。这里对于边界我们只取一边,但是操作中两边都取也无所谓。那么|AB|=y1-y0+x1-x0=(x1+y1)-(x0+y0)。在A的区域内距离A最近的点也即满足条件的点中x+y最小的点。因此我们可以将所有点按x坐标排序,再按y-x离散,用线段树或者树状数组维护大于当前点的y-x的最小的x+y对应的点。时间复杂度O(NlogN)。

至于坐标变换,一个比较好处理的方法是第一次直接做;第二次沿直线y=x翻转,即交换x和y坐标;第三次沿直线x=0翻转,即将x坐标取相反数;第四次再沿直线y=x翻转。注意只需要做4次,因为边是双向的。

至此,整个问题就可以在O(NlogN)的复杂度内解决了。

*/以上为讲解,下面是略微详解及注释(需要注意的地方)。

 

关于坐标变换:以第一象限为例,第一次可以遍历一半,当x、y翻转以后就可以遍历到第一象限另一半;然后把所有点以x轴对称,便可得到原先第四象限的一半,然后再翻转x、y,便可得到原先第四象限的另一半。那么第二、三象限呢?已经不用遍历了,以第一象限为例,因为我们是对所有点遍历其第一象限,假设以y为原点,x在第三象限,其实相当于以x为原点,y在x第一象限;第二象限也同理。所以我们只需遍历一、四象限即可。

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
struct point
{
    int x,y,id;
    bool operator < (const point& p) const
    {
        if(x!=p.x)
            return x<p.x;
        else
            return y<p.y;
    }
}a[10005];
struct cc
{
    int pos,val;
}tree[1000005];
int b[10005],c[10005];
int tot;
int lowbit(int x){return x&(-x);}
void update(int x,int pos,int val)
{
    int i=x;
    while(i>=1)
    {
        if(tree[i].val>val)
            tree[i].val=val,tree[i].pos=pos;
        i-=lowbit(i);  // 每个当前新增点只对前面有影响,所以减(前缀)
    }
}
int query(int x)
{
    int i=x,val=tree[x].val,pos=tree[x].pos;
    while(i<tot)
    {
        if(tree[i].val<val)
            val=tree[i].val,pos=tree[i].pos;
        i+=lowbit(i); //本题找斜率大于当前点,所以加(后缀)
    }
    return pos;
}
int pa[1000005];
int findset(int x)
{
    return pa[x]!=x?pa[x]=findset(pa[x]):x;
}
void combine(int x,int y)
{
    int u=findset(x),v=findset(y);
    if(u!=v)
        pa[u]=v;
}
struct edge
{
    int u,v,val;
    bool operator < (const edge & x) const
    {
        return val<x.val;
    }
}e[1000005];
int main()
{
    int n,m,i,j,k,ans,num,sum,pos,tott=1;
    scanf("%d%d",&n,&m);
    m=n-m;
    for(i=1;i<=n;i++)
    {
        scanf("%d%d",&a[i].x,&a[i].y);
        pa[i]=i,a[i].id=i;
    }
    for(j=1;j<=4;j++)  //每次45°遍历,因为有重复(a为原点遍历到b跟b为原点遍历到a重复),所以实则每次90(一象限一半+三象限一半,或者四象限一半+二象限一半),4次足以
    {
        if(j==2||j==4)
        {
            for(i=1;i<=n;i++)
                swap(a[i].x,a[i].y);
        }
        else if(j==3)
        {
            for(i=1;i<=n;i++)
                a[i].x=-a[i].x;
        }
        sort(a+1,a+n+1);
        for(i=1;i<=n;i++)
            b[i]=c[i]=a[i].y-a[i].x;
        sort(b+1,b+n+1);
        tot=unique(b+1,b+n+1)-b;
        for(i=1;i<tot;i++)
        tree[i].val=0x3f3f3f3f;
        for(i=n;i>=1;i--)  // 按x从大到小遍历,这样不会错过在第一象限且斜率>1的点。
        {
            num=lower_bound(b+1,b+tot,c[i])-b;
            pos=query(num);
            if(pos!=-1) e[tott++]={a[i].id,a[pos].id,abs(a[pos].x-a[i].x)+abs(a[pos].y-a[i].y)}; // 因为坐标不断变换,我们必须找到一个统一的编号来最后形成最小生成树,所以就以初始编号即可。
            update(num,i,a[i].x+a[i].y);
        }
    }
    sort(e+1,e+tott);
    for(i=1;i<tott;i++)
    {
        int u=findset(e[i].u),v=findset(e[i].v);
        if(u!=v)
        {
            combine(e[i].u,e[i].v);
            m--;
            if(m==0)
            {
                ans=e[i].val;
                break;
            }
        }
    }
    printf("%d\n",ans);
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值