把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

LOJ6160 美团 CodeM 初赛 Round A 二分图染色【容斥】

题目链接

题目解析

考试的时候我是在按照点在想,可以把点分成四类:只有绿色边,有一条红色边,有一条蓝色边,有一条红色边和一条蓝色边(后三种其余都是绿色边),然后把\(n\)分解成四个整数的和,再分别计数。但是一个点的情况可能会影响到其它与之有边相连的点,所以不好做。

不要怕,直接来

还是从边的角度来考虑。

由于绿色没有影响,所以我们把它当作不染色。先算只有一种颜色的染色方案。设\(f(n)\)表示对\(n\)个点的二分图的染色方案,由于同一个点只能染一条边,先枚举一个有染色边的点数\(i\),然后就相当于从这\(n\)个点中选\(i\)个点出来,连到另一边的点去,而另一边的点也不能同时连两条有色边,所以另一边也要选\(i\)个点出来。注意到我一边选点的时候不在乎顺序,但是连到另一边的时候,另一边的点与这一边的点的对应情况不一样,那么连出来的边就不一样,所以另一边的点有顺序。

得到\(f(n)=\sum_{i=0}^nC_n^iA_n^i\)

现在有两种颜色,如果直接算\(f^2(n)\)会把一条边染上两种颜色,现在考虑一下怎么减去不合法的方案。可以用容斥原理,我们钦定一些边让它们被染上两种颜色,总方案数\(=\)钦定\(0\)条边被染两种颜色,其它随便\(-\)钦定\(1\)条边被染两种颜色,其它随便\(+\)钦定\(2\)条边被染两种颜色,其它随便\(……\)

具体而言,\(ans=\sum_{i=0}^n (-1)^i*C(n,i)*A(n,i)*f^2(n-i)\)

\((-1)^i\)是容斥系数,\(C(n,i)*A(n,i)\)是钦定\(i\)条边出来染两种颜色,原理同上,\(*f^2(n-i)\)是剩下的边随便染色。

组合数可以预处理,但是\(f( i)\)\(n^2\)求,现在,我们就有了一个\(n^2\)的优秀算法。


瓶颈在\(f(i)\)的求法,我们考虑能不能不用通项公式,而是\(O(n)\)递推出来。

我们考虑在\(n-1\)对点的外面再加上一对点会有哪些方案:

  1. 新加点的边不染色,方案数是\(f(n-1)\)
  2. 新加点之间的边染色,方案数是\(f(n-1)\)
  3. 新加点的其中一个与另一边原来的\(n-1\)个点之间的边选一条染色,方案数是\((n-1)f(n-1)\)
  4. 同理,新加点的另一个与另一边原来的\(n-1\)个点之间的边选一条染色,方案数是\((n-1)f(n-1)\)
  5. 上面四种情况的方案数加起来,我们发现\(4,5\)可能与原来的点存在冲突,也就是我其中一个点和另一边原来点连边的时候,那个原来点可能与另一个原来点已经有染色边。这个怎么解决呢?我们可以假设如果有冲突,就把冲突的点连给另外那个新增点。举个例子:\(a,b\)是新增点,\(a\)往对面的原来点\(x\)连边,而\(x\)又已经与原来点\(y\)有一条染色边,那么我们把\(x-y\)这条边改成\(b-y\)。这样改动之后,不会有冲突,但是\(4,5\)中有算重的方案,所以我们还要减去:\(a\)与对面原来\(n-1\)个点连边,\(b\)与对面\(n-1\)个点连边的方案数(这其实是一个小容斥),为\((n-1)^2*f[n-2]\)

然后得到递推关系\(f(n)=2n*f(n-1)-(n-1)^2*f[n-2]\)

(因为我懒,没有画图XD,所以不太清楚的地方可以手动画图~


转化一下问题

棋盘问题可以转化为二分图来解决,那么这个二分图也可以转化为棋盘。

把二分图的\(X\)部放在\(x\)轴,\(Y\)部放在\(y\)轴,那么每一个格点就代表了一条边,我们对格点染色。

同种颜色的边不能在一个结点上,也就是同一行同一列只能有一个这个颜色,就是在棋盘上放车的问题。

同样地,先考虑一种车,可以得到\(f(n)=\sum_{i=0}^nC_n^iA_n^i\),即在\(n\)行中选出\(i\)行放车,选出来之后它们的列可以顺次有\(n,n-1,n-2...n-i+1\)种选择,当然也可以理解成有顺序地选。(这个似乎比二分图染色直观一点

还是一样的容斥,钦定有\(i\)个两个颜色的车放在了同一个格点。\(ans=\sum_{i=0}^n (-1)^i*C(n,i)*A(n,i)*f^2(n-i)\)

同样的考虑递推式。从\(f(n-1)\)\(f(n)\)相当于外面多了一圈格点共\(2n-1\)个出来。

我们枚举选择周围的一圈格点,然后删掉那一行一列,剩下的\(n-1\)行,\(n-1\)列化归成\(f(n)\)

还有一种方案是外面这一圈一个都不选,所以一共有\(2n\)种,再乘上化归之后的\(f (n-1)\)

那个,我之前想错了,我以为是外面这一圈是或者的关系,然后乘上里面的那个原来的\(n-1\)的棋盘,那这样的方案(如下图)就没有办法算:

但实际上是可以的,按照前面的正确思路理解,就是删掉选中的那一行那一列,剩下的最后一列还是在化归的方案里。

但是还是有算重的情况,就是下图的情况,它在行,列里面都算过,要减去。

(其实也可以想成之前的那种替换的想法,如果有冲突,就把冲突的点搬走,然后就相当于有了多出来的行列都有格子选的情况,但是这样会算重,就是一种方案可以是由行来算,冲突搬到列去,也可以是列来算,把冲突搬到行里去)


►Code View

#include<cstdio>
#include<algorithm>
#include<queue>
#include<set>
using namespace std;
#define N 10000005
#define MOD 1000000007
#define INF 0x3f3f3f3f3f3f3f3f
#define LL long long
LL rd()
{
	LL x=0,f=1;char c=getchar();
	while(c<'0'||c>'9'){if(c=='-') f=-1; c=getchar();}
	while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
	return f*x;
}
int n;
LL fac[N],inv[N],f[N];
LL ksm(LL a,LL b)
{
	LL res=1ll;
	while(b)
	{
		if(b&1) res=res*a%MOD;
		a=a*a%MOD;
		b>>=1;
	}
	return res;
}
void Init()
{
	fac[0]=1,inv[0]=1;
	for(int i=1;i<=N-5;i++)
		fac[i]=fac[i-1]*i%MOD;
	inv[N-5]=ksm(fac[N-5],MOD-2);
	for(int i=N-6;i>=1;i--)
		inv[i]=inv[i+1]*(i+1)%MOD;
}
LL C(int a,int b)
{
	return fac[a]*inv[a-b]%MOD*inv[b]%MOD;
}
LL A(int a,int b)
{
	return fac[a]*inv[a-b]%MOD;
}
int main()
{
	n=rd();
	Init();
	f[0]=1,f[1]=2;//边界 一条边 染/不染
	for(int i=2;i<=n;i++)
		f[i]=(2ll*i*f[i-1]%MOD-1ll*(i-1)*(i-1)%MOD*f[i-2]%MOD+MOD)%MOD;
	//for(int i=2;i<=n;i++)
	//	printf("%lld\n",f[i]);
	LL ans=0;
	for(int i=0;i<=n;i++)
	{
		LL res=C(n,i)*A(n,i)%MOD*f[n-i]%MOD*f[n-i]%MOD;
		if(i&1) ans=(ans-res+MOD)%MOD;
		else ans=(ans+res)%MOD;
	}
	printf("%lld\n",ans);
	return 0;
}
/*
f(n)=sum i=0->n C(n,i)*A(n,i)
ans= sum i=0->n (-1)^i*C(n,i)*A(n,i)*f(n-i)^2
f(n)=2n*f(n-1)-(n-1)^2*f(n-2)
*/
posted @ 2020-12-03 22:43  Starlight_Glimmer  阅读(130)  评论(0编辑  收藏  举报
浏览器标题切换
浏览器标题切换end