Hall 定理 学习笔记
\(Hall\) 定理
一般用于求 特殊二分图(比如连边是一个区间) 上的 最大匹配 或判断是否存在 完美匹配。
对于二分图 \(G = (V, E)\),设 \(N(v)\) 表示点 \(v\) 的邻居集。
\(Hall\) 定理:设二分图 \(G\) 的两部分分别为 \(V_L, V_R\),且 \(|V_L| \leq |V_R|\)。则其存在一个大小为 \(|V_L|\) 的匹配当且仅当 \(\forall S \subseteq V_L, |S| \leq |\bigcup\limits_{v \in S} N(v)|\) 。
推论 \(1\):对于一个 \(k\) 正则二分图(每个点度数都为 \(k\),其中 \(k \geq 1\)),若其左右点数想等,那么其必有完美匹配。
推论 \(2\):设二分图的左右两部分分别为 \(V_L, V_R\),那么其最大匹配为 \(|V_L| - \max\limits_{S \subseteq V_L}(|S| - |\bigcup\limits_{v \in S}N(v)|)\)。
注意推论二中 \(S\) 可以取 \(\emptyset\)。
证明对应用的作用不大,等到以后有时间了再补。
习题
[ARC106E] Medals(Hall定理判定最大匹配,FMT)
题意:
你有 \(N\) 个朋友,他们会来你家玩,第 \(i\) 个人 \(1, \dots, A_i\) 天来玩,然后 \(A_i + 1, \dots, 2A_i\) 天不来,然后 \(2A_i + 1, \dots, 3A_i\) 天又会来,以此类推。
每天你会选一个来玩的人,给他颁个奖,如果没人来玩,你就不颁奖。
你要给每个人都颁 \(K\) 个奖,问至少需要多少天。
\(1 \leq N \leq 18, 1 \leq K \leq 10^5, 1 \leq A_i \leq 10^5\)。
分析:
首先答案不超过 \(2NK\)。原因是考虑 \(Hall\) 定理,那么将一个人拆成 \(K\) 个点,这 \(K\) 个点连边情况相同,都向能被颁奖的天连边。设人为左部点,一共有 \(NK\) 个点,注意到任意一个左部点的非空子集 \(S\) 在前 \(x\) 天都至少有 \(\frac{x}{2}\) 个邻居,那么前 \(2NK\) 天就一定能满足所有左部点都被匹配上。
知道了答案不会太大,考虑预处理出来前 \(2NK\) 天每一天能给哪些人颁奖,记它为 \(msk_i\)。我们考虑 二分答案,设需要检验前 \(mid\) 天,那么由于每个人拆成的点的邻域完全相同,因此 \(Hall\) 定理在最差情况下的左不点集合 \(S\) 一定完全包含了某几个人拆成的点,只需要求出 \(f_s\) 表示 \(s\) 集合中所有人拆成的点的邻域大小即可,也就是 \(i:1 \sim mid\) 中有多少个 \(cnt_i\) 与 \(s\) 交集非空。子集 \(dp\)(sos dp) 即可。
复杂度 \(O(N^2K + (NK + N2^N) \log (NK))\)。
CODE:
// 我怎么连这都想不到
#include<bits/stdc++.h>
using namespace std;
const int N = 19;
const int K = 1e5 + 10;
int n, k, a[N], msk[N * K * 2];
int f[1 << 18][19][2], cnt[1 << 18];
inline bool check(int x) {
memset(f, 0, sizeof f);
for(int i = 1; i <= x; i ++ ) f[msk[i]][0][0] ++;
for(int i = 1; i <= n; i ++ ) {
for(int j = 0; j < (1 << n); j ++ ) {
if((j >> i - 1) & 1) {
f[j][i][0] = f[j ^ (1 << i - 1)][i - 1][0];
f[j][i][1] = f[j ^ (1 << i - 1)][i - 1][1] + f[j][i - 1][0] + f[j][i - 1][1];
}
else {
f[j][i][0] = f[j][i - 1][0] + f[j ^ (1 << i - 1)][i - 1][0];
f[j][i][1] = f[j][i - 1][1] + f[j ^ (1 << i - 1)][i - 1][1];
}
}
}
for(int s = 0; s < (1 << n); s ++ ) {
if(f[s][n][1] < k * cnt[s]) return 0;
}
return 1;
}
int main() {
scanf("%d%d", &n, &k); int lim = 2 * n * k;
for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for(int i = 0; i < (1 << n); i ++ )
for(int j = 0; j < n; j ++ )
if((i >> j & 1)) cnt[i] ++;
for(int i = 1; i <= lim; i ++ )
for(int j = 1; j <= n; j ++ )
if(((i - 1) / a[j] + 1) & 1) msk[i] |= (1 << j - 1);
int l = 1, r = lim, mid, res;
while(l <= r) {
mid = (l + r >> 1);
if(check(mid)) res = mid, r = mid - 1;
else l = mid + 1;
}
cout << res << endl;
return 0;
}
[ARC076F] Exhausted?(Hall定理求最大匹配,线段树)
题意:
有 \(n\) 个人,\(m\) 把椅子,椅子的编号为 \(1 \sim m\)。第 \(i\) 个人可以坐编号在 \([1, l_i] \cup [r_i, m]\) 中的椅子,每个椅子最多只能坐 \(1\) 个人,问最少需要补充几把椅子(补充的椅子可以给任何人使用)。
\(1 \leq n, m \leq 2 \times 10^5, 0 \leq l_i < r_i \leq m + 1\)。
分析:
首先不难发现这是一个最大匹配问题:将人看作左部点,椅子看作右部点,每个人想两个区间连边。那么 \(n\) - 这张图的最大匹配就是答案。
发现这是特殊二分图,考虑 \(Hall\) 定理:根据推论 \(2\),最大匹配等于 \(|V_L| - \max\limits_{S \subseteq V_L}(|S| - |\bigcup\limits_{v \in S}N(v)|)\)。两边区间的并不好求,转化为求交,要求的就是:
将括号拆了,由于 \(n, m\) 是定值,只需要求 \(|S| + |\bigcap\limits_{v \in S}[l_i + 1, r_i - 1]|\) 的最大值即可。
为了方便我们令 \(l_i \gets l_i + 1,r_i \gets r_i - 1\)。那么可以枚举交区间 \([l,r ]\),然后用 \(r - l + 1 + \sum[l_i \leq l 且 r_i \geq r]\) 更新答案。
考虑怎么用数据结构维护,很经典的套路:对 \(r\) 扫描线,维护所有 \(l\) 的答案。然后会发现一个区间 \([l_i, r_i]\) 可以拆成在 \(l_i\) 将当前位置的增量 \(d\) 加 \(1\),从 \(r_i\) 处走过需要将 \([l_i, r_i]\) 区间减 \(1\)。\(r\) 扫到一个位置就让当前的 \(r\) 加上 \(d\)。
支持区间加,求全局 \(\max\),线段树维护即可。建树时将每个位置 \(i\) 的值赋为 \(-i\),每次查需要加上 \(r + 1\)。
时间复杂度 \(O(n\log n)\)。
CODE:
// 经典套路:扫描 r 维护所有 l 的答案
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 2e5 + 10;
int n, m, l[N], r[N];
vector< int > add[N], del[N];
struct SegmenTree { // 只需要支持单点加, 求全局 max 即可
int l, r, tag, mx;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define tag(x) tree[x].tag
#define mx(x) tree[x].mx
} tree[N * 4];
inline void update(int p) {mx(p) = max(mx(p << 1), mx(p << 1 | 1));}
void build(int p, int l, int r) {
l(p) = l, r(p) = r;
if(l == r) {mx(p) = -l; return ;}
int mid = (l + r >> 1);
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
update(p);
}
inline void Add(int p, int c) {
tag(p) += c, mx(p) += c;
}
inline void spread(int p) {
if(tag(p)) {
Add(p << 1, tag(p));
Add(p << 1 | 1, tag(p));
tag(p) = 0;
}
}
void change(int p, int l, int r, int c) {
if(l <= l(p) && r >= r(p)) {
Add(p, c); return ;
}
spread(p);
int mid = (l(p) + r(p) >> 1);
if(l <= mid) change(p << 1, l, r, c);
if(r > mid) change(p << 1 | 1, l, r, c);
update(p);
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ ) {
scanf("%d%d", &l[i], &r[i]);
l[i] ++, r[i] --;
if(l[i] > r[i]) continue;
add[l[i]].pb(i);
del[r[i]].pb(i);
}
build(1, 1, m); int d = 0, res = 0;
for(int i = 1; i <= m; i ++ ) {
d += (add[i].size());
change(1, i, i, d);
res = max(res, i + 1 + mx(1));
for(auto v : del[i]) {
change(1, l[v], r[v], -1);
d --;
}
}
res = max(res - m, n - m);
int ans = min(n, n - res);
cout << n - ans << endl;
return 0;
}
[AGC037D] Sorting a Grid(Hall定理求构造)
挺有意思的一道题。但是写 \(dinic\) 中的 \(bfs\) 没返回 \(0\) 调试半天。
题意:
给定一个 \(n \times m\) 的矩阵 \(A\),保证 \(A\) 内的元素为 \(1 \sim n \times m\) 的排列。
将 \(A\) 每一行元素任意排列得到 \(B\)。
将 \(B\) 每一列元素任意排列得到 \(C\)。
将 \(C\) 每一行元素任意排列得到 \(D\)。
要求 \(D_{i, j} = (i - 1) \times m + j\),请输出一组合法的 \(B, C\)。
\(1 \leq n, m \leq 100\)。
分析:
倒着考虑这个过程:
由于 \(C\) 只能排列每一行,因此 \(C\) 的第 \(i\) 行一定包含了 \([(i - 1) \times m + 1, i \times m]\) 中的所有数。
由于 \(B\) 要能到 \(C\) 并且 \(B\) 只能排列每一列,所以 \(B\) 中每一列的 \(n\) 个数应该分别为最终位置的第 \(1,\dots, n\) 行的一个数。
那么将所有数字分成 \(n\) 类:第 \(i\) 类数字为 \([(i - 1) \times m + 1, i \times m]\),那么只要让 \(B\) 的每一列都包含了每一类数中的一个,那么从 \(B\) 到 \(D\) 就是很容易构造的:只需要先把每一列排序得到 \(C\),然后再把每一行排序就能得到 \(D\)。并且能够看出如果 \(B\) 不满足每一列都拥有每一类数字的一个,那么一定不能到 \(D\)。
现在的问题就是怎样把 \(A\) 调整到一个合法的 \(B\):
我中间尝试了很多对整体建图的思路,但是发现好像连一个暴力的网络流都建不出来。那么我们摒弃整体考虑,来从左到右依次考虑每一列。
对一列怎么去构造呢?假设当前考虑到了第 \(i\) 列,前 \(i - 1\) 列都被调整过了(每一列都含有 每一类数中的一个),那么我们建出 \(n\) 个左部点代表第 \(i\) 列的 \(n\) 类数,同时建出 \(n\) 个右部点代表每一行。然后如果第 \(j\) 行当前还存在第 \(i\) 类数字没有使用过,那么就从 \(L_{i}\) 连一条指向 \(R_j\) 的有向边。
对这个二分图跑一遍最大匹配,如果最大匹配为 \(n\),那么就很容易得到一组构造了。
但是会不会最大匹配小于 \(n\)??
答案是否定的。考虑 \(Hall\) 定理,注意到存在完美匹配的充要条件是 \(\forall S \subseteq V_L, |V_L| \leq |\bigcup\limits_{v \in S}N(v)|\)。但是这里用左部点的子集不太好证,我们考虑右部点的子集:
那么注意到此时任意一个子集 \(S\) 都代表了一个行的集合,那么一共就有 \(|S| \times (m - i)\) 个数字(一行还有 \(m -i\) 个),当前每类数字都只有 \(m - i\) 个,因此这些行不可能只包含了少于 \(|S|\) 种数字,注意到 \(|\bigcup\limits_{v \in S}N(v)|\) 的含义就是这些行包含了多少种数字,因此不等式恒成立,所以一定存在完美匹配。
那么依次考虑每一列任意找一组完美匹配就是对的。
总复杂度 \(O(n^3\sqrt{n})\)。
CODE:
// 一定有解
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 110;
const int INF = 1e8;
int n, m, a[N][N], tmp[N], b[N][N], c[N][N], cnt[N][N];
int bel[N][N], L[N], R[N], S, T, ndc, ID[N * 2]; // bel[i][j] 表示第 i 列的 第 i 行数是属于哪一行
vector< int > vec[N][N];
struct edge {
int v, last, cap;
} E[N * N * 2 + 4 * N];
int head[N * 2], tot;
inline void add(int u, int v, int cap) {
E[tot] = (edge) {v, head[u], cap}; head[u] = tot ++;
E[tot] = (edge) {u, head[v], 0}; head[v] = tot ++;
}
int d[N * 2], cur[N * 2];
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0; cur[s] = head[s];
queue< int > q; q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v;
if(d[ver] == -1 && E[i].cap) {
d[ver] = d[u] + 1;
if(ver == t) return 1;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return 0;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int res = 0;
for(int i = cur[u]; ~i && limit; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit, cap));
limit -= v; res += v; E[i].cap -= v; E[i ^ 1].cap += v;
if(!v) d[ver] = -1; // 只有榨不出来一点了才删掉v。如果根据 lim 就删掉, 可能因为边的容量太小,而不是 v 后面榨不出来
}
cur[u] = i;
}
return res;
}
inline void dinic(int s, int t) {
while(bfs(s, t)) while(Find(s, t, INF)) ;
}
inline void get(int x) { // 处理第 x 列
memset(head, -1, sizeof head); tot = 0;
for(int i = 1; i <= n; i ++ ) add(S, L[i], 1), add(R[i], T, 1);
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= n; j ++ )
if(cnt[j][i] > 0) add(L[i], R[j], 1);
}
dinic(S, T);
for(int i = 1; i <= n; i ++ )
for(int j = head[L[i]]; ~j; j = E[j].last)
if(E[j].cap == 0) bel[x][i] = ID[E[j].v], cnt[ID[E[j].v]][i] --;
}
int main() {
scanf("%d%d", &n, &m);
S = ++ ndc; T = ++ ndc;
for(int i = 1; i <= n; i ++ ) L[i] = ++ ndc;
for(int i = 1; i <= n; i ++ ) R[i] = ++ ndc, ID[R[i]] = i;
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ ) {
scanf("%d", &a[i][j]);
vec[i][(a[i][j] - 1) / m + 1].pb(a[i][j]);
cnt[i][(a[i][j] - 1) / m + 1] ++;
}
}
for(int i = 1; i <= m; i ++ ) get(i);
for(int i = 1; i <= m; i ++ ) {
for(int j = 1; j <= n; j ++ ) {
b[bel[i][j]][i] = vec[bel[i][j]][j].back();
vec[bel[i][j]][j].pop_back();
}
}
for(int i = 1; i <= m; i ++ ) {
for(int j = 1; j <= n; j ++ ) tmp[j] = b[j][i];
sort(tmp + 1, tmp + n + 1);
for(int j = 1; j <= n; j ++ ) c[j][i] = tmp[j];
}
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ )
printf("%d ", b[i][j]);
puts("");
}
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= m; j ++ )
printf("%d ", c[i][j]);
puts("");
}
return 0;
}
[AGC029F] Construction of a tree(Hall定理求构造)
题意:
给定 \(n - 1\) 个点集 \(S_i\)(全集为 \(\{1,\dots,n\}\)),从每个集合选两个点连边,使得最后形成一棵树。输出方案。
\(2 \leq n \leq 10^5, \sum |S_i| \leq 2 \times 10^5\)。
分析:
自己想出来了。
任选一个点作为最终树的根,这里不妨设为 \(1\)。那么相当于你要给所有 \(2 \sim n\) 选出一条父亲边。
将 \(2 \sim n\) 分别与它们在的集合连边,那么所有点的父亲边只能在它们相连的集合里面选。也就是我们要给每个点分配一个集合用来选出它的父亲。
可以得到一个 必要条件 是建出的二分图有 完美匹配。我们用 \(dinic\) 任意求出一组 完美匹配,可以说明要么不存在合法解,要么一定能根据这组完美匹配构造出一组合法解。
过程是这样:
设树上 \(i\) 号点(\(2 \leq i \leq n\))在二分图上对应编号为 \(L_i\) 的点,第 \(i\) 个集合对应编号为 \(R_i\) 的点。
- 先把 \(1\) 所在的所有集合对应的 \(R_i\) 染上颜色 \(1\),表示这些集合已经有了可以确定在树上的父亲。
- 然后把已经染色的 \(R_i\) 的匹配点 \(L_i\) 的父亲都设置成 \(col_{L_i}\),然后把这些 \(L_i\) 的非匹配集合染上颜色 \(L_i\)。
- 重复这个过程即可。
首先如果上述过程能构造出一组解,那么这组解一定是合法的,相当于我们每次往 \(1\) 为根的树上加叶子。
如果构造不出来呢?我们发现这等价于在染色过程中存在一个时刻使得所有被染色左部点集合 \(S_L\) 和右部点集合 \(S_R\),它们互相为彼此的邻域集合。这时候就会停止染色,然后存在有点没有被染色的情况。根据 \(Hall\) 定理,一定有 \(|S_L| \geq |S_R|, |S_R| \geq |S_L|\),那么 \(S_L, S_R\) 就是一个完美匹配子集的所有点,并且由于它们互相为彼此的邻域,所以任意一个完美匹配下它们都是内部相互匹配的(如果有一个右部点不在 \(S_L\) 的匹配点中,那么 \(S_L\) 显然不可能有完美匹配)。因此一旦有这种情况,无论你怎样调整完美匹配都是无解的。
然后就做完了,复杂度 \(O(\sum|S_i|\sqrt{n})\)。
CODE:
// 好像会了:考虑除去 1 每个点,连向它所在的集合。 然后求最大匹配
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 1e5 + 10;
const int M = 2e5 + 10;
const int INF = 1e8;
vector< int > S[N];
int n, St, Tt, node[N * 2], ndc;
int col[N * 2], Link[N], idx[N * 2]; // 每个点是由那个连起来的
struct edge {
int v, last, cap;
} E[M * 2 + N * 4];
int head[N * 2], tot;
int d[N * 2], cur[N * 2];
inline void add(int u, int v, int cap) {
E[tot] = (edge) {v, head[u], cap}; head[u] = tot ++;
E[tot] = (edge) {u, head[v], 0}; head[v] = tot ++;
}
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0;
queue< int > q; q.push(s); cur[s] = head[s];
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return 1;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return 0;
}
int Find(int u, int t, int limit) {
if(u == t) return limit;
int res = 0;
for(int i = cur[u]; ~i && limit; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit, cap));
res += v, limit -= v, E[i].cap -= v, E[i ^ 1].cap += v;
if(!v) d[ver] = -1;
}
cur[u] = i;
}
return res;
}
inline void dinic(int s, int t) {
while(bfs(s, t)) while(Find(s, t, INF)) ;
}
int main() {
memset(head, -1, sizeof head);
scanf("%d", &n); ndc = n;
St = ++ ndc, Tt = ++ ndc;
for(int i = 2; i <= n; i ++ ) add(St, i, 1);
for(int i = 1; i < n; i ++ ) {
node[i] = ++ ndc; idx[node[i]] = i;
int k; scanf("%d", &k);
for(int j = 1; j <= k; j ++ ) {
int x; scanf("%d", &x);
S[i].pb(x);
if(x != 1) add(x, node[i], 1);
}
add(node[i], Tt, 1);
}
dinic(St, Tt);
for(int i = 2; i <= n; i ++ ) {
for(int j = head[i]; ~j; j = E[j].last) {
int ver = E[j].v, cap = E[j].cap;
if(!cap) Link[idx[ver]] = i;
}
}
queue< int > q;
for(int i = 1; i < n; i ++ ) {
bool f = 0;
for(auto v : S[i])
if(v == 1) {f = 1; break;}
if(f) q.push(node[i]), col[node[i]] = 1;
}
while(!q.empty()) {
int u = q.front(); q.pop();
int c = (u > n ? col[u] : u);
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(ver == St || ver == Tt) continue;
if(cap) {
if(!col[ver]) col[ver] = c, q.push(ver);
}
}
}
for(int i = 2; i <= n; i ++ )
if(!col[i]) {puts("-1"); return 0;}
for(int i = 1; i < n; i ++ )
printf("%d %d\n", Link[i], col[Link[i]]);
return 0;
}
CF103E Buying Sets(最大权闭合子图)
题意:
给定 \(n\) 个数集 \(A_i\),第 \(i\) 个集合的大小为 \(m_i\),每个集合中的数都在 \(1 \sim n\) 以内。
保证任意 \(k\) 个集合中的数字并集大小大于等于 \(k\)。每个集合有一个权值 \(w_i\),你可以任意选择一些集合,假设选了 \(k\) 个,你需要保证选出集合的数字并集大小 恰好为 \(k\),问你能选出的集合的权值和最小是多少。你可以一个集合也不选。
\(1 \leq n \leq 300, -10^6 \leq w_i \leq 10^6, 1 \leq m_i \leq n\)。
分析:
神题。让我意识到了自己在网络流最小割的应用方面还存在一些漏洞。
感觉无论怎么去考虑复杂的建图,都很难让并集大小恰好为 \(k\) 刻画出来。那么我们考虑最简单的建图。
将集合与数字分别看做两类点,每个集合向它包含的数字连一条有向边。我们想让选出的集合的邻域并集大小与选出集合数量相同,并且最小化选出集合的权值和。
我们不能建图来控制邻域数量,那么就考虑 给点赋值 来让所有不合法情况取不到。先来考虑怎么描述并集,发现只要描述成一个集合选了,它包含的元素就都得选就可以(因为一个元素只会被选一次,相当于并集里只会被算一次)。这很像 最大权闭合子图 的描述。
由于我们要求最小,因此先将所有点权取反变成求最大。怎么让所有不合法的方案都不能成为 最大 呢?考虑给所有集合对应的点的权值加上 \(inf\),给元素对应的点的权值减去 \(inf\),那么数量相等的方案中 \(+-\) 抵消,由于任意 \(k\) 个集合中的数字并集大小大于等于 \(k\),因此只剩下并集大于 \(k\) 这种不合法情况,此时会多出若干 \(-inf\) 一定不优。
求出最大权闭合子图后,别忘了取相反数。复杂度 \(O(nmf)\)。
CODE:
// 确实是网络流神题:想到最大权闭合子图就很好做。
// 最大权闭合子图的做法还需要再记一记啊
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL INF = 1e8;
const LL inf = 1e18;
const int N = 350;
int n, L[N], R[N], S, T, ndc;
LL res, all;
struct edge {
int v, last; LL cap;
} E[N * N * 2 + 4 * N];
int head[N * 2], tot;
int d[N * 2], cur[N * 2];
inline void add(int u, int v, LL cap) {
E[tot] = (edge) {v, head[u], cap}; head[u] = tot ++;
E[tot] = (edge) {u, head[v], 0}; head[v] = tot ++;
}
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0; cur[s] = head[s];
queue< int > q; q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v; LL cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return 1;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return 0;
}
LL Find(int u, int t, LL limit) {
if(u == t) return limit;
LL res = 0;
for(int i = cur[u]; ~i && limit; i = E[i].last) {
int ver = E[i].v; LL cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
LL v = Find(ver, t, min(cap, limit));
res += v; limit -= v; E[i].cap -= v; E[i ^ 1].cap += v;
if(!v) d[ver] = -1;
}
}
return res;
}
inline LL dinic(int s, int t) {
LL res = 0, flow;
while(bfs(s, t)) while(flow = Find(s, t, inf)) res += flow;
return res;
}
int main() {
memset(head, -1, sizeof head);
scanf("%d", &n);
S = ++ ndc; T = ++ ndc;
for(int i = 1; i <= n; i ++ ) L[i] = ++ ndc;
for(int i = 1; i <= n; i ++ ) R[i] = ++ ndc;
for(int i = 1; i <= n; i ++ ) {
int k; scanf("%d", &k);
for(int j = 1; j <= k; j ++ ) {
int x; scanf("%d", &x);
add(L[i], R[x], INF);
}
}
for(int i = 1; i <= n; i ++ ) {
int w; scanf("%d", &w);
add(S, L[i], INF - w);
all += (INF - w);
}
for(int i = 1; i <= n; i ++ ) {
add(R[i], T, INF);
}
res = min(res, -(all - dinic(S, T)));
cout << res << endl;
return 0;
}
CF1519F Chests and Keys(拆点,Hall定理,状态压缩, dp)
题意:
有 \(n\) 个宝箱,\(m\) 种锁,每种锁都对应一个钥匙。买第 \(i\) 把钥匙需要 \(b_i\) 元,第 \(i\) 个宝箱里有 \(a_i\) 元。
\(A\) 和 \(B\) 在玩一个游戏,\(A\) 需要先给每个宝箱放置一些锁,给第 \(i\) 个宝箱放置第 \(j\) 种锁需要 \(c_{i, j}\) 元。然后 \(B\) 可以购买一些钥匙打开这些钥匙对应的锁,只有一个宝箱上的所有锁都被打开了 \(B\) 才能获得里面的钱。如果 \(B\) 能获得 \(> 0\) 元的收益,那么 \(B\) 就获胜,否则 \(B\) 就输了。
问 \(A\) 想要获胜最少需要花费多少代价买锁,如果不可能获胜输出 \(-1\)。
\(1 \leq n, m \leq 6, 1 \leq a_i, b_i \leq 4, 1 \leq c_{i, j} \leq 10^7\)。
分析:
神仙题。
首先如果 \(\sum a_i > \sum b_i\),那么 \(A\) 必输,否则一定可以获胜。
假设已经放置好了所有的锁,设第 \(i\) 个宝箱上锁的集合为 \(R_{i}\),那么需要满足:
这看起来很像 \(Hall\) 定理的形式,我们能否转化成一个二分图匹配模型呢?
考虑 拆点:将每个宝箱拆成 \(a_i\) 个点,每种钥匙拆成 \(b_i\) 个点,每个宝箱拆成的所有点都向它对应的钥匙拆成的所有点连一条边(类似完全二分图的样子)。
那么这是一张二分图,成宝箱拆成的点为 左部点,钥匙拆成的点为 右部点。
原来需要满足的式子就可以描述成任意若干个宝箱拆成的所有点,它们的邻居集大小大于等于这些宝箱的拆点数之和。发现这等价于 任意一个左部点集合 \(S\),都满足它的右部点邻居集 \(N(S)\) 的大小大于等于它的大小。也就是存在一个大小为 \(\sum a_i\) 的匹配。
那么我们不用确定连边,只需要确定出一组"完美匹配"就可以反过来构造最少的边从而满足条件。并且发现此时的代价也是好算的:只需要知道一个宝箱拆成的点所匹配的点的种类集合就可以算答案。
所以我们依次考虑每一个宝箱拆成的 \(a_i\),那么我们只关心当前右部点每类钥匙还剩下多少个点没有匹配,假设为状态 \(S\)。所以设 \(f_{i, S}\) 表示考虑到了第 \(i\) 个宝箱,当前每类钥匙拆成的点还剩下的数量为 \(S\) 的最小花费。由于我们不关心具体的匹配情况,只关心每类匹配了多少个,因此转移就是枚举每一类钥匙匹配了多少个点 \(\{p_1,\dots, p_m\}\),满足 \(\sum p_j = a_i\),代价是非常好算的。
复杂度为 \(O(n \times (\prod (b_i + 1))^2)\)。常数非常小。
CODE:
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 7;
const int M = 16000;
int n, m, a[N], b[N], c[N][N], tot;
int f[N][M], cnt[M], all;
vector< int > g;
map< int, vector< int > > mp;
map< vector< int >, int > idx;
void dfs(int x) {
if(x == m + 1) {
idx[g] = ++ tot;
mp[tot] = g;
cnt[tot] = all;
return ;
}
for(int i = 0; i <= b[x]; i ++ ) {
g.pb(i); all += i; dfs(x + 1); g.pop_back(); all -= i;
}
}
void trans(int x, int r, int i, int cost) {
if(x == m) {
if(g[x - 1] < r) return ;
else {
g[x - 1] -= r;
if(r > 0) cost += c[i][x];
int u = idx[g]; // 转移给 f[i][u]
f[i][u] = min(f[i][u], cost);
g[x - 1] += r;
return ;
}
}
else {
for(int j = 0; j <= min(r, g[x - 1]); j ++ ) {
g[x - 1] -= j;
trans(x + 1, r - j, i, cost + (j > 0) * c[i][x]);
g[x - 1] += j;
}
}
}
int main() {
scanf("%d%d", &n, &m);
int sa = 0, sb = 0;
for(int i = 1; i <= n; i ++ ) {
scanf("%d", &a[i]);
sa += a[i];
}
for(int i = 1; i <= m; i ++ ) {
scanf("%d", &b[i]);
sb += b[i];
}
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
scanf("%d", &c[i][j]);
if(sa > sb) puts("-1");
else {
all = 0; dfs(1);
memset(f, 0x3f, sizeof f);
f[0][tot] = 0;
for(int i = 1; i <= n; i ++ ) {
for(int j = 1; j <= tot; j ++ ) {
if(f[i - 1][j] < 1e9 && cnt[j] >= a[i]) {
g = mp[j];
trans(1, a[i], i, f[i - 1][j]);
}
}
}
int res = 2e9;
for(int i = 1; i <= tot; i ++ ) {
if(cnt[i] == sb - sa) res = min(res, f[n][i]);
}
cout << res << endl;
}
return 0;
}
[CTS2023] 琪露诺的符卡交换(构造,转化, Hall定理)
题意:
一共有 \(n\) 种卡片,每种卡片恰好有 \(n\) 个。同时有 \(n\) 个人,每个人手上有 \(n\) 张卡片,第 \(i\) 个人的第 \(j\) 张卡片的种类为 \(a_{i, j}\)。保证所有人手上的卡片并恰好为所有的 \(n \times n\) 张卡片。
你需要交换一些卡片使得每个人都能拥有所有的 \(n\) 种卡片,但是你需要保证 每张卡片最多被交换一次。输出一组构造方案,或者判断无解。
\(1 \leq n \leq 200\)。
分析:
第一步比较难想,但是想到第一步就很好做了。
考虑把第 \(i\) 个人拥有卡片放在第 \(i\) 行,那么会形成一个 \(n \times n\) 的矩形 \(b_{i, j}\)。
对于所有 \(1 \leq i < j \leq n\),我们交换 \((i, j)\) 和 \((j, i)\) 位置上的卡片。那么每张卡片最多被交换一次,此时第 \(i\) 个人拥有的卡片变成了第 \(i\) 列的所有卡片。
那么只需要我们将每一行的卡片按照一定顺序排列,满足 每一列都是一个 \(1 \sim n\) 的排列,就可以合法的构造出一组解了。
然后惊奇的发现,这不就是上面的 [AGC037D] Sorting a Grid 吗!!!结论是一定能构造一组合法顺序,方法就是从最左边依次确定所有列,然后每次建二分图求出一组合法匹配即可。
用 \(dinic\) 求二分图最大匹配,复杂度 \(O(n^{3.5})\)。
CODE:
// 只需要转化一步,就能变成AGC037D
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
const int N = 205;
const int INF = 1e8;
int n, cnt[N][N], a[N][N], bel[N];
vector< int > idx[N][N];
int R[N], ndc, S, T, ID[N * 2];
struct edge {
int v, last, cap;
} E[N * N * 2 + 4 * N];
int tot, head[N * 2];
int d[N * 2], cur[N * 2];
inline void add(int u, int v, int cap) {
E[tot] = (edge) {v, head[u], cap}; head[u] = tot ++;
E[tot] = (edge) {u, head[v], 0}; head[v] = tot ++;
}
inline bool bfs(int s, int t) {
memset(d, -1, sizeof d); d[s] = 0; cur[s] = head[s];
queue< int > q; q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
for(int i = head[u]; ~i; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == -1 && cap) {
d[ver] = d[u] + 1;
if(ver == t) return 1;
cur[ver] = head[ver];
q.push(ver);
}
}
}
return 0;
}
inline int Find(int u, int t, int limit) {
if(u == t) return limit;
int res = 0;
for(int i = cur[u]; ~i && limit; i = E[i].last) {
int ver = E[i].v, cap = E[i].cap;
if(d[ver] == d[u] + 1 && cap) {
int v = Find(ver, t, min(limit, cap));
res += v, limit -= v, E[i].cap -= v, E[i ^ 1].cap += v;
if(!v) d[ver] = -1;
}
cur[u] = i;
}
return res;
}
inline void dinic(int s, int t) {
while(bfs(s, t)) while(Find(s, t, INF)) ;
}
inline void ope(int x) {
memset(head, -1, sizeof head); tot = 0;
ndc = n; S = ++ ndc, T = ++ ndc;
for(int i = 1; i <= n; i ++ ) add(S, i, 1);
for(int i = 1; i <= n; i ++ ) {
R[i] = ++ ndc; ID[ndc] = i;
add(R[i], T, 1);
for(int j = 1; j <= n; j ++ )
if(cnt[i][j]) {
add(j, R[i], 1);
}
}
dinic(S, T);
for(int i = 1; i <= n; i ++ ) {
for(int j = head[i]; ~j; j = E[j].last) {
int ver = E[j].v, cap = E[j].cap;
if(!cap) {
a[ID[ver]][x] = idx[ID[ver]][i].back();
idx[ID[ver]][i].pop_back(); cnt[ID[ver]][i] --;
}
}
}
}
inline void solve() {
scanf("%d", &n); memset(cnt, 0, sizeof cnt);
for(int i = 1; i <= n; i ++ ) for(int j = 1; j <= n; j ++ ) idx[i][j].clear();
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= n; j ++ ) {
int x; scanf("%d", &x);
idx[i][x].pb(j);
cnt[i][x] ++;
}
for(int i = 1; i <= n; i ++ ) ope(i); // 依次考虑每一列
printf("%d\n", n * (n - 1) / 2);
for(int i = 1; i <= n; i ++ )
for(int j = i + 1; j <= n; j ++ )
printf("%d %d %d %d\n", i, a[i][j], j, a[j][i]);
}
int main() {
int TT; scanf("%d", &TT);
while(TT -- ) solve();
return 0;
}
[JOISC 2022] 蚂蚁与方糖(ddp, Hall定理求最大匹配)
题意:
给定 \(q, L\),有一个数轴,初始数轴上一个点也没有。
有 \(q\) 次操作,每次操作给定 \(T_i, X_i, A_i\),表示在数轴上 \(x = X_i\) 的位置添加 \(A_i\) 个点。若 \(T_i = 1\) 则添加点为 红点,否则为 蓝点。
一个红点和一个蓝点在数轴上的距离 \(\leq L\) 时它们才可以 匹配。一个点最多匹配一次。你需要在每次操作后求出红蓝点的最大匹配数。
\(1 \leq q \leq 5 \times 10^5, 1 \leq L \leq 10^9, 0 \leq X_i \leq 10^9, 1 \leq A_i \leq 10^9\)。
分析:
详见这里。
总结
- \(Hall\) 定理最直接的应用是在 特殊二分图 上求最大匹配 或 判断完美匹配。多和 线段树 结合。
- \(Hall\) 定理也可以用来证明一些 构造题 的 解存在的充要性。对于这种构造题来说,一般都是先尝试转化成每步都是一个二分图匹配的模型。然后在用 \(Hall\) 定理证明拆成每步构造对解的构造就是充要的。
- 对于一些式子列出了像 \(Hall\) 定理的,尝试通过 拆点 的方式转化成二分图完美匹配的问题,这样就可以进一步简化限制要求,降低复杂度。

浙公网安备 33010602011771号