Codeforces_943_D_Permutation Game

Gusare發表於2024-10-22

題目指路: https://codeforces.com/contest/1968/problem/D

題目大意

Bodya, Sasha 兩人博弈

輸入為

  1. 一個n的排列p
  2. 一個長為n的陣列a(a[i]代表在i位置的分數)
  3. 可操作次數k
  4. 和各自的起點

每次操作,若玩家的位置為pos

  1. 玩家先加上當前位置下標對應的分數a[pos]
  2. 然後可以選擇:
    • 留在當前位置pos
    • 或者前往該位置在排列的對應元素p[pos]

若雙方都以最優策略遊戲,求最後獲勝的人,輸出姓名,若平局則輸出Draw

思路

由排列的性質可以知道,如果以i->p[i]的規則建圖e的話,會形成一個或多個有向環
所以題目等價於:求B和S在各自出發點所在的環上操作k次的最大得分
每次操作:要麼留在原地,要麼沿著有向邊往下走一步
比較妙的地方,同時也是我tle的地方在於:對這個最大值的求法

如果k足夠大,那麼得分最大化的策略很顯然:
走到環上得分最大的點上,然後一直不動。

但是k有限制,那麼在點pos:

  1. 如果a[e[pos]]>a[pos],也就是下一個點的得分大於當前點,那麼必然移動。
  2. 反之,那麼可能移動/不移動。狀態無法確定。

求得分最大值

首先得注意到:最大操作次數是: min(n,k)
k是題目給的步數限制,這個容易理解
但是也要注意到,一個環的階(就是組成環的點的數量)最大為n,此時所有的排列只組成一個環。
有了前面對k有限制下的分析,可以知道:
一個點如果在環上一直往下走,在回到本身之前,必然在某點停下。
注意到這點很重要,因為知道了這點我們可以列舉停下的位置,然後在所有這些位置裡求得分最大值,這個最大值就是該玩家的得分最大值。
在點u停下的得分=得分字首和+(k-已操作次數)*a[u]
每個點的得分可以由前面的點遞推得到

更細節的講解可以見程式碼 > <

AC程式碼

#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f3f3f3f3f
using namespace std;

ll e[200005],a[200005]; //e存圖,a儲存得分
ll n,k,pb,ps,p[200005]; //p儲存n的排序,pb和ps是B和S兩位玩家的出發點

ll score(ll pp,ll lmt);
void solve();

ll score(ll pp,ll lmt){
    vector<bool> vis(n+1,false); //vis陣列記錄點是否走過(由分析可知環上每個點最多走一次)
    ll ans=0;	//ans維護得分最大值
    ll sum=0;  //sum記錄得分字首和
    ll cnt=0;  //cnt記錄已操作次數
    while(!vis[pp] && cnt<=lmt){ //兩層限制:(1) 回到自身停下 (2)不能超過最大操作次數lmt
        ans=max(ans,sum+a[pp]*(k-cnt));	//得分=字首和+剩餘操作次數*該點得分
        sum+=a[pp];	//字首和更新
        vis[pp]=true; //該點標記為走過
        pp=e[pp]; //往下走
        cnt++; //操作次數自增
    }
    return ans; //返回最大值
}

void solve(){
    cin>>n>>k>>pb>>ps;
    for(ll i=1;i<=n;i++){ //存序列,建圖
        cin>>p[i];
        e[i]=p[i];
    }
    for(ll i=1;i<=n;i++){ //存得分
        cin>>a[i];
    }

    ll lmt=min(n,k);  //操作的最大次數限制
    ll bb=score(pb,lmt);
    ll ss=score(ps,lmt);

    // cerr<<bb<<' '<<ss<<'\n';

    if(bb==ss){
        cout<<"Draw\n";
    } else if(bb>ss){
        cout<<"Bodya\n";
    } else {
        cout<<"Sasha\n";
    }
}
int main(){
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    ll t=1;
    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

收穫

對我來說,這題的突破點在於意識到“回到自身前必然在某點停下”這條資訊,並且利用這點列舉終點求最大值。