1. 程式人生 > 其它 >I-Increasing Subsequence 期望DP+逆元

I-Increasing Subsequence 期望DP+逆元

題目傳送門

題意

  給定一個長度為\(n\),元素為\(1,2,\dots,n\)的某種序列,\(Alice\)\(Bob\)輪流取數,要求每次取的數都要大於先前兩個人曾選過的數,並且當前選擇的數要在前一位玩家選擇位置的右邊,當前可以選擇的數都將會等概率被選擇,求出能夠進行遊戲輪次的期望值。

思路

  由於存在兩個人選擇,因此我們可以使用二維陣列來分別表示兩個人選擇的位置,有兩個人,所以可以用兩個二維陣列來分別表示是誰在做選擇,當然也可以增加一維來表示。於是我們想到可以使用動態規劃的方式來計算期望值。

 暴力計算\(O(N^3)\)

  假設我們分別使用\(g[i][j],f[i][j]\)

表示當前是\(Alice選\)和當前是\(Bob\)選擇,那麼兩個狀態之間的轉移就取決於當前能夠選擇數的個數,期望的貢獻就等於從另一個狀態轉移到當前狀態的期望總和。

  參考程式碼
點此展開
//Author:Daneii
//O(n^3)演算法,必定超時
#include <bits/stdc++.h>

using namespace std;

#define in(x) scanf("%d",&x)
#define lin(x) scanf("%lld",&x)
#define din(x) scanf("%lf",&x)

typedef long long ll;
typedef long double ld;
typedef pair<int,int> PII;

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

int n;
ll a[N];
ll f[N][N],g[N][N];
ll inv[N];

//O(N)時間內預處理出1...n中每個數的逆元
void inv_init()
{
    inv[0]=inv[1]=1;
    for(int i=2;i<=n;i++)
        inv[i]=(mod-mod/i)*inv[mod%i]%mod;
}

int main()
{
    #ifdef LOCAL
    freopen("D:/VSCodeField/C_Practice/.input/a.in", "r", stdin);
    #endif

    in(n);
    inv_init();
    for(int i=1;i<=n;i++) lin(a[i]);

    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=n;j++)
        {
            if(i==j) continue;
            if(j==0)
                g[i][0]=inv[n];
            //列舉k
            //首先是現在是第一個人來選
            int cnt=0;//統計個數
            for(int k=j+1;k<=n;k++)
            {
                if(a[k]>a[i]&&a[k]>a[j])//滿足每次選擇的數都比先前的要大
                    cnt++;
            }
            //第一個人選擇後轉移到第二個人
            for(int k=j+1;k<=n;k++)
            {
                if(a[k]>a[i]&&a[k]>a[j])
                {
                    f[i][k]=(f[i][k]+inv[cnt]*g[i][j])%mod;
                }
            }

            //第二個人選
            cnt=0;
            for(int k=i+1;k<=n;k++)
            {
                if(a[k]>a[j]&&a[k]>a[i])
                {
                    cnt++;
                }
            }
            for(int k=i+1;k<=n;k++)
            {
                if(a[k]>a[j]&&a[k]>a[i])
                {
                    g[k][j]=(g[k][j]+inv[cnt]*f[i][j])%mod;
                }
            }
        }
    }

    ll ans=0;
	//將所有情況累加起來就是結果
    for(int i=1;i<=n;i++)
        for(int j=0;j<=n;j++)
            if(i==j) continue;
            else ans=((ans+f[i][j])%mod+g[i][j])%mod;
        
    printf("%lld\n",ans);

    return 0;
}

 優化計算\(O(N^2)\)

  在暴力演算法中,我們發現每次都需要計算比當前選擇數大的數的個數,也就是\(k\)的個數,由於每次都需要遍歷一遍陣列,增加了一維迴圈。我們考慮是否能夠將這一維優化掉。由於\(k\)的個數,只與當前的數有關係,因此我們可以預處理一下\(k\)與每個數的大小關係,從而對於個數查詢實現\(O(1)\)的查詢。
  我們會發現,由於\(k\)必須要大於\(i\)或者\(j\),相當於每次都是看右邊的陣列,因此我們可以修改陣列的定義,讓\(f[i][j]\)\(Alice\)上一輪選擇了\(i\),\(Bob\)上一輪選擇了\(j\),那麼\(i\)\(j\)位置上元素的大小關係就可以反應是誰在選擇,而\(k\)

的個數只需要我們從後向前計算,累計比當前\(i\)大和比當前\(j\)大的個數就可以實現\(O(1)\)的查詢。
  現在考慮狀態轉移,如果當前是\(a[i]>a[j]\),說明當前\(Bob\)選,因此狀態轉移就是\(f[i][j]=1+\sum_{k>j,a[k]>a[i]}f[i][k]\),如果是\(a[i]<a[j]\),說明當前是\(Alice\)選,狀態轉移為\(f[i][j]=1+\sum_{k>i,a[k]>a[j]}f[k][j]\).

  參考程式碼
點此展開
//Author:Daneii
#include <bits/stdc++.h>

using namespace std;

#define in(x) scanf("%d",&x)
#define lin(x) scanf("%lld",&x)
#define din(x) scanf("%lf",&x)

typedef long long ll;
typedef long double ld;
typedef pair<int,int> PII;

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

int n;
ll p[N];
ll cnt[N];//用於統計比p[i]大的對應cnt[j]的個數
ll sum[N];//用於計算字首和
ll inv[N];//預處理逆元
ll f[N][N];//狀態定義:Alice上輪選擇了i,Bob上輪選擇了j
//因此狀態轉移要注意p[i]與p[j]之間的大小關係

void inv_init()
{
    inv[1]=1;
    for(int i=2;i<=n;i++)
    {
        inv[i]=(mod-mod/i)*inv[mod%i]%mod;
    }
}

int main()
{
    #ifdef LOCAL
    freopen("D:/VSCodeField/C_Practice/.input/a.in", "r", stdin);
    #endif

    in(n);
    inv_init();
    for(int i=1;i<=n;i++) lin(p[i]);

    for(int i=n;i>=1;i--)
    {
        ll bcnt=0,pre=0;
        for(int j=n;j>=0;j--)//j等於0即無法進行
        {
            if(i==j) continue;//不存在這樣的局面
            if(p[i]>p[j])
            {
                //說明目前狀態時從Alice選
                f[i][j]=((pre*inv[bcnt])%mod+1)%mod;
                sum[j]=(sum[j]+f[i][j])%mod;//統計此時比j大的字首和
                cnt[j]++;//記錄下比j大的個數
            }
            else
            {   //此時是Bob選
                f[i][j]=(sum[j]*inv[cnt[j]]%mod+1)%mod;
                pre=(pre+f[i][j])%mod;
                bcnt++;
            }
        }
    }

    ll ans=0;
    for(int i=1;i<=n;i++)
        ans=(ans+f[i][0])%mod;
    printf("%lld",ans*inv[n]%mod);


    return 0;
}