I-Increasing Subsequence
fi,j,0/1f_{i,j,0/1}fi,j,0/1表示上一轮第一个人选了iii,第二个人选了jjj,并且当前是第1/2个人选择的概率。
转移考虑枚举k下一步往哪走
fi,k,1=∑fi,j,0/cntf_{i,k,1}=\sum f_{i,j,0}/ \text{cnt}fi,k,1=∑fi,j,0/cnt
fk,j,0=∑fi,j,1/cntf_{k,j,0}=\sum f_{i,j,1}/ \text{cnt}fk,j,0=∑fi,j,1/cnt
答案是所有数组之和:每进一个新状态都意味着游戏多进行了一局,因此把出现的概率全部累加就是期望局数。
显然的暴力~
时间复杂度:O(n3)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
状态表示:fu,vf_{u,v}fu,v表示最后一个人选择vvv而倒数第二个人选择uuu,值域从小到大走需要满足u<vu<vu<v停下来的期望步数。
状态转移:考虑移动一步fu,v→fv,kf_{u,v}\to f_{v,k}fu,v→fv,k需要满足u<v<kand posu<posku<v<k \text{ and } \text{pos}_u<\text{pos}_ku<v<k and posu<posk
fu,v=1cnt∑kfv,k+1f_{u,v}=\frac{1}{\text{cnt}}\sum_k f_{v,k}+1fu,v=cnt1k∑fv,k+1
发现满足posu<posk\text{pos}_u<\text{pos}_kposu<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] + 1for(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
不难发现每次我们想知道posk<posj\text{pos}_k<\text{pos}_jposk<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);
}
总结:
其实对于上述两种方法有本质的区别就是定义的状态一个是概率一个是期望步数,需要注意如何定义状态~
要加油哦~