算法报告二 最长公共子序列
16122020 钟顺源
一、题目要求
求两个字符串a,b的最长公共子序列的长度,并打印出所有成立的子序列
二、分析
这是一道典型的动态规划+路径记录的题。
一般解决动态规划的路径打印,都是在动态规划的过程中记录达到这个状态的直接前驱,最后用一个深度搜索从末状态逆推前状态,再此过程中打印出答案。
这道题的难点是要把所有的可能序列全打印出来,这该如何解决呢?
先不急,先把基础的转移方程写出来
两个串分别为a,b。
定义状态
d
p
[
i
]
[
j
]
:
a
串
的
前
i
个
字
符
组
成
的
字
符
串
和
b
串
前
j
个
字
符
组
成
的
字
符
串
的
最
长
公
共
子
序
列
的
长
度
dp[i][j] :a串的前i个字符组成的字符串和b串前j个字符组成的字符串的最长公共子序列的长度
dp[i][j]:a串的前i个字符组成的字符串和b串前j个字符组成的字符串的最长公共子序列的长度
状态转移方程
d
p
[
i
]
[
j
]
{
0
                                                                                                
i
f
  
i
=
0
∣
∣
j
=
0
d
p
[
i
−
1
]
[
j
−
1
]
+
1
                                          
i
f
  
i
,
j
>
0
,
x
i
=
y
i
m
a
x
(
d
p
[
i
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
)
          
i
f
  
i
,
j
>
0
,
x
i
≠
y
i
dp[i][j]\begin{cases} & \text 0 \;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;{}if\; i=0||j=0\\ & \text dp[i-1][j-1]+1\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;if\;i,j>0,x_i=y_i \\ & max(dp[i][j-1],dp[i-1][j]) \;\;\;\;\;if\;i,j>0,x_i\neq y_i \end{cases}
dp[i][j]⎩⎪⎨⎪⎧0ifi=0∣∣j=0dp[i−1][j−1]+1ifi,j>0,xi=yimax(dp[i][j−1],dp[i−1][j])ifi,j>0,xi̸=yi
两个字符串的尾部字符若是相同直接等于去掉尾部字符的情况下的答案加1。
若是不同,取去掉一个末尾后情况最多的那个状态的答案。
初始化状态
d
p
0
−
n
.
0
=
0
dp_{0-n.0}=0
dp0−n.0=0
d
p
0.0
−
m
=
0
dp_{0.0-m}=0
dp0.0−m=0
很容易想嘛,若是一个序列前0个字符肯定不能和任何字符有公共子序列。
那怎么打印所有的可能序列呢?
要输出所有的情况,不能仅仅记录一个前驱,要将所有的可能的状态前驱全部记录下来。
最后在输出答案的时候,dfs每一个前驱,到递归边界的时候记录答案,这里最好用set保存,去重。
三、代码
#include<bits/stdc++.h>
using namespace std;
#define clr(a, x) memset(a, x, sizeof(a))
#define mp(x, y) make_pair(x, y)
#define pb(x) push_back(x)
#define X first
#define Y second
#define fastin \
ios_base::sync_with_stdio(0); \
cin.tie(0);
typedef long long LL;
typedef pair<int,int> PII;
const int INF = 0x3f3f3f3f;
const int N=55;
char a[N],b[N];
int dp[N][N];
map<PII,set<PII> > pre;
set<string> ans;
int len;
void dfs(int n,int m,string tmp){
if(!n||!m) ans.insert(tmp);
for(auto v:pre[PII(n,m)]){
int x=v.X,y=v.Y;
if(a[n]==b[m]) dfs(x,y,a[n]+tmp);
else dfs(x,y,tmp);
}
}
int main()
{
int t;scanf("%d",&t);
int cas=0;
while(t--){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++) cin>>b[i];
clr(dp,0);
pre.clear();
ans.clear();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(a[i]==b[j]) {
dp[i][j] = dp[i-1][j-1] + 1;
pre[PII(i,j)].insert(PII(i-1,j-1));
}
else {
if(dp[i-1][j]>dp[i][j-1]){
dp[i][j] = dp[i-1][j];
pre[PII(i,j)].insert(PII(i-1,j));
}else if(dp[i-1][j]<dp[i][j-1]){
dp[i][j] = dp[i][j-1];
pre[PII(i,j)].insert(PII(i,j-1));
}else{
dp[i][j] = dp[i][j-1];
pre[PII(i,j)].insert(PII(i,j-1));
pre[PII(i,j)].insert(PII(i-1,j));
}
}
}
len=dp[n][m];
dfs(n,m," ");
cout<<ans.size()<<endl;
for(auto v:ans){
cout<<v<<endl;
}
}
return 0;
}
四、体会
这次实验也比较简单,dp方程还是很好推的,而且记录路径也是非常常见的,没有什么恶心的处理。尤其n<=50,即使在记录路径的时候处理的不是很好,dfs写的常数比较大,也不会超时,几乎只要想到了,就能写出来了。