WHUT第八周训练整理
写在前面的话:我的能力也有限,错误是在所难免的!因此如发现错误还请指出一同学习!
索引
(难度由题目自身难度与本周做题情况进行分类,仅供新生参考!)
零、并查集与最小生成树
一、easy:01、02、03、04、05、06、07、08、09、10、11、12、13
二、medium:14、15、16、17、19、22、23、25、26
三、hard:18、20、21、24
本题解报告大部分使用的是C++语言,在必要的地方使用C语言解释。
零、并查集与最小生成树
两个知识点的学习相信大家已经自己在网上学习过了,这里就大概提一下就可以了。
首先并查集是一个性能很强的工具,路径压缩过后几乎可以在 O ( 1 ) O(1) O(1) 的时间内对一类相同元素找到代表元以及两类不同元素的合并,并查集所花费的时间可以说是常数级别的。注意,使用并查集的前提是集合中的所有元素之间的联系都是双向的且具有传递性,如果仅仅只是单向的关系或者不具有传递性,那么不能使用并查集。
而最小生成树求的就是一张图中将所有点连通且总权值最大的树,它分为两种常用的算法:Kruskal 以及 Prim,前者在稀疏图(点多边少)中的表现更好,后者在稠密图(点少边多)种的表现更好。Kruskal 的时间复杂度为 O ( E l o g E ) O(ElogE) O(ElogE),而 Prim 的时间复杂度为 O ( N 2 ) O(N^2) O(N2),Prim 若使用堆优化则时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)。( E E E 表示边数, N N N 表示点数)
Kruskal 的实现需要使用并查集,而 Prim 则不需要。
一、easy
1001:How Many Tables(并查集)
题意:摆桌子,只有相互认识的人才能坐在同一张桌子旁,认识具有传递性,如果 A A A 认识 B B B,而 B B B 认识 C C C,那么 A A A 就认识 C C C。现在有 N N N 个人, M M M 种关系,问至少需要多少张桌子?
范围: 1 ≤ N , M ≤ 1000 1 \le N,M \le 1000 1≤N,M≤1000
分析:典型的并查集背景,将有关系的人放在同一个集合中,最后遍历所有人记录不同集合的个数即可。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
int n, m;
int fa[MAXN]; // 每个集合的代表元
// 查
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
// 并
void unin(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
}
int vis[MAXN]; // 标记第i个集合有没有出现过
signed main()
{
int T;
cin >> T;
while (T--)
{
memset(vis, 0, sizeof(vis)); // 清空数组
cin >> n >> m;
// 使用并查集之前必须初始化,每个元素的代表元一开始都是自己
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 0; i < m; i++)
{
int u, v;
cin >> u >> v;
unin(u, v); // 有关系的并起来
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
int fx = find(i);
// 如果该集合第一次出现,则统计答案
if (!vis[fx])
{
vis[fx] = 1;
ans++;
}
}
cout << ans << endl;
}
return 0;
}
1002:小希的迷宫(并查集)
题意:有 N N N 个房间,若干条通道双向连接两个房间,问现在的布局是否满足任意两个房间都可以相通,并且路径是唯一的。
范围: 1 ≤ N ≤ 1 e 5 1 \le N \le 1e5 1≤N≤1e5
分析:首先,道路是无向边,房间的连通关系满足传递性。现在问任意两个房间是否都相通,则可以用并查集判断是否所有的房间都在同一个集合中。题目还要求路径是唯一的,就是说图中不能出现环,并查集也可以判断,当加入的边两个端点已经属于同一个集合的时候,那么就说明出现了环。
Notice:注意细节,本题的输入还是要仔细处理的,详见代码。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int fa[MAXN], vis[MAXN];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void unin(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
}
int main()
{
int u, v;
int maxV = 0; // 用来确定房间的数目
int flag = 0; // 标记是否破坏规则
for (int i = 1; i < MAXN; i++)
fa[i] = i;
while (cin >> u >> v)
{
if (u == v && u == -1)
break;
// 出现两个0,说明所有的关系已经给出,现在判断结果
if (u == v && u == 0)
{
int f = -1; // 唯一的集合标号
for (int i = 1; i <= maxV; i++)
{
if (!vis[i]) // 没有出现的房间不用管
continue;
int fx = find(i);
if (f == -1)
f = fx;
else if (f != fx) // 出现不同集合
flag = 1;
}
if (flag)
{
cout << "No" << endl;
}
else
{
cout << "Yes" << endl;
}
// 初始化
for (int i = 1; i <= maxV; i++)
{
fa[i] = i;
vis[i] = 0;
}
flag = maxV = 0;
continue;
}
vis[u] = vis[v] = 1; // 这两个房间出现过
maxV = max(maxV, max(u, v)); // 最大的房间编号
int fx = find(u), fy = find(v);
if (fx == fy)
flag = 1;
unin(fx, fy);
}
return 0;
}
1003:Is It A Tree?(并查集)
题意:给出若干个关系,判断这张图是否为一颗树。
范围:未明确指出。
分析:需要满足三个条件:
- 只有一个入度为 0 0 0 的点
- 其余所有点的入度都为 1 1 1
- 所有点都连通
无向边,且连通关系满足传递性,可以使用并查集。本题跟 1002 1002 1002 类似,只需要在其基础上加入入度数组 i n E d g e inEdge inEdge 判断入度条件即可。详见代码。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 10;
int fa[MAXN], vis[MAXN], inEdge[MAXN];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void unin(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
}
int main()
{
int u, v;
int kase = 1;
int maxV = 0;
int flag = 0;
for (int i = 1; i < MAXN; i++)
fa[i] = i;
while (cin >> u >> v)
{
if (u == v && u == -1)
break;
if (u == v && u == 0)
{
int cnt = 0; // 入度为0的数量
int f = -1; // 判断条件3
for (int i = 1; i <= maxV; i++)
{
if (!vis[i])
continue;
if (!inEdge[i])
cnt++;
if (cnt > 1 || inEdge[i] > 1) // 条件12
flag = 1;
int fx = find(i);
if (f == -1)
f = fx;
else if (f != fx) // 条件3
flag = 1;
}
cout << "Case " << kase++ << " is ";
if (flag)
{
cout << "not a tree." << endl;
}
else
{
cout << "a tree." << endl;
}
for (int i = 1; i <= maxV; i++)
{
fa[i] = i;
vis[i] = 0;
inEdge[i] = 0;
}
flag = maxV = 0;
continue;
}
vis[u] = vis[v] = 1;
maxV = max(maxV, max(u, v));
inEdge[v]++; // 入度增加
int fx = find(u), fy = find(v);
if (fx == fy)
flag = 1;
unin(fx, fy);
}
return 0;
}
1004:More is better(并查集)
题意:一个房间中有很多个男孩,有 N N N 种朋友关系,可以指定任意个男孩出去,要保证最后留在房间里面的男孩都是直接或间接的朋友,问最后留在房间里面的男孩数量最多可以是多少?
范围: 1 ≤ N ≤ 1 e 5 1 \le N \le 1e5 1≤N≤1e5,男孩的数量可能有 10000000 10000000 10000000 个
分析:这题是真的暴力啊,这种 1 e 7 1e7 1e7 的数据范围多组输入都可以这样暴力,大拇指好吧。
首先朋友关系是双向的,并且是传递的,可以使用并查集。这个问题的本质就是问这些男孩之中最大的相互认识的团体的大小,即集合的大小。因此可以使用并查集计算每个集合中的元素个数,那么答案就是所有集合中最大的个数。
Notice:直接用 c i n cin cin 会超时的!用 s c a n f scanf scanf 或者关闭流同步!而且注意 n = = 0 n==0 n==0 的情况!
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e7 + 10;
int n, ans;
int fa[MAXN], num[MAXN];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void unin(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
num[fy] += num[fx]; // 此时fy是代表元,集合的大小增加了fy所在集合的大小
ans = max(ans, num[fy]); // 取最大值
}
int main()
{
while (~scanf("%d", &n))
{
if (n == 0)
{
cout << 1 << endl;
continue;
}
// 暴力1e7初始化!男孩的序号可能到1e7
for (int i = 0; i < MAXN; i++)
{
fa[i] = i;
num[i] = 1; // 每个集合一开始只有一个元素
}
ans = 0;
for (int i = 0; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
unin(u, v);
}
cout << ans << endl;
}
return 0;
}
1005:Constructing Roads(Kruskal)
题意:有 N N N 个村庄, M M M 条已经修好的边,现在问还需要修哪些边,才能在最小的花费下实现连通所有的村庄。
范围: 1 ≤ N ≤ 100 , 0 ≤ M ≤ N ∗ ( N − 1 ) 2 1 \le N \le 100~,~0\le M \le \frac{N*(N-1)}{2} 1≤N≤100 , 0≤M≤2N∗(N−1)
分析:明显是要我们求最小生成树,但是考虑到已经有现成的边可以白嫖,所以在求的时候就可以把已经连通的村庄整体看成一个村庄,可以使用并查集让他们在同一个集合里面。既然使用并查集了,还需要求最小生成树,那就自然用 Kruskal 啦。
回顾 Kruskal,首先将所有的边按照长度从小到大排序,然后每次选出最小的边,看两端点是否已经连通,连通则跳过,否则加上这条边。而这道题目要求构造的最小生成树有点不同,图中已经有一些边存在,所以我们可以先将这些边的端点利用并查集连接,然后再贪心地选择长度最短的边加入答案即可。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
int n, q;
// 结构体——边,重载运算符 < 实现自定义排序规则,sort时按照长度排序
struct Edge
{
int u, v, len;
bool operator<(Edge other) const
{
return len < other.len;
}
} edges[MAXN * MAXN];
int fa[MAXN];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void unin(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
}
int main()
{
while (cin >> n)
{
for (int i = 1; i <= n; i++)
fa[i] = i;
int index = 0;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
int v;
cin >> v;
edges[index++] = {
i, j, v}; // 这样写比较方便,可以参考
}
}
sort(edges, edges + index); // 按长度从小到大排序
cin >> q;
// 已经修好的路所连接的村庄放入同一个集合
for (int i = 0; i < q; i++)
{
int u, v;
cin >> u >> v;
unin(u, v);
}
int ans = 0;
// Kruskal
for (int i = 0; i < index; i++)
{
int u = edges[i].u, v = edges[i].v, len = edges[i].len;
int fx = find(u), fy = find(v);
if (fx == fy)
continue;
ans += len;
unin(fx, fy);
}
cout << ans << endl;
}
return 0;
}
1006:畅通工程(并查集)
题意:有 N N N 个城市, M M M 条道路已经铺好,现在问还需要多少条边才能让所有的城市相互连通。
范围: 1 ≤ N ≤ 1000 1 \le N \le 1000 1≤N≤1000, M M M 的范围未指出
分析:道路是双向的,连通关系具有传递性,可以使用并查集。首先已经连通的城市整体可以看成是一个大城市,如果这样处理完之后图中只剩下孤立的点,表示一个个大城市,现在要让所有城市连通,就是要让剩下的这些点连通。假设剩下 A A A 个大城市,那么我们只需要 A − 1 A-1 A−1 条边就可以形成一颗树,实现连通。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000 + 10;
int n, m;
int fa[MAXN], vis[MAXN];
int find(int x)
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void unin(int x, int y)
{
int fx = find(x), fy = find(y);
if (fx == fy)
return;
fa[fx] = fy;
}
int main()
{
while (~scanf("%d", &n), n)
{
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n; i++)
fa[i] = i;
scanf("%d", &m);
for (int i = 0; i < m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
unin(u, v); // 形成大城市
}
int cnt = 0; // 大城市数量
for (int i = 1; i <= n; i++)
{
int fx = find(i);
if (vis[fx])
continue;
vis[fx] = 1;
cnt++;
}
printf("%d\n", cnt - 1); // 大城市之间形成树
}
return 0;
}
1007:还是畅通工程(最小生成树)
题意:有 N N N 个城市,给出任意两个城市之间的距离,求让这些城市连通的最小边权总和。
范围: 1 ≤ N ≤ 100 1 \le N \le 100 1≤N≤100
分析:最小生成树板子题。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;
int n;
int F[MAXN]; //并查集使用
struct Edge
{
int u; //起点
int v; //终点
int w; //权值
} edge[MAXM]; //存储边的信息
int tol; //边数,加边前赋值为0
void addEdge(int u, int v, int w)
{
edge[tol].u = u;
edge[tol].v = v;
edge[tol++].w = w;
return;
}
bool cmp(Edge a, Edge b)
{
//排序函数,将边按照权值从小到大排序
return a.w < b.w;
}
int find(int x)
{
if (F[x] == -1)
{
return x;
}
else
{
return F[x] = find(F[x]);
}
}
int Kruskal(int n) //传入点数,返回最小生成树的权值,如果不连通则返回-1
{
memset(F, -1, sizeof(F));
sort(edge, edge + tol, cmp);
int cnt = 0; //计算加入的边数
int ans = 0;
for (int i = 0; i < tol; i++)
{
int u = edge[i].u;
int v = edge[i].v;
int w = edge[i].w;
int tOne = find(u);
int tTwo = find(v);
if (tOne != tTwo)
{
ans += w;
F[tOne] = tTwo;
cnt++;
}
if (cnt == n - 1)
{
break;
}
}
if (cnt < n - 1)
{
return -1; //不连通
}
else
{
return ans;
}
}
int main()
{
while (~scanf("%d", &n), n)
{
tol = 0;
for (int i = 0; i < n * (n - 1) / 2; i++)
{
int u, v, len;
scanf("%d%d%d", &u, &v, &len);
addEdge(u, v, len);
}
cout << Kruskal(n) << endl;
}
return 0;
}
1008:畅通工程(最小生成树)
题意:有 N N N 个城市, M M M 条双向边,求让这些城市连通的最小边权总和。
范围: 1 ≤ N ≤ 100 1 \le N \le 100 1≤N≤100
分析:还是最小生成树板子题,不存在 M S T MST MST 则输出 “?” 。
Code:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100 + 10;
const int MAXM = MAXN * MAXN;
int n, m;
int F[MAXN]; //并查集使用
struct Edge
{
int u; //起点
int v; //终点
int w; //权值
} edge[MAXM]; //存储边的信息
int tol; //边数,加边前赋值为0
void addEdge(int u, int v, int w)
{
edge[tol].u = u;
edge[tol].v = v;
edge[tol++].w = w;
return;
}
bool cmp(Edge a, Edge b)
{
//排序函数,将边按照权值从小到大排序
return a.w < b.w;
}
int find(int x)
{
if (F[x] == -1)
{
return x;
}
else
{
return F[x] = find(F[x]);
}
}
int Kruskal(