程序设计思维与实践 Week6 作业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

分析

本题的解题思路:
在这里插入图片描述
此时的存储方式可以是邻接表
在这里插入图片描述
或者是链式前向星,其补充知识可以参考【链式前向星+存图】讲解
在这里插入图片描述
在这里插入图片描述
本来还有一种邻接矩阵的存储方式,但是复杂度过高,这里不适用。邻接矩阵的部分相关知识如下:
在这里插入图片描述
该题我用的是邻接表。

  1. 构造vector<pair<int,int>> G[10010];则G[i].push_back({ a,b });表示i邻接a点,边的权值为b。每次循环输入n后记得要G[i].clear();
  2. ans[u]用来记录到u的距离的最大值,会不断更新。每次重新输入n后也要记得ans[i] = 0;
  3. 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

分析

考虑:

  1. 如果没有把“黄河之水天上来”删掉,那么就是一个最小生成树问题。
  2. 考虑图重构:加一个超级源点 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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值