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
思路
首先根据题意,电脑连接形成了一个图(更准确是形成了一个树,因为每次只加入一个叶子节点),要找到任何一个节点到树中其他节点的最长距离,需要使用到一个结论:
假设v1,v2是树的直径的两个节点,树中任意一个节点u到树中其他节点的最长距离为u到v1或v2中最长的距离
因此,首先就要找到树的直径的节点。
根据这个结论,从任意一个点开始遍历,找最长路径的长度,总能找到一个直径的节点。
//返回一个直径节点的编号
int bfs(int start){
while(!q.empty()) q.pop();
memset(vis,false,sizeof(bool)*maxn);
memset(dis,0,sizeof(int)*maxn);
q.push(Edge(start,0)); vis[start]=true;dis[start]=0;
int last=1;
while(!q.empty()){
Edge temp=q.front();q.pop();
for(int i=1;i<=s[temp.to][0].dis;i++){
if(vis[s[temp.to][i].to]) continue;
q.push(s[temp.to][i]);
vis[s[temp.to][i].to]=true;
dis[s[temp.to][i].to]=s[temp.to][i].dis+dis[temp.to];
if(dis[last]<dis[s[temp.to][i].to]) last=s[temp.to][i].to;
}
}
return last;
}
从这个节点再次开始遍历,不仅可以找到另一个节点,
if(dis[last]<dis[s[temp.to][i].to]) last=s[temp.to][i].to;
还可以记录每个节点到这个直径节点的距离。
dis[s[temp.to][i].to]=s[temp.to][i].dis+dis[temp.to];
每个节点在树中的最长路径要么是到v1的距离,要么是到v2的距离,那么从v2开始再次进行遍历,比较任意节点到两个直径节点的距离,求取最大值即可。
for(int i=1;i<=n;i++)
printf("%d\n",max(dis[i],dis1[i]));
代码
#include <iostream>
#include <stdio.h>
#include <vector>
#include <queue>
#include <string.h>
using namespace std;
struct Edge{
int to,dis;
Edge(){to=-1;dis=0;}
Edge(int id,int len){
to=id;
dis=len;
}
};
const int maxn=1e4+5;
int n;
vector<Edge> s[maxn];
bool vis[maxn];
int dis[maxn];
int dis1[maxn];
queue<Edge> q;
int bfs(int start){
while(!q.empty()) q.pop();
memset(vis,false,sizeof(bool)*maxn);
memset(dis,0,sizeof(int)*maxn);
q.push(Edge(start,0)); vis[start]=true;dis[start]=0;
int last=1;
while(!q.empty()){
Edge temp=q.front();q.pop();
for(int i=1;i<=s[temp.to][0].dis;i++){
if(vis[s[temp.to][i].to]) continue;
q.push(s[temp.to][i]);
vis[s[temp.to][i].to]=true;
dis[s[temp.to][i].to]=s[temp.to][i].dis+dis[temp.to];
if(dis[last]<dis[s[temp.to][i].to]) last=s[temp.to][i].to;
}
}
return last;
}
void bfs1(int start){
while(!q.empty()) q.pop();
memset(vis,false,sizeof(bool)*maxn);
memset(dis1,0,sizeof(int)*maxn);
q.push(Edge(start,0)); vis[start]=true;dis1[start]=0;
// int last=1;
while(!q.empty()){
Edge temp=q.front();q.pop();
for(int i=1;i<=s[temp.to][0].dis;i++){
if(vis[s[temp.to][i].to]) continue;
q.push(s[temp.to][i]);
vis[s[temp.to][i].to]=true;
// dis[s[temp.to][i].to]=max(dis[s[temp.to][i].to],s[temp.to][i].dis+dis[temp.to]);
dis1[s[temp.to][i].to]=s[temp.to][i].dis+dis1[temp.to];
// if(dis[last]<dis[s[temp.to][i].to]) last=s[temp.to][i].to;
}
}
// return last;
}
int main(){
while(~scanf("%d",&n)){
for(int i=0;i<maxn;i++)
s[i].clear();
for(int i=0;i<=n;i++)
s[i].emplace_back(Edge(0,0));
for(int i=2;i<=n;i++){
int id,len;
scanf("%d%d",&id,&len);
s[i].emplace_back(Edge(id,len));s[i][0].dis++;
s[id].emplace_back(Edge(i,len));s[id][0].dis++;
}
int start=bfs(1);
int end=bfs(start);
// for(int i=0;i<=n;i++)
// cout<<dis[i]<<" ";
// cout<<endl;
bfs1(end);
// for(int i=0;i<=n;i++)
// cout<<dis1[i]<<" ";
// cout<<endl;
for(int i=1;i<=n;i++)
printf("%d\n",max(dis[i],dis1[i]));
}
}
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
输出要隔离的人数,每组数据的答案输出占一行
Sample 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
Sample Output
4
1
1
思路
并查集的主要作用就是并与查,这个题目很好的体现了这两个功能。
首先是查,即检查一个元素是属于哪一个组,返回这个组的代表元素
int find(int x){
if(arr[x] == x) return x;
return arr[x]=find(arr[x]);
}
其次是并,即将两个元素合并为一个组
bool unite(int x,int y){
x=find(x),y=find(y);
if(x==y) return false;
if(rnk[x]>rnk[y]) swap(x,y);
arr[x]=y;
rnk[y]+=rnk[x];
return true;
}
对于这个题目而言,每一个学生小团体当然是一个组,但是同一个学生可能属于不同的小团体,通过并查集的合并会将两个小团体合成为一个,同时不断更新并查集的中元素的数目。因此,对每一个小团体中的元素分别进行一个合并,最后可以实现同一个元素所在两个组的合并。
在并查集中,没有必要专门用一个数组来记录并查集中元素的数目,虽然这样做并不会显著的减少空间。在代表元素的位置使用元素数目的相反数来记录这个组的元素数目,这样可以减少使用一个数组。
int find(int x){
if(arr[x]<0) return x;
arr[x]=find(arr[x]);return arr[x];
}
bool unite(int x,int y){
x=find(x);y=find(y);
if(x==y) return false;
if((-arr[x]) >= (-arr[y])){arr[x]+=arr[y];arr[y]=x;}
else{arr[y]+=arr[x];arr[x]=y;}
return true;
}
代码
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
int n,m;
const int nmax=3e4;
const int mmax=5e2;
vector<int> s[mmax];
int arr[nmax];
int rnk[nmax];
void init(int n){
for(int i=0;i<n;i++){
arr[i]=i;
rnk[i]=1;
}
}
int find(int x){
if(arr[x] == x) return x;
return arr[x]=find(arr[x]);
}
bool unite(int x,int y){
x=find(x),y=find(y);
if(x==y) return false;
if(rnk[x]>rnk[y]) swap(x,y);
arr[x]=y;
rnk[y]+=rnk[x];
return true;
}
int main(){
while(~scanf("%d%d",&n,&m) && (m!=0 || n!=0)){
// memset(arr,-1,sizeof(int)*nmax);
init(n);
for(int i=0;i<m;i++){
int number;scanf("%d",&number);
int last=-1;
for(int j=1; j <= number; j++){
int temp;scanf("%d",&temp);
if(last!=-1) unite(temp,last);
last=temp;
}
}
cout<<rnk[find(0)]<<endl;
}
}
C-掌握魔法の东东
东东在老家农村无聊,想种田。农田有 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矩阵
Output
东东最小消耗的MP值
Examples
Input
4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0
Output
9
思路
如果不考虑”黄河之水天上来“,那么这就是一个n个点生成一个连通图使得消耗最小的问题,即最小生成树。
考虑”黄河之水天上来“,从”天上“可以向每一块田灌溉,向每一个节点都有一定的消耗,那么可以看作”天上“是超级源点,与每一个节点都有联通和对应的权重(消耗),这样就完成了图重构,成立一个最小生成树问题。
for(int i=1;i<=n;i++){
int temp=0;scanf("%d",&temp);
s.push_back(Edge(0,i,temp));
}
for(int i=1;i<=n;i++){
int temp=0;
for(int j=1;j<=i;j++) scanf("%d",&temp);
for(int j=i+1;j<=n;j++){
scanf("%d",&temp);
s.push_back(Edge(i,j,temp));
}
}
kruskal算法:一个n个节点的图,从边集中不断选取最小权值的边,加入到当前生成树中,如果有环,舍弃,如果没有环,继续添加,直到生成树中有n个节点。
对边集的结构体使用sort()重新排序每次可以得到当前最小权值的边,关键在于判断是否有环。有环,即这条边的两个端点都在同一个连通图中,由此可以使用并查集来判断两个端点是否属于同一个连通图,如果两个端点的代表元素不相同,即这两个端点属于不同的连通分量,合并并查集即可
while(number<n)
{
if(unite(s.at(index).from,s.at(index).to)){
total+=s.at(index).weight;
index++;number++;
}
else
index++;
}
(需要注意的是,在合并并查集的时候会丢失掉最小生成树的树形结构,当然可以不断重新改变并查集的代表元素来保持最小生成树的树形结构,但是没有必要,因为在生成树的过程中只需要判断一条边的两个端点是否输入同一个并查集)
注意因为添加了超级源点,并查集初始化时应该为n+1
代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge{
int from,to,weight;
Edge(){
from=0;to=0;weight=0;
}
Edge(int _from,int _to,int _weight){
from=_from;
to=_to;
weight=_weight;
}
bool operator < (Edge& edge){
return weight<edge.weight;
}
void show(){
cout<<"from:"<<from<<" to:"<<to<<" weight:"<<weight<<endl;
}
};
const int nmax=3e2+5;
vector<Edge> s;
int arr[nmax];
void init(int n){
for(int i=0;i<n;i++) arr[i]=i;
}
int find(int x){
if(arr[x] == x) return x;
return arr[x]=find(arr[x]);
}
bool unite(int x,int y){
x=find(x),y=find(y);
if(x==y) return false;
arr[x]=y;
return true;
}
int main(){
int n=0;scanf("%d",&n);
s.clear();init(n+1);//注意这里是n+1,因为添加了超级原点
for(int i=1;i<=n;i++){
int temp=0;scanf("%d",&temp);
s.push_back(Edge(0,i,temp));
}
for(int i=1;i<=n;i++){
int temp=0;
for(int j=1;j<=i;j++) scanf("%d",&temp);
for(int j=i+1;j<=n;j++){
scanf("%d",&temp);
s.push_back(Edge(i,j,temp));
}
}
sort(s.begin(),s.end());
int number=0,index=0,total=0;
while(number<n)
{
if(unite(s.at(index).from,s.at(index).to)){
total+=s.at(index).weight;
index++;number++;
}
else
index++;
}
cout<<total<<endl;
return 0;
}
D-数据中心
Example
Input
4
5
1
1 2 3
1 3 4
1 4 5
2 3 8
3 4 2
Output
4
思路
题目比较长,首先将有用的信息过滤出来
深度为h的第j条边权重为th,j,
深度为h的时间为Th=max(th,j)
传输时间Tmax=max(Th)=max(th,j)
问题即转化为给定无向图,求解一棵生成树,使得最大边的权值最小,也就是求解瓶颈生成树
由于最小生成树一定是瓶颈生成树,因此求出最小生成树,取边的权重中最大的即可
求取最小生成树的方法与上面C题的方法和相似,代码也类似,稍微有区别的是并查集初始化为n,并且每次取的是边的权值的最大值
time=max(time,s.at(index).weight);
并查集中没有路径压缩可能会超时,平时在写并查集的时候注意加上路径压缩
代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge{
int from,to,weight;
Edge(){from=0;to=0;weight=0;}
Edge(int _from,int _to,int _weight){
from=_from;
to=_to;
weight=_weight;
}
void show(){
cout<<"from:"<<from<<" to:"<<to<<" weight:"<<weight<<endl;
}
bool operator < (Edge edge){
return weight<edge.weight;
}
};
const int mmax=5e4;
const int nmax=1e5;
int m,n,root;
vector<Edge> s;
int arr[nmax];
int rnk[nmax];
void init(int n){
for(int i=0;i<n;i++){
arr[i]=i;rnk[i]=1;
}
}
int find(int x){
if(arr[x]==x) return x;
return arr[x]=find(arr[x]);
}
bool unite(int x,int y){
x=find(x);y=find(y);
if(x==y) return false;
if(rnk[x]>rnk[y]) swap(x,y);
arr[x]=y;rnk[y]+=rnk[x];
return true;
}
int main(){
scanf("%d%d%d",&n,&m,&root);init(n);
for(int i=0;i<m;i++){
int from,to,weight;
scanf("%d%d%d",&from,&to,&weight);
s.push_back(Edge(from,to,weight));
}
sort(s.begin(),s.end());
// for(int i=0;i<m;i++)
// s.at(i).show();
int number=1,index=0,time=0;
while(number<n){
// s.at(index).show();
if(unite(s.at(index).from,s.at(index).to)){
number++;
time=max(time,s.at(index).weight);
}
index++;
}
cout<<time;
return 0;
}