第十周
求最小生成树
Prim算法
有一个集合的S。选择任意一点加入集合,更新与该点相连的点的距离。每次都选择不在集合S所有点中距离最短的加入集合S,更新该点相连的点的距离,直到所有点都加入S。
Kruskal算法
将所有边按长短排序,每次选择剩下的边中最短且两个端点不在一个树中的边。
并查集
并查集是用来快速查询两个点是否处于同一个集合的结构。
存储某个点的父节点的数组
int parents[maxn];
初始化(将所有点的父节点设置为自己)
void initialize()
{
for (int i = 1; i <= n; i++)
{
parents[i] = i;
}
}
查找最终的父节点
int find(int a) //要查找的点
{
return parents[a] == a ? a : find(parents[a]); //a的父节点是自己(也就是a没有父节点了),a就是最终的父节点;否则就再往上找。
}
由于树的结构可能非常复杂,所以查找操作可能会非常慢。如果所有树都只有一个父节点,查询速度就会很快。所以可以在查询时顺便改造树的结构。
int find(int a)
{
return parents[a] == a ? a : parents[a] = find(parents[a]);
}
合并操作
void merge(int a, int b)
{
parents[find(a)] = find(b);
}
查询是否在同一树中
bool check(int a, int b)
{
return parents[a] == parents[b];
}
第一题
题目1
给定一个无向图,求最少用多少笔可以画出图中所有的边。
思路1
根据题解中提到的欧拉路,一张图只有在没有奇数条边的点或有两个奇数条边的点时才能一笔画出。
所以如果如果图中没有奇数条边的点或有两个奇数条边的点,就是一笔完成。往后每增加两个奇数条边的点,就需要多画一笔。
因此只需要记录每个点有多少边,就可以解体
代码1
#include<iostream>
using namespace std;
const int maxn = 1005;
//记录每个点的边数的数组、记录奇数边的点数
int cnt[maxn], ans = 0;
int main()
{
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
//记录点的边数
cnt[a]++;
cnt[b]++;
}
for (int i = 1; i <= n; i++)
{
//边数是奇数是计数
if (cnt[i] & 1)
{
ans++;
}
}
//如果是0个点,就是1,别的情况就是点数除以2
cout << (ans == 0 ? 1 : (ans / 2)) << endl;
return 0;
}
第二题
题目2
m×n 个小格子,每个格子里种了一株合根植物,它的根可能会沿着南北或东西方向伸展。告诉哪些小格子间出现了连根现象,算出这个园中一共有多少株合根植物。
思路2
很明显是并查集,默一遍并查集。
代码2
#include<iostream>
using namespace std;
int pa[1000001], n, m, ans;
//标记某个格子是否是数的根节点,用来计算有多少树
bool exist[1000001];
//下面是并查集
void initialize()
{
for (int i = 1; i <= n * m; i++)
{
pa[i] = i;
}
}
int find(int a)
{
return pa[a] == a ? a : (pa[a] = find(pa[a]));
}
void merge(int a, int b)
{
pa[find(a)] = find(b);
}
bool check(int a, int b)
{
return find(a) == find(b);
}
int main()
{
cin >> n >> m;
int k;
cin >> k;
//初始化
initialize();
for (int i = 0; i < k; i++)
{
int a, b;
cin >> a >> b;
//合并树
merge(a, b);
}
for (int i = 1; i <= m * n; i++)
{
//找到某个格子的植物所在的树的根
int p = find(i);
//这个根是第一次找到,就标记已找过,并累加计数
if (!exist[p])
{
exist[p] = true;
ans++;
}
}
cout << ans << endl;
return 0;
}
第三题
题目3
有一块大奶酪,它的高度为h,长度和宽度无限大,奶酪中间有许多半径相同的球形空洞。建立空间坐标系,在坐标系中,奶酪的下表面为z = 0,奶酪的上表面为z = h。有一只小老鼠。如果两个空洞相切或是相交,则老鼠可以从其中一个空洞跑到另一个空洞。如果一个空洞与下表面相切或是相交,老鼠则可以从奶酪下表面跑进空洞;如果一个空洞与上表面相切或是相交,老鼠则可以从空洞跑到奶酪上表面。老鼠在不破坏奶酪的情况下,能否利用已有的空洞从奶酪的下表面跑到奶酪的上表面去?
思路3
仍然是并查集,但是合并的判定会比较复杂,需要运用数学知识。如果有一个洞在上表面,一个洞在下表面,这两个洞在并查集中又属于同一个树,则能从下面到上面。
代码3
#include<iostream>
#include<cstring>
using namespace std;
//并查集
int pa[1001];
//坐标(不开long long的话,计算距离就爆了)
long long cx[1001], cy[1001], cz[1001];
//记录每个洞是否在上表面和下表面
bool istop[1001], isbottom[1001];
//判断两个洞是否相交
bool isIntersect(int a, int b, long long r)
{
//数学
return 4 * r * r >= (cx[a] - cx[b]) * (cx[a] - cx[b]) + (cy[a] - cy[b]) * (cy[a] - cy[b]) + (cz[a] - cz[b]) * (cz[a] - cz[b]);
}
//并查集
void initialize(int n)
{
for (int i = 1; i <= n; i++)
{
pa[i] = i;
}
}
int find(int a)
{
return pa[a] == a ? a : (pa[a] = find(pa[a]));
}
void merge(int a, int b)
{
pa[find(a)] = find(b);
}
bool check(int a, int b)
{
return find(a) == find(b);
}
int main()
{
int t;
cin >> t;
while (t--)
{
int n, h;
long long r;
cin >> n >> h >> r;
//初始化所有的数组
initialize(n);
memset(isbottom, 0, sizeof(isbottom));
memset(istop, 0, sizeof(istop));
for (int i = 1; i <= n; i++)
{
cin >> cx[i] >> cy[i] >> cz[i];
//遍历已有的所有洞,判断是否相交,相交就合并
for (int j = 1; j < i; j++)
{
//相交
if (isIntersect(i, j, r))
{
merge(i, j);
}
}
//在下表面
if (cz[i] - r <= 0)
{
isbottom[i] = true;
}
//在上表面
if (cz[i] + r >= h)
{
istop[i] = true;
}
}
//标记这个子问题答案是否为Yes
bool isYes = false;
//外层循环找在上表面的洞
for (int i = 1; i <= n; i++)
{
//不在上表面就跳过
if (!istop[i])
{
continue;
}
//内存循环找在下表面的洞
for (int j = 1; j <= n; j++)
{
//不在下表面就跳过
if (!isbottom[j])
{
continue;
}
//两个洞在同一个树中
if (check(i, j))
{
isYes = true;
cout << "Yes" << endl;
break;
}
}
//已得到答案跳过剩下的循环
if (isYes)
{
break;
}
}
//没找到,就是不行
if (!isYes)
{
cout << "No" << endl;
}
}
return 0;
}
第四题
题目4
所有村庄都造成了一定的损毁,在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车。给出每个村庄重建完成的时间,求在某个时间两个村庄间的最短距离。
思路4
这是一个求最短路径的问题。但是由于有多组询问,每次询问都进行求最短路较慢。
解答中提供了使用Floyd算法解决的思路。Floyd通过间断点来更新最短距离,适合题目的村庄重建。
一个村庄重建之后,只需要把这个村庄作为间断点更新一次最短距离,就能很快的得到最新的最短距离。
代码4
#include<iostream>
#include<cstring>
using namespace std;
int main()
{
//n、m、村庄重建的时间、村庄间的距离
int n, m, village[201], path[201][201];
//初始化距离无穷大
memset(path, 0x3f, sizeof(path));
cin >> n >> m;
for (int i = 0; i < n; i++)
{
cin >> village[i];
//自己到自己的距离是0
path[i][i] = 0;
}
for (int i = 0; i < m; i++)
{
int from, to;
cin >> from >> to;
cin >> path[from][to];
path[to][from] = path[from][to];
}
int q;
cin >> q;
//当前的时间,第几个村庄重建
int now = 0, vcount = 0;
while (q--)
{
int x, y, t;
cin >> x >> y >> t;
//当前的时间没到询问时间
while (now <= t)
{
//当前的村庄重建的时间在当前时间之前
while (village[vcount] <= now && vcount < n)
{
//更新距离
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
path[i][j] = min(path[i][vcount] + path[vcount][j], path[i][j]);
}
}
//下一个村庄
vcount++;
}
//下一天
now++;
}
//村庄还未重建
if (village[x] > t || village[y] > t)
{
cout << -1 << endl;
}
//村庄已重建
else
{
cout << (path[x][y] == 0x3f3f3f3f ? -1 : path[x][y]) << endl;
}
}
return 0;
}
第五题
题目5
雨林的地表还是被大水淹没着,部分植物的树冠露在水面上。给出树坐标。有M个猴子跳跃的能力不同,问能在所有树冠上觅食的猴子数量。
思路5
是一个最小生成树的问题。为每两个树都添加上一条边,求出最小生成树,根据树中最长的边判断猴子即可。
代码5
Kruskal算法
#include<iostream>
#include<algorithm>
using namespace std;
//并查集、m、存放每个猴子的跳跃距离、坐标
int pa[1001], m, mdis[501], n, cx[1001], cy[1001];
//存放边
struct edge
{
//两个端点
int a, b;
//距离
int d;
}e[1001 * 1001];
//并查集
void initialize()
{
for (int i = 1; i <= n; i++)
{
pa[i] = i;
}
}
int find(int a)
{
return pa[a] == a ? a : pa[a] = find(pa[a]);
}
void merge(int a, int b)
{
pa[find(a)] = find(b);
}
bool check(int a, int b)
{
return find(a) == find(b);
}
int main()
{
cin >> m;
for (int i = 1; i <= m; i++)
{
cin >> mdis[i];
}
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> cx[i] >> cy[i];
}
//记录当前是第几条边
int cnt = 0;
//生成边
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < i; j++)
{
e[++cnt].a = i;
e[cnt].b = j;
e[cnt].d = (cx[i] - cx[j]) * (cx[i] - cx[j]) + (cy[i] - cy[j]) * (cy[i] - cy[j]);
}
}
//按边长度排序
sort(e + 1, e + cnt + 1, [](edge left, edge right){return left.d < right.d;});
//当前是第i条边,已经选了p条边
int i = 1, p = 1;
//最大的边的距离
//初始化
int maxd = 0;
initialize();
//Kruskal
while (i <= n - 1)
{
if (!check(e[p].a, e[p].b))
{
merge(e[p].a, e[p].b);
i++;
//记录最大边
maxd = max(maxd, e[p].d);
}
p++;
}
//答案
int ans = 0;
for (int j = 1; j <= m; j++)
{
//因为都以距离的平方记录,这里也要平方
if (maxd <= mdis[j] * mdis[j])
{
ans++;
}
}
cout << ans << endl;
return 0;
}