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
解题思路
题意:每台电脑都用网线连接到一台先前安装的电脑上。希望知道第i台电脑到其他电脑的最大网线长度。
make_graph()
图的存储:使用一个结构体数组记录图中的所有边,结构体里有三个成员:u,v,w,分别表示这个边关联的两个顶点和边权。然后用邻接表存储,
find()
要找以每个顶点为端点的最长路径的长度,另一个端点一定是这个树的最长路的两个端点之一。
所以首先要求出整棵树的最长路端点,即求树的“直径”:
假设树的最长路的两个叶子节点为v1, v2,从任意一点 u 出发走到的最 远的点一定是(v1,v2)中的一点,然后再从 v1 或者 v2 出发走到的最 远点一定是 v2 或者 v1。由此经过两次遍历就能找到最长路径。
这个就用dfs实现,更新最大长度的同时更新端点,遍历一遍就可以标记到最长路的端点。
dfs1() dfs2()
因为另一个端点一定是这个树的最长路的两个端点之一,所以分别从v1 v2开始遍历图,分别记下到达所有顶点的长度,然后取max,就是每个顶点的最大长度。
一开始忘了上课讲的。。。就直接dfs所有顶点到v1 v2哈哈哈zz,肯定超时。。。
完整代码
#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
int n;
int vis[10010];
int cnt = 0, thedis = 0, dis = 0;
int v1, v2;
int vv;
struct edge
{
int nn;
int len;
};
vector<edge> node[10010];
int v1_dis[100000], v2_dis[100000];
void make_graph(int n)
{
for(int i=2;i<=n;i++)
{
int num, ll;
scanf("%d %d", &num, &ll);
edge e;
e.nn = num; e.len = ll;
node[i].push_back(e);
e.nn = i;
node[num].push_back(e);
}
}
int find(int x)
{
for(int i=0;i<node[x].size();i++)
{
int next = node[x][i].nn;
if(vis[next] == 1) continue;
dis += node[x][i].len;
if(dis > thedis)
{
thedis = dis;
vv = next;
}
vis[next] = 1;
find(next);
dis -= node[x][i].len;
}
return vv;
}
void dfs1(int x)
{
for(int i=0;i<node[x].size();i++)
{
int next = node[x][i].nn;
if(vis[next] == 1) continue;
v1_dis[next] = v1_dis[x] + node[x][i].len;
vis[next] = 1;
dfs1(next);
}
return;
}
void dfs2(int x)
{
for(int i=0;i<node[x].size();i++)
{
int next = node[x][i].nn;
if(vis[next] == 1) continue;
v2_dis[next] = v2_dis[x] + node[x][i].len;
vis[next] = 1;
dfs2(next);
}
return;
}
int main()
{
while(scanf("%d", &n) != EOF)
{
make_graph(n);
memset(v1_dis, 0 ,sizeof(v1_dis));
memset(v2_dis, 0 ,sizeof(v2_dis));
for(int k=0;k<=n;k++) vis[k] = 0;
vis[1] = 1; thedis = 0; vv = -1;
v1 = find(1);
for(int k=0;k<=n;k++) vis[k] = 0;
vis[v1] = 1; thedis = 0; vv = -1;
v2 = find(v1);
for(int k=0;k<=n;k++) vis[k] = 0;
vis[v1] = 1;
dfs1(v1);
for(int k=0;k<=n;k++) vis[k] = 0;
vis[v2] = 1;
dfs2(v2);
for(int i=1;i<=n;i++)
cout<<max(v1_dis[i], v2_dis[i])<<endl;
for (int i=0;i<=n;i++)
node[i].clear();
}
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
输出要隔离的人数,每组数据的答案输出占一行
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
解题思路
这是要一个典型的分组分类问题,当然是用并查集解决。
并查集的实现:
初始化
指示 n 个孤立的元素,它们独自是一个分组
所以初始化即标识代表元是它们自己本身
查询操作
我们要查询一个节点所在的集合的编号(代表元的编号)
一般来说,对于树形的结构,我们默认代表元是根
因为它是所有节点的祖先(父亲的父亲的父亲的…父亲)
我们可以顺着边往上找 -> 找代表元的方法统一 -> 可以封装成一个方法
路径压缩
为了提高查询的复杂度,丢弃节点间的关系
—— 把自己直接挂在根节点下
合并
把一个分组的根挂到另一个分组的根即可
完整代码
#include <iostream>
#include <stdio.h>
#include <string.h>
int n, m;
int f[30010], cnt[30010];
using namespace std;
int find(int x)
{
if(x == f[x]) return x;
find(f[x]);
}
void Union(int x,int y)
{
int fx, fy;
fx = find(x); fy = find(y);
f[fx] = fy;
if(fx == fy) return;
cnt[fy] += cnt[fx];
}
int main()
{
int ans;
while(true)
{
cin>>n>>m;
if(n==0 && m==0) break;
for(int i=0;i<n;i++)
{
f[i] = i;
cnt[i] = 1;
}
for(int i=0;i<m;i++)
{
int k;
cin>>k;
int num, before = -1;
for(int j=0;j<k;j++)
{
cin>>num;
if(j > 0) Union(before, num);
before = num;
}
}
ans = cnt[find(0)];
cout<<ans<<endl;
}
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矩阵
Output
东东最小消耗的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
解题思路
黄河之水天上来,那就加一个顶点,把“天”记0号顶点。对着n+1个顶点求最小生成树。
kruskal算法
将全部边按照权值由小到大排序。
sort(e, e+k, cmp);
按顺序(边权由小到大的顺序)考虑每条边,只要这条边和我们已经选 择的边不构成圈,就保留这条边,否则放弃这条边。啊这里判断是否成环用到了并查集。
if(find(e[i].u) != find(e[i].v))
{
Union(e[i].u, e[i].v);
ans = ans + e[i].w;
}
成功选择(n-1)条边后,形成一棵最小生成树,当然如果算法无法选择出(n-1)条边,则说明原图不连通。
完整代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int n;
int id[310];
int ans = 0;
struct edge
{
int u, v, w;
}e[100000];
bool cmp(edge x,edge y)
{
return x.w<y.w;
}
int find(int x)
{
if(id[x] == x) return x;
return id[x]=find(id[x]);
}
void Union(int x,int y)
{
int a = find(x);
int b = find(y);
id[a] = b;
}
int main()
{
cin>>n;
int k = 0;
for(int i=1;i<=n;i++)
{
int ww; cin>>ww;
e[k].u = 0; e[k].v = i;
e[k].w = ww;
k++;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int ww; cin>>ww;
if(i == j) continue;
e[k].u = i; e[k].v = j;
e[k].w = ww;
k++;
}
sort(e, e+k, cmp);
for(int i=0;i<=n;i++) id[i]=i;
for(int i=0;i<k;i++)
{
if(find(e[i].u) != find(e[i].v))
{
Union(e[i].u, e[i].v);
ans = ans + e[i].w;
}
}
cout<<ans<<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
解题思路
压缩一下题意,就是一个最小瓶颈生成树问题。
性质:无向图的最小生成树一定是最小瓶颈生成树。
这篇博客有证明:
https://www.cnblogs.com/c1299401227/p/5808380.html
所以问题转换成了求最小生成树。这题同样用kruskal求最小生成树,先给所有边按边权从小到大排个序,一路更新ans,这样最后一条边就是记的边权最大的边。
完整代码
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, root;
int num = 0, ans = 0;
int f[100000];
struct edge
{
int u, v, w;
}e[100000];
bool cmp(edge x, edge y)
{
return x.w < y.w;
}
int find(int x)
{
return f[x] == x ? x : f[x] = find(f[x]);
}
bool Union(int x, int y)
{
x = find(x); y = find(y);
if(x == y) return false;
f[x] = y;
return true;
}
int main()
{
cin>>n>>m>>root;
for(int i=0;i<n;i++) f[i] = i;
for(int i=0;i<m;i++) cin>>e[i].u>>e[i].v>>e[i].w;
sort(e, e+m, cmp);
for(int i=0;i<m;i++)
{
if(Union(e[i].u, e[i].v))
{
ans = e[i].w;
num += 1;
if(num == n-1) break;
}
}
cout<< ans<< endl;
return 0;
}
总结:
这次作业学到了很多新的算法,比如求树的直径,还有求最长路。并查集的应用非常广泛,写起来也很方便。然后就是求最小生成树,两次都用了kruskal。特别是最后一题最小瓶颈生成树的性质,受益匪浅hhhh。