知识点 - 曼哈顿距离最小生成树
解决问题类型:
求解曼哈顿距离上的最小生成树
前置知识
-
曼哈顿距离:给定二维平面上的N个点,在两点之间连边的代价。(即 d i s t a n c e ( P 1 , P 2 ) = ∣ x 1 - x 2 ∣ + ∣ y 1 - y 2 ∣ distance(P1,P2) = |x1-x2|+|y1-y2| distance(P1,P2)=∣x1-x2∣+∣y1-y2∣)
-
曼哈顿距离最小生成树问题求什么?求使所有点连通的最小代价。
-
最小生成树的“环切”性质:在图G = (V, E)中,如果存在一个环,那么把环上的最大边e删除后得到的图 G ’ = ( V , E − e ) G’= (V, E- {e}) G’=(V,E−e)的最小生成树的边权和与G相同。
暴力实现
朴素的算法可以用 O ( N 2 ) O(N^2) O(N2)的Prim,或者处理出所有边做Kruskal,但在这里总边数有 O ( N 2 ) O(N^2) O(N2)条,所以Kruskal的复杂度变成了 O ( N 2 l o g N ) O(N^2logN) O(N2logN)。
但是事实上,真正有用的边远没有 O ( N 2 ) O(N^2) O(N2)条。我们考虑每个点会和其他一些什么样的点连边。
实现
结论:以一个点为原点建立直角坐标系,在每45度内只会向距离该点最近的一个点连边
证明结论:假设我们以点A为原点建系,考虑在y轴向右45度区域内的任意两点 B ( x 1 , y 1 ) B(x1,y1) B(x1,y1)和 C ( x 2 , y 2 ) C(x2,y2) C(x2,y2),不妨设 ∣ A B ∣ ≤ ∣ A C ∣ |AB|≤|AC| ∣AB∣≤∣AC∣(这里的距离为曼哈顿距离),如下图
这种连边方式可以保证边数是 O ( N ) O(N) O(N)的,那么如果能高效处理出这些边,就可以用Kruskal在 O ( N l o g N ) O(NlogN) O(NlogN)的时间内解决问题。下面我们就考虑怎样高效处理边。
我们只需考虑在一块区域内的点,其他区域内的点可以通过坐标变换“移动”到这个区域内。为了方便处理,我们考虑在y轴向右45度的区域。在某个点 A ( x 0 , y 0 ) A(x0,y0) A(x0,y0)的这个区域内的点 B ( x 1 , y 1 ) B(x1,y1) B(x1,y1)满足 x 1 ≥ x 0 x1≥x0 x1≥x0且 y 1 − x 1 > y 0 − x 0 y1-x1>y0-x0 y1−x1>y0−x0。这里对于边界我们只取一边,但是操作中两边都取也无所谓。那么 ∣ A B ∣ = y 1 − y 0 + x 1 − x 0 = ( x 1 + y 1 ) − ( x 0 + y 0 ) |AB|=y1-y0+x1-x0=(x1+y1)-(x0+y0) ∣AB∣=y1−y0+x1−x0=(x1+y1)−(x0+y0)。在A的区域内距离A最近的点也即满足条件的点中 x + y x+y x+y最小的点。因此我们可以将所有点按x坐标排序,再按y-x离散,用线段树或者树状数组维护大于当前点的y-x的最小的x+y对应的点。时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)。
至于坐标变换,一个比较好处理的方法是第一次直接做;第二次沿直线y=x翻转,即交换x和y坐标;第三次沿直线x=0翻转,即将x坐标取相反数;第四次再沿直线y=x翻转。注意只需要做4次,因为边是双向的。
至此,整个问题就可以在 O ( N l o g N ) O(NlogN) O(NlogN)的复杂度内解决了。
一个点把平面分成了8个部分:
由上面的废话可知,我们只需要让这个点与每个部分里距它最近的点连边。
拿R1来说吧:
如图, i i i的 R 1 R1 R1区域里距 i i i最近的点是 j j j。也就是说,其他点k都有: x j + y j < = x k + y k xj + yj <= xk + yk xj+yj<=xk+yk
那么k将落在如下阴影部分:
显然,边 ( i , j ) , ( j , k ) , ( i , k ) (i,j), (j,k), (i,k) (i,j),(j,k),(i,k)构成一个环 < i , j , k > <i,j,k> <i,j,k>,而 ( i , k ) (i,k) (i,k)一定是最长边,可以被删去。所以我们只连边 ( i , j ) (i,j) (i,j)。
为了避免重复加边,我们只考虑R1~R4这4个区域。(总共加了4N条边)
这4个区域的点 ( x , y ) (x,y) (x,y)要满足什么条件?
- 如果点 ( x , y ) (x,y) (x,y)在R1,它要满足: x ≥ x i , y – x ≥ y i – x i x ≥ xi ,y – x ≥ yi – xi x≥xi,y–x≥yi–xi(最近点的x + y最小)
- 如果点 ( x , y ) (x,y) (x,y)在R2,它要满足: y ≥ y i , y – x ≤ y i – x i y ≥ yi ,y – x ≤ yi – xi y≥yi,y–x≤yi–xi(最近点的x + y最小)
- 如果点 ( x , y ) (x,y) (x,y)在R3,它要满足: y ≤ y i , y + x ≥ y i + x i y ≤ yi ,y + x ≥ yi + xi y≤yi,y+x≥yi+xi(最近点的y – x最小)
- 如果点 ( x , y ) (x,y) (x,y)在R4,它要满足: x ≥ x i , y + x ≤ y i – x i x ≥ xi ,y + x ≤ yi – xi x≥xi,y+x≤yi–xi(最近点的y – x最小)
其中一个条件用排序,另一个条件用数据结构(这种方法很常用),在数据结构上询问,找最近点。因为询问总是前缀或后缀,所以可以用树状数组。
例题
模板题:POJ 3241
代码
///#include<bits/stdc++.h>
///#include<unordered_map>
///#include<unordered_set>
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<queue>
#include<bitset>
#include<set>
#include<stack>
#include<map>
#include<list>
#include<new>
#include<vector>
#define MT(a,b) memset(a,b,sizeof(a));
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double pi=acos(-1.0);
const double E=2.718281828459;
const ll mod=1e8+7;
const int INF=0x3f3f3f3f;
int n,k;
struct node{
int x;
int y;
int id;
bool friend operator<(node a,node b){
return a.x==b.x?a.y<b.y:a.x<b.x;
///保证树状数组更新和查询时不会遗漏
}
}point[10005];
struct edge{
int s;
int e;
int c;
bool friend operator<(edge a,edge b){
return a.c<b.c;
}
}load[40000];
int sign=0;
int p[10005];
int find(int x){
return p[x]==x?x:p[x]=find(p[x]);
}
void kruskal(){
for(int i=1;i<=n;i++)
p[i]=i;
sort(load+1,load+1+sign);
int cnt=0;
for(int i=1;i<=sign;i++){
int x=find(load[i].s);
int y=find(load[i].e);
if(x!=y){
cnt++;
p[x]=y;
if(cnt==n-k){
printf("%d\n",load[i].c);
return ;
}
}
}
}
int id[10005]; ///y-x为索引的编号
int xy[10005]; ///y-x为索引 x+y的最小值
void update(int index,int minn,int s) ///index:y-x minn:x+y s:编号
{
index+=1000;
for(int i=index;i>=1;i-=(i&(-i))){
if(xy[i]>minn){
xy[i]=minn;
id[i]=s;
}
}
}
void query(int index,int minn,int s) ///index:y-x minn:x+y s:编号
{
index+=1000;
int e=-1,c=INF;
///现在以编号s为原点,查询y-x>=index的点中x+y的最小值
for(int i=index;i<10000;i+=(i&(-i))){
if(xy[i]<c){
e=id[i];
c=xy[i];
}
}
if(e!=-1)
load[++sign]=edge{s,e,c-minn};
}
void build_edge()
{
/// 以(xi,yi)为原点,对于第1区域内的点(x,y)满足条件
/// x>=xi,y-x>=yi-xi,(x+y)最小
sort(point+1,point+1+n);
memset(id,-1,sizeof(id));
fill(xy,xy+10005,INF);
///按照x升序
///保证后面查询时,x都比当前的x大
for(int i=n;i>=1;i--){
int index=point[i].y-point[i].x;
int minn=point[i].x+point[i].y;
query(index,minn,point[i].id);
update(index,minn,point[i].id);
}
}
int main() ///第K大边
{
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d %d",&point[i].x,&point[i].y);
point[i].id=i;
}
///1象限建边
build_edge();
///2象限建边
for(int i=1;i<=n;i++)
swap(point[i].x,point[i].y);
build_edge();
///3象限建边
for(int i=1;i<=n;i++)
point[i].x=-point[i].x;
build_edge();
///4象限建边
for(int i=1;i<=n;i++)
swap(point[i].x,point[i].y);
build_edge();
kruskal();
return 0;
}