图专题

在这里插入图片描述

1.图的相关定义

注意,图中重要的两个权值!做题时搞清楚,是对 “点权” 的处理还是对 “边权” 的处理。

1.1 点权

需要单独开一个数组来记录点权,weight[] 。

1.2 边权

邻接矩阵、邻接表存的就是边权。

2.图的存储

2.1邻接矩阵

节点数量在 1000 1000 1000 以下
存储边权

int G[MAXN][MAXN]

存储点权

int weight[MAXN]

2.2邻接表

存储边权:

struct node{
	int v;		// 终点
	int w;		// 边权
}

vector<node> Adj[MAXN];		// 邻接表

存储点权:

int weight[MAXN]

3.图的遍历

3.0 图的模拟

3.0.1 图上路径的模拟

核心:按照给定路径(或要求),在图上 模拟走出路径,但一般限制条件较多,理清楚要求即可。并不涉及到 DFS/BFS。

典例 A1150 Travelling Salesman Problem- 图的模拟

题目大意:给出 m 对边信息(建图),给出 k 条路径判断是 TS、TS simple 路径。
TS simple 路径:从一个点出发,遍历到图上所有节点回到初始点,的简单 路径。则有判断条件如下:

  1. 途中有节点不可达 Not a TS cycle.
  2. 没有遍历到所有的点(用 set 统计) Not a TS cycle.
  3. 起点终点不一 Not a TS cycle.
  4. 简单路径:在满足上述条件下, 路径的长度 > k+1(说明必有点重复访问)

代码:

#include<cstdio>
#include<algorithm>
#include<vector>
#include<set>
#define MAXN 10010
using namespace std;

int n,m;
int G[MAXN][MAXN];
int ans = 0x3f3f3f3f,ansi;
int a,b,c;
int main(){
	scanf("%d%d",&n,&m)	;
	for(int i = 1;i <= m;++i){
		scanf("%d%d%d",&a,&b,&c);
		G[a][b] = G[b][a] = c;
	}
	int k,s;
	scanf("%d",&s)	;
	
	for(int i = 1;i <= s;++i){
		int k;
		scanf("%d",&k);
		vector<int> v(k);
		set<int> st;
		
		scanf("%d",&v[0]);
		int dist = 0;
		int flag = 1;			// 是否可达?  
		for(int j = 1;j < k;++j){
			scanf("%d",&v[j]);
			st.insert(v[j]);
			if(G[v[j-1]][v[j]] == 0)	flag = 0;
			dist += G[v[j-1]][v[j]];
		}
		// 是否可达 
		 if(flag == 0){
		 	printf("Path %d: NA (Not a TS cycle)\n",i);
		 }else if(st.size() != n || v[0] != v[k-1]){		// 没有访问到所有的点,或者没有访问到原点 
		 	printf("Path %d: %d (Not a TS cycle)\n",i,dist);
		 }else {			// 构成 TS 回路
		 	if(k > n+1){		// 必定有重复 
		 		printf("Path %d: %d (TS cycle)\n",i,dist);
			 } else 	printf("Path %d: %d (TS simple cycle)\n",i,dist);
		 	if(dist < ans){
		 		ans = dist;
		 		ansi = i;
			 }
		 }
	}
	printf("Shortest Dist(%d) = %d\n",ansi,ans);
	
	return 0;
}

3.0.2 图的边信息

核心:根本不涉及到图的结构,只要保存下图的边,遍历所有的边进行判断即可。

典例 A1154 Vertex Coloring

题目大意:给出⼀个图(先给出所有边,后给出每个点的颜⾊),问是否满⾜:所有的边的两个点的
颜⾊不相同
分析:把 所有边存起来,把所有点的颜⾊存起来(存的过程中放⼊set统计颜⾊个数),枚举所有边,
检查是否每条边的两点个颜⾊是否相同,若全不相同,则输出颜⾊个数,否则输出No~
代码:

#include<bits/stdc++.h>
using namespace std;

typedef pair<int,int> PII;
vector<PII> vec;
int n,m;
int a,b;
int main() {

	scanf("%d%d",&n,&m);
	for(int i = 0; i < m; ++i) {
		scanf("%d%d",&a,&b);
		vec.push_back(make_pair(a,b));
	}
	scanf("%d",&b);
	while(b--) {
		vector<int> v(n);		// 保存每个的颜色
		set<int> st;
		for(int i = 0; i < n; ++i) {
			scanf("%d",&v[i]);
			st.insert(v[i]);
		}
		int ok = 1;
		for(int i = 0; i < vec.size(); ++i) {
			if(v[vec[i].first] == v[vec[i].second]){
				ok = 0;
				printf("No\n");break;;
			}
		}
		if(ok)	printf("%d-coloring\n",st.size());
	}

	return 0;
}

3.1 DFS遍历

* DFS 遍历题型 总结

三件事
1.建图
2.设计DFS
3.遍历图

DFS 内部实现 也就三件事
0.维护访问数组 vis[ i ]
1.终点判断(一般没有)
2.修改标记 [+ 具体操作(计数、更新)]
3.所有可走点 DFS

const int MAXV = 1000;		// 最大节点数
const int INF = 0x7fffffff;

邻接矩阵版:

int n;	// 节点数
// 1. 建图
int G[MAXV][MAXV];			// 邻接表
bool vis[MAXV] = {false};				// 访问标记

//2.设计 DFS
void DFS(int u,int depth){
	// 1.终点判断

	// 2.操作
	vis[u] = true;
	
	//3.所有可走点 DFS
	for(int i = 0;i  < n;++i){
		// 没访问过,且可达
		if(vis[i] == false && G[u][i]= INF){
			DFS(i,depth+1);
		}
	}
}

//3.遍历图
void DFSTrave(){
	for(int i = 0; i <n;++i){
		if(vis[i] == false){
			DFS(i,1);
		}
	}
	
}

邻接表版:

int n;	// 节点数
// 1. 建图
struct node{
	int v;
	int w;
}
vector<node> Adj[MAXV];					// 邻接表
bool vis[MAXV] = {false};				// 访问标记

//2.设计 DFS
void DFS(int u,int depth){
	// 1.终点判断

	// 2.操作
	vis[u] = true;
	
	//3.所有可走点 DFS
	for(int i = 0;i  < Adj[u].size();++i){
		// 没访问过,且可达
		if(vis[ Adj[u][i] ] == false){
			DFS(Adj[u][i],depth+1);
		}
	}
}

//3.遍历图
void DFSTrave(){
	for(int i = 0; i <n;++i){
		if(vis[i] == false){
			DFS(i,1);
		}
	}
	
}

典例 Head of a Gang

题意:给出若干人之间的通话长度,这些通话将他们分为若干组。每个组的总边权 = 通话长度之和,每个人的点权 = 该人参与通话长度之和。给定阈值 K,只要一个组的总边权大于K,且人数大于2,即为犯罪团伙“Gang”,组内点权最大的视为头目
输出:犯罪团伙的个数,按字典序从小到大输出每个团伙的头目姓名,和成员人数。

分析:本题的处理设计到 边权 和 点权。
需要搞清楚很重要一个问题:
DFS到底是 遍历 所有的 还是所有的 ??
显然,本题要求所有的边权之和,是要遍历所有的边,即点是可以重复访问的(否则无法访问所有的边)。

遍历点:vis[ i ] 数组标记该点是否访问,修改标记代表已访问。
遍历边:G[ i ][ j ] 的权重是否合法,修改边权代表已访问。

本题要点:
1.是遍历所有的 “边” 而不是 “点。通过修改 G[][] 邻接表的边权使边无法重复访问;
2.map 适用于 string - int 之间转换的保存,并不是用来代替 字符串哈希。
3.DFS 过程中用 “引用” 保存 成员数,头目,总边权,在 DFS 外进行判断。

代码:

#include<cstdio>
#include<vector>
#include<string>
#include<map>
#include<iostream>
#define MAXN 1010
using namespace std;

// 邻接矩阵 
int G[MAXN][MAXN];
//记录点权
int weight[MAXN] ;
// 访问数组
bool vis[MAXN] = {false}; 

map<string,int>	name2int;				
map<int,string> int2name;
map<string,int>	gang;		// 记录头目对应的 gang 的人数 

int n,k;
string name1,name2;
int Time;

int number;			// 用输入的顺序作为编号 

int change(string& str){
	if(name2int.find(str) != name2int.end()){
		// 已经出现过,则直接返回
		return name2int[str]; 
	}else{
		name2int[str] = number++;
		return name2int[str];
	}
}

// 2. DFS
void DFS(int u,int& num,int& head,int& total){
	// 1.终点判断 
	
	// 2.操作 
	// 置为访问 
	
//	cout<<"now node: "<<int2name[u]<<endl;
	if(vis[u] == false) num++;
	vis[u] = true;
	
	if(weight[u] > weight[head])	head = u;
	
	// 3.所有可走点 dfs
	for(int i = 0;i < number;++i){
		if( G[u][i] > 0){
			// 没访问过且可达
			total += G[u][i];
			G[u][i] = 0;
			G[i][u] = 0;
//			cout<<"now total:"<<total<<endl;
			DFS(i,num,head,total);
		}
	}	
}

// 3.遍历图
void DFSTrave(){
	for(int i = 0;i < number;++i){
		if(int2name.find(i) != int2name.end()){
			int num = 0,head = -1,total = 0;
			DFS(i,num,head,total);
//			cout<<"head:"<<int2name[head]<<"total:"<<total<<endl;
			if(num > 2 && total > k){
				gang[int2name[head]] = num;
			}
		}
	}
} 

int main(){
	
	scanf("%d%d",&n,&k);
	// 1.建图 
	for(int i = 0;i < n;++i){
		// 输入一个人,还要判断他是否出现过 
		cin>>name1>>name2>>Time;
		
		int n1 = change(name1);
		int n2 = change(name2);
		int2name[n1] = name1;
		int2name[n2] = name2;
		
		// 更新点权
		weight[n1] += Time;
		weight[n2] += Time;
		
		// 更新边权
		G[n1][n2] += Time;
		G[n2][n1] += Time; 
				
	}
	
	// 遍历图 
	DFSTrave();
	cout<<gang.size()<<endl;
	for(map<string,int>::iterator it = gang.begin(); it != gang.end();++it){
		cout<<it->first<<" "<<it->second<<endl;
	}
	
	return 0;
}

典例 Battle over cities - 并查集

题意:给出一张无向图,删除某点后,求需要几条边使得剩下的点连通?
思路:连通块的个数 - 1;
连通块:DFS;并查集

方法二:并查集
由于后续需要删除点,所以不能读入时建立并查集,应当先保存下边的信息,然后每一次动态建立。

#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#define MAXN 10010
using namespace std;

int father[MAXN];
vector<int> Adj[MAXN];
bool vis[MAXN] = {false};
int n,m,k;

// 初始化
void init() {
	for(int i = 1; i <= n; ++i) {
		father[i] = i;
	}
}

// 查
int find_father(int x) {
	int a  = x;
	while(x != father[x]) {
		x = father[x];
	}

	// 路径压缩
	while(a != x) {
		int z  = a;
		a = father[a];
		father[z] = x;
	}
	
	return x;
}

// 并
void merge(int a,int b) {
	int a1 = find_father(a);
	int b1 = find_father(b);

	if(a1!=b1)	father[a1] = b1;
}

int f(int no) {
	
	// 初始化 
	init();
	
	//建立并查集
	for(int i = 1;i<=n;++i){
		if(i == no )	continue;
		for(int j = 0;j < Adj[i].size();++j){
			if(Adj[i][j] != no)	merge(i,Adj[i][j]);
		}
	} 
	
	// 统计连通块个数
	int cnt = 0;
	for(int i = 1;i <= n;++i){
		if(i == no)	continue;
		if(i == father[i])	++cnt;
	} 
	
	return cnt;
}

int main() {

	scanf("%d%d%d",&n,&m,&k);
	// 初始化

	int a,b;

	//由于,要删去某个点,所以要先保存下边信息,然后再建立并查集
	for(int i = 0; i < m; ++i) {
		scanf("%d%d",&a,&b);
		Adj[a].push_back(b);
		Adj[b].push_back(a);
	}
	
	int tmp;
	for(int querry = 0; querry < k; ++querry) {
		scanf("%d",&tmp);
		printf("%d\n",f(tmp)-1);
	}


	return 0;
}

3.2 BFS遍历

* BFS 遍历题型 总结

1.涉及到层数时,优先选用BFS。
2.用并查集统计连通块的个数,节省时间。

BFS内部实现 三件事:

0.维护入队标记 inq[ i ]
1.创建队列,初始节点入队
2.队不空时循环,出队一个元素
3.所有相邻可走点入队,修改标记

邻接矩阵版:

int n; 
int G[MAXV][MAXV];
bool inq[MAXV] = {false};		// 是否入队标记 

void BFS(int u){
	// 1.建队列 ,初始节点入队 
	queue<int> qu;
	qu.push(u);
	inq[u] = true;
	
	// 队不空时循环 
	while(!qu.empty()){
		// 出队一个元素 
		int now = qu.front();
		qu.pop();
		
		// 所有可走点入队
		for(int i = 0;i <n;++i){
			if(inq[i] == false && G[u][i] != INF){
				qu.push(i);
				inq[i] = true;
			}
		} 
	}
	
	
}


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

邻接表版:

/// 邻接表 版 
vector<int> Adj[MAXV];
int n;
bool inq[MAXV] = {false};		// 是否入队标记
void BFS(int u) {
	// 1.建队列 ,初始节点入队
	queue<int> qu;
	qu.push(u);
	inq[u] = true;

	// 队不空时循环
	while(!qu.empty()) {
		// 出队一个元素
		int now = qu.front();
		qu.pop();

		// 所有可走点入队
		for(int i = 0; i < Adj[u].size(); ++i) {
			if(inq[ Adj[u][i] ] == false ) {
				qu.push(i);
				inq[i] = true;
			}
		}
	}
}

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

添加层号:


struct Node{
	int v;			// 节点编号 
	int layer;		// 层号 
};

vector<Node> Adj[MAXV];
int n;
bool inq[MAXV] = {false};		// 是否入队标记
void BFS(int s) {
	// 1.建队列 ,初始节点入队
	queue<Node> qu;
	Node start;
	start.v = s;
	start.layer = 1;
	qu.push(start);
	inq[start.v] = true;

	// 队不空时循环
	while(!qu.empty()) {
		// 出队一个元素
		Node topNode = qu.front();
		qu.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 ) {
				qu.push(next);
				inq[next.v] = true;
			}
		}
	}
}

典例 The deepest root

题意:给出 N N N 个顶点和 N − 1 N-1 N1 条边,问:它们能否构成一棵树?如果能,从中选出树根,使得树的高度最大。输出所有的树根。
思路:
1.求连通块的个数:并查集;
2.求树的深度:BFS。

#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
#include<cstring>
#define MAXN 10010
using namespace std;

struct Node{
	int v;			// 终点 
	int layer;		// 层数 
};
vector<Node> G[MAXN];
bool inq[MAXN] = {false};
int n;

int father[MAXN];

void init(){
	for(int i = 1;i <= n;++i)	father[i] = i;
}

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

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


int BFS(int u){
	int maxLayer = 1;
	queue<Node> qu;
	
	Node start;
	start.v = u;
	start.layer = 1;
	
	inq[u] = true;
	qu.push(start);
	
	Node topNode;
	while(!qu.empty()){
		topNode = qu.front();
		int u = topNode.v;
		qu.pop();
		
		Node tmp;
		for(int i = 0;i < G[u].size();++i){
			if(inq[G[u][i].v ] == false){
				tmp.v = G[u][i].v;
				tmp.layer = topNode.layer + 1;
				maxLayer = max(maxLayer,tmp.layer);
				qu.push(tmp);
				inq[tmp.v] = true;
			}
		}
		
	}
	return maxLayer;
}

int main(){
	scanf("%d",&n);
	int a,b;Node t;
	init();
	for(int i = 0;i < n-1;++i){
		scanf("%d%d",&a,&b);
		t.v = b;
		G[a].push_back(t);
		t.v = a;
		G[b].push_back(t);
		Union(a,b);
	}
	
	// 用并查集统计连通块的个数 
	int cnt = 0; 
	for(int i = 1;i <= n;++i)	if(i == father[i]) ++cnt;
	if(cnt > 1)	{
		printf("Error: %d components",cnt);
		return 0;
	}
	
	// 从每个点 BFS,记录下最大值
	int maxDepth = 0;
	vector<int> ans;
	for(int i = 1;i <= n;++i){
		memset(inq,false,sizeof(inq));
		if(inq[i] == false)	{
			int tmp = BFS(i);
			if(tmp == maxDepth){
				ans.push_back(i);
			}else if(tmp > maxDepth){
				maxDepth = tmp;
				ans.clear();
				ans.push_back(i);
			}
		}
	}
	for(int i= 0;i < ans.size();++i){
		printf("%d\n",ans[i]);
	}
	
	
	return 0;
}

典例 7-10 Save James Bond - 遍历

链接

题目大意:地图大小100*100(-50-+50),007站在一个直径为15的岛上,需要踩着鳄鱼跳到岸边。给出鳄鱼的位置和007的跳跃距离,问能否跳到岸边?

分析:由于只需要回答是否,则判断所有可达的点中是否存在可以跳到岸边的点即可。
注意节点用struct 或 pair 存储。

代码:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<vector>
#include<map>
#include<queue>
#include<cmath>
#define INF 0x3f3f3f3f
#define MAXN 1010
using namespace std;
typedef pair<int,int> PII;

int n,dist;

double calDist(const PII& a,const PII& b) {
	return sqrt((a.first-b.first)*(a.first-b.first) + (a.second-b.second*(a.second-b.second)));
}

int a,b;
PII nodes[MAXN];
map<PII,bool> inq;
int main() {

	scanf("%d%d",&n,&dist)	;
	if(dist + 7.5 >= 50){
		printf("Yes\n");return 0;
	}
	for(int i = 0; i< n; ++i) {
//		scanf("%d%d",&a,&b);
		scanf("%d%d",&nodes[i].first,&nodes[i].second);
	}

	// bfs 遍历寻找可达的点中能否上岸
	int flag  = 0;
	// 初始节点入队
	queue<PII> qu;
	for(int i = 0; i< n; ++i) {
		if(calDist(nodes[i],make_pair(0,0)) <= 7.5+dist)	{
			qu.push(nodes[i]);
			inq[nodes[i]] = true;
		}
	}


	while(!qu.empty()) {
		PII top = qu.front();
		qu.pop();
		if(abs(top.first)+dist >= 50 || abs(top.second)+dist >= 50)	{
			flag = 1;
			break;
		}

		for(int i = 0; i < n; ++i) {
			if(!inq[nodes[i]] && calDist(top,nodes[i]) <= dist) {
				qu.push(nodes[i]);
				inq[nodes[i]] = true;
			}
		}
	}
	printf("%s\n",flag?"Yes":"No");


	return 0;
}

3.3 最短路径

3.3.1 * Dijkstra 算法

单源最短路径算法。

零、Dijkstra 考察内容总结
  1. Dijkstra 基础算法:初始化 + 三步;
  2. 记录路径:添加 p r e [ ] pre[] pre[] 数组;
  3. 添加第二尺度:再加一个数组保存特征,在 第三步的更新步骤中,当路径更小更新路径时,更新特征;当路径相等时,考虑是否更新特征。
一、Dijkstra 算法概述

0.设点的全集为 V V V, 点集 S S S ,表示已经求出 从起点 S 0 S_0 S0到目标点的最短距离的那些点,初始时为空;设 d [ i ] d[i] d[i]表示 S 0 S_0 S0 S i S_i Si的最短距离,并设 d [ S 0 ] = 0 d[S_0]=0 d[S0]=0;设 p r e [ i ] pre[i] pre[i]记录最短路径上 S i S_i Si的前趋结点,初始设为自身。
1.每次从 V − S V-S VS中取出一个距离 S 0 S_0 S0最近的点;
2.加入 S S S
3.扩展 u u u; 考虑:它的加入,是否使 S 0 S_0 S0 V − S V-S VS中的点的距离变小了,是则更新。

为了在初始时能选出起点,应使 d [ S 0 ] = 0 d[S_0]=0 d[S0]=0;为了获取路径,由于记录的是前趋结点,则从终点开始递归,使用后序思想,在递归到终点向上返回时,再输出。

代码如下:

邻接矩阵版:

/ Dijkstra算法三大步骤
// 0. 初始化,将 S0 到 所有点的距离设为 INF ; 将 d[s0] = 0;
// 1. n 次循环,一次从 V-S 中取出一个距 S0 最近的一个点,
// 2.将其加入 S
// 3.从刚加入的点扩展;判断刚加入的点,是否会更新 V-S 中的点到 S0 的距离。
// 注意:这样一定是保证 距离最短的,

// 记录路径:
//		使用 pre[] 数组记录 更新权值后 的结点的前趋
//		使用 DFS 在向上返回时输出

bool vis[MAXN] = {false};				// S 集合标记
int d[MAXN];				// 所有点到 S0 的最短距离
int n,m,s;		// 顶点个数,边数,起点编号						// 节点个数
int pre[MAXN];	// 记录 S0 到 V 的最短路径 (显然只能记录一条路径)

// 输出路径
void DFS(int s,int v) {			// 从终点开始递归,递归到底向上返回时输出,后序思想
	if(v == s)	{
		printf("%d\n",pre[v]) ;
		return;
	}

	DFS(s,pre[v]);
	printf("%d\n",v) ;
}


// 邻接矩阵版 Dijkstra
int G[MAXN][MAXN];
void Dijkstra(int s) {

	// 0.初始化,
	fill(d,d+MAXN,INF);
	for(int i = 0; i < n; ++i)	pre[i] = i;
	d[s] = 0;

	// n 次循环,一次从 V-S 中取出一个距离 S0 最近的点,初始时 S 为空
	for(int i = 0; i < n; ++i) {

		// 从 V-S 中取出一个距离 S0 最近的点
		int u = -1,MIN = INF;
		for(int j = 0; j < n; ++j) {
			if(vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}

		// 将其加入 S 集合
		if(u == -1)	return ;		// 说明没有点可达,即不连通
		vis[u] = true;

		// 扩展;判断刚加进来的点,是否会更新 V-S 中的点到 S0 的距离
		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;			// 记录路径
			}
		}

	}
}

邻接表版:
PS:就只是在访问边的时候不一样,其他都一模一样。

// 邻接表版 Dijkstra
struct Node {
	int v;
	int dis;		// dis  为边权
};
vector<Node> Adj[MAXN];

void Dijkstra(int s){
	// 0.初始化 S(vis[]) ,d[]
	fill(d,d+MAXN,INF);
	d[s] = 0;

	// 循环 n 次
	for(int i = 0;i < n;++i){

		// 从 V-S 中取出一个元素
		int u = -1,MIN = INF;
		for(int j = 0;j < n;++j){
			if(vis[j] == false && d[j] < MIN){
				u = j;
				MIN = d[j];
			}
		}

		// 将其加入 S 集合
		if(u == -1)	return ;
		vis[u] = true;

		// 3.扩展,判断是否更新
		for(int j = 0; j < Adj[u].size();++j){
			int v = Adj[u][j].v;
			if(vis[v] == false && Adj[u][j].dis + d[u] < d[v]){
				d[v] = Adj[u][j].dis + d[u];
				pre[v] = u;
			}
		}

	}
}
二、记录路径

见 一 中代码。

三、添加第二尺度

最短路径不止一条,需要通过第二尺度判断。

再加一个数组保存第二尺度(边权、点权、路径条数),在 第三步的扩展更新步骤中,当路径更小更新路径时,更新特征;当路径相等时,考虑是否更新特征。

首先,距离仍是第一要素,距离更新时,第二尺度随之更新;距离相等时,判断第二尺度是否需要更新。

1.给每条边再增加一个边权(比如花费)。要求最短路径时花费之和最小。

添加数组 cost[][] 记录新增的边权
		c[i]	记录 到 i 的最小花费之和, c[s] = 0

// 1.选出距离 S0 最近的点

// 2.加入 S

// 3.扩展,判断更新
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[] 保存点权
		w[i] 记录到 i 的最大点权之和, w[s] = weight[s]
// 1.选出距离 S0 最近的点

// 2.加入 S

// 3.扩展,判断更新
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[i] 记录到 i 的最短路径条数

// 1.选出距离 S0 最近的点

// 2.加入 S

// 3.扩展,判断更新
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];			// 继承 u 的路径条数 
		}
		else if(d[u] + G[u][v] == d[v]){
			// 相等时加一个额外判断即可
			num[v] += num[u];			// 最短距离同时累加 num
		}
	}
}

典例 Emergency - Dijkstra

注意搞清楚题意有向图还是无向图
一般来说,两城市之间的公路应该是无向图,除非特别说明。

题意:给出城市之间的无向图,每个城市有点权,求出 c 1 c_1 c1 c 2 c_2 c2的最短路径条数,以及最大点权。

方法一:普通 Dijkstra 开两个数组 n u m [ ] num[] num[] + w [ ] w[] w[] 分别记录。

#include<cstdio>
#include<algorithm>
#define MAXN 600
#define INF 0x3fffffff
using namespace std;


int n,m,c1,c2;
int u,v,t;

int G[MAXN][MAXN];
int weight[MAXN];

int vis[MAXN] = {false};
int d[MAXN];
int num[MAXN];
int w[MAXN];

void Dijkstra(int s){
	
	//  0.初始化 
	fill(d,d+MAXN,INF);
	d[s] = 0;
	num[s] = 1;
	w[s] = weight[s];
	
	//  n 次循环
	for(int i = 0;i < n;++i){
		// 1.从 V-S 中选出距 S0 最近的
		int u = -1,MIN = INF;
		for(int j = 0;j < n;++j){
			if(vis[j] == false && d[j] < MIN){
				u = j;
				MIN = d[j];
			}
		} 
		
		// 2. 加入 S ,
		if(u == -1)	return ;
		vis[u] = true;
		
		// 3. 扩展,判断 u 的加入是否 更新 V-S  的距离
		// 实际上就是 扩展边 的过程 
		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];
					w[v] = w[u] + weight[v];
				}else if(d[u] + G[u][v] == d[v]){
					num[v] += num[u];
					if(w[u] + weight[v] > w[v]){
						w[v] = w[u] + weight[v];
					}
				}
			}
		} 
		
	} // end of for n
	
}

int main(){
	
	// 初始化
	fill(G[0],G[0]+MAXN*MAXN,INF);
	
	scanf("%d%d%d%d",&n,&m,&c1,&c2);
	for(int i = 0;i < n;++i)	scanf("%d",&weight[i]);
	for(int i = 0;i < m;++i){
		scanf("%d%d%d",&u,&v,&t);
		G[u][v] = t;
		G[v][u] = t;
	}
	
	Dijkstra(c1);
	
	printf("%d %d",num[c2],w[c2]);
	
	return 0;
}

3.3.2 * Dijkstra + DFS 算法

此套算法模板的

核心思想:

  • 1.Dijkstra 只找出 “ 所有的” 最短路径(第一尺度);
  • 2.DFS从所有的最短路径中找出最优路径(第二尺度)。

步骤

1.Dijkstra

  • 求出最短路径 d[] (第一标尺);
  • 同时用 vector 数组 pre 存下所有最短路径,

2.DFS 从终点逆向递归

  • 求出一条完整的路径 tmpPath;
  • 遍历路径,记录下最优路径 Path.

第二标尺的判断:

  • 边权,用 cost[][] 存,c[i] 表示起点到 i 的最小花费。
  • 点权:用 weight[] 存,w[i] 表示起点到 i 的最大价值。
  • 数目,用 num[] 存,可在 Dijkstra 中,也可在 DFS 终点。

Dijkstra找最短路径的方法相同,用 vector pre[] 记录下 v 所有的前趋结点。
重点在于 DFS 找最优路径。 从终点出发,一路DFS到起点,然后一起计算路径上的总权重,路径已经被记录在pre中了。

模板:

vector<int> tmpPath,Path;	
void DFS(int v){
	//递归终点
	if(v == st){	// 如果到达起点
		// 终点需要单独插入
		tmpPath.push_back(v);
		
		int value = 0;
		计算value的值,点权或者边权。
		if(value 优于 optvalue){
			optvalue = value;
			Path = tmpPath;
		}

		// 终点需要单独删除,因为 return 后的代码执行不到了
		tmpPath.pop_back();
		return ;
	}
	//递归式
	tmpPath.push_back(v);
	for(int i = 0;i < pre[v],size();++i){
		DFS(pre[v][i]);
	}
	// 恢复环境
	tmpPath.pop_back();
}

例如:
计算边权点权的代码:
PS:路径条数的统计,可以使用普通Dijkstra,也可以在递归终点统计。
题目简洁、只有一个第二尺度时,使用普通Dijkstra也会更简洁。

vector<int> tmpPath,Path;	
void DFS(int v){

	递归终点
	//1.计算边权
	if(v == st){	// 如果到达起点
		int value = 0;
		// 逆着计算,因为DFS是从终点逆向起点的
		for(int i = tmpPath.size()-1;i ‘>0;--i){
			int id = tmpPath[i],idNext = tmpPath[i-1];
			value += G[id][idNext];
		}
		
		if(value > maxValue){
			maxValue = value;
			Path = tmpPath;
		}
	}
	
	递归式
	。。。
	
	//2.计算点权
	if(v == st){	// 如果到达起点
		int value = 0;
		// 逆着计算,因为DFS是从终点逆向起点的
		for(int i = tmpPath.size()-1;i ‘>=0;--i){
			int id = tmpPath[i];
			value += weight[id];
		}
		if(value > maxValue){
					
		}
	}

}

模板

int G[MAXN][MAXN];
bool vis[MAXN] = {false};
int d[MAXN];

int weight[MAXN];
vector<int> pre[MAXN];
int num[MAXN];

void Dijkstra(int s) {

	// 0.初始化
	fill(d,d+MAXN,INF);
	d[s] = 0;
	num[s] = 1;

	// n 次循环
	for(int i = 0; i < n; ++i) {
		// 1. 从 V-S 中取出一个距离 S0 最近的点
		int u = -1,MIN = INF;
		for(int j = 0; j < n; ++j) {
			if(vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}

		// 2.加入 s
		if(u == -1)	return ;
		vis[u] = true;

		// 3 .从 u 扩展 V-S
		for(int v = 0; v < n; ++v) {
			if(vis[v] == false && G[u][v] != INF ) {
				if(d[u] + G[u][v] < d[v]) {
					第一标尺
					pre[v].clear();
					pre[v].push_back(u);
					d[v] = d[u] + G[u][v];
					
					第二标尺中的 num 可以放在这
					num[v] = num[u];
				} else if(d[u] + G[u][v] == d[v]) {
					pre[v].push_back(u);
					
					num[v] += num[u];
				}
			}
		}

	}

}

vector<int> tmpPath,Path;			// 由于 tmpPath 是全局变量,所以需要回复环境的 
int maxValue;						// 记录下最大的点权或边权 

// 从终点 (固定的) 开始逆向 DFS 至 起点(固定的),然后一起算路径上的总权值 
void DFS(int s){
	// 递归终点,当递归到起点时,结束 
	if(s == c1){
		tmpPath.push_back(s);
		
		// 计算权值 (点权) 
		计算第二标尺
		int value = 0;
		for(int i = tmpPath.size()-1;i >= 0 ;--i){
			value += weight[tmpPath[i]];
		}
		if(value > maxValue){
			maxValue = value;
			Path = tmpPath;
		}
		
		// 恢复环境
		tmpPath.pop_back(); 
	}
	
	
	// 递归式,所有可走点 DFS
	
	//添加路径
	tmpPath.push_back(s) ;
	// 所有可走点 DFS 
	for(int i = 0;i < pre[s].size();++i){
		DFS(pre[s][i]);
	} 
	// 恢复环境 
	tmpPath.pop_back();
}
典例 Emergency - Dijkstra + DFS

方法二: Dijkstra + DFS
注意:是有向边还是无向边!!

#include<cstdio>
#include<algorithm>
#include<vector>
#define MAXN 1010
#define INF 0x3fffffff
using namespace std;


// Dijkstra + DFS 思想:
//		Dijkstra 只记录下最短路径(第一标尺),但要记录下多条路径,就要用 vector
// 					每次遇到更小的路径时需要清空 vector 路径,所以不需要初始化
//		DFS 从终点开始向起点递归,寻找满足条件的最优路径(第二标尺),保存路径,递归到终点时,一起计算路径上的权值

int n,m,c1,c2;
int G[MAXN][MAXN];
bool vis[MAXN] = {false};
int d[MAXN];

int weight[MAXN];
vector<int> pre[MAXN];
int num[MAXN];

void Dijkstra(int s) {

	// 0.初始化
	fill(d,d+MAXN,INF);
	d[s] = 0;
	num[s] = 1;

	// n 次循环
	for(int i = 0; i < n; ++i) {
		// 1. 从 V-S 中取出一个距离 S0 最近的点
		int u = -1,MIN = INF;
		for(int j = 0; j < n; ++j) {
			if(vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}

		// 2.加入 s
		if(u == -1)	return ;
		vis[u] = true;

		// 3 .从 u 扩展 V-S
		for(int v = 0; v < n; ++v) {
			if(vis[v] == false && G[u][v] != INF ) {
				if(d[u] + G[u][v] < d[v]) {
					pre[v].clear();
					pre[v].push_back(u);
					d[v] = d[u] + G[u][v];
					num[v] = num[u];
				} else if(d[u] + G[u][v] == d[v]) {
					pre[v].push_back(u);
					num[v] += num[u];
				}
			}
		}

	}

}

vector<int> tmpPath,Path;			// 由于 tmpPath 是全局变量,所以需要回复环境的 
int maxValue;						// 记录下最大的点权或边权 

// 从终点 (固定的) 开始逆向 DFS 至 起点(固定的),然后一起算路径上的总权值 
void DFS(int s){
	// 递归终点,当递归到起点时,结束 
	if(s == c1){
		tmpPath.push_back(s);
		
		// 计算权值 (点权) 
		int value = 0;
		for(int i = tmpPath.size()-1;i >= 0 ;--i){
			value += weight[tmpPath[i]];
		}
		if(value > maxValue){
			maxValue = value;
			Path = tmpPath;
		}
		
		// 恢复环境
		tmpPath.pop_back(); 
	}
	
	
	// 递归式,所有可走点 DFS
	
	//添加路径
	tmpPath.push_back(s) ;
	// 所有可走点 DFS 
	for(int i = 0;i < pre[s].size();++i){
		DFS(pre[s][i]);
	} 
	// 恢复环境 
	tmpPath.pop_back();
}

int main() {
	fill(G[0],G[0]+MAXN*MAXN,INF);
	scanf("%d%d%d%d",&n,&m,&c1,&c2);
	for(int i  = 0; i< n; ++i) {
		scanf("%d",&weight[i]);
	}
	int u,v,tmp;
	for(int i= 0; i <m; ++i) {
		scanf("%d%d%d",&u,&v,&tmp);
		G[u][v] = tmp;
		G[v][u] = tmp;
	}

	Dijkstra(c1);
	DFS(c2);
	
	printf("%d %d",num[c2],maxValue);
	

	return 0;
}
典例 Public Bike Management - 加油问题,第三标尺

题意:城市里有一些公共自行车站,每个车站的最大容量为一个偶数 C m a x Cmax Cmax,如果该车站的自行车数量为 C m a x 2 \frac{Cmax}{2} 2Cmax,那么就称该车站处于完美状态。
如果一个车站的容量是满的或者空的,控制中心就要携带或者从路上搜集一定数量的自行车前往该车站,以使问题车站及沿途所有车站处于完美状态。现给出 C m a x Cmax Cmax、车站数目 n n n、问题车展编号 S p S_p Sp、无向边数 M M M以及边权,求一条从控制中心到 S p S_p Sp的最短路径,输出需要携带的自行车数目、路径、需要携带回中心的数目。如果路径有多条,则选择携带最少的;如果仍有多条,则选择带回中心最少的。

注意“调整过程需要在前往问题车站的过程中就调整完毕,返程不调整。

分析:
1.调整的过程是动态的,类似加油问题,而不能单纯的算总和。
英文原文是读不出动态过程的。
2.本题加入了第三标尺,第一标尺是距离,第二标尺是需求need,第三标尺是剩余remain。
注意:第二标尺更新时,第三标尺无条件更新,不要说是 Dijkstra+DFS就忘了这一点。

加油问题的逻辑:
在DFS从终点回溯到起点,计算路径总权值时,设need表示需要从中心带的数目,remain表示当前剩余的可用来补给的数目。

// 递归终点
	if(v == 0) {
		tmpPath.push_back(v);
		int need = 0,remain = 0;

		for(int i = tmpPath.size()-2; i >= 0; --i) {
			int id = tmpPath[i];
			if(weight[id] >= cmax / 2) {
				// 如果超过了一半,可以用来补给
				remain += weight[id] - cmax / 2;
			} else {
				// 当前不够

				if(remain > cmax/2-weight[id]) {
					// 携带的够补给,那么就补给
					remain -=   cmax/2-weight[id];
				} else {
					// 不够,补给剩下的要从 PBMC 带

					need += (cmax/2-weight[id])-remain;
					remain = 0;
				}
			}


		}

		

完整代码:

#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
#define INF 0x3fffffff
#define MAXN 550
using namespace std;


int cmax,n,sp,m;
int weight[MAXN];
int G[MAXN][MAXN];

int d[MAXN];
bool vis[MAXN] = {false};
int w[MAXN];
vector<int> pre[MAXN];

void Dijkstra(int s) {

	// 0.初始化
	fill(d,d+MAXN,INF);
	d[0] = 0;

	// n 次循环
	for(int i = 0; i <= n; ++i) {
		// 1. 从 V-S 中取出最近的
		int u = -1,MIN = INF;
		for(int j = 0; j <= n; ++j) {
			if(vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}

		// 2.加入 S
		if(u == -1)	return ;
		vis[u] = true;

		//3.扩展 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].clear();
					pre[v].push_back(u);
				} else if( d[u]+G[u][v] == d[v]) {
					pre[v].push_back(u);
				}
			}
		}
	}
}

vector<int> tmpPath,Path;
int minRemain = INF,minNeed = INF;
int bestValue;
int flag;			// 0 是找最大值,1 是找最小值 (full)
void DFS(int v) {
	// 递归终点
	if(v == 0) {
		tmpPath.push_back(v);
		int need = 0,remain = 0;

		for(int i = tmpPath.size()-2; i >= 0; --i) {
			int id = tmpPath[i];
			if(weight[id] >= cmax / 2) {
				// 如果超过了一半,可以用来补给
				remain += weight[id] - cmax / 2;
			} else {
				// 当前不够

				if(remain > cmax/2-weight[id]) {
					// 携带的够补给,那么就补给
					remain -=   cmax/2-weight[id];
				} else {
					// 不够,补给剩下的要从 PBMC 带

					need += (cmax/2-weight[id])-remain;
					remain = 0;
				}
			}


		}

		if(need < minNeed) {
			minNeed = need;
			Path = tmpPath;
			minRemain = remain;
		} else if(need == minNeed) {
			if(remain < minRemain) {
				minRemain = remain;
				Path = tmpPath;
			}
		}


		tmpPath.pop_back();
		return ;
	}

	// 递归式,所有可走点 DFS
	tmpPath.push_back(v);
	for(int i = 0; i < pre[v].size(); ++i) {
		DFS(pre[v][i]);
	}
	tmpPath.pop_back();
}

int main() {
	fill(G[0],G[0]+MAXN*MAXN,INF);
	scanf("%d%d%d%d",&cmax,&n,&sp,&m);
	for(int i = 1; i <= n; ++i) {
		scanf("%d",&weight[i]);
	}
	int si,sj,t;
	for(int i = 0; i < m; ++i) {
		scanf("%d%d%d",&si,&sj,&t);
		G[si][sj] = G[sj][si] = t;
	}


	Dijkstra(0);
	DFS(sp);
	int len = Path.size();

	printf("%d ",minNeed);
	printf("0");
	for(int i = len-2; i >= 0; --i) {
		printf("->%d",Path[i]);
	}
	printf(" %d",minRemain);


	return 0;
}
典例 Gas Station - 多次Dijkstra

题意:加油站选址需要使加油站与任何住宅之间的最小距离尽可能远;但是同时必须保证所有房屋都在其服务范围内。题目给定城市地图和加油站的几个候选地点,题目要求给出地点推荐。如果有多个解,则输出到所有房子的平均距离最小的那个。如果这样的解决方案仍然不是惟一的,则输出索引号最小的解决方案。

思路:分别求每一个加油站候选位置到居民区的距离,由于有多个点,所以需要多次 Dijkstra ,(最开始想的是Floyd)。并且,题目只要求距离,所以不用保存路径,也不用DFS求路径,只获得 d [ ] d[] d[] 数组即可。
第一标尺:距离;
第二标尺:最小距离最大;
第三标尺:平均距离最小。

注意点:居民点 i ( 1 − N ) i(1-N) i(1N) 与 gas station G i G_i Gi 的处理,读入字符串,将 G i G_i Gi保存为 N + i N+i N+i,即跟着居民后面累加。使用 atoi 函数

代码:

#include<cstdio>
#include<algorithm>
#include<vector>
#include<string>
#include<cstring>
#define MAXN 1020
#define INF 0x3fffffff
using namespace std;


int n,m,k,ds;
int G [MAXN][MAXN];
int dist;
char p1[10],p2[10];
int a,b;

int N;
int d[MAXN];
bool vis[MAXN] = {false};

void Dijkstra(int s) {

	// 0.初始化
	fill(d,d+MAXN,INF);
	fill(vis,vis+MAXN,false);
	d[s] = 0;

	// n 次循环
	for(int i = 1; i <= N; ++i) {
		// 1.从 V-S 中挑一个最近的
		int u = -1,MIN = INF;
		for(int j = 1; j <= N; ++j) {
			if(vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}

		// 2.加入 S
		if(u == -1)	return ;
		vis[u] = true;

		// 3.扩展 u
		for(int v = 1; 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];
				}
			}
		}
	}
	
}
double maxMinDis = 0;
double minAvgDis = INF;
int ans;
int main() {

	scanf("%d%d%d%d",&n,&m,&k,&ds);
	N = n+m;
	fill(G[0],G[0]+MAXN*MAXN,INF);
	for(int i = 0; i < k; ++i) {
		scanf("%s%s",p1,p2);
		scanf("%d",&dist);
		if(p1[0] == 'G') {
			a = n + atoi(p1+1);
		} else a = atoi(p1);
		if(p2[0] == 'G') {
			b = n + atoi(p2+1);
		} else b = atoi(p2);
//		printf("a:%d b:%d\n",a,b);
		G[a][b] = G[b][a] = dist;
	}
	
//	for(int i = 1;i <= N;++i){
//		for(int j  =1;j <= N;++j){
//			printf("%d ",G[i][j]);
//		}
//		printf("\n");
//	}

	// 从 每个 Gas 出发,Dijkstra,求 最小距离和平均距离
	for(int i = n+1; i <= N; ++i) {
		int flag = 1;
		Dijkstra(i);
		// 求最小距离
		// mindis 记录最小距离,totdis 记录总长度们用来求平均
		double mindis = INF,totdis = 0;
		for(int j = 1; j <= n; ++j) {
			totdis += d[j];
			if(d[j] > ds){
				flag = 0;
				break;
			}
			if(d[j] < mindis) {
				mindis = d[j];
			}
		}
		
		if(flag == 0)	continue;
		// 更新最小值 
		double avg = totdis / n;
		if(mindis > maxMinDis) {
			maxMinDis = mindis;
			minAvgDis = avg;
			ans = i;
		} else if(mindis == maxMinDis) {
			if(avg < minAvgDis){
				minAvgDis = avg;
				ans = i;
			}
				
		}
		
		
	}
	
	if(ans){
		printf("G%d\n",ans-n);
		printf("%.1lf %.1lf",maxMinDis,minAvgDis);
	}else printf("No Solution");
	

	return 0;
}

3.4拓扑排序

主要作用:判断有向无环图(DAG)。
步骤:

  • 1.定义一个队列 Q Q Q,将所有入度为 0 0 0 的点加入队列;
  • 2.取出队首节点输出;删去所有从它出发的边,令这些边到达的点入度减 1.
  • 3.反复进行 2. 操作,直到队列为空。

如果队列为空时,入过队的结点数恰好为 N,则拓扑排序成功,是有向无环图;否则排序失败,图中有环。
(只有入度为 0 的点才能入队,有节点没有入队,说明有环的存在)
由于需要记录结点的入度,需要额外建立一个数组 i n D e g r e e [ M A X N ] inDegree[MAXN] inDegree[MAXN] ,读入图时记录每个点的入度。

代码:

#include<cstdio>
#include<algorithm>
#include<vector>
#define MAXN 10010
using namespace std;

// topologicalSort 思想:
//	将 入度为 0 的点加入队列,然后删除他的边,将新的 入度为0的点加入
// 重复上述步骤,看最后 加入拓扑排序路径的点 是否是全部,是则为 DAG

vector G[MAXN];
int inDegree[MAXN];		// 保存每个点的入度
int n;
bool topologicalSort() {

	int sum = 0;		// 统计排序的数目
	queue<int> qu;

	// 初始时 入度 为 0 的点 入队 
	for(int i = 0; i < n; ++i) {
		if(inDegree[i] == 0) {	
			qu.push(i);
		}
	}

	while(!qu.empty()) {
		int u = qu.front();
		qu.pop();
		++sum;

		// 删除它的出边
		for(int i = 0; i < G[u].size(); ++i) {
			int v = G[u][i];
			inDegree[v]--;
			if(inDegree[v] == 0) {
				qu.push(v);

			}
		}
		G[u].clear();			// 如无必要,可不写
	}
	
	if(sum == n)	return true;
	else return false;
}


int main() {
	...
	return 0;
}
典例 Topological Order

题目:给出图,和几个序列,找出不是拓扑序列的序列。
输入:N,K表示点的个数和边的个数。K行,代表边。
跟着S,输入S个序列。
输出:不是拓扑序列的编号。
在这里插入图片描述
回顾拓扑排序的过程:
0.将初始入度为 0 的点加入队列,
1.出队一个元素,删除他的边,若该边指向的点入度为 0,则将其加入;
2.重复上述步骤直至队列为空,最后判断入过队的个数。

其中,可能有多个入度为 0 的点,其入队的先后顺序不同,可获得不同的拓扑序列。

而现在题目给出了一个具体的序列,需要我们判断是否是拓扑序列。那么问题转化为:
按照给定的序列进行入队操作,判断是否是合法(入队的元素入度为 0 )的入队操作。
即:按照给定序列,该入队的结点,其入度是否为 0 ?

代码:

#include<cstdio>
#include<algorithm>
#include<queue>
#define MAXN 1010
#define INF 0x3fffffff
using namespace std;


int n,k;
int G[MAXN][MAXN];
int a,b;
int s[MAXN];

int inDegree[MAXN];
int inDegree1[MAXN];
bool topologicalSort() {
	for(int i = 1; i <= n; ++i)	inDegree1[i] = inDegree[i];


	// 按照给出序列进行操作
	for(int  i= 0; i < n; ++i) {
		int u = s[i];
		// 入度不为 0, 直接失败
		if(inDegree1[u] != 0)	return false;

		// 否则将其入队,然后删除边
		for(int j = 1; j <= n; ++j ) {
			if(G[u][j] != INF)	--inDegree1[j];
		}

//		printf("\ninDegree1:\n");
//	for(int i = 0;i < n;++i)	printf("%d ",inDegree1[i]);
//	printf("\n");
	}


	return true;
}

vector<int> ans;
int main() {

	fill(G[0],G[0]+MAXN*MAXN,INF);
	scanf("%d%d",&n,&k);
	for(int i = 0; i < k; ++i) {
		scanf("%d%d",&a,&b);
		G[a][b] = 1;
		inDegree[b]++;
	}
	scanf("%d",&k);
	for(int i = 0; i < k; ++i) {
		for(int j = 0; j < n; ++j) {
			scanf("%d",&s[j]);
		}

		if(topologicalSort() == false)	ans.push_back(i);
	}

	for(int i = 0; i < ans.size(); ++i) {
		printf("%d",ans[i]);
		if(i < ans.size()-1)	printf(" ");
	}

	return 0;
}

3.5 BellmanFord- SPFA

BellmanFord

步骤

  • V-1 轮循环(V为节点数),每轮循环遍历所有的 (故使用邻接表),进行松弛(更新)操作(与Dijkstra更新的部分一样)。
  • 在做一轮松弛,如果有点被更新,则说明有负权环。

注意:前趋点的保存 pre[] 数组换用 set<int> pre[MAXN]。因为一个点可能会更新多次。
dfs 求第二标尺也是一模一样,只是要用 迭代器 it 遍历 set 。

解释:一点到另一点的最短路径最长为 v-1,故进行 v-1 轮循环。每次遍历所有的边进行松弛。

代码:

int n,m,c1,c2;
int weight[MAXN];				// 点权
struct edge {
	int v;
	int dist;
};

vector<edge> G[MAXN];
int d[MAXN];
set<int> pre[MAXN];			// 换用 set 存储,因为可能会有多次更新

/*
	思想:重复 n-1 轮:遍历所有的边,进行松弛操作
*/
bool bellman(int s) {

	// 0. 初始化
	fill(d,d+MAXN,INF);
	d[s] = 0;

	// 重复 n-1 轮
	for(int i = 0; i <n-1; ++i) {
		// 遍历所有的边,进行松弛操作 :   !!! 取每个点的每条边
		for(int u = 0; u < n; ++u) {
			for(int j = 0; j < G[u].size(); ++j) {
				int v = G[u][j].v;		// 边的目标点
				int dis = G[u][j].dist;	// 边的长度

				// 松弛
				if(d[u] + dis < d[v]) {
					d[v] = d[u] + dis;
					pre[v].clear();
					pre[v].insert(u);
				} else if(d[u] + dis == d[v])	pre[v].insert(u);
			}
		}
	}

	// 判断负权环	: 再做一轮,如果有更新,则说明有负权环
	for(int u = 0; u < n; ++u) {
		for(int j = 0; j < G[u].size(); ++j) {
			int v = G[u][j].v;		// 边的目标点
			int dis = G[u][j].dist;	// 边的长度

			// 松弛
			if(d[u] + dis < d[v]) {
				return false;
			}
		}
	}

	return true;
}

vector<int> tmpPath,Path;
int maxValue;
int num;
void dfs(int x) {			// 与 Dijkstra 一样 的
	if(x == c1) {
		tmpPath.push_back(x);

		// 能够到达终点,路径条数 + 1
		num++;

		int value = 0;
		for(int i = 0; i < tmpPath.size(); ++i)	value += weight[tmpPath[i]];
		if(value > maxValue) {
			maxValue = value;
			Path = tmpPath;
		}

		tmpPath.pop_back();
	}


	tmpPath.push_back(x);
	for(auto it = pre[x].begin(); it != pre[x].end(); ++it)
		dfs(*it);
	tmpPath.pop_back();
}


//vector<egde> G[MAXN];			// 邻接表
int a,b,dist;
int main() {

	cin >> n >> m >> c1 >> c2;
	for(int i = 0; i < n; ++i)cin >> weight[i];
	for(int i = 0; i < m; ++i)	{
		cin >> a >> b >> dist;
		G[a].push_back(edge {b,dist});
		G[b].push_back(edge {a,dist});
	}

	// Bellman-Ford 算法
	bellman(c1);
	dfs(c2);
	
	cout << num << " " << maxValue << endl;

	return 0;
}

SPFA

思想
使用 edge 结构体保存边权,建立邻接表。

struct edge{
	int v,dist;
}
  • 外层 n-1 轮循环换用 队列 来控制,由此需要多维护两个数组
    • inq[] 入队标记
    • num[] 入队次数,用于判断负权环
  • 内层遍历所有的点,变为 遍历队首 u 的所有边

步骤
队不空时循环:
0. 初始化;建队;起点入队
1.出队一个元素,修改 inq 标记
2.所有可走点入队:遍历 u 的所有边,进行松弛;若 v 发生了松弛,判断 inq[v] 并加入队列

代码:


int n,m,c1,c2;
int weight[MAXN];			// 点权
struct edge {
	int v;
	int dist;
	edge(int _v,int _dist):v(_v),dist(_dist) {	}
};
vector<edge> G[MAXN];		// 邻接表
int a,b,dist;

int d[MAXN];
set<int> pre[MAXN] ;

bool inq[MAXN];				// 入队标记
int num[MAXN];				// 入队次数

bool SPFA(int s) {

	// 0. 初始化
	fill(d,d+MAXN,INF);
	fill(inq,inq+MAXN,false);


	//  初始节点入队
	queue<int> qu;
	qu.push(s);
	inq[s] = true;
	num[s]++;
	d[s] = 0;

	// 队不空时循环
	while(!qu.empty()) {

		// 出队一个元素
		int u = qu.front();
		qu.pop();
		inq[u] = false;			// 修改入队标记

		// 所有可走点入队:遍历所有的边,松弛了的点就入队
		for(int j = 0; j < G[u].size(); ++j) {		// 对应的边
			int v = G[u][j].v;
			int dist = G[u][j].dist;

			// 松弛
			if(d[u]+dist < d[v]) {
				d[v] = d[u] + dist;

				pre[v].clear();
				pre[v].insert(u);

				// 入队
				if(inq[v] == false)	{
					qu.push(v) ;
					inq[v] = true;
					// 判断负权环
					if(num[v] >= n)	return false;
				}
			} else if(d[u] + dist == d[v]) {
				pre[v].insert(u);
			}
		}

	}

	return true;

}

vector<int> tmpPath,Path;
int maxValue;
int cnt;
void dfs(int x) {
	if(x == c1)	{
		cnt++;
		tmpPath.push_back(x);

		int value = 0;
		for(int i = 0; i < tmpPath.size(); ++i)	value += weight[tmpPath[i]];
		if(value > maxValue) {
			maxValue = value;
			Path = tmpPath;
		}

		tmpPath.pop_back();
		return ;
	}

	tmpPath.push_back(x);
	for(auto it = pre[x].begin(); it != pre[x].end(); ++it)	dfs(*it);
	tmpPath.pop_back();
}


int main() {

	cin >> n >> m >> c1 >> c2;
	for(int i = 0 ; i < n; ++i)	cin >> weight[i];
	for(int i = 0; i < m; ++i)	{
		cin >> a >> b >> dist;
		G[a].push_back(edge {b,dist});
		G[b].push_back(edge {a,dist});
	}

	SPFA(c1);

	dfs(c2);

	cout << cnt <<" "<< maxValue << endl;

	return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值