【XCPC模板整理 - 第二期】图论+网络流+树形结构
前言
这是我个人使用的一些模板封装。
限于个人能力,可能存在诸多不足与漏洞,在未加测试直接使用前请务必小心谨慎。更新可能会滞后于我本地的文档,如有疑问或者催更之类的可以在评论区留言。
全文模板测试均基于以下版本信息,请留意版本兼容问题。
Windows, 64bit
G++ (ISO C++20)
stack=268435456
开启O2优化
同时,可能还使用到了如下宏定义:
#define int long long
#define endl "\n"
using LL = long long;
using ld = long double;
using PII = pair<int, int>;
using TII = tuple<int, int, int>;
单源最短路径(SSSP问题)
(正权稀疏图)动态数组存图+Djikstra算法
使用优先队列优化,以 \(\mathcal O(M\log N)\) 的复杂度计算。
vector<int> dis(n + 1, 1E18);
auto djikstra = [&](int s = 1) -> void {
using PII = pair<int, int>;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.emplace(0, s);
dis[s] = 0;
vector<int> vis(n + 1);
while (!q.empty()) {
int x = q.top().second;
q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (auto [y, w] : ver[x]) {
if (dis[y] > dis[x] + w) {
dis[y] = dis[x] + w;
q.emplace(dis[y], y);
}
}
}
};
(负权图)Bellman ford 算法
使用结构体存边(该算法无需存图),以 \(\mathcal{O} (NM)\) 的复杂度计算,注意,当所求点的路径上存在负环时,所求点的答案无法得到,但是会比 INF 小(因为负环之后到所求点之间的边权会将 d[end]
的值更新),该性质可以用于判断路径上是否存在负环:在 \(N-1\) 轮后仍无法得到答案(一般与 \({\tt INF} / 2\) 进行比较)的点,到达其的路径上存在负环。
下方代码例题:求解从 \(1\) 到 \(n\) 号节点的、最多经过 \(k\) 条边的最短距离。
const int N = 550, M = 1e5 + 7;
int n, m, k;
struct node { int x, y, w; } ver[M];
int d[N], backup[N];
void bf() {
memset(d, 0x3f, sizeof d); d[1] = 0;
for (int i = 1; i <= k; ++ i) {
memcpy(backup, d, sizeof d);
for (int j = 1; j <= m; ++ j) {
int x = ver[j].x, y = ver[j].y, w = ver[j].w;
d[y] = min(d[y], backup[x] + w);
}
}
}
int main() {
cin >> n >> m >> k;
for (int i = 1; i <= m; ++ i) {
int x, y, w; cin >> x >> y >> w;
ver[i] = {x, y, w};
}
bf();
for (int i = 1; i <= n; ++ i) {
if (d[i] > INF / 2) cout << "N" << endl;
else cout << d[n] << endl;
}
}
(负权图)SPFA 算法
以 \(\mathcal{O}(KM)\) 的复杂度计算,其中 \(K\) 虽然为常数,但是可以通过特殊的构造退化成接近 \(N\) ,需要注意被卡。
const int N = 1e5 + 7, M = 1e6 + 7;
int n, m;
int ver[M], ne[M], h[N], edge[M], tot;
int d[N], v[N];
void add(int x, int y, int w) {
ver[++ tot] = y, ne[tot] = h[x], h[x] = tot;
edge[tot] = w;
}
void spfa() {
ms(d, 0x3f); d[1] = 0;
queue<int> q; q.push(1);
v[1] = 1;
while(!q.empty()) {
int x = q.front(); q.pop(); v[x] = 0;
for (int i = h[x]; i; i = ne[i]) {
int y = ver[i];
if(d[y] > d[x] + edge[i]) {
d[y] = d[x] + edge[i];
if(v[y] == 0) q.push(y), v[y] = 1;
}
}
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int x, y, w; cin >> x >> y >> w;
add(x, y, w);
}
spfa();
for (int i = 1; i <= n; ++ i) {
if (d[i] == INF) cout << "N" << endl;
else cout << d[n] << endl;
}
}
(正权稠密图)邻接矩阵存图+Djikstra算法
很少使用,以 \(\mathcal{O} (N^2)\) 的复杂度计算。
const int N = 3010;
int n, m, a[N][N];
int d[N], v[N];
void dji() {
ms(d, 0x3f); d[1] = 0;
for (int i = 1; i <= n; ++ i) {
int x = 0;
for (int j = 1; j <= n; ++ j) {
if(v[j]) continue;
if(x == 0 || d[x] > d[j]) x = j;
}
v[x] = 1;
for (int j = 1; j <= n; ++ j) d[j] = min(d[j], d[x] + a[x][j]);
}
}
int main() {
cin >> n >> m;
ms(a, 0x3f);
for (int i = 1; i <= m; ++ i) {
int x, y, w; cin >> x >> y >> w;
a[x][y] = min(a[x][y], w); //注意需要考虑重边问题
a[y][x] = min(a[y][x], w); //无向图建双向边
}
dji();
for (int i = 1; i <= n; ++ i) {
if (d[i] == INF) cout << "N" << endl;
else cout << d[n] << endl;
}
}
多源汇最短路(APSP问题)
(稠密图)邻接矩阵+Floyd算法
使用邻接矩阵存图,可以处理负权边,以 \(\mathcal{O}(N^3)\) 的复杂度计算。注意,这里建立的是单向边,计算双向边需要额外加边。
const int N = 210;
int n, m, d[N][N];
void floyd() {
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
while (m -- ) {
int x, y, w; cin >> x >> y >> w;
d[x][y] = min(d[x][y], w);
}
floyd();
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
if (d[i][j] > INF / 2) cout << "N" << endl;
else cout << d[i][j] << endl;
}
}
}
最小生成树(MST问题)
(稀疏图)Prim算法
使用邻接矩阵存图,以 \(\mathcal{O}(N^2+M)\) 的复杂度计算,思想与 \(\tt djikstra\) 基本一致。
const int N = 550, INF = 0x3f3f3f3f;
int n, m, g[N][N];
int d[N], v[N];
int prim() {
ms(d, 0x3f); //这里的d表示到“最小生成树集合”的距离
int ans = 0;
for (int i = 0; i < n; ++ i) { //遍历 n 轮
int t = -1;
for (int j = 1; j <= n; ++ j)
if (v[j] == 0 && (t == -1 || d[j] < d[t])) //如果这个点不在集合内且当前距离集合最近
t = j;
v[t] = 1; //将t加入“最小生成树集合”
if (i && d[t] == INF) return INF; //如果发现不连通,直接返回
if (i) ans += d[t];
for (int j = 1; j <= n; ++ j) d[j] = min(d[j], g[t][j]); //用t更新其他点到集合的距离
}
return ans;
}
int main() {
ms(g, 0x3f); cin >> n >> m;
while (m -- ) {
int x, y, w; cin >> x >> y >> w;
g[x][y] = g[y][x] = min(g[x][y], w);
}
int t = prim();
if (t == INF) cout << "impossible" << endl;
else cout << t << endl;
} //22.03.19已测试
(稠密图)Kruskal算法
平均时间复杂度为 \(\mathcal{O}(M\log M)\) ,简化了并查集。
struct DSU {
vector<int> fa;
DSU(int n) : fa(n + 1) {
iota(fa.begin(), fa.end(), 0);
}
int get(int x) {
while (x != fa[x]) {
x = fa[x] = fa[fa[x]];
}
return x;
}
bool merge(int x, int y) { // 设x是y的祖先
x = get(x), y = get(y);
if (x == y) return false;
fa[y] = x;
return true;
}
bool same(int x, int y) {
return get(x) == get(y);
}
};
struct Tree {
using TII = tuple<int, int, int>;
int n;
priority_queue<TII, vector<TII>, greater<TII>> ver;
Tree(int n) {
this->n = n;
}
void add(int x, int y, int w) {
ver.emplace(w, x, y); // 注意顺序
}
int kruskal() {
DSU dsu(n);
int ans = 0, cnt = 0;
while (ver.size()) {
auto [w, x, y] = ver.top();
ver.pop();
if (dsu.same(x, y)) continue;
dsu.merge(x, y);
ans += w;
cnt++;
}
assert(cnt < n - 1); // 输入有误,建树失败
return ans;
}
};
缩点 (Tarjan算法)
(有向图)强连通分量缩点
强连通分量缩点后的图称为 SCC。以 \(\mathcal O (N + M)\) 的复杂度完成上述全部操作。
性质:缩点后的图拥有拓扑序 \(color_{cnt}, color_{cnt-1},…,1\) ,可以不需再另跑一遍 \(\tt topsort\) ;缩点后的图是一张有向无环图( \(\tt DAG\) 、拓扑图)。
struct SCC {
int n;
vector<vector<int>> ver;
vector<int> dfn, low, col, S;
int now, cnt;
SCC(int n) : n(n) {
ver.assign(n + 1, {});
dfn.resize(n + 1, -1);
low.resize(n + 1);
col.assign(n + 1, -1);
S.clear();
now = cnt = 0;
}
void add(int x, int y) {
ver[x].push_back(y);
}
void tarjan(int x) {
dfn[x] = low[x] = now++;
S.push_back(x);
for (auto y : ver[x]) {
if (dfn[y] == -1) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if (col[y] == -1) {
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x]) {
int pre;
cnt++;
do {
pre = S.back();
col[pre] = cnt;
S.pop_back();
} while (pre != x);
}
}
pair<int, vector<vector<int>>> rebuild() { // [新图的顶点数量, 新图]
work();
vector<vector<int>> adj(cnt + 1);
for (int i = 1; i <= n; i++) {
for (auto j : ver[i]) {
int x = col[i], y = col[j];
if (x != y) {
adj[x].push_back(y);
}
}
}
return {cnt, adj};
}
void work() {
for (int i = 1; i <= n; i++) { // 避免图不连通
if (dfn[i] == -1) {
tarjan(i);
}
}
}
};
(无向图)割边缩点
割边缩点后的图称为边双连通图 (E-DCC),该模板可以在 \(\mathcal O (N + M)\) 复杂度内求解图中全部割边、划分边双(颜色相同的点位于同一个边双连通分量中)。由于割边特殊性,注意这里使用的是链式前向星。
性质补充:对于一个边双,删去任意边后依旧联通;对于边双中的任意两点,一定存在两条不相交的路径连接这两个点(路径上可以有公共点,但是没有公共边)。
struct E_DCC {
int n;
vector<int> h, ver, ne;
vector<int> dfn, low, col, S;
int now, cnt, tot;
vector<bool> bridge; // 记录是否是割边
E_DCC(int n, int m) : n(n) {
m *= 2; // 注意链式前向星边的数量翻倍
ver.resize(m + 1);
ne.resize(m + 1);
bridge.resize(m + 1);
h.resize(n + 1, -1);
dfn.resize(n + 1);
low.resize(n + 1);
col.resize(n + 1);
S.clear();
tot = cnt = now = 0;
}
void add(int x, int y) { // 注意,这里的编号从 0 开始
ver[tot] = y, ne[tot] = h[x], h[x] = tot++;
ver[tot] = x, ne[tot] = h[y], h[y] = tot++;
}
void tarjan(int x, int fa) { // 这里是缩边双,不是缩点,不相同
dfn[x] = low[x] = ++now;
S.push_back(x);
for (int i = h[x]; ~i; i = ne[i]) {
int y = ver[i];
if (!dfn[y]) {
tarjan(y, i); // 这里储存的是父亲边的编号
low[x] = min(low[x], low[y]);
// y 不能到达 x 的任何一个祖先节点,(x - y) 即为一条割边
// 但是在这里,我们不直接储存 (x - y) 这条边,而是储存边的编号
// 这样做是为了处理重边的情况(点可能相同,但是边的编号绝对不相同)
if (dfn[x] < low[y]) {
bridge[i] = bridge[i ^ 1] = true;
}
} else if (i != (fa ^ 1)) { // 这里同样的,使用边的编号来处理重边情况
low[x] = min(low[x], dfn[y]);
}
}
if (dfn[x] == low[x]) {
int pre = 0;
cnt++;
do {
pre = S.back();
S.pop_back();
col[pre] = cnt;
} while (pre != x);
}
}
pair<int, vector<vector<int>>> rebuild() { // [新图的顶点数量, 新图]
work();
vector<vector<int>> adj(cnt + 1);
for (int i = 0; i < tot; ++i) {
if (bridge[i]) { // 如果 (i, i ^ 1) 是割边
int x = col[ver[i]], y = col[ver[i ^ 1]];
adj[x].push_back(y); // 割边两端点颜色必定不同,故直接连边
}
}
return {cnt, adj};
}
void work() {
for (int i = 1; i <= n; i++) { // 避免图不连通
if (dfn[i] == 0) {
tarjan(i, -1);
}
}
}
};
(无向图)割点缩点
割点缩点后的图称为点双连通图 (V-DCC),该模板可以在 \(\mathcal O (N + M)\) 复杂度内求解图中全部割点、划分点双(颜色相同的点位于同一个点双连通分量中)。
性质补充:每一个割点至少属于两个点双。
struct V_DCC {
int n;
vector<vector<int>> ver, col;
vector<int> dfn, low, S;
int now, cnt;
vector<bool> point; // 记录是否为割点
V_DCC(int n) : n(n) {
ver.resize(n + 1);
dfn.resize(n + 1);
low.resize(n + 1);
col.resize(2 * n + 1);
point.resize(n + 1);
S.clear();
cnt = now = 0;
}
void add(int x, int y) {
if (x == y) return; // 手动去除重边
ver[x].push_back(y);
ver[y].push_back(x);
}
void tarjan(int x, int root) {
low[x] = dfn[x] = now++;
S.push_back(x);
if (x == root && !ver[x].size()) { // 特判孤立点
++cnt;
col[cnt].push_back(x);
return;
}
int flag = 0;
for (auto y : ver[x]) {
if (!dfn[y]) {
tarjan(y, root);
low[x] = min(low[x], low[y]);
if (dfn[x] <= low[y]) {
flag++;
if (x != root || flag > 1) {
point[x] = true; // 标记为割点
}
int pre = 0;
cnt++;
do {
pre = S.back();
col[cnt].push_back(pre);
S.pop_back();
} while (pre != y);
col[cnt].push_back(x);
}
} else {
low[x] = min(low[x], dfn[y]);
}
}
}
pair<int, vector<vector<int>>> rebuild() { // [新图的顶点数量, 新图]
work();
vector<vector<int>> adj(cnt + 1);
for (int i = 1; i <= cnt; i++) {
if (!col[i].size()) { // 注意,孤立点也是 V-DCC
continue;
}
for (auto j : col[i]) {
if (point[j]) { // 如果 j 是割点
adj[i].push_back(point[j]);
adj[point[j]].push_back(i);
}
}
}
return {cnt, adj};
}
void work() {
for (int i = 1; i <= n; ++i) { // 避免图不连通
if (!dfn[i]) {
tarjan(i, i);
}
}
}
};
染色法判定二分图 (dfs算法)
判断一张图能否被二分染色。
vector<int> vis(n + 1);
auto dfs = [&](auto self, int x, int type) -> void {
vis[x] = type;
for (auto y : ver[x]) {
if (vis[y] == type) {
cout << "NO\n";
exit(0);
}
if (vis[y]) continue;
self(self, y, 3 - type);
}
};
for (int i = 1; i <= n; ++i) {
if (vis[i]) {
dfs(dfs, i, 1);
}
}
cout << "Yes\n";
链式前向星建图与搜索
很少使用这种建图法。\(\tt dfs\) :标准复杂度为 \(\mathcal O(N+M)\)。节点子节点的数量包含它自己(至少为 \(1\)),深度从 \(0\) 开始(根节点深度为 \(0\))。\(\tt bfs\) :深度从 \(1\) 开始(根节点深度为 \(1\))。\(\tt topsort\) :有向无环图(包括非联通)才拥有完整的拓扑序列(故该算法也可用于判断图中是否存在环)。每次找到入度为 \(0\) 的点并将其放入待查找队列。
namespace Graph {
const int N = 1e5 + 7;
const int M = 1e6 + 7;
int tot, h[N], ver[M], ne[M];
int deg[N], vis[M];
void clear(int n) {
tot = 0; //多组样例清空
for (int i = 1; i <= n; ++i) {
h[i] = 0;
deg[i] = vis[i] = 0;
}
}
void add(int x, int y) {
ver[++tot] = y, ne[tot] = h[x], h[x] = tot;
++deg[y];
}
void dfs(int x) {
a.push_back(x); // DFS序
siz[x] = vis[x] = 1;
for (int i = h[x]; i; i = ne[i]) {
int y = ver[i];
if (vis[y]) continue;
dis[y] = dis[x] + 1;
dfs(y);
siz[x] += siz[y];
}
a.push_back(x);
}
void bfs(int s) {
queue<int> q;
q.push(s);
dis[s] = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = h[x]; i; i = ne[i]) {
int y = ver[i];
if (dis[y]) continue;
d[y] = d[x] + 1;
q.push(y);
}
}
}
bool topsort() {
queue<int> q;
vector<int> ans;
for (int i = 1; i <= n; ++i)
if (deg[i] == 0) q.push(i);
while (!q.empty()) {
int x = q.front();
q.pop();
ans.push_back(x);
for (int i = h[x]; i; i = ne[i]) {
int y = ver[i];
--deg[y];
if (deg[y] == 0) q.push(y);
}
}
return ans.size() == n; //判断是否存在拓扑排序
}
} // namespace Graph
一般图最大匹配(带花树算法)
与二分图匹配的差别在于图中可能存在奇环,时间复杂度与边的数量无关,为 \(\mathcal O(N^3)\) 。下方模板编号从 \(0\) 开始,例题为 UOJ #79. 一般图最大匹配 。
struct DSU {
vector<int> fa;
DSU() {}
void init(int n) {
fa.resize(n + 1);
iota(fa.begin(), fa.end(), 0);
}
int get(int x) {
while (x != fa[x]) {
x = fa[x] = fa[fa[x]];
}
return x;
}
bool merge(int x, int y) { // 设x是y的祖先
x = get(x), y = get(y);
if (x == y) return false;
fa[y] = x;
return true;
}
bool same(int x, int y) {
return get(x) == get(y);
}
};
struct MaxMatch {
int n, cnt;
vector<vector<int>> ver;
vector<int> pre, mark, match;
DSU dsu;
MaxMatch(int n) : n(n) {
cnt = 0;
ver.resize(n);
match.resize(n, -1);
pre.resize(n, -1);
mark.resize(n, -1);
}
void add(int x, int y) {
ver[x].push_back(y);
ver[y].push_back(x);
}
int lca(int x, int y) {
++cnt;
while (1) {
if (x != -1) {
x = dsu.get(x);
if (mark[x] == cnt) break;
mark[x] = cnt;
x = match[x] != -1 ? pre[match[x]] : -1;
}
swap(x, y);
}
return x;
}
bool get_match(int s) {
dsu.init(n);
vector<int> q;
q.push_back(s);
vector<int> type(n, -1);
type[s] = 0;
for (int i = 0; i < (int)q.size(); ++i) { // 注意这里不能用 auto
int x = q[i];
for (auto y : ver[x]) {
if (type[y] == -1) {
pre[y] = x;
type[y] = 1;
int z = match[y];
if (z == -1) {
for (int u = y; u != -1;) {
int v = match[pre[u]];
match[u] = pre[u];
match[pre[u]] = u;
u = v;
}
return true;
}
q.push_back(z);
type[z] = 0;
} else if (type[y] == 0 && !dsu.same(x, y)) {
int z = lca(x, y);
auto blossom = [&](int x, int y, int z) -> void {
while (!dsu.same(x, z)) {
pre[x] = y;
if (type[match[x]] == 1) {
type[match[x]] = 0;
q.push_back(match[x]);
}
if (dsu.get(x) == x) {
dsu.merge(z, x); // z为祖先,注意顺序
}
if (dsu.get(match[x]) == match[x]) {
dsu.merge(z, match[x]); // z为祖先,注意顺序
}
y = match[x];
x = pre[y];
}
};
blossom(x, y, z);
blossom(y, x, z);
}
}
}
return false;
};
pair<int, vector<int>> work() { // {最大匹配数量, i号点的另一个匹配点 (0代表无匹配)}
int matching = 0;
for (int x = 0; x < n; ++x) {
if (match[x] == -1 && get_match(x)) {
matching++;
}
}
return {matching, match};
}
};
signed main() {
int n, m;
cin >> n >> m;
MaxMatch match(n);
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y;
match.add(x - 1, y - 1);
}
auto [ans, match] = match.work();
cout << ans << endl;
for (auto it : match) {
cout << it + 1 << " ";
}
}
二分图最大匹配
定义:找到边的数量最多的那个匹配。
一般我们规定,左半部包含 \(n_1\) 个点(编号 \(1 - n_1\)),右半部包含 \(n_2\) 个点(编号 \(1-n_2\) ),保证任意一条边的两个端点都不可能在同一部分中。
匈牙利算法(KM算法)解
\(\mathcal O (NM)\) 。
signed main() {
int n1, n2, m;
cin >> n1 >> n2 >> m;
vector<vector<int>> ver(n1 + 1);
for (int i = 1; i <= m; ++i) {
int x, y;
cin >> x >> y;
ver[x].push_back(y); //只需要建立单向边
}
int ans = 0;
vector<int> match(n2 + 1);
for (int i = 1; i <= n1; ++i) {
vector<int> vis(n2 + 1);
auto dfs = [&](auto self, int x) -> bool {
for (auto y : ver[x]) {
if (vis[y]) continue;
vis[y] = 1;
if (!match[y] || self(self, match[y])) {
match[y] = x;
return true;
}
}
return false;
};
if (dfs(dfs, i)) {
ans++;
}
}
cout << ans << endl;
}
HopcroftKarp算法(HK算法、基于最大流模型)解
该算法基于网络流中的最大流模型,但是会比直接使用 \(\tt dinic\) 算法更快,因为常数更小,最坏时间复杂度为 \(\mathcal O(\sqrt NM)\) ,但实际运行复杂度还要比这一数字小上 \(10\) 倍。
struct HopcroftKarp {
vector<vector<int>> g;
vector<int> pa, pb, vis;
int n, m, dfn, res;
HopcroftKarp(int _n, int _m) : n(_n + 1), m(_m + 1) {
assert(0 <= n && 0 <= m);
pa.assign(n, -1);
pb.assign(m, -1);
vis.resize(n);
g.resize(n);
res = 0;
dfn = 0;
}
void add(int x, int y) {
assert(0 <= x && x < n && 0 <= y && y < m);
g[x].push_back(y);
}
bool dfs(int v) {
vis[v] = dfn;
for (int u : g[v]) {
if (pb[u] == -1) {
pb[u] = v;
pa[v] = u;
return true;
}
}
for (int u : g[v]) {
if (vis[pb[u]] != dfn && dfs(pb[u])) {
pa[v] = u;
pb[u] = v;
return true;
}
}
return false;
}
int solve() {
while (1) {
dfn++;
int cnt = 0;
for (int i = 0; i < n; i++) {
if (pa[i] == -1 && dfs(i)) {
cnt++;
}
}
if (cnt == 0) break;
res += cnt;
}
return res;
}
};
signed main() {
int n1, n2, m;
cin >> n1 >> n2 >> m;
HopcroftKarp flow(n1, n2);
while (m--) {
int x, y;
cin >> x >> y;
flow.add(x, y);
}
cout << flow.solve() << endl;
}
二分图最大权匹配(二分图完美匹配)
定义:找到边权和最大的那个匹配。
一般我们规定,左半部包含 \(n_1\) 个点(编号 \(1 - n_1\)),右半部包含 \(n_2\) 个点(编号 \(1-n_2\) )。
使用匈牙利算法(KM算法)解,时间复杂度为 \(\mathcal O(N^3)\) 。下方模板用于求解最大权值、且可以输出其中一种可行方案,例题为 UOJ #80. 二分图最大权匹配 。
struct MaxCostMatch {
vector<int> ansl, ansr, pre;
vector<int> lx, ly;
vector<vector<int>> ver;
int n;
MaxCostMatch(int n) : n(n) {
ver.resize(n + 1, vector<int>(n + 1));
ansl.resize(n + 1, -1);
ansr.resize(n + 1, -1);
lx.resize(n + 1);
ly.resize(n + 1, -1E18);
pre.resize(n + 1);
}
void add(int x, int y, int w) {
ver[x][y] = w;
}
void bfs(int x) {
vector<bool> visl(n + 1), visr(n + 1);
vector<int> slack(n + 1, 1E18);
queue<int> q;
function<bool(int)> check = [&](int x) {
visr[x] = 1;
if (~ansr[x]) {
q.push(ansr[x]);
visl[ansr[x]] = 1;
return false;
}
while (~x) {
ansr[x] = pre[x];
swap(x, ansl[pre[x]]);
}
return true;
};
q.push(x);
visl[x] = 1;
while (1) {
while (!q.empty()) {
int x = q.front();
q.pop();
for (int y = 1; y <= n; ++y) {
if (visr[y]) continue;
int del = lx[x] + ly[y] - ver[x][y];
if (del < slack[y]) {
pre[y] = x;
slack[y] = del;
if (!slack[y] && check(y)) return;
}
}
}
int val = 1E18;
for (int i = 1; i <= n; ++i) {
if (!visr[i]) {
val = min(val, slack[i]);
}
}
for (int i = 1; i <= n; ++i) {
if (visl[i]) lx[i] -= val;
if (visr[i]) {
ly[i] += val;
} else {
slack[i] -= val;
}
}
for (int i = 1; i <= n; ++i) {
if (!visr[i] && !slack[i] && check(i)) {
return;
}
}
}
}
int work() {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
ly[i] = max(ly[i], ver[j][i]);
}
}
for (int i = 1; i <= n; ++i) bfs(i);
int res = 0;
for (int i = 1; i <= n; ++i) {
res += ver[i][ansl[i]];
}
return res;
}
void getMatch(int x, int y) { // 获取方案 (0代表无匹配)
for (int i = 1; i <= x; ++i) {
cout << (ver[i][ansl[i]] ? ansl[i] : 0) << " ";
}
cout << endl;
for (int i = 1; i <= y; ++i) {
cout << (ver[i][ansr[i]] ? ansr[i] : 0) << " ";
}
cout << endl;
}
};
signed main() {
int n1, n2, m;
cin >> n1 >> n2 >> m;
MaxCostMatch match(max(n1, n2));
for (int i = 1; i <= m; i++) {
int x, y, w;
cin >> x >> y >> w;
match.add(x, y, w);
}
cout << match.work() << '\n';
match.getMatch(n1, n2);
}
最长路(topsort+DP算法)
计算一张 \(\tt DAG\) 中的最长路径,在执行前可能需要使用 \(\tt tarjan\) 重构一张正确的 \(\tt DAG\) ,复杂度 \(\mathcal O(N+M)\) 。
namespace LP { // Longest_Path,最长路封装(Topsort)
vector<PII> ver[N];
int deg[N];
int d[N];
void clear(int n) {
FOR(i, 1, n) {
ver[i].clear();
deg[i] = 0;
}
}
void add(int x, int y, int w) {
ver[x].pb({y, w});
++deg[y];
}
void topsort(int n, int s) {
queue<int> q;
FOR(i, 1, n) {
if (deg[i] == 0) q.push(i);
}
fill(d + 1, d + 1 + n, -INFF);
d[s] = 0;
while (!q.empty()) {
int x = q.front();
q.pop();
for (auto [y, w] : ver[x]) {
d[y] = max(d[y], d[x] + w);
--deg[y];
if (deg[y] == 0) q.push(y);
}
}
}
void solve(int n, int s) {
topsort(n, s);
}
} // namespace LP
int main() {
int n, m;
cin >> n >> m;
FOR(i, 1, n) {
int x, y, w;
cin >> x >> y >> w;
LP::add(x, y, w);
}
int start, end;
cin >> start >> end; //输入源汇
LP::solve(n, start);
cout << LP::d[end] << endl;
LP::clear(n); //清空
}
最短路径树(SPT问题)
定义:在一张无向带权联通图中,有这样一棵生成树:满足从根节点到任意点的路径都为原图中根到任意点的最短路径。
性质:记根节点 \(Root\) 到某一结点 \(x\) 的最短距离 \(dis_{Root,x}\) ,在 \(SPT\) 上这两点之间的距离为 \(len_{Root,x}\) ——则两者长度相等。
该算法与最小生成树无关,基于最短路 \(\tt Djikstra\) 算法完成(但多了个等于号)。下方代码实现的功能为:读入图后,输出以 \(1\) 为根的 \(\tt SPT\) 所使用的各条边的编号、边权和。
map<pair<int, int>, int> id;
namespace G {
vector<pair<int, int> > ver[N];
map<pair<int, int>, int> edge;
int v[N], d[N], pre[N], vis[N];
int ans = 0;
void add(int x, int y, int w) {
ver[x].push_back({y, w});
edge[{x, y}] = edge[{y, x}] = w;
}
void djikstra(int s) { // !注意,该 djikstra 并非原版,多加了一个等于号
priority_queue<PII, vector<PII>, greater<PII> > q; q.push({0, s});
memset(d, 0x3f, sizeof d); d[s] = 0;
while (!q.empty()) {
int x = q.top().second; q.pop();
if (v[x]) continue; v[x] = 1;
for (auto [y, w] : ver[x]) {
if (d[y] >= d[x] + w) { // !注意,SPT 这里修改为>=号
d[y] = d[x] + w;
pre[y] = x; // 记录前驱结点
q.push({d[y], y});
}
}
}
}
void dfs(int x) {
vis[x] = 1;
for (auto [y, w] : ver[x]) {
if (vis[y]) continue;
if (pre[y] == x) {
cout << id[{x, y}] << " "; // 输出SPT所使用的边编号
ans += edge[{x, y}];
dfs(y);
}
}
}
void solve(int n) {
djikstra(1); // 以 1 为根
dfs(1); // 以 1 为根
cout << endl << ans; // 输出SPT的边权和
}
}
bool Solve() {
int n, m; cin >> n >> m;
for (int i = 1; i <= m; ++ i) {
int x, y, w; cin >> x >> y >> w;
G::add(x, y, w), G::add(y, x, w);
id[{x, y}] = id[{y, x}] = i;
}
G::solve(n);
return 0;
}
无源汇点的最小割问题 Stoer–Wagner
也称为全局最小割。定义补充(与《网络流》中的定义不同):
割:是一个边集,去掉其中所有边能使一张网络流图不再连通(即分成两个子图)。
通过递归的方式来解决无向正权图上的全局最小割问题,算法复杂度 \(\mathcal O(VE + V^{2}\log V)\) ,一般可近似看作 \(\mathcal O(V^3)\) 。
signed main() {
int n, m;
cin >> n >> m;
DSU dsu(n); // 这里引入DSU判断图是否联通,如题目有保证,则不需要此步骤
vector<vector<int>> edge(n + 1, vector<int>(n + 1));
for (int i = 1; i <= m; i++) {
int x, y, w;
cin >> x >> y >> w;
dsu.merge(x, y);
edge[x][y] += w;
edge[y][x] += w;
}
if (dsu.Poi(1) != n || m < n - 1) { // 图不联通
cout << 0 << endl;
return 0;
}
int MinCut = INF, S = 1, T = 1; // 虚拟源汇点
vector<int> bin(n + 1);
auto contract = [&]() -> int { // 求解S到T的最小割,定义为 cut of phase
vector<int> dis(n + 1), vis(n + 1);
int Min = 0;
for (int i = 1; i <= n; i++) {
int k = -1, maxc = -1;
for (int j = 1; j <= n; j++) {
if (!bin[j] && !vis[j] && dis[j] > maxc) {
k = j;
maxc = dis[j];
}
}
if (k == -1) return Min;
S = T, T = k, Min = maxc;
vis[k] = 1;
for (int j = 1; j <= n; j++) {
if (!bin[j] && !vis[j]) {
dis[j] += edge[k][j];
}
}
}
return Min;
};
for (int i = 1; i < n; i++) { // 这里取不到等号
int val = contract();
bin[T] = 1;
MinCut = min(MinCut, val);
if (!MinCut) {
cout << 0 << endl;
return 0;
}
for (int j = 1; j <= n; j++) {
if (!bin[j]) {
edge[S][j] += edge[j][T];
edge[j][S] += edge[j][T];
}
}
}
cout << MinCut << endl;
}
欧拉路径/欧拉回路 Hierholzers
定义:只有连通图才有欧拉路径/欧拉回路。
无向图欧拉路径:度数为奇数的点只能有 \(0\) 或 \(2\) 个;无向图欧拉回路:度数为奇数的点只能有 \(0\) 个。
有向图欧拉路径:\(\tt ^1\) 要么所有点的出度均等于入度;\(\tt ^2\) 要么有一个点出度比入度多 \(1\) (起点)、有一个点入度比出度多 \(1\) (终点)、其余点出度均等于入度。有向图欧拉回路:所有点的出度均等于入度。
\(\mathcal{Provided \ by \ \pmb{Hamine}}\) 。求有向图字典序最小的欧拉路径。如果不存在欧拉路径,输出一行 No。否则输出一行 \(m+1\) 个数字,表示字典序最小的欧拉路径。参
const int N = 1e5 + 10;
LL n, m, in[N], out[N];
vector <LL> p(N);
vector < pair<LL, LL> > g[N];
stack <LL> ans;
void dfs(LL u){
for (int i = p[u]; i < (int)g[u].size(); i = max(i + 1LL, p[u]) ){
auto [v, vis] = g[u][i];
p[u] = i + 1;
dfs(v);
}
ans.push(u);
};
int main(){
ios::sync_with_stdio(false);cin.tie(0);
cin >> n >> m;
for (int i = 0; i < m; i ++ ){
LL u, v;
cin >> u >> v;
g[u].push_back({v, i});
out[u] ++ ;
in[v] ++ ;
}
LL st = 0, ed = 0, s = 1;
bool ok = true;
for (int i = 1; i <= n; i ++ ){
sort(g[i].begin(), g[i].end());
if (in[i] != out[i]){
if (in[i] == out[i] + 1){
ed ++ ;
}
else if (out[i] == in[i] + 1){
st ++ ;
s = i;
}
else{
ok = false;
break;
}
}
}
if ( (st == 1 && ed == 1 && ok) || (!st && !ed && ok) ){
dfs(s);
while (ans.size()){
cout << ans.top() << " ";
ans.pop();
}
}
else{
cout << "No\n";
}
return 0;
}
差分约束
\(\mathcal{Provided \ by \ \pmb{Hamine}}\) 。给出一组包含 \(m\) 个不等式,有 \(n\) 个未知数的形如:
的不等式组,求任意一组满足这个不等式组的解。若无解,输出 "NO"。参考
const int N = 5e3 + 10;
struct edge{
LL u, v, w;
}e[N];
LL n, m, d[N];
void bellman_ford(){
memset(d, 0x3f, sizeof d);
d[1] = 0;
for (int i = 1; i < n; i ++ )
for (int j = 0; j < m; j ++ )
d[e[j].v] = min(d[e[j].v], d[e[j].u] + e[j].w);
for (int i = 0; i < m; i ++ )
if (d[e[i].v] > d[e[i].u] + e[i].w){
cout << "NO\n";
return;
}
for (int i = 1; i <= n; i ++ )
cout << d[i] << " \n"[i == n];
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
cin >> n >> m;
for (int i = 0; i < m; i ++ ){
LL u, v, w;
cin >> v >> u >> w;
e[i] = {u, v, w};
}
bellman_ford();
return 0;
}
树的直径
struct Tree {
int n;
vector<vector<int>> ver;
Tree(int n) {
this->n = n;
ver.resize(n + 1);
}
void add(int x, int y) {
ver[x].push_back(y);
ver[y].push_back(x);
}
int getlen(int root) { // 获取x所在树的直径
map<int, int> dep; // map用于优化输入为森林时的深度计算,亦可用vector
function<void(int, int)> dfs = [&](int x, int fa) -> void {
for (auto y : ver[x]) {
if (y == fa) continue;
dep[y] = dep[x] + 1;
dfs(y, x);
}
if (dep[x] > dep[root]) {
root = x;
}
};
dfs(root, 0);
int st = root; // 记录直径端点
dep.clear();
dfs(root, 0);
int ed = root; // 记录直径另一端点
return dep[root];
}
};
树论大封装(直径+重心+中心)
struct Tree {
int n;
vector<vector<pair<int, int>>> e;
vector<int> dep, parent, maxdep, d1, d2, s1, s2, up;
Tree(int n) {
this->n = n;
e.resize(n + 1);
dep.resize(n + 1);
parent.resize(n + 1);
maxdep.resize(n + 1);
d1.resize(n + 1);
d2.resize(n + 1);
s1.resize(n + 1);
s2.resize(n + 1);
up.resize(n + 1);
}
void add(int u, int v, int w) {
e[u].push_back({w, v});
e[v].push_back({w, u});
}
void dfs(int u, int fa) {
maxdep[u] = dep[u];
for (auto [w, v] : e[u]) {
if (v == fa) continue;
dep[v] = dep[u] + 1;
parent[v] = u;
dfs(v, u);
maxdep[u] = max(maxdep[u], maxdep[v]);
}
}
void dfs1(int u, int fa) {
for (auto [w, v] : e[u]) {
if (v == fa) continue;
dfs1(v, u);
int x = d1[v] + w;
if (x > d1[u]) {
d2[u] = d1[u], s2[u] = s1[u];
d1[u] = x, s1[u] = v;
} else if (x > d2[u]) {
d2[u] = x, s2[u] = v;
}
}
}
void dfs2(int u, int fa) {
for (auto [w, v] : e[u]) {
if (v == fa) continue;
if (s1[u] == v) {
up[v] = max(up[u], d2[u]) + w;
} else {
up[v] = max(up[u], d1[u]) + w;
}
dfs2(v, u);
}
}
int radius, center, diam;
void getCenter() {
center = 1; //中心
for (int i = 1; i <= n; i++) {
if (max(d1[i], up[i]) < max(d1[center], up[center])) {
center = i;
}
}
radius = max(d1[center], up[center]); //距离最远点的距离的最小值
diam = d1[center] + up[center] + 1; //直径
}
int rem; //删除重心后剩余连通块体积的最小值
int cog; //重心
vector<bool> vis;
void getCog() {
vis.resize(n);
rem = INT_MAX;
cog = 1;
dfsCog(1);
}
int dfsCog(int u) {
vis[u] = true;
int s = 1, res = 0;
for (auto [w, v] : e[u]) {
if (vis[v]) continue;
int t = dfsCog(v);
res = max(res, t);
s += t;
}
res = max(res, n - s);
if (res < rem) {
rem = res;
cog = u;
}
return s;
}
};
点分治 / 树的重心
\(\mathcal{Provided \ by \ \pmb{Wida}}\) 。重心的定义:删除树上的某一个点,会得到若干棵子树;删除某点后,得到的最大子树最小,这个点称为重心。我们假设某个点是重心,记录此时最大子树的最小值,遍历完所有点后取最大值即可。
重心的性质:重心最多可能会有两个,且此时两个重心相邻。
点分治的一般过程是:取重心为新树的根,随后使用 \(\tt dfs\) 处理当前这棵树,灵活运用 child
和 pre
两个数组分别计算通过根节点、不通过根节点的路径信息,根据需要进行答案的更新;再对子树分治,寻找子树的重心,……。时间复杂度降至 \(\mathcal O(N\log N)\) 。
int root = 0, MaxTree = 1e18; //分别代表重心下标、最大子树大小
vector<int> vis(n + 1), siz(n + 1);
auto get = [&](auto self, int x, int fa, int n) -> void { // 获取树的重心
siz[x] = 1;
int val = 0;
for (auto [y, w] : ver[x]) {
if (y == fa || vis[y]) continue;
self(self, y, x, n);
siz[x] += siz[y];
val = max(val, siz[y]);
}
val = max(val, n - siz[x]);
if (val < MaxTree) {
MaxTree = val;
root = x;
}
};
auto clac = [&](int x) -> void { // 以 x 为新的根,维护询问
set<int> pre = {0}; // 记录到根节点 x 距离为 i 的路径是否存在
vector<int> dis(n + 1);
for (auto [y, w] : ver[x]) {
if (vis[y]) continue;
vector<int> child; // 记录 x 的子树节点的深度信息
auto dfs = [&](auto self, int x, int fa) -> void {
child.push_back(dis[x]);
for (auto [y, w] : ver[x]) {
if (y == fa || vis[y]) continue;
dis[y] = dis[x] + w;
self(self, y, x);
}
};
dis[y] = w;
dfs(dfs, y, x);
for (auto it : child) {
for (int i = 1; i <= m; i++) { // 根据询问更新值
if (q[i] < it || !pre.count(q[i] - it)) continue;
ans[i] = 1;
}
}
pre.insert(child.begin(), child.end());
}
};
auto dfz = [&](auto self, int x, int fa) -> void { // 点分治
vis[x] = 1; // 标记已经被更新过的旧重心,确保只对子树分治
clac(x);
for (auto [y, w] : ver[x]) {
if (y == fa || vis[y]) continue;
MaxTree = 1e18;
get(get, y, x, siz[y]);
self(self, root, x);
}
};
get(get, 1, 0, n);
dfz(dfz, root, 0);
最近公共祖先 LCA
树链剖分解法
预处理时间复杂度 \(\mathcal O(N)\) ;单次查询 \(\mathcal O(\log N)\) ,常数较小。
struct HLD {
int n, idx;
vector<vector<int>> ver;
vector<int> siz, dep;
vector<int> top, son, parent;
HLD(int n) {
this->n = n;
ver.resize(n + 1);
siz.resize(n + 1);
dep.resize(n + 1);
top.resize(n + 1);
son.resize(n + 1);
parent.resize(n + 1);
}
void add(int x, int y) { // 建立双向边
ver[x].push_back(y);
ver[y].push_back(x);
}
void dfs1(int x) {
siz[x] = 1;
dep[x] = dep[parent[x]] + 1;
for (auto y : ver[x]) {
if (y == parent[x]) continue;
parent[y] = x;
dfs1(y);
siz[x] += siz[y];
if (siz[y] > siz[son[x]]) {
son[x] = y;
}
}
}
void dfs2(int x, int up) {
top[x] = up;
if (son[x]) dfs2(son[x], up);
for (auto y : ver[x]) {
if (y == parent[x] || y == son[x]) continue;
dfs2(y, y);
}
}
int lca(int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] > dep[top[y]]) {
x = parent[top[x]];
} else {
y = parent[top[y]];
}
}
return dep[x] < dep[y] ? x : y;
}
int clac(int x, int y) { // 查询两点间距离
return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
void work(int root = 1) { // 在此初始化
dfs1(root);
dfs2(root, root);
}
};
树上倍增解法
预处理时间复杂度 \(\mathcal O(N\log N)\) ;单次查询 \(\mathcal O(\log N)\) ,但是常数比树链剖分解法更大。
封装一:基础封装,针对无权图。
struct Tree {
int n;
vector<vector<int>> ver, val;
vector<int> lg, dep;
Tree(int n) {
this->n = n;
ver.resize(n + 1);
val.resize(n + 1, vector<int>(30));
lg.resize(n + 1);
dep.resize(n + 1);
for (int i = 1; i <= n; i++) { //预处理 log
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
}
}
void add(int x, int y) { // 建立双向边
ver[x].push_back(y);
ver[y].push_back(x);
}
void dfs(int x, int fa) {
val[x][0] = fa; // 储存 x 的父节点
dep[x] = dep[fa] + 1;
for (int i = 1; i <= lg[dep[x]]; i++) {
val[x][i] = val[val[x][i - 1]][i - 1];
}
for (auto y : ver[x]) {
if (y == fa) continue;
dfs(y, x);
}
}
int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
while (dep[x] > dep[y]) {
x = val[x][lg[dep[x] - dep[y]] - 1];
}
if (x == y) return x;
for (int k = lg[dep[x]] - 1; k >= 0; k--) {
if (val[x][k] == val[y][k]) continue;
x = val[x][k];
y = val[y][k];
}
return val[x][0];
}
int clac(int x, int y) { // 倍增查询两点间距离
return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
void work(int root = 1) { // 在此初始化
dfs(root, 0);
}
};
封装二:扩展封装,针对有权图,支持“倍增查询两点路径上的最大边权”功能。
struct Tree {
int n;
vector<vector<int>> val, Max;
vector<vector<pair<int, int>>> ver;
vector<int> lg, dep;
Tree(int n) {
this->n = n;
ver.resize(n + 1);
val.resize(n + 1, vector<int>(30));
Max.resize(n + 1, vector<int>(30));
lg.resize(n + 1);
dep.resize(n + 1);
for (int i = 1; i <= n; i++) { //预处理 log
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
}
}
void add(int x, int y, int w) { // 建立双向边
ver[x].push_back({y, w});
ver[y].push_back({x, w});
}
void dfs(int x, int fa) {
val[x][0] = fa;
dep[x] = dep[fa] + 1;
for (int i = 1; i <= lg[dep[x]]; i++) {
val[x][i] = val[val[x][i - 1]][i - 1];
Max[x][i] = max(Max[x][i - 1], Max[val[x][i - 1]][i - 1]);
}
for (auto [y, w] : ver[x]) {
if (y == fa) continue;
Max[y][0] = w;
dfs(y, x);
}
}
int lca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
while (dep[x] > dep[y]) {
x = val[x][lg[dep[x] - dep[y]] - 1];
}
if (x == y) return x;
for (int k = lg[dep[x]] - 1; k >= 0; k--) {
if (val[x][k] == val[y][k]) continue;
x = val[x][k];
y = val[y][k];
}
return val[x][0];
}
int clac(int x, int y) { // 倍增查询两点间距离
return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
int query(int x, int y) { // 倍增查询两点路径上的最大边权(带权图)
auto get = [&](int x, int y) -> int {
int ans = 0;
if (x == y) return ans;
for (int i = lg[dep[x]]; i >= 0; i--) {
if (dep[val[x][i]] > dep[y]) {
ans = max(ans, Max[x][i]);
x = val[x][i];
}
}
ans = max(ans, Max[x][0]);
return ans;
};
int fa = lca(x, y);
return max(get(x, fa), get(y, fa));
}
void work(int root = 1) { // 在此初始化
dfs(root, 0);
}
};
树上启发式合并 (DSU on tree)
\(\mathcal O(N\log N)\) 。
struct HLD {
vector<vector<int>> e;
vector<int> siz, son, cnt;
vector<LL> ans;
LL sum, Max;
int hson;
HLD(int n) {
e.resize(n + 1);
siz.resize(n + 1);
son.resize(n + 1);
ans.resize(n + 1);
cnt.resize(n + 1);
hson = 0;
sum = 0;
Max = 0;
}
void add(int u, int v) {
e[u].push_back(v);
e[v].push_back(u);
}
void dfs1(int u, int fa) {
siz[u] = 1;
for (auto v : e[u]) {
if (v == fa) continue;
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
}
}
void calc(int u, int fa, int val) {
cnt[color[u]] += val;
if (cnt[color[u]] > Max) {
Max = cnt[color[u]];
sum = color[u];
} else if (cnt[color[u]] == Max) {
sum += color[u];
}
for (auto v : e[u]) {
if (v == fa || v == hson) continue;
calc(v, u, val);
}
}
void dfs2(int u, int fa, int opt) {
for (auto v : e[u]) {
if (v == fa || v == son[u]) continue;
dfs2(v, u, 0);
}
if (son[u]) {
dfs2(son[u], u, 1);
hson = son[u]; //记录重链编号,计算的时候跳过
}
calc(u, fa, 1);
hson = 0; //消除的时候所有儿子都清除
ans[u] = sum;
if (!opt) {
calc(u, fa, -1);
sum = 0;
Max = 0;
}
}
};
最大流
使用 \(\tt Dinic\) 算法,最坏复杂度为 \(\mathcal O(N^2*M)\) ,一般用于处理 \(N \le 10^5\) 。一般步骤:\(\tt BFS\) 建立分层图,无回溯 \(\tt DFS\) 寻找所有可行的增广路径。封装:求从点 \(S\) 到点 \(T\) 的最大流。
template <typename T> struct Flow_ {
const int n;
const T inf = std::numeric_limits<T>::max();
struct Edge {
int to;
T w;
Edge(int to, T w) : to(to), w(w) {}
};
vector<Edge> ver;
vector<vector<int>> h;
vector<int> cur, d;
Flow_(int n) : n(n + 1), h(n + 1) {}
void add(int u, int v, T c) {
h[u].push_back(ver.size());
ver.emplace_back(v, c);
h[v].push_back(ver.size());
ver.emplace_back(u, 0);
}
bool bfs(int s, int t) {
d.assign(n, -1);
d[s] = 0;
queue<int> q;
q.push(s);
while (!q.empty()) {
auto x = q.front();
q.pop();
for (auto it : h[x]) {
auto [y, w] = ver[it];
if (w && d[y] == -1) {
d[y] = d[x] + 1;
if (y == t) return true;
q.push(y);
}
}
}
return false;
}
T dfs(int u, int t, T f) {
if (u == t) return f;
auto r = f;
for (int &i = cur[u]; i < h[u].size(); i++) {
auto j = h[u][i];
auto &[v, c] = ver[j];
auto &[u, rc] = ver[j ^ 1];
if (c && d[v] == d[u] + 1) {
auto a = dfs(v, t, std::min(r, c));
c -= a;
rc += a;
r -= a;
if (!r) return f;
}
}
return f - r;
}
T work(int s, int t) {
T ans = 0;
while (bfs(s, t)) {
cur.assign(n, 0);
ans += dfs(s, t, inf);
}
return ans;
}
};
using Flow = Flow_<int>;
有源汇点的最大流最小割问题
定义补充:
割:是一种点的划分方式——将所有的点划分为 \(S\) 和 \(T=V-S\) 两个集合,其中源点 \(s\in S\) ,汇点 $t\in T $。
割的容量:割 \((S,T)\) 的容量 \(c(S,T)\) 为所有从 \(S\) 到 \(T\) 的边的容量之和,即 \(\displaystyle c(S,T)=\sum_{u\in S,v\in T}c(u,v)\) 。
最小割:求得一个割 \((S,T)\) ,使得割的容量 \(c(S,T)\) 最小。
定理:\(f(S,T)_{\max}=c(S,T)_{\min}\) 。
const int N = 1e4 + 5, M = 2e5 + 5;
int n, m, s, t, tot = 1, lnk[N], ter[M], nxt[M], val[M], dep[N], cur[N];
void add(int u, int v, int w) {
ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, val[tot] = w;
}
void addedge(int u, int v, int w) {
add(u, v, w);
add(v, u, 0);
}
int bfs(int s, int t) {
memset(dep, 0, sizeof(dep));
memcpy(cur, lnk, sizeof(lnk));
std::queue<int> q;
q.push(s), dep[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = lnk[u]; i; i = nxt[i]) {
int v = ter[i];
if (val[i] && !dep[v]) q.push(v), dep[v] = dep[u] + 1;
}
}
return dep[t];
}
int dfs(int u, int t, int flow) {
if (u == t) return flow;
int ans = 0;
for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
int v = ter[i];
if (val[i] && dep[v] == dep[u] + 1) {
int x = dfs(v, t, std::min(val[i], flow - ans));
if (x) val[i] -= x, val[i ^ 1] += x, ans += x;
}
}
if (ans < flow) dep[u] = -1;
return ans;
}
int dinic(int s, int t) {
int ans = 0;
while (bfs(s, t)) {
int x;
while ((x = dfs(s, t, 1 << 30))) ans += x;
}
return ans;
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t);
while (m--) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
addedge(u, v, w);
}
printf("%d\n", dinic(s, t));
return 0;
}
费用流
给定一个带费用的网络,规定 \((u,v)\) 间的费用为 \(f(u,v) \times w(u,v)\) ,求解该网络中总花费最小的最大流称之为最小费用最大流。借助 \(\tt Bellman-Ford\) 求解最短路,总时间复杂度为 \(\mathcal O(NMf)\) ,其中 \(f\) 代表最大流。
struct MinCostFlow {
using LL = long long;
using PII = pair<LL, int>;
const LL INF = numeric_limits<LL>::max();
struct Edge {
int v, c, f;
Edge(int v, int c, int f) : v(v), c(c), f(f) {}
};
const int n;
vector<Edge> e;
vector<vector<int>> g;
vector<LL> h, dis;
vector<int> pre;
MinCostFlow(int n) : n(n), g(n) {}
void add(int u, int v, int c, int f) {
if (f < 0) {
g[u].push_back(e.size());
e.emplace_back(v, 0, f);
g[v].push_back(e.size());
e.emplace_back(u, c, -f);
} else {
g[u].push_back(e.size());
e.emplace_back(v, c, f);
g[v].push_back(e.size());
e.emplace_back(u, 0, -f);
}
}
bool dijkstra(int s, int t) {
dis.assign(n, INF);
pre.assign(n, -1);
priority_queue<PII, vector<PII>, greater<PII>> que;
dis[s] = 0;
que.emplace(0, s);
while (!que.empty()) {
auto [d, u] = que.top();
que.pop();
if (dis[u] < d) continue;
for (int i : g[u]) {
auto [v, c, f] = e[i];
if (c > 0 && dis[v] > d + h[u] - h[v] + f) {
dis[v] = d + h[u] - h[v] + f;
pre[v] = i;
que.emplace(dis[v], v);
}
}
}
return dis[t] != INF;
}
pair<int, LL> flow(int s, int t) {
int flow = 0;
LL cost = 0;
h.assign(n, 0);
while (dijkstra(s, t)) {
for (int i = 0; i < n; ++i) h[i] += dis[i];
int aug = numeric_limits<int>::max();
for (int i = t; i != s; i = e[pre[i] ^ 1].v) aug = min(aug, e[pre[i]].c);
for (int i = t; i != s; i = e[pre[i] ^ 1].v) {
e[pre[i]].c -= aug;
e[pre[i] ^ 1].c += aug;
}
flow += aug;
cost += LL(aug) * h[t];
}
return {flow, cost};
}
};