CF140E New Year Garland 题目分析
挺不错的动态规划题目。
思路
一看到题目便可以知道每一层和层与层之间是要分开来算的(这种类似的动态规划还有很多)。
我们先看看层与层之间的。
层与层
题目要求:相邻的两层的小球颜色集合不相同。
那么区分相邻两层小球颜色集合不同可以通过数量不同来区分,然后再进行讨论即可。
根据上述,显然地,设 \(f_{i,j}\) 表示已经完成前 \(i\) 层,到了第 \(i\) 层小球颜色集合的数量为 \(j\) 的总方案。
我们先抛开重不重复不谈,那么它的总方案肯定是从前面的那一层转移过来,即 \(f_{i-1,k}\),其中 \(k\in [1,l_{i-1}]\)。
那么是不是就是
呢?显然不是,这里的 \(j\) 是数量,并不是选了什么,因此还要有 \(C_m^j\) 来确定选 \(j\) 种颜色球的方案(这是对于当前 \(i\) 的)。
每一层
我们还需要当前这一层的贡献,而每一层的贡献求法类似,根据我们的总状态可以设 \(g_{i,j}\) 表示长度为 \(i\) 的位置给你放 \(j\) 种颜色(此处颜色确定,并且按从大到小的顺序排列)的球的放的方案是多少。
转移也是显然的:
后面之所以要乘上 \((j-1)\) 是因为有 \(j\) 种颜色,并且不能与上一个相等,故为 \((j-1)\) 个。
合起来!
我们可以预先求出 \(g_{i,j}\),因此这个处理的复杂度为 \(\mathcal{O}((\max l_i)^2)\)。
根据上述,我们不难把 \(f_{i,j}\)(不考虑层与层之间的要求)的求法合并为:
实际上我们的每一层方案不一定是要从小到大排列的,因此我们乘上 \(j!\) 来保证每一种可能(这个可以理解为将这 \(j\) 个不同颜色原本按照一个一个编号排着,想通颜色的是同一个标号,然后你决定要将每个编号的每种可能都取到,这样就要乘上 \(j!\)),还不理解可以参考下述:
按照原本的转移我得到的所有可能情况是 \(\{a_i\}\)。
比如说,我有 \(j\) 种颜色,将它们依次标号为:\(\{1,2,3,\dots,j\}\)。
实际上我可以是:\(\{2,1,j,\dots,4\}\),或者是 \(\{4,1,j-1,\dots,j\}\) 等等都有可能。
但是这所有的情况总和便是 \(j!\)。触类旁通,枚举所有可能全排列(长度为 \(n\))的时间复杂度就是 \(\mathcal{O}(n!)\)。
如果你还是不能理解就可以用数学的思想考虑:
总共有 \(j\) 个位置,我填在第 \(1\) 位的方案为 \(j\),第 \(2\) 位的为 \(j-1\)。
易有:第 \(i\) 位的方案为 \(j-i\)。
最后填完 \(j\) 个位置的方案为 \(j!\)。
由于模数 \(p\) 不一定是质数,这就导致我们的 \(C_{m}^j\) 不能用逆元求解,但是我们发现:
于是合并一下:
真是棒极了!现在我们考虑与上一层不同的情况,我们发现两层颜色集合相等的两种情况的充分条件是 \(k=j\)。
然后我们又发现,两者颜色集合完全相等在 \(C_{m}^j\) 种只有一种情况。
故我们可以得到:
这个乘上 \(j!\) 与上述同理。
一些提示(思考后再看)
我给出两个提示:一是怎么求 \(P_{m}^j\),二是怎么优化。
有人说:
这不还是要逆元吗?
但我说:
而显然:
然后从 \(i-1\) 做到 \(i\) 不就行了,每次只多乘上一个数。
如何优化?
其实很简单。
我们发现:\(\sum_{k=1}^{l_{i-1}}f_{i-1,k}\) 其实跟 \(j\) 没有任何关系,直接在枚举 \(i\) 的时候加一下不就完了。
代码
令 \(L=\max l_i\),总时间复杂度为 \(\mathcal{O}(L^2+\sum l_i)\)。
代码如下:
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <algorithm>
#include <cstring>
#include <vector>
#define int long long
#define N 1000006
#define M 5005
using namespace std;
int n,m,mod,l[N],g[M][M],mxm,p[N],f[2][M],jc[M];
signed main(){cin >> n >> m >> mod;for (int i = 1;i <= n;i ++) cin >> l[i],mxm = max(mxm,l[i]);jc[0] = 1;for (int i = 1;i <= mxm;i ++) jc[i] = jc[i - 1] * i % mod;g[1][1] = 1;for (int i = 2;i <= mxm;i ++)for (int j = 1;j <= min(i,m);j ++)g[i][j] = (g[i - 1][j - 1] + g[i - 1][j] * (j - 1) % mod) % mod;
// cout << g[2][2] << ' ' << p[2] << ' ' << p[1] << endl;p[0] = 1;for (int i = 1;i <= mxm;i ++)p[i] = p[i - 1] * (m - i + 1) % mod;for (int j = 1;j <= min(l[1],m);j ++)f[1][j] = g[l[1]][j] * p[j] % mod;for (int i = 2;i <= n;i ++) {int sum = 0;for (int k = 1;k <= min(m,l[i - 1]);k ++)sum = (sum + f[i - 1 & 1][k]) % mod;
// cout << sum << endl;for (int j = 1;j <= min(m,l[i]);j ++) {f[i & 1][j] = sum * g[l[i]][j] % mod * p[j] % mod;if (j <= l[i - 1]) f[i & 1][j] = (f[i & 1][j] - f[i - 1 & 1][j] * g[l[i]][j] % mod * jc[j] % mod + 5 * mod) % mod;
// cout << f[i & 1][j] << endl;}}int ans = 0;for (int j = 1;j <= l[n];j ++)ans = (ans + f[n & 1][j]) % mod;cout << ans;return 0;
}
提示一个很容易错的点:直接算 \(f_1\) 的时候记得取模,不然大数据过不了。