二分图相关
%
零、定义:
- 二分图:
二分图是一张图
- 匹配:
一张图的匹配定义为一个原图的一个子图,且满足任意两条边均没有公共端点。一个匹配的大小定义为其中边的个数,一个图的最大匹配定义为匹配的大小最大的一个匹配。特殊的,对于一个匹配且该匹配完全包含左部点或右部点时,我们称该匹配是完美匹配,完美匹配一定是原图中的最大匹配。
- 点覆盖:
一张图的覆盖定义为原图中点集的一个子集,满足原图中每条边均有一个端点在该点集中。一个覆盖的大小定义为点集的大小,最小点覆盖就是原图中最小的覆盖。
- 独立集:
一张图的独立集定义为原图中点集的一个子集,满足原图中每条边的端点至少有一个不在该点集中。一个覆盖的大小定义为点集的大小,最大独立集就是原图中最大的独立集。
一、二分图最大匹配:
二分图中的最大匹配求法较一般图是简单的,我们接下来探究对于二分图如何求最大匹配。
我们要引入匈牙利算法。依次考虑左部点并进行匹配,假设我们考虑到第
注:其中我们称该过程中走的边为增广路或交替路,因为这条路径上的边是匹配边和非匹配边交替的。
匈牙利算法看起来相当暴力,实际上,它的时间复杂度为
code
bool dfs(int u){ for(int i = 1; i <= n; i++){ if(!vis[i] && G[u][i]){ vis[i] = true; if(!match[i] || dfs(match[i])){ match[i] = u; return true; } } } return false; }
二、二分图最小点覆盖:
对于求解二分图中的最小点覆盖问题,我们可以考虑将其转化为最大匹配问题求解。
König定理:二分图中,最大匹配等于最小点覆盖。
- 证明:
首先我们可以知道最大匹配
接下来我们考虑构造出一种方案取到下界。具体方案是从未匹配的右部点出发,走交替路并且标记经过的点,最后选择左边标记的点和右边未标记的点。画图容易发现每个点恰好是一条匹配边的端点,原因大概是从左向右走的一定是匹配边,因此左边标记过的点如果是匹配点,一定会将所在匹配边的右部点标记而不会选进点集中。而右边未标记的点一定是匹配点,于是恰好覆盖了所有匹配边且恰好没有重复。
对于完全覆盖的证明,我们考虑使用反证法,假设有一条边没有被覆盖到,则它对应的左部点一定是未标记过,而右部点是标记过的。进一步的,该边不可能是未匹配边,但是若该边是匹配边,则右部点只可能是由左部点走过来,但是这样左部点就标记了,推出矛盾,从而定理得证。
三、二分图最大独立集:
与最小点覆盖问题类似,我们依旧考虑转化问题。
实际上,我们可以考虑将点覆盖与独立集建立映射关系。具体的,一个独立集的补集即为原图的一个点覆盖。这是因为独立集中的点两两之间没有任何边,于是剩下的点就会覆盖所有边。
从而最大独立集大小 = 点数 - 最小点覆盖。
四、Hall 定理:
Hall 定理是关于二分图完美匹配的强定理,适用于判定的情况。
Hall 定理: 对于一个二分图,存在完美匹配的充分必要条件是对于任意点集
,它的邻域 满足 。
- 证明:
必要性是显然的,这里证明充分性。
还是考虑使用反证法,设这个图找完最大匹配后还有至少一个未匹配点
-
若
是非匹配点,可以匹配使得匹配数加一,矛盾。 -
若
是匹配点,则一定会有一个点匹配点 与 相连。
由题设,一定还会有一个点
-
若
是非匹配点,可以匹配使得匹配数加一,矛盾。 -
若
是匹配点,则一定会有一个点匹配点 与 相连。
同理会有一个点
五、例题:
1.CF981F Round Marriage
看到最小化最大值,考虑二分答案
我们将距离小于等于
考虑反证法证明。若不存在一个连续的区间且满足差
设对于
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e6 + 10, INF = 1e18; int n, L, a[N], b[N]; bool check(int d){ int mn = INF; for(int i = n + 1; i <= 3 * n; i++){ int L = lower_bound(b + 1, b + 4 * n + 1, a[i] - d) - b, R = upper_bound(b + 1, b + 4 * n + 1, a[i] + d) - b - 1; // if(R - L >= n) L = R - n + 1; // cout << i << " " << d << " " << L << " " << R << "\n"; if(i - R > mn) return false; mn = min(mn, i - L); } return true; } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> L; for(int i = 1; i <= n; i++) cin >> a[i]; for(int i = 1; i <= n; i++) cin >> b[i]; sort(a + 1, a + n + 1); sort(b + 1, b + n + 1); for(int i = n + 1; i <= 4 * n; i++) b[i] = b[i - n] + L, a[i] = a[i - n] + L; int l = 0, r = L, ans = 0; while(l <= r){ int mid = (l + r >> 1); if(check(mid)) ans = mid, r = mid - 1; else l = mid + 1; } cout << ans; return 0; }
2.P3488 [POI2009] LYZ-Ice Skates
将鞋子拆成
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 2e5 + 10; int n, m, k, d; namespace Segtree{ #define ls (o << 1) #define rs (o << 1 | 1) #define mid (l + r >> 1) struct node{ int pre, suf, sum, mxsum; }tnode[N << 2]; node merge(struct node n1, struct node n2){ node ret; ret.sum = n1.sum + n2.sum; ret.pre = max(n1.pre, n1.sum + n2.pre); ret.suf = max(n2.suf, n2.sum + n1.suf); ret.mxsum = max(max(n1.mxsum, n2.mxsum), n1.suf + n2.pre); return ret; } void pushup(int o){tnode[o] = merge(tnode[ls], tnode[rs]);} void build(int o, int l, int r){ if(l == r){tnode[o] = {-k, -k, -k, -k}; return;} build(ls, l, mid); build(rs, mid + 1, r); pushup(o); } void add(int o, int l, int r, int x, int t){ if(l == r){int v = tnode[o].mxsum + t; tnode[o] = {v, v, v, v}; return;} if(x <= mid) add(ls, l, mid, x, t); else add(rs, mid + 1, r, x, t); pushup(o); } } using namespace Segtree; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m >> k >> d; build(1, 1, n); while(m--){ int r, x; cin >> r >> x; add(1, 1, n, r, x); if(tnode[1].mxsum > d * k) cout << "NIE" << "\n"; else cout << "TAK" << "\n"; } return 0; }
3.ARC106E Medals
首先注意到答案上界不会太大,可以依次给每个人
我们将日期与人连边,代表那天可以给那个人颁奖,问题转化为是否有一个完美匹配(注意:这里默认每个日期的点大小为
注意到
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 18, K = 1e5 + 10, M = 5e6 + 10; int n, k, a[N + 10], P[M + 10]; int f[(1 << N) + 10]; int popcnt(int S){if(!S) return 0; return popcnt(S >> 1) + (S & 1);} bool check(int day){ for(int i = 0; i < (1 << n); i++) f[i] = 0; for(int d = 1; d <= day; d++) f[P[d]]++; for(int i = 0; i < n; i++){ for(int S = 0; S < (1 << n); S++){ if(S & (1 << i)) f[S] += f[S ^ (1 << i)]; } } for(int S = 0; S < (1 << n); S++){ if(popcnt(S) * k > day - f[S ^ ((1 << n) - 1)]) return false; } return true; } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> k; for(int i = 1; i <= n; i++) cin >> a[i]; for(int d = 1; d <= 2 * n * k; d++){ for(int i = 1; i <= n; i++) if((a[i] + d - 1) / a[i] % 2 != 0) P[d] |= (1 << (i - 1)); } int l = 0, r = 2 * n * k, ans = 0; while(l <= r){ int mid = ((l + r) >> 1); if(check(mid)) r = mid - 1, ans = mid; else l = mid + 1; } cout << ans; return 0; }
4.POJ 6062 Pair
首先将
所以等价于
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 3e5 + 10; int n, m, h, a[N], b[N]; namespace Segtree{ #define ls (o << 1) #define rs (o << 1 | 1) #define mid (l + r >> 1) int tmax[N << 2], tag[N << 2]; void pushup(int o){tmax[o] = max(tmax[ls], tmax[rs]);} void pushdown(int o){ if(!tag[o]) return; tmax[ls] += tag[o]; tag[ls] += tag[o]; tmax[rs] += tag[o]; tag[rs] += tag[o]; tag[o] = 0; } void build(int o, int l, int r){ if(l == r){tmax[o] = -l; return;} build(ls, l, mid); build(rs, mid + 1, r); pushup(o); } void add(int o, int l, int r, int s, int t, int x){ if(s <= l && r <= t){ tmax[o] += x; tag[o] += x; return; } pushdown(o); if(s <= mid) add(ls, l, mid, s, t, x); if(mid < t) add(rs, mid + 1, r, s, t, x); pushup(o); } } using namespace Segtree; bool cmp(int x, int y){return x > y;} int getpos(int val){ int l = 1, r = m, ans = 0; while(l <= r){ if(b[mid] >= val) ans = mid, l = mid + 1; else r = mid - 1; } return ans; } int cnt; void upd(int x, int f){ int p = getpos(h - x); if(!p) p++, cnt += f; add(1, 1, m, p, m, f); } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n >> m >> h; build(1, 1, m); for(int i = 1; i <= m; i++) cin >> b[i]; for(int i = 1; i <= n; i++) cin >> a[i]; sort(b + 1, b + m + 1, cmp); for(int i = 1; i <= m; i++) upd(a[i], 1); int ans = (tmax[1] <= 0 && (!cnt)); for(int l = 2; l + m - 1 <= n; l++){ upd(a[l - 1], -1); upd(a[l + m - 1], 1); ans += (tmax[1] <= 0 && (!cnt)); //cout << l << " " << tmax[1] << "\n"; } cout << ans; return 0; }
5.ARC080F Prime Flip
首先发现对于区间操作比较繁琐,将原序列放到异或前缀和数组上讨论,这样等价于每次同时两个
但是每种配对方式的代价是不相同的,考虑分类讨论
-
是奇质数:代价为 。 -
是偶数:由哥德巴赫猜想可知,偶数可以拆成两个奇质数的和,找一个中转点即可,于是代价为 。 -
是奇数但非质数:可以拆成一个奇质数和一个偶数,于是代价为 。
若配对代价为
code
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 100 + 10; int odd[N], eve[N], cnt0, cnt1; int n, a[N], match[N]; bool G[N][N], vis[N]; bool isprime(int x){ if(x == 1) return false; for(int i = 2; i * i <= x; i++) if(x % i == 0) return false; return true; } bool dfs(int u){ for(int i = 1; i <= cnt0; i++){ if(!vis[i] && G[u][i]){ vis[i] = true; if(!match[i] || dfs(match[i])){ match[i] = u; return true; } } } return false; } void add(int x){ if(x & 1) eve[++cnt1] = x; else odd[++cnt0] = x; } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n; for(int i = 1; i <= n; i++) cin >> a[i]; add(a[1]); for(int i = 2; i <= n; i++){ if(a[i] > a[i - 1] + 1){ add(a[i - 1] + 1); add(a[i]); } } add(a[n] + 1); for(int i = 1; i <= cnt1; i++){ for(int j = 1; j <= cnt0; j++){ G[i][j] = isprime(abs(eve[i] - odd[j])); // cout << i << " " << j << " " << abs(eve[i] - odd[j]) << "\n"; } } int o = 0; for(int i = 1; i <= cnt1; i++){ memset(vis, 0, sizeof vis); o += dfs(i); } // cout << cnt1 << " " << cnt0 << " " << o << "\n"; cout << o + ((cnt0 - o) / 2) * 2 + ((cnt1 - o) / 2) * 2 + ((cnt1 - o) & 1) * 3 << "\n"; return 0; }
本文作者:Little_corn
本文链接:https://www.cnblogs.com/little-corn/p/18306004
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步