2021牛客暑期多校训练营1 I-Increasing Subsequence(期望dp+优化)

I-Increasing Subsequence

f i , j , 0 / 1 f_{i,j,0/1} fi,j,0/1表示上一轮第一个人选了 i i i,第二个人选了 j j j,并且当前是第1/2个人选择的概率

转移考虑枚举k下一步往哪走
f i , k , 1 = ∑ f i , j , 0 / cnt f_{i,k,1}=\sum f_{i,j,0}/ \text{cnt} fi,k,1=fi,j,0/cnt

f k , j , 0 = ∑ f i , j , 1 / cnt f_{k,j,0}=\sum f_{i,j,1}/ \text{cnt} fk,j,0=fi,j,1/cnt

答案是所有数组之和:每进一个新状态都意味着游戏多进行了一局,因此把出现的概率全部累加就是期望局数。
显然的暴力~
时间复杂度: O ( n 3 ) O(n^3) O(n3)

Code1
#include<bits/stdc++.h>

using namespace std;
using ll=long long;

const int N=5010;
const int mod=998244353;

ll f[N][N][2];
ll inv[N];
int a[N],n;
ll qmi(ll a,ll b)
{
    ll res=1;
    while(b)
    {
        if(b&1) res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}
int main()
{
    ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
    cin>>n;
    for(int i=1;i<=n;i++) inv[i]=qmi(i,mod-2);
    for(int i=1;i<=n;i++) cin>>a[i];
    
    for(int i=1;i<=n;i++)
        for(int j=0;j<=n;j++)
        {
            if(j==0) f[i][j][0]=inv[n];
            
            int ct=0;
            // 第一个人
            for(int k=j+1;k<=n;k++)
                if(a[k]>a[i]&&a[k]>a[j]) ct++;
            for(int k=j+1;k<=n;k++)
                if(a[k]>a[i]&&a[k]>a[j]) f[i][k][1]=(f[i][k][1]+inv[ct]*f[i][j][0])%mod;
            // 第二个人
            ct=0;
            for(int k=i+1;k<=n;k++)
                if(a[k]>a[i]&&a[k]>a[j]) ct++;
            for(int k=i+1;k<=n;k++)
                if(a[k]>a[i]&&a[k]>a[j]) f[k][j][0]=(f[k][j][0]+inv[ct]*f[i][j][1])%mod;
        }
    ll ans=0;
    for(int i=1;i<=n;i++)
        for(int j=0;j<=n;j++) ans+=f[i][j][0]+f[i][j][1],ans%=mod;
    cout<<ans<<'\n';
    return 0;
}

没听懂讲题人上述解法的优化,询问了mrk大佬的思路。并且参考下面题解,感觉更容易理解lalalzo题解

对于上述做法关键是我们需要区分这一步是谁走的,而下面的做法考虑在所给序列的值域上从小到大走,保证了所选必须比前面所有选的值要大,还有一个限制就是对于每一个人的所选序号单增。

设计dp

状态表示: f u , v f_{u,v} fu,v表示最后一个人选择 v v v而倒数第二个人选择 u u u,值域从小到大走需要满足 u < v u<v u<v停下来的期望步数。

状态转移:考虑移动一步 f u , v → f v , k f_{u,v}\to f_{v,k} fu,vfv,k需要满足 u < v < k  and  pos u < pos k u<v<k \text{ and } \text{pos}_u<\text{pos}_k u<v<k and posu<posk
f u , v = 1 cnt ∑ k f v , k + 1 f_{u,v}=\frac{1}{\text{cnt}}\sum_k f_{v,k}+1 fu,v=cnt1kfv,k+1
发现满足 pos u < pos k \text{pos}_u<\text{pos}_k posu<posk意味着每一个人的所选序号单增。非常巧妙啊~

于是有下面Code2常见的记忆化搜索写法(我之前比较熟悉记忆化搜索)

Code2
#include<bits/stdc++.h>
using namespace std;
template <class T=int> T rd()
{
    T res=0;
    char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while( isdigit(ch)) res=(res<<1)+(res<<3)+(ch^48),ch=getchar();
    return res;
}
const int N=5010,mod=998244353;
using ll=long long;
int pos[N],n;
ll inv[N],f[N][N];
ll qmi(ll a,ll b)
{
    ll v=1;
    while(b)
    {
        if(b&1) v=v*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return v;
}
// u<v<k
ll dfs(int u,int v)
{
    if(f[u][v]!=-1) return f[u][v];
    // f[u][v] -> f[v][k]  u<v<k&&pos[u]<pos[k]
    int ct=0;
    for(int k=v+1;k<=n;k++) if(pos[u]<pos[k]) ct++;
    f[u][v]=(ct>0?1:0);
    for(int k=v+1;k<=n;k++) if(pos[u]<pos[k]) f[u][v]=(f[u][v]+dfs(v,k)*inv[ct])%mod;
    return f[u][v];
}
int main()
{
    n=rd();
    for(int i=1;i<=n;i++) pos[rd()]=i;
    for(int i=1;i<=n;i++) inv[i]=qmi(i,mod-2);
    memset(f,-1,sizeof f);
    ll ans=0;
    for(int i=1;i<=n;i++) ans=(ans+dfs(0,i))%mod;
    ans=ans*inv[n]%mod;
    printf("%lld\n",ans);
    
}
Code3

把上面记忆化搜索代码转化为迭代(考虑该状态会对哪些状态产生贡献),就有了下面的Code3

#include<bits/stdc++.h>
using namespace std;
template <class T=int> T rd()
{
    T res=0;
    char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while( isdigit(ch)) res=(res<<1)+(res<<3)+(ch^48),ch=getchar();
    return res;
}
const int N=5010,mod=998244353;
using ll=long long;
int pos[N],n;
ll inv[N],f[N][N];
ll qmi(ll a,ll b)
{
    ll v=1;
    while(b)
    {
        if(b&1) v=v*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return v;
}
int main()
{
    n=rd();
    for(int i=1;i<=n;i++) pos[rd()]=i;
    for(int i=1;i<=n;i++) inv[i]=qmi(i,mod-2);
    // f[k][i] -> f[i][j]  k<i<j && pos[k]<pos[j]
    // f[k][i] +=1/ct f[i][j] + 1
    for(int i=n;i>=1;i--)
    {
        for(int k=0;k<i;k++)
        {
            int ct=0;
            for(int j=i+1;j<=n;j++) if(pos[j]>pos[k]) ct++;
            for(int j=i+1;j<=n;j++)
                if(pos[j]>pos[k])
                    f[k][i]=(f[k][i]+inv[ct]*f[i][j]%mod);
            if(ct) f[k][i]=(f[k][i]+1)%mod;
        }
    }
    ll ans=0;
    for(int i=1;i<=n;i++) ans=(ans+f[0][i])%mod;
    ans=ans*inv[n]%mod;
    printf("%lld\n",ans);
    
}
Code4

不难发现每次我们想知道 pos k < pos j \text{pos}_k<\text{pos}_j posk<posj,可以预处理前缀和优化。

#include<bits/stdc++.h>
using namespace std;
template <class T=int> T rd()
{
    T res=0;
    char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while( isdigit(ch)) res=(res<<1)+(res<<3)+(ch^48),ch=getchar();
    return res;
}
const int N=5010,mod=998244353;
using ll=long long;
int pos[N],n;
ll sum[N],cnt[N],inv[N],f[N][N];
ll qmi(ll a,ll b)
{
    ll v=1;
    while(b)
    {
        if(b&1) v=v*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return v;
}
int main()
{
    n=rd();
    for(int i=1;i<=n;i++) pos[rd()]=i;
    for(int i=1;i<=n;i++) inv[i]=qmi(i,mod-2);
    for(int i=n;i>=1;i--)
    {
        memset(cnt,0,sizeof cnt);
        memset(sum,0,sizeof sum);
        for(int j=i+1;j<=n;j++)
        {
            cnt[pos[j]]++;
            sum[pos[j]]=f[i][j];
        }
        for(int j=n-1;j>=0;j--)
        {
            cnt[j]+=cnt[j+1];
            sum[j]=(sum[j]+sum[j+1])%mod;
        }
        for(int k=0;k<i;k++) 
        {
            int id=pos[k];
            f[k][i]=(sum[id]*inv[cnt[id]]%mod+1)%mod;
        }
    }
    ll ans=0;
    for(int i=1;i<=n;i++) ans=(ans+f[0][i])%mod;
    ans=ans*inv[n]%mod;
    printf("%lld\n",ans);
}

总结:

其实对于上述两种方法有本质的区别就是定义的状态一个是概率一个是期望步数,需要注意如何定义状态~

要加油哦~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值