【JZOJ5870】【NOIP2018模拟9.15】地图 (递推+DP+组合数学)

本文探讨了一类图论问题的解决方案,通过DP方法高效地处理链和环的组合,实现了复杂度为O(n^2)的算法。文章详细介绍了两种DP策略,一种涉及递推、组合数学和容斥原理,另一种则更简洁,直接处理链和环的构建过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Problem

这里写图片描述

Hint

这里写图片描述

Solution

  • 首先,图中只会存在链和环。
  • 记图中有one个度数为1的点,two个度数为2的点。囿于每条链有两个度数为1的点(链的两端),链的数量是确定的:one2\frac{one}22one
  • 这时,我灵(nao)光(zi)一(wa)闪(te),想到了一个优(sha)美(bi)的方法。

我的SB方法:递推+组合数学+容斥

  • 观察到链和环的方案相对独立。那么根据乘法原理,我们可以求出链的方案tot1,环的方案tot2,总方案即为tot1*tot2。
  • 那么分类讨论一下。
链的情况
  • 我们先令one个点两两匹配,构成one2\frac{one}22one条只含两个点的链。
  • 不妨枚举当前有n条链。假设增加一条,则点数增加为2*n+2。
  • 枚举点1连接的是哪个点,这有2n+1种可能;而剩下的2n个点,两两匹配成i条链。
  • 因此,递推式为hn+1=(2∗n+1)∗hnh_{n+1}=(2*n+1)*h_nhn+1=(2n+1)hn,其中n为链数。

  • 现在,我们再将度数为2的点插入到这些链中。
  • gig_igi表示有i个度数为2的点在链中。新插一个点,我们可以插在所有链的非链首节点的左边。譬如下图:
    在这里插入图片描述
  • 我们可以插在任意一个蓝点左边,因此有3+2=5种方案。实际上,记有k条链,第i条链的链长为lenilen_ileni,插入新点的方案即为∑i=1kleni−1\sum_{i=1}^k len_i-1i=1kleni1。 不难发现,这其实等于已插入的点数+链数
  • 因此,gi=gi−1∗(i−1+one2)g_i=g_{i-1}*(i-1+\frac{one}2)gi=gi1(i1+2one),其中one2\frac{one}22one为链数。
环的情况
  • 环的情况就有些复杂了。
  • 考虑DP。设fi,jf_{i,j}fi,j表示i个度数为2的点在环上,其中有j个一元环的方案数。
  • 囿于原图不存在自环,我们最终得到的应是fi,0f_{i,0}fi,0
  • 然后可以得到三种转移:1.新建一个一元环;2.令当前点加入到一个一元环中;3.令当前点加入到一个多元环中。

  • 然而还有一个坑点——那就是环翻转一下,和原来全等,但是我们会算重。
  • 不妨在新建环的时候,就将其贡献记为12\frac 1221,这样最终算出的结果便是去了重的。
  • 然而,我们这样只能算出无自环的方案数,不能算出无二元环(重边)的方案数。

  • 考虑容斥。
  • 枚举有i个度数为2的点在环上,其中有j个二元环。那么正负性为(−1)j(-1)^j(1)j,系数为Ci2j∗hj∗2−jC_i^{2j}*h_j*2^{-j}Ci2jhj2j,其中hjh_jhj为上述提到的,点两两匹配的方案数。
  • 系数中有个2−j2^{-j}2j的原因是我们把二元环的贡献都算成了12\frac 1221(建环时是12\frac 1221,再插一个点是1,12∗1=12\frac 12*1=\frac 12211=21),然而二元环的贡献应是1;于是在去掉二元环的方案中,我们应该也乘回这些12\frac 1221以弥补二元环的缺失。
  • 然后对于每个i,都求一波∑j=0⌊i2⌋(−1)j∗Ci2j∗hj∗2−j∗fi−2j,0\sum_{j=0}^{\lfloor \frac i2 \rfloor} (-1)^j*C_i^{2j}*h_j*2^{-j}*f_{i-2j,0}j=02i(1)jCi2jhj2jfi2j,0,而这便是真正的fif_ifi(无自环、无二元环的方案数)。

  • 时间复杂度:O(n2)O(n^2)O(n2)
Code
#include <bits/stdc++.h>
#define P(x,y) x=((x)+(y))%mo
#define T(x,y) x=((x)*(y))%mo
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
typedef long long ll;

const int N=4001;
const ll mo=998244353;
int k,n,d,one,two;
ll i,j,ls,dou[N],lian[N],div2,f[N][N],g,xs,C[N][N],zf,ans;

ll fpow(ll x,ll y)
{
	ll ans=1;
	for(;y;y>>=1,T(x,x)) if(y&1)T(ans,x);
	return ans;
}

int main()
{
	freopen("map.in","r",stdin);
	freopen("map.out","w",stdout);
	scanf("%d",&n);
	fo(i,1,n) 
	{
		scanf("%d",&d);
		d&1 ? one++ : two++;
	}
	if(one&1) {puts("0"); return 0;}
	
	dou[0]=1;
	fo(i,1,n) dou[i]=dou[i-1]*(i*2-1)%mo;
	lian[0]=dou[ls=one>>1];
	fo(i,1,two) lian[i]=lian[i-1]*(ls+i-1)%mo;
	
	f[0][0]=1; div2=fpow(2,mo-2);
	fo(i,0,two-1)
		fo(j,0,i)
			if(f[i][j])
			{
				P(f[i+1][j+1],f[i][j]*div2);
				P(f[i+1][j],f[i][j]*(i-j));
				if(j) P(f[i+1][j-1],f[i][j]*j);
			}
	
	fo(i,0,two)
	{
		C[i][0]=1;
		fo(j,1,i) C[i][j]=(C[i-1][j-1]+C[i-1][j])%mo;
	}
	
	fo(i,0,two) 
	{
		g=0;
		fo(j,0,i>>1)
		{
			xs=C[i][j<<1]*dou[j]%mo*fpow(div2,j)%mo;
			zf=(j&1?-1:1);
			P(g,zf*xs*f[i-j*2][0]); P(g,mo);
		}
		
		P(ans,g*C[two][i]%mo*lian[two-i]);
	}
	
	printf("%lld",ans);
}

一个更为舒服的方法

  • 实际上,这道题一个DP就解决了。
  • fi,jf_{i,j}fi,j表示i个度数为2的点,其中j个点在环上(即剩下的i-j个点在链上)的方案数。
  • 有以下三种转移:
    fi,j={fi−3,j−3∗Ci−12新建三元环fi−1,j−1∗(j−1)将点i插入一个环中fi−1,j   ∗(i−j−1+one2)将点i插入一条链中 f_{i,j}=\left\{ \begin{aligned} &amp;f_{i-3,j-3} *C_{i-1}^2 &amp; &amp; 新建三元环 \\ &amp;f_{i-1,j-1} *(j-1) &amp; &amp; 将点i插入一个环中 \\ &amp;f_{i-1,j} \ \ \ *(i-j-1+\frac{one}2) &amp; &amp; 将点i插入一条链中 \end{aligned} \right. fi,j=fi3,j3Ci12fi1,j1(j1)fi1,j   (ij1+2one)ii
  • 新建环的贡献为何是Ci−12C_{i-1}^2Ci12呢?我们假定点i就在新环内,然后从剩下的i-1个点中选2个出来陪它。如若不然,则有可能算重。然后不必再因会翻转除以2,因为三元环定是唯一的。

  • 这样的话,ans=∑i=0twoftwo,ians=\sum_{i=0}^{two} f_{two,i}ans=i=0twoftwo,i,其中two为度数为2的节点的个数。
  • 但还没把链首、链尾两两匹配的方案数算上,所以最后要再乘上。
  • 时间复杂度:O(n2)O(n^2)O(n2)

Code

#include <bits/stdc++.h>
#define P(x,y) x=(x+y)%mo
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
typedef long long ll;

const int N=2001;
const ll mo=998244353;
int n,d,two;
ll i,j,one,f[N][N],ans;

int main()
{
	freopen("map.in","r",stdin);
	freopen("map.out","w",stdout);
	scanf("%d",&n);
	fo(i,1,n)
	{
		scanf("%d",&d);
		d&1?one++:two++;
	}
	if(one&1) {puts("0"); return 0;}
	f[0][0]=1;
	fo(i,1,two)
		fo(j,0,i)
		{
			if(j>=3) f[i][j]=f[i-3][j-3]*((i-1)*(i-2)>>1)%mo;
			if(j>=1) P(f[i][j],f[i-1][j-1]*(j-1));
			P(f[i][j],f[i-1][j]*(one/2+i-j-1));
		}
	fo(i,0,two) P(ans,f[two][i]);
	fo(i,3,one) if(i&1) (ans*=i)%=mo;
	printf("%lld",ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值