[ARC128D](计数dp)
非常有思维的一道题
在遇到计数类dp时,我们首先要找到子问题。然后我们要设计dp状态(这个通常就是当前的计数),然后转移方程要做到不重不漏(这我感觉是最难的地方)。
对于这题,我们先设计出dp方程:dp[i]代表1-i都保留时序列的种类。然后我们可以想到如果i-j能消成只有a[i],a[j],那么dp[i]+=dp[j] (先将后面消成a[i],a[j],然后前面再消,能理解吧?) 因为每个dp[i]的第一位和最后一位都不同。(自己看翻译)。然后这个朴素的dp是\(O(n^{2})\)的。考虑优化。首先我们考虑优化判定能不能消的过程。我们发现如果两个相邻的数是相同的,那么这两个数必定不会被消掉。考虑将这这些数隔开。那么讨论就简单许多了。前提是相邻数互不相等:
1.只有一种颜色,不符合前提
2.有两种颜色:那么只可能形如 b|abababab|a,这种情况能够全删只可能是 a|b|a。因为只要中间那段长度大于1后,随便删掉哪个数都会变成第1种情况。
3.有三种及以上颜色:那么最坏情况下也肯定形如 a|babacababababcababab|a,我们单独把 c 拿出来,可以发现,我们只要以 c 为中心,不断地消除左边和右边的数,然后遇到另一个c时,留下中间那个再消即可。
区间数的种类在dp的过程中用双指针非常好维护。
然后我们发现这个转移是一段范围的,可以用前缀和优化,就做完了。代码有点细节:
#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long n,a[200005],b[200005],dp[200005],sum[200005],tot,lst[200005],ans=1;
int main(){
cin>>n;
for(long long i=1;i<=n;i++){
cin>>a[i];
}
long long l=1;
for(long long i=1;i<=n;i++){
if(!b[a[i]]) tot++;
b[a[i]]++;
while(tot>=3&&l<i-2){//第一个限制条件是保证数的种类的,第二个是为了避免和分讨中的第二种情况算重
b[a[l]]--;
if(!b[a[l]]) tot--;
l++;//因为不满足条件后还会++一次,所以下文是sum[lst[i]-1]
}
lst[i]=l;
}
l=1;//当前断开的的段的左端点
dp[0]=1;
for(long long i=1;i<=n;i++){
if(a[i]==a[i-1]) l=i;//断开相邻颜色相同的段
dp[i]=dp[i-1];//如果后面不处理的话就是前面的方案
if(i>=3) if(a[i-1]!=a[i-2]&&a[i-1]!=a[i]) dp[i]=(dp[i]+dp[i-2])%mod;
//如果i-1能消对应分讨中2的a|b|a中的情况,这样也要算上dp[i-2]的贡献,意思是消掉i-1的方案,不消的情况在上一行解决了
if(lst[i]>l) dp[i]=((dp[i]+sum[lst[i]-1])%mod-sum[l-1]+mod)%mod;//对应第二段第一句话
sum[i]=(sum[i-1]+dp[i])%mod;//前缀和优化
}
cout<<dp[n]<<endl;
return 0;
}