二分图与网络流笔记
zhiyangfan · · 个人记录
二分图与网络流
这不是 CSP-S 刚考了个网络流嘛,我想着去再刷点题,好家伙这不刷不知道,一刷发现自己网络流的东西忘完了,赶紧写个博客总结一下,就当是我备战 NOIp 复习的开始吧。
概念部分
网络流
基本定义
类比水流,我们定义图论中的流量网络。在一个有向图
- 容量限制:
\forall (u,v)\in E ,都应该有0\le f_{u,v}\le c_{u,v} 。 - 流量守恒:
\forall u ,都应该有\sum_{v'} f_{u,v'}-\sum_{v''} f_{v'',u}=\begin{cases}|f|&u=T\\-|f|&u=S\\0&\texttt{otherwise}\end{cases} 。其中(u,v') 表示u 的所有出边,(v'',u) 表示u 的所有入边。|f| 叫做该可行流的流量,是源点的净流出量,也是汇点的净流入量。
注意一个流量网络一定存在
最大流算法
最大流算法就是在给定的流量网络上找到最大流的流量。
EK 算法
我们定义流量网络上的增广路为一条满足上面所有边
这个过程其实就是 c 初始时为容量
#include <queue>
#include <cstdio>
#include <cstring>
const int N = 300, M = 5e3 + 10, inf = 2e9; typedef long long ll;
struct edge { int u, v, next, c; }E[M << 1]; int p[N], cnt, s, t, pre[N];
void init() { memset(p, -1, sizeof(p)); cnt = 0; }
void insert(int u, int v, int c) { E[cnt].u = u; E[cnt].v = v; E[cnt].c = c; E[cnt].next = p[u]; p[u] = cnt++; }
void addedge(int u, int v, int c) { insert(u, v, c); insert(v, u, 0); } //建反边
inline void bfs()
{
std::queue<int> q; q.push(s); memset(pre, -1, sizeof(pre)); int u;
while (!q.empty())
{
u = q.front(); q.pop();
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v;
if (pre[v] == -1 && E[i].c) //如果这条路没有走过且该边仍有剩余流量
{
pre[v] = i; //记录路径
if (v == t) break;
q.push(v); //继续增广
}
}
if (pre[t] != -1) break; //如果找到汇点就停
}
}
inline ll EK()
{
ll ret = 0;
while (1)
{
bfs(); if (pre[t] == -1) break; //如果找不到增广路,就停
int minx = inf;
for (int u = t; u != s; u = E[pre[u]].u)
minx = std::min(minx, E[pre[u]].c); //在增广路上找最小剩余流量
for (int u = t; u != s; u = E[pre[u]].u)
E[pre[u]].c -= minx, E[pre[u] ^ 1].c += minx;
//更新,因为反边和正边的标号是相邻的,所以 ^ 1 就是反边
ret += minx;
}
return ret;
}
int main()
{
init(); int n, m; scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1, x, y, z; i <= m; ++i)
scanf("%d%d%d", &x, &y, &z), addedge(x, y, z);
printf("%lld\n", EK()); return 0;
}
该算法的时间复杂度为
Dinic 算法
发现
#include <queue>
#include <cstdio>
#include <cstring>
const int N = 2e5 + 10, inf = 2e9; typedef long long ll;
struct edge{ int v, next, c; }E[N << 1]; int p[N], d[N], cur[N], cnt, s, t;
inline void init() { memset(p, -1, sizeof (p)); cnt = 0; }
inline void insert(int u, int v, int c) { E[cnt].v = v; E[cnt].c = c; E[cnt].next = p[u]; p[u] = cnt++; }
inline void addedge(int u, int v, int c) { insert(u, v, c); insert(v, u, 0); }
inline bool bfs()
{
memset(d, -1, sizeof (d)); d[s] = 0; cur[s] = p[s];
std::queue<int> q; q.push(s); int u;
while (!q.empty())
{
u = q.front(); q.pop();
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[v] = p[v];
if (d[v] == -1 && E[i].c) { d[v] = d[u] + 1; q.push(v); }
}
}
return d[t] != -1;
}
ll dfs(int u, int flow)
{
//flow 记录链上最小剩余流量,到汇点后返回
if (u == t) return flow; ll ans = 0, ret;
for (int i = cur[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[u] = i; //1)
if (E[i].c && d[v] == d[u] + 1)
{
ret = dfs(v, std::min(flow, E[i].c)); //更新记录的最小剩余流量
E[i].c -= ret; E[i ^ 1].c += ret; //回溯时更新流量
flow -= ret; ans += ret; if (!flow) break; //2)
}
}
if (!ans) d[u] = -1; //3)
return ans;
}
//不断 bfs 找增广路并 dfs 更新
inline ll dinic() { ll ans = 0; while (bfs()) ans += dfs(s, inf); return ans; }
int main()
{
init(); int n, m; scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1, x, y, z; i <= m; ++i)
scanf("%d%d%d", &x, &y, &z), addedge(x, y, z);
printf("%lld\n", dinic()); return 0;
}
该算法的时间复杂度为
这里有必要专门提一下代码中标记的一些优化点,没了它们 cur。这个被称作当前弧优化,我们发现一条增广路一旦被遍历过就不能再被增广了,而每次再重新搜索这些链是很费时间的,所以我们用个 cur 记录一下当前遍历到哪条边了,就不会重复遍历了。然后是 2) 位置上的 break;,这个就比较显然了,当前已经没有流量可以继续增广了,跳出就好。最后是 3) 位置上的 d[u] = -1;,这个表示这条链没有可以增广的地方,把它封死,以后不再进来了。只有这些剪枝和优化都加上了,
最小割
对于流量网络
对于一个割
引理1.1 最小割等于最大流。
证明1.1 不会。但感觉可以感性理解,实在想知道的话可以百度。
有了最小割最大流定理后,我们可以把一些问题抽象为最小割模型,并用最大流问题解决,一些常见模型将会在下文给出。
费用流
在原流量网络的定义上,我们给每条边
考虑在 vis 记录一下当前正在遍历的路径,不能重复访问。还要注意,反边的作用是反悔,所以边权应该是正边的相反数。
#include <queue>
#include <cstdio>
#include <cstring>
const int N = 4e5 + 10, M = 9e6 + 100; typedef long long ll;
const ll inf = 0x3f3f3f3f3f3f3f3f; ll mincost, d[N]; int vis[N];
struct edge { int v, next; ll w, c; }E[M]; int p[N], cur[N], cnt, s, t;
inline void init() { memset(p, -1, sizeof(p)); cnt = 0; }
inline void insert(int u, int v, ll c, ll w) { E[cnt].v = v; E[cnt].c = c; E[cnt].w = w; E[cnt].next = p[u]; p[u] = cnt++; }
inline void addedge(int u, int v, ll c, ll w) { insert(u, v, c, w); insert(v, u, 0, -w); }
bool bfs()
{
memset(d, 0x3f, sizeof (d)); d[s] = 0; cur[s] = p[s];
std::queue<int> q; q.push(s); vis[s] = 1; int u;
while (!q.empty())
{
u = q.front(); q.pop(); vis[u] = 0;
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[v] = p[v];
if (E[i].c && d[v] > d[u] + E[i].w )
{
d[v] = d[u] + E[i].w;
if (!vis[v]) vis[v] = 1, q.push(v);
}
}
}
return d[t] != inf;
}
ll dfs(int u, ll flow)
{
if (u == t) return flow; ll ans = 0, ret; vis[u] = 1;
for (int i = cur[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[u] = i;
if (E[i].c && d[v] == d[u] + E[i].w && !vis[v])
{
ll ret = dfs(v, std::min(E[i].c, flow));
E[i].c -= ret; E[i ^ 1].c += ret; mincost += ret * E[i].w;
flow -= ret; ans += ret; if (!flow) break;
}
}
if (ans == 0) d[u] = inf; vis[u] = 0;
return ans;
}
inline ll dinic() { ll ans = 0; while (bfs()) ans += dfs(s, inf); return ans; }
int main()
{
init(); int n, m; scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1, u, v, w, c; i <= m; i++)
scanf("%d%d%d%d", &u, &v, &w, &c), addedge(u, v, w, c);
ll ans = dinic(); printf("%lld %lld\n", ans, mincost);
return 0;
}
该算法的时间复杂度为
因为费用流有双重限制,既要求最大流,也要求费用最大/小,所以有很多问题都可以归结为费用流,既要求题目条件得到满足,也要求花费的费用最小,把题目要求用流量表示,费用用权值表示即可。下文会给出一些经典模型。
最大权闭合子图
对于一张有向图
求解这个问题是最小割模型的经典应用。对于原图的一条有向边
考虑为什么这样割是对的。一条有向边
上下界网络流
暂时不会。
二分图
基本定义
对于一个无向图
引理2.1 一张图是二分图,当且仅当其不含有奇环。
证明2.1 发现如果成环只能是在左右部点之间来回跳形成的,显然长度为偶数,必要性得证。如果原图没有环,则显然可以分为二分图,如果出现偶环,可以把相邻点分到不同的集合内(注意到奇环做不到这样),所以可以构造出一个二分图,充分性得证。
判断二分图可以通过
匹配相关
对于一张无向图
考虑图
引理2.2 设
证明2.2 考虑反证,如果
引理2.3 Hall定理:设二分图
证明2.3 证明显然不会。
匈牙利算法
根据上文所说的增广路定理,我们找二分图最大匹配其实相当于不断寻找增广路进行更新直到找不到增广路就找到了最大匹配。匈牙利算法就是基于这个思想的,它一开始把最大匹配置空,不断寻找增广路,并用上文所说的交换状态更新最大匹配,直到找不到增广路算法结束。
#include <cstdio>
#include <cstring>
const int N = 1e3 + 10, M = 5e4 + 10;
struct edge{ int v, next; }E[M]; int p[N], cnt;
inline void init() { memset(p, -1, sizeof p); cnt = 0; }
inline void insert(int u, int v) { E[cnt].v = v; E[cnt].next = p[u]; p[u] = cnt++; }
int n, m, e, match[N], vistime[N];
bool dfs(int u, int tag)
{
if (vistime[u] == tag) return false; vistime[u] = tag;
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v;
if (match[v] == 0 || dfs(match[v], tag)) { match[v] = u; return true; }
}
return false;
}
int main()
{
init(); scanf("%d%d%d", &n, &m, &e);
for (int i = 1, u, v; i <= e; i++) scanf("%d%d", &u, &v), insert(u, v);
int ans = 0;
for (int i = 1; i <= n; i++) if (dfs(i, i)) ans++;
printf("%d\n", ans); return 0;
}
该算法时间复杂度为
最大流
二分图匹配的问题可以用最大流模型来解决。考虑从源点
#include <queue>
#include <cstdio>
#include <cstring>
const int N = 1e3 + 10, M = 1e5 + 10, inf = 2e9;
struct edge{ int v, next, c; }E[M << 1]; int p[N], d[N], cur[N], cnt, s, t;
inline void init() { memset(p, -1, sizeof (p)); cnt = 0; }
inline void insert(int u, int v, int c) { E[cnt].v = v; E[cnt].c = c; E[cnt].next = p[u]; p[u] = cnt++; }
inline void addedge(int u, int v, int c) { insert(u, v, c); insert(v, u, 0); }
inline bool bfs()
{
memset(d, -1, sizeof (d)); d[s] = 0; cur[s] = p[s];
std::queue<int> q; q.push(s); int u;
while (!q.empty())
{
u = q.front(); q.pop();
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[v] = p[v];
if (d[v] == -1 && E[i].c) { d[v] = d[u] + 1; q.push(v); }
}
}
return d[t] != -1;
}
int dfs(int u, int flow)
{
if (u == t) return flow; int ans = 0, ret;
for (int i = cur[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[u] = i;
if (E[i].c && d[v] == d[u] + 1)
{
ret = dfs(v, std::min(flow, E[i].c));
E[i].c -= ret; E[i ^ 1].c += ret;
flow -= ret; ans += ret; if (!flow) break;
}
}
if (!ans) d[u] = -1;
return ans;
}
inline int dinic() { int ans = 0; while (bfs()) ans += dfs(s, inf); return ans; }
int main()
{
init(); int n, m, e; scanf("%d%d%d", &n, &m, &e); s = 0; t = n + m + 1;
for (int i = 1, x, y; i <= e; ++i) scanf("%d%d", &x, &y), addedge(x, y + n, 1);
for (int i = 1; i <= n; ++i) addedge(s, i, 1);
for (int i = 1; i <= m; ++i) addedge(i + n, t, 1);
printf("%d\n", dinic()); return 0;
}
最大权匹配
在一张边有边权的二分图上,最大权匹配定义为在所有最大匹配中匹配边权值和最大的匹配。解决这个问题有个专门的算法叫
#include <queue>
#include <cstdio>
#include <cstring>
const int N = 1e3 + 10, M = N * N + 1e6; typedef long long ll;
const ll inf = -0x8080808080808080; ll maxcost, d[N]; int vis[N];
struct edge { int v, next; ll w, c; }E[M << 1]; int p[N], cur[N], cnt, s, t;
inline void init() { memset(p, -1, sizeof(p)); cnt = 0; }
inline void insert(int u, int v, ll c, ll w) { E[cnt].v = v; E[cnt].c = c; E[cnt].w = w; E[cnt].next = p[u]; p[u] = cnt++; }
inline void addedge(int u, int v, ll c, ll w) { insert(u, v, c, w); insert(v, u, 0, -w); }
bool bfs()
{
memset(d, 0x80, sizeof (d)); d[s] = 0; cur[s] = p[s];
std::queue<int> q; q.push(s); vis[s] = 1; int u;
while (!q.empty())
{
u = q.front(); q.pop(); vis[u] = 0;
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[v] = p[v];
if (E[i].c && d[v] < d[u] + E[i].w )
{
d[v] = d[u] + E[i].w;
if (!vis[v]) vis[v] = 1, q.push(v);
}
}
}
return d[t] != -inf;
}
ll dfs(int u, ll flow)
{
if (u == t) return flow; ll ans = 0, ret; vis[u] = 1;
for (int i = cur[u], v; i + 1; i = E[i].next)
{
v = E[i].v; cur[u] = i;
if (E[i].c && d[v] == d[u] + E[i].w && !vis[v])
{
ll ret = dfs(v, std::min(E[i].c, flow));
E[i].c -= ret; E[i ^ 1].c += ret; maxcost += ret * E[i].w;
flow -= ret; ans += ret; if (!flow) break;
}
}
if (ans == 0) d[u] = inf; vis[u] = 0;
return ans;
}
inline ll dinic() { ll ans = 0; while (bfs()) ans += dfs(s, inf); return ans; }
int main()
{
init(); int n, m; scanf("%d%d", &n, &m); s = 0; t = n + n + 1;
for (int i = 1, x, y, z; i <= m; ++i)
scanf("%d%d%d", &x, &y, &z), addedge(x, y + n, 1, z);
for (int i = 1; i <= n; ++i) addedge(s, i, 1, 0), addedge(i + n, t, 1, 0);
dinic(); printf("%lld\n", maxcost);
for (int u = n + 1; u <= n + n; ++u)
for (int i = p[u]; i + 1; i = E[i].next)
if (E[i].c && E[i].v <= n) { printf("%d ", E[i].v); break; }
printf("\n"); return 0;z
}
二分图最小点覆盖
对于一张无向图
二分图最大独立集
对于一张无向图
对于一张无向图
无向图的最大团,等于补图
对于二分图,最大独立集大小等于顶点总数减去最小点覆盖集的大小等于顶点总数减去最大匹配的大小。
DAG 的最小路径覆盖
给定一张 DAG,用尽量少的不相交路径覆盖所有顶点,这样的路径集合称为最小路径覆盖。考虑二分图建模,把原图中所有点拆成
经典建模
目前更新的基本只有网络流 24 题里面的一些建模,之后我做到一些新的建模会随时补上的。注意这里用到的基本全都是
拆点
拆点有多种作用,比如把点的流量限制转化到边上,标记一个点的不同功能等等,这里给出一些例题具体说明。
P1251 餐巾计划问题
一个餐厅在连续的
首先发现既要满足要求,也要花费最小,可以想到费用流模型。注意到每天早晚的功能是不一样的,早上是要接收餐巾,晚上要送洗或保存餐巾。所以我们把每天
int main()
{
init(); N = read(); int x; S = 0, T = 2 * N + 1;
for (int i = 1; i <= N; i++)
x = read(), addedge(S, i + N, x, 0), addedge(i, T, x, 0);
int p = read(), m = read(), f = read(), n = read(), s = read();
for (int i = 1; i <= N; i++)
{
addedge(S, i, inf, p);
if (i != N) addedge(i + N, i + 1 + N, inf, 0);
if (i + m <= N) addedge(i + N, i + m, inf, f);
if (i + n <= N) addedge(i + N, i + n, inf, s);
}
dinic(); printf("%lld", mincost); return 0;
}
P2754 [CTSC1999]家园 / 星际转移问题
有
首先看无解,把所有可能停靠的地方连边,看看
inline int dinic(int s)
{
int ans = 0, d;
//这里稍微有点不一样,为了效率我们是每次都在残量网络上跑的,所以最大流是每次累加的
while (bfs())
{
for (int i = 0; i <= s; ++i) cur[i] = p[i];
ans += dfs(S, inf);
}
return maxflow += ans;
}
int main()
{
init(); scanf("%d%d%d", &n, &m, &k); S = 0; T = n + 1;
for (int i = 1; i <= n + 1; ++i) f[i] = i;
for (int i = 1; i <= m; ++i)
{
scanf("%d%d", &lim[i], &l[i][0]);
for (int j = 1; j <= l[i][0]; ++j)
{
scanf("%d", &l[i][j]);
l[i][j] = l[i][j] == -1 ? n + 1 : l[i][j];
if (j > 1) merge(l[i][j], l[i][j - 1]);
}
}
if (getf(0) != getf(n + 1)) { printf("0\n"); return 0; }
for (int ans = 1; ; ++ans)
{
for (int i = 0; i <= n; ++i)
addedge(i + (ans - 1) * (n + 2), i + ans * (n + 2), inf);
addedge(n + 1 + ans * (n + 2), n + 1 + (ans - 1) * (n + 2), inf);
for (int i = 1, las, now; i <= m; ++i)
{
las = (ans - 1) % l[i][0] + 1;
now = ans % l[i][0] + 1;
addedge(l[i][las] + (ans - 1) * (n + 2), l[i][now] + ans * (n + 2), lim[i]);
}
if (dinic((ans + 1) * (n + 2)) >= k) { printf("%d\n", ans); break; }
}
return 0;
}
P2766 最长不下降子序列问题
给出一个长为
- 计算其最长不下降子序列的长度
s 。 - 每个元素只允许使用一次的情况下,从原序列中能取出多少长为
s 的子序列。 - 如果允许多次使用
x_1,x_n ,则从原序列中能取出多少长为s 的子序列。
首先对于第一问,我们
inline void add(int d)
{
init(); s = 0; t = 2 * n + 1;
for (int i = 2; i < n; ++i) addedge(i, i + n, 1);
addedge(1, 1 + n, d ? inf : 1); addedge(n, n + n, (d && f[n] == ans) ? inf : 1);
if (f[1] == 1) addedge(s, 1, d ? inf : 1);
if (f[n] == ans) addedge(n + n, t, d ? inf : 1);
for (int i = 2; i < n; ++i)
if (f[i] == 1) addedge(s, i, 1);
else if (f[i] == ans) addedge(i + n, t, 1);
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[j] <= a[i] && f[j] + 1 == f[i]) addedge(j + n, i, 1);
}
int main()
{
scanf("%d", &n); if (n == 1) { printf("1\n1\n1\n"); return 0; }
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
for (int i = 1; i <= n; ++i)
{
f[i] = 1;
for (int j = 1; j < i; ++j)
if (a[j] <= a[i] && f[j] + 1 > f[i]) f[i] = f[j] + 1;
}
for (int i = 1; i <= n; ++i) ans = std::max(ans, f[i]); printf("%d\n", ans);
if (ans == 1) { printf("%d\n%d\n", n, n); return 0; }
add(0); printf("%d\n", dinic()); add(1); printf("%d\n", dinic());
return 0;
}
P4016 负载平衡问题
给出
因为既要满足仓库货物相同,又要满足搬运次数少,所以考虑费用流。首先显然有最终每个仓库应该有
int main()
{
init(); int n, sum = 0; scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), sum += a[i];
sum /= n; s = 0; t = n * 2 + 1;
for (int i = 1; i <= n; ++i)
if (a[i] > sum) addedge(s, i, a[i] - sum, 0);
else if (a[i] < sum) addedge(i + n, t, sum - a[i], 0);
for (int i = 1; i < n; ++i) addedge(i, i + 1, inf, 1), addedge(i, i + 1 + n, inf, 1);
addedge(n, 1, inf, 1); addedge(n, 1 + n, inf, 1);
for (int i = n; i > 1; --i) addedge(i, i - 1, inf, 1), addedge(i, i - 1 + n, inf, 1);
addedge(1, n, inf, 1); addedge(1, n + n, inf, 1);
dinic(); printf("%d\n", mincost); return 0;
}
P2765 魔术球问题/P2764 最小路径覆盖问题
这俩题差不多,我只放非板子题的题面了。有
先把非板子题变成板子。还是依次枚举答案(其实跟上面的家园一样,都满足单调性可以二分,但因为我们要每次加入点边,不方便回撤,所以就只能枚举了),对于枚举到的
inline bool check(int a)
{
int d = ceil(sqrt(a));
return d * d == a;
}
void dfs(int u)
{
st[++st[0]] = u; vis[u] = 1;
for (int i = G2.p[u], v; i + 1; i = G2.E[i].next)
{
v = G2.E[i].v; if (G2.E[i].vis || vis[v]) continue;
dfs(v); //vis 表示这个点或边走过了或不能走
}
}
int main()
{
G1.init(); G2.init(); int n, d = 0, kn, ans;
scanf("%d", &n); s = 0; t = 1e6; kn = 1e5;
for (ans = 1; ; ++ans)
{
G1.addedge(s, ans, 1); G1.addedge(ans + kn, t, 1);
for (int i = 1; i < ans; ++i)
if (check(i + ans))
{
G2.insert(i, ans, 0), G1.addedge(i, ans + kn, 1);
id[++tp].first = G1.cnt - 1; id[tp].second = G2.cnt - 1;
}
d += dinic(); if (ans - d > n) break;
}
printf("%d\n", ans - 1); vis[ans] = 1; //ans 已经不合法了,不能走
for (int i = 1; i <= tp; ++i)
if (!G1.E[id[i].first].c) G2.E[id[i].second].vis = 1; //没匹配上,不能走
for (int i = 1; i < ans; ++i)
if (!vis[i])
{
st[0] = 0; dfs(i); //对于能走的点依次 dfs 找路径即可
for (int j = 1; j <= st[0]; ++j) printf("%d ", st[j]);
printf("\n");
}
return 0;
}
最小割模型
最小割模型包括许多部分,会在每道例题具体说明。
P2762 太空飞行计划问题
现在能做
这个可以转化为最大权独立集理解,不过我个人更喜欢直接用二分图表示,感觉也可以单独叫依赖问题。考虑把实验看成左部点,器材看成右部点,源点
int main()
{
init(); int n, m, sum = 0; scanf("%d%d", &m, &n); s = 0; t = n + m + 1;
for (int i = 1, val, ulen, tool; i <= m; ++i)
{
scanf("%d", &val); sum += val; ulen = 0; addedge(s, i, val);
memset(tools, 0, sizeof (tools)); std::cin.getline(tools, 10000);
while (sscanf(tools + ulen, "%d", &tool) == 1)
{
addedge(i, tool + m, inf);
if (tool == 0) ++ulen;
else while (tool) tool /= 10, ++ulen;
++ulen;
}
}
for (int i = 1, w; i <= n; ++i) scanf("%d", &w), addedge(i + m, t, w);
int ans = sum - dinic();
for (int i = 1; i <= m; ++i) if (d[i] != -1) printf("%d ", i); printf("\n");
for (int i = 1; i <= n; ++i) if (d[i + m] != -1) printf("%d ", i); printf("\n");
printf("%d\n", ans); return 0;
}
P2774 方格取数问题/P3355 骑士共存问题
又是差不多的两道题,题意放两个题的共同抽象题意吧。方格上每个点都有点权,但有些点对(点对不能被同时选的必要不充分条件是横纵坐标和奇偶性不同)不能同时取到,问能取的最大点权为多少。
这是典型的二者选其一模型,可以转化为最小割来求解。考虑先全部选,然后去掉尽量少的边权。把
int main()
{
init(); int n, m, sum = 0; scanf("%d%d", &n, &m); s = 0; t = n * m + 1;
for (int i = 1, a; i <= n; ++i)
for (int j = 1; j <= m; ++j)
{
scanf("%d", &a), sum += a;
if ((i + j) & 1) addedge(id(i, j), t, a);
else
{
addedge(s, id(i, j), a);
for (int k = 0; k < 4; ++k)
{
int tx = i + nxt[k][0], ty = j + nxt[k][1];
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
addedge(id(i, j), id(tx, ty), inf);
}
}
}
printf("%d\n", sum - dinic()); return 0;
}
费用流模型
像上文说的那样,费用流用来求解既要满足某一条件,又要使某种花费最小/大的问题。
P4009 汽车加油行驶问题
有初始装满油的汽车在网格图上从起点行驶到终点,这个汽车充满油后能行驶
因为既要满足到达终点,也要满足花费最小,所以考虑费用流。这个油量比较烦人,考虑分层图套路,从
inline void add(int x, int y, int k)
{
for (int l = 0; l < 2; ++l)
{
int tx = x + nxt[l][0], ty = y + nxt[l][1];
if (tx < 1 || ty < 1 || tx > n || ty > n) continue;
addedge(id(x, y, k), id(tx, ty, k - 1), inf, 0);
}
for (int l = 2; l < 4; ++l)
{
int tx = x + nxt[l][0], ty = y + nxt[l][1];
if (tx < 1 || ty < 1 || tx > n || ty > n) continue;
addedge(id(x, y, k), id(tx, ty, k - 1), inf, b);
}
}
int main()
{
init(); int K, a, c; scanf("%d%d%d%d%d", &n, &K, &a, &b, &c);
s = 0; t = id(n, n, K) + 1;
for (int i = 1, x; i <= n; ++i)
for (int j = 1; j <= n; ++j)
{
scanf("%d", &x);
if (x) for (int k = 0; k < K; ++k) addedge(id(i, j, k), id(i, j, K), 1, a);
for (int k = 1; k <= K; ++k) if ((x && k == K) || !x) add(i, j, k);
addedge(id(i, j, 0), id(i, j, K), 1, a + c);
}
addedge(s, id(1, 1, K), 1, 0);
for (int k = 0; k <= K; ++k) addedge(id(n, n, k), t, 1, 0);
dinic(); printf("%d\n", mincost); return 0;
}
P4012 深海机器人问题/火星探险问题
这俩差不多。在一个网格图上有一些边上有边权(或点有点权),也有一些有障碍,但重复经过只能计算一次。从起点
既要到达终点,也要边权和最大,显然考虑费用流。不过问题在于重复经过只计算一次,因为普通的费用流多次经过是算多次的。考虑连两条边,一条容量为
void getPath(int x, int d)
{
if (x == n * m) return ; int u = x + n * m, v;
if (x % m > 0) //not at the rightmost
{
v = x + 1; int i; //go right
for (i = p[u]; i + 1; i = E[i].next)
if (E[i].v == v) break;
if (E[i ^ 1].c) //there was a car
{
--E[i ^ 1].c; printf("%d 1\n", d);
getPath(v, d); return ;
}
}
if (x <= n * m - m) //not at the bottom
{
v = x + m; int i; //go down
for (i = p[u]; i + 1; i = E[i].next)
if (E[i].v == v) break;
if (E[i ^ 1].c)
{
--E[i ^ 1].c; printf("%d 0\n", d);
getPath(v, d); return ;
}
}
}
int main()
{
init(); int k; scanf("%d%d%d", &k, &m, &n); s = 0; t = 2 * n * m + 1;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) scanf("%d", &mp[i][j]);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
{
if (mp[i][j] == 1) continue;
if (mp[i][j] == 2) addedge(id(i, j), id(i, j) + n * m, 1, 1);
addedge(id(i, j), id(i, j) + n * m, inf, 0);
if (i < n && mp[i + 1][j] != 1) addedge(id(i, j) + n * m, id(i + 1, j), inf, 0);
if (j < m && mp[i][j + 1] != 1) addedge(id(i, j) + n * m, id(i, j + 1), inf, 0);
}
addedge(s, id(1, 1), k, 0); addedge(id(n, m) + n * m, t, inf, 0);
int d = dinic(); for (int i = 1; i <= d; ++i) getPath(1, i); return 0;
}
P4013 数字梯形问题
有一个数字梯形,第一行有
- 这
m 条路径互不相交。 - 这
m 条路径只允许在数字结点处相交。 - 这
m 条路径允许在任意位置相交。
既要到达底部,又要权值最大,考虑费用流。对于边上的限制,我们把点转移时走的边容量设为
int main()
{
int m, n; scanf("%d%d", &m, &n); int d = id(n, n + m - 1);
s = 0, t = d * 2 + 1;
for (int i = 1, len = m; i <= n; ++i, ++len)
for (int j = 1; j <= len; ++j) scanf("%d", &a[i][j]);
init(); for (int i = 1; i <= m; ++i) addedge(s, id(1, i), 1, 0);
for (int i = 1; i <= n + m - 1; ++i) addedge(id(n, i) + d, t, inf, 0);
for (int i = 1, len = m; i <= n; ++i, ++len)
for (int j = 1; j <= len; ++j) addedge(id(i, j), id(i, j) + d, 1, a[i][j]);
for (int i = 1, len = m; i < n; ++i, ++len)
for (int j = 1; j <= len; ++j)
addedge(id(i, j) + d, id(i + 1, j), 1, 0),
addedge(id(i, j) + d, id(i + 1, j + 1), 1, 0);
dinic(); printf("%d\n", maxcost); maxcost = 0;
init(); for (int i = 1; i <= m; ++i) addedge(s, id(1, i), 1, 0);
for (int i = 1; i <= n + m - 1; ++i) addedge(id(n, i) + d, t, inf, 0);
for (int i = 1, len = m; i <= n; ++i, ++len)
for (int j = 1; j <= len; ++j) addedge(id(i, j), id(i, j) + d, inf, a[i][j]);
for (int i = 1, len = m; i < n; ++i, ++len)
for (int j = 1; j <= len; ++j)
addedge(id(i, j) + d, id(i + 1, j), 1, 0),
addedge(id(i, j) + d, id(i + 1, j + 1), 1, 0);
dinic(); printf("%d\n", maxcost); maxcost = 0;
init(); for (int i = 1; i <= m; ++i) addedge(s, id(1, i), 1, 0);
for (int i = 1; i <= n + m - 1; ++i) addedge(id(n, i) + d, t, inf, 0);
for (int i = 1, len = m; i <= n; ++i, ++len)
for (int j = 1; j <= len; ++j) addedge(id(i, j), id(i, j) + d, inf, a[i][j]);
for (int i = 1, len = m; i < n; ++i, ++len)
for (int j = 1; j <= len; ++j)
addedge(id(i, j) + d, id(i + 1, j), inf, 0),
addedge(id(i, j) + d, id(i + 1, j + 1), inf, 0);
dinic(); printf("%d\n", maxcost); return 0;
}
P4015 运输问题
有
...我不想说那句话了,反正显然考虑费用流。这个比较板子了,从源点向仓库连容量为
int main()
{
int m, n; scanf("%d%d", &m, &n); s = 0, t = m + n + 1;
for (int i = 1; i <= m; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++)
scanf("%d", &c[i][j]);
init();
for (int i = 1; i <= m; i++) addedge(s, i, a[i], 0);
for (int i = 1; i <= n; i++) addedge(i + m, t, b[i], 0);
for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++)
addedge(i, j + m, inf, c[i][j]);
dinic(1); printf("%d\n", mincost);
init();
for (int i = 1; i <= m; i++) addedge(s, i, a[i], 0);
for (int i = 1; i <= n; i++) addedge(i + m, t, b[i], 0);
for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++)
addedge(i, j + m, inf, c[i][j]);
dinic(2); printf("%d\n", maxcost);
return 0;
}
P2770 航空路线问题
给定一张航空图,边表示两个城市之间的直通航道,不存在两个城市在同一个经线上。现在要求找出满足以下两个条件的一条经过城市最多的路径或报告无解:
- 从最西端城市出发,单向从西到东经过若干城市到达最东端城市,然后再单向从东向西途经若干城市飞回起点。
- 除起点外,每个城市都只能经过一次。
既满足绕回来,也要经过城市最多,费用流。这说是找个回路,其实我们也可以理解为找两条从起点到终点的不相交的路径。由于点的流量限制,考虑拆点,除了起点终点外每个点只能经过一次,
std::map<std::string, int> mp; std::string c1, c2, c[N]; int n, vs[N];
void findPath(int u, int d)
{
//第一次正序输出
if (!d) std::cout << c[u - n] << std::endl, vs[u] = 1;
for (int i = p[u], v; i + 1; i = E[i].next)
{
v = E[i].v; //break; 意思是第一次只找一条路径
if (v <= n && !E[i].c && (!d ? 1 : !vs[v + n])) { findPath(v + n, d); if (!d) break; }
}
//第二次倒序输出且不输出终点
if (d) std::cout << c[u - n] << std::endl;
}
int main()
{
std::ios::sync_with_stdio(0); std::cin.tie(0); std::cout.tie(0);
init(); int m; std::cin >> n >> m; s = 0; t = n * 2 + 1; int flag = 0;
for (int i = 1; i <= n; ++i)
std::cin >> c[i], mp[c[i]] = i, addedge(i, i + n, i == 1 || i == n ? 2 : 1, 1);
for (int i = 1, x, y; i <= m; ++i)
{
std::cin >> c1 >> c2; x = mp[c1]; y = mp[c2];
if (x > y) std::swap(x, y);
addedge(x + n, y, 1, 0); flag |= (x == 1 && y == n);
}
addedge(s, 1, 2, 0); addedge(n + n, t, 2, 0); int d = dinic();
if (d == 2) std::cout << maxcost - 2 << std::endl;
//特判只有起点和终点之间有边的情况
else if (d == 1 && flag) { std::cout << 2 << '\n' << c[1] << '\n' << c[n] << '\n' << c[1] << std::endl; return 0; }
else { std::cout << "No Solution!" << std::endl; return 0; }
findPath(1 + n, 0); findPath(1 + n, 1); return 0;
}
二分图匹配模型
二分图匹配就是解决匹配的问题(废话),可以是物体和特性的匹配之类的。
P2756 飞行员配对方案问题
有
二分图匹配板子题。具体建边看上面的介绍吧,这题主要重点是最后找路径,看代码吧。
int main()
{
init(); int m, n, u, v; scanf("%d%d%d%d", &m, &n, &u, &v);
while (u != -1 && v != -1)
addedge(u, v, 1), scanf("%d%d", &u, &v);
s = 0, t = n + 1;
for (int i = 1; i <= m; i++) addedge(s, i, 1);
for (int i = m + 1; i <= n; i++) addedge(i, t, 1);
printf("%d\n", dinic());
//一条边满流了就说明两边的点能组成队伍
for (int i = 0; i < cnt; i += 2)
if (E[i].c == 0 && E[i].v > m && E[i].v <= n)
printf("%d %d\n", E[i ^ 1].v, E[i].v);
return 0;
}
P2763 试题库问题
有
不那么板子的二分图匹配板子。看到物品和它特性的匹配可以想到二分图匹配。从源点向每道试题连一条容量为
int main()
{
init(); int k, n, m = 0; scanf("%d%d", &k, &n); S = 0, T = n + k + 1;
for (int i = 1; i <= n; i++) addedge(S, i, 1);
for (int i = 1, x; i <= k; i++) scanf("%d", &x), addedge(i + n, T, x), m += x;
for (int i = 1, q; i <= n; i++)
{
scanf("%d", &q);
for (int j = 1, x; j <= q; j++)
scanf("%d", &x), addedge(i, n + x, 1);
}
if (dinic() != m) printf("No Solution!"); //如果不能满流显然无解
else //对于每个 tag 找对应选的试题
for (int u = n + 1; u <= n + k; u++)
{
printf("%d: ", u - n);
for (int i = p[u]; i + 1; i = E[i].next) //满流表示选了
if (E[i ^ 1].c == 0 && E[i].v != T) printf("%d ", E[i].v); printf("\n");
}
return 0;
}
P3254 圆桌问题
有来自
比上面的题稍微不板子一点的二分图匹配板子题。代表和餐桌明显是匹配关系,考虑二分图匹配。从源点向所有单位连一条容量为
int main()
{
init(); int m, n; scanf("%d%d", &m, &n); s = 0, t = m + n + 1;
for (int i = 1, r; i <= m; i++) scanf("%d", &r), addedge(s, i, r), sum += r;
for (int i = 1, c; i <= n; i++) scanf("%d", &c), addedge(i + m, t, c);
for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++)
addedge(i, j + m, 1);
if (dinic() == sum) printf("1\n");
else { printf("0\n"); return 0; }
for (int u = 1; u <= m; u++)
{
for (int i = p[u]; i + 1; i = E[i].next)
if (E[i].f == 0) printf("%d ", E[i].v - m);
printf("\n");
}
return 0;
}
区间覆盖模型
大概就是选择一个类似线段的东西会覆盖一段区间,对区间内的点被覆盖次数有要求。大概都是把整个区间范围连成一条链,在这条链上来回连边做操作。
P3358 最长k可重区间集问题
在一条数轴上有
显然区间选的越多越好,在此限制下我们还要长度之和最大,考虑费用流。这种类型的题我们可以考虑取
int main()
{
init(); int n, k; scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &l[i], &r[i]), tmp[++tp] = l[i], tmp[++tp] = r[i];
std::sort(tmp + 1, tmp + tp + 1);
tp = std::unique(tmp + 1, tmp + tp + 1) - tmp - 1;
s = 0; t = tp + 1; addedge(s, 1, k, 0); addedge(tp, t, inf, 0);
for (int i = 1; i < tp; ++i) addedge(i, i + 1, inf, 0);
for (int i = 1; i <= n; ++i)
addedge(getid(l[i]), getid(r[i]), 1, r[i] - l[i]);
//getid 是二分找离散化后对应的值的函数
dinic(); printf("%d\n", maxcost); return 0;
}
P3357 最长k可重线段集问题
在一个平面直角坐标系上有
乍一看跟上一题差不多,反正都是只跟横坐标有关,拍扁到 x 轴上就好了。但不同于上题,这里会允许自环的存在,即
int main()
{
init(); int n, k; scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
{
scanf("%lld%lld%lld%lld", &sg[i].x1, &sg[i].y1, &sg[i].x2, &sg[i].y2);
sg[i].len = calc(sg[i]); sg[i].x1 <<= 1; sg[i].x2 <<= 1;
if (sg[i].x1 == sg[i].x2) ++sg[i].x2; else ++sg[i].x1;
tmp[++tp] = sg[i].x1; tmp[++tp] = sg[i].x2;
}
std::sort(tmp + 1, tmp + tp + 1);
tp = std::unique(tmp + 1, tmp + tp + 1) - tmp - 1;
s = 0; t = tp + 1; addedge(s, 1, k, 0); addedge(tp, t, inf, 0);
for (int i = 1; i < tp; ++i) addedge(i, i + 1, inf, 0);
for (int i = 1, x, y; i <= n; ++i)
{
x = getid(sg[i].x1), y = getid(sg[i].x2); //getid 函数意义同上
if (x > y) std::swap(x, y); addedge(x, y, 1, sg[i].len);
}
dinic(); printf("%lld\n", maxcost); return 0;
}
P3980 [NOI2008] 志愿者招募
网络流 24 题有数道不是网络流解法的题没被我加进来,所以补一道吧。
在连续的
既要需求得到满足,也要花费最少,显然费用流。发现这道题类似区间覆盖,所以我们先连起来一条
int main()
{
init(); int n, m; scanf("%d%d", &n, &m); s = 0; t = n + 2;
for (int i = 1, x; i <= n; ++i) scanf("%d", &x), addedge(i, i + 1, inf - x, 0);
for (int i = 1, s, t, c; i <= m; ++i)
{
scanf("%d%d%d", &s, &t, &c);
addedge(s, t + 1, inf, c);
}
addedge(s, 1, inf, 0); addedge(n + 1, t, inf, 0);
dinic(); printf("%lld\n", mincost); return 0;
}