企鹅豆渣在玩企鹅棋,企鹅棋的规则如下:
企鹅棋一共有
n
n
n 个格子,从左到右以
1
−
n
1−n
1−n 编号。每一个格子会有一种属性
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,1≤i≤n−1,棋子会从
a
i
a_i
ai 跳到
a
i
+
1
a_i+1
ai+1。
你需要求出有多少种不同的跳格子行程安排
n
≤
2
e
3
n\le 2e3
n≤2e3
考虑按顺序把一个数字加到排列里面
手玩样例:
{
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}
发现原序列在每一个时间都是一坨一坨的连通块
经过我们严谨的讨论,发现有三种连通块:
- 连着开头 s t a r t start start
- 连着结尾 e n d end end
- 在中间 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] 的头疼讨论
- 新开
- [ s t a r t ] + n o w [start]+now [start]+now
- [ s t a r t ] + n o w + [ m i d ] [start]+now+[mid] [start]+now+[mid]
- n o w + [ e n d ] now+[end] now+[end]
- [ m i d ] + n o w + [ e n d ] [mid]+now+[end] [mid]+now+[end]
- n o w + [ m i d ] now+[mid] now+[mid]
- [ m i d ] + n o w [mid]+now [mid]+now
- [ 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
j−k=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;
}
}