#CF D. MEX Sequences 题解(状态机dp)

6 篇文章 0 订阅
3 篇文章 0 订阅

原题链接.

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. 详见五个细节

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值