树直径
各点最远距离
-
问题
实验室里原先有一台电脑(编号为1),最近氪金带师咕咕东又为实验室购置了N-1台电脑,编号为2到N。每台电脑都用网线连接到一台先前安装的电脑上。但是咕咕东担心网速太慢,他希望知道第i台电脑到其他电脑的最大网线长度 -
输入输出
输入文件包含多组测试数据。对于每组测试数据,第一行一个整数N (N<=10000),接下来有N-1行,每一行两个数,对于第i行的两个数,它们表示与i号电脑连接的电脑编号以及它们之间网线的长度。网线的总长度不会超过10^9,每个数之间用一个空格隔开。
对于每组测试数据输出N行,第i行表示i号电脑的答案 (1<=i<=N)。
解题
-
直径两端点
直径概念——树中最长的路径。端点——直径两端点。
一个点能到达的最远距离=到两端点最大距离。(下面将会证明) -
第一个端点
从任意一点开始,能到达对的最远点一定是其中一个端点。(下面证明) -
另一个端点
从第一个端点开始,能到达的最大点一定是另一个端点。(下面证明) -
三次遍历
第一次,从任意一点开始,找出第一个端点。
第二次,从第一个端点开始,找到第二个,并且记录各个点到达第一端点的距离。
第三次,从第二端点开始,计算到达第二端点的距离(如果大于上次记录,修改)。 -
寻找最距离
从点P出发,最距离=max(周围点最远距离)。使用DFS遍历返回周围点的最远距离,记录最大。另外我们要知道当前遍历(后者说路径),从哪一点来的,因为是无向图,不能从当前点回到出发点(重复遍历死循环)。 -
边界
已经避免回到原点的情况,如果当前点不能再向其他点移动,即为结束。 -
最远点
维护从出发点到此节点最大距离,记录到最大距离的最远点。
证明
- 端点
从任意一点P开始。
如果P找到的最远点是Q,Q有没有可能不是端点。假设直径两端点是AB。
当AB与PQ相交,首先我们知道Q是P能到的最大点。如果AB是直径,那么AB>PQ,PQ>PC+CA(P到A小于到Q),得到CQ>CA。这显然不可能,因为AB是直径,AB>BC+CQ。同时也证明,任意一点P最远点一定在AB之中。
如果AB与PQ不想交,AB>PQ,PN+NQ>PN+NM+MA,得QN+NM>AM+MN,QNB>AMB。不成立。所以P最远点在端点AB中。
代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e4+5;
struct edge{
int p,w;
edge(int a,int b){
p=a,w=b;
}
};
vector<edge> pnt[N];
int n,s;
int path[N],maxl;
void DFS(int u,int v,int l){
int next,w;
if(l>maxl){
s=u;
maxl=l;
}
for(int i=0;i<pnt[u].size();i++){
next=pnt[u][i].p;
w=pnt[u][i].w;
if(next==v)continue;
DFS(next,u,w+l);
path[next]=max(path[next],w+l);
}
}
int main(){
int a,b;
while(cin>>n){
maxl=0;
for(int i=1;i<=n;i++){
pnt[i].clear();
}
for(int i=2;i<=n;i++){
cin>>a>>b;
pnt[i].push_back(edge(a,b));
pnt[a].push_back(edge(i,b));
}
memset(path,0,sizeof(path));
DFS(1,0,0);
DFS(s,0,0);
DFS(s,0,0);
for(int i=1;i<=n;i++){
cout<<path[i]<<endl;
}
}
return 0;
}
并查集
戴口罩
- 问题
新型冠状病毒肺炎(Corona Virus Disease 2019,COVID-19),简称“新冠肺炎”,是指2019新型冠状病毒感染导致的肺炎。
如果一个感染者走入一个群体,那么这个群体需要被隔离!
小A同学被确诊为新冠感染,并且没有戴口罩!!!!!!
危!!!时间紧迫!!!!和小A同学直接或者间接接触过的同学,将他们隔离,防止更大范围的扩散。众所周知,学生的交际可能是分小团体的,一位学生可能同时参与多个小团体内。 - 输入输出
多组数据,对于每组测试数据:
第一行为两个整数n和m(n = m = 0表示输入结束,不需要处理),n是学生的数量,m是学生群体的数量。0 < n <= 3e4 , 0 <= m <= 5e2
学生编号为0~n-1
小A编号为0随后,m行,每行有一个整数num即小团体人员数量。随后有num个整数代表这个小团体的学生。
输出要隔离的人数,每组数据的答案输出占一行。
解题
-
并查集
要记录团体成员可以使用并查集,为了防止并查集内部过于混乱没有主次,定义一个数组记录每个并查集的大小(成员越多越大)两个并查集合并采用大树上挂小树。 -
多个团体
这道题要找的是与小A有直接或间接关联的所有人。
直接关联就是与小A同一个团体的同学(这里称直接学生),间接关联就是与直接学生有关联的同学。
因此,我们不用在意有几个团体,用一个并查集相连,最后小A的大团体就是最终人数。
代码
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int N=3e4,M=5e2;
int uF[N];
int rank[N];
int find(int x){
if(uF[x]==x)
return x;
return uF[x]=find(uF[x]);
}
bool unite(int a,int b){
int aa,bb;
aa=find(a),bb=find(b);
if(aa==bb)
return false;
if(rank[a]>rank[b])
uF[bb]=aa,rank[aa]+=rank[bb];
else
uF[aa]=bb,rank[bb]+=rank[aa];
return true;
}
int main(){
int n,m,a,b,t;
cin>>n>>m;
while(n||m){
for(int i=0;i<n;i++)
rank[i]=1;
for(int i=0;i<n;i++)
uF[i]=i;
for(int i=0;i<m;i++){
cin>>t>>a;
for(int j=1;j<t;j++){
cin>>b;
unite(a,b);
}
}
cout<<rank[find(0)]<<endl;
cin>>n>>m;
}
}
超级源点+最小生成树
农田灌溉
- 问题
东东在老家农村无聊,想种田。农田有 n 块,编号从 1~n。种田要灌氵
众所周知东东是一个魔法师,他可以消耗一定的 MP 在一块田上施展魔法,使得黄河之水天上来。他也可以消耗一定的 MP 在两块田的渠上建立传送门,使得这块田引用那块有水的田的水。 (1<=n<=3e2)
黄河之水天上来的消耗是 Wi,i 是农田编号 (1<=Wi<=1e5)
建立传送门的消耗是 Pij,i、j 是农田编号 (1<= Pij <=1e5, Pij = Pji, Pii =0)
东东为所有的田灌氵的最小消耗 - 输入输出
第1行:一个数n ,第2行到第n+1行:数wi,第n+2行到第2n+1行:矩阵即pij矩阵
输出东东最小消耗的MP值。
解题
-
kruskal
这样的题很容易想到kruskal算法,生成最小树。 -
超级源点
生成最小树很简单,但是我们n个农田至少要有一个从天灌溉(作为水的源头),加入超级源点作为点0,生成一个0到n的最小树,W就是0到其它点的代价(权)。 -
代码简化
可以看到1到n之间的权是一个矩阵(设行列为i和j),作为一个无向图重复输入Pij和Pji是没有意义的。所以加入一个变量p,对于i<j的Pij直接输入到p(没有任何作用)。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=3e2+5;
int pnt[N],r[N];
int n,k=0;
struct edge{
int a,b,w;
}e[N*N/2];
bool cmp(edge u,edge v){
return u.w<v.w;
}
int find(int x){
if(pnt[x]==x){
return x;
}
return pnt[x] = find(pnt[x]);
}
bool unite(int a,int b){
int aa,bb;
aa = find(a), bb = find(b);
if(aa == bb)
return false;
if(r[aa] > r[bb])
pnt[bb] = aa, r[aa] += r[bb];
else
pnt[aa] = bb, r[bb] += r[aa];
return true;
}
int main(){
int v,sum=0;
edge ne;
cin>>n;
for(int i = 0;i <= n;i++){
pnt[i]=i;
r[i]=1;
}
for(int i = 1;i <= n;i++){
cin>>e[k].w;
e[k].a=0;
e[k++].b=i;
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
if(j>i) {
cin>>e[k].w;
e[k].a=i;
e[k++].b=j;
}
else
cin>>v;
}
}
sort(e,e+k,cmp);
for(int i = 0;i < k;i++){
ne = e[i];
if(unite(ne.a,ne.b)){
sum+=ne.w;
}
}
cout<<sum<<endl;
}
最小瓶颈生成树
数据中心
解题
-
瓶颈生成树
本题要求生成一棵树,这棵树的Tmax最小。
Tmax=每个深度最大的Th,而每个Th=各自深度中最大的T,T就是点点之间的权。
总结下来,生成一棵树使这个数的最大权能达到最小,即为最小瓶颈生成树。 -
证明
在无向图中,瓶颈生成树=最小生成树。
下面用反证法来证明一下,假设最小生成树不是瓶颈生成树,也就是说存在一个树它的最大权比最小生成树权更小。
我们最小生成树是从最小开始生成, 如果真的存在这样一颗树不是最小生成树的瓶颈生成树,生成树算法中一定会选定这个瓶颈树,假设不成立。 -
补充
无向图中,最小生成树一定是瓶颈树,最小生成树不一定是瓶颈生成树。
例如:一个树有五个点,我们选中了四个边做最想生成树 1 3 2 2,它也是瓶颈生成树。但是,1 3 3 3也是瓶颈生成树。
所以,一个树可能有多个瓶颈生成树。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 5e4+5,M=1e5+5;
int pnt[N],rnk[N];
struct edge{
int u,v,w;
bool operator<(const edge &a){
return w<a.w;
}
}e[M];
int find(int x){
if(x == pnt[x])
return x;
return pnt[x] = find(pnt[x]);
}
bool unite(int a,int b){
int aa,bb;
aa = find(a), bb = find(b);
if(aa == bb)
return false;
if(rnk[aa]>rnk[bb]){
pnt[bb] = aa, rnk[aa] += rnk[bb];
}
else{
pnt[aa] = bb, rnk[bb] += rnk[aa];
}
return true;
}
int main(){
int n,m,p;
cin>>n>>m>>p;
for(int i=0;i<m;i++){
cin>>e[i].u>>e[i].v>>e[i].w;
}
for(int i=0;i<=n;i++){
pnt[i]=i;
rnk[i]=1;
}
sort(e,e+m);
p=0;
int count=n-1;
for(int i=0;i<m;i++){
if(unite(e[i].u,e[i].v)){
p=max(p,e[i].w);
count--;
if(!count)
break;
}
}
cout<<p<<endl;
}