Atcoder Regular Contest 089 D - ColoringBalls(DP)
阅读原文时间:2023年07月09日阅读:1

Atcoder 题面传送门 & 洛谷题面传送门

神仙题。

在下文中,方便起见,用 R/B 表示颜色序列中球的颜色,用 r/b 表示染色序列中将连续的区间染成的颜色。

首先碰到这一类计算有多少个可以被得到的序列的问题,我们为了避免计算重复,肯定会先考虑什么样的序列能够被得到,从而设计一个 DP 状态。此题也不例外。考虑对于一个颜色序列它能否能够被得到,显然对于这个颜色序列中白色格子,它们肯定从来没有被覆盖,因此我们考虑这些白色格子被分割而成的连续段。我们将这些连续段分为两类:不含蓝色格子和含蓝色格子。对于前者,显然一个 r 即可搞定。后者稍微有点复杂,我们来分析下什么样的序列能够染出它,显然同色连续段可以缩在一起,而如果开头是蓝色格子,那我们肯定需要先在它上面染一遍 r,因此我们不妨在它前面补一个红色格子,对于结尾的格子也同理,这样我们只用考虑 RBRBRB…BR 能够被什么样的染色序列得到即可。手玩一下 RBRBRBR 的染色方案可以发现,RBRBRBR 能够被以下四个长度为 \(4\) 的序列得到:

  • rbrr
  • rbrb
  • rbbr
  • rbbb

我们可以很明显地发现,能够得到 RBRBRBR 的染色序列的头两个元素都是 rb,而后两个元素是什么都无所谓,并且显然没有比这个长度更短的序列了,而对于更长的序列,它们肯定包含子序列 rb,并且 rb 以后都至少有 \(2\) 个元素,因此它们肯定是没有这四个长度为 \(4\) 的序列来得更优的,因此我们不妨归纳一下:对于有 \(B\) 个蓝色连续段的杂色序列,有用的序列长度都是 \(B+1\),并且头两个元素都是 rb,至于后面的元素,则无所谓。

因此我们假设分出来的杂色连续段有 \(x\)​ 个,它们当中蓝色格子分别有 \(c_1,c_2,\cdots,c_x\) 个,而只含红色的连续段有 \(y\) 个,那么这样的染色序列能够被染出来当且仅当 \(S\) 能够被拆分成 \(x+y\) 个子序列(不必全部字符都属于某个子序列,但每个字符最多属于一个子序列),其中 \(y\) 个恰好是 r,而对于另外 \(x\) 个,都以 rb 开头,并且第 \(i\) 个长度恰为 \(c_i+1\)。

这样我们可以贪心地判定一个序列是否可以被染出:我们先贪心地选出最靠前的 \(x\) 个 rb,再从剩下的子序列中贪心地选出 \(y\) 个 r,然后我们对于此时的局面,对每个 \(i\) 求出 \([i,n]\) 中有多少个格子目前没有被染色,设为 \(sum_i\),然后我们将选出的 \(x\) 个 rbb 的位置从小到大排序,设为 \(ed_1,ed_2,\cdots,ed_x\),再将 \(c\) 数组从大到小排序,即不妨假设 \(c_i\ge c_{i+1}\),这样我们就贪心地将第一个 rb 分给 \(c\) 最大的连续段,第二个 rb 分给 \(c\) 第二大的,以此类推,这样我们可知一段序列符合要求,当且仅当 \(\forall i,sum_{ed_i}\ge\sum\limits_{j=i}^x(c_j-1)\)。

我们考虑枚举 \(x,y\)​,然后按照上面的方式先贪心地选出 \(x\)​ 个 rb 和 \(y\)​ 个 r,这样我们可以知道 \(sum_i\)​ 及 \(ed_i\)​。然后我们考虑倒着 DP,设 \(dp_{i,j,k}\)​ 表示目前钦定了 \(i\sim x\)​ 的段中蓝色点的个数,目前 \(\sum\limits_{j=i}^xc_j-1=j,c_i=k\)​,有多少种杂色段之间两两之间位置关系的可能,转移就枚举 \(c_j=k\)​ 的 \(j\)​ 有多少个,设为 \(p\)​,那么转移就类似于一个二项加法卷积,即 \(dp_{i,j,k}=\sum\limits_{p}\sum\limits_{l}dp_{i+p,j-(k-1)p,l}·\dbinom{x-i+1}{p}[l<k]\)​,注意到 \(p\)​ 的枚举是调和级数,因此 \(p\)​ 枚举的总复杂度是 \(\ln n\)​ 级别的,而 \(l\)​ 的枚举可以前缀和优化掉,这样单次 DP 就达到了 \(n^3\ln n\)​​。

接下来考虑怎样计算一次 DP 对答案的贡献,即如何用 \(dp_{1,j,k}\)​ 即 \(x,y\)​ 的值计算答案,首先段之间的位置关系,\(x\)​ 个杂色段之间的位置关系已经在 DP 过程中钦定了,而 \(y\)​ 个杂色段与 \(x\)​ 个杂色段之间的位置关系就做个组合数,即 \(\dbinom{x+y}{x}\)​,接下来考虑怎样计算每个连续段中球的数量有多少种可能,我们先假设每个连续段为一个“盒子”,每个盒子中必须装同一颜色的球,那么先不妨假设对于一个由 \(c\)​ 个 B 构成的杂色连续段,它只有 BRBRBRBR…B,那么这样有 \(2c-1\)​ 个盒子,每个盒子中都必须至少有一个球,而两边还可以放一些 R,因此还会产生两个可空的盒子,对于每个纯色连续段,它肯定恰好会产生一个必须放球的盒子,而连续段与连续段之间的 \(x+y-1\)​ 个空隙,它们必须放黑球,有 \(x+y-1\)​ 个必须放球的盒子,而两边的黑球盒子可以空,因此还有两个可以为空的盒子,这样必须放球的盒子数 \(A=2j+2x+2y-1\)​,可以为空的盒子数 \(B=2x+2\)​,组合数算贡献即可。

时间复杂度 \(n^5\ln n\),由于常数很小,可以通过。

启示:

  • 对于求解有多少个序列能够被得到的这一类问题,不妨先考虑一下什么样的序列能够被得到,而如果没有推出足够的性质(比如说如果每次操作的啥啥啥不同,最终得到的序列一定不同)之前,最好不要直接从操作序列的时间轴入手 DP。

  • 如果一些信息在 DP 的过程中不能直接求得/计算 DP 的系数时需要用到这些信息,那不妨先枚举它们,这样 DP 的时候系数就比较好求了。

P.S.:如果你下面程序的最后一组数据输出 2088,看看是不是多算了 BRB黑BRB 这一不合法的序列。

#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fill0(a) memset(a, 0, sizeof(a))
#define fill1(a) memset(a, -1, sizeof(a))
#define fillbig(a) memset(a, 63, sizeof(a))
#define pb push_back
#define ppb pop_back
#define mp make_pair
#define mt make_tuple
#define eprintf(...) fprintf(stderr, __VA_ARGS__)
template <typename T1, typename T2> void chkmin(T1 &x, T2 y){
    if (x > y) x = y;
}
template <typename T1, typename T2> void chkmax(T1 &x, T2 y){
    if (x < y) x = y;
}
typedef pair<int, int> pii;
typedef long long ll;
typedef unsigned int u32;
typedef unsigned long long u64;
typedef long double ld;
namespace fastio {
    #define FILE_SIZE 1 << 23
    char rbuf[FILE_SIZE], *p1 = rbuf, *p2 = rbuf, wbuf[FILE_SIZE], *p3 = wbuf;
    inline char getc() {
        return p1 == p2 && (p2 = (p1 = rbuf) + fread(rbuf, 1, FILE_SIZE, stdin), p1 == p2) ? -1: *p1++;
    }
    inline void putc(char x) {(*p3++ = x);}
    template <typename T> void read(T &x) {
        x = 0; char c = getchar(); T neg = 0;
        while (!isdigit(c)) neg |= !(c ^ '-'), c = getchar();
        while (isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
        if (neg) x = (~x) + 1;
    }
    template <typename T> void recursive_print(T x) {
        if (!x) return;
        recursive_print (x / 10);
        putc (x % 10 ^ 48);
    }
    template <typename T> void print(T x) {
        if (!x) putc('0');
        if (x<0) putc('-'), x = -x;
        recursive_print(x);
    }
    template <typename T> void print(T x,char c) {print(x); putc(c);}
    void print_final() {fwrite(wbuf, 1, p3-wbuf, stdout);}
}
const int MAXN = 70;
const int MAXC = 500;
const int MOD = 1e9 + 7;
int n, k, c[MAXC + 5][MAXC + 5]; char s[MAXN + 5];
bool vis[MAXN + 5]; int ed[MAXN + 5], suf[MAXN + 5], sum[MAXN + 5];
bool contribute(int x, int y) {
    memset(vis, 0, sizeof(vis));
    int c1 = 0, c2 = 0;
    for (int i = 1; i <= k; i++) {
        if (s[i] == 'r') {
            if (c1 < x + y) c1++, vis[i] = 1;
        } else {
            if (c2 < x && c1 > c2) c2++, vis[i] = 1, ed[c2] = i;
        }
    }
    if (c1 != x + y || c2 != x) return 0;
    suf[k + 1] = 0;
    for (int i = k; i; i--) suf[i] = suf[i + 1] + (!vis[i]);
    memset(sum, 0, sizeof(sum));
    for (int i = 1; i <= x; i++) sum[i] = suf[ed[i]];
    return 1;
}
int dp[MAXN + 5][MAXN + 5][MAXN + 5], sdp[MAXN + 5][MAXN + 5][MAXN + 5];
void calc_dp(int x, int y) {
    memset(sdp, 0, sizeof(sdp)); memset(dp, 0, sizeof(dp));
    dp[x + 1][0][0] = 1;
    for (int i = 0; i <= n; i++) sdp[x + 1][0][i] = 1;
    for (int i = x; i; i--) {
        for (int j = 0; j <= sum[i]; j++) {
            for (int l = 1; l <= n; l++) {
                for (int p = 1; p * (l - 1) <= j && p <= x - i + 1 && j - (l - 1) * p <= sum[i + p]; p++) {
                    dp[i][j][l] = (dp[i][j][l] + 1ll * c[x - i + 1][p] * sdp[i + p][j - (l - 1) * p][l - 1]) % MOD;
                }
                sdp[i][j][l] = (sdp[i][j][l - 1] + dp[i][j][l]) % MOD;
            }
        }
    }
}
int main() {
//    freopen("color.in", "r", stdin);
//    freopen("color.out", "w", stdout);
    scanf("%d%d%s", &n, &k, s + 1);
    for (int i = 0; i <= MAXC; i++) {
        c[i][0] = 1;
        for (int j = 1; j <= i; j++) {
            c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % MOD;
        }
    }
    int res = 0;
    for (int x = 0; x <= n; x++) {// x sequences with at least 1 blue ball
        for (int y = 0; x + y <= n; y++) {// y sequences with only red balls
            if (!x && !y) continue;
            if (!contribute(x, y)) continue;
//            printf("x = %d; y = %d:\n", x, y);
//            for (int i = 1; i <= k; i++) printf("%d ", vis[i]);
//            printf("\n");
//            for (int i = 1; i <= x; i++) printf("%d ", sum[i]);
//            printf("\n");
            calc_dp(x, y);
            for (int s = 0; s <= sum[1]; s++) {
                for (int lst = 0; lst <= n; lst++) if (dp[1][s][lst]) {
                    int A = (2 * s + x) + (x + y - 1) + y, B = 2 * x + 2;
//                    printf("!! %d %d %d %d %d %d\n", s, lst, dp[1][s][lst], A, B,
//                    1ll * dp[1][s][lst] * c[x + y][y] % MOD * c[n + B - 1][A + B - 1] % MOD);
                    res = (res + 1ll * dp[1][s][lst] * c[x + y][y] % MOD * c[n + B - 1][A + B - 1]) % MOD;
                }
            }
        }
    }
    printf("%d\n", (res + 1) % MOD);
    return 0;
}
/*
4 3
rrb

7 4
rbrb

7 6
rbrrrb
*/