文章目录
A 氪金带东
1. 题目大意
实验室里原先有一台电脑(编号为1),最近氪金带师咕咕东又为实验室购置了N-1台电脑,编号为2到N。每台电脑都用网线连接到一台先前安装的电脑上。但是咕咕东担心网速太慢,他希望知道第i台电脑到其他电脑的最大网线长度,但是可怜的咕咕东在不久前刚刚遭受了宇宙射线的降智打击,请你帮帮他。
输入:
输入文件包含多组测试数据。对于每组测试数据,第一行一个整数N (N<=10000),接下来有N-1行,每一行两个数,对于第i行的两个数,它们表示与i号电脑连接的电脑编号以及它们之间网线的长度。网线的总长度不会超过10^9,每个数之间用一个空格隔开。
输出:
对于每组测试数据输出N行,第i行表示i号电脑的答案 (1<=i<=N)
样例:
5
1 1
2 1
3 1
1 1
3
2
3
4
4
2. 思路历程
- 要求出第i台电脑到其他电脑的最大网线长度,可以逆向考虑其他电脑到第i台电脑的最大网线长度,而最大长度肯定是与端点之间的长度。
- 这里引入树的直径的概念,树中任意两点之间距离的最大值。
- 树的直径一定是某两个叶子之间的距离
- 从树中任选一个点开始遍历这棵树,找到一个距离这个点最远的叶子,然后再从这个叶子开始遍历,找到离这个叶子最远的另一个叶子,这两点之间的距离即树的直径。
- 这道题的最大长度就转换成了第i台电脑到直径的两个端点的最大值,加上逆向考虑即从两个端点开始dfs,沿路径方向对每台电脑的距离进行更新。
3. 具体实现
- 创建结构体Edge,包含当前节点、下一个节点和权重。
这是图的链式前向星的存储方式,用数组来模拟链表,head数组模拟头指针,相比较邻接链表而言,边在链式前向星中存储的相对顺序与输入顺序相反,其他差异不大。 - 创建vis数组用于记录节点是否被访问,数组dis1dis2分别记录各点到直径两个端点的距离,num用于记录边的下标,endpoint记录端点(由于两个端点分开计算,可以用同一变量记录)
- 初始化变量,构造好图后进入代码主体,其包含三次dfs:
- 第一次dfs用于找到端点1,可以从任意节点出发(这里取节点1),找到的路径最大的节点即为端点1
- 第二次dfs用于找到端点2,从端点2出发,找到的路径最大值即为端点2,这里dfs过程中也同时求出了每个节点i到端点1的距离
dis1[i]
- 第三次dfs用于求出每个节点i到端点2的距离
dis2[i]
- 对于每个i,dis1[i]和dis2[i]的最大值即为最大长度。
4. 代码
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
struct Edge
{
int vertex, weight, next;
}edges[100111];
bool vis[100111];
int N, num, endpoint;
int head[100111], dis1[100111], dis2[100111];
void addEdge(int u, int v, int w)
{
edges[num].vertex = v;
edges[num].weight = w;
edges[num].next = head[u];
head[u] = num;
num++;
}
void dfs(int u, int *dis)
{
vis[u] = true;
if (dis[u] > dis[endpoint]) endpoint = u;
for (int i = head[u]; i != -1; i = edges[i].next)
{
int v = edges[i].vertex;
if (!vis[v])
{
vis[v] = true;
dis[v] = dis[u] + edges[i].weight;
dfs(v, dis);
}
}
}
int main()
{
while (cin >> N)
{
num = 0;
endpoint = 1;
memset(head, -1, sizeof(head));
memset(dis1, 0, sizeof(dis1));
memset(dis2, 0, sizeof(dis2));
memset(vis, 0, sizeof(vis));
int v, w;
for (int i = 2; i <= N; i++)
{
cin >> v >> w;
addEdge(i, v, w);
addEdge(v, i, w);
}
dfs(1, dis1);
memset(dis1, 0, sizeof(dis1));
memset(vis, 0, sizeof(vis));
dfs(endpoint, dis1);
memset(vis, 0, sizeof(vis));
dfs(endpoint, dis2);
for (int i = 1; i <= N; i++)
cout << max(dis1[i], dis2[i]) << endl;
}
return 0;
}
B 带好口罩!
1. 题目大意
新型冠状病毒肺炎(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个整数代表这个小团体的学生。
输出:
输出要隔离的人数,每组数据的答案输出占一行
样例:
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
4
1
1
2. 思路历程
- 要求出有多少学生与小A同学接触,即求出多少学生与小A属于同一类,是一个并查集的问题。
- 并查集是一种用来管理元素分组状况的数据结构。
- 可以高效地进行查询元素A和元素B是否属于同一组,以及合并元素A和元素B所在的组两种操作
- 可以使用类似于树形的结构实现,但并不在意严格意义上的父子关系或树形状,只关心节点属于哪一个整体
- 合并的复杂度为
O(1)
,为了提高查询的复杂度,可以直接把节点挂在跟节点下
- 通过对所有学生进行并查集操作,把他们那都放入正确的分组,最后小A所在的组元素个数即为与小A接触的学生数。
3. 具体实现
- 创建数组parent用于记录并查集,i的根结点为
parent[i]
,初始化所有parent[i] = i
,表示每个学生自成一组。 - 函数
find(int x)
是并查集的查询函数(包含路径紧缩),函数unite(int x, int y)
是并查集的合并函数。 - 对于每个团体,找到团体第一个学生所在的组作为基准组,将该团体中其他学生所在的组与基准组合并(已同组则不操作)
- 遍历小A所在的组,计算组内元素个数。
4. 代码
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, parent[100111];
int find(int x)
{
if (parent[x] == x)
return x;
return parent[x] = find(parent[x]);
}
void unite(int x, int y)
{
x = find(x);
y = find(y);
parent[x] = y;
}
int main()
{
while (cin >> n >> m)
{
if (n == 0 && m == 0) break;
for (int i = 0; i < n; i++) parent[i] = i;
int num, id1, id;
for (int i = 0; i < m; i++)
{
cin >> num >> id1;
id1 = find(id1);
for (int j = 1; j < num; j++)
{
cin >> id;
id = find(id);
if (id != id1)
unite(id1, id);
}
}
int ans = 0;
for (int i = 0; i < n; i++)
{
if (find(i) == find(0))
ans++;
}
cout << ans << endl;
}
return 0;
}
C 掌握魔法の东东 I
1. 题目大意
东东在老家农村无聊,想种田。农田有 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值
样例:
4
5
4
4
3
0 2 2 2
2 0 3 3
2 3 0 4
2 3 4 0
9
2. 思路历程
- 化简题意为,已知每两块田之间通水的消耗和每块田自己通水的消耗,要求整块田通水的最小消耗。
- 去掉每块田自己通水的消耗,就是最小生成树问题,因此增加超级原点,把自己通水这种情况当成超级原点,则所有n个点都与超级原点相邻且路径有一个权重,然后对
n + 1
个点进行最小生成树即可。
3. 具体实现
- 创建结构体Edge,包含当前节点、下一个节点和权重(图的链式前向星存储方式),对其进行操作符重载,用于按权重排序,每次取权重最小的边。
- 创建数组parent用于记录并查集,i的根结点为
parent[i]
,初始化所有parent[i] = i
,表示所有点都还不连通。 - 函数
find(int x)
是并查集的查询函数(包含路径紧缩),函数unite(int x, int y)
是并查集的合并函数。 - 构造好图后对边进行排序,然后进行最小生成树算法
- 权重由小到大依次考虑所有点,若当前点加入图后不生成闭环则符合要求,将边加入生成树并更新权重和
- 当加入图中的边等于 n 时跳出循环(已达到树的边数)
4. 代码
#include <iostream>
#include <algorithm>
using namespace std;
struct Edge
{
int vertex, weight, next;
bool operator < (const Edge& edge) const
{
return weight < edge.weight;
}
}edges[100011];
int n, parent[100011];
int tot = 0, ans = 0, cnt = 0;
void addEdge(int u, int v, int w)
{
edges[++tot].vertex = u;
edges[tot].next = v;
edges[tot].weight = w;
}
int find(int x)
{
if (parent[x] == x)
return x;
return parent[x] = find(parent[x]);
}
void unite(int x, int y)
{
x = find(x);
y = find(y);
parent[x] = y;
}
int main()
{
cin >> n;
int w;
for (int i = 1; i <= n; i++)
{
cin >> w;
addEdge(i, 0, w);
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> w;
if (i != j)
addEdge(i, j, w);
}
}
for (int i = 1; i <= n; i++) parent[i] = i;
sort(edges + 1, edges + tot + 1);
for (int i = 1; i <= tot; i++)
{
int v1 = find(edges[i].vertex);
int v2 = find(edges[i].next);
if (v1 != v2)
{
unite(v1, v2);
ans += edges[i].weight;
cnt++;
}
if (cnt == n) break;
}
cout << ans;
return 0;
}
D 数据中心
1. 题目大意
样例:
4
5
1
1 2 3
1 3 4
1 4 5
2 3 8
3 4 2
4
2. 思路历程
- 化简题意为,构造一个连通无向图,使每个节点都与root节点连通,且完成任务所需的时间最少,时间为Tmax = max(Th)。
- 理解时,把Tmax当作T,h当作边的编号,题意即为求解一棵生成树,使得最大边全最小。
- 可以用类似最小生成树的方法求解,是最小瓶颈生成树,把原来unite后权重相加改为取最大权重即可。
3. 具体实现
- 创建结构体Edge,包含当前节点、下一个节点和权重(图的链式前向星存储方式),对其进行操作符重载,用于按权重排序,每次取权重最小的边。
- 创建数组parent用于记录并查集,i的根结点为
parent[i]
,初始化所有parent[i] = i
,表示所有点都还不连通。 - 函数
find(int x)
是并查集的查询函数(包含路径紧缩),函数unite(int x, int y)
是并查集的合并函数。
- 权重由小到大依次考虑所有点,若当前点加入图后不生成闭环则符合要求,将边加入生成树并更新最大权重
- 当加入图中的边等于 n - 1 时跳出循环(已达到树的边数)
4. 代码
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
struct Edge
{
int vertex, weight, next;
bool operator < (const Edge& edge) const
{
return weight < edge.weight;
}
}edges[200022];
int n, m, root, parent[100011];
int tot = 0, ans = 0, cnt = 0;
void addEdge(int u, int v, int w)
{
edges[++tot].vertex = u;
edges[tot].next = v;
edges[tot].weight = w;
}
int find(int x)
{
if (parent[x] == x)
return x;
return parent[x] = find(parent[x]);
}
void unite(int x, int y)
{
x = find(x);
y = find(y);
parent[x] = y;
}
int main()
{
cin >> n >> m >> root;
int u, v, w;
for (int i = 0; i < m; i++)
{
cin >> u >> v >> w;
addEdge(u, v, w);
addEdge(v, u, w);
}
for (int i = 0; i <= n; i++) parent[i] = i;
sort(edges + 1, edges + tot + 1);
for (int i = 1; i <= tot; i++)
{
int v1 = find(edges[i].vertex);
int v2 = find(edges[i].next);
if (v1 != v2)
{
unite(v1, v2);
ans = max(ans, edges[i].weight);
cnt++;
}
if (cnt == n - 1) break;
}
cout << ans;
return 0;
}