1. 程式人生 > 其它 >[省選集訓2022] 模擬賽18

[省選集訓2022] 模擬賽18

題目描述

給定兩邊各 \(n\) 個點的二分圖,在這張圖中,左邊的 \(i\) 號點向右邊的 \(1,2,3...a_i\) 號點連邊。請你求出圖中的簡單環個數,答案對 \(998244353\) 取模,定義兩個環不同為存在一條邊在一個環出現了但是在另一箇中不出現。

\(1\leq n\leq 5000,1\leq a_i\leq n\)

解法

首先可以把 \(a_i\) 從小到大排序,發現這樣並不會改變答案而且簡化了問題。

沒有什麼其他的性質了,直接考慮 \(dp\) 吧。考慮每次增量一個左部的點,然後考慮它連出去的兩條邊,達到的效果就是合併兩條路徑

所以我們可以記錄路徑個數,為了簡化計算我們定義路徑為有向路徑

。設 \(dp[i][j]\) 表示考慮前 \(i\) 個左部點,形成的有向路徑數量是 \(j\) 個。轉移可以分兩部,第一步是新增 \(a_i-a_{i-1}\) 個右部單點:

\[dp[i][j]\leftarrow \sum_{k=0}^{a_i-a_{i-1}} dp[i-1][j-k]\cdot {a_i-a_{i-1}\choose k} \]

第二步是任取兩條有向路徑合併:

\[dp[i][j]\leftarrow dp[i][j+1]\cdot j\cdot (j+1) \]

時間複雜度 \(O(n^2)\),我們在第一步轉移之後將 \(dp[i][1]\) 計入答案。

總結

不要糾結於神奇性質,有時候可以直接規劃。

#include <cstdio>
#include <algorithm>
using namespace std;
const int M = 5005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,ans,a[M],C[M][M],dp[M][M];
void add(int &x,int y) {x=(x+y)%MOD;}
signed main()
{
	freopen("ring.in","r",stdin);
	freopen("ring.out","w",stdout);
	n=read();
	for(int i=0;i<=n;i++)
	{
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%MOD;
	}
	for(int i=1;i<=n;i++) a[i]=read();
	sort(a+1,a+1+n);dp[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		int d=a[i]-a[i-1];
		for(int j=0;j<=a[i];j++)
			for(int k=max(0ll,j-d);k<=j;k++)
				add(dp[i][j],dp[i-1][k]*C[d][j-k]);
		add(ans,dp[i][1]-a[i]+MOD);
		for(int j=0;j<=a[i];j++)
			add(dp[i][j],dp[i][j+1]*j%MOD*(j+1));
	}
	printf("%lld\n",ans*((MOD+1)/2)%MOD);
}