重返现世(Min-max容斥+dp)
题目:洛谷P4707
题目描述:
有\(n\)中物品,每个时刻会随机生成一种物品,其中第\(i\)种物品被生成的概率为\(\frac{p_i}{m}\),求一共生成了任意\(k\)种不同物品(一种物品允许生成多次)的期望时间
\(k \leq n \leq 1000\),\(|n - k| \leq 10\),\(0 \leq p_i \leq m\),\(\sum_{i=1}^n p_i = m\),\(m \leq 10000\)
蒟蒻题解:
假设最终所有物品都出现了,记录每个物品第一次出现的时间,那么问题就是要求第一次出现的时间第\(n-k+1\)大的期望值为多少,我们可以先让\(k = n-k+1 \leq 11\)
考虑\(kth\ Min-max\)容斥:
问题转换为计算\(\sum_{T \subseteq S} (-1)^{|T|-k} \binom{|T|-1}{k-1} E(min(T))\)
对于\(E(min(T))\),为\(T\)中元素第一次出现时间的期望值,所以\(E(min(T)) = \frac{1}{\sum_{i \in T} p_i}\)
\(n\)的范围可以达到\(1000\),暴力枚举\(T\),显然是不行的,任何与\(2^n\)有关的方法也都不通,考虑用\(dp\)去做
观察式子,只要枚举所有的\(|T|\),\(\sum_{i \in T} p_i\)就能求出答案
所以可以得到一个比较暴力的\(dp\):
其中\(f_{i,j,k}\)表示对于前\(i\)个数,\(|T|\)为\(j\),\(\sum_{i \in T} p_i\)为\(k\)的方案数,时间复杂度为\(\mathcal O(n^2m)\),只能做\(70\)分
发现这个\(dp\)的转移不太好优化,尝试在状态上稍作修改,观察哪些状态是不必要的
对于\(|T|\),添加进一个值后,\(T\)变成了\(|T|+1\),\((-1)^{|T|-k}\)变成\((-1)^{|T|+1-k}\),相当于乘上\(-1\),而对于\(\binom{|T|-1}{k-1}\)变成\(\binom{|T|}{k-1} = \binom{|T|-1}{k-1} + \binom{|T|-1}{k - 2}\),又可以化成添加之前的\(|T|-1\),中间改变的是\(k\)的值,我们可以尝试把\(|T|\)替换成和\(k\)有关的东西(这里的\(k\)是指读入后的\(n-k+1\),范围最大到\(11\))
设\(dp\)方程\(f_{i,j,k}\)表示对于前\(i\)个数,\(\sum_{i \in T} p_i\)为\(j\),\(k\)为\(\binom{|T|-1}{k-1}\)中的\(k\),此时的\(\sum_{T} (-1)^{|T|-k} \binom{|T|-1}{k-1}\)之和
考虑状态转移,如果不选第\(i\)位,那么\(f_{i,j,k} = f_{i-1,j,k}\)
如果选了第\(i\)位,那么会使\(|T|\)增加\(1\)
可以推出:
考虑边界问题,对于选了多个数的可以从前面推出,考虑选了恰好一个数,即\(|T|=1\),\(f_{i,p_i,k}\)的值就为\((-1)^{1-k} \binom{0}{k-1}\),同时也为\(f_{i-1,0,k-1} - f_{i-1,0,k}\),即\(f_{i-1,0,k-1} - f_{i-1,0,k} = (-1)^{1-k} \binom{0}{k-1}\)
当\(k=1\)时,\(f_{i-1,0,0} - f_{i-1,0,1} = 1\)
当\(k > 1\)时,\(f_{i-1,0,k-1} - f_{i-1,0,k} = 0\)
我们可以令\(f_{i,0,0}=1\),其余初始值均为\(0\),这是一种合法的方法,但不是唯一的方法,P4707洛谷题解里似乎有好几种方法,或者也可以网上去查找
这题就这么结束了?不是这样的!
如果直接设\(f_{i,j,k}\),这样会超内存,观察\(dp\)转移
右边式子第一维是小\(1\)的,第二维、第三维也都小于等于左边式子,可以从大到小枚举二三位,从而不需要开第一维的空间
时间复杂度为\(\mathcal O(nm(n-k))\),空间复杂度为\(\mathcal O(m(n-k))\)
参考程序:
#include<bits/stdc++.h>
using namespace std;
#define Re register int
typedef long long ll;
const int N = 10005, p = 998244353;
int n, k, m, sum, ans, f[N][12];
inline int read()
{
char c = getchar();
int ans = 0;
while (c < 48 || c > 57) c = getchar();
while (c >= 48 && c <= 57) ans = (ans << 3) + (ans << 1) + (c ^ 48), c = getchar();
return ans;
}
inline int fpow(ll x, int y)
{
ll z = 1;
while (y)
{
if (y & 1) z = z * x % p;
x = x * x % p, y >>= 1;
}
return z;
}
inline void inc(int &x, int y)
{
x += y;
if (x >= p) x -= p;
}
inline void sub(int &x, int y)
{
x -= y;
if (x < 0) x += p;
}
int main()
{
n = read(), k = n - read() + 1, m = read(), f[0][0] = 1;
for (Re i = 1; i <= n; ++i)
{
int u = read();
sum += u;
for (Re j = sum; j >= u; --j)
for (Re t = k; t; --t) sub(f[j][t], f[j - u][t]), inc(f[j][t], f[j - u][t - 1]);
}
for (Re i = 1; i <= m; ++i) ans = (ans + 1ll * f[i][k] * fpow(i, p - 2)) % p;
ans = 1ll * ans * m % p;
printf("%d", ans);
return 0;
}