LGP4590 [TJTS 2018] 游园会 学习笔记
LGP4590 [TJTS 2018] 游园会 学习笔记
前言
本文“LCS”均指最长公共子序列。
题意简述
给定一个长为 \(m\) 的字符串 \(S\)。对于从 \(0\) 到 \(m\) 的每一个整数 \(i\),求有多少长为 \(n\) 的字符串 \(T\),满足 \(|\text{lcs}(S,T)|=i\),且 \(T\) 中不存在子串 \(\texttt{NOI}\)。
\(m\le 15,n\le 10^3\)。本题中出现的所有字符串的字符集均为 \(\{\texttt{N},\texttt{O},\texttt{I}\}\)。
时间限制 \(\text{6.00s}\)。
做法解析
这是个对满足某限制的字符串进行计数的东西,我们往DP和自动机之类的方面想。
设 \(f_{i,\dots}\) 为当前考虑到长度为 \(i\) 的串,满足某些限制下的串数,我们每次转移枚举当前字符串结尾填哪个字符,这是DP的框架。
我们肯定还要记一维 \(j\) 处理最长公共子序列的限制,至于不含子串 \(\texttt{NOI}\) 则很容易处理,我们记一维 \(k\in \{0,1,2\}\) 表示这个字符串末尾匹配 \(\texttt{NOI}\) 的进度就行。
问题来了,\(j\) 这一维具体怎么搞?令 \(j\) 表示 \(|\text{lcs}(S,T)|\)?不好意思,当新加进来一个字符时你无法判断这一位能否进一步匹配,因为你不知道上一次匹配到哪了。
然而 \(m\le 15\)。这明示我们状压。状压了就好办了。怎么个状压法?
设我们状压状态的第 \(j\) 位对应 \(S\) 的第 \(j+1\) 个字符。一种最直接的想法是令第 \(j\) 位为 \(1\) 表示 \(S_j\) 处在当前的LCS中,反之亦然。这么做的问题是有时LCS并不唯一。举个例子:
\(S[1,3]=\texttt{ONI}\),\(T[1,3]=\texttt{OIN}\)
显然此时其LCS并不唯一,我们显然不能把三位都涂上 \(1\),怎么办?一个想法是我们钦定当有多个LCS时涂“最靠前”(优先考虑第一位,在此基础上考虑第二位,依此类推)的那个,另一个想法是我们存差分数组(对于同一个 \(S\),随着匹配长度增加,其LCS长度是单增的,而且每次新增进一个字符参与匹配,LCS长度最多增加 \(1\))。实际上手玩一下可以发现这两个想法实际上是一个东西。
可以证明这么做满足无后效性。粗略地理解,我们往 \(T\) 后面加字符可能可以发掘出 \(S\) 有可以更好匹配的地方,但是我们无论何时都考虑的是整个串 \(S\),所以说一个状态就意味着满足这个状态的 \(T\) 已有的部分不可能再有任何用处了(即使这些部分可以凑成别的长度相同的LCS,也没有必要考虑,因为等长的LCS在后续只加 \(T\) 的转移中的表现是相同的),相当于空气,满足这个状态的 \(T\) 就可以视作当前的LCS去考虑后续转移。
啊,满足无后效性我们就放心了!
那我们最终的DP状物就出来了:设 \(f_{i,j,k}\) 为考虑到 \(T\) 的第 \(i\) 位,当前和 \(S\) 的LCS被状压的结果为 \(j\),\(\texttt{NOI}\) 的组合进度为 \(k\) 的方案数。我们枚举下一位填哪个字母即可转移。时间复杂度 \(O(n2^m)\)。
我们设所有可达状态构成的集合为 \(\Sigma\)(这是大写希腊字母西格玛,不是求和符号)。实际上,有用状态数量 \(|\Sigma|\) 大概率是不能跑满 \(2^m\) 的。如果我们提前设计一个记忆化搜索把 \(j\) 这一维的转移关系都搜出来,我们会发现其只有 \(2^m\) 的 \(0<x<1\) 倍。在这题里面,搜出来的可达状态经粗略测试不会超过 \(6\times 10^3\) 种。
这下我们主循环里面 \(j\) 这一维的枚举就从 \(2^m\) 优化到 \(|\Sigma|\) 了,可喜可贺!总时间复杂度 \(O(n|\Sigma|)\),下文中的代码总用时 \(\text{157ms}\),完全用不着六秒的时限呐!
代码实现
主DP采用刷表法转移,那个循环里面八个转移从上到下依次对应:
- \(\texttt{X+N}\),匹配进度从 \(0\) 来到 \(1\)(其中 \(\texttt{X}\) 不为 \(\texttt{N}\) 或 \(\texttt{NO}\))。
- \(\texttt{X+O}\),仍然没有匹配进度。
- \(\texttt{X+I}\),仍然没有匹配进度。
- \(\texttt{N+N}\),匹配进度仍为 \(1\)。
- \(\texttt{N+O}\),匹配进度从 \(1\) 来到 \(2\)。
- \(\texttt{N+I}\),匹配进度归零。
- \(\texttt{NO+N}\),匹配进度回到 \(1\)。
- \(\texttt{NO+O}\),匹配进度归零。
- 为什么没有 \(\texttt{NO+I}\) 啊,好难猜啊。
可以看到 nxt 数组中的第二维正对应我们新加的字符。
#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
using namespace omodint;
using mint=m107;
const int MaxM=20,BipM=(1<<15),MaxN=1e3+5,MaxS=6e3+5;
int N,M;char S[MaxM];
int tot,mp[BipM],nxt[MaxS][3],len[MaxS],g[2][MaxM];
mint dp[MaxN][MaxS][3],ans[MaxM];
int dfs(int u){
if(mp[u])return mp[u];mp[u]=++tot;
auto work=[&](int c,char ch)->void {
for(int i=1;i<=M;i++)g[0][i]=g[0][i-1]+((u>>(i-1))&1);
len[mp[u]]=g[0][M];int v=0;
for(int i=1;i<=M;i++){
g[1][i]=max(g[0][i],g[1][i-1]);
if(S[i]==ch)maxxer(g[1][i],g[0][i-1]+1);
v|=((g[1][i]-g[1][i-1])<<(i-1));
}
nxt[mp[u]][c]=dfs(v);
};
work(0,'N'),work(1,'O'),work(2,'I');
return mp[u];
}
int main(){
readis(N,M),scanf("%s",S+1);
dfs(0);dp[0][mp[0]][0]=1;
for(int i=0,ci=0;i<N;i++,ci=i&1){
for(int j=1;j<=tot;j++){
if(dp[ci][j][0]!=0){
dp[ci^1][nxt[j][0]][1]+=dp[ci][j][0];
dp[ci^1][nxt[j][1]][0]+=dp[ci][j][0];
dp[ci^1][nxt[j][2]][0]+=dp[ci][j][0];
}
if(dp[ci][j][1]!=0){
dp[ci^1][nxt[j][0]][1]+=dp[ci][j][1];
dp[ci^1][nxt[j][1]][2]+=dp[ci][j][1];
dp[ci^1][nxt[j][2]][0]+=dp[ci][j][1];
}
if(dp[ci][j][2]!=0){
dp[ci^1][nxt[j][0]][1]+=dp[ci][j][2];
dp[ci^1][nxt[j][1]][0]+=dp[ci][j][2];
}
for(int k=0;k<=2;k++)dp[ci][j][k]=0;
}
}
for(int j=1;j<=tot;j++)for(int k=0;k<=2;k++)ans[len[j]]+=dp[N&1][j][k];
for(int i=0;i<=M;i++)writil(miti(ans[i]));
return 0;
}
后记
写题解的好处在于它逼着你把题目讲清楚,要讲清楚就要你自己理解透彻。
你看别人的题解里面有些地方其实没证明,别人可能觉得显然,但是你自己真的搞懂了吗?还是说蒙混过去了?
动态规划和贪心的正确性不想明白,自己心里面真的能安心么?
望诸君共勉。
浙公网安备 33010602011771号