二分图相关

% \(\rm \color{black}{L}\color{red}{BY}\) 学长。

零、定义:

  • 二分图:

二分图是一张图 \(G = (V, E)\),其中点集 \(V\) 可以分成两个部分 \((V1, V2)\),满足 \(V1 \cap V2 = \emptyset, V1 \cup V2 = V\),且 \(V1, V2\) 中均没有边,即对于 \(\forall e \in E, e = (v_i, v_j)\),均有 \(v_i \in V1, v_j \in V2\)。注意:接下来称 \(V1\) 为左部点,\(V2\) 为右部点。

  • 匹配:

一张图的匹配定义为一个原图的一个子图,且满足任意两条边均没有公共端点。一个匹配的大小定义为其中边的个数,一个图的最大匹配定义为匹配的大小最大的一个匹配。特殊的,对于一个匹配且该匹配完全包含左部点或右部点时,我们称该匹配是完美匹配完美匹配一定是原图中的最大匹配

  • 点覆盖:

一张图的覆盖定义为原图中点集的一个子集,满足原图中每条边均有一个端点在该点集中。一个覆盖的大小定义为点集的大小,最小点覆盖就是原图中最小的覆盖。

  • 独立集:

一张图的独立集定义为原图中点集的一个子集,满足原图中每条边的端点至少有一个不在该点集中。一个覆盖的大小定义为点集的大小,最大独立集就是原图中最大的独立集。

一、二分图最大匹配:

二分图中的最大匹配求法较一般图是简单的,我们接下来探究对于二分图如何求最大匹配。

我们要引入匈牙利算法。依次考虑左部点并进行匹配,假设我们考虑到第 \(i\) 个点。我们找到与之相连的且没有标记过的右部点 \(v\),并做标记,若 \(v\) 没有匹配,则给让 \(v\)\(i\) 匹配并返回 \(i\) 匹配成功。否则我们考虑更换与 \(v\) 匹配的左部点的匹配点,若更换成功,则将 \(v\)\(i\) 匹配,否则考虑下一个点。当 \(i\) 一个点都没有匹配到时,返回没有找到即可。

注:其中我们称该过程中走的边为增广路交替路,因为这条路径上的边是匹配边和非匹配边交替的。

匈牙利算法看起来相当暴力,实际上,它的时间复杂度为 \(O(nm)\)

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定理:二分图中,最大匹配等于最小点覆盖

  • 证明:

首先我们可以知道最大匹配 \(\le\) 最小点覆盖。因为最小点覆盖至少要覆盖到匹配边,而匹配边端点互不相交,故每条匹配边都需要一个单独的点去覆盖。

接下来我们考虑构造出一种方案取到下界。具体方案是从未匹配的右部点出发,走交替路并且标记经过的点,最后选择左边标记的点和右边未标记的点。画图容易发现每个点恰好是一条匹配边的端点,原因大概是从左向右走的一定是匹配边,因此左边标记过的点如果是匹配点,一定会将所在匹配边的右部点标记而不会选进点集中。而右边未标记的点一定是匹配点,于是恰好覆盖了所有匹配边且恰好没有重复。

对于完全覆盖的证明,我们考虑使用反证法,假设有一条边没有被覆盖到,则它对应的左部点一定是未标记过,而右部点是标记过的。进一步的,该边不可能是未匹配边,但是若该边是匹配边,则右部点只可能是由左部点走过来,但是这样左部点就标记了,推出矛盾,从而定理得证。

三、二分图最大独立集:

与最小点覆盖问题类似,我们依旧考虑转化问题。

实际上,我们可以考虑将点覆盖与独立集建立映射关系。具体的,一个独立集的补集即为原图的一个点覆盖。这是因为独立集中的点两两之间没有任何边,于是剩下的点就会覆盖所有边。

从而最大独立集大小 = 点数 - 最小点覆盖。

四、Hall 定理:

Hall 定理是关于二分图完美匹配的强定理,适用于判定的情况。

Hall 定理: 对于一个二分图,存在完美匹配充分必要条件是对于任意点集 \(S \in V1, S \cap V2 = \emptyset\),它的邻域 \(N(S)\) 满足 \(|N(S)| \ge |S|\)

  • 证明:

必要性是显然的,这里证明充分性。

还是考虑使用反证法,设这个图找完最大匹配后还有至少一个未匹配点 \(a\)。找到与 \(a\) 相邻的一个点 \(b\),讨论 \(b\) 的匹配情况:

  • \(b\) 是非匹配点,可以匹配使得匹配数加一,矛盾。

  • \(b\) 是匹配点,则一定会有一个点匹配点 \(c\)\(b\) 相连。

由题设,一定还会有一个点 \(d\)\(a\)\(c\) 相连(因为 \(|N({a, c})| \ge 2\))。那么我们再对点 \(d\) 进行讨论:

  • \(d\) 是非匹配点,可以匹配使得匹配数加一,矛盾。

  • \(d\) 是匹配点,则一定会有一个点匹配点 \(e\)\(d\) 相连。

同理会有一个点 \(f\)\(a\)\(c\)\(e\) 相连.......这样会一直循环下去,但由于点的有限性,而且由于现在的匹配不是完美匹配,最终一定会找到一个未匹配点,结果会得到一个增广路,于是得到矛盾,证毕。

五、例题:

1.CF981F Round Marriage

看到最小化最大值,考虑二分答案 \(P\)

我们将距离小于等于 \(P\) 的男女连上边,问题转化为该二分图是否存在完美匹配。若我们将 \(a, b\) 从小到大排序并断环为链,容易发现 \(a_i\)\(b\) 数组有连边的点是一个区间。我们欲找到一个 \(a\) 中集合不满足 \(\rm Hall\) 定理,即尽量男女数量的差 $ < 0$。这里有一个关键的性质:使得男女数量差 \(< 0\) 的选择集合在 \(a\) 中至少有一个是一个连续的区间。

考虑反证法证明。若不存在一个连续的区间且满足差 \(<0\),则至少有两个相离的区间满足,其中至少有一个差 \(< 0\),这个区间就是符合题设的。

设对于 \(a_i\),他在 \(b\) 中有连边的区间是 \([L_i, R_i]\)。对于一个区间 \((i, j]\) 若它不满足 \(\rm Hall\) 定理,则 \(j - i \ge R_j - L_i\),移项得 \(L_i - i \ge R_j - j\),维护 \(1 ~ i - 1\)\(R_j - j\),并判断即可。

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

将鞋子拆成 \(k\) 个点并与人连边,于是再次转化为了判断完美匹配问题。与上一题类似的,能够卡掉完美匹配的其中肯定是有一个区间的。设第 \(i\) 号脚的人有 \(a_i\) 个,则区间 \([i, j]\) 不满足 \(\rm Hall\) 定理的判定条件是 \(\sum^r_{i = l}{a_i} \ge (r - l + 1) * k + d * k\),移项得:\(\sum^r_{i = l}{a_i - k} \ge k * d\)。这等价与将所有 \(a_i\) 全体减 \(k\) 后求最大子段和,支持单点修改。线段树即可。

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

首先注意到答案上界不会太大,可以依次给每个人 \(2 * k\) 天颁奖,这样就需要 \(2 * n * k\) 颁奖。然后再注意到答案具有单调性,于是可以二分答案 \(day\)

我们将日期与人连边,代表那天可以给那个人颁奖,问题转化为是否有一个完美匹配(注意:这里默认每个日期的点大小为 \(k\),可以理解为拆成了 \(k\) 个点并依次连边,但是由于这 \(k\) 个点的一致性,我们可以一起考虑)。

注意到 \(n\) 的范围很小,因此我们可以暴力枚举子集并判断是否满足 \(Hall\) 定理。假设我们枚举的人集合是 \(S\),则与这个集合有连边的点 \(i\) 满足 \(P_i \cap S \ne \emptyset\),其中 \(P_i\) 是第 \(i\) 天可以选的人的集合。求这个是困难的,不妨考虑求 \(day - P_i \cap S = \emptyset\),即答案的补集。显然的,\(P_i\)\(S\) 没有交等价于 \(P_i\)\(S\) 补集的子集。一个 \(\rm SOS DP\) 即可。

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

首先将 \(b\) 从大到小排序,并将可以配对的 \((a_i, b_j)\) 进行连边,不难发现对于任意 \(a_i\)\(b\) 中的连边区间均是一段前缀的形式,问题转化为是否存在完美匹配。还是找一个集合用 \(\rm Hall\) 定理卡掉完美匹配,不难发现若不存在完美匹配,一定有一个 \(b\) 中的后缀不满足 \(\rm Hall\) 定理。感性理解就是你选一段区间一定会包含到后面的 \(b\),不如全选上。反证法不难证明。

所以等价于 \(a\) 中有一个数在 \(b\) 中排名大于区间右端点,用线段树维护区间加和全局最小值即可。

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

首先发现对于区间操作比较繁琐,将原序列放到异或前缀和数组上讨论,这样等价于每次同时两个 \(1\) 变为 \(0\),询问将上面的 \(1\) 全部变为 \(0\) 所需最少操作数。

但是每种配对方式的代价是不相同的,考虑分类讨论 \((i, j)\) 操作的代价:

  • \(a_i - a_j\) 是奇质数:代价为 \(1\)

  • \(a_i - a_j\) 是偶数:由哥德巴赫猜想可知,偶数可以拆成两个奇质数的和,找一个中转点即可,于是代价为 \(2\)

  • \(a_i - a_j\) 是奇数但非质数:可以拆成一个奇质数和一个偶数,于是代价为 \(3\)

若配对代价为 \(1\) 的个数有 \(o\) 对,\(cnt0, cnt1\) 分别是奇数位异或数组上 \(1\) 和偶数位异或数组上为 \(1\) 的个数。那么答案为 \(o + \lfloor\frac{cnt0 - o}{2}\rfloor \times 2 + \lfloor\frac{cnt1 - o}{2}\rfloor \times 2 + (cnt1 \bmod 2) \times 3\)。我们要让答案最小,就要让 \(o\) 尽量大。那么我们把奇数位看成左部点,偶数位作为右部点,其中将差为奇质数的连边即可,求最大匹配即可。

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;
}
posted @ 2024-07-16 20:45  Little_corn  阅读(42)  评论(0编辑  收藏  举报