UOJ #390 【UNR #3】百鸽笼 容斥+DP

题目分析

算法0

每个管理员选哪一列,将构成一个长度为 N − 1 N-1 N1的序列,序列的种数可以通过经典的将 a a a个相同元素插入到一个没有该元素的长度为 b b b的序列里问题,轻松求出。

若一列 i i i要求有空笼,则标号 i i i只出现 a i − 1 a_i-1 ai1次,每种序列出现的概率都是一样的,一下子就算出来啦!
...
期望得分:0

算法1

分析一下算法0错在哪——每种序列的出现概率并不是相等的,因为每一个管理员选择列的时候,由于剩余有空笼的列数量不同,所以做每一个决策的概率不同。

那么有没有办法使概率相同呢?

如果一直没有一列的鸽笼被用完,每一次决策的概率就一直是相同的,每一种选择序列出现的概率也都是相同的。

于是我们需要将“这一列最后被用完”转化为“这一列最先被用完,其他的列都没用完”,显然是容斥。

一通乱容斥后得到: a n s ( i ) = 1 − ∑ S ( − 1 ) ∣ S ∣ + 1 g ( i , S ) ans(i)=1-\sum_{S} (-1)^{|S|+1} g(i,S) ans(i)=1S(1)S+1g(i,S),其中 g ( i , S ) g(i,S) g(i,S)表示, S S S集合里的列都晚于 i i i被用完的概率,显然 S S S集合外的列都不用考虑了。

生成一个管理员的选择序列, i i i出现 a i a_i ai次,其他的 j j j都不能出现大于等于 a j a_j aj次,且最后一个元素是 i i i,长度为 L L L(不一定是 N − 1 N-1 N1,因为当满足“ S S S集合里的列都晚于 i i i被用完”条件后,之后管理员的选择无关紧要了)。这样的序列个数是可以算出来的,并且每个长度为 L L L的序列出现的概率都是 1 ∣ S ∣ + 1 \frac{1}{|S|+1} S+11

序列只有长度是有用信息,用DP,设 f ( i ) f(i) f(i)表示长度为 i i i的以上要求的序列的个数,枚举集合 S S S中每一个元素,在序列中出现了多少次,即可 O ( n 4 ) O(n^4) O(n4)地DP。

总复杂度 O ( 2 n n 4 ) O(2^n n^4) O(2nn4)跑不满,期望得分30分。

算法2

复杂度瓶颈在于要枚举集合,但实际上一个集合所带来的有用的信息只有集合大小和管理员选择序列长度 L L L。所以干脆每算一个答案 a n s ( i ) ans(i) ans(i),做一遍DP,设 f ( i , j ) f(i,j) f(i,j)表示当前集合大小为 i i i,序列长度为 j j j的方案数,枚举除了 i i i外的每一列加不加入集合,加入多少个进序列即可。

复杂度 O ( n 6 ) O(n^6) O(n6)跑不满,可能能拿100分。

算法3

算法2的复杂度瓶颈在于,做 i i i列为有空笼列的答案时,DP不能把 i i i给考虑进去,可考虑其他列的步骤是重复的。所以先做一次所有列都考虑的DP,然后对于每个 i i i逆向DP除去第 i i i列的贡献即可。

复杂度 O ( n 5 ) O(n^5) O(n5)跑不满,期望得分100分。

代码

#include<bits/stdc++.h>
using namespace std;
#define RI register int
const int mod=998244353;
int n,all;
int a[32],C[905][905],inv[32],bin[32][905],g[32][905],tmp[32][905];

int qm(int x) {return x>=mod?x-mod:x;}
void prework() {
	for(RI i=0;i<=all;++i) {
		C[i][0]=1;
		for(RI j=1;j<=i;++j) C[i][j]=qm(C[i-1][j-1]+C[i-1][j]);
	}
	inv[1]=1;for(RI i=2;i<=n;++i) inv[i]=1LL*(mod-mod/i)*inv[mod%i]%mod;
	for(RI i=1;i<=n;++i) {
		bin[i][0]=1;
		for(RI j=1;j<=all;++j) bin[i][j]=1LL*bin[i][j-1]*inv[i]%mod;
	}
}
void work() {
	int lim=0;g[0][0]=1;
	for(RI i=1;i<=n;++i) {
		lim+=a[i]-1;
		for(RI j=i;j>=1;--j)
			for(RI k=lim;k>=0;--k)
				for(RI t=0;t<a[i]&&t<=k;++t)
					g[j][k]=qm(g[j][k]-1LL*g[j-1][k-t]*C[k][t]%mod+mod);
	}
	for(RI i=1;i<=n;++i) {
		int ans=0;
		for(RI j=0;j<n;++j)
			for(RI k=0;k<=lim-a[i]+1;++k) tmp[j][k]=g[j][k];
		for(RI j=1;j<n;++j)
			for(RI k=0;k<=lim-a[i]+1;++k)
				for(RI t=0;t<a[i]&&t<=k;++t)
					tmp[j][k]=qm(tmp[j][k]+1LL*tmp[j-1][k-t]*C[k][t]%mod);
		for(RI j=0;j<n;++j)
			for(RI k=0;k<=lim-a[i]+1;++k)
				ans=qm(ans+1LL*tmp[j][k]*C[k+a[i]-1][k]%mod*bin[j+1][k+a[i]]%mod);
		printf("%d ",ans);
	}
}

int main()
{
	scanf("%d",&n);
	for(RI i=1;i<=n;++i) scanf("%d",&a[i]),all+=a[i];
	prework(),work(),puts("");
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值