NOIp 2018 前的图论板子总结
存图
存边
直接开一个结构体数组存边
struct Edge {
int begin, end, weight;
} edge[10010];
int edge_count;
inline void AddEdge(const int &u, const int &v, const int &w) {
edge[edge_count++] = Edge {u, v, w};
}
应用:
- Kruskal's algorithm
Adjacency matrix
用二维数组adj[i][j]
表示\(i\)与\(j\)的关系
int adj[1010][1010];
#define ADD_EDGE(u, v, w) adj[u][v] = w
应用:
- Floyd-Warshall algorithm
- Hangarian algorithm
- Kuhn-Munkres algorithm
Adjacency list
有几种形式, 以adj[i]
表示以\(i\)为开头的边
应用: 各种图论算法
vector
优点: 访问方便, 存图方便
缺点: 消耗空间, 容易\(MLE\); 删边速度慢
struct Edge {
int destination, weight;
};
std::vector<Edge> adj[1010];
加边
#define ADD_EDGE(u, v, w) adj[u].push_back(Edge {v, w})
访问
for (register int i(0); i < adj[u].size(); ++i) {
adj[u][i]...
...
}
或
for (auto i : adj[u]) {
i...
...
}
list
优点: 添边删边速度快
缺点: 不易访问; 容易\(MLE\)
struct Edge {
int destination, weight;
};
std::list<Edge> adj[1010];
加边
#define ADD_EDGE(u, v, w) adj[u].push_back(Edge {v, w})
访问
for (register std::list<Edge>::iterator i = adj[u].begin(); i != adj[u].end(); ++i) {
*i...
...
}
或
for (auto i : adj[u]) {
i...
...
}
链式前向星
优点: 空间重复利用, 通常不会\(MLE\), 而且很快
缺点: 开小了会\(WA\), 开大了会\(TLE\), 有时还会\(RE\)
应用: 各种图论算法
struct Edge {
int destination, weight, next;
} edge[10010];
int head[1010], edge_count;
加边
inline void AddEdge(const int &u, const int &v, const int &w) {
edge[edge_count] = Edge {v, w, head[u]},
head[u] = edge_count++;
}
访问
for (register int i(head[u]); i != -1; i = edge[i].next) {
edge[i]...
...
}
最小生成树
题目链接: Luogu P3366 【模板】最小生成树
Kruskal's algorithm
将边以权值从小到大排序遍历, 用并查集加边, 同时维护答案。
特点: 适合稀疏图
时间复杂度: \(\Theta(|E| \lg |V|)\)
#include <cstdio>
#include <algorithm>
struct Edge {
int begin, end, weight;
inline bool operator <(const Edge &another) const {
return this->weight < another.weight;
}
} edge[200010];
int unions[5010];
int n, m;
inline int Find(const int&);
inline int Kruskal();
int main(int argc, char const *argv[]) {
scanf("%d %d", &n, &m);
for (register int i(0); i <= n; ++i) {
unions[i] = i;
}
for (register int i(0), u, v, w; i < m; ++i) {
scanf("%d %d %d", &u, &v, &w),
edge[i] = Edge {u, v, w};
}
register int ans(Kruskal());
if (ans) printf("%d\n", ans);
else puts("orz");
return 0;
}
inline int Find(const int &x) {
return unions[x] == x ? x : (unions[x] = Find(unions[x]));
}
inline int Kruskal() {
register int ret(0);
std::sort(edge, edge + m);
for (register int i(0), countt(0), u, v; i < m; ++i) {
u = Find(edge[i].begin), v = Find(edge[i].end);
if (u != v) {
unions[v] = u,
++countt,
ret += edge[i].weight;
if (countt == n - 1) {
return ret;
}
}
}
return 0;
}
Prim's algorithm
随机选择一个结点作为树, 每次找与这棵树相连且不构成环的最小边加入进来, 形成生成树。
可以使用优先队列优化。
特点: 适合稠密图
时间复杂度: \(\Theta(|E| \lg |V|)\)
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
struct Edge {
int destination, weight;
};
std::vector<Edge> adj[5010];
int distance[5010], vis[5010];
int n, m;
inline int Prim();
int main(int argc, char const *argv[]) {
memset(distance, 0x3f, sizeof(distance));
scanf("%d %d", &n, &m);
for (register int i(0), u, v, w; i < m; ++i) {
scanf("%d %d %d", &u, &v, &w);
adj[u].push_back(Edge {v, w}),
adj[v].push_back(Edge {u, w});
}
register int ans(Prim());
if (ans) printf("%d\n", ans);
else puts("orz");
return 0;
}
inline int Prim() {
register int ret(0), countt(0);
distance[1] = 0;
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int> >, std::greater<std::pair<int, int> > > Q;
Q.push(std::pair<int, int>(0, 1));
while (!Q.empty()) {
register int w(Q.top().first), u(Q.top().second);
Q.pop();
if (!vis[u]) {
++countt,
ret += w,
vis[u] = 1;
for (auto i : adj[u]) {
if (i.weight < distance[i.destination]) {
distance[i.destination] = i.weight, Q.push(std::pair<int, int>(distance[i.destination], i.destination));
}
}
}
}
return ret * (countt == n);
}
强连通分量
题目链接: USACO 06 Jan. The Cow Prom
Tarjan's strongly connected components algorithm
是基于对图深度优先搜索(DFS)的算法, 每个强连通分量为搜索树中的一棵子树。搜索时, 把当前搜索树中未处理的节点加入一个堆栈, 回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
时间复杂度: \(\Theta(|V|+|E|)\)
#include <cstdio>
#include <vector>
#include <stack>
#include <algorithm>
std::vector<int> adj[10010];
int dfn[10010], low[10010], color_count[10010], indexx, countt;
bool instack[10010];
int n, m, ans;
std::stack<int> S;
inline int Tarjan(const int&);
int main(int argc, char const *argv[]) {
scanf("%d %d", &n, &m);
for (register int i(0), u, v;i < m; ++i) {
scanf("%d %d", &u, &v);
adj[u].push_back(v);
}
for (register int i(1); i <= n; ++i) {
if (!dfn[i]) {
Tarjan(i);
}
}
for (register int i(1); i <= countt; ++i) {
if (color_count[i] > 1) ++ans;
}
printf("%d\n", ans);
return 0;
}
inline void Tarjan(const int &cur) {
dfn[cur] = low[cur] = ++indexx;
S.push(cur), instack[cur] = 1;
for (auto i : adj[cur]) {
if (!dfn[i]) {
Tarjan(i);
low[cur] = std::min(low[cur], low[i]);
} else if (instack[i]) {
low[cur] = std::min(low[cur], low[i]);
}
}
if (dfn[cur] == low[cur]) {
++countt;
while (S.top() != cur) {
register int node(S.top());
S.pop(),
instack[node] = 0,
++color_count[countt];
}
S.pop(),
++color_count[countt],
instack[cur] = 0;
}
}
二分图最大匹配
题目链接: Luogu P3386 【模板】二分图匹配
Hungarian algorithm
通过寻找增广路来计算最大匹配值
时间复杂度:
- Adjacency matrix: \(\Theta(n^3)\)
- Adjacency list: \(\Theta(nm)\)
#include <cstdio>
#include <vector>
#include <cstring>
std::vector<int> adj[1010];
int n, m, e;
int vis[1010], mate[1010];
inline bool Match(const int&);
inline int Hungarian();
int main(int argc, char const *argv[]) {
scanf("%d %d %d", &n, &m ,&e);
for (register int i(0), u, v; i < e; ++i) {
scanf("%d %d", &u, &v);
if (u <= n && v <= m) {
adj[u].push_back(v);
}
}
printf("%d\n", Hungarian());
return 0;
}
inline int Hungarian() {
register int ret(0);
for (register int i(1); i <= n; ++i) {
memset(vis, 0, sizeof(vis));
ret += Match(i);
}
return ret;
}
inline bool Match(const int &cur) {
for (auto i : adj[cur]) {
if (!vis[i]) {
vis[i] = 1;
if (!mate[i] || Match(mate[i])) {
mate[i] = cur;
return true;
}
}
}
return false;
}
二分图最佳匹配
Kuhn-Munkres algorithm
是一种逐次修改可行顶标的方法,使之对应的等价子图逐次增广(增加边),最后出现完备匹配.
时间复杂度: \(\Theta(n^3)\)
#include <cstdio>
#include <cstring>
int nl, nr;
int adj[310][310];
int mate[310], dist_l[310], dist_r[310];
int slack[310];
bool vis_l[310], vis_r[310];
int n;
inline bool BestMatch(const int&);
inline int KuhnMunkres();
int main(int argc, char const *argv[]) {
while(~scanf("%d", &n)) {
for (register int i(0); i < n; ++i) {
for(register int j(0); j < n; ++j) {
scanf("%d", &adj[i][j]);
}
}
nl = nr = n;
printf("%d\n" ,KuhnMunkres());
}
return 0;
}
inline bool BestMatch(const int &cur) {
vis_l[cur] = true;
for (register int r(0); r < nr; ++r) {
if (!vis_r[r]) {
register int tmp(dist_l[cur] + dist_r[r] - adj[cur][r]);
if (!tmp) {
vis_r[r] = true;
if(!~mate[r] || BestMatch(mate[r])){
mate[r] = cur;
return true;
}
}
else if(slack[r] > tmp)
slack[r] = tmp;
}
}
return false;
}
inline int KuhnMunkres() {
memset(mate, -1, sizeof(mate));
memset(dist_r, 0, sizeof(dist_r));
for (register int i(0); i < nl; ++i) {
dist_l[i] = 0x80000000;
for (register int j(0); j < nr; ++j) {
if (adj[i][j] > dist_l[i]) {
dist_l[i] = adj[i][j];
}
}
}
for (register int l(0); l < nl; ++l) {
for (register int i(0); i < nr; ++i) {
slack[i] = 0x7fffffff;
}
while(true) {
memset(vis_l, false, sizeof vis_l ),
memset(vis_r, false, sizeof vis_r );
if (BestMatch(l)) break;
register int d(0x7fffffff);
for (register int i(0); i < nr; ++i){
if (!vis_r[i] && d > slack[i]) {
d = slack[i];
}
}
for (register int i(0); i < nl; ++i){
if (vis_l[i]) {
dist_l[i] -= d;
}
}
for (register int i(0); i < nr; ++i) {
if (vis_r[i]) dist_r[i] += d;
else slack[i] -= d;
}
}
}
register int res(0);
for (register int i(0); i < nr; ++i) {
if(~mate[i]) {
res += adj[mate[i]][i];
}
}
return res;
}
最近公共祖先
Heavy path decomposition
学倍增和Tarjan的时候没好好学现在只会树链剖分
题目链接: Luogu P3379 【模板】最近公共祖先(LCA)
时间复杂度:
Dfs1
: \(\Theta(n)\)Dfs2
: \(\Theta(n)\)LowestCommonDivisor
: \(\Theta(\lg n)\)
模板题要写读入优化过
#include <cstdio>
#include <vector>
#include <algorithm>
int f(-1);
inline char GetCharacter() {
static char buf[2000000], *p1 = buf, *p2 = buf;
return (p1 == p2) &&
(p2 = (p1 = buf) + fread(buf, 1, 2000000, stdin), p1 == p2) ?
EOF : *p1++;
}
#define IS_DIGIT(c) (c >= '0' && c <= '9')
inline void Read(int &x) {
f = 1, x = 0;
static char c = GetCharacter();
while (!IS_DIGIT(c)) {
if (c == '-') f = -1;
c = GetCharacter();
}
while (IS_DIGIT(c)) x = x * 10 + c - '0', c = GetCharacter();
x *= f;
}
#undef IS_DIGIT
std::vector<int> adj[500010];
int heavy[500010], size[500010], father[500010], top[500010], depth[500010];
int root, n, m;
int Dfs1(const int &cur, const int &fathernode) {
size[cur] = 1;
father[cur] = fathernode;
depth[cur] = depth[fathernode] + 1;
for (auto i : adj[cur]) {
if (i != fathernode) {
size[cur] += Dfs1(i, cur);
if (size[i] > size[heavy[cur]]) heavy[cur] = i;
}
}
return size[cur];
}
void Dfs2(const int &cur, const int &topnode) {
top[cur] = topnode;
if (heavy[cur]) {
Dfs2(heavy[cur], topnode);
for (auto i : adj[cur]) {
if (heavy[cur] != i && father[cur] != i) {
Dfs2(i, i);
}
}
}
}
inline int LowestCommonAncestor(const int &x, const int &y) {
int a(x), b(y);
while (top[a] != top[b]) {
if (depth[top[a]] < depth[top[b]]) std::swap(a, b);
a = father[top[a]];
}
return depth[a] > depth[b] ? b : a;
}
int main(int argc, char const *argv[]) {
Read(n), Read(m), Read(root);
for (int i(1), u, v; i < n; ++i) {
Read(u), Read(v);
adj[u].push_back(v), adj[v].push_back(u);
}
Dfs1(root, root);
Dfs2(root, root);
while (m--) {
int u, v;
Read(u), Read(v);
printf("%d\n", LowestCommonAncestor(u, v));
}
return 0;
}
最短路
Floyd-Warshall algorithm
枚举每一个结点作为中间点, 再枚举每一个起点和终点, 可以松弛就进行松弛。
时间复杂度: \(\Theta(n^3)\)
题目链接: HDU 2544 最短路
#include <cstdio>
#include <cstring>
#include <algorithm>
int adj[110][110];
int n, m;
int main(int argc, char **argv) {
while (~scanf("%d %d", &n, &m) && (n || m)) {
memset(adj, 0x3f, sizeof(adj));
for (register int i(1), u, v, w; i <= m; ++i) {
scanf("%d %d %d", &u, &v, &w);
adj[u][v] = adj[v][u] = std::min(adj[u][v], w);
}
for (register int i(1); i <= n; ++i) adj[i][i] = 0;
for (register int k(1); k <= n; ++k) {
for (register int i(1); i <= n; ++i) {
for (register int j(1); j <= n; ++j) {
adj[j][i] = adj[i][j] = std::min(adj[i][j], adj[i][k] + adj[k][j]);
}
}
}
printf("%d\n", adj[1][n]);
}
return 0;
}
Shortest Path Faster Algorithm(SPFA)
是Bellman-Ford algorithm的改进, 通常情况下不会出什么问题, 但精心设计的稠密图可以轻易卡掉SPFA, 使用需谨慎。
平均时间复杂度: \(\Theta(|E|)\)
理论上界: \(\Theta(|V||E|)\)
题目链接: Luogu P3371 【模板】单源最短路径(弱化版)
#include <deque>
#include <cstdio>
#include <vector>
#include <cstring>
struct Edge {
int destination, weight;
};
std::vector<Edge> adj[10010];
int n, m, s;
int distance[10010];
bool vis[10010];
inline void ShortestPathFasterAlgorithm();
int main(int argc, char const *argv[]) {
scanf("%d %d %d", &n, &m, &s);
for (register int i(0), u, v, w; i < m; ++i) {
scanf("%d %d %d", &u, &v, &w);
adj[u].push_back(Edge {v, w});
}
ShortestPathFasterAlgorithm();
for (register int i(1); i <= n; ++i) {
printf("%d ", distance[i] == 0x3f3f3f3f ? 2147483647 : distance[i]);
}
return 0;
}
inline void ShortestPathFasterAlgorithm() {
memset(distance, 0x3f, sizeof(distance));
distance[s] = 0;
std::deque<int> Q;
Q.push_back(s);
vis[s] = 0;
while (!Q.empty()) {
register int cur(Q.front());
Q.pop_front();
vis[cur] = 0;
for (auto i : adj[cur]) {
if (distance[i.destination] > distance[cur] + i.weight) {
distance[i.destination] = distance[cur] + i.weight;
if (!vis[i.destination]) {
if (distance[i.destination] < distance[Q.front()]) Q.push_front(i.destination);
else Q.push_back(i.destination);
vis[i.destination] = 1;
}
}
}
}
}
Dijkstra's algorithm
与最小生成树的Prim's algorithm相像, 主要思想为贪心, 适用于稠密图。
通常情况下使用SPFA, 如果数据卡SPFA可以尝试堆优化的Dijkstra's algorithm。
时间复杂度: \(\Theta((|E|)\lg |E|)\)
题目链接: Luogu P4779 【模板】单源最短路径(标准版)
#include <cstdio>
#include <vector>
#include <queue>
#include <cstring>
std::vector<std::pair<int, long long> > adj[100010];
long long distance[100010];
int vis[100010];
int n, m, s;
inline void Dijkstra() {
register std::priority_queue<std::pair<long long, int>, std::vector<std::pair<long long, int> >, std::greater<std::pair<long long, int> > > Q;
memset(distance, 0x3f, sizeof(distance));
distance[s] = 0;
Q.push(std::pair<long long, int>(0ll, s));
while (!Q.empty()) {
register int cur(Q.top().second);
Q.pop();
if (!vis[cur]) {
vis[cur] = 1;
for (auto i : adj[cur]) {
if (distance[i.first] > distance[cur] + i.second) {
distance[i.first] = distance[cur] + i.second;
Q.push(std::pair<long long, int>(distance[i.first], i.first));
}
}
}
}
}
int main(int argc, char const *argv[]) {
scanf("%d %d %d", &n, &m, &s);
for (register int i(1), u, v, w; i <= m; ++i) {
scanf("%d %d %d", &u, &v, &w);
adj[u].push_back(std::pair<int, long long>(v, (long long)(w)));
}
Dijkstra();
for (register int i(1); i <= n; ++i) {
printf(i == n ? "%lld\n" : "%lld ", distance[i]);
}
return 0;
}
最大流
Edmonds-Karp algorithm
最大流的基础方法, 思想非常简单:
每次BFS找到一条增广路进行增广, 记录该路径上可增广的最大流, 在图中减去即可。、
时间复杂度: \(\Theta(|V||E|^2)\)
题目链接: USACO4.2 Drainage Ditches
#include <cstdio>
#include <queue>
#include <vector>
#include <algorithm>
#include <cstring>
#define INF 2147483647
int predeccessor[210], flow[210];
int G[210][210];
int n, m;
inline int Bfs(const int &S, const int &T) {
register std::queue<int> q;
memset(predeccessor, -1, sizeof(predeccessor));
predeccessor[S] = 0;
flow[S] = INF;
q.push(S);
while (!q.empty()) {
register int current(q.front());
q.pop();
if (current == T) {
break;
}
for (register int i(1); i <= n; ++i) {
if (G[current][i] && predeccessor[i] == -1) {
predeccessor[i] = current;
flow[i] = std::min(flow[current], G[current][i]);
q.push(i);
}
}
}
return predeccessor[T] == -1 ? -1 : flow[T];
}
inline int EdmondsKarp(const int &S, const int &T) {
register int ret(0), increase(0);
while((increase = Bfs(S, T)) != -1) {
register int current(T);
while (current != S) {
G[predeccessor[current]][current] -= increase;
G[current][predeccessor[current]] += increase;
current = predeccessor[current];
}
ret += increase;
}
return ret;
}
int main(int argc, char **argv) {
scanf("%d %d", &m, &n);
for (register int i(0), u, v, w; i < m; ++i) {
scanf("%d %d %d", &u, &v, &w);
G[u][v] += w;
}
printf("%d\n", EdmondsKarp(1, n));
return 0;
}
Dinic's algorithm
首先利用BFS对网络进行分层, 然后利用DFS从前一层向后一层反复寻找增广路(利用回溯), 当这次DFS无法继续增广时, 重复BFS步骤, 直到BFS无法到达汇点时, 算法结束。
时间复杂度: \(\Theta(|V|^2 |E|)\)
题目链接: Luogu P3376 【模板】网络最大流
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
int n, m, s, t;
int depth[10010];
struct Edge {
int destination, maxflow, next;
Edge() {
destination = maxflow = next = 0;
}
} edge[200010];
int head[10010];
inline void AddEdge(const int &u, const int &v, const int &w) {
static int edge_count(0);
edge[edge_count].destination = v;
edge[edge_count].maxflow = w;
edge[edge_count].next = head[u];
head[u] = edge_count++;
}
inline bool BreadthFirstSearch(const int &S, const int &T) {
register std::queue<int> q;
memset(depth, 0, sizeof(depth));
depth[S] = 1;
q.push(S);
while (!q.empty()) {
register int current(q.front());
q.pop();
for (register int i(head[current]); i != -1; i = edge[i].next) {
if (edge[i].maxflow && !depth[edge[i].destination]) {
depth[edge[i].destination] = depth[current] + 1;
q.push(edge[i].destination);
}
}
}
return depth[T];
}
inline int DepthFirstSearch(const int ¤t,
register int maxflow,
const int &T) {
if (current == T) return maxflow;
for (register int i(head[current]); i != -1; i = edge[i].next) {
if (depth[edge[i].destination] == depth[current] + 1 && edge[i].maxflow) {
register int delta(DepthFirstSearch(edge[i].destination,
std::min(maxflow, edge[i].maxflow),
T));
if (delta) {
edge[i].maxflow -= delta;
edge[i ^ 1].maxflow += delta;
return delta;
}
}
}
return 0;
}
inline int Dinic(const int &S, const int &T) {
register int ret(0), delta;
while (BreadthFirstSearch(S, T)) {
while ((delta = DepthFirstSearch(S, 0x3f3f3f3f, T))) {
ret += delta;
}
}
return ret;
}
int main(int argc, char **argv) {
memset(head, -1, sizeof(head));
scanf("%d %d %d %d", &n, &m, &s, &t);
for (register int i(1), u, v, w; i <= m; ++i) {
scanf("%d %d %d", &u, &v, &w);
AddEdge(u, v, w), AddEdge(v, u, 0);
}
printf("%d\n", Dinic(s, t));
return 0;
}
最小费用最大流
Dinic's algorithm
将费用看作路径长度, 把Dinic's algorithm中的BFS换成SPFA, 每次找费用最小的进行增广
时间复杂度: \(\Theta(|V|^2 |E|)\)
题目链接: Luogu P3381 【模板】最小费用最大流
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
int n, m, s, t;
struct Edge {
int destination, maxflow, cost, next;
Edge() {
destination = maxflow = cost = next = 0;
}
} edge[100010];
int head[5010];
int distance[5010], pre_node[5010], pre_edge[5010], flow[5010], vis[5010];
int maxflow, mincost;
inline void AddEdge(const int &u, const int &v, const int &w, const int &c) {
static int edge_count(0);
edge[edge_count].destination = v;
edge[edge_count].maxflow = w;
edge[edge_count].cost = c;
edge[edge_count].next = head[u];
head[u] = edge_count++;
}
inline bool ShortestPathFasterAlgorithm(const int &S, const int &T) {
memset(distance, 0x3f, sizeof(distance));
memset(flow, 0x3f, sizeof(flow));
memset(vis, 0, sizeof(vis));
register std::queue<int> q;
q.push(S);
vis[S] = 1, distance[S] = 0, pre_node[T] = -1;
while (!q.empty()) {
register int current(q.front());
q.pop();
vis[current] = 0;
for (register int i(head[current]); i != -1; i = edge[i].next) {
if (edge[i].maxflow &&
distance[edge[i].destination] > distance[current] + edge[i].cost) {
distance[edge[i].destination] = distance[current] + edge[i].cost;
pre_node[edge[i].destination] = current;
pre_edge[edge[i].destination] = i;
flow[edge[i].destination] = std::min(flow[current], edge[i].maxflow);
if (!vis[edge[i].destination]) {
vis[edge[i].destination] = 1;
q.push(edge[i].destination);
}
}
}
}
return pre_node[T] != -1;
}
inline void Dinic(const int &S, const int &T) {
while (ShortestPathFasterAlgorithm(S, T)) {
register int current(T);
maxflow += flow[T];
mincost += flow[T] * distance[T];
while (current != S) {
edge[pre_edge[current]].maxflow -= flow[T];
edge[pre_edge[current] ^ 1].maxflow += flow[T];
current = pre_node[current];
}
}
}
int main(int argc, char **argv) {
memset(head, -1, sizeof(head));
scanf("%d %d %d %d", &n, &m, &s, &t);
for (register int i(0), u, v, w, c; i < m; ++i) {
scanf("%d %d %d %d", &u, &v, &w, &c);
AddEdge(u, v, w, c), AddEdge(v, u, 0, -c);
}
Dinic(s, t);
printf("%d %d\n", maxflow, mincost);
return 0;
}