CSP-S 模拟 企鹅棋 (组合数学)(DP)

40 篇文章 0 订阅
12 篇文章 0 订阅

企鹅豆渣在玩企鹅棋,企鹅棋的规则如下:
企鹅棋一共有 n n n 个格子,从左到右以 1 − n 1−n 1n 编号。每一个格子会有一种属性 S i S_i Si
• B 属性格子,从这个格子出发,可以到达其他任意一个格子。
• L 属性格子,从这个格子出发,只能跳到左边的格子。
• R 属性格子,从这个格子出发,只能跳到右边的格子。
棋子从第 s s s 号格子出发,最终跳到第 e e e 号格子,并且经过每一个格子恰好一次。
也就是说,行程是一个大小为 n n n 的排列, 满足 a 1 = s , a n = e a_1 = s, a_n = e a1=s,an=e 并且
对于任意 i , 1 ≤ i ≤ n − 1 i,1\le i\le n-1 i,1in1,棋子会从 a i a_i ai 跳到 a i + 1 a_i+1 ai+1
你需要求出有多少种不同的跳格子行程安排
n ≤ 2 e 3 n\le 2e3 n2e3


考虑按顺序把一个数字加到排列里面
手玩样例:
{ 2 , 5 , 1 , 3 , 4 } \{2,5,1,3,4\} {2,5,1,3,4}
{ ? , ? , 1 , ? , ? } \{?,?,1,?,?\} {?,?,1,?,?}
{ 2 , ? , 1 , ? , ? } \{2,?,1,?,?\} {2,?,1,?,?}
{ 2 , ? , 1 , 3 , ? } \{2,?,1,3,?\} {2,?,1,3,?}
{ 2 , ? , 1 , 3 , 4 } \{2,?,1,3,4\} {2,?,1,3,4}
{ 2 , 5 , 1 , 3 , 4 } \{2,5,1,3,4\} {2,5,1,3,4}
发现原序列在每一个时间都是一坨一坨的连通块


经过我们严谨的讨论,发现有三种连通块:

  1. 连着开头 s t a r t start start
  2. 连着结尾 e n d end end
  3. 在中间 m i d mid mid

发现在中间的怎么排列都是没有关系的,也就是说 m i d mid mid 每个都是等价的,只和个数有关

由于我们从小到大加入 i i i,如果 i = S i=S i=S 那么强制放在开头, i = E i=E i=E 那么强制放在结尾
也就是说我们只需要通过 i i i 的大小就可以知道有没有 s t a r t , e n d start,end start,end

考虑 d p dp dp d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从小到大填到 i i i,连通块个数有 j j j 个的方案数


暴力分类讨论:

i + 1 = S i+1=S i+1=S
强制填在开头
接后面: n o w + [ m i d ] now+[mid] now+[mid] n o w now now 要往后跳的一个更小的地方,需要满足 S n o w = L , B S_{now}=L,B Snow=L,B
新开:发现新开的要向后面连边,所以必须满足 S n o w = R , B S_{now}=R,B Snow=R,B

i + 1 = E i+1=E i+1=E
考虑新开和 [ m i d ] + n o w [mid]+now [mid]+now,转移没有限制因为不用向后接

o t h e r w i s e otherwise otherwise
考虑不加最后一个点,就可以少掉 [ s t a r t ] + n o w + [ e n d ] [start]+now+[end] [start]+now+[end] 的头疼讨论

  1. 新开
  2. [ s t a r t ] + n o w [start]+now [start]+now
  3. [ s t a r t ] + n o w + [ m i d ] [start]+now+[mid] [start]+now+[mid]
  4. n o w + [ e n d ] now+[end] now+[end]
  5. [ m i d ] + n o w + [ e n d ] [mid]+now+[end] [mid]+now+[end]
  6. n o w + [ m i d ] now+[mid] now+[mid]
  7. [ m i d ] + n o w [mid]+now [mid]+now
  8. [ m i d ] + n o w + [ m i d ] [mid]+now+[mid] [mid]+now+[mid]

限制条件利用填的位置判断即可
然后 1 会多一个,3,5,8 会少一个,其它情况不变
另外,由于 [ m i d ] [mid] [mid] 并没有考虑顺序,接 m i d mid mid 的需要考虑接哪一个,接 [ m i d ] [mid] [mid] 中间的要考虑接哪两个中间


**总结:**一道不错的计数题
通过发现性质及抽象为连通块用 d p dp dp 解决
将等价的联通块算到个数里面是非常巧妙的思想


#include<bits/stdc++.h>
#define cs const
using namespace std;
typedef long long ll;
int read(){
	int cnt = 0, f = 1; char ch = 0;
	while(!isdigit(ch)){ ch = getchar(); if(ch == '-') f = -1; }
	while(isdigit(ch)) cnt = cnt*10 + (ch-'0'), ch = getchar();
	return cnt * f;
}
cs int N = 2e3 + 5;
cs int Mod = (int)1e9 + 7;
int add(int a, int b){ return a + b >= Mod ? a + b - Mod : a + b; }
int mul(int a, int b){ return 1ll * a * b % Mod; }
void Add(int &a, int b){ a = add(a, b); }
char s[N];
int n, S, E, f[N][N];
int main(){
	scanf("%s", s + 1); n = strlen(s + 1);
	S = read(), E = read();
	f[0][0] = 1;
	// free 间不考虑顺序 , 全部等价  
	for(int i = 0; i < n - 1; i++){
		for(int j = i == 0 ? 0 : 1; j <= i; j++){
			if(!f[i][j]) continue;
			// f[i][j] -> f[i + 1][?]
			int ct = j; 
			if(i >= S) -- ct;
			if(i >= E) -- ct;
			if(ct < 0) continue;
			char x = s[i + 1];
			// i+1 == S, i+1 is the first
			// i+1 + <free> & i+1 is new
			if(i + 1 == S){
				if(x == 'R' || x == 'B') Add(f[i + 1][j + 1], f[i][j]);
				if(x == 'L' || x == 'B') Add(f[i + 1][j], mul(f[i][j], ct));
			}
			else if(i + 1 == E){
				Add(f[i + 1][j + 1], f[i][j]);
				Add(f[i + 1][j], mul(f[i][j], ct));
			}
			else{
				// new
				if(x == 'R' || x == 'B') Add(f[i + 1][j + 1], f[i][j]);
				// <start> + (i+1) 
				if(i + 1 > S && (x == 'R' || x == 'B')) Add(f[i + 1][j], f[i][j]);
				// <start> + (i+1) + <free>
				if(i + 1 > S && (x == 'L' || x == 'B') && j) Add(f[i + 1][j - 1], mul(ct, f[i][j]));
				// (i+1) + <end>
				if(i + 1 > E && (x == 'L' || x == 'B')) Add(f[i + 1][j], f[i][j]);
				// <free> + (i+1) + <end> 
				if(i + 1 > E && (x == 'L' || x == 'B') && j) Add(f[i + 1][j - 1], mul(ct, f[i][j]));
				// (i+1) + <free>
				if(ct >= 1 && (x == 'L' || x == 'B')) Add(f[i + 1][j], mul(ct, f[i][j]));
				// <free> + (i+1)
				if(ct >= 1 && (x == 'R' || x == 'B')) Add(f[i + 1][j], mul(ct, f[i][j]));
				// <free> + (i+1) + <free> 
				if(ct >= 2 && (x == 'L' || x == 'B')) Add(f[i + 1][j - 1], mul(mul(ct, ct - 1), f[i][j]));
				// <free> has no order, A + i + B is different from B + i + A
			}
		}
	}
	int ans = 0;
	// <end> 
	if(n == S){ if(s[n] == 'L' || s[n] == 'B') ans = f[n - 1][1]; }
	// <start> 
	else if(n == E){ ans = f[n - 1][1]; }
	else {
		// <start> + <end>  
		if(s[n] == 'L' || s[n] == 'B') ans = f[n - 1][2];
	} cout << ans; return 0;
}

解法 2:
并不容易发现上述解法的性质
发现 S = 1 , E = n S=1,E=n S=1,E=n 80 80 80
考虑到每个点会向后或向前接一个点
同样 d p dp dp
d p i , j , k dp_{i,j,k} dpi,j,k 表示到了 i i i,前 i i i 个会向后面伸 j j j 个插头,后面的会向 i i i 前面伸 k k k 插头的方案数
如果当前为 R R R,那么会向后面伸插头,讨论从前面还是后面要插头转移
如果当前为 L L L,那么会向前面伸插头,同样讨论从前面还是后面要插头
如果当前位 B B B,那么可以向前或向后伸插头
然后发现无论哪一种转移,都有 j , k j,k j,k 同时不变或加减 1
而初始状态是 f 1 , 1 , 0 = 1 f_{1,1,0} = 1 f1,1,0=1,所以恒有 j − k = 1 j-k=1 jk=1
一个点向后伸插头的方案是不确定的,我们统计方案数在向前接插头的时候统计就可以做到不重不漏

这也是一个巧妙的方法,把最后的形成的序列在原序列上用插头来抽象
接哪一个插头无所谓,只需要知道个数,然后通过等价条件减小状态

#include<bits/stdc++.h>
#define cs const
using namespace std;
typedef long long ll;
int read(){
	int cnt = 0, f = 1; char ch = 0;
	while(!isdigit(ch)){ ch = getchar(); if(ch == '-') f = -1; }
	while(isdigit(ch)) cnt = cnt*10 + (ch-'0'), ch = getchar();
	return cnt * f;
}
cs int N = 2e3 + 5;
cs int Mod = (int)1e9 + 7;
int add(int a, int b){ return a + b >= Mod ? a + b - Mod : a + b; }
int mul(int a, int b){ return 1ll * a * b % Mod; }
void Add(int &a, int b){ a = add(a, b); }
char s[N];
int n, S, E, f[N][N];
int main(){
	scanf("%s", s + 1); n = strlen(s + 1);
	S = read(), E = read();
	if(S == 1 && E == n){
		f[1][1] = 1;
		for(int i = 2; i < n; i++){
			char x = s[i];
			for(int j = 0; j <= i; j++){
				int k = j - 1;
				if(x != 'L'){ // can go right 
					Add(f[i][j + 1], f[i - 1][j]);
					if(j) Add(f[i][j], mul(j, f[i - 1][j]));
				}
				if(x != 'R'){
					if(k) Add(f[i][j - 1], mul(f[i - 1][j], mul(k, k))); // can not go to S 
					if(j) Add(f[i][j], mul(f[i - 1][j], k));
				} 
			}
		} cout << f[n - 1][1]; return 0;
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FSYo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值