思路
手搓样例,发现用分裂根本不符合人类认知规律,遂使用合并。一秒想到区间 dp,用
d
p
i
,
j
dp_{i,j}
dpi,j 表示
i
,
j
i,j
i,j 之间的字母合并后可以得到多长的只包含“S”的字符串(记录最短长度)。可是要是先把
i
,
k
i,k
i,k 之间的合并得到“A”,再把
k
+
1
,
j
k+1,j
k+1,j 之间的合并得到“B”,最后把“A”和“B”合并得到“S”,该怎么表示?
首先,当一段字母合并后得到了一个不是“S”的字母时,就必须要再和左右两边的字母合并,最终得到“S”。其次,如果得到了“S”,就可以选择继续合并或者把几个合并得到的“S”拼起来(我们默认初始的每个字母是由这个字母本身合并得到的)。所以可以先用
m
g
i
,
j
mg_{i,j}
mgi,j 进行
⌈
\lceil
⌈ 合并成一个字母
⌋
\rfloor
⌋ 的操作,然后用
d
p
i
,
j
dp_{i,j}
dpi,j 进一步实现
⌈
\lceil
⌈ 把几个“S”拼起来
⌋
\rfloor
⌋ 的操作。
由于合并得到的是单个字母,可以用状态压缩进行表示,
m
g
i
,
j
mg_{i,j}
mgi,j 在二进制下从低到高第
k
k
k 位如果是
1
1
1,表示可以用
i
,
j
i,j
i,j 之间的字母合并得到char('A'+k)
。
d
p
dp
dp 和
m
g
mg
mg 都采取枚举断点
k
k
k 的方法计算。
d
p
i
,
j
dp_{i,j}
dpi,j 的边界情况是:
- i = j i=j i=j 时,若 s i s_i si 是“S”,那么 d p i , j = 1 dp_{i,j}=1 dpi,j=1,否则是 INF。
-
i
<
j
i<j
i<j 时,若
i
,
j
i,j
i,j 之间的字母可以合并得到一个“S”,即
mg[i][j]&(1<<'S'-'A')
,那么 d p i , j = 1 dp_{i,j}=1 dpi,j=1。
代码
#include<bits/stdc++.h>
using namespace std;
int can[26][26],n,kk;
int dp[105][105],mg[105][105];
string s;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>s;
can[s[1]-'A'][s[2]-'A']|=1<<s[0]-'A';
}
cin>>kk;
for(int i=1;i<=kk;i++){
cin>>s;
int si=s.size();
s=' '+s;
memset(mg,0,sizeof(mg));
for(int l=1;l<=si;l++) mg[l][l]=1<<s[l]-'A';
for(int j=2;j<=si;j++)
for(int l=1;l+j-1<=si;l++){
int k=l+j-1;
for(int mid=l;mid<k;mid++)
for(int a=0;a<26;a++)
if(mg[l][mid]&(1<<a))
for(int b=0;b<26;b++)
if(mg[mid+1][k]&(1<<b))
mg[l][k]|=can[a][b];
}
memset(dp,0x3f,sizeof(dp));
for(int j=1;j<=si;j++)
for(int l=1;l+j-1<=si;l++){
int k=l+j-1;
if(mg[l][k]&(1<<'S'-'A')){
dp[l][k]=1;
continue;
}
for(int mid=l;mid<k;mid++)
dp[l][k]=min(dp[l][k],dp[l][mid]+dp[mid+1][k]);
}
if(dp[1][si]<0x3f3f3f3f) cout<<dp[1][si]<<endl;
else cout<<"NIE\n";
}
return 0;
}