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

本文探讨了一款名为企鹅棋的游戏,其规则涉及不同属性的格子,玩家需从特定起点跳跃至终点,每格仅访问一次。文章提出了两种解法,一是通过观察连通块的性质,运用动态规划解决;二是采用插头模型,跟踪前后连接数量,简化状态空间。

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


考虑按顺序把一个数字加到排列里面
手玩样例:
{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. 连着开头 startstartstart
  2. 连着结尾 endendend
  3. 在中间 midmidmid

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

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

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


暴力分类讨论:

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

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

otherwiseotherwiseotherwise
考虑不加最后一个点,就可以少掉 [start]+now+[end][start]+now+[end][start]+now+[end] 的头疼讨论

  1. 新开
  2. [start]+now[start]+now[start]+now
  3. [start]+now+[mid][start]+now+[mid][start]+now+[mid]
  4. now+[end]now+[end]now+[end]
  5. [mid]+now+[end][mid]+now+[end][mid]+now+[end]
  6. now+[mid]now+[mid]now+[mid]
  7. [mid]+now[mid]+now[mid]+now
  8. [mid]+now+[mid][mid]+now+[mid][mid]+now+[mid]

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


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


#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=nS=1,E=nS=1,E=n808080
考虑到每个点会向后或向前接一个点
同样 dpdpdp
dpi,j,kdp_{i,j,k}dpi,j,k 表示到了 iii,前 iii 个会向后面伸 jjj 个插头,后面的会向 iii 前面伸 kkk 插头的方案数
如果当前为 RRR,那么会向后面伸插头,讨论从前面还是后面要插头转移
如果当前为 LLL,那么会向前面伸插头,同样讨论从前面还是后面要插头
如果当前位 BBB,那么可以向前或向后伸插头
然后发现无论哪一种转移,都有 j,kj,kj,k 同时不变或加减 1
而初始状态是 f1,1,0=1f_{1,1,0} = 1f1,1,0=1,所以恒有 j−k=1j-k=1jk=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;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

FSYo

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

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

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

打赏作者

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

抵扣说明:

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

余额充值