1. 程式人生 > 其它 >Atcoder Regular Contest 089 D - ColoringBalls(DP)

Atcoder Regular Contest 089 D - ColoringBalls(DP)

神仙 DP 題 %%%

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
*/