**
A - 氪金带东
**
题意
实验室里原先有一台电脑(编号为1),最近氪金带师咕咕东又为实验室购置了N-1台电脑,编号为2到N。每台电脑都用网线连接到一台先前安装的电脑上。但是咕咕东担心网速太慢,他希望知道第i台电脑到其他电脑的最大网线长度,但是可怜的咕咕东在不久前刚刚遭受了宇宙射线的降智打击,请你帮帮他。
提示: 样例输入对应这个图,从这个图中你可以看出,距离1号电脑最远的电脑是4号电脑,他们之间的距离是3。 4号电脑与5号电脑都是距离2号电脑最远的点,故其答案是2。5号电脑距离3号电脑最远,故对于3号电脑来说它的答案是3。同样的我们可以计算出4号电脑和5号电脑的答案是4.
Input
输入文件包含多组测试数据。对于每组测试数据,第一行一个整数N (N<=10000),接下来有N-1行,每一行两个数,对于第i行的两个数,它们表示与i号电脑连接的电脑编号以及它们之间网线的长度。网线的总长度不会超过10^9,每个数之间用一个空格隔开。
Output
对于每组测试数据输出N行,第i行表示i号电脑的答案 (1<=i<=N).
Sample Input
5
1 1
2 1
3 1
1 1
Sample Output
3
2
3
4
4
思路:
这道题的解题思路是关于树的最大直径。先从任意一点出来,通过dfs找到直径的一个端点,然后从这个端点出发找到另外一个直径端点,并且同时更新各点到此端点的距离。然后从另外一个端点再进行一次dfs,更新各点到端点的距离,取各点到两个端点的最大值。此题要找的不是最长路径,是找某一个节点x所能到达的最长路径。对于树中的一个节点,它的最长路径是向右延展的最长路径和向左延展的最长路径的最大值。由于不知道往哪边走是最长路径,所以在找到树的直径的情况下,要分别从右边的最远点和左边的最远点再次遍历,以找到直径的两个端点到每一个节点的距离。
代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=100010;
struct node
{
int u,v,next,val;
}edge[maxn];
int head[maxn],tot;
int vis[maxn];//标记是否到达过
int maxv;//起点能到达的最远点
int dis1[maxn],dis2[maxn];//左右距离
void add(int u,int v,int w)
{
edge[tot].u=u;
edge[tot].v=v;
edge[tot].next=head[u];
edge[tot].val=w;
head[u]=tot;
tot++;
}
void init(int n)
{
tot=0;
for(int i=0;i<n;i++)
head[i]=-1;
}
void dfs(int u,int f,int* d)
{
for (int i=head[u];~i;i=edge[i].next)
{
int v=edge[i].v;
int w=edge[i].val;
if(v!=f)
{
d[v]=d[u]+edge[i].val;
if (d[maxv]<d[v]) maxv=v;
dfs(v,u,d);
}
}
}
int main() {
int n,a,value;
while (~scanf("%d",&n))
{
init(n+1);
for (int i=2;i<=n;i++)
{
scanf("%d%d",&a,&value);
add(i,a,value);
add(a,i,value);//加双向边
}
dis1[1]=0;
dfs(1,0,dis1);//求直径
dis1[maxv]=0;
dfs(maxv,0,dis1);//求一边最远距离
dis2[maxv]=0;
dfs(maxv,0,dis2);//求另一边最远距离
for (int i=1;i<=n;i++){
int ans=max(dis1[i],dis2[i]);
printf("%d\n",ans);
}
}
return 0;
}
B - 戴好口罩!
题意
新型冠状病毒肺炎(Corona Virus Disease 2019,COVID-19),简称“新冠肺炎”,是指2019新型冠状病毒感染导致的肺炎。
如果一个感染者走入一个群体,那么这个群体需要被隔离!
小A同学被确诊为新冠感染,并且没有戴口罩!!!!!!
危!!!
时间紧迫!!!!
需要尽快找到所有和小A同学直接或者间接接触过的同学,将他们隔离,防止更大范围的扩散。
众所周知,学生的交际可能是分小团体的,一位学生可能同时参与多个小团体内。
请你编写程序解决!戴口罩!!
Input
多组数据,对于每组测试数据:
第一行为两个整数n和m(n = m = 0表示输入结束,不需要处理),n是学生的数量,m是学生群体的数量。0 < n <= 3e4 , 0 <= m <= 5e2
学生编号为0~n-1
小A编号为0
随后,m行,每行有一个整数num即小团体人员数量。随后有num个整数代表这个小团体的学生。
Output
输出要隔离的人数,每组数据的答案输出占一行
Example
Input
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Output
4
1
1
思路
利用并查集的思想,将各个群体放在一起。注意并查集可以采用压缩路径的方法来节省查找时间。此题完全是并查集的一个应用:将最初的传染源作为起点0,输入每一个小团体后将有相同人员的团体合并,求得合并后的大团体的人数即为我们需要隔离的人数
并查集是一种用来管理元素分组情况的数据结构。并查集可以高效地进行如下操作:
查询元素A和元素B是否属于同一组
合并元素A和元素B所在的组
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int maxn=30010;
int par[maxn],rank[maxn];
int find(int x)
{
return par[x]==x?x:par[x]=find(par[x]);
}
bool unite(int x,int y)
{
x=find(x),y=find(y);
if(x==y) return false;
par[x]=y;
rank[y]+=rank[x];
return true;
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m))
{
if(n==0&&m==0) break;
for(int i=0;i<n;i++)
par[i]=i,rank[i]=1;
for(int i=1;i<=m;i++)
{
int num,a=-1;
scanf("%d",&num);
while(num--)
{
int b;
scanf("%d",&b);
if(a!=-1) unite(b,a);
a=b;
}
}
printf("%d\n",rank[find(0)]);
}
return 0;
}
C - 掌握魔法の东东 I
题意
东东在老家农村无聊,想种田。农田有 n 块,编号从 1~n。种田要灌溉
众所周知东东是一个魔法师,他可以消耗一定的 MP 在一块田上施展魔法,使得黄河之水天上来。他也可以消耗一定的 MP 在两块田的渠上建立传送门,使得这块田引用那块有水的田的水。 (1<=n<=3e2)
黄河之水天上来的消耗是 Wi,i 是农田编号 (1<=Wi<=1e5)
建立传送门的消耗是 Pij,i、j 是农田编号 (1<= Pij <=1e5, Pij = Pji, Pii =0)
东东为所有的田灌溉的最小消耗
Input
第1行:一个数n
第2行到第n+1行:数wi
第n+2行到第2n+1行:矩阵即pij矩阵
Ouput
东东最小消耗的MP值
Example
Input
4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0
Output
9
思路
此题是一个kruskal的板子题,但需要注意的是“黄河之水天上来”的处理**(图重构)**
我们只需要加一个超级源点0号作为“天”,天与每一块田之间相连消耗MP,一共有n条天与田地相连的边,然后对这n+1个点进行最小生成树操作即可。
代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstdlib>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=310;
int n;
struct node
{
int u,v,w;//两点和两点之间的权重
bool operator<(const node&a)const
{
return w<a.w;
}//重载<,权值从小到大排序
}edge[maxn*maxn];
int f[maxn];
int tot;//边总数
int find(int t)
{
return f[t]==t?f[t]:f[t]=find(f[t]);
}
int kruskal()
{
for(int i=0;i<=n;i++)
{
f[i]=i;//点的序数
}
sort(edge,edge+tot);//sort(排序开始地址,结束地址,排序方法)排序方法可不写默认为从小到大
int ans=0;
int cnt=0;
for(int i=0;i<tot;i++) //sort已将边按权重大小排好,此时只需顺序调用
{
if(find(edge[i].u)!=find(edge[i].v)) //不为环路可选取边
{
f[find(edge[i].u)]=find(edge[i].v);//将后一个点的点序数赋给前一个点
ans+=edge[i].w;
++cnt;
if(cnt==n) return ans;
}
}
return -1;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&edge[tot].w); //黄河之水天上来的权重
edge[tot].u=0;//加一个超级源点0号
edge[tot].v=i;
tot++;
}
for(int i=1;i<=n;i++)//输入矩阵权重
{
for(int j=1;j<=n;j++)
{
scanf("%d",&edge[tot].w);
if(i>j)
{
edge[tot].u=i;
edge[tot].v=j;
tot++;
}
}
}
printf("%d\n",kruskal());
return 0;
}
D - 数据中心
(CCF CSP 20181204 数据中心【瓶颈生成树】)
Example
Input
4
5
1
1 2 3
1 3 4
1 4 5
2 3 8
3 4 2
Output
4
思路
最小生成树一定是瓶颈生成树
【证明】:假设最小生成树不是瓶颈树,设最小生成树T的最大权边为e,则存在一棵瓶颈树Tb,其所有的边的权值小于w(e)。删除T中的e,形成两棵树T’, T’’,用Tb中连接T’, T’'的边连接这两棵树,得到新的生成树,其权值小于T,与T是最小生成树矛盾。
此题利用kruskal算法:
由题中:root节点只能接收数据,其余任何节点可以将数据传输给另外一个节点,但是不能将数据传输给多个节点。所有节点可以接收多个不同节点的数据我们可以知道,此题可以将root看为树的根节点,最后生成一个包含了所有点的图,看作是一个无向图由边权大小求最小树的问题
并且需要注意的是:我们要求的是一棵最大边权最小的生成树,max函数比较(题C中的代码改两行就可以跑这个了.)
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstdlib>
#include<algorithm>
#include<cstring>
int ans=0;int cnt=0;
using namespace std;
struct node
{
int u,v,w;//两点和两点之间的权重
bool operator<(const node&a)const
{
return w<a.w;
}//重载<,权值从小到大排序
}edge[110000];
int f[51000];
int n,m,root;
int find(int t)
{
return f[t]==t?f[t]:f[t]=find(f[t]);
}
int kruskal()
{
for(int i=1;i<=n;i++)
{
f[i]=i;
}
sort(edge+1,edge+1+m);//sort(排序开始地址,结束地址,排序方法)排序方法可不写默认为从小到大
for(int i=1;i<=m;i++) //sort已将边按权重大小排好,此时只需顺序调用
{
if(find(edge[i].u)!=find(edge[i].v)) //不为环路可选取边
{
f[find(edge[i].u)]=find(edge[i].v);//将后一个点的点序数赋给前一个点
ans=max(ans,edge[i].w);
if(++cnt==n-1) break;
}
}
return cnt==n-1? ans:-1;
}
int main()
{
scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);//val为u何v之间的权重值
}
printf("%d\n",kruskal());
return 0;
}