这周学些图论。
图论大概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;
}
相关内容很多,但现在不讲。
现在讲一种方法:
拆点法
拆点在很多算法中都非常有用。
具体地说,如果直接套摸版没有办法,或者每条边的权值是不定的,或者有各种限制条件等等,都可以尝试用拆点。
题目是不会让你改写模版的,因此模版的基础变形是最重要的。到灵活变形时,就不需要靠积累了。