原题链接.
1.题意:
给你一个线性序列,其元素是大小不超过序列长度的自然数,求其中满足某性质的子序列个数。设子序列a[n],如果他的任一长度为i的前缀都满足 ∣ a [ i ] − m e x ∣ < = 1 |a[i]-mex|<=1 ∣a[i]−mex∣<=1(mex为除去该前缀所有的数之外最小的自然数),我们就说这个子序列具有该性质。
2.思路:
法一:(我的方法)
考虑用dp解题,那么为了进行状态转移,我们需要知道具有该性质的子序列有怎样的规律。
手画一下发现满足该性质的子序列无非有两种。其一:称之为"递增序列",其定义为:1)第一个元素是0 ; 2)
a
[
i
+
1
]
=
a
[
i
]
或
a
[
i
]
+
1
a[i+1]=a[i]或a[i]+1
a[i+1]=a[i]或a[i]+1
其二:称之为“反复横跳序列“ ,该序列特点是有一个为递增序列的前缀(长度可为0),然后存在
a
[
i
+
1
]
=
a
[
i
]
+
2
a[i+1]=a[i]+2
a[i+1]=a[i]+2,而后,序列的数只能取a[i]+2或a[i]。举个例子:如序列0 1 2 3 3 4 4 4 5 5 6 6 8 6 8 6 6 8 8
可以看出,从6到8破坏了前缀的递增性,而后的元素只能取6或8.
于是我们可以定义一个dp[i][j]来维护前i个元素中以j为结尾的递增序列个数,再定义一个sp[i][j][2] ,其中sp[i][j][0]表示前i个元素中以j\j+2结尾的反复横跳序列,sp[i][j][1]为前i个元素中以j\j-2结尾的反复横跳序列。
这里有5个小细节:
1.为什么sp数组要增加一维?考虑一下数据:0 2 4 这样的话可以发现0 2 4也会被统计在内,这是因为反复横跳的话,跳上去后,必须要跳下来。因此增加一维表示的就是这个以j为结尾的序列是跳上去还是跳下来,这样就可以状态转移了。这个地方让我wa了一发
2.考虑到dp数组比较大,最好全局定义。但是全局定义的话就涉及初始化的问题,可以发现,如果对整个dp数组memset的话必然超时,这个地方又让我TLE了一发,因此需要对长度为n的数组进行初始化,这个过程应该每一次求完答案后进行。
3.dp优化,省略一维
4.为了方便处理边界问题,这里我用了个小技巧,在这个序列最前端假想了1个-1,大家可以考虑一下这样的妙处。
5.状态转移要备份,最后一起赋值。(因为第一维必须是i-1,更新了第一维就相当于i了)
状态转移方程如下:
dp[i][j]=dp[i-1][j]+dp[i][j-1]+dp[i-1][j]
sp[i][j][1]=sp[i-1][j][1]+sp[i-1][j-2][0]+dp[i-1][j-2]+sp[i-1][j][1]
sp[i][j][0]=sp[i-1][j][0]+sp[i-1][j+2][1]+sp[i][j][0]
法二(更优美的方法)
仍用dp,不过数组的意义不一样。
用dp[i][j]表示前i个数中mex为j且符合要求的递增序列的个数
用sp[i][j]表示前i个数中mex为j且符合要求的反复横跳序列个数
3.代码
方法一
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N=5e5+10;
const int mod=998244353;
typedef long long LL;
int dp[N];//dp[i][j]代表前i个数中以j结尾的单增子序列的个数,
int sp[N][2];//sp[i][j][0]代表前i个数中所有以j j+2为重复段元素且结尾为j的“反复横跳序列”
//sp[i][j][1]代表前i个数中所有以j-2 j为重复段元素且结尾为j的“反复横跳序列”
int ans,n;
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
cin>>t;
while(t--)
{
//memset(dp,0,sizeof dp);
ans=0;
cin>>n;
if(n==-1)break;
//for(int i=0;i<=n+1;i++)dp[i]=sp[i][0]=sp[i][1]=0;
dp[0]=1;//假想一个-1序列
sp[0][0]=1;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
x++;
if(x==1)//0
{
dp[x]=((LL)dp[x]+dp[x]+1)%mod;
sp[x][0]=((LL)sp[x][0]+sp[x][0]+sp[x+2][1])%mod;
}
else
{
dp[x]=((LL)dp[x]+dp[x]+dp[x-1])%mod;
int aa=sp[x][0],bb=sp[x][1];
if(x+2<=n+1)aa=((LL)sp[x][0]+sp[x][0]+sp[x+2][1])%mod;
if(x-2>=0)bb=((LL)sp[x][1]+sp[x][1]+((x==2)?1:sp[x-2][0]+dp[x-2]))%mod;
sp[x][0]=aa,sp[x][1]=bb;
//cout<<bb<<endl;
}
}
for(int i=1;i<=n+1;i++)
ans=(ans+(LL)dp[i]+sp[i][0]+sp[i][1])%mod;
cout<<ans<<endl;
for(int i=0;i<=n+1;i++)dp[i]=sp[i][0]=sp[i][1]=0;
}
}
方法二
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N=5e5+10;
const int mod=998244353;
typedef long long LL;
int dp[N];//dp[i][j]代表前i个数中mex值为j结尾的单增子序列的个数,
int sp[N];//sp[i][j]代表前i个数中所有mex值为j的“反复横跳序列”
int ans,n;
int main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
cin>>t;
while(t--)
{
int n;
cin>>n;
dp[0]=1;
ans=0;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
if(x>=1)sp[x-1]=((LL)sp[x-1]+sp[x-1]+dp[x-1])%mod;
if(x<=n-1)sp[x+1]=((LL)sp[x+1]+sp[x+1])%mod;
dp[x+1]=((LL)dp[x+1]+dp[x+1]+dp[x])%mod;
}
for(int i=1;i<=n+1;i++)
ans=((LL)ans+dp[i])%mod;
for(int i=0;i<=n-1;i++)
ans=((LL)ans+sp[i])%mod;
cout<<ans<<endl;
for(int i=0;i<n+1;i++)
sp[i]=dp[i]=0;
}
}
4.收获
1.dpdp,你真细
2. 详见五个细节