P11234 [CSP-S 2024] 擂臺遊戲 題解
前言
作者在考場上用了約 1h 把前三道題做完了,然後用了約半小時想了帶 \(\log\) 的做法,但是我決定放手一搏去想線性的做法,於是又想了有 1h 之後覺得想到了正解,然後我就一直寫到了考試結束,但是最終沒有調出來遺憾離場,因此寫個題解來紀念一下。
題解
首先因為 \(n,m\) 是同階的,所以肯定是考慮把所有位置的答案全部算出來。首先,設 \(N\) 為第一個不小於 \(n\) 的 \(2\) 的整數次冪,我們用 \(N-n\) 個 \(0\) 去補齊剩下的位置,第一步肯定是把所有輪獲勝的人用一棵線段樹建出來,這一步是好做的。
現在考慮依次求出每個位置的答案 \(ans_i\),我第一想法是正著去掃,但是感覺不太好做,所以決定倒著去掃。有一個顯然的結論就是,如果一個位置從一個確定的數變成了任意值,那麼原來在答案裡的人現在依然在答案裡,也就是說,對於二進位制位數相同的一段,\(ans_i\) 是不增的,於是現在就考慮計算如果一個位置由確定值變成了任意值,有哪些新的可能的答案。當然,對於二進位制位數不同的段,每次都要清空並重新計算,以下僅考慮對於同一個段的計算。
首先,我們線段樹先預處理出了每個結點是哪一個人會獲勝,對於當前的段,答案裡肯定有本來就會獲勝的那一個人,所以先講其計入答案(注意,以下說的計入答案都指的是先判斷這個人是否已被計入答案再加,因為可能有重複)。
接著,如果對於一個線段樹的結點,滿足以下條件,我們就將其稱為壞點:這個區間的 \(d=0\),且編號小的人會獲勝。如果一個結點是壞點,那麼對於這個結點右兒子中包含的所有的人,他們由確定值變為任意值對答案都是沒有貢獻的,因為到這個結點的時候他們一定都會被打敗(所有人是從後往前一個一個變為不確定的,所以一個人變之後前面的所有人的值依然是確定的)。那麼我們可以給整個區間打上標記,表示這個區間都沒有貢獻。如果掃到了一個被打標記的人,就直接跳過。
否則的話,這個人一定是有貢獻的,因為只要將他的初始值設為 \(\infty\),他就一定會贏,將他計入答案。接著考慮哪些人也會跟著計入答案,我們考慮從當前這個人代表的葉子結點不斷往上跳。設當前點為 \(x\),父親為 \(fa_x\),我們分 \(x\) 是 \(fa_x\) 的左兒子還是右兒子兩種情況來考慮:
- \(x\) 是右兒子,那麼 \(fa_x\) 的左兒子結點的勝者 \(y\) 就有可能會被記入答案,具體的,我們對於每一個結點都再記錄一個值 \(mx\),表示根到這個結點的所有對局中,\(d\) 為選到它的所有對局中輪數最大的是多少,這個也可以在預處理中簡單計算。那麼如果 \(a_y\ge mx\),\(y\) 就能被計入答案,因為我們可以將所有的任意值設為剛好在與 \(y\) 對局時輸掉,這樣就能獲勝。
- \(x\) 是左兒子且 \(fa_x\) 是一個壞點,那麼此時 \(fa_x\) 右兒子中的所有人都已經變成了任意值,他們都應該被計入答案。可以理解為這些人原來有個限制使得他們不能獲勝,現在這個限制沒了,所以這些人都可以獲勝了。
- \(x\) 是左兒子且 \(fa_x\) 不是一個壞點,那麼在繼續往上跳的貢獻都已經被 \(fa_x\) 的右兒子計算過了,直接 break。
於是,我們就計算完了所有的貢獻,每次在一個人變之前,讓 \(ans_i\) 賦為當前的答案即可。
考慮時間複雜度,首先建樹什麼的都是線性的,然後對於計算答案的部分,我們考慮對於每一個線段樹的結點,它不可能同時被左兒子和右兒子訪問(因為被左兒子訪問要求這是個壞點,但是如果是壞點的話右兒子直接被打上標記跳過了),所以每個結點都只會被訪問一次,所以複雜度是 \(\mathcal{O}(Tn)\) 的。
另外,還有一個需要注意的問題:如果每一個二進位制位數不同的段都預處理 \(mx\) 的話常數太大了,我們發現每個結點的 \(d\) 是一開始給定的,所以這個只需要在 \(T\) 組資料前預處理即可。其他細節,比如讀入之類的具體可以看程式碼。
程式碼
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ls x<<1
#define rs x<<1|1
#define lson x<<1,l,mid
#define rson x<<1|1,mid+1,r
#define ll long long
using namespace std;
const int N = (1<<17)+5;
int mx[20][N<<2],lg[N],aa[N],ok[N],a[N],c[N],n,m,nn = 1;
bool vis[N];ll ans[N],sum;
struct node{ll sum;int x;bool tp,d;}t[N << 2];
inline void build(int x,int l,int r)
{
if(l == r){t[x].x = l;return ;}
int mid = l+r>>1,k = lg[r-l+1],d = t[x].d;
build(lson);build(rson);
int win = a[t[x<<1|d].x] >= k;
t[x].x = t[x<<1|(d^!win)].x;
if(!d&&win)t[x].tp = 1,ok[r]++,ok[mid]--;
else t[x].tp = 0;
}
inline void build2(int x,int l,int r,int *mx)
{
t[x].sum = (l+r)*(r-l+1ll)/2;
if(l == r)return ;
int mid = l+r>>1,k = lg[r-l+1],d = t[x].d;
if(~mx[x])mx[ls] = mx[rs] = mx[x];
else mx[x<<1|d] = k;
build2(lson,mx);build2(rson,mx);
}
inline void up(int x){sum += !vis[x]*x;vis[x] = 1;}
inline int rd()
{
char c;int f = 1;
while(!isdigit(c = getchar()))if(c=='-')f = -1;
int x = c-'0';
while(isdigit(c = getchar()))x = x*10+(c^48);
return x*f;
}
inline char gc()
{char c;while((c = getchar()) <= ' ');return c;}
int main()
{
// freopen("arena.in","r",stdin);
// freopen("arena.out","w",stdout);
n = rd();m = rd();
for(int i = 1;i <= n;i++)aa[i] = rd();
for(int i = 1;i <= m;i++)c[i] = rd();
while(nn < n)nn <<= 1;
for(int i = 2;i <= nn;i++)lg[i] = lg[i>>1]+1;
for(int i = nn/2;i;i >>= 1)
for(int j = i;j < i*2;j++)
t[j].d = gc()-'0';
memset(mx,-1,sizeof mx);
for(int s = 0;(1<<s) <= nn;s++)
build2(1<<s,1,nn>>s,mx[s]);
for(int T = rd();T--;)
{
int yh[4] = {rd(),rd(),rd(),rd()};
for(int i = 1;i <= n;i++)a[i] = aa[i]^yh[i&3];
memset(ok,0,sizeof ok);build(1,1,nn);
for(int i = nn,rt = 1,s = 0;i;i--)
{
if((1<<lg[i]) == i)
{
if(i != nn)rt <<= 1,s++;
for(int j = 1;j <= i;j++)vis[j] = 0;
sum = 0;up(t[rt].x);
}
ans[i] = sum;
if(ok[i] += ok[i+1])continue;
up(i);int x = i+nn-1;
while(x != rt)
{
int d = x&1;x >>= 1;
if(!d)
if(t[x].tp)sum += t[rs].sum;
else break;
else if(a[t[ls].x] >= mx[s][ls])up(t[ls].x);
}
}
ll num = 0;
for(int i = 1;i <= m;i++)num ^= i*ans[c[i]];
printf("%lld\n",num);
}
return 0;
}
總結
這是一道細節很多的好題,我覺得考場上至少再給我個 1h 才能寫出來,但是我覺得能想出來已經很不錯了,NOIP 再戰吧!
關於程式碼或做法有任何問題的歡迎私信我!