題目指路: https://codeforces.com/contest/1968/problem/D
題目大意
Bodya, Sasha 兩人博弈
輸入為
- 一個n的排列p
- 一個長為n的陣列a(a[i]代表在i位置的分數)
- 可操作次數k
- 和各自的起點
每次操作,若玩家的位置為pos
- 玩家先加上當前位置下標對應的分數a[pos]
- 然後可以選擇:
- 留在當前位置pos
- 或者前往該位置在排列的對應元素p[pos]
若雙方都以最優策略遊戲,求最後獲勝的人,輸出姓名,若平局則輸出Draw
思路
由排列的性質可以知道,如果以i->p[i]的規則建圖e的話,會形成一個或多個有向環
所以題目等價於:求B和S在各自出發點所在的環上操作k次的最大得分
每次操作:要麼留在原地,要麼沿著有向邊往下走一步
比較妙的地方,同時也是我tle的地方在於:對這個最大值的求法
如果k足夠大,那麼得分最大化的策略很顯然:
走到環上得分最大的點上,然後一直不動。
但是k有限制,那麼在點pos:
- 如果a[e[pos]]>a[pos],也就是下一個點的得分大於當前點,那麼必然移動。
- 反之,那麼可能移動/不移動。狀態無法確定。
求得分最大值
首先得注意到:最大操作次數是: 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;
}
收穫
對我來說,這題的突破點在於意識到“回到自身前必然在某點停下”這條資訊,並且利用這點列舉終點求最大值。