loj3276 「JOISC 2020 Day2」遗迹 (dp)

题意

你有一个长度为2n的数组,并且对于i=1~n都恰好有2个位置=i。现在进行如下操作n次:
对于所有位置 i i i满足 a [ i ] ≠ 0 a[i]\not=0 a[i]=0,若在此次操作前,i的右侧存在与a[i]相同的值,则 a [ i ] − − a[i]-- a[i].

最后会剩下n个不为0的位置。现在告诉你这些位置,请求出一开始分布的方案数。

分析

有点东西的

  • 先考察一下这个操作在干什么。操作结束之后,可以保证1~n都只有一个。
  • 考虑位置2n,他的值始终不会变化。
  • 那么其他位置最终不会为a[2n]。
  • 考虑位置2n-1,若a[2n-1]=a[2n],则a[2n-1]最终为a[2n]-1;否则,其左边的位置就不能为a[2n-1]与a[2n].
  • 以此类推,我们得到一个已知初始状态,求最终是什么的方法:从右往左做,若数x在右侧出现过,则x-=1,直到x=0,当前位置不会被留下来。

接下来对这个过程dp:

  • f [ i ] [ j ] f[i][j] f[i][j]表示,已经处理了 [ i , 2 n ] [i,2n] [i,2n],与1相连的覆盖块是 [ 1 , j ] [1,j] [1,j]
  • 对于一个被留下来的位置:
    假如他当前不能扩展第二维,那先暂时不填。
    假如他能扩展第二维,那我们枚举其扩展了到j+k,那么还需要填上k-1个被留下来但是还没有填的位置,恰好将 [ j + 2 , j + k ] [j+2,j+k] [j+2,j+k]覆盖。这里的方案数需要预处理一下。
  • 对于一个没有被留下来的位置,他可以填[1,j]里面任意一个数。由于不好分辨具体有多少种,不妨将两个相同的数标号,最后将答案除去2^n。这里方案数为 2 j − ( j + c n t [ 0 ] ) 2j-(j+cnt[0]) 2j(j+cnt[0]),cnt[0]为i+1~2n中没有留下来的总数。

我们还要预处理 h [ i ] h[i] h[i]表示i个位置恰好放满1~i的方案数。设 g [ i ] [ j ] g[i][j] g[i][j]表示当前放了值 [ 1 , i ] [1,i] [1,i]的,每一种最多两个,共放了j个。只要保证值 [ 1 , i ] [1,i] [1,i]中的个数不超过i个,即能保证不会有多余的。最后使个数恰好为i个,即恰好完全覆盖,也就是 h [ i ] = g [ i ] [ i ] h[i]=g[i][i] h[i]=g[i][i]。 转移时要注意两个相同的数也要区分标号。

#include <bits/stdc++.h>
using namespace std;
const int N = 2 * 610, mo = 1e9 + 7;
typedef long long ll;
int n, a[N], key[N];
ll g[N][N], f[N][N];
ll jc[N], njc[N];

ll ksm(ll x, ll y) {
    ll ret = 1;
    for (; y; y >>= 1) {
        if (y & 1)
            ret = ret * x % mo;
        x = x * x % mo;
    }
    return ret;
}

void getg() {
    for (int i = 1; i <= n; i++) {
        g[i - 1][0] = 1;
        for (int j = 1; j <= i; j++) {
            g[i][j] = (g[i - 1][j] + g[i - 1][j - 1] * j * 2) % mo;
            if (j >= 2) {
                g[i][j] = (g[i][j] + g[i - 1][j - 2] * j * (j - 1)) % mo;
            }
        }
    }
}

ll C(ll n, ll m) {
    if (n < m)
        return 0;
    return jc[n] * njc[n - m] % mo * njc[m] % mo;
}
int main() {
    cin >> n;
    jc[0] = 1;
    for (int i = 1; i <= n; i++) jc[i] = jc[i - 1] * i % mo;
    njc[n] = ksm(jc[n], mo - 2);
    for (int i = n - 1; ~i; i--) njc[i] = njc[i + 1] * (i + 1) % mo;

    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        key[a[i]] = 1;
    }
    getg();
    f[2 * n + 1][0] = 1;
    int cnt[2];
    cnt[0] = cnt[1] = 0;
    for (int i = 2 * n; i; i--) {
        //不中的cnt[0]个点要放在1~j之间,因此j>=cnt[0]
        for (int j = cnt[0]; j <= cnt[1]; j++)
            if (f[i + 1][j]) {
                if (!key[i]) {
                    f[i][j] = (f[i][j] + f[i + 1][j] * (2 * j - (j + cnt[0]))) % mo;
                } else {
                    f[i][j] = (f[i][j] + f[i + 1][j]) % mo;  //不放颜色的情况
                    for (int k = 1; j + k <= cnt[1] + key[i]; k++) {
                        f[i][j + k] = (f[i][j + k] + f[i + 1][j] * C(cnt[1] - j, k - 1) % mo *
                                                         g[k - 1][k - 1] % mo * (k + 1)) %
                                      mo;
                    }
                }
            }
        cnt[key[i]]++;
    }
    cout << f[1][n] * ksm((mo + 1) / 2, n) % mo << endl;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值