机试: 图 (图的存储、图的遍历、最短路径、最小生成树、拓扑排序、关键路径)

  • 参考《算法笔记》

图的存储

邻接矩阵

  • 用一个二维数组表示,在顶点数小于 1000 时使用 (否则空间开销太大)

邻接表

  • 每个顶点的出边可以偷懒用 vector 表示 (不用链表),因此如果邻接表只存放每条边的终点编号,而不存放边权, 则 vector 中的元素类型可以直接定义为 int 型:
vector<int> Adj[N];

在这里插入图片描述

  • 如果需要同时存放边的终点编号和边权, 那么可以建立结构体 Node, 用来存放每条边的终点编号和边权
struct Node {
	int v; //边的终点编号
	int w; //边权
};
vector<Node> Adj[N];

图的遍历

DFS

const int MAXV = 1000; 			// 最大顶点数
const int INF = 1000000000;

邻接矩阵版

int n, G[MAXV][MAXV]; 		// n 为顶点数
bool vis[MAXV] = {false};

void DFS(int u, int depth) { // u 为当前访问的顶点标号, depth 为深度
	vis[u] = true;
	// 如果需要对 u 进行一些操作, 可以在这里进行

	for(int v = 0; v < n; v++) {
		if (vis[v] == false && G[u][v] != INF) {
			DFS(v, depth + 1);
		}
	}
}

void DFSTrave() { 
	for(int u = 0; u < n; u++) {
		if (vis[u] == false) {
			DFS(u, 1);
		}
	}

邻接表版

vector<int> Adj[MAXV];
int n;
bool vis[MAXV] = {false};

void DFS(int u, int depth) {
	vis[u] = true;
	// 如果需要对 u 进行一些操作, 可以在这里进行
	
	for (int i = 0; i < Adj[u].size(); i++) {
		int v = Adj[u][i];
		if (vis[v] == false) {
			DFS(v, depth + 1);
		}
	}
}

void DFSTrave() {
	for(int u = 0; u < n; u++){
		if (vis[u] == false) {
			DFS(u, 1);
		}
	}
}

BFS

邻接矩阵版

int n, G[MAXV][MAXV];
bool inq[MAXV] = {false};

void BFS(int u) {
	queue<int> q;
	q.push(u);
	inq[u] = true;
	while (!q.empty()){
		int u = q.front();
		q.pop();
		// 可以在这里访问节点
		for(int v = 0; v < n; v++) {
			if (inq[v] == false && G[u][v] != INF) {
				q.push(v);
				inq[v] = true;
			}
		}
	}
}

void BFSTrave() {
	for(int u = 0; u < n; u++) {
		if(inq[u] == false) {
			BFS(q);
		}
	}
}

邻接表版

vector<int> Adj[MAXV];
int n;
bool inq[MAXV] = {false};

void BFS(int u){
	queue<int> q;
	q.push(u);
	inq[u] = true;
	while(!q.empty()){
		int u = q.front();
		q.pop();
		// 可以在这里访问节点
		for(int i = 0; i < Adj[u].size(); i++) {
			int v = Adj[u][i];
			if(inq[v] == false) {
				q.push(v);
				inq[v] = true;
			}
		}
	}
}

void BFSTrave() {
	for(int u = 0; u < n; u++) {
		if(inq[u] == false) {
			BFS(q);
		}
	}
}
// 输出顶点的层号
struct Node {
	int v;
	int layer; // 顶点层号
};

vector<Node> Adj[MAXV];
int n;
bool inq[MAXV] = {false};

void BFS(int s){
	queue<Node> q;
	q.push({s, 0});
	inq[s] = true;
	
	while(!q.empty()){
		Node topNode = q.front();
		q.pop();
		int u = topNode.v;
		// 可以在这里访问节点
		for(int i = 0; i < Adj[u].size(); i++) {
			Node next = Adj[u][i];
			next.layer = topNode.layer + 1;
			if(inq[next.v] == false) {
				q.push(next);
				inq[next.v] = true;
			}
		}
	}
}

void BFSTrave() {
	for(int u = 0; u < n; u++) {
		if(inq[u] == false) {
			BFS(q);
		}
	}
}

PAT (Advanced Level) 1013 Battle Over Cities

题目在此

  • 这题的主要思路就是计算连通分量的个数,最后的结果为连通分量个数减一
    • 如果用图的遍历来做还是比较容易的;关于删去某个顶点,我的思路是把该顶点的 vis 设为 true,没必要真的进行删除
    • 也可以用并查集来做 (更加繁琐,但提供了一种不一样的思路): 判断无向图每条边的两个顶点是否在同一个集合内, 如果在同一个集合内, 则不做处理; 否则, 将这两个顶点加入同一个集合。 最后统计集合的个数即可 (连通分量个数)
// 图的遍历
#include <cstdio>
#include <vector>

using namespace std;

const int MAXV = 1001;

vector<vector<int>> Adj(MAXV);
bool vis[MAXV];

int N, M, K;

void DFS(int v, vector<vector<int>> &graph)
{
	vis[v] = true;
	for (size_t i = 0; i != graph[v].size(); ++i)
	{
		int adj = graph[v][i];
		if (!vis[adj])
		{
			DFS(adj, graph);
		}
	}
}

int main()
{
	scanf("%d %d %d", &N, &M, &K);

	while (M--)
	{
		int from, to;
		scanf("%d %d", &from, &to);
		Adj[from].push_back(to);
		Adj[to].push_back(from);
	}
	while (K--)
	{
		int city;
		scanf("%d", &city);
		for (int i = 1; i <= N; ++i)
		{
			vis[i] = false;
		}
		vis[city] = true;	// 遍历时不考虑被占领的城市
		int cnt = 0;	// 连通分量的个数

		for (int i = 1; i <= N; ++i)
		{
			if (!vis[i])
			{
				DFS(i, Adj);
				++cnt;
			}
		}
		printf("%d\n", cnt - 1);
	}
	
	return 0;
}
// 并查集 (代码来自《算法笔记》)
#include <cstdio>
#include <vector>
using namespace std;
const int N = 1111;
vector<int> G[N]; // 邻接表

int father[N];
bool vis[N];

int findFather(int x) {
	int a = x;
	while (x != father[x]) {
		x = father[x];
	}
	while(a != father[a]) {
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}

void Union(int a, int b) {
	int faA = findFather(a);
	int faB = findFather(b);
	if (faA != faB) {
		father[faA] = faB;
	}
}

// 初始化 father 数组与 hashTable 数组
void init() {
	for(int i = 1; i < N; i++) {
		father[i] = i;
		vis[i] = false;
	}
}

int n, m, k;
int main() { 
	scanf("%d%d%d", &n, &m, &k);
	for(int i = 0; i < m; i++) {
		int a, b;
		scanf("%d%d", &a, &b);
		G[a].push_back(b);
		G[b].push_back(a);
	}
	int currentPoint; // 当前需要删除的顶点编号
	for(int query = 0; query < k; query++) {
		scanf("%d", &currentPoint);
		init();
		for {int i = 1; i <= n; i++) {
			for(int j = 0; j < G[i].size(); j++) {
				int u = i, v = G[i][j]; 	// 边的两个端点 u, v
				if(u == currentPoint || v= = currentPoint) continue;
				Union(u, v);
			}
		}
		int block = 0; // 连通块个数
		for(int i = 1; i <= n; i++) ( // 遍历所有结点
			if(i == currentPoint) continue;
			int fa_i = findFather(i); 
			if(vis[fa_i] == false) { // 如果当前连通块的根结点未被访问
				block++;
				vis[fa_i] = true;
			}
		}
		printf("%d\n", block - 1);
	}
	return 0;
}

PAT (Advanced Level) 1021 Deepest Root

题目在此

  • 首先用并查集或图的遍历判定给定的图有几个连通分量,如果只有一个,则一定为树,否则输出连通分量的个数
  • 之后需要选择合适的根结点, 使得树的高度最大。我只想到暴力方法:从每个结点开始都进行一次 BFS/DFS,选择其中 BFS/DFS 能搜索到最深高度的结点作为根结点 (很幸运这道题并没有卡时间)。实际上还有更简单的做法:
    • 先任意选择一个结点, 从该结点开始遍历整棵树, 获取能达到的最深的顶点(记为结点集合 A A A): 然后从集合 A A A 中任意一个结点出发遍历整棵树,获取能达到的最深的顶点(记为结点集合 B B B)。这样集合 A A A 与集合 B B B 的并集即为所求的使树高最大的根结点
    • 直观上还是比较好理解的,书上有具体证明,这里略去

下面同时写了 DFS 和 BFS,总的来说还是最好用 DFS,比较简单

#include <cstdio>
#include <vector>
#include <queue>
#include <algorithm>
#include <set>

using namespace std;

const int MAXV = 10001;
int N;

vector<int> G[MAXV];
bool vis[MAXV];

struct Node {
	int vertex;
	int layer;
};

void BFS(int v, set<int>& deepest_nodes)	// deepest_nodes 用来记录从 v 开始遍历,搜索到的最深的结点
{
	vis[v] = true;

	int max_height = 0;
	queue<Node> q;
	q.push({ v, 0 });
	deepest_nodes = set<int>{ v };

	while (!q.empty())
	{
		Node topNode = q.front();
		int u = topNode.vertex;
		int new_layer = topNode.layer + 1;
		q.pop();
		for (size_t i = 0; i != G[u].size(); ++i)
		{
			int adj = G[u][i];

			if (!vis[adj])
			{
				q.push({ adj, new_layer });
				vis[adj] = true;
				if (new_layer > max_height)
				{
					max_height = new_layer;
					deepest_nodes = set<int>{ adj };
				}
				else if (new_layer == max_height)
				{
					deepest_nodes.insert(adj);
				}
			}
		}
	}
}

void DFS(int v, int height, int& max_height, set<int>& deepest_nodes)
{
	vis[v] = true;
	if (height > max_height)
	{
		deepest_nodes = set<int>{ v };
		max_height = height;
	}
	else if (height == max_height) {
		deepest_nodes.insert(v);
	}
	for (size_t i = 0; i != G[v].size(); ++i)
	{
		int adj = G[v][i];
		if (!vis[adj])
		{
			DFS(adj, height + 1, max_height, deepest_nodes);
		}
	}
}

int main()
{
	scanf("%d", &N);

	for (int i = 0; i != N - 1; ++i)
	{
		int a, b;
		scanf("%d %d", &a, &b);
		G[a].push_back(b);
		G[b].push_back(a);
	}

	int block_cnt = 0;
	set<int> deepest_nodes_A, deepest_nodes_B;
	for (int i = 1; i <= N; ++i)
	{
		if (!vis[i])
		{
			int max_height = -1;
			DFS(i, 0, max_height, deepest_nodes_A);
			++block_cnt;
		}
	}
	if (block_cnt == 1)
	{
		for (int j = 1; j <= N; ++j)
		{
			vis[j] = false;
		}
		//BFS(*deepest_nodes_A.begin(), deepest_nodes_B);
		int max_height = -1;
		DFS(*deepest_nodes_A.begin(), 0, max_height, deepest_nodes_B);
		deepest_nodes_A.insert(deepest_nodes_B.begin(), deepest_nodes_B.end());
		for (const auto& root : deepest_nodes_A)
		{
			printf("%d\n", root);
		}
	}
	else {
		printf("Error: %d components", block_cnt);
	}

	return 0;
}

PAT (Advanced Level) 1034 Head of a Gang

题目在此

  • 总的来说,第一次做这种题还是比较耗时间的,之后可以多看看,摸清套路
    • (1) 姓名与编号的转换,我用的字符串 hash,也可以直接使用 map<string, int>
    • (2) 我是在 DFS 的时候计算点权,更简单的方法是在输入的时候就计算点权
    • (3) 如果用邻接矩阵的话,似乎代码又会简单好多
  • 如果是在输入的时候就计算出点权的话,还可以用并查集来做,只要注意合并函数中需要总是保持点权更大的结点为集合的根结点即可
#include <vector>
#include <cstdio>
#include <utility>
#include <string>
#include <algorithm>

using namespace std;

int N, K;
const int MAXV = 26 * 26 * 26;

struct Node {
	int v;
	int weight;
};
vector<Node> Adj[MAXV];
bool vis[MAXV];
vector<pair<string, int>> Ans;	// <head, n_mem>

int name2id(char name[3])
{
	int id = 0;
	for (int i = 0; i != 3; ++i)
	{
		id = id * 26 + (name[i] - 'A');
	}
	return id;
}

string id2name(int id)
{
	string name;
	name.resize(3);

	for (int i = 2; i >= 0; --i)
	{
		name[i] = id % 26 + 'A';
		id /= 26;
	}
	return name;
}

// 计算该连通分量的 n_mem 和 total_weight 
// 实际用 weights 记录每个连通分量中每个成员的 <id, weight(点权)> 
// 		-> n_mem 为 weights.size(); total_weight 为所有成员点权的一半
void DFS(int v,  vector<pair<int, int>> &weights)
{
	vis[v] = true;
	int total_weight = 0;
	for (size_t i = 0; i != Adj[v].size(); ++i)
	{
		int u = Adj[v][i].v;
		total_weight += Adj[v][i].weight;	// 每条边都应该被计算
		if (vis[u] == false)
		{
			DFS(u, weights);
		}
	}
	weights.push_back({ v, total_weight });
}

int main()
{
	scanf("%d %d", &N, &K);

	for (int i = 0; i != N; ++i)
	{
		char name1[4], name2[4];
		int weight;
		scanf("%s %s %d", name1, name2, &weight);
		int id1 = name2id(name1), id2 = name2id(name2);

		int flag = false;
		for (size_t j = 0; j != Adj[id1].size(); ++j)
		{
			if (Adj[id1][j].v == id2)	// 无向图添加边 AAA 打 BBB 的电话与 BBB 打 AAA 的电话意思一样
			{
				Adj[id1][j].weight += weight;
				flag = true;
			}
		}
		if (!flag)
		{
			Adj[id1].push_back({ id2, weight });
		}
		flag = false;
		for (size_t j = 0; j != Adj[id2].size(); ++j)
		{
			if (Adj[id2][j].v == id1)	// 无向图添加边 AAA 打 BBB 的电话与 BBB 打 AAA 的电话意思一样
			{
				Adj[id2][j].weight += weight;
				flag = true;
			}
		}
		if (!flag)
		{
			Adj[id2].push_back({ id1, weight });
		}
	}

	for (int i = 0; i != MAXV; ++i)
	{
		if (vis[i] == false && !Adj[i].empty())
		{
			vector<pair<int, int>> weights;
			DFS(i, weights);
			// 检查该连通分量是否为 gang (n_mem > 2 && weight > K)
			if (weights.size() > 2)
			{
				int total_weight = weights[0].second;
				int max_weight = weights[0].second;
				int max_id = weights[0].first;
				for (size_t j = 1; j != weights.size(); ++j)
				{
					if (weights[j].second > max_weight)
					{
						max_weight = weights[j].second;
						max_id = weights[j].first;
					}
					total_weight += weights[j].second;
				}
				if (total_weight / 2 > K)	// total_weight 是该连通分量中所有边权重之和 (每条边算了两次,因此要除以 2)
				{
					Ans.push_back({ id2name(max_id), weights.size() });
				}
			}
		}
	}
	sort(Ans.begin(), Ans.end(), [](const pair<string, int>& lhs, const pair<string, int>& rhs) -> bool {
		return lhs.first < rhs.first;
		});
	printf("%u\n", Ans.size());
	for (size_t i = 0; i != Ans.size(); ++i)
	{
		printf("%s %d\n", Ans[i].first.c_str(), Ans[i].second);
	}
	
	return 0;
}

PAT (Advanced Level) 1076 Forwards on Weibo

题目在此

  • 这题显然适合用 BFS 做
#include <cstdio>
#include <queue>

using namespace std;

const int MAXV = 1001;

int N, L;
int Graph[MAXV][MAXV];
bool inq[MAXV];

struct Node {
	int v;
	int layer;
};

void BFS(int s, int& cnt)
{
	for (int i = 0; i <= N; ++i)
	{
		inq[i] = false;
	}

	queue<Node> q;
	q.push({ s, 0 });
	inq[s] = true;

	while (!q.empty())
	{
		Node topNode = q.front();
		q.pop();
		if (topNode.layer < L)
		{
			for (int i = 1; i <= N; ++i)
			{
				if (Graph[topNode.v][i] > 0 && !inq[i])
				{
					++cnt;
					q.push({ i, topNode.layer + 1 });
					inq[i] = true;
				}
			}
		}
	}
}

int main()
{
	scanf("%d %d", &N, &L);

	for (int i = 1; i <= N; ++i)
	{
		int m;
		scanf("%d", &m);
		while (m--)
		{
			int adj;
			scanf("%d", &adj);
			Graph[adj][i] = 1;
		}
	}

	int k;
	scanf("%d", &k);
	while (k--)
	{
		int cnt = 0;
		int id;
		scanf("%d", &id);
		BFS(id, cnt);
		printf("%d\n", cnt);
	}
	
	return 0;
}

最短路径

Dijkstra

const int MAXV = 1000; 		// 最大顶点数
const int INF = 3fffffff;	// 这里为了防止溢出, 不使用 0x7fffffff

邻接矩阵版

T ( n ) = O ( n 2 ) T(n)=O(n^2) T(n)=O(n2)

int n, G[MAXV][MAXV]; 		// n 为顶点数
int d[MAXV]; 				// 起点到达各点的最短路径长度
int pre[MAXV]; 				// pre[v] 表示从起点到顶点 v 的最短路径上 v 的前一个顶点
bool vis[MAXV] = {false}; 	// 标记数组, vis[i]==true 表示已访问

void Dijkstra(int s) { // s 为起点
	fill(d, d + MAXV, INF); 	// fill 函数将整个 d 数组赋为 INF
	for(int i = 0; i < n; i++)
		pre[i] = i; 			// 初始状态设每个点的前驱为自身
	d[s] = 0; 					// 起点 s 到达自身的距离为 0
	
	for(int i = 0; i < n; i++) {
		int u = -1, MIN = INF; 	// u 使 d[u] 最小, MIN 存放该最小的 d[u]
		for(int j = 0; j < n; j++){ // 找到未访问的顶点中 d[j] 最小的
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		// 找不到小于 INF 的 d[u], 说明剩下的顶点和起点 s 不连通
		if (u == -1) 
			return;
		vis[u] = true; // 标记 u 为己访问
		for(int v = 0; v < n; v++) {
			if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
				d[v] = d[u] + G[u][v];
				pre[v] = u; 		// 记录 v 的前驱顶点是 u
			}
		}
	}
}

邻接表版

T ( n ) = O ( n 2 + e ) T(n)=O(n^2+e) T(n)=O(n2+e)

如果寻找最短路径改用最小堆,则 T ( n ) = O ( n log ⁡ n + e ) T(n)=O(n\log n+e) T(n)=O(nlogn+e)

struct Node {
	int v, dis; // v 为边的目标顶点, dis 为边权
};
vector<Node> Adj[MAXV];
int n; 			// n 为顶点数
int d[MAXV];
int pre[MAXV];
bool vis[MAXV] = {false};

void Dijkstra(int s) {
	fill(d, d + MAXV, INF);
	for(int i = 0; i < n; i++)
		pre[i] = i;
	d[s] = 0;
	for (int i = 0; i < n; i++) {
		int u = -1, MIN = INF;
		for(int j = 0; j < n; j++) {
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		if(u == -1) 
			return;
		vis[u] = true;
		// 只有下面这个 for 与邻接矩阵的写法不同
		for (int j = 0; j < Adj[u].size(); j++) {
			int v = Adj[u][j].v;
			if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
				d[v] = d[u] + Adj[u][j].dis;
				pre[v] = u;
			}
		}
	}
}

输出路径

void DFS(int s, int v) { // s 为起点编号, v 为当前访问的顶点编号(从终点开始递归)
	if (v == s) { // 如果当前已经到达起点 s, 则输出起点并返回
		printf("%d\n", s);
		return;
	}
	DFS(s, pre[v]); 	// 递归访问 v 的前驱顶点 pre[v]
	printf("%d\n", v); // 从最深处 return 回来之后,输出每一层的顶点号
}

第二标尺 (边/点权最大和、最短距离条数)

  • 题目更多时候会出现从起点到终点的最短路径不止一条。于是,题目就会给出一个第二标尺(第一标尺是距离),要求在所有最短路径中选择第二标尺最优的一条路径。而第二标尺常见的是以下三种出题方法或其组合:
    • (1) 给每条边再增加一个边权(比如说花费),然后要求在最短路径有多条时要求路径上的花费之和最小(如果边权是其他含义,也可以是最大)
    • (2) 给每个点增加一个点权(例如每个城市能收集到的物资),然后在最短路径有多条时要求路径上的点权之和最大(如果点权是其他含义的话也可以是最小)
    • (3) 直接问有多少条最短路径
  • 对这三种出题方法, 都只需要增加一个数组来存放新增的边权或点权或最短路径条数,然后在 Dijkstra 算法中修改优化 d[v] 的那个步骤即可, 其他部分不需要改动
  • (1) 新增边权。以新增的边权代表花费为例,用 cost[u][v] 表示 u u u- v v v 的花费,并增加一个数组 c[], 令从起点 s s s 到达顶点 u u u 的最少花费为 c[u], 初始化时只有 c[s] 为0、其余 c[u] 均为 INF
for(int v = 0; v < n; v++) {
	if (vis[v] == false && G[u][v] != INF) {
		if (d[u] + G[u][v] < d[v]) {
			d[v] = d[u] + G[u][v];
			c[v] = c[u] + cost[u][v];
		} else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) {
			c[v] = c[u] + cost[u][v]; 
		}
	}
}
  • (2) 新增点权。以新增的点权代表城市中能收集到的物资为例, 用 weight[u] 表示城市 u u u 中的物资数目, 并增加一个数组 w[], 令从起点 s s s 到达顶点 u u u 可以收集到的最大物资为 w[u], 初始化时只有 w[s]weight[s]、其余 w[u] 均为 0
for(int v = 0; v < n; v++) {
	if (vis[v] == false && G[u][v] != INF) {
		if (d[u] + G[u][v] < d[v]) {
			d[v] = d[u] + G[u][v];
			w[v] = w[u] + weight[v];
		} else if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]) {
			w[v] = w[u] + weight[v];
		}
	}
}
  • (3) 求最短路径条数。只需要增加一个数组 num[], 令从起点 s s s 到达顶点 u u u 的最短路径条数为 num[u], 初始化时只有 num[s] 为 1、其余 num[u] 均为 0
for(int v = 0; v < n; v++) {
	if (vis[v] == false && G[u][v] != INF) {
		if (d[u] + G[u][v] < d[v]) {
			d[v] = d[u] + G[u][v];
			num[v] = num[u];
		} else if(d[u] + G[u][v] == d[v]) {
			num[v] += num[u];
		}
	}
}

Dijkstra + DFS (通用模板)

  • 上面给出的 3 种情况都是以路径上边权或点权之 “和” 为第二标尺的,例如路径上的花费之和最小、路径上的点权之和最小、最短路径的条数都体现了"和"的要求。事实上也可能出现一些逻辑更为复杂的计算边权或点权的方式, 此时按上面的方式只使用 Dijkstra 算法就不一定能算出正确的结果(原因是不一定满足最优子结构),或者即便能算出, 其逻辑也极其复杂, 很容易写错
  • 显然更简单明了的方法 (Dijkstra + DFS) 是: 先在 Dijkstra 算法中记录下所有最短路径(只考虑距离),然后从这些最短路径中选出一条第二标尺最优的路径 (DFS)
    • 当然,逻辑简单的时候还是用之前介绍的方法在代码上更简单一些

  • (1) 使用 Dijkstra 算法记录所有最短路径: 由于此时要记录所有最短路径,为了适应多个前驱的情况,不妨把 pre 数组定义为 vector<int> pre[MAXV] 来存放所有能产生最短路径的前驱结点 (也可以设为 set<int> pre[MAXV])
int n, G[MAXV][MAXV]; 		// n 为顶点数
int d[MAXV]; 				// 起点到达各点的最短路径长度
vector<int> pre[MAXV]; 		// pre[v] 表示从起点到顶点 v 的最短路径上 v 的前一个顶点
bool vis[MAXV] = {false}; 	// 标记数组, vis[i]==true 表示已访问

void Dijkstra(int s) { // s 为起点
	fill(d, d + MAXV, INF); 	// fill 函数将整个 d 数组赋为 INF
	d[s] = 0; 					// 起点 s 到达自身的距离为 0
	
	for(int i = 0; i < n; i++) {
		int u = -1, MIN = INF; 	// u 使 d[u] 最小, MIN 存放该最小的 d[u]
		for(int j = 0; j < n; j++){ // 找到未访问的顶点中 d[j] 最小的
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		// 找不到小于 INF 的 d[u], 说明剩下的顶点和起点 s 不连通
		if (u == -1) 
			return;
		vis[u] = true; // 标记 u 为己访问
		for(int v = 0; v < n; v++) {
			if(vis[v] == false && G[u][v] != INF) {
				if(d[u] + G[u][v] < d[v]){
					d[v] = d[u] + G[u][v];
					pre[v] = vector<int>{u}; 		// 记录 v 的前驱顶点是 u
				} else if(d[u] + G[u][v] == d[v]) {
					pre[v].push_back(u);
				}
			}
		}
	}
}
  • (2) DFS 遍历所有最短路径, 找出一条使第二标尺最优的路径: 每次 DFS 搜索到叶结点,就可以得到一条完整路径,并对这条路径计算其第二标尺的值(例如把路径上的边权或是点权累加出来), 令其与当前第二标尺的最优值进行比较。如果比当前最优值更优,则更新最优值, 并用这条路径覆盖当前的最优路径。这样, 当所有最短路径都遍历完毕后, 就可以得到最优第二标尺与最优路径
    在这里插入图片描述
int optvalue; 				// 第二标尺最优值
vector<int> pre[MAXV];		// 存放结点的前驱结点
vector<int> path, tempPath; // 最优路径、临时路径
int num;					// 最短路径的条数

void DFS(int v) { // v 为当前访问结点
	if (v == st) { // 如果到达了叶子结点 st (即路径的起点)
		tempPath.push_back(v);
		int value; // 存放临时路径 tempPath 的第二标尺的值
		计算路径 tempPath 上的 value 值;
		if(value 优于 optvalue) {
			optvalue = value;
			path = tempPath;
		}
		ternpPath.pop_back();
		++num;
		return;
	}
	tempPath.push_back(v);
	for(int i = 0; i < pre[v].size(); i++) {
		DFS(pre[v][i]);
	}
	tempPath.pop_back();
}
  • 需要注意的是, 由于递归的原因, 存放在 tempPath 中的路径结点是逆序的, 因此访问结点需要倒着进行

: 计算路径 tempPath 上的 value 值

// 边权之和
int value = 0;
for(int i = tempPath.size() - 1; i > 0; i--) { // 倒着访问结点
	int id = tempPath[i], idNext = tempPath[i - 1];
	value += V[id][idNext];
}

int value = 0;
for(int i = tempPath.size() - 1; i > 0; i--) { // 倒着访问结点
	int id = tempPath[i];
	value += W [id];
}

Bellman-Ford (BF)

  • 至于最短路径的求解方法、有多重标尺时的做法均与 Dijkstra 算法中介绍的相同。唯一要注意的是统计最短路径条数的做法: 由于 Bellman-Ford 算法期间会多次访问曾经访问过的顶点, 如果单纯按照 Dijkstra 算法中介绍的 num 数组的写法, 将会反复累计已经计算过的顶点。为了解决这个问题, 需要设置记录前驱的数组 set<int> pre[MAXV], 当遇到一条和已有最短路径长度相同的路径时, 必须重新计算最短路径条数
struct Node {
	int v, dis; 			// v 为邻接边的目标顶点,dis 为邻接边的边权
};
vector<Node> Adj[MAXV]; 	// 邻接表
int n, weight[MAXV]; 		// n 为顶点数, weight[] 记录点权
int d[MAXV]; 				// 起点到达各点的最短路径长度
int w[MAXV];				// 记录最大点权值和
int num[MAXV];				// 记录最短路径条数
set<int> pre[MAXV];			// 前驱

bool Bellman(int s) { 		// s 为源点
	fill(d, d + MAXV, INF); // fill 函数将整个 d 数组赋为 INF
	memset(num, 0, sizeof(num));
	memset(w, 0, sizeof(w));
	d[s] = 0; 				// 起点 s 到达自身的距离为 0
	w[s] = weight[s];
	num[s] = 1;
	
	// 以下为求解数组 d 的部分
	for (int i = 0; i < n - 1; i++) { 	// 执行 n - 1 轮操作,n 为顶点数
		for(int u = 0; u < n; u++) { 	// 每轮操作都遍历所有边
			for (int j = 0; j < Adj[u].size(); j++) {
				int v = Adj[u][j].v; 		// 邻接边的顶点
				int dis = Adj[u][j].dis; 	// 邻接边的边权
				if (d[u] + dis < d[v]} { 	// 以 u 为中介点可以使 d[v] 更小
					d[v] = d[u] + dis; 		// 松弛操作
					w[v] = w[u] + weight[v];
					num[v] = num[u];
					pre[v] = set<int> {u};
				} else if(d[u] + dis == d[v])
					if(w[u] + weight[v] > w[v]) {
						w[v] = w[u] + weight[v];
					}
					pre[v].insert(u);
					num[v] = 0; 		// 重新统计 num[v]
					for(auto it = pre[v].begin(); it != pre[v].end(); it++) {
						num[v] += num[*it];
					}
				}
			}
		}
	}
	// 以下为判断负环的代码
	for(int u = 0; u < n; u++) { // 对每条边进行判断
		for(int j = 0; j < Adj[u].size(); j++) {
			int v = Adj[u][j].v; 		// 邻接边的顶点
			int dis = Adj[u][j].dis; 	// 邻接边的边权
			if(d[u] + dis < d[v]) { 	// 如果仍可以被松弛
				return false; 			// 说明图中有从源点可达的负环
			}
		}
	}
	return true; 	// 数组 d 的所有值都已经达到最优
}

SPFA

如果事先知道图中不会有环, 那么 num 数组的部分可以去掉

vector<Node> Adj[MAXV] ; 		// 邻接表
int n, d[MAXV], num[MAXV]; 		// num 数组记录顶点的入队次数
bool inq[MAXV]; 				// 顶点是否在队列中

bool SPFA(int s) {
	// 初始化部分
	memset(inq, false, sizeof(inq));
	memset(num, 0, sizeof(num));
	fill(d, d + MAXV, INF);
	
	// 源点入队部分
	queue<int> Q;
	Q.push(s); 			// 源点入队
	inq[s] = true; 
	num[s]++;
	d[s] = 0; 			// 源点的 d 值为 0
	
	// 主体部分
	while (!Q.empty()) {
		int u = Q.front();
		Q.pop();
		inq[u] = false; 	// 设置 u 为不在队列中
		// 遍历 u 的所有邻接边 v
		for (int j = 0; j < Adj[u].size(); j++) {
			int v = Adj[u][j].v;
			int dis = Adj[u][j].dis;
			// 松弛操作
			if (d[u] + dis < d[v]) {
				d[v] = d[u] + dis;
				if(!inq[v]) { // 如果 v 不在队列中
					Q.push(v);
					inq[v] = true;
					num[v]++;
					if (num[v] >= n) 
						return false; // 有可达负环
				}
			}
		}
	}
	return true; //无可达负环
}

Floyd

  • 由于时间复杂度是 O ( n 3 ) O(n^3) O(n3),因此顶点数 n n n 的限制约在 200 以内
const int INF = 0x3fffffff;
const int MAXV = 200; 		// MAXV 为最大顶点数
int n; 						// n 为顶点数
int dis[MAXV][MAXV]; 		// dis[i][j] 表示顶点 i 和顶点 j 的最短距离
int path[MAXV][MAXV];		// path[i][j] 表示顶点 i 到顶点 j 的最短路径上,顶点 j 的前一个顶点编号

// dis 需初始化为邻接矩阵 (对角线元素为 0)
void Floyd() {
	// 加入 k 作为中间顶点进行试探;路径: i->j
	for (int k = 0; k < n; ++k)
	{
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				if (dis[i][k] + dis[k][j] < dis[i][j])
				{
					dis[i][j] = dis[i][k] + dis[k][j];
					if (i == j && dis[i][j] < 0)
					{
						throw;	// 负值圈
					}
					path[i][j] = path[k][j]; // 注意这里更新 path 的方式
				}
			}
		}
	}

PAT (Advanced Level) 1018 Public Bike Management

题目在此

  • 本题的解不满足最优子结构性质,因此不能只用 Dijkastra 解决,必须用 Dijkastra + DFS
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int MAXV = 501;
const int INFTY = 0x3fffffff;

int Cmax, N, Sp, M, Perfect;
int C[MAXV];
int Graph[MAXV][MAXV];
bool vis[MAXV];
int dis[MAXV];
vector<int> path[MAXV];
vector<int> tmp_path;
vector<int> opt_path;
int min_need = INFTY;
int n_take_away;

void DFS(int v)
{
    if (v == 0)
    {
        tmp_path.push_back(v);
        int need = 0;       // 需要的自行车数
        int remain = 0;     // 当前多出的自行车数
        for (auto it = tmp_path.rbegin(); it != tmp_path.rend(); ++it)
        {
            int v = *it;
            if (C[v] < 0)   // 当前站点需要自行车
            {
                if (remain > abs(C[v]))
                {
                    remain -= abs(C[v]);    // 用当前多出的自行车补足
                }
                else {
                    need += abs(C[v]) - remain; // 多出的自行车不够,还需要额外增加自行车
                    remain = 0;     
                }
            }
            else if (C[v] > 0)  // 当前站点需要带走自行车
            {
                remain += C[v];
            }
        }
        if (need < min_need)
        {
            min_need = need;
            opt_path = tmp_path;
            n_take_away = remain;
        }
        else if (need == min_need
            && remain < n_take_away)
        {
            opt_path = tmp_path;
            n_take_away = remain;
        }
        tmp_path.pop_back();
        return;
    }
    tmp_path.push_back(v);
    for (size_t i = 0; i != path[v].size(); ++i)
    {
        DFS(path[v][i]);
    }
    tmp_path.pop_back();
}

int main()
{
    scanf("%d %d %d %d", &Cmax, &N, &Sp, &M);
    ++N;	// PBMS 为站点 0
    Perfect = Cmax / 2;
    for (int i = 1; i != N; ++i)
    {
        scanf("%d", &C[i]);
        C[i] -= Perfect;
        dis[i] = INFTY;
    }
    for (int i = 0; i != N; ++i)
    {
        for (int j = 0; j != N; ++j)
        {
            Graph[i][j] = INFTY;
        }
    }
    for (int i = 0; i != M; ++i)
    {
        int si, sj, tij;
        scanf("%d %d %d", &si, &sj, &tij);
        Graph[si][sj] = tij;
        Graph[sj][si] = tij;
    }

    // Dijkastra
    dis[0] = 0;
    path[0].push_back(0);
    for (int i = 0; i != N; ++i)
    {
        int min = -1, min_dis = INFTY;
        for (int j = 0; j != N; ++j)
        {
            if (!vis[j] && dis[j] < min_dis)
            {
                min_dis = dis[j];
                min = j;
            }
        }
        if (min == -1)
        {
            break;
        }
        vis[min] = true;
        for (int j = 0; j != N; ++j)
        {
            if (Graph[min][j] != INFTY && !vis[j])
            {
                if (dis[j] > dis[min] + Graph[min][j])
                {
                    dis[j] = dis[min] + Graph[min][j];
                    path[j] = vector<int>{ min };
                }
                else if (dis[j] == dis[min] + Graph[min][j])
                {
                    path[j].push_back(min);
                }
            }
        }
    }
    DFS(Sp);
    printf("%d 0", min_need);
    for (int i = opt_path.size() - 2; i >= 0; --i)
    {
        printf("->%d", opt_path[i]);
    }
    printf(" %d", n_take_away);

    return 0;
}

PAT (Advanced Level) 1030 Travel Plan

题目在此

  • 添加边的时候是按照有向图添的,但实际是无向图… 耗费 15min 才想到. orz
#include <cstdio>

using namespace std;

const int MAXV = 500;
const int INFTY = 0x3fffffff;

int Graph[MAXV][MAXV];
int Cost[MAXV][MAXV];
int vis[MAXV];
int N, M, S, D;

int minCost[MAXV];
int dis[MAXV];
int path[MAXV];

void print_path(int v)
{
	if (v == path[v])
	{
		printf("%d ", v);
		return;
	}
	print_path(path[v]);
	printf("%d ", v);
}

int main()
{
	scanf("%d %d %d %d", &N, &M, &S, &D);
	for (int i = 0; i != N; ++i)
	{
		for (int j = 0; j != N; ++j)
		{
			Graph[i][j] = INFTY;
		}
		dis[i] = INFTY;         // 这边一定要记得初始化 dis 和 minCost 数组
        minCost[i] = INFTY;
	}
	for (int i = 0; i != M; ++i)
	{
		int c1, c2, d, c;
		scanf("%d %d %d %d", &c1, &c2, &d, &c);
		Graph[c1][c2] = d;      // 无向图
		Cost[c1][c2] = c;
		Graph[c2][c1] = d;
		Cost[c2][c1] = c;
	}

	minCost[S] = 0;
	dis[S] = 0;
	path[S] = S;

	for (int i = 0; i != N; ++i)
	{
		int min = -1, min_dis = INFTY;
		for (int j = 0; j != N; ++j)
		{
			if (dis[j] < min_dis && vis[j] == false)
			{
				min_dis = dis[j];
				min = j;
			}
		}
		if (min == -1)
		{
			break;
		}
		vis[min] = true;
		for (int j = 0; j != N; ++j)
		{
			if (vis[j] == false && Graph[min][j] != INFTY)
			{
				if (Graph[min][j] + dis[min] < dis[j])
				{
					dis[j] = Graph[min][j] + dis[min];
					minCost[j] = Cost[min][j] + minCost[min];
					path[j] = min;
				}
				else if (Graph[min][j] + dis[min] == dis[j]
					&& minCost[j] > Cost[min][j] + minCost[min])
				{
					minCost[j] = Cost[min][j] + minCost[min];
					path[j] = min;
				}
			}
		}
	}
	print_path(D);
	printf("%d %d", dis[D], minCost[D]);

	return 0;
}
  • Dijkastra + DFS
#include <cstdio>
#include <vector>

using namespace std;

const int MAXV = 500;
const int INFTY = 0x3fffffff;

int Graph[MAXV][MAXV];
int Cost[MAXV][MAXV];
int vis[MAXV];
int N, M, S, D;

int dis[MAXV];
vector<int> Pre[MAXV]; // 前驱
vector<int> tempPath, optpath; // 临时路径、最优路径
int minCost = INFTY;

void print_path(int v)
{
	for(auto it = optpath.rbegin(); it != optpath.rend(); ++it)
	{
		printf("%d ", *it);
	}
}

void DFS(int v)
{
	if (v == S)		// 到达路径起点
	{
		tempPath.push_back(v);
		int cost = 0;
		for (size_t i = 0; i + 1 < tempPath.size(); ++i)
		{
			cost += Cost[tempPath[i]][tempPath[i + 1]];
		}
		if (cost < minCost)
		{
			minCost = cost;
			optpath = tempPath;
		}
		tempPath.pop_back();
		return;
	}
	tempPath.push_back(v);
	for (size_t i = 0; i != Pre[v].size(); ++i)
	{
		DFS(Pre[v][i]);
	}
	tempPath.pop_back();
}

int main()
{
	scanf("%d %d %d %d", &N, &M, &S, &D);
	for (int i = 0; i != N; ++i)
	{
		for (int j = 0; j != N; ++j)
		{
			Graph[i][j] = INFTY;
		}
		dis[i] = INFTY;
	}
	for (int i = 0; i != M; ++i)
	{
		int c1, c2, d, c;
		scanf("%d %d %d %d", &c1, &c2, &d, &c);
		Graph[c1][c2] = d;
		Cost[c1][c2] = c;
		Graph[c2][c1] = d;
		Cost[c2][c1] = c;
	}

	// Dijkastra
	dis[S] = 0;
	for (int i = 0; i != N; ++i)
	{
		int min = -1, min_dis = INFTY;
		for (int j = 0; j != N; ++j)
		{
			if (dis[j] < min_dis && vis[j] == false)
			{
				min_dis = dis[j];
				min = j;
			}
		}
		if (min == -1)
		{
			break;
		}
		vis[min] = true;
		for (int j = 0; j != N; ++j)
		{
			if (vis[j] == false && Graph[min][j] != INFTY)
			{
				if (Graph[min][j] + dis[min] < dis[j])
				{
					dis[j] = Graph[min][j] + dis[min];
					Pre[j] = vector<int>{ min };
				}
				else if (Graph[min][j] + dis[min] == dis[j])
				{
					Pre[j].push_back(min);
				}
			}
		}
	}
	
	// DFS
	DFS(D);
	print_path(D);
	printf("%d %d", dis[D], minCost);

	return 0;
}

PAT (Advanced Level) 1072 Gas Station

题目在此

  • 注意题意:加油站之间也是彼此有路连接的,所以最短路径计算的时候也要把加油站算上
  • 另外,结果输出是直接用 %.1f 输出即可,不必特意去四舍五入
#include <cstdio>
#include <cstring>

using namespace std;

const int MAXV = 1011;
const int INFTY = 0x3fffffff;

int N, M, K, Ds;
int Graph[MAXV][MAXV];
bool vis[MAXV];
int dis[MAXV];
// solution
int opt_min_dis = -1;
int opt_sum_dis;
int opt_idx = -1;

int get_vertex_idx(char name[])
{
    int i = 0, idx = 0, len = strlen(name);
    while (i < len)
    {
        if (name[i] != 'G')
        {
            idx = idx * 10 + (name[i] - '0');
        }
        ++i;
    }
    if (name[0] == 'G')
    {
        return idx + N;    // Gas station 的编号排在城市之后
    }
    else {
        return idx;
    }
}

int main()
{
    scanf("%d %d %d %d", &N, &M, &K, &Ds);
    for (int i = 1; i <= N + M; ++i)
    {
        for (int j = 1; j <= N + M; ++j)
        {
            Graph[i][j] = INFTY;
        }
    }
    for (int i = 0; i != K; ++i)
    {
        char v1[4], v2[4];
        int dis;
        scanf("%s %s %d", v1, v2, &dis);
        int v1_idx = get_vertex_idx(v1);
        int v2_idx = get_vertex_idx(v2);
        Graph[v1_idx][v2_idx] = dis;
        Graph[v2_idx][v1_idx] = dis;
    }
    int vertex_num = N + M;
    
    for (int i = 1; i <= M; ++i)
    {
        int gas_station = N + i;    // 源点
        for (int j = 1; j <= vertex_num; ++j)
        {
            vis[j] = false;
            dis[j] = INFTY;
        }
        dis[gas_station] = 0;
        bool fail_flag = false;
        // Dijkastra
        for (int j = 0; j != vertex_num; ++j)
        {
            int min = -1, min_dis = INFTY;
            for (int k = 1; k <= vertex_num; ++k)
            {
                if (dis[k] < min_dis && !vis[k])
                {
                    min_dis = dis[k];
                    min = k;
                }
            }
            if (min == -1)
            {
                break;
            }
            vis[min] = true;
            for (int k = 1; k <= vertex_num; ++k)
            {
                if (Graph[min][k] != INFTY && !vis[k])
                {
                    if (dis[k] > Graph[min][k] + dis[min])
                    {
                        dis[k] = Graph[min][k] + dis[min];
                    }
                }
            }
        }
        int min_dis = INFTY;
        int sum_dis = 0;
        for (int j = 1; j <= N; ++j)
        {
            if (dis[j] > Ds)
            {
                fail_flag = true;
                break;
            }
            if (dis[j] < min_dis)
            {
                min_dis = dis[j];
            }
            sum_dis += dis[j];
        }
        if (!fail_flag)
        {
            if (min_dis > opt_min_dis)
            {
                opt_min_dis = min_dis;
                opt_sum_dis = sum_dis;
                opt_idx = i;
            }
            else if (min_dis == opt_min_dis
                && sum_dis < opt_sum_dis)
            {
                opt_sum_dis = sum_dis;
                opt_idx = i;
            }
        }
    }

    if (opt_idx == -1)
    {
        printf("No Solution");
    }else{
        printf("G%d\n%.1f %.1f", opt_idx, (double)opt_min_dis, opt_sum_dis / (double)N);
    }

    return 0;
}

最小生成树

Prim

const int MAXV = 1000; 		// 最大顶点数
const int INF = 0x3fffffff; // 设 INF 为一个很大的数

邻接矩阵

  • O ( n 2 ) O(n^2) O(n2)
int n, G[MAXV][MAXV]; 		// n 为顶点数, MAXV 为最大顶点数
int d[MAXV]; 				// 顶点与集合 S 的最短距离
bool vis[MAXV] = {false}; 	// 标记数组,vis[i] == true 表示已访问。初值均为 false
int prim() { // 函数返回最小生成树的边权之和
	fill(d, d + MAXV, INF); 	// fill 函数将整个 d 数组赋为 INF
	d[0] = 0; 					// 默认 0 号为初始点
	int ans = 0; 				// 存放最小生成树的边权之和
	
	for(int i = 0; i < n; i++) {
		int u = -1, MIN = INF; // u 使 d[u] 最小, MIN 存放该最小的 d[u]
		for(int j = 0; j < n; j++) { // 找到未访问的顶点中 d[] 最小的
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		// 找不到小于 INF 的 d[u], 则剩下的顶点和集合 s 不连通
		if(u == -1) 
			return -1;
		vis[u] = true;
		ans += d[u];	// 将与集合 s 距离最小的边加入最小生成树
		for(int v = 0; v < n; v++) {
			if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]) {
				d[v] = G[u][v];
			}
		}
	}
	return ans;
}

邻接表

struct Node{
	int v, dis; // v 为边的目标顶点, dis 为边权
};
vector<Node> Adj[MAXV]; // 邻接表;Adj[u] 存放从顶点 u 出发可以到达的所有顶点
int n; 					// n 为顶点数
int d[MAXV]; 			// 顶点与集合 s 的最短距离
bool vis[MAXV] = {false}; // 标记数组,vis[i] == true 表示已访问。初值均为 false
// O(n^2)
int prim() { 	// 函数返回最小生成树的边权之和
	fill(d, d + MAXV, INF);
	d[0] = 0; 		// 默认0 号为初始点
	int ans = 0; 	// 存放最小生成树的边权之和
	
	for(int i = 0; i < n; i++) {
		int u = -1, MIN = INF;
		for(int j = 0; j < n; j++) {
			if(vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		if(u == -1) 
			return -1;
		vis[u] = true;
		ans += d[u];		// 将与集合 s 距离最小的边加入最小生成树
		// 只有下面这个 for 与邻接矩阵的写法不同
		for(int j = 0; j < Adj[u].size(); j++) {
			int v = Adj[u][j].v; // 通过邻接表直接获得 u 能到达的顶点 v
			if(vis[v] == false && Adj[u][j].dis < d[v]) {
				d[v] = Adj[u][j].dis;
			}
		}
	}
	return ans;
}

Kruskal

struct edge {
	int u, v; 	// 边的两个端点编号, 用于判定它们是否属于同一个连通块
	int cost;	// 边权
}E[MAXE]; 		// 最多有 MAXE 条边
// 并查集
int father[N];
int findFather(int v) {
	if (v  == father[v]) 
		return v; // 找到根结点
	else {
		int F = findFather(father[v]);
		father[v] = F;
		return F;
	}
}
// kruskal 函数返回最小生成树的边权之和
// 参数 n 为顶点个数, m 为图的边数
int kruskal(int n, int m) {
	// ans 为所求边权之和, Num_Edge 为当前生成树的边数
	int ans = 0, Num_Edge = 0;
	for(int i = 1; 1 <= n; i++) ( // 假设顶点范围是 [1,n]
		father[i] = i;	 // 并查集初始化 -> 初始时所有端点都属于不同的连通块
	}
	// 所有边按边权从小到大排序
	sort(E, E + m, [](const &edge lhs, const &edge rhs) -> bool {
		return lhs.cost < rhs.cost;
	}); 
	
	for(int i = 0; i < m; i++) { // 枚举所有边
		int faU = findFather(E[i].u); // 查询测试边两个端点所在集合的根结点
		int faV = findFather(E[i].v);
		if (faU != faV) { 	// 如果不在一个集合中
			father[faU] = faV; 	// 合并集合(即把测试边加入最小生成树中)
			ans += E[i].cost; 	// 边权之和增加测试边的边权
			Num_Edge++; 		// 当前生成树的边数加1
			if(Num_Edge == n - 1) 
				break; 			// 边数等于顶点数减 1 时结束算法
		}
	}		
	if(Num_Edge != n - 1) 
		return -1; 	// 无法连通时返回-1
	else 
		return ans; // 返回最小生成树的边权之和
}

拓扑排序

vector<int> G[MAXV]; 		// 邻接表
int n, inDegree[MAXV]; 		// 顶点数, 入度

bool topologicalSort() {
	int num = 0;		// 记录加入拓扑序列的顶点数
	queue<int> q;		// 如果要求有多个入度为 0 的顶点,选择编号最小的顶点,那么把 queue 改成 priority_queue
	for(int i = 0; i < n; i++) {	
		if(inDegree[i] == 0) {
			q.push(i); // 将所有入度为 0 的顶点入队
		}
	}
	while (!q.empty()) {
		int u = q.front();
		// printf("%d", u);  // 此处可输出顶点 u, 作为拓扑序列中的顶点
		q.pop();
		for(int i = 0; i < G[u].size(); i++) {
			int v = G[u][i];	// u 的后继结点 v
			inDegree[v]--;		// 顶点 v 的入度减 1
			if (inDegree[v] == 0) {	// 顶点 v 的入度减为 0 则入队
				q.push(v);
			}
		}
		G[u].clear() ; 		// 清空顶点 u 的所有出边(如无必要可不写)
		num++; 				// 加入拓扑序列的顶点数加 1
	}
	return num == n;		// 有环则返回 false
}	

关键路径

struct Node{
	int v;	// 邻接点
	int w;	// 边权
};
vector<int> G[MAXV];	// 邻接表
int ve[MAXV];
int vl[MAXV];
stack<int> topOrder;		// 拓扑序列
// 拓扑排序,顺便求 ve 数组
bool topologicalSort() {
	queue<int> q;
	for(int i = 0; i < n; i++) {
		if(inDegree[i) = 0) {
			q.push(i);
		}
	}
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		topOrder.push(u); 	// 将 u 加入拓扑序列
		for(int i = 0; i < G[u].size(); i++) {
			int v = G[u][i].v;  // 后继结点编号为 v
			inDegree[v]--;
			if(inDegree[v] == 0) {
				q.push(v);
			}
			// 用 ve[u] 来更新 u 的所有后继结点 v
			if(ve[u] + G[u][i].w > ve[v]) {
				ve[v] = ve[u] + G[u][i].w;
			}
		}
	}
	return topOrder.size() == n;
}
// 适用汇点确定且唯一的情况
// 关键路径,不是有向无环图返回 -1,否则返回关键路径长度
int CriticalPath() {
	memset(ve, 0, sizeof(ve)); 		// ve 数组初始化为 0 (因为之后要求最大值)
	if(topologicalSort() == false) {
		return -1; // 不是有向无环图,返回 -1
	}
	// 求出汇点对应的 ve 值 (最大的 ve 值)
	int maxLength = 0;
	for(int i = 0; i < n; i++) {
		if(ve[i] > maxLength) {
			maxLength = ve[i];
		}
	}
	fill(vl, vl + n, maxLength); // vl 数组初始化,初始值为汇点的 ve 值
	// 使用逆拓扑序列求解 vl 数组
	while (!topOrder.empty()) {
		int u = topOrder.top();
		topOrder.pop();
		for(int i = 0; i < G[u].size(); i++) {
			int v = G[u][i].v; 	// u 的后继结点 v
			// 用 u 的所有后继结点 v 的 vl 值来更新 vl[u]
			if(vl[v] - G[u][i].w < vl[u]) {
				vl[u] = vl[v] - G[u][i].w;
			}
		}
	}
	
	// 遍历邻接表的所有边,计算活动的最早开始时间 e 和最迟开始时间 l
	for(int u = 0; u < n; u++) {
		for(int i = 0; i < G[u].size(); i++)
			int v = G[u][i].v, w = G[u][i].w;
			int e = ve[u], l = vl[v] - w;
			if (e == l) {	// 如果 e == l, 说明活动 u->v 是关键活动
				printf("%d->%d\n", u, v); // 输出关键活动
			}
		}
	}
	return ve[n - 1]; 	// 返回关键路径长度
}
  • 如果要完整输出所有关键路径, 就需要把关键活动存下来, 方法就是新建一个邻接表, 当确定边 u → v u\rightarrow v uv 是关键活动时, 将边 u → v u\rightarrow v uv 加入邻接表。这样最后生成的邻接表就是所有关键路径合成的图了, 可以用 DFS 遍历来获取所有关键路径
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值