图论总结 Dijkstra Tarjan 最小生成树 二分图 最短路 强连通分量 双连通分量 Bellman-Ford SPFA 二分图染色 Kruskal Prim 网络流 二分图匹配 Dinic

这周学些图论。
图论大概NOIP考的有这些算法:

Dijkstra

SCC

BCC

Bipartite

Kruskal

(Prim)

Bellman-Ford

Dinic

以及一些常用技巧。

首先讲Dijkstra

一般的Dijkstra就是堆优化对吧

算单源最短路的时间复杂度是O(nlogn)

完整代码。

#include <vector>
#include <queue>
#include <cstring>
#include <cstdio>
using namespace std;
const int maxn = 100050;
const int inf = 1e9;
struct edge{
	int to, dist;
};
struct heap{
	int d, u;
	bool operator < (const heap& rhs) const {
		return d > rhs.d;
	}
};
vector<edge> G[maxn];
bool vis[maxn];
int dj[maxn], pre[maxn];
int n, m;

void dijkstra(int st) {
	priority_queue<heap> Q;
	for(int i = 0; i < n; i++) dj[i] = inf;
	dj[st] = 0;
	memset(vis, 0, sizeof(vis));
	Q.push((heap){0, st});
	while(!Q.empty()) {
		heap j = Q.top(); Q.pop();
		int u = j.u;
		if(vis[u]) continue;
		vis[u] = 1;
		for(int i = 0; i < G[u].size(); i++) {
			int v = G[u][i].to, w = G[u][i].dist;
			if(dj[v] > dj[u] + w) {
				dj[v] = dj[u] + w;
				pre[v] = u;
				Q.push((heap){dj[v], v});
			}
		}
	}
}

int main() {
	scanf("%d%d", &n, &m);
	for(int i = 0; i < m; i++) {
		int from, to, dist;
		scanf("%d%d%d", &from, &to, &dist);
		edge e;
		e.to = to - 1; e.dist = dist;
		G[from - 1].push_back(e);
	}
	dijkstra(0);
	for(int i = 0; i < n; i++) printf("%d ", dj[i]);
	return 0;
}

讲几个算法

比如一个图有一些边,给定st(start)和ed(end),但你可以选择一条路,经过它的花费是K。求最短路。

常用的做法是从st做一次Dijkstra,然后从ed做一次反的Dijkstra。这样每个节点就有两个值,到st的距离和到ed的距离。然后枚举每一条边,找每一条边两端点到st、ed的和加上K的最小值。

Dijkstra求最短路还可以加上一些限制。改一下到st距离的意义,然后对入队条件加一些限制即可。heap里两个元素不变。

一个猜想:只要是有给定st点和ed点的问题,如果可以通过从st出发DFS到ed找出所有边,并对经过的边这个数组经过向一边扫描处理可以得到答案的问题,都可以用Dijkstra做。

SCC强连通分量

#include <iostream>
#include <vector>
#include <stack>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 100050;
vector<int> G[maxn];
stack<int> S;
int n, m, dcl, sccn;
int sccno[maxn], pre[maxn];

int getlow(int u) {
	int lowu = pre[u] = ++dcl;
	S.push(u);
	for(int i = 0; i < G[u].size(); i++) {
		int v = G[u][i];
		if(!pre[v]) {
			int lowv = getlow(v);
			lowu = min(lowu, lowv);
		}else if(!sccno[v]) lowu = min(lowu, pre[v]);
	}
	if(lowu == pre[u]) {
		sccn++;
		for(;;) {
			int j = S.top(); S.pop();
			sccno[j] = sccn;
			if(u == j) break;
		}
	}
	return lowu;
}
void find_scc() {
	dcl = sccn = 0;
	memset(sccno, 0, sizeof(sccno));
	memset(pre, 0, sizeof(pre));
	for(int i = 0; i < n; i++) if(!pre[i]) getlow(i);
}

int main() {
	cin >> n >> m;
	for(int i = 0; i < m; i++) {
		int from, to;
		cin >> from >> to;
		G[from - 1].push_back(to - 1);
	}
	find_scc();
	for(int i=0;i<n;i++)cout<<sccno[i]<<" ";
	cout<<endl;
}

主要用处是缩点,去掉有向图中的环变成DAG,然后就可以用DP乱整了。

BCC双连通分量

注意stack中存的是边

#include <vector>
#include <stack>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
const int maxn = 100050;
struct edge{
	int from, to;
};
vector<int> G[maxn];
stack<edge> S;
int dcl, bccn, pre[maxn], bccno[maxn], iscut[maxn];
int n, m;

int getlow(int u, int fa) {
	int lowu = pre[u] = ++dcl;
	int child = 0;
	for(int i = 0; i < G[u].size(); i++) {
		int v = G[u][i];
		if(!pre[v]) {
			S.push((edge){u, v});
			child++;
			int lowv = getlow(v, u);
			lowu = min(lowu, lowv);
			if(lowv >= pre[u]) {
				iscut[u] = 1;
				bccn++;
				for(;;) {
					edge j = S.top(); S.pop();
					bccno[j.from] = bccn;
					bccno[j.to] = bccn;
					if(j.from == u && j.to == v) break;
				}
			}
		}else if(pre[v] < pre[u] && v != fa) {
	        S.push((edge){u, v});
			lowu = min(lowu, pre[v]);
		}
	}
	if(fa < 0 && child == 1) iscut[u] = 0;
	return lowu;
}
void find_bcc() {
	dcl = bccn = 0;
	memset(pre, 0, sizeof(pre));
	memset(bccno, 0, sizeof(bccno));
	memset(iscut, 0, sizeof(iscut));
	for(int i = 0; i < n; i++) if(!pre[i]) getlow(i, -1);
}

int main() {
	scanf("%d%d", &n, &m);
	for(int i = 0; i < m; i++) {
		int from, to;
		scanf("%d%d", &from, &to);
		G[from - 1].push_back(to - 1);
		G[to - 1].push_back(from - 1);
	}
	find_bcc();
	for(int i = 0; i < n; i++) printf("%d ", bccno[i]);
	printf("\n");
	return 0;
}

没什么好讲的。

Bipartite二分图染色

判断图是不是二分图。这个遍历邻边的方法在很多都有用,比如2-SAT等。

#include <vector>
#include <cstdio>
#include <iostream>
using namespace std;
const int maxn = 100050;
vector<int> G[maxn];
int col[maxn];
int n, m;
int read() {
	int a = 0, c;   
	do c = getchar(); while(c < 48 || c > 57);
	do{a = a * 10 + c - 48; c = getchar();} while(c > 47 && c < 58);
	return a;
}

int bipartite(int u) {
	for(int i = 0; i < G[u].size(); i++) {
		int v = G[u][i];
		if(col[v] == col[u]) return 0;
		if(!col[v]) {
			col[v] = 3 - col[u];
			if(!bipartite(v)) return 0;
		}
	}
	return 1;
}

int main() {
	n = read(); m = read();
	for(int i = 0; i < m; i++) {
		int from = read() - 1, to = read() - 1;
		G[from].push_back(to);
//		G[to].push_back(from);
	}
	col[0] = 1;
	cout << bipartite(0) << endl;
	for(int i = 0; i < n; i++) cout << col[i] << " ";
	cout << endl;
	return 0;
}

另外,虽然我用的都是vector存邻接表,但据说经典的样式更好。

Kruskal

限于篇幅,不贴代码了。这里讲几个应用。

最小瓶颈路:求无向图中u到v的一条路径,使经过的边权最大值最小。

方法:求出最小生成树,则u到v在树上的路径就是所求路径。

每对结点间的最小瓶颈路:求无向图中每两个节点间的最小瓶颈路大小。

方法:先求出最小生成树。然后选定一个点为根节点,跑DFS。对于每对结点间的所求值,取为其父亲节点和定点的值与父亲节点与子节点距离的较大值。

次小生成树。

方法:求最小生成树,枚举添加哪一条新边。这样会出现一条回路。那么要删除的边在添加边两端点的路径上的 最长边。这样求出每对结点间的最大边权,求法与上一个方法类似(不同)。时间复杂度O(n2)。

二分图匹配

标准做法是Hungarian,但这个不好。用Dinic更好。

应用:

一般来说,题目不会直接给出明显的二分图,甚至连图都没有。但你只要发现要求是每两个什么什么怎么样,就可以想到用二分图匹配。

给定n*n的01矩阵,问能否通过交换整行或整列的办法,使得左上到右下的对角线上都是1。

建立二分图,一半是行编号(1~n),一半是列编号(n+1~2n)

对于每一个1,连接所在行和所在列的结点。然后求最大匹配数是否等于n即可。

网络流

Dinic

先贴个模版

#include <vector>
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn = 100050;
const int maxm = 1000050;
const int inf = 1e9+7;
struct edge{
	int from, to, cap, flow;
};
vector<edge> E;
vector<int> G[maxn];
int n, m, s, t;
int cur[maxn], d[maxn], vis[maxn];

void addedge(int from, int to, int cap) {
	E.push_back((edge){from, to, cap, 0});
	E.push_back((edge){to, from, 0, 0});
	int m = E.size();
	G[from].push_back(m-2);
	G[to].push_back(m-1);
}

int enlevel() {
	memset(vis, 0, sizeof(vis));
	queue<int> Q;
	Q.push(s);
	d[s] = 0;
	vis[s] = 1;
	while(!Q.empty()) {
		int x = Q.front(); Q.pop();
		for(int i = 0; i < G[x].size(); i++) {
			edge& e = E[G[x][i]];
			if(!vis[e.to] && e.cap > e.flow) {
				vis[e.to] = 1;
				d[e.to] = d[x] + 1;
				Q.push(e.to);
			}
		}
	}
	return vis[t];
}
int large(int x, int a) {
	if(x == t || a == 0) return a;
	int flow = 0, f;
	for(int& i = cur[x]; i < G[x].size(); i++) {
		edge& e = E[G[x][i]];
		if(d[x] + 1 == d[e.to]) if(f = large(e.to, min(a, e.cap-e.flow)) > 0) {
			e.flow += f;
			E[G[x][i^1]].flow -= f;
			flow += f;
			a -= f;
			if(a == 0) break;
		}
	}
	return flow;
}
int maxflow() {
	int flow = 0;
	while(enlevel()) {
		memset(cur, 0, sizeof(cur));
		flow += large(s, inf);
	}
	return flow;
}

int main() {
	cin >> n >> m >> s >> t;
	for(int i = 0; i < m; i++) {
		int from, to, cap;
		cin >> from >> to >> cap;
		addedge(from - 1, to - 1, cap - 1);
	}
	cout << maxflow();
	return 0;
}

相关内容很多,但现在不讲。

现在讲一种方法:

拆点法

拆点在很多算法中都非常有用。

具体地说,如果直接套摸版没有办法,或者每条边的权值是不定的,或者有各种限制条件等等,都可以尝试用拆点。

题目是不会让你改写模版的,因此模版的基础变形是最重要的。到灵活变形时,就不需要靠积累了。

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/myjs999/article/details/77994846
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞