CF 140E. New Year Garland


New Year Garland

题意

\(m\) 种颜色去装点 \(n\) 层的圣诞树。圣诞树的第 \(i\) 层恰好由 \(li_i\) 个彩灯串成一行,且同一层内的相邻彩灯颜色不同,同时相邻两层所使用的彩灯的颜色集合不同。问有多少种装点方案,答案对 \(p\) 取模。

分析

计数DP,用 \(f(i, j)\) 表示用 \(j\) 种颜色装点 \(i\) 个彩灯的合法方案数量,对第 \(i\) 个彩灯,可以用新的颜色涂,或者用旧的颜色,显然有:

\[f(i, j) = f(i-1, j-1) + f(i-1, j) \times (j-1) \]

\(dp(i, j)\) 表示把前面 \(i\) 层涂好,且第 \(i\) 行用了 \(j\) 种颜色的合法方案数量。

\(sum(i)\) 表示把前面 \(i\) 行涂好的方案数量。

如果不考虑相邻行颜色集重复问题,那么有:

\[dp(i, j) = sum(i-1) * f(li(i), j) * A(m, j) \]

A(m, j) 表示 \(m\)\(j\) 子集的排列。

考虑相邻行颜色集重复问题,那么有:

\[dp(i, j) = dp(i, j) - dp(i-1, j) * f(li(i), j) * j! \]

考虑占用空间比较大,采用滚动数组。

Code

#include 
#include 
using namespace std;

const int N = 5050;

typedef long long ll;

int li[1000010]; // 第i行的彩球数量
int f[N][N]; // f(i, j) 表示用j种颜色涂i个彩球的合法方案数量
int dp[2][N]; // dp(i, j) 表示将前i行涂好,且第i行使用j种颜色涂的方案数量
int sum[2]; // sum(i) = \sum dp[i][j] , 1 <= j <= li 表示所有把前面i行涂好的方案数
int A[N], fac[N];

int main ()
{
    int n, m, p; cin >> n >> m >> p;
    for (int i = 1; i <= n; i ++ ) cin >> li[i];
    
    f[0][0] = 1; // 临界状态
    for (int i = 1; i <= 5000; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = (f[i-1][j-1] + (ll)f[i-1][j] * (j-1) % p) % p;
    
    fac[0] = 1; for (int i = 1; i <= 5000; i ++ ) fac[i] = (ll)i * fac[i-1] % p;
    
    A[0] = 1; // 预处理A(m, i)
    for (int i = 1; i <= 5000; i ++ ) A[i] = (ll)A[i-1] * (m - i + 1) % p;
    
    sum[0] = 1; // 给空的彩灯涂,1种方案
    for (int i = 1; i <= n; i ++ ) // 枚举每一行
    {
        sum[i%2] = 0;
        for (int j = 1; j <= li[i]; j ++ ) // 枚举使用的颜色数量
        {
            dp[i%2][j] = (ll)A[j] * sum[1-i%2] % p * f[li[i]][j] % p;
            if (j <= li[i-1])
                dp[i%2][j] = (dp[i%2][j] - (ll)fac[j] * dp[1-i%2][j] % p * f[li[i]][j] % p + p) % p;
            sum[i%2] = (sum[i%2] + dp[i%2][j]) % p;
        }
    }
    cout << sum[n%2] << '\n';
    return 0;
}