网络流与二分图(deserted)
本篇博客已被废弃,新版详见 网络流,二分图与图的匹配。
0. Change log
- 2021.12.5:更换模板代码。新增二分图部分。
- 2022.1.11:重构网络流部分。新增网络流的应用与模型。
- 2022.1.12~1.13:新增上下界网络流部分。
1. 网络流
网络流的核心在于 建图:建图是精髓,建图是人类智慧。网络流的建图方法从某种程度上刻画了 贪心问题的内在性质,从而简便地支持了 反悔,不需要我们为每道贪心问题都寻找一个反悔策略。
1.1. 基本定义
一个网络是一张有向图
网络的可行流分为有源汇(通常用
- 首先给出定义,流函数
是从二元 有序对 向实数集 的映射,其中 。 满足 容量限制: :每条边的流量不能超过容量。若 ,则称边 满流。 具有 斜对称 性质: : 有 的流量,也可以说 有 的流量。 具有 流量守恒 性质:除源汇点外(无源汇网络流则不存在源汇点),从每个结点流入和流出的流量相等,即 :每个结点 不储存流量,流进去多少就流出来多少。
-
对于 有源汇 网络,根据斜对称和容量守恒性质,可以得到
,此时这个相等的和称为当前流 的 流量。 -
定义一种流在网络
上的 残量网络 为容量函数 的网络,根据容量限制,我们有 。若 ,则视边 在残量网络上不存在,即 。换句话说,将每条边的容量减去流量后,删去满流边即可得到残量网络。 -
定义 增广路
是 残量网络 上从 源点 到 汇点 的一条路径。一般来说,无源汇网络流不讨论增广路。 -
将
分成 互不相交 的两个点集 ,其中 , ,这种 点的划分方式 叫做 割。定义割的 容量 为 ,流量 为 $\sum\limits_{u \in A}\sum_\limits{v \in B} f(u, v)。若 所属点集不同,则称有向边 为 割边。
接下来的讨论范围将限制于 有源汇 网络流,对于 无源汇 网络流,见更下方的无源汇网络流部分。
1.2. 网络最大流:EK 与 Dinic
网络最大流相关算法,最著名的是 Edmonds-Karp 和 Dinic。对于更高级的 SAP / ISAP / HLPP,此处不做介绍。
简单地说,给定一张网络
1.2.1. 增广
接下来要介绍的两个算法均使用了 不断寻找增广路 的思想。具体地,找到残量网络
在为当前边
上述操作称为一次 增广。
关于增广,有一个常用技巧:网络流建图一般使用链式前向星。我们会将每条边与它的反向边按编号 连续存储,编号分别记为
1.2.2. 最大流最小割定理
在介绍 EK 和 Dinic 之前,我们还需要一个贯穿网络流始终的最核心,最基本的结论:最大流等于最小割。
-
任何一组流的流量 不大于 任何一组割的容量:考虑一个可行流
,其流量等于任意一种割的割边流量之和。考虑每单位流量,设其经过
的割边 的次数为 ,经过 的割边次数为 ,显然有 ,否则就不可能从 流到 了。根据斜对称性质与割的流量的定义,每单位流量对割边流量之和的贡献为
,因此网络总流量就等于割边流量之和。根据容量限制,推出流量 割的容量。 -
存在一组流的流量 等于 一组割的容量:我们断言 最大流存在,显然此时 残量网络不连通(若连通可以继续增广,与最大流的最大性矛盾),这为我们自然地提供了一组割,使得其容量等于流量,即当前可行流的流量。
综上,我们证明了最大流等于最小割。
1.2.3. Edmonds-Karp 算法
Edmonds-Karp 算法的核心思想是不断找 长度最小的增广路 进行增广,通过 bfs 实现。为此,我们记录流向每个点的边的编号,然后从汇点
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v, int w) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, S, T, fr[N], vis[N], fl[N]; ll ans;
int main(){
cin >> n >> m >> S >> T;
for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w);
while(1) {
queue <int> q; mem(fl, 0, N), mem(vis, 0, N);
fl[S] = inf, vis[S] = 1, q.push(S);
while(!q.empty()) { // 整个 BFS 过程
int t = q.front(); q.pop();
for(int i = hd[t]; i; i = nxt[i]) {
int it = to[i];
if(!lim[i] || vis[it]) continue; // 如果剩余流量为 0,在残量网络上不存在这条边,不能走
vis[it] = 1, fl[it] = min(fl[t], lim[i]); // 记录流量
fr[it] = i ^ 1, q.push(it); // 记录流向每个点的边
}
} if(!fl[T]) break;
int p = T; ans += fl[T];
while(p != S) lim[fr[p]] += fl[t], lim[fr[p] ^ 1] -= fl[t], p = to[fr[p]]; // 从 T 一路反推到 S,并更新每条边的剩余流量
} cout << ans << endl;
}
时间复杂度证明摘自 ycx 的博客。我们需要这样一条引理:每次增广后残量网络上
考虑反证法,假设存在结点
若
设关键边
1.2.4. Dinic 算法
Dinic 算法的核心思想是 分层图 以及 相邻层之间增广,通过 bfs 和 dfs 实现:首先宽搜给图分层,分层完毕后从
Dinic 算法有重要的 当前弧优化:增广时,容量已经等于流量的边无用,可以直接跳过,不需要每次深搜到同一个点时都从邻接表头开始遍历。为此,记录从每个点出发第一条没有流满的边,称为 当前弧。每次深搜到该结点就从当前弧开始增广。注意,每次多路增广前每个点的 当前弧应初始化设为邻接表头 ,因为并非一旦流量等于容量,这条边就永远无用,反向边流量的增加会让它重新出现在残量网络中。
当前弧优化后的 Dinic 时间复杂度
关于当前弧优化的注意事项:
for(int i = cur[u]; res && i; i = nxt[i]) {
cur[u] = i;
// do something
}
上述代码不可以写成:
for(int &i = cur[u]; res && i; i = nxt[i]) {
// do something
}
因为若 res
变成
另一种解决方法是在循环末尾判断 if(!res) return flow;
。总之,在写当前弧优化时千万注意 不能跳过没有流满的边。模板题 P3381 网络最大流 代码如下:
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1];
void add(int u, int v, int w) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0;
} int n, m, s, t, dis[N], cur[N]; ll ans;
ll dfs(int u, ll res) {
if(u == t || !res) return res; ll flow = 0;
for(int i = cur[u]; res && i; i = nxt[i]) {
int it = to[i], c = min(res, (ll)lim[i]); cur[u] = i;
if(c && dis[u] + 1 == dis[it]) { // 仅在相邻两层之间增广
ll k = dfs(it, c);
flow += k, res -= k, lim[i] -= k, lim[i ^ 1] += k;
}
} return dis[u] = flow ? dis[u] : 0, flow;
}
int main(){
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w);
while(1) {
queue <int> q; mem(dis, 0x3f, N), dis[s] = 0, q.push(s);
while(!q.empty()) { // bfs 分层图
int t = q.front(); q.pop();
for(int i = hd[t]; i; i = nxt[i])
if(lim[i] && dis[to[i]] > 1e9)
dis[to[i]] = dis[t] + 1, q.push(to[i]);
} if(dis[t] > 1e9) break; cpy(cur, hd, N), ans += dfs(s, 1e18);
} cout << ans << endl;
}
1.3. 无负环的费用流:SSP 与 Primal-Dual
费用流一般指 最小费用最大流(Minimum cost maximum flow,简称 MCMF)。
相较于一般的网络最大流,在原有网络
通俗地说,
1.3.1. 连续最短路算法
连续最短路算法 Successive shortest path,简称 SSP。顾名思义,这一算法的核心思想是每次找到 长度最短的增广路 进行增广,且仅在网络 无负环 时能得到正确答案。
SSP 算法有两种实现,一种基于 EK 算法,另一种基于 Dinic 算法。这两种实现均要求将 bfs 换成 SPFA(每条边的权值即
时间复杂度
正确性证明见下方 ycx 的博客。这里给出重要结论:只要初始网络无负环,则 任意时刻残量网络无负环。
注意,SPFA 在队首为 break
,因为第一次取出 dis[T]
不一定取得最短路。
int cnt = 1, hd[N], nxt[M << 1], to[M << 1], lim[M << 1], cst[M << 1];
void add(int u, int v, int w, int c) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
} int MCMF(int T) {
int flow = 0, cost = 0;
while(1) {
queue <int> q; static int dis[N], vis[N], fr[N];
mem(dis, 0x3f, T + 1), mem(vis, 0, T + 1), dis[0] = 0, q.push(0);
while(!q.empty()) { // SPFA
int t = q.front(); q.pop(), vis[t] = 0;
for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = dis[t] + cst[i];
if(d < dis[it]) {
dis[it] = d, fr[it] = i;
if(!vis[it]) vis[it] = 1, q.push(it);
}
}
} if(dis[T] > 1e9) break; int fl = 1e9;
for(int u = T; u; u = to[fr[u] ^ 1]) cmin(fl, lim[fr[u]]);
for(int u = T; u; u = to[fr[u] ^ 1]) lim[fr[u]] -= fl, lim[fr[u] ^ 1] += fl; // Edmonds-Karp
flow += fl, cost += 1ll * dis[T] * fl;
} return cost;
}
1.3.2. Primal-Dual 原始对偶算法
建议首先学习 Johnson 全源最短路算法。
和 SSP 一样,Primal-Dual 原始对偶算法也仅适用于 无负环 的网络中,其核心思想为:我们为每个点赋一个 势
找到增广路后,每次增广都会改变残量网络的形态。为此,我们只需用每次增广时 Dijkstra 跑出来的最短路加在
- 如果
在增广路上,有 。由于 ,所以 ,即 反边边权为 。 - 对于原有的边,我们有
,即 ,原边权仍然非负。
实际表现方面,Primal-Dual 相较于 SSP 并没有很大的优势,大概率是因为 SPFA 本身已经够快了,且堆优化的 Dijkstra 常数较大。同时,在实际意义的限制下,很难建出一些能够卡掉 SPFA 的网络。
const int N = 5e3 + 5;
const int M = 5e4 + 5;
int cnt = 1, hd[N], to[M << 1], nxt[M << 1], lim[M << 1], cst[M << 1];
void add(int u, int v, int w, int c) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
} int n, m, s, t, h[N], vis[N], dis[N], fr[N], flow, cost;
int main(){
cin >> n >> m >> s >> t;
for(int i = 1, u, v, w, c; i <= m; i++) cin >> u >> v >> w >> c, add(u, v, w, c);
queue <int> q; mem(h, 0x3f, N), h[s] = 0, q.push(s);
while(!q.empty()) {
int t = q.front(); q.pop(); vis[t] = 0;
for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = h[t] + cst[i];
if(d < h[it]) {h[it] = d; if(!vis[it]) vis[it] = 1, q.push(it);}
}
} while(1) {
priority_queue <pii, vector <pii>, greater <pii>> q;
mem(dis, 0x3f, N), mem(vis, 0, N), q.push({dis[s] = 0, s});
while(!q.empty()) {
pii t = q.top(); q.pop();
if(vis[t.se]) continue; vis[t.se] = 1;
for(int i = hd[t.se]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = t.fi + cst[i] + h[t.se] - h[it];
if(d < dis[it]) fr[it] = i, q.push({dis[it] = d, it});
}
} if(dis[t] > 1e9) break;
int c = (1ll << 31) - 1;
for(int i = 1; i <= n; i++) h[i] += dis[i];
for(int i = t; i != s; i = to[fr[i] ^ 1]) cmin(c, lim[fr[i]]);
for(int i = t; i != s; i = to[fr[i] ^ 1]) lim[fr[i]] -= c, lim[fr[i] ^ 1] += c;
flow += c, cost += h[t] * c;
} cout << flow << " " << cost << "\n";
}
*1.4. 关于网络流的理解
在费用流的过程中,我们的策略是 贪心找到长度最短的增广路 并进行增广,但当前决策并不一定最优,因此需要为反边添加流量,表示 支持反悔。因此,网络流就是可反悔贪心,而运用上下界网络流等技巧可以很方便地处理问题的一些限制。
更一般的,网络流是一种特殊的贪心,它们之间可以相互转化:对于具有特定增广模式(网络具有某种性质)的网络流,可以从贪心的角度思考,从而使用数据结构维护。而大部分贪心题目也可以通过网络流解释。
换句话说,网络流 将贪心用图的形式刻画,而解决网络流问题的算法与某种支持反悔的贪心策略相对应,这使得我们不需要为每道贪心题目都寻找某种反悔策略,相反,建出图后就是一遍最大流或者费用流的事儿了。
网络流相关问题,关键在于发现 题目的每一种方案与一种流或割对应。例如在 P2057 [SHOI2007]善意的投票 一题中,直接将每个小朋友拆点不可行,因为无法考虑到他与他的朋友意见不一致时的贡献。为此,我们应用 最小割等于最大流 这一结论,考虑如何用一组割来表示一种意见方案,最终得到解法:每割掉一条边都表示付出
1.5. 上下界网络流
上下界网络流相较于原始网络
1.5.1. 无源汇可行流
无源汇上下界可行流是上下界网络流的基础。我们需要为一张无源汇的网络寻找一个流函数
解决该问题的核心思想,是 先满足流量下界限制,再尝试调整。具体地,我们首先让每条边
这启发我们新建一个网络
若
1.5.2. 有源汇可行流
从
1.5.3. 有源汇最大流
有源汇上下界最大流算法基于一个非常有用的结论:给定 任意 一组 可行流,对其运行最大流算法,我们总能得到正确的最大流。这是因为最大流算法本身 支持撤销,即退流操作。所以,无论初始的流函数
因此,我们考虑先求出任意一组可行流,再进行 调整:首先对网络
接下来进行调整:根据结论,我们只需要以
易错点 1:调整的整个过程在
易错点 2:可行流流量是
1.5.4. 有源汇最小流
根据
1.5.5. 有源汇费用流
只需将最大流算法换成费用流即可。所有
1.6. 应用与模型
1.6.1. 最小割点
通常情况下题目要求的最小割是 最小割边,但如果问题变成:删去每个 点
应用网络流的核心技巧:点边转化,将每个点拆成入点
1.6.2. 集合划分模型
其中
建模:将
因此,对上述网络
- 当
出现负值时,普通的最大流不能得到正确结果,因为我们无法解决 容量为负的最大流问题。考虑将 同时加上 ,最后再在求出的最小割中减掉 。这是因为 必须在选或不选 任选一种 方案,所以同时为 加上 对最小割的影响为 。体现在图上即 ,为了使 不连通,必须 至少割掉一条边。要使代价最小化,我们不会同时割掉两条边,所以 恰好割掉一条边。 - 当
出现负值时,除非所有 均为负值且要求代价最大化,此时所有边权取反,否则问题不可做。取反可以通过 代价和贡献的转化 理解,即若代价为 ,则贡献为 ,一般我们希望 最大化贡献,最小化代价。
1.6.3. 最大权闭合子图
一张 有向图
最大权闭合子图问题,即每个点
考虑 集合划分模型,对于每个结点,我们可以将其划分到 选 或 不选 的集合当中,体现在建图上即
由于最大割是 NP-Hard 问题,所以考虑权值取相反数求最小割。对于
综上,我们得到了求解最大权闭合子图的一般算法:对于
另一种理解方式是我们首先全选所有正权点,然后减掉代价。删掉正权点的代价为
1.6.4. 有负环的费用流
考虑运用上下界网络流将负权边强制满流,并令反边
const int N = 200 + 5;
const int M = 3e4 + 5;
int n, m, s, t;
struct Graph {
int cnt = 1, hd[N], nxt[M << 1], to[M << 1], lim[M << 1], cst[M << 1];
void add(int u, int v, int w, int c) {
nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v, lim[cnt] = w, cst[cnt] = c;
nxt[++cnt] = hd[v], hd[v] = cnt, to[cnt] = u, lim[cnt] = 0, cst[cnt] = -c;
} int S, T;
pii mincost(int _S, int _T) {
int flow = 0, cost = 0; S = _S, T = _T;
while(1) {
queue <int> q; static int fr[N], dis[N], vis[N];
mem(dis, 0x3f, N), mem(vis, 0, N), q.push(S), dis[S] = 0;
while(!q.empty()) {
int t = q.front(); q.pop(), vis[t] = 0;
for(int i = hd[t]; i; i = nxt[i]) if(lim[i]) {
int it = to[i], d = dis[t] + cst[i];
if(d < dis[it]) {
dis[it] = d, fr[it] = i;
if(!vis[it]) vis[it] = 1, q.push(it);
}
}
} if(dis[T] > 1e9) return make_pair(flow, cost);
int fl = 1e9;
for(int i = T; i != S; i = to[fr[i] ^ 1]) cmin(fl, lim[fr[i]]);
for(int i = T; i != S; i = to[fr[i] ^ 1]) lim[fr[i]] -= fl, lim[fr[i] ^ 1] += fl;
cost += fl * dis[T], flow += fl;
}
}
};
struct Bound {
int m, u[M], v[M], lo[M], hi[M], val[M]; Graph G;
void add(int a, int b, int c, int d) {
if(d < 0)
u[++m] = a, v[m] = b, lo[m] = c, hi[m] = c, val[m] = d,
u[++m] = b, v[m] = a, lo[m] = 0, hi[m] = c, val[m] = -d;
else u[++m] = a, v[m] = b, lo[m] = 0, hi[m] = c, val[m] = d;
} pii mincost(int S, int T) {
static int w[N], dt = 0, flow, cost = 0;
for(int i = 1; i <= m; i++) {
w[u[i]] -= lo[i], w[v[i]] += lo[i], cost += lo[i] * val[i];
G.add(u[i], v[i], hi[i] - lo[i], val[i]);
} int SS = n + 1, TT = n + 2;
for(int i = 1; i <= n; i++)
if(w[i] > 0) dt += w[i], G.add(SS, i, w[i], 0);
else G.add(i, TT, -w[i], 0);
G.add(T, S, 1e9, 0);
pii res = G.mincost(SS, TT); cost += res.se, flow += G.lim[G.hd[S]];
G.hd[S] = G.nxt[G.hd[S]], G.hd[T] = G.nxt[G.hd[T]];
res = G.mincost(S, T), cost += res.se, flow += res.fi;
return make_pair(flow, cost);
}
} f;
int main() {
cin >> n >> m >> s >> t;
for(int i = 1, a, b, c, d; i <= m; i++)
cin >> a >> b >> c >> d, f.add(a, b, c, d);
pii ans = f.mincost(s, t);
cout << ans.fi << " " << ans.se << endl;
return flush(), 0;
}
1.6.5. 最大费用最大流
将所有权值取相反数转化为最小费用最大流,根据上一部分的技巧,求解(可能)有负环的费用流。
1.7. 例题
现在你已经对网络流的基本原理有了一定了解,就让我们来看一看下面这个简单的例子,把我们刚刚学到的知识运用到实践中吧。
*I. P4249 [WC2007] 剪刀石头布
注意到对于任意三个不相同的点
本题的核心技巧:拆组合数贡献。将
*II. P1251 餐巾计划问题
网络流相关问题,一个十分重要的技巧是拆点。如果把每天仅看成一个点,我们无法区分干净的餐巾和脏餐巾,即干净的餐巾用完后还能作为脏餐巾继续流,而不是直接流到汇点去了。
因此考虑拆点,每天晚上得到
III. P2936 [USACO09JAN] Total Flow S
最大流模板题。
*IV. P1345 [USACO5.4] 奶牛的电信 Telecowmunication
最小割点模板题。
*V. P4016 负载平衡问题
考虑求出平均值
*VI. P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查
好题,分析见 1.3. 部分。
VII. P2045 方格取数加强版
考虑借鉴最小割点的 点边转化 思想,将每个点
VIII. P2805 [NOI2009] 植物大战僵尸
对于每个植物,从它的攻击位置向它连边,表示若选择攻击位置,则必须选择该植物。对反图拓扑排序(因为环以及能到达环的点都不可以选择)后就是最大权闭合子图问题了。
IX. P2762 太空飞行计划问题
最大权闭合子图板子题。
X. P3410 拍照
草。
XI. P2604 [ZJOI2010]网络扩容
对于第一问跑一边最大流板子,第二问为每条边新建与其平行且容量为
XI. CF1082G Petya and Graph
如果一条边被选,那么其两个端点也必须选,这是最大权闭合子图模型。
XII. P5192 Zoj3229 Shoot the Bullet | 东方文花帖
对于每一天
XIII. P4843 清理雪道
有源汇上下界最小流板题:从
XIV. P4553 80 人环游世界
有源汇上下界费用流板题。
XV. P2754 [CTSC1999]家园 / 星际转移问题
考虑建出分层图,在分层图上求解网络最大流。答案每增加
1.8. 参考资料与博客
- ycx:网络流与二分图 学习笔记。
- OI wiki:最大流。
2. 二分图
2.1. 定义,判定与性质
二分图的定义:设无向图
一张图是二分图的 充分必要条件 是 不存在奇环。必要性:从任意一个点出发,必须经过偶数条边才能回到这个点;充分性:对不存在奇环的图 黑白染色可以得到一组划分
通过这一性质,我们得到在线性时间
2.2. 二分图匹配
二分图的匹配的定义如下:给定一张二分图
2.2.1. 最大匹配
二分图最大匹配问题即求出选出边集
- 边导出子图:选出若干条边,以及这些边所连接的所有顶点组成的图。
- 点导出子图:选出若干个点,以及两端都在该点集的所有边组成的图。
求解该问题的经典算法是匈牙利算法,见 Part 3.1 匈牙利算法。我们尝试用网络流解决该问题:一个结点最多与一条边相连,即 结点度数
使用 Dinic 求解二分图最大匹配,时间复杂度是优秀的
2.2.2. 最大多重匹配
多重匹配指任意一个结点
2.2.3. 带权最大匹配
对于最小权最大匹配,将最大流算法换成最小费用最大流;
对于最大权最大匹配,将最大流算法换成最大费用最大流。由于图中不可能出现正环,所以只需要朴素地权值取反求最小费用最大流。
对于最大权 完美 匹配,有专门的 KM 算法 解决这类问题。详见 Part 3.2 KM 算法。
2.3. 二分图相关问题
2.3.1. 最小点覆盖集
二分图点覆盖集定义如下:给定一张二分图
二分图的点覆盖集与割相对应:建出我们求最大匹配时的图,我们钦定割仅在两端取到。若在两部点之间的边取到,可以调整至两端,即割
上述构造是一组合法的点覆盖集,反证法可证:若存在边
应用:对于每条限制
2.3.2. 最大独立集
二分图的独立集定义如下:给定一张二分图
考虑二分图
2.3.3. 最大团
二分图的团定义如下:给定一张二分图
建出
使
如果仅仅在原图上面跑最大流,我们无法控制二分图某部点被割掉的点的数量,考虑如何在 不影响最大匹配的前提下 为每条边附上权值,使得我们求出的最小割 尽量割
不妨将
2.5. 应用与模型
2.5.1. DAG 最小路径覆盖
给定一张 DAG
最小 不相交 路径覆盖:考虑一个点是否有入边和出边。对于
最小相交路径覆盖:
2.6. 例题
*I. [BZOJ2162] 男生女生
本题可以看做割裂的两部分,一部分是求二分图使某部点尽量多的最大团,另一部分则需要用到二项式反演,见 反演与狄利克雷卷积 Part 2. 二项式反演。
II. P2055 [ZJOI2009] 假期的宿舍
非常裸的二分图最大匹配。对于右部点,所有回家的人向汇点连边,对于左部点,所有要在学校住下的人从源点连边,两部点之间认识的人连边,求最大匹配是否与左部点个数相等即可。
III. P3701 主主树
对于能左边能打败右边的,连一条容量为 YYY
给 J
+1s 不会让自己 -1s,因此每个 J
的生命值还要加上对应的 YYY
个数。带权最大匹配对
IV. P2756 飞行员配对方案问题
二分图匹配模板题。对于输出方案,只需找到所有满流(即剩余流量为
V. P6268 [SHOI2002] 舞会
二分图最大独立集模板题,答案即
VI. P7368 [USACO05NOV] Asteroids G
一个小行星被消除当且仅当它所在的行或列被选中,考虑建出二分图,则题目转化为二分图最小点覆盖集,直接跑最大匹配即可。
VII. P1231 教辅的组成
三分图最大匹配(大雾)。注意点:为限制中间点的度数为
VIII. P2774 方格取数问题
二分图最大带权独立集问题,转化为最大带权匹配,直接做就行了。
IX. P2765 魔术球问题
显然的 DAG 最小路径覆盖。本题还可以贪心,感觉很神。
X. P3254 圆桌问题
二分图多重匹配板子题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现