二分图之Hall定理小记

Hall 定理

对于二分图 G = ( V , E ) G=(V,E) G=(V,E) ,令 N ( v ) N(v) N(v) 表示点 v v v 的邻居集,则关于图 G G G 的最大匹配我们有如下结论:

Hall 定理 :设二分图 G G G 的两部分分别为 V L V_L VL V R V_R VR ∣ V L ∣ ≤ ∣ V R ∣ |V_L|\le|V_R| VLVR ,则其存在一个大小为 ∣ V L ∣ |V_L| VL 的匹配当且仅当 ∀ S ⊆ V L \forall S\subseteq V_L SVL ,都有 ∣ S ∣ ≤ ∣ ⋃ v ∈ S N ( v ) ∣ |S|\le|\bigcup\limits_{v\in S}N(v)| SvSN(v)

推论 1 :对于一个 k k k 正则二分图(每个点度数都为 k k k ,其中 k ≥ 1 k\ge1 k1 ),若其左右点数相等,那么其必有完美匹配。

推论 2 :设二分图 G G G 的两部分分别为 V L V_L VL V R V_R VR ,则其最大匹配为 ∣ V L ∣ − max ⁡ S ⊆ V L ( ∣ S ∣ − ∣ ⋃ v ∈ S N ( v ) ∣ ) |V_L|-\max\limits_{S\subseteq V_L}(|S|-|\bigcup\limits_{v\in S}N(v)|) VLSVLmax(SvSN(v)) ,也等于 min ⁡ S ⊆ V L ( ∣ V L ∣ − ∣ S ∣ + ∣ ⋃ v ∈ S N ( v ) ∣ ) \min\limits_{S\subseteq V_L}(|V_L|-|S|+|\bigcup\limits_{v\in S}N(v)|) SVLmin(VLS+vSN(v))

广义Hall定理 :给定一张二分图。如果存在一个匹配覆盖左部图中的点集 X X X ,且存在一个匹配覆盖右部图中的点集 Y Y Y ,那么存在一个匹配同时覆盖 X X X Y Y Y

A.HDU 6667 Roundgod and Milk Tea

在这里插入图片描述
这是二分图的最大匹配问题,如果建图直接跑肯定爆炸,考虑使用 H a l l Hall Hall 定理的推论 2 2 2 。现在问题就是 max ⁡ S ⊆ V L ( ∣ S ∣ − ∣ ⋃ v ∈ S N ( v ) ∣ ) \max\limits_{S\subseteq V_L}(|S|-|\bigcup\limits_{v\in S}N(v)|) SVLmax(SvSN(v)) 是多少。显然,对于 S S S ,我们只需要找每一个人,和所有人加起来的集合以及空集就可以了。

#include<bits/stdc++.h>
using namespace std;
int n,a[1000010],b[1000010];
long long sum1,sum2,maxx;
int main()
{
	freopen("tea.in","r",stdin);
	freopen("tea.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&a[i],&b[i]);
		sum1+=a[i];
		sum2+=b[i];
	}
	maxx=max(sum1-sum2,0LL);
	for(int i=1;i<=n;i++)
	maxx=max(maxx,a[i]-(sum2-b[i]));
	printf("%lld",sum1-maxx);
	return 0;
}

B.ARC076D-Exhausted

在这里插入图片描述
感觉跟上一题很像,但是这个 S S S 我们不太好直接确定一个小的范围了。我们先使用推论 2 2 2 把要求的东西写出来。
a n s = n − ( ∣ V L ∣ − max ⁡ S ⊆ V L ( ∣ S ∣ − ⋃ v ∈ S N ( v ) ) ) = max ⁡ S ⊆ V L ( ∣ S ∣ − ⋃ v ∈ S N ( v ) ) ans=n-(|V_L|-\max\limits_{S\subseteq V_L}(|S|-\bigcup\limits_{v\in S}N(v)))=\max\limits_{S\subseteq V_L}(|S|-\bigcup\limits_{v\in S}N(v)) ans=n(VLSVLmax(SvSN(v)))=SVLmax(SvSN(v))
我们注意到这里 S S S 的邻居集是上下两部分,取并很难搞,故将它转成交集来求。
a n s = max ⁡ S ⊆ V L ( ∣ S ∣ − ( m − ( l , r ) ) ) = max ⁡ S ⊆ V L ( ∣ S ∣ + ( l , r ) ) − m ans=\max\limits_{S\subseteq V_L}(|S|-(m-(l,r)))=\max\limits_{S\subseteq V_L}(|S|+(l,r))-m ans=SVLmax(S(m(l,r)))=SVLmax(S+(l,r))m
现在问题就变为了:给定一些区间,任取区间,求区间交大小和区间个数的和的最大值。
我们考虑把区间按左端点排序,枚举区间,用线段树来维护右端点和区间个数。
这样的好处在于,我们加入一个区间之后,对于后面的区间的左端点一定大于前面区间的左端点,这样就可以忽视左端点的影响,只需要考虑右端点即可。同时我们相当于枚举交集的左端点,考虑所有可能的右端点,保证了不重不漏。
细节:因为 ( l , r ) (l,r) (l,r) 有可能为 ϕ \phi ϕ ,所以线段树每个端点存的右端点为 r − 1 r-1 r1 ,也就是叶子结点为空集,这样就避免了讨论空集的情况。

#include<bits/stdc++.h>
using namespace std;
int n,m,ans;
struct node
{
	int l,r;
} a[200010];
struct node1
{
	int l,r,maxx,add;
} tree[800000];
bool mycmp(node x,node y)
{
	return x.l<y.l;
}
void build(int p,int l,int r)
{
	tree[p].l=l;
	tree[p].r=r;
	if(l==r)
	{
		tree[p].maxx=r-1;
		return;
	}
	int mid=(l+r)/2;
	build(2*p,l,mid);
	build(2*p+1,mid+1,r);
	tree[p].maxx=max(tree[2*p].maxx,tree[2*p+1].maxx);
}
void spread(int p)
{
	if(tree[p].add)
	{
		tree[2*p].add+=tree[p].add;
		tree[2*p].maxx+=tree[p].add;
		tree[2*p+1].add+=tree[p].add;
		tree[2*p+1].maxx+=tree[p].add;
		tree[p].add=0;
	}
}
void change(int p,int l,int r)
{
	if(tree[p].l>=l&&tree[p].r<=r)
	{
		tree[p].maxx++;
		tree[p].add++;
		return;
	}
	spread(p);
	int mid=(tree[p].l+tree[p].r)/2;
	if(l<=mid)
	change(2*p,l,r);
	if(r>mid)
	change(2*p+1,l,r);
	tree[p].maxx=max(tree[2*p].maxx,tree[2*p+1].maxx);
}
int ask(int p,int l,int r)
{
	if(tree[p].l>=l&&tree[p].r<=r)
	return tree[p].maxx;
	spread(p);
	int mid=(tree[p].l+tree[p].r)/2,x=0;
	if(l<=mid)
	x=max(x,ask(2*p,l,r));
	if(r>mid)
	x=max(x,ask(2*p+1,l,r));
	return x;
}
int main()
{
	freopen("exhausted.in","r",stdin);
	freopen("exhausted.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	scanf("%d%d",&a[i].l,&a[i].r);
	sort(a+1,a+n+1,mycmp);
	build(1,1,m+1);
	ans=max(n-m,0);//记得空集和 0
	for(int i=1;i<=n;i++)
	{
		change(1,a[i].l+1,a[i].r);
		ans=max(ans,ask(1,a[i].l+1,a[i].r)-a[i].l-m);
	}
	printf("%d",ans);
	return 0;
}

C.CF981F Round Marriage

在这里插入图片描述
先把新郎和新娘从小到大排序,最小的最大考虑二分,二分一个距离之后考虑把新娘的环给破掉,复制两倍,每一个新郎可以配对的就是一段区间,且区间左右端点单调不降。之后使用 H a l l Hall Hall 定理判断是否具有完美匹配。设第 i i i 个新郎能匹配的新娘区间为 [ L i , R i ] [L_i,R_i] [Li,Ri] ,因为连续的一段新郎区间的约束一定比零散的要强,我们只需要考虑连续的新郎区间即可,设新郎区间为 [ l , r ] [l,r] [l,r] (这里也需破环为链)。则需满足 r − l + 1 ≤ R r − L l + 1 ⇒ R r − r ≥ L l − l r-l+1\le R_r-L_l+1 \Rightarrow R_r-r\ge L_l-l rl+1RrLl+1RrrLll 这里我们可以直接枚举新郎,记录最大的 L i − i L_i-i Lii 再与 R i − i R_i-i Rii 判断即可。
对于每一个新郎的匹配新娘区间,因为区间端点单调不降,可以使用双指针来获得。

#include<bits/stdc++.h>
using namespace std;
int n,L,a[600010],b[800010];
bool check(int x)
{
	int ll=1,rr=0,maxx=-1e9;
	for(int i=n+1;i<=3*n;i++)
	{
		while(a[i]-b[ll]>x)
		ll++;
		while(rr<4*n&&b[rr+1]-a[i]<=x)
		rr++;
		maxx=max(maxx,ll-i);
		if(rr-i<maxx)
		return 0;
	}
	return 1;
}
int main()
{
	scanf("%d%d",&n,&L);
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]);
	for(int i=1;i<=n;i++)
	scanf("%d",&b[i]);
	sort(a+1,a+n+1);
	sort(b+1,b+n+1);
	for(int i=1;i<=2*n;i++)
	a[i+n]=a[i]+L;
	for(int i=1;i<=3*n;i++)
	b[i+n]=b[i]+L;
	int l=0,r=L/2;
	while(l+1<r)
	{
		int mid=(l+r)/2;
		if(check(mid))
		r=mid;
		else
		l=mid;
	}
	if(check(l))
	printf("%d",l);
	else
	printf("%d",r);
	return 0;
}

D.[POI2009]LYZ-Ice Skates

在这里插入图片描述
在这里插入图片描述
考虑使用 H a l l Hall Hall 定理判定是否足够。对于 S S S 显然只需要判定连续的一段区间就可以了。设 a [ i ] a[i] a[i] 表示 i i i 号脚有多少人,则就需要判断 max ⁡ ∑ l ≤ i ≤ r a [ i ] ≤ ( r − l + 1 + d ) ⋅ k ⇒ max ⁡ ∑ l ≤ i ≤ r ( a [ i ] − k ) ≤ d ⋅ k \max\sum\limits_{l\le i\le r}a[i]\le(r-l+1+d)\cdot k\Rightarrow\max\sum\limits_{l\le i\le r}(a[i]-k)\le d\cdot k maxlira[i](rl+1+d)kmaxlir(a[i]k)dk ,发现这就是最大子段和,使用线段树维护即可。

#include<bits/stdc++.h>
using namespace std;
int n,m,k,d;
struct node
{
	int l,r;
	long long lmax,rmax,maxx,sum;
} tree[800000];
void build(int p,int l,int r)
{
	tree[p].l=l;
	tree[p].r=r;
	if(l==r)
	{
		tree[p].lmax=-k;
		tree[p].maxx=-k;
		tree[p].rmax=-k;
		tree[p].sum=-k;
		return;
	}
	int mid=(l+r)/2;
	build(2*p,l,mid);
	build(2*p+1,mid+1,r);
	tree[p].lmax=max(tree[2*p].lmax,tree[2*p].sum+tree[2*p+1].lmax);
	tree[p].rmax=max(tree[2*p+1].rmax,tree[2*p+1].sum+tree[2*p].rmax);
	tree[p].maxx=max({tree[2*p].maxx,tree[2*p+1].maxx,tree[2*p].rmax+tree[2*p+1].lmax});
	tree[p].sum=tree[2*p].sum+tree[2*p+1].sum;
}
void add(int p,int x,int y)
{
	if(tree[p].l==x&&tree[p].r==x)
	{
		tree[p].lmax+=y;
		tree[p].maxx+=y;
		tree[p].rmax+=y;
		tree[p].sum+=y;
		return;
	}
	int mid=(tree[p].l+tree[p].r)/2;
	if(x<=mid)
	add(2*p,x,y);
	else
	add(2*p+1,x,y);
	tree[p].lmax=max(tree[2*p].lmax,tree[2*p].sum+tree[2*p+1].lmax);
	tree[p].rmax=max(tree[2*p+1].rmax,tree[2*p+1].sum+tree[2*p].rmax);
	tree[p].maxx=max({tree[2*p].maxx,tree[2*p+1].maxx,tree[2*p].rmax+tree[2*p+1].lmax});
	tree[p].sum=tree[2*p].sum+tree[2*p+1].sum;
}
int main()
{
	freopen("skates.in","r",stdin);
	freopen("skates.out","w",stdout);
	scanf("%d%d%d%d",&n,&m,&k,&d);
	build(1,1,n-d);
	while(m--)
	{
		int r,x;
		scanf("%d%d",&r,&x);
		add(1,r,x);
		if(tree[1].maxx<=1LL*d*k)
		puts("TAK");
		else
		puts("NIE");
	}
	return 0; 
}

E.CF103E Buying Sets

在这里插入图片描述
先建一个网络流图,左边是集合,右边是元素,集合里有哪个元素就向哪个元素连一条容量为正无穷的边,发现这很权闭合图,如果把权值取反,把元素的点权当成 0 0 0 ,就是求最大权闭合图,如果没有"子集并的大小等于子集个数"这个条件,直接跑最大权闭合图就可以了(虽然没有这个条件直接枚举就行…),加上这个条件怎么做呢?考虑一个神奇构造,我们把所有集合的点权加上 i n f inf inf ,所有元素的点权减去 i n f inf inf (注意这个 i n f inf inf 小于中间连的边的正无穷,又远大于原先子集的点权)。这么做有两点好处:1.所有集合都与源点相连,所有元素都与汇点相连。2.最小割的割边数一定等于 n n n
好处 1 1 1 显然,好处 2 2 2 证明如下:因为任意 k k k 个子集的并的大小 ≥ k \ge k k ,所以最小割的割边数一定 ≥ n \ge n n(反证法自证),又因为最小割的割边数可以等于 n n n (全割一边),以及如果比 n n n 多割一条边因为增加了 i n f inf inf 所以一定不为最小割,所以最小割的割边数一定等于 n n n
最大权闭合图的最小割有一个实际意义:割掉与源点相连的边相当于不选这个点,割掉与汇点相连的边相当于选这个点。配合好处 2 2 2 ,我们容易得到这个图的最小割一定会使集合数等于元素数。综上,这样构造完后直接跑最小割求最大值即可(最后记得对答案取反)。

#include<bits/stdc++.h>
using namespace std;
int n,S,T,head[1000],tot=-1,d[1000],cur[1000];
long long ans;
struct node
{
	int to,nex;
	long long v;
} edge[200000];
void add(int x,int y,long long z)
{
	edge[++tot].nex=head[x];
	edge[tot].to=y;
	edge[tot].v=z;
	head[x]=tot;
}
bool bfs()
{
	queue<int> q;
	memset(d,-1,sizeof d);
	q.push(S);
	d[S]=0,cur[S]=head[S];
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];~i;i=edge[i].nex)
		{
			if(d[edge[i].to]==-1&&edge[i].v)
			{
				d[edge[i].to]=d[x]+1;
				cur[edge[i].to]=head[edge[i].to];
				if(edge[i].to==T)
				return 1;
				q.push(edge[i].to); 
			}
		}
	}
	return 0;
}
long long find(int x,long long limit)
{
	if(x==T)
	return limit;
	long long flow=0;
	for(int i=cur[x];~i&&flow<limit;i=edge[i].nex)
	{
		cur[x]=i;
		if(d[edge[i].to]==d[x]+1&&edge[i].v)
		{
			long long c=find(edge[i].to,min(limit-flow,edge[i].v));
			if(!c)
			d[edge[i].to]=-1;
			flow+=c;
			edge[i].v-=c;
			edge[i^1].v+=c;
		}
	}
	return flow;
}
long long Dinic()
{
	long long ans=0,flow;
	while(bfs())
	{
		while(flow=find(S,1e18))
		ans+=flow;
	}
	return ans;
}
int main()
{
	freopen("z.in","r",stdin);
	freopen("z.out","w",stdout);
	scanf("%d",&n);
	memset(head,-1,sizeof head);
	T=2*n+1;
	for(int i=1;i<=n;i++)
	{
		int t;
		scanf("%d",&t);
		for(int j=1;j<=t;j++)
		{
			int x;
			scanf("%d",&x);
			add(i,n+x,1e18);
			add(n+x,i,0);
		}
	}
	for(int i=1;i<=n;i++)
	{
		int p;
		scanf("%d",&p);
		p=-p;
		add(S,i,p+1e9);
		add(i,S,0);
		ans+=(p+1e9);
		add(i+n,T,1e9);
		add(T,i+n,0);
	}
	printf("%lld",-(ans-Dinic()));
	return 0;
}

F. [ARC106E] Medals

在这里插入图片描述
在这里插入图片描述
首先二分答案转化为判定性问题,对于 1 1 1 个人至少 k k k 次可以转化为 k k k 个周期相同的人至少 1 1 1 。对于判定,我们使用 H a l l Hall Hall 定理,因为 n n n 很小,所以 S S S 集合数量不多,考虑状压,我们考虑一下二分的上界是多少, a = 1 a=1 a=1 的周期约束性最强,也就是说最坏的情况是每两天匹配一个人,所以 r = 2 n k r=2nk r=2nk 。所以天数是在我们可以接受的范围内,我们考虑预处理出每一天有哪些人休息,状压成一个状态,那么对于这个状态的子集,都会缺少这一天,做一遍高维前缀和就可以处理出每一个 S S S 的缺少的天数,用二分值一减就是邻居集大小,直接判断即可。

#include<bits/stdc++.h>
using namespace std;
int n,k,a[20],st[3600010],f[300000],num[300000];
bool check(int x)
{
	memset(f,0,sizeof f);
	for(int i=1;i<=x;i++)
	f[st[i]]++;
	for(int i=0;i<n;i++)
	{
		for(int j=(1<<n)-1;~j;j--)//高维前缀和内层循环顺序无影响
		if(((j>>i)&1)==0)
		f[j]+=f[j^(1<<i)];
	}
	for(int i=1;i<(1<<n);i++)
	if(x-f[i]<num[i]*k)
	return 0;
	return 1;
}
int main()
{
	freopen("medals.in","r",stdin);
	freopen("medals.out","w",stdout);
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]);
	for(int i=1,now=0;i<=2*n*k;i++)
	{
		st[i]=now;
		for(int j=1;j<=n;j++)
		if(i%a[j]==0)
		now^=(1<<(j-1));
	}
	for(int i=1;i<(1<<n);i++)
	num[i]=num[i&(i-1)]+1;
	int l=n*k,r=2*n*k;
	while(l+1<r)
	{
		int mid=(l+r)/2;
		if(check(mid))
		r=mid;
		else
		l=mid;
	}
	if(check(l))
	printf("%d",l);
	else
	printf("%d",r);
	return 0;
}

G. [AGC029F] Construction of a tree

在这里插入图片描述

在这里插入图片描述
A = { E 1 , E 2 , . . . , E n − 1 } , S ⊆ A 且 S ≠ ϕ A=\{E_1,E_2,...,E_{n-1}\},S\subseteq A 且 S \ne \phi A={E1,E2,...,En1},SAS=ϕ f ( S ) f(S) f(S) 表示 S S S 中每个集合元素并集的大小,那么有 f ( S ) > ∣ S ∣ f(S)>|S| f(S)>S ,不然不可能构成一颗树。发现这个十分像 H a l l Hall Hall 定理,先建图,左部图是集合,右部图是元素,集合连向自己含有的元素。跑一个二分图最大匹配,如果最大匹配大小不等于 n − 1 n-1 n1 ,则一定无解。
之后考虑如何构造方案。我们从没有匹配到的那个点 x x x 出发,做 d f s dfs dfs ,访问所有没有访问过的集合,再从集合出发,访问集合所匹配的点 y y y ,那么这个集合的选出就是 x , y x,y x,y 。之后从 y y y 点重复上述操作。做完之后,如果元素没有全部被访问过,说明无解,因为,访问过的元素数等于访问过的集合数加 1 1 1 ,剩下的集合数等于剩下的元素数,而剩下的集合的连边一定在剩下的元素之中,也就是说 S ≥ f ( S ) S\ge f(S) Sf(S) ,与有解条件矛盾,故无解;如果有解,直接输出方案即可。

#include<bits/stdc++.h>
using namespace std;
int n,S,T,head[300000],tot=-1,d[300000],cur[300000],st;
bool v[300000];
struct node
{
	int l,r,id;
	bool operator <(const node &x)const{
		return id<x.id;
	}
};
vector<node> s; 
struct node1
{
	int to,nex,v;
} edge[1000000];
void add(int x,int y,int z)
{
	edge[++tot].nex=head[x];
	edge[tot].to=y;
	edge[tot].v=z;
	head[x]=tot;
}
bool bfs()
{
	queue<int> q;
	q.push(S);
	memset(d,-1,sizeof d);
	d[S]=0,cur[S]=head[S];
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];~i;i=edge[i].nex)
		{
			if(d[edge[i].to]==-1&&edge[i].v)
			{
				d[edge[i].to]=d[x]+1;
				cur[edge[i].to]=head[edge[i].to];
				if(edge[i].to==T)
				return 1;
				q.push(edge[i].to);
			}
		}
	}
	return 0;
}
int find(int x,int limit)
{
	if(x==T)
	return limit;
	int flow=0;
	for(int i=cur[x];~i&&flow<limit;i=edge[i].nex)
	{
		cur[x]=i;
		if(d[edge[i].to]==d[x]+1&&edge[i].v)
		{
			int c=find(edge[i].to,min(edge[i].v,limit-flow));
			if(!c)
			d[edge[i].to]=-1;
			flow+=c;
			edge[i].v-=c;
			edge[i^1].v+=c;
		}
	}
	return flow;
}
int Dinic()
{
	int ans=0,flow;
	while(bfs())
	{
		while(flow=find(S,(int)1e9))
		ans+=flow;
	}
	return ans;
}
void dfs(int x,int lx,int fa,int ls)
{
	v[x]=1;
	if(lx==2)
	s.push_back((node){fa,x,ls});
	if(lx==0||lx==2)
	{
		for(int i=head[x];~i;i=edge[i].nex)
		{
			if(edge[i].to!=T&&!v[edge[i].to])
			dfs(edge[i].to,1,x,edge[i].to);
		}
		return;
	}
	for(int i=head[x];~i;i=edge[i].nex)
	{
		if(!edge[i].v)
		{
			dfs(edge[i].to,2,fa,ls);
			return;
		}
	}
}
int main()
{
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
	scanf("%d",&n);
	T=2*n;
	memset(head,-1,sizeof head);
	for(int i=1;i<n;i++)
	{
		int c;
		scanf("%d",&c);
		for(int j=1;j<=c;j++)
		{
			int x;
			scanf("%d",&x);
			add(i,n-1+x,1);
			add(n-1+x,i,0);
		}
		add(S,i,1);
		add(i,S,0);
		add(n-1+i,T,1);
		add(T,n-1+i,0);
	}
	add(2*n-1,T,1);
	add(T,2*n-1,0);
	if(Dinic()<n-1)
	{
		printf("-1");
		return 0;
	}
	for(int i=0;i<=tot;i+=2)
	{
		if(edge[i].to==T&&edge[i].v==1)
		{
			st=edge[i^1].to;
			break;
		}
	}
	dfs(st,0,0,0);
	if(s.size()<n-1)
	{
		printf("-1");
		return 0;
	}
	sort(s.begin(),s.end());
	for(int i=0;i<s.size();i++)
	printf("%d %d\n",s[i].l-(n-1),s[i].r-(n-1));
	return 0;
}

H. [AGC037D] Sorting a Grid

在这里插入图片描述
在这里插入图片描述
考虑从 D D D 倒退 C C C ,不妨先把 D D D 染色, D D D 的每一行是一种相同的颜色,不同行颜色不同。那么 C C C 的每一行颜色一定相同, B , C B,C B,C 每一列的颜色种类一定为 n n n 。从 B B B 变到 C C C 是简单的,那么现在的问题就是:给定一个矩阵 A A A ,矩阵中每一个元素都有一个颜色,一共有 n n n 种颜色,每一种颜色都有 m m m 个元素,只通过行变换使得每一列都恰好有 n n n 种颜色。
考虑这样一个构造:构建一个二分图,左边有 n n n 个节点,每个节点代表 A A A 矩阵中的每一行, 右边也是 n n n 个节点,每个节点代表一种颜色,每个行节点向本行有的颜色连边(这个颜色有几个元素就连几条边)。那么跑一次二分图完美匹配就相当于每行选择一个颜色,正好有 n n n 种,也就是一列的选择,做 m m m 次即可。一定可以做 m m m 次完美匹配吗?考虑当做到第 k k k 次时,每一个行节点向外连的边是 m − k + 1 m-k+1 mk+1 条,每一个颜色节点的入边也是 m − k + 1 m-k+1 mk+1 条,也就是说这是一个 m − k + 1 m-k+1 mk+1 正则二分图,因为左右两边节点数相同,根据推论 1 1 1 ,一定有完美匹配。

#include<bits/stdc++.h>
using namespace std;
int n,m,S,T,head[210],tot=-1,b[110][110],d[210],cur[210],co[10010];
queue<int> a[210][210];
struct node
{
	int to,nex,v;
} edge[30000];
void add(int x,int y,int z)
{
	edge[++tot].nex=head[x];
	edge[tot].to=y;
	edge[tot].v=z;
	head[x]=tot;
}
bool bfs()
{
	queue<int> q;
	q.push(S);
	memset(d,-1,sizeof d);
	d[S]=0,cur[S]=head[S];
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		for(int i=head[x];~i;i=edge[i].nex)
		{
			if(d[edge[i].to]==-1&&edge[i].v)
			{
				d[edge[i].to]=d[x]+1;
				cur[edge[i].to]=head[edge[i].to];
				if(edge[i].to==T)
				return 1;
				q.push(edge[i].to);
			}
		}
	}
	return 0;
}
int find(int x,int limit)
{
	if(x==T)
	return limit;
	int flow=0;
	for(int i=cur[x];~i&&flow<limit;i=edge[i].nex)
	{
		cur[x]=i;
		if(d[edge[i].to]==d[x]+1&&edge[i].v)
		{
			int c=find(edge[i].to,min(edge[i].v,limit-flow));
			if(!c)
			d[edge[i].to]=-1;
			flow+=c;
			edge[i].v-=c;
			edge[i^1].v+=c;
		}
	}
	return flow;
}
void Dinic()
{
	while(bfs())
	while(find(S,(int)1e9));
}
bool mycmp(int x,int y)
{
	return co[x]<co[y];
}
int main()
{
	freopen("grid.in","r",stdin);
	freopen("grid.out","w",stdout);
	scanf("%d%d",&n,&m);
	T=2*n+1;
	memset(head,-1,sizeof head);
	for(int i=1;i<=n;i++)
	{
		add(S,i,1);
		add(i,S,0);
		for(int j=1;j<=m;j++)
		{
			int x,c;
			scanf("%d",&x);
			c=(x-1)/m+1;
			co[x]=c;
			a[i][c].push(x);
			add(i,n+c,1);
			add(n+c,i,0);
		}
	}
	for(int i=1;i<=n;i++)
	{
		add(n+i,T,1);
		add(T,n+i,0);
	}
	for(int i=1;i<=m;i++)
	{
		Dinic();
		for(int j=0;j<=tot;j+=2)
		{
			if(edge[j^1].to==S)
			{
				edge[j].v=1;
				edge[j^1].v=0;
			}
			else if(edge[j].to>n&&edge[j].to<=2*n)
			{
				if(edge[j].v==0&&edge[j^1].v==1)
				{
					b[edge[j^1].to][i]=a[edge[j^1].to][edge[j].to-n].front();
					a[edge[j^1].to][edge[j].to-n].pop();
					edge[j^1].v=0;
				}
			}
			else
			{
				edge[j].v=1;
				edge[j^1].v=0;
			}
		}
	}
	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<=m;i++)
	{
		vector<int> s;
		for(int j=1;j<=n;j++)
		s.push_back(b[j][i]);
		sort(s.begin(),s.end(),mycmp);
		for(int j=1;j<=n;j++)
		b[j][i]=s[j-1];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		printf("%d ",b[i][j]);
		puts("");
	}
	return 0;
}

I. [CERC2016]二分毯 Bipartite Blanket

在这里插入图片描述
根据广义 H a l l Hall Hall 定理,我们只需要分别计算 A , B A,B A,B 中有多少个点集可以被覆盖,最后合并统计答案即可。
发现 n n n 很小,那么我们可以对每个子集分别计算,对于一个子集可以被覆盖,需要本身满足 ∣ S ∣ ≤ ∣ ⋃ v ∈ S N ( v ) ∣ |S|\le|\bigcup\limits_{v\in S}N(v)| SvSN(v),同时子集也需要满足,我们可以先算出本身是否满足,然后高维前缀和即可。
算出 A , B A,B A,B 分别有哪几个集合可以被覆盖(记得算上空集),排序之后双指针统计答案即可。

#include<bits/stdc++.h>
using namespace std;
int n,m,id[30],f[1100000],v[30],w[30],g[1100000],num[1100000],t;
long long ans;
bool h[1100000];
char s[30][30];
vector<int> a,b;
inline int lowbit(int x)
{
	return x&(-x);
}
int main()
{
	freopen("blanket.in","r",stdin);
	freopen("blanket.out","w",stdout);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	scanf("%s",s[i]+1);
	for(int i=1;i<=n;i++)
	scanf("%d",&v[i]);
	for(int i=1;i<=m;i++)
	scanf("%d",&w[i]);
	for(int i=1;i<=m;i++)
	{
		int now=0;
		for(int j=1;j<=n;j++)
		{
			if(s[j][i]=='0')
			now^=(1<<(j-1));
		}
		f[now]++;
	}
	for(int i=0;i<n;i++)
	{
		for(int j=(1<<n)-1;~j;j--)
		if((j>>i)&1)
		f[j^(1<<i)]+=f[j];
	}
	for(int i=1;i<(1<<n);i++)
	{
		g[i]=g[i&(i-1)]+v[(int)log2(lowbit(i))+1];
		if(m-f[i]>=__builtin_popcount(i))
		h[i]=1;
	}
	h[0]=1;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<(1<<n);j++)
		{
			if((j>>i)&1)
			h[j]&=h[j^(1<<i)];
		}
	}
	for(int i=0;i<(1<<n);i++)
	{
		if(h[i])
		a.push_back(g[i]);
	}
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++)
	{
		int now=0;
		for(int j=1;j<=m;j++)
		{
			if(s[i][j]=='0')
			now^=(1<<(j-1));
		}
		f[now]++;
	}
	for(int i=0;i<m;i++)
	{
		for(int j=(1<<m)-1;~j;j--)
		if((j>>i)&1)
		f[j^(1<<i)]+=f[j];
	}
	memset(h,0,sizeof h);
	for(int i=1;i<(1<<m);i++)
	{
		g[i]=g[i&(i-1)]+w[(int)log2(lowbit(i))+1];
		if(n-f[i]>=__builtin_popcount(i))
		h[i]=1;
	}
	h[0]=1;
	for(int i=0;i<m;i++)
	{
		for(int j=0;j<(1<<m);j++)
		{
			if((j>>i)&1)
			h[j]&=h[j^(1<<i)];
		}
	}
	for(int i=0;i<(1<<m);i++)
	{
		if(h[i])
		b.push_back(g[i]);
	}
	scanf("%d",&t);
	sort(a.begin(),a.end());
	sort(b.begin(),b.end());
	int l=a.size();
	for(int i=0;i<b.size();i++)
	{
		while(l>0&&a[l-1]+b[i]>=t)
		l--;
		ans+=a.size()-l;
	}
	printf("%lld",ans);
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值