【JZOJ5870】【NOIP2018模擬9.15】地圖 (遞推+DP+組合數學)

Problem

這裏寫圖片描述

Hint

這裏寫圖片描述

Solution

  • 首先,圖中只會存在鏈和環。
  • 記圖中有one個度數爲1的點,two個度數爲2的點。囿於每條鏈有兩個度數爲1的點(鏈的兩端),鏈的數量是確定的:one2\frac{one}2
  • 這時,我靈(nao)光(zi)一(wa)閃(te),想到了一個優(sha)美(bi)的方法。

我的SB方法:遞推+組合數學+容斥

  • 觀察到鏈和環的方案相對獨立。那麼根據乘法原理,我們可以求出鏈的方案tot1,環的方案tot2,總方案即爲tot1*tot2。
  • 那麼分類討論一下。
鏈的情況
  • 我們先令one個點兩兩匹配,構成one2\frac{one}2條只含兩個點的鏈。
  • 不妨枚舉當前有n條鏈。假設增加一條,則點數增加爲2*n+2。
  • 枚舉點1連接的是哪個點,這有2n+1種可能;而剩下的2n個點,兩兩匹配成i條鏈。
  • 因此,遞推式爲hn+1=(2n+1)hnh_{n+1}=(2*n+1)*h_n,其中n爲鏈數。

  • 現在,我們再將度數爲2的點插入到這些鏈中。
  • gig_i表示有i個度數爲2的點在鏈中。新插一個點,我們可以插在所有鏈的非鏈首節點的左邊。譬如下圖:
    在這裏插入圖片描述
  • 我們可以插在任意一個藍點左邊,因此有3+2=5種方案。實際上,記有k條鏈,第i條鏈的鏈長爲lenilen_i,插入新點的方案即爲i=1kleni1\sum_{i=1}^k len_i-1。 不難發現,這其實等於已插入的點數+鏈數
  • 因此,gi=gi1(i1+one2)g_i=g_{i-1}*(i-1+\frac{one}2),其中one2\frac{one}2爲鏈數。

環的情況

  • 環的情況就有些複雜了。
  • 考慮DP。設fi,jf_{i,j}表示i個度數爲2的點在環上,其中有j個一元環的方案數。
  • 囿於原圖不存在自環,我們最終得到的應是fi,0f_{i,0}
  • 然後可以得到三種轉移:1.新建一個一元環;2.令當前點加入到一個一元環中;3.令當前點加入到一個多元環中。

  • 然而還有一個坑點——那就是環翻轉一下,和原來全等,但是我們會算重。
  • 不妨在新建環的時候,就將其貢獻記爲12\frac 12,這樣最終算出的結果便是去了重的。
  • 然而,我們這樣只能算出無自環的方案數,不能算出無二元環(重邊)的方案數。

  • 考慮容斥。
  • 枚舉有i個度數爲2的點在環上,其中有j個二元環。那麼正負性爲(1)j(-1)^j,係數爲Ci2jhj2jC_i^{2j}*h_j*2^{-j},其中hjh_j爲上述提到的,點兩兩匹配的方案數。
  • 係數中有個2j2^{-j}的原因是我們把二元環的貢獻都算成了12\frac 12(建環時是12\frac 12,再插一個點是1,121=12\frac 12*1=\frac 12),然而二元環的貢獻應是1;於是在去掉二元環的方案中,我們應該也乘回這些12\frac 12以彌補二元環的缺失。
  • 然後對於每個i,都求一波j=0i2(1)jCi2jhj2jfi2j,0\sum_{j=0}^{\lfloor \frac i2 \rfloor} (-1)^j*C_i^{2j}*h_j*2^{-j}*f_{i-2j,0},而這便是真正的fif_i(無自環、無二元環的方案數)。

  • 時間複雜度:O(n2)O(n^2)

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}表示i個度數爲2的點,其中j個點在環上(即剩下的i-j個點在鏈上)的方案數。
  • 有以下三種轉移:
    fi,j={fi3,j3Ci12fi1,j1(j1)ifi1,j   (ij1+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.
  • 新建環的貢獻爲何是Ci12C_{i-1}^2呢?我們假定點i就在新環內,然後從剩下的i-1個點中選2個出來陪它。如若不然,則有可能算重。然後不必再因會翻轉除以2,因爲三元環定是唯一的。

  • 這樣的話,ans=i=0twoftwo,ians=\sum_{i=0}^{two} f_{two,i},其中two爲度數爲2的節點的個數。
  • 但還沒把鏈首、鏈尾兩兩匹配的方案數算上,所以最後要再乘上。
  • 時間複雜度:O(n2)O(n^2)

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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章