[题解]P2516 [HAOI2010] 最长公共子序列——求LCS个数
总的来说,这道题确实很精妙,难度也不小,耗费了不少时间去调。本来想过用容斥的思想,却因为当时理解不深没有继续思考就放弃了。学会了思路后对\(LCS\)有了更深层次的理解。
题意简述
给定\(A,B\)两个字符串(以.
结尾),求它们的最长公共子序列的长度和个数(取模\(10^8\))。
注意:两个字符串可能有多个最长公共子序列,同一个最长公共子序列,哪怕放的位置不同,也算作两个。
样例
输入:
ABCBDAB. BACBBD.
输出:
4 7
样例解释
长度为\(4\):ABBD
\(*1\),ACBB
\(*1\),ACBD
\(*2\),BCBB
\(*1\),BCBD
\(*2\)。
思路简述
根据\(LCS\)算法的思想,我们画出以下表格(注意路径不唯一,所以每个格子不止一个箭头)。右下角的暗红色数字是\(f\)数组,记录长度。
第一问就是\(f[n][m]\)的值(\(n,m\)表示\(A,B\)的长度)。
第二问怎么解决呢?结果显然就是从\((n,m)\)开始,根据格子上的指示回溯的路径条数。当时以为这里用记忆化搜索直接计数就能得到正确答案,但其实这样做可能会有重复。如下图:
从右下开始走,左-上-左上
和上-左-左上
的效果是一样的,都是选择了C
加入子序列。而左上-左上
的效果不同,选择了D
和C
加入子序列。由此可以看出并不是路径不同最终子序列就不同。应该是路径中经过↖
的位置不同,最终子序列就不同(注意不同的子序列可能字符串表示相同,前面题意简述说明了)。
所以搜索的方法会有重复计算的错误,考虑和\(f\)一样,定义一个\(g\)数组,\(g[i][j]\)表示\(A,B\)长度分别为\(i,j\)的前缀\(LCS\)的个数。
-
如果\(a[i]=b[j]\),说明此处有一个
↖
,那么应该有这样子\(4\)种情况:
也就是说,如果\(a[i]=b[j]\),首先\(g[i][j]+=g[i-1][j-1]\),表示从这个格子往↖
走的个数(长度为\(3+1=4\)),如果\(f[i-1][j]=f[i][j]\)或者\(f[i][j-1]=f[i][j]\),说明我们也可以不选↖
,而是选择←
或↑
,仍然能选\(4\)个。 -
如果\(a[i]\neq b[j]\),那么此处没有
↖
。判断方法相同:- 如果\(f[i-1][j]=f[i][j]\)说明有
↑
,\(g[i][j]+=g[i-1][j]\); - 如果\(f[i][j-1]=f[i][j]\)说明有
←
,\(g[i][j]+=g[i][j-1]\)。
但是如果\(f[i-1][j-1]=f[i][j]\)(\(a[i]\neq b[j]\)不代表\(f[i-1][j-1]\neq f[i][j]\)),说明
左-
和上-
的操作可能有重复计算(就是第二张图那种,左-上
和上-左
都能到达\((i-1,j-1)\),又因为\(f[i-1][j-1]=f[i][j]\),所以都是最长。这样就重复计算了),需要\(g[i][j]-=g[i-1][j-1]\)。 - 如果\(f[i-1][j]=f[i][j]\)说明有
综上,我们得到状态转移方程:
\(f[i][j]= \begin{cases} f[i-1][j-1]+1 & if\ a[i]=b[j]\\ max(f[i-1][j],f[i][j-1]) & otherwise \end{cases}\)
\(g[i][j] \begin{cases} =1 & if\ i=0\ or\ j=0\\ otherwise:\\ +=g[i-1][j-1] & if\ a[i]=b[j]\\ +=g[i-1][j] & if\ f[i-1][j]=f[i][j]\\ +=g[i][j-1] & if\ f[i][j-1]=f[i][j]\\ -=g[i-1][j-1] & if\ f[i-1][j-1]=f[i][j]\\ \end{cases}\)
注意事项
- 别忘取模\(10^8\)。
- 开\(5000*5000\)会炸空间,所以需要滚动数组优化空间。
- 虽然递推有减法,但我们不需要担心出现负数影响取模。因为我们一定会作加法,而且加的数所在位置一定不在减的数所在位置左或上(还是比较容易理解的)。
具体见代码~
Code
点击查看代码
#include<bits/stdc++.h> #define int long long #define mod 100000000ll using namespace std; string a,b; int n,m,f[5010][5010]; int g[5010][5010]; signed main(){ cin>>a>>b; n=a.size()-1,m=b.size()-1; a=' '+a,b=' '+b; bool cur=1,pre=0; for(int i=0;i<=m;i++) g[0][i]=1; g[1][0]=1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ g[cur][j]=0; if(a[i]==b[j]) f[cur][j]=f[pre][j-1]+1; else f[cur][j]=max(f[pre][j],f[cur][j-1]); if(a[i]==b[j]) g[cur][j]+=g[pre][j-1]; if(f[cur][j]==f[cur][j-1]) g[cur][j]+=g[cur][j-1]; if(f[cur][j]==f[pre][j]) g[cur][j]+=g[pre][j]; if(f[cur][j]==f[pre][j-1]) g[cur][j]-=g[pre][j-1]; g[cur][j]%=mod; } cur=!cur,pre=!pre; } cout<<f[pre][m]<<endl<<g[pre][m]; return 0; }
欢迎在评论区留下你的问题或建议。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效