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

来源:互联网 发布:软件汉化 编辑:程序博客网 时间:2024/06/06 03:06

这周学些图论。
图论大概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;}

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



现在讲一种方法:

拆点法

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

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

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

阅读全文
1 0
原创粉丝点击