P1661 扩散

题目 题目 题目

题目描述

一个点每过一个单位时间就会向四个方向扩散一个距离,如图。

P1661 扩散_第1张图片

两个点a、b连通,记作e(a,b),当且仅当a、b的扩散区域有公共部分。连通块的定义是块内的任意两个点u、v都必定存在路径e(u,a0),e(a0,a1),…,e(ak,v)。给定平面上的n给点,问最早什么时刻它们形成一个连通块。

输入格式

第一行一个数n,以下n行,每行一个点坐标。

【数据规模】

对于20%的数据,满足1≤N≤5; 1≤X[i],Y[i]≤50;

对于100%的数据,满足1≤N≤50; 1≤X[i],Y[i]≤10^9。

输出格式

一个数,表示最早的时刻所有点形成连通块。

样例 #1

样例输入 #1

2
0 0
5 5

样例输出 #1

5

分析 分析 分析

Floyd做法

代码

#include
using namespace std;
struct node{
	int a,b;
}q[1000];
istream &operator >> (istream& in,node& x){
	in>>x.a>>x.b;return in;
}
inline int calc(node x,node y){
	int ad=abs(x.a-y.a),bd=abs(x.b-y.b);
	return ceil((ad+bd)/2.0);
}
int n,dis[1000][1000];
int main(){
	cin>>n;int ans=-1e10;
	for (int i=1;i<=n;i++)
		cin>>q[i];
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			int tmp=calc(q[i],q[j]);
			dis[i][j]=tmp;
		}
	}
	for (int k=1;k<=n;k++){
		for (int i=1;i<=n;i++){
			for (int j=1;j<=n;j++)
				dis[i][j]=min(max(dis[i][k],dis[k][j]),dis[i][j]);
		}
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++)
			ans=max(ans,dis[i][j]);
	}
	cout<<ans;
	return 0;
}

分步分析

我们分着来看:
1.node

struct node{
	int a,b;
};
istream &operator >> (istream& in,node& x){
	in>>x.a>>x.b;return in;
}

a,b表示一组坐标,通过对>>的运算符重载实现对node的输入
我们考虑有两个点A( x 1 x_1 x1, y 1 y_1 y1),B( x 2 x_2 x2, y 2 y_2 y2)
那么从AB(不考虑中间有别的点)的扩散所需时间为
t = ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ 2 t={|x_1-x_2|+|y_1-y_2|\over2} t=2x1x2+y1y2
注意, ∣ x 1 − x 2 ∣ + ∣ y 1 + y 2 ∣ {|x_1-x_2|+|y_1+y_2|} x1x2+y1+y2是一个偶数是才适用,由于 i n t int int型的浮点会直接舍去,所以可以转为 d o u b l e double double使用ceil()函数,那么公式为:
t = ⌈ ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ 2 ⌉ t=\left\lceil\frac{|x_1-x_2|+|y_1-y_2|}{2}\right\rceil t=2x1x2+y1y2
代码为

inline int calc(node x,node y){
	int ad=abs(x.a-y.a),bd=abs(x.b-y.b);
	return ceil((ad+bd)/2.0);
}

2.建图
我们需表示出每个点到其他点的权,使权值为 t t t(见上文):

for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			int tmp=calc(q[i],q[j]);
			dis[i][j]=tmp;
		}
	}

利用calc求出 i i i j j j的权,建图存在dis数组中
3.floyd
代码的核心部分,利用了floyd算法的思想,我们设想有一 k k k位于 i i i j j j之间,由于 k k k的扩散, i i i j j j的权不一定是 t t t了,上三点同时扩散,假设 i i i先与 k k k连通,那么三点同时连通的时间(图上的权)为 d i s [ k ] [ j ] dis[k][j] dis[k][j],反之为 d i s [ i ] [ k ] dis[i][k] dis[i][k],数学表达式为:
d i s n e w = max ⁡ { d i s [ i ] [ k ] , d i s [ k ] [ j ] } dis_{new}=\max\{dis[i][k],dis[k][j]\} disnew=max{dis[i][k],dis[k][j]}
但又有可能在三点时连通前 i i i j j j先一步连通,所以:
d i s [ i ] [ j ] = min ⁡ { d i s [ i ] [ j ] , d i s n e w } dis[i][j]=\min\{dis[i][j],dis_{new}\} dis[i][j]=min{dis[i][j],disnew}
即:
d i s [ i ] [ j ] = min ⁡ { d i s [ i ] [ j ] , max ⁡ { d i s [ i ] [ k ] , d i s [ k ] [ j ] } } dis[i][j]=\min\{dis[i][j],\max\{dis[i][k],dis[k][j]\}\} dis[i][j]=min{dis[i][j],max{dis[i][k],dis[k][j]}}
代码:

for (int k=1;k<=n;k++){
		for (int i=1;i<=n;i++){
			for (int j=1;j<=n;j++)
				dis[i][j]=min(max(dis[i][k],dis[k][j]),dis[i][j]);
		}
	}

4.输出
在最后,我们要输出答案,当最后两点连通时,便形成连通块了
代码

	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++)
			ans=max(ans,dis[i][j]);
	}

由于ans初始化为一个负数,最终ans就是答案

Kruskal+并查集做法

代码

#include
using namespace std;
struct node{
	int a,b;
}q[1000];
istream &operator >> (istream& in,node& x){
	in>>x.a>>x.b;return in;
}
inline int calc(node x,node y){
	int ad=abs(x.a-y.a),bd=abs(x.b-y.b);
	return ceil((ad+bd)/2.0);
}
int n,u[100000],v[100000],w[100000],r[100000],p[100000];
bool cmp(const int i,const int j){return w[i]<w[j];}
int find(int x){return p[x]==x?x:p[x]=find(p[x]);}
int main(){
	cin>>n;int ans=0;
	for (int i=1;i<=n;i++)
		cin>>q[i];
	int cnt=0;
	for (int i=0;i<n;i++) p[i]=i;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			if (i==j) continue; 
			int tmp=calc(q[i],q[j]);
			u[cnt]=i;v[cnt]=j;w[cnt]=tmp;r[cnt]=cnt;
			cnt++;
		}
	}
	sort(r,r+cnt,cmp);
	for (int i=0;i<cnt;i++){
		int e=r[i];int x=find(u[e]),y=find(v[e]);
		if (x!=y){
			ans=w[e];p[x]=y;
		}
	}
	cout<<ans;
	return 0;
}

分步分析

比起Floyd算法,Kruskal更难理解一些吧
1.并查集与p数组
并查集 讲的不好,看不懂看其他人的吧
顾名思义,其分为
:比如:

123
342
312
423
521
62
721

这虽然是一棵树,我们设数组a来存它的元素

int a[]={123,342,312,423,521,62,721};

我们只考虑父节点,用p数组存储元素num下标i的父节点j,即:
a [ i ] = n u m   ;   p [ i ] = j a[i]=num\ ;\ p[i]=j a[i]=num ; p[i]=j
例如:62的下标为5,423的下标为3,p[5]=3;

int p[]={0,0,0,0,1,3,3};

不难发现,所有元素的根节点均是123,下标为0,我们可以说,这个集合叫0,所有元素均属于0;
这是只有一个集合,我们看下面的集合:

0
1
2
4
3
5
6

为了便于理解,我们使元素与下标相同

int a[]={0,1,2,3,4,5,6};
int p[]={0,0,0,3,1,3,3};

现在有了0和3两个集合,此时便要查找某个元素属于哪个集合了,比如6属于集合3,1属于集合0,我们很容易想出用递归来做:

int find(int x){
	if (p[x]==x) return x
	else return find(p[x]);
}

可以这样做,但数据很大时,需递归多次,如

0
1
2
3
4

查一次4用到了find(3),查3也用到了find(3),效率低下
但这个集合与它是相同的:

0
1
2
3
4

很显然,这个集合中的任意元素调用find()的时不再递归,结果也相同,效率也高,这种方法叫路径压缩
代码:

int find(int x){return p[x]==x?x:p[x]=find(p[x]);}

那么呢?
我们要把a所在集合,与b所在集合合并,那么可以

p[find(a)]=find(b);

把"根节点"的p数组改下就好了
2.图的数组

int u[100000],v[100000],w[100000];

u[i],v[i]表示第i条边的起点与终点,w[i]表示第i条边的权
建图代码:(r的作用后面解释)

	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			if (i==j) continue; 
			int tmp=calc(q[i],q[j]);
			u[cnt]=i;v[cnt]=j;w[cnt]=tmp;r[cnt]=cnt;
			cnt++;
		}
	}

3.间接排序
根据Kruskal的思想,我们把所有边表示出来,从最小的边开始选,只要不构成环就选,我们要先进行排序,定义数组r,第i小的边的序号存储在r[i],通过对r的修改,表示出我们对边处理的顺序,这需要间接排序,
代码:

bool cmp(const int i,const int j){return w[i]<w[j];}
int main(){
	sort(r,r+cnt,cmp);
	return 0;
}

sort对r进行排序,但依据是w的大小
4.Kruskal
代码的核心
代码:

	for (int i=0;i<cnt;i++){
		int e=r[i];int x=find(u[e]),y=find(v[e]);
		if (x!=y){
			ans=w[e];p[x]=y;
		}
	}

p[x]=y;实现了并集;
重要,若求最小生成树的权值和,应写为ans+=w[e],但点是同时扩散的,故应输出最长边,改为ans=w[e];
由于已排好序,故输出ans即可;

end

你可能感兴趣的:(算法)