全网第一![FJOI2017] 回文子序列问题 题解报告
题目历史地位
本题是
本篇题解应该是全网目前能搜到的第一篇满分正解。
附赠
- [FJOI2015] 带子串包含约束LCS问题
- [FJOI2016] 所有公共子序列问题
- [FJOI2017] 回文子序列问题
关于子序列自动机
看到又是
考虑到本题的特殊性,这里重新介绍一下子序列自动机的构建与使用,它应该是最简单的自动机。
构建
子序列自动机(Subsequence Automaton,为防止和后缀自动机 SAM 名称冲突,简称 SSAM)实际上是一张子序列的 DAG,上面任意连续路径都是原串的一段子序列。
以下是一个例子,以FJOIAKIOI构建的子序列自动机:
如图每条边指向的是自己之后第一个出现这个字符的位置,据此可以想到一种构建方法:构建时我们可以从尾部开始反向构建,每次复制上一次的所有边,修改上个字符的出现位置。
乍一看好像很乱?没事,接下来一步步梳理。
以下是构建过程(蓝边表示继承的边,红边表示修改的边):
- Code
int Sub[N][M],tmp[M];
void build(int a[],int n,bool rev){
memset(tmp,0,sizeof(tmp));
for(int i=n;i>=1;i--){
tmp[a[i]]=i;
memcpy(Sub[i-1],tmp,sizeof(tmp));
}
}
设有一串长 n,字符集为
朴素构建
对于大型字符集,由于构建过程每个结点的连边最多只修改上一个结点连边的一条,实际上就是一个可持久化数组,主席树解决。
构建
这个对解决本题帮助不大,不列代码了。
最长公共子序列
设有两串
这个问题现在就转化为了
答案就是
int dfs(int x,int y){
if(f[x][y])return f[x][y];
f[x][y]=1;
int u,v;
for(int i=0;i<m;i++){
u=SubA[x][i],v=SubB[y][i];
if(!u or !v)continue;
f[x][y]=max(f[x][y],dfs(u,v)+1);
}
return f[x][y];
}
ans=dfs(0,0)-1;
时间复杂度
对于更广义的最长公共子序列问题:
例题:[TJOI2008] 公共子串 (三串公共子序列问题)
最长回文子序列
设有一串长
对原串的正反串分别建子序列自动机,跑最长公共子序列。
时间复杂度
解题过程
那么本题非常明显了,对A、B串的正反串分别建子序列自动机,然后跑最长公共子序列。
同时发现字符集很大,但字符最多出现种数很少(最多只有
时间复杂度
然而笔者因为实现方式,被卡成 50PTS。
(接下来是笔者的漫长卡常之路)
终于卡到了 90PTS,此时笔者已经尝试无数种卡常方法。
实在没办法了,笔者开始考虑是不是算法本身有能优化的地方?
确实有,实际上笔者忽略了一个很关键的要点:「回文子序列」
回忆 Manacher ,猛然想到:实际上一个串自身的最长回文子序列只要比对一半就好了。这样最长回文子序列
这里是最长回文子序列的改进示例,注意要特判奇回文:
- Code
int dfs(int x,int y){
if(x>n-y)return x==n-y+1;
if(f[x][y])return f[x][y];
f[x][y]=2;
int u,v;
for(int i=0;i<m;i++){
u=SubPre[x][i],v=SubSuf[y][i];
if(!u or !v)continue;
f[x][y]=max(f[x][y],dfs(u,v)+2);
}
return f[x][y];
}
ans=dfs(0,0)-2;
那么
如果到这了,恭喜,
代码
为防止贺题er,代码放 Record 里。
极小概率(纯整活,仅供娱乐):
后记
实际上笔者绕的弯路比此处展示的多得多,例如尝试卡光速读、手写 hashmap、玄学优化等等。感兴趣的可以看笔者的几页测评记录。
目前本题终于有满分题解了,可喜可贺。
如果本篇题解对您有帮助,请点个赞再走吧!如果没有帮助,也可以看在笔者幸苦写题解的份赏个赞吧! (也可以关注笔者嘤QWQ)