Week5 作业4道
本周主要是练习图和树的性质与应用(上)。包括存储方式,遍历方式,并查集和最小生成树。
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
分析
本题的解题思路:
此时的存储方式可以是邻接表:
或者是链式前向星,其补充知识可以参考【链式前向星+存图】讲解:
本来还有一种邻接矩阵的存储方式,但是复杂度过高,这里不适用。邻接矩阵的部分相关知识如下:
该题我用的是邻接表。
- 构造vector<pair<int,int>> G[10010];则G[i].push_back({ a,b });表示i邻接a点,边的权值为b。每次循环输入n后记得要G[i].clear();
- ans[u]用来记录到u的距离的最大值,会不断更新。每次重新输入n后也要记得ans[i] = 0;
- s用来记录一次dfs后距离起点最远的点,初始赋值s为1表示起点为1,第一次从1开始找到用例中的4这个点,第二次从4开始找到5这个点,第三次再从5找到4。必须得进行三次dfs,如果只进行前两次的话,ans[3]=2,(而实际ans[3]=3)漏掉了从5开始出发到4中的部分过程。
C++
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int s;
vector<pair<int,int>> G[10010];
int maxDis[10010] = { 0 };
int ans[10010] = { 0 };
void dfs(int u,int from,int dis) {//u为此时所在点的位置,标记from为来自于哪个点
maxDis[u] = dis;//到u点此时的距离
ans[u] = max(maxDis[u], ans[u]);//与之前保存的距离比较并更新
if (maxDis[u] > maxDis[s]) s = u;//找到最远距离的点并赋值给s
for (int i = 0; i < G[u].size(); i++) {
if (G[u][i].first != from)//避免往回(来的路线)走
dfs(G[u][i].first, u, dis + G[u][i].second);
}
}
int main() {
int n;
while (cin >> n) {
for (int i = 1; i <= n; i++) {
G[i].clear(); ans[i] = 0;
}
for (int i = 2; i <= n; i++) {
int a, b;
cin >> a>>b;
G[i].push_back({ a,b });
G[a].push_back({ i,b });
}
s = 1; dfs(s, 0, 0); dfs(s, 0, 0); dfs(s, 0, 0);
for (int i = 1; i <= n; i++)cout << ans[i] << endl;
}
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
分析
本题主要是并查集的运用。
-
并查集是一种用来管理元素分组情况的数据结构。并查集可以高效地进行如
下操作- 查询元素A和元素B是否属于同一组
- 合并元素A和元素B所在的组
-
并查集的实现
- 代表元素
- 我们不在意树形结构,只在意某个元素在哪个分组
- 与其再对分组进行编号,不如直接从分组中选一个代表元素
- 用代表元来标记一个组
- 初始化
- 指示 n 个孤立的元素,它们独自是一个分组
- 所以初始化即标识代表元是它们自己本身
- 查询操作
- 我们要查询一个节点所在的集合的编号(代表元的编号)
- 一般来说,对于树形的结构,我们默认代表元是根
- 因为它是所有节点的祖先(父亲的父亲的父亲的…父亲),我们可以
顺着边往上找 -> 找代表元的方法统一 -> 可以封装成一个方法 - 我们说过我们并不在意树形结构的信息,所以就算树形结构丢失也
无妨 —— 路径压缩
- 路径压缩
- 为了提高查询的复杂度,丢弃节点间的关系
- —— 把自己直接挂在根节点下
- 合并
- 把一个分组的根挂到另一个分组的根即可
- 并查集的复杂度
- 有兴趣可以去了解 Ackermann 函数,及其反函数 α(n)
- 路径压缩+启发式合并的并查集的复杂度是 O(α(n))
- 你可以简单记为 ——
“如果 n 小于宇宙中的原子数目的话,那么并查集的均摊复杂度可以看做 O(4)”
- 代表元素
-
并查集本身的性质可以用于解决一些简单问题(作业),或者作为复杂问题
解决的辅助 -
然后我们需要用并查集是因为要讲 —— Kruskal 最小生成树
其中并查集的实现的补充如下:
本题主要要注意:
unite(x, last);last = x;
的合并步骤的写法,最终找到包含0的那组的数量用rnk记录。
C++
#include<iostream>
using namespace std;
int par[30010], rnk[30010];//rnk记录同一个并查集中的数量
void init(int n) { for (int i = 0; i <= n-1; i++) par[i] = i, rnk[i] = 1; }
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;
if (rnk[x] > rnk[y]) par[y] = x, rnk[x] = (rnk[y] += rnk[x]);
else
par[x] = y, rnk[y] = (rnk[x] += rnk[y]);
return true;
}
int main() {
int n, m, last, sum = 0;
while (cin >> n >> m) {
if (n == 0 && m == 0) break;
init(n);
for (int i = 0; i < m; i++) {
int num;
cin >> num;
for (int j = 0; j < num; j++) {
int x;
cin >> x;
if (j != 0)
unite(x, last);//每一项与上一项合并
last = x;
}
}
cout << rnk[find(0)] << endl;//输出包含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矩阵
Output
东东最小消耗的MP值
Sample Input
4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0
Sample Output
9
分析
考虑:
- 如果没有把“黄河之水天上来”删掉,那么就是一个最小生成树问题。
- 考虑图重构:加一个超级源点 0 点,并向 n 个点连边为 W i ,然后对这
n+1 个点跑最小生成树即可。其中最小生成树用 Kruskal 最小生成树,其中要用到并查集的知识。
对于最小生成树知识:
C++
#include<iostream>
#include<algorithm>
using namespace std;
struct edge {
int v1, v2, weight;
bool operator <(const edge e) const {
return weight < e.weight;
}
}edges[100010];
int n, number;
int par[100000], rnk[100010];
void init() { for (int i = 0; i <= n; i++) par[i] = i, rnk[i] = 1; }
int find(int x) { return par[x] == x ? x : par[x] = find(par[x]); }
bool unite(edge edg) {//依次将权值最小的边连的两个点合并
int x, y;
x = find(edg.v1), y = find(edg.v2);
if (x == y) return false;
if (rnk[x] > rnk[y]) par[y] = x, rnk[x] = (rnk[y] += rnk[x]);
else
par[x] = y, rnk[y] = (rnk[x] += rnk[y]);
return true;
}
int main() {
int next;
cin >> n;
init();
for (int i = 0; i < n; i++) {
edges[i].v1 = 0, edges[i].v2 = i + 1;
cin >> edges[i].weight;
}
number = n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> next;
if (next == 0) { continue; }
edges[number].weight = next; edges[number].v1 = i; edges[number].v2 = j;
number++;
}
}
sort(edges, edges + number);//边的大小进行排序
int ans = 0, num = 0;//用num记录边的数量,超过n则可以直接退出循环了
for (int i = 0; i < number; i++) {
if (num >= n ) { break; }
if (unite(edges[i])) {
ans += edges[i].weight;//合并成功才在最终答案中加入这条边
num++;
}
}
cout << ans << endl;
return 0;
}
D - 数据中心( CSP201812-4)
Sample Input
4
5
1
1 2 3
1 3 4
1 4 5
2 3 8
3 4 2
Sample Output
4
Note
分析
该题与上一题的思路和解法差不太多,上一题是求和,这题是求最短路径中比较每一段边长,求出其中的最大值,即
ans = max(ans, e[i].w);
C++
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 50005;
const int M = 100005;
struct edge {
int u, v, w;
bool operator<(const edge &t) const { return w < t.w; }
}e[M];
int n, m, root;//根节点此时好像无用
int par[N];
int rnk[N];
void init(int n) { for (int i = 1; i <= n; i++) par[i] = i, rnk[i] = 1; }
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;
if (rnk[x] > rnk[y]) par[y] = x, rnk[x] = (rnk[y] += rnk[x]);
else
par[x] = y, rnk[y] = (rnk[x] += rnk[y]);
return true;
}
int kruskal_return_max() {
sort(e + 1, e + 1 + m);//排序,每次取权值最小的那一条
int cnt = 0, ans = 0;
for (int i = 1; i <= m; i++) {
if (unite(e[i].u, e[i].v)) {
ans = max(ans, e[i].w);//取其中最大的一条
if (++cnt == n - 1)break;
}
}
return cnt == n - 1 ? ans : -1;
}
int main() {
cin >> n >> m >> root;
init(n);
for (int i = 1; i <= m; i++) cin >> e[i].u >> e[i].v >> e[i].w;
cout << kruskal_return_max() << endl;
return 0;
}