二分图总结

二分图总结

  • 基础知识
  • 二分图判定
  • 二分图匹配
    • Hungarian 算法
    • KM 算法
  • 二分图覆盖与独立集
    • 二分图最小点覆盖
    • 二分图最大独立集
    • DAG 的最小路径点覆盖
    • DAG 的最小路径可重复点覆盖
  • Hall定理
    • HDU 6667 Roundgod and Milk Tea
    • [ARC076F] Exhausted?
    • CF981F Round Marriage
    • [POI2009]LYZ-Ice Skates
    • CF103E Buying Sets
    • [ARC106E] Medals
    • [AGC029F] Construction of a tree
    • [AGC037D] Sorting a Grid
    • [CERC2016] 二分毯 Bipartite Blanket

基础知识

\qquad 假设存在一张图 G = ( V , E ) G=(V,E) G=(V,E),如果我们可以将 V V V 分成两个非空点集 V L , V R V_L,V_R VL,VR,使得这两个点集内部无边,两个点集之间允许连边,那么我们便称图 G G G 是一张二分图,将点集 V L , V R V_L,V_R VL,VR 分别成为二分图的左部、右部。

二分图判定

\qquad 一个很显然的结论是:如果一张图中存在奇环,那么这张图一定不是二分图;否则一定是二分图。那么我们便可以通过给点染色的方式来判断这张图中是否存在奇环。

\qquad 核心 C o d e : Code: Code:

int dfs(int x) {
	int flag = 0;
	for(int i = head[x]; i; i = edge[i].lst) {
		int v = edge[i].to;
		if(col[v] && col[v] == col[x]) {//相邻点颜色相同,存在奇环
			flag = 1;
			break;
		}
		else if(col[v] && col[v] == 3 - col[x]) continue;
		col[v] = 3 - col[x], flag |= dfs(v);
	}
	return flag;
}

二分图匹配

\qquad 对于一张二分图 G = ( V , E ) G=(V,E) G=(V,E),如果我们选取图中的一个边集 E ′ E' E,使得 E ′ E' E 中任意两条边无公共端点,那么我们称 E ′ E' E 是该二分图的一组匹配。一张二分图会有许多个匹配。这些匹配中, ∣ E ′ ∣ |E'| E 最大的一个匹配称为该二分图的一组最大匹配。如果一张二分图的左部点数等于右部点数等于 N N N,且最大匹配的大小也等于 N N N,那么我们称这组最大匹配为该二分图的完备匹配。如果一张二分图中的点可以被匹配多次,但是有一个上界,那么我们称这类问题为二分图的多重匹配问题。如果一张二分图中的边带边权,那么我们定义二分图的所有最大匹配边权和最大的那组匹配为该二分图的带权最大匹配

Hungarian 算法

\qquad 对于任意一组匹配 E ′ E' E,我们定义 E ′ E' E 中的边为匹配边,匹配边的端点是匹配点,剩余的边和点分别成为非匹配边和非匹配点。如果存在一条路径链接了两个非匹配点,这条路径满足非匹配边和匹配边交替出现,那么我们称这条路径为一条增广路,或交错路

\qquad 很显然,一条增广路的长度一定是奇数,而且如果我们把这条路径上所有边的状态取反,即非匹配边变为匹配边,匹配边变为非匹配边,那么取反后在原图中便可得到一个更大的匹配。匈牙利算法(Hungarian)便是依靠这一性质展开的。它的主要流程是:1、在原图中寻找增广路;2、对增广路的所有边状态取反;3、重复上述过程直至不存在增广路。

\qquad 整个过程最主要的一部便是找增广路。Hungarian 算法的实现方法是:对于每个左部点 x x x,枚举它的所有出边所连的右部点 y y y,如果枚举到点 y y y 当前是非匹配点,那么他们之间就构成了一条长度为 1 1 1 的增广路,匹配即可;如果枚举到点 y y y 已经与点 x ′ x' x 匹配,但从 x ′ x' x 出发还可以找到一个 y ′ y' y x ′ x' x 匹配,那么 x − y − x ′ − y ′ x-y-x'-y' xyxy 便构成了一条增广路,匹配即可。Hungarian 算法采取 d f s dfs dfs 的实现方式,这样在递归回溯时便可直接对路径状态进行取反。时间复杂度 O ( n m ) O(nm) O(nm)

\qquad 核心 C o d e : Code: Code:

bool match(int x) {
	for(int i = head[x]; i; i = edge[i].lst) {
		int v = edge[i].to;
		if(!vis[v]) {
			vis[v] = 1;
			if(!p[v]/*还未匹配*/ || match(p[v])/*已匹配,但p[v]还可以找到新的匹配*/) {
				p[v] = x;//匹配
				return 1;
			}
		}
	}
	return 0;//无法匹配
}

int Hungarian() {
	int ans = 0;
	for(auto i : M) {//枚举左部点,M中存的是左部点
		memset(vis, 0, sizeof vis);
		if(match(i)) ans ++;//匹配成功
	}
	return ans;
}

KM 算法

\qquad KM 算法主要用于解决二分图的带权最大匹配。但是它只能在满足带权最大匹配一定是完备匹配的图中正确求解。

\qquad 在 Hungarian 算法中,如果某个左部点匹配失败,那么在 d f s dfs dfs 的过程中,所有访问过的节点和边共同构成一棵树。这棵树的根节点和叶子节点都是非匹配点,而且这棵树的奇数层上的边一定是非匹配边,偶数层上的边一定是匹配边。我们称这样一颗树为交错树

\qquad 在二分图中,我们给每个左部节点赋值 A i A_i Ai,给每个右部节点赋值 B j B_j Bj,这些值要满足 ∀ i , j , A i + B j ≥ w ( i , j ) \forall i,j,A_i+B_j\geq w(i,j) i,j,Ai+Bjw(i,j) w ( i , j ) w(i,j) w(i,j) i , j i,j i,j 之间的边权,没有边视作负无穷。我们把这些值称为顶点标记值,简称顶标

\qquad 我们把二分图中所有节点和满足 A i + B j = w ( i , j ) A_i+B_j=w(i,j) Ai+Bj=w(i,j) 的边构成的子图称为该二分图的相等子图

\qquad 基于相等子图的概念,我们可以得到一个定理:若相等子图中存在完备匹配,那么这个完备匹配就是二分图的带权最大匹配。证明也很简单:相等子图中完备匹配的边权值和等于 ∑ i = 1 n A i + B i \sum_{i=1}^nA_i+B_i i=1nAi+Bi,即所有顶标之和。又因为 ∀ i , j , A i + B j ≥ w ( i , j ) \forall i,j,A_i+B_j\geq w(i,j) i,j,Ai+Bjw(i,j),所以一定不存在大于这一权值的匹配。

\qquad KM 算法的原理便是这一定理。它的流程是:1、先给所有点在满足顶标的要求的基础之上任意赋一个顶标;2、适当调整顶标使得扩大相等子图规模,直至相等子图中存在完备匹配。

\qquad 对于当前的相等子图,我们用 Hungarian 跑最大匹配。如果最大匹配不是完备匹配,那么必然有一个点没有匹配,这个点在 d f s dfs dfs 时形成了一棵交错树。现在我们要想扩大相等子图的规模,就要从两个角度考虑:1、让右部点访问到更多左部点;2、让左部点访问到更多右部点。因为在 Hungarian 的过程中,我们并不会改变已有的匹配,所以右部点是无法访问到更多的左部点的(因为交错树上右部点访问左部点必须走匹配边)。我们只能考虑如何让左部点访问到更多右部点。

\qquad 我们现在构造一组方案:让交错树中的左部节点的顶标都减小 δ \delta δ,右部节点的顶标都增加 δ \delta δ。现在,我们来分析这一构造产生的影响。

\qquad 对于一条匹配边,它的两端点要么同时在交错树上,要么同时不在交错树上,两端点顶标和不变,所以不会受影响;对于一条非匹配边,除了出现上述两种情况外还可能出现:左部点在交错树上,右部点不在交错树上。此时,两端点顶标和就会减小。减小过后,这条边就有可能被加入到相等子图中,被访问到。所以,这样的构造是可以扩大相等子图的规模的。但是,为了保证 A i + B j ≥ w ( i , j ) A_i+B_j\geq w(i,j) Ai+Bjw(i,j) 的性质不被破坏, δ \delta δ 的取值应该是 min ⁡ ( A i + B j − w ( i , j ) ) \min(A_i+B_j-w(i,j)) min(Ai+Bjw(i,j))。这样不断重复下去,直到所有左部点都匹配成功,此时我们便得到了原图的带权最大匹配。

\qquad 但是,上述过程是可以进一步优化的。因为每次修改顶标后,相等子图的规模只会扩大,不会减小,所以我们每次遍历时只需从最新加入的边开始即可。不过此时如果匹配成功,我们是无法直接通过递归回溯来更新增广路的,因为我们并没有从根开始遍历。所以,我们需要记录一个 l s t lst lst 数组表示每一个右部点在交错树上的上一个右部点,按照 l s t lst lst 倒退回去即可更新增广路。时间复杂度 O ( n 3 ) O(n^3) O(n3)

\qquad 核心 C o d e : Code: Code:

/*
va[i]:左部点i是否在交错树上
vb[i]:右部点i是否在交错树上
la[i],lb[i]:左部点、右部点顶标
p[i]:右部点i匹配的左部点
lst[i]:如上文
upd[i]:右部点i对应的最小的delta
*/
bool dfs(int x, int fa) {
	va[x] = 1;
	for(int i = 1; i <= n; i ++) {
		if(!vb[i]) {
			if(la[x] + lb[i] == w[x][i]) {//在相等子图上
				vb[i] = 1, lst[i] = fa;
				if(!p[i] || dfs(p[i], i)) {
					p[i] = x;
					return 1;
				}
			}
			else if(upd[i] > la[x] + lb[i] - w[x][i]) {//不在
				upd[i] = la[x] + lb[i] - w[x][i];
				lst[i] = fa;//注意!!!此时右部点i可能作为倒推的起点,所以需要记录lst值
			}
		}
	}
	return 0;
}

int KM() {
	for(int i = 1; i <= n; i ++) {
		lb[i] = 0, la[i] = -0x7f7f7f7f;
		for(int j = 1; j <= n; j ++) la[i] = max(la[i], w[i][j]);
	}
	memset(p, 0, sizeof p);
	for(int i = 1; i <= n; i ++) {
		memset(va, 0, sizeof va), memset(vb, 0, sizeof vb);
		memset(upd, 0x7f, sizeof upd), memset(lst, 0, sizeof lst);
		int st = 0; p[st] = i;//一开始让左部点i和右部点0匹配
		while(p[st]) {
			if(dfs(p[st], st)) break;//匹配成功
			int delta = 0x7f7f7f7f;
			for(int j = 1; j <= n; j ++) {
				if(!vb[j] && delta > upd[j]) delta = upd[j], st = j;//从最新加入的边开始,所以st要一起更新
			}
			for(int j = 1; j <= n; j ++) {
				if(va[j]) la[j] -= delta;
				if(vb[j]) lb[j] += delta;
				else upd[j] -= delta;
			}
			vb[st] = 1;
		}
		while(st) p[st] = p[lst[st]], st = lst[st];//倒推更新增广路
	}
	//此时p中便是右部点的匹配方案
}

\qquad 例题:Ants。

二分图覆盖与独立集

\qquad 给定一张二分图 G = ( V , E ) G=(V,E) G=(V,E),从中选出一个点集 V ′ V' V,如果 E E E 中任意一条边都有至少一个端点在 V ′ V' V 内,那么我们称点集 V ′ V' V G G G 的一个点覆盖集。如果 V ′ V' V 中的点两两之间无边,那么我们称 V ′ V' V G G G 的一个点独立集

二分图最小点覆盖

\qquad 二分图中 ∣ V ′ ∣ |V'| V 最小的一个点覆盖集就被称为该图的最小点覆盖。在这里,有一个定理(柯尼希定理):二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数。证明不会……

\qquad 例题:[USACO05JAN] Muddy Fields G,Machine Schedule。

二分图最大独立集

\qquad 二分图中 ∣ V ′ ∣ |V'| V 最大的一个点独立集就被称为改图的最大独立集。在这里,有一个定理:最大独立集的大小等于 n n n 减去最大匹配数。证明不会……

DAG 的最小路径点覆盖

\qquad 给定一张 DAG,要求用尽量少的不相交的边覆盖 DAG 中的所有点,这一问题被称作 DAG 的最小路径点覆盖问题。在这里,有一个定理:DAG 的最小路径点覆盖中包含的路径数等于 n n n 减去它的拆点二分图(把 1 ∼ n 1\sim n 1n 拆成 1 ∼ 2 n 1\sim 2n 12n,其中 1 ∼ n 1\sim n 1n 为左部点, n + 1 ∼ 2 n n+1\sim 2n n+12n 为右部点,对于原图中的每一条边 ( x , y ) (x,y) (x,y),在新图上对应边 ( x , y + n ) (x,y+n) (x,y+n),最终得到的二分图被称为拆点二分图)的最大匹配数。证明依旧不会……

\qquad 例题:Air Raid。

DAG 的最小路径可重复点覆盖

\qquad 如果一个点可以被覆盖多次(即选的边允许相交),那么该怎么办呢?

\qquad 如果两条路径 x 1 ∼ p ∼ y 1 x_1\sim p\sim y_1 x1py1 x 2 ∼ p ∼ y 2 x_2\sim p\sim y_2 x2py2 相交于点 p p p,那我们完全可以把它看成 x 1 ∼ y 1 x_1\sim y_1 x1y1 x 2 ∼ y 2 x_2\sim y_2 x2y2 这样的两条不相交路径,所以我们可以先对原图传递闭包,然后再做一般的最小路径覆盖问题。

Hall定理

\qquad 内容:一张二分图的左部为 V L V_L VL,右部为 V R V_R VR,不妨假设 ∣ V L ∣ ≤ ∣ V R ∣ |V_L|\leq |V_R| VLVR。则这张二分图存在一个大小为 ∣ V L ∣ |V_L| VL 的匹配的充要条件为 ∀ S ⊆ V L , ∣ S ∣ ≤ ∣ ∪ v ∈ S N ( v ) ∣ \forall S\subseteq V_L,|S|\leq |\cup_{v\in S}N(v)| SVL,SvSN(v),这里的 N ( v ) N(v) N(v) 表示图中点 v v v 的邻居集。

\qquad 推论 1 1 1:对于一张 k k k 正则二分图(每个点度数均为 k k k k ≥ 1 k \geq 1 k1),若其左右部点数相同,则其必有完美匹配。

\qquad 推论 2 2 2:一张左部、右部分别为 V L V_L VL V R V_R VR 的二分图,它的最大匹配为 ∣ V L ∣ − max ⁡ S ⊆ V L ( ∣ S ∣ − ∣ ∪ v ∈ S N ( v ) ∣ ) |V_L|-\max_{S\subseteq V_L}(|S|-|\cup_{v\in S}N(v)|) VLmaxSVL(SvSN(v)),或者写成 min ⁡ S ⊆ V L ( ∣ V L ∣ − ∣ S ∣ + ∣ ∪ v ∈ S N ( v ) ∣ ) \min_{S\subseteq V_L}(|V_L|-|S|+|\cup_{v\in S}N(v)|) minSVL(VLS+vSN(v))

HDU 6667 Roundgod and Milk Tea

二分图总结_第1张图片

\qquad 很显然是一道二分图最大匹配的题。但是直接跑显然不行,时间复杂度直接爆炸。此时,我们就考虑使用 H a l l Hall Hall 定理的推论 2 2 2。在推论 2 2 2 中,我们要最大化 ∣ S ∣ − ∣ ∪ v ∈ S N ( v ) ∣ |S|-|\cup_{v\in S}N(v)| SvSN(v) 的值。在这里,很显然的一个结论是:当 S S S 表示整个班时,这个值最大。所以我们直接 O ( n ) O(n) O(n) 求一下各个班的最大值即可。

	scanf("%d", &n);
	for(int i = 1; i <= n; i ++) scanf("%lld%lld", &a[i], &b[i]), aa += a[i], ab += b[i];
	LL ans = min(aa, ab);//S为空集
	for(int i = 1; i <= n; i ++) ans = min(ans, aa - a[i] + (ab - b[i]));
	printf("%lld\n", ans);

[ARC076F] Exhausted?

\qquad 题面

\qquad 很显然这题是一个最大匹配裸题,答案就是 n n n 减最大匹配数。我们便可直接使用 H a l l Hall Hall 定理推论 2 2 2。此时,需要稍微化简下式子:

  a n s = n − ( n − max ⁡ S ⊆ N ( ∣ S ∣ − ∣ ∪ v ∈ S N ( v ) ∣ ) ) \,ans=n-(n-\max_{S\subseteq N}(|S|-|\cup_{v\in S}N(v)|)) ans=n(nmaxSN(SvSN(v)))
= max ⁡ S ⊆ N ( ∣ S ∣ − ∪ v ∈ S N ( v ) ) \qquad=\max_{S\subseteq N}({|S|-\cup_{v\in S}N(v)}) =maxSN(SvSN(v))
= max ⁡ S ⊆ N ( ∣ S ∣ − ∣ ∪ v ∈ S ( 0 , l v ] ∪ [ r v , m ] ∣ ) \qquad=\max_{S\subseteq N}(|S|-|\cup_{v\in S}(0,l_v]\cup [r_v,m]|) =maxSN(SvS(0,lv][rv,m])
= max ⁡ S ⊆ N ( ∣ S ∣ − ( m − ∣ ∩ v ∈ S ( l v , r v ) ∣ ) ) \qquad=\max_{S\subseteq N}(|S|-(m-|\cap_{v\in S}(l_v,r_v)|)) =maxSN(S(mvS(lv,rv)))
= max ⁡ S ⊆ N ( ∣ S ∣ + ∣ ∩ v ∈ S [ l v + 1 , r v − 1 ] ∣ ) − m \qquad=\max_{S\subseteq N}(|S|+|\cap_{v\in S}[l_v+1,r_v-1]|)-m =maxSN(S+vS[lv+1,rv1])m

\qquad 式子化简到这一步就非常好看了。现在我们就要求前面这一部分的最大值。我们可以考虑枚举区间(那个交集)来统计。假设当前交集为 [ L , R ] [L,R] [L,R],那么我们可以将式子进一步化为:

a n s = max ⁡ ( R − L + 1 + n u m [ l i ≤ L ] & & [ r i ≥ R ] ) − m ans=\max(R-L+1+num[l_i\leq L]\&\&[r_i\geq R])-m ans=max(RL+1+num[liL]&&[riR])m

\qquad 所以我们便可以将区间按照 l i l_i li 排序,然后枚举 l i l_i li,用线段树来维护最大值即可。

	for(int i = 1; i <= n; i ++) scanf("%d%d", &a[i].l, &a[i].r), a[i].l ++, a[i].r --;
	sort(a + 1, a + n + 1, cmp);
	for(int i = 1; i <= n; i ++) {
		if(a[i].l > a[i].r) continue;//空集,无意义
		loc[a[i].l].push_back(a[i].r);
	}
	int ans = n - m;
	//线段树中存 R+num 的最大值。因为已经按照L排好序了,所以无需考虑L的限制
	build(1, 1, m);
	for(int l = 1; l <= m; l ++) {
		for(auto p : loc[l]) modify(1, 1, p);
		ans = max(ans, query(1, l, m) - l + 1 - m);
	}
	printf("%d\n", ans);

CF981F Round Marriage

\qquad 题面

\qquad 显然我们可以二分答案之后判定当前值是否可行。假设当前二分值为 m i d mid mid,那么我们让新郎和与他距离不超过 m i d mid mid 的新娘连边,跑最大匹配看是否是完备匹配即可。 H a l l Hall Hall 定理可知,本题要满足的限制是: r − l + 1 ≤ n r r − n l l + 1 r-l+1\leq nr_r-nl_l+1 rl+1nrrnll+1,这里 [ l , r ] [l,r] [l,r] 表示新郎的区间, [ n l i , n r i ] [nl_i,nr_i] [nli,nri] 表示第 i i i 个新郎可以匹配的新娘。上面不等式移项可得 n l l − l ≤ n r r − r nl_l-l\leq nr_r-r nlllnrrr,也就是说 n l l − l nl_l-l nlll 恒小于 n r r − r nr_r-r nrrr,根据它具备的单调性,我们可以用双指针求出 n l i , n r i nl_i,nr_i nli,nri,顺便记录一下 n l i − i nl_i-i nlii 的最大值,判断即可。注意,在断环成链的时候,新郎需要赋值 2 2 2 遍,新娘需要复制 4 4 4 遍才能覆盖所有情况。

bool check(int x) {
	int l = 1, r = 1, maxx = -1e9;
	for(int i = 1; i <= (n << 1); i ++) {
		while(l <= (n << 2) && b[l] < a[i] - x) l ++;
		while(r <= (n << 2) && b[r] <= a[i] + x) r ++;
		maxx = max(maxx, l - i);
		if(r - 1 - i < maxx) return 0;
	}
	return 1;
}

[POI2009]LYZ-Ice Skates

\qquad 题面

\qquad 根据 H a l l Hall Hall 定理,我们可得 ∑ i = l r a [ i ] ≤ k × ( r + d − l + 1 ) \sum_{i=l}^ra[i]\leq k\times (r+d-l+1) i=lra[i]k×(r+dl+1),也就是任意一端区间人的数量必须小于等于鞋的数量。我们把右边的 k × ( r − l + 1 ) k\times (r-l+1) k×(rl+1) 移到左边,化简得 ∑ i = l r ( a [ i ] − k ) ≤ k × d \sum_{i=l}^r(a[i]-k)\leq k\times d i=lr(a[i]k)k×d,不等式左边显然是全局的最大子段和,线段树维护一下即可。

	build(1, 1, n);//初始时每个值都是-k
	for(int i = 1, r, x; i <= m; i ++) {
		scanf("%d%d", &r, &x);
		modify(1, r, x);
		(dat(1) <= 1LL * k * d ? puts("TAK") : puts("NIE"));
	}

CF103E Buying Sets

\qquad 题面

\qquad 首先,如果没有子集并大小等于子集个数这一限制,那这显然就是一道最大权闭合图板子题。但是有了限制怎么搞呢?我们考虑神奇构造。在这里,先设 i n f = 1 0 8 , I N F = 1 0 9 inf=10^8,INF=10^9 inf=108,INF=109,设这两个数旨在:1、他们都是正无穷;2、 i n f < I N F infinf<INF。我们按照最大权闭合图的方式建图。从源点向所有子集连一条容量为子集权值的边,子集与数字之间连容量为 I N F INF INF 的边,数字向汇点连容量是 0 0 0 的边。但是,但从流量上看这就不是一张合法的网络。所以我们考虑让从源点指向集合、从数字指向汇点的边的边权整体加 i n f inf inf,这样它就是一张合法的网络。但是这样一张网络怎么就能满足子集并大小等于子集个数的限制呢?

\qquad 首先,我们最大权闭合图模型是利用最小割来实现的。既然是最小割,那么我们就不可能去割中间容量为 I N F INF INF 的边,这一定不优。其次,两边与源汇点相连的边容量都是 i n f + v a l inf+val inf+val,那么我们可以得到一个结论:割掉的从源点出发的边的数量一定等于指向汇点的边的数量。因为一旦两边割掉的边的数量不相同,那么从源点流出的流量就一定不可能等于流入汇点的流量,所以这样就满足了题目中的个数限制。

	for(int i = 1, x; i <= n; i ++) {
		scanf("%d", &x);
		for(int j = 1, y; j <= x; j ++) {
			scanf("%d", &y);
			add(i, n + y, INF);
		}
	}
	for(int i = 1, p; i <= n; i ++) {
		scanf("%d", &p), p = -p;//因为求的是“最小权”闭合图,所以边权要取反,最后输出dinic()-all
		add(S, i, inf + p), add(i + n, T, inf), all += inf + 1LL * p;
	}
	printf("%lld\n", dinic() - all);

[ARC106E] Medals

\qquad 题面

\qquad 首先一眼的二分答案转化为判定问题。看到要给每个人颁 K K K 个奖,第一想法肯定是拆点。不过,如果我们根据 H a l l Hall Hall 定理来分析就会发现,如果把一个左部点 i i i 拆成 K K K 个点,这 K K K 个点的邻居集(即 N ( v ) N(v) N(v))是相同的,所以我们显然没有拆点的必要,直接整体考虑这 K K K 个点即可。在这之后我们只要判断这 2 n 2^n 2n 个子集是否满足条件即可。想要判断,就要先求 N ( S ) N(S) N(S)。求 N ( S ) N(S) N(S) 可以直接高维前缀和做到 O ( n 2 n ) O(n2^n) O(n2n),做完后判断即可。

bool check(int x) {
	memset(g, 0, sizeof g);
	for(int i = 1; i <= x; i ++) g[f[i]] ++;
	for(int j = 0; j < n; j ++)
		for(int i = 0; i < (1 << n); i ++)
			if((i >> j) & 1) g[i] += g[i ^ (1 << j)];//高维前缀和
	for(int i = 0; i < (1 << n); i ++)
		if(x - g[(1 << n) - 1 - i]/*注意:此时g[mask]表示只有mask中为1的这些人,不允许有别的人时,包含的天数,而这显然和我们要求的 ∪N(v) 不同。例如,若mask=10001,那么状态11000贡献的天数就不会被加到g[10001]中,这样就会导致第一个人的 N(v) 少算。所以我们要用全集减补集来表示 ∪N(v)*/ < num[i] * k) return 0;
	return 1;
}
//main中预处理
	for(int i = 1; i <= 2 * n * k; i ++)
		for(int j = 1; j <= n; j ++)
			f[i] |= ((1 << (j - 1)) * ((i % (2 * a[j]) <= a[j]) && (i % (2 * a[j]) > 0)));//f[i]:包含第i天的点集
	for(int i = 1; i < (1 << n); i ++) num[i] = num[i / 2] + (i & 1);

[AGC029F] Construction of a tree

\qquad 题面

\qquad 首先,我们考虑本题如何判断无解。我们假设生成的树的根节点为 1 1 1。若我们删除根结点,那么树中剩下的部分就是子节点连带着它的父边。父边从哪来?从给定的集合来!也就是说若把点和集合看成二分图的两部,剩下的部分必须要构成一个完备匹配。我们直接跑一个 d i n i c dinic dinic 判断是否满流即可( H u n g a r i a n Hungarian Hungarian 可能会 T T T)。判断完是否有解,我们考虑怎么构造。

\qquad 我们考虑这样一个构造方案:从根节点开始,遍历包含根结点的所有集合,让与这些集合匹配的点作为根节点的子节点,然后在递归下去直到构造结束。不难证明这一构造方案一定是合法的,因为得到的匹配是完备匹配。构造完输出即可。

void dfs(int x) {
	if(vis1[x]) return ;
	vis1[x] = 1;
	for(auto i : pos[x]) {
		if(vis2[i]) continue;
		vis2[i] = 1;
		ans[i] = MP(min(x, p[i]), max(x, p[i]));//MP:make_pair
		dfs(p[i]);
	}
}
//main中
	for(int i = 1, x; i < n; i ++) {
		scanf("%d", &x);
		for(int j = 1, y; j <= x; j ++) {
			scanf("%d", &y);
			pos[y].push_back(i);
			if(y != 1) add(y, i + n, 1);
		}
		add(i + n, T, 1), add(S, i + 1, 1);
	}
	if(dinic() < n - 1) return puts("-1"), 0;//判断无解
	for(int i = 2; i <= n; i ++) {
		for(int j = head[i]; ~j; j = edge[j].lst) {
			int v = edge[j].to, w = edge[j].cap;
			if(!w) {
				p[v - n] = i;//记录每个集合的匹配点
				break;
			}
		}
	}
	dfs(1);//递归构造
	for(int i = 1; i < n; i ++) printf("%d %d\n", ans[i].first, ans[i].second);

[AGC037D] Sorting a Grid

\qquad 题面

\qquad 我们考虑从 D D D 倒推。我们可以给 D D D 中的 n n n 行分别赋上 1 ∼ n 1\sim n 1n 的颜色,那么从 D D D 变到 C C C 时,颜色显然不变。从 C C C 变到 B B B 时,不难发现 B B B 的每一列的 n n n 个数字分别被涂上了 1 ∼ n 1\sim n 1n n n n 种颜色,不重不漏。也就是说,我们从 A A A B B B 变换时,需要满足每一列恰好出现 1 ∼ n 1\sim n 1n n n n 种颜色。

\qquad 我们按列考虑。假设前 j − 1 j-1 j1 列都已完成配对。对于第 j j j 列,我们让第 i i i 行与它包含的未匹配的颜色相连,然后跑一边最大匹配,得到的便是第 j j j 列的构造方案。根据 H a l l Hall Hall 定理推论 1 1 1,我们可以证明这样一组构造方案一定是存在的。构造后输出即可。

	for(int l = 1; l <= m; l ++) {//按列考虑
		memset(head, -1, sizeof head), tot = -1;
		for(int i = 1; i <= n; i ++) {
			memset(c, 0, sizeof c);
			for(int j = 1; j <= m; j ++) {
				if(vis[i][j]) continue;
				c[(A[i][j] - 1) / m + 1] = 1;
			}
			for(int j = 1; j <= n; j ++)
				if(c[j]) add(i, n + j, 1);//建图
		}
		for(int i = 1; i <= n; i ++) add(S, i, 1), add(i + n, T, 1);
		dinic();//匹配
		memset(cho, 0, sizeof cho);
		for(int i = 1; i <= n; i ++) {
			for(int j = head[i]; ~j; j = edge[j].lst) {
				int v = edge[j].to, w = edge[j].cap;
				if(!w) {
					cho[i] = v - n;
					break;
				}
			}
		}
		for(int i = 1; i <= n; i ++) {
			for(int j = 1; j <= m; j ++) {
				if(!vis[i][j] && (A[i][j] - 1) / m + 1 == cho[i]) {
					B[i][l] = A[i][j], vis[i][j] = 1;//构造B
					break;
				}
			}
		}
	}
	for(int i = 1; i <= n; i ++) {
		for(int j = 1; j <= m; j ++) {
			printf("%d ", B[i][j]);
			C[(B[i][j] - 1) / m + 1][j] = B[i][j];//由B构造C
		}
		puts("");
	}
	for(int i = 1; i <= n; i ++) {
		for(int j = 1; j <= m; j ++)
			printf("%d ", C[i][j]);
		puts("");
	}

[CERC2016] 二分毯 Bipartite Blanket

\qquad 题面

\qquad 在做这个题之前,我们先了解一个定理:给定一张二分图。如果存在一个匹配覆盖左部图中的点集 X X X,且存在一个匹配覆盖右部图中的点集 Y Y Y,那么存在一个匹配同时覆盖 X X X Y Y Y。有了这一定理,这道题就变得简单许多。不难发现,此时这一题与上面的 [ A R C 106 E ] M e d a l s [ARC106E] Medals [ARC106E]Medals 很像。不过,他们之间还有很重要的一点不同:这道题想要判断状态 m a s k mask mask 是否合法,还要判断它的子集是否全部合法。也就是说我们要再做一遍高维前缀和来判断。上一个题不需要是因为 m a s k mask mask 的任何一个子集与 m a s k mask mask ∪ N ( v ) \cup N(v) N(v) 都是相同的,所以不用再做一遍。

	//left
	for(int j = 1; j <= m; j ++)
		for(int i = 1; i <= n; i ++)
			f[j] |= ((1 << (i - 1)) * mp[i][j]);
	for(int i = 1; i <= m; i ++) g[f[i]] ++;
	for(int j = 0; j < n; j ++)
		for(int i = 0; i < (1 << n); i ++)
			if((i >> j) & 1) g[i] += g[i ^ (1 << j)];
	for(int i = 0; i < (1 << n); i ++)
		for(int j = 1; j <= n; j ++)
			w[i] += a[j] * ((i >> (j - 1)) & 1);
	memset(v, true, sizeof v);
	for(int i = 0; i < (1 << n); i ++)
		if(m - g[(1 << n) - 1 - i] < num[i]) v[i] = 0;
	for(int j = 0; j < n; j ++)
		for(int i = 0; i < (1 << n); i ++)
			if(((i >> j) & 1) && !v[i ^ (1 << j)]) v[i] = 0;
	for(int i = 0; i < (1 << n); i ++)
		if(v[i]) mat.push_back(w[i]);
	sort(mat.begin(), mat.end());//把合法的左部子集的权值存起来
	//right
	memset(f, 0, sizeof f), memset(g, 0, sizeof g), memset(w, 0, sizeof w);
	for(int i = 1; i <= n; i ++)
		for(int j = 1; j <= m; j ++)
			f[i] |= ((1 << (j - 1)) * mp[i][j]);
	for(int i = 1; i <= n; i ++) g[f[i]] ++;
	for(int j = 0; j < m; j ++)
		for(int i = 0; i < (1 << m); i ++)
			if((i >> j) & 1) g[i] += g[i ^ (1 << j)];
	for(int i = 0; i < (1 << m); i ++)
		for(int j = 1; j <= m; j ++)
			w[i] += b[j] * ((i >> (j - 1)) & 1);
	memset(v, true, sizeof v);
	for(int i = 0; i < (1 << m); i ++)
		if(n - g[(1 << m) - 1 - i] < num[i]) v[i] = 0;
	for(int j = 0; j < m; j ++)
		for(int i = 0; i < (1 << m); i ++)
			if(((i >> j) & 1) && !v[i ^ (1 << j)]) v[i] = 0;
	int len = mat.size();
	long long ans = 0;
	for(int i = 0; i < (1 << m); i ++) {
		if(v[i]) {
			int loc = lower_bound(mat.begin(), mat.end(), T - w[i]) - mat.begin();
			ans += 1LL * (len - loc);//查找当前子集能匹配多少个左部子集
		}
	}
	printf("%lld\n", ans);

你可能感兴趣的:(个人总结,内容总结,算法,c++,图论,经验分享)