[USACO22OPEN] 262144 Revisited P [題解]
\(262144\) \(Revisited\) \(P\)
題意
一個遊戲規則如下:
給定一個長度為 \(N\) 的陣列 \(A\),每一次可以選擇相鄰的兩個數合併,合併過後將其替換為一個大於兩個數最大值的數字(例如 \((5, 7)\) 可以被合併為 \(8\))。顯然,遊戲會在 \(N - 1\) 輪後結束,此時只會剩下一個數,你的目的是儘可能讓這個數小。
在這個陣列的所有連續子段上進行遊戲,輸出所有連續子段上的遊戲結果的和。
對於所有資料: \(2\leq N\leq 262144, 1\leq A_i \leq 10 ^ 6\)
\(Subtask1:N\leq300\)
\(Subtask2:N\leq3000\)
\(Subtask3:A_i\leq 40\)
\(Subtask4:A_i\) 單調不降
\(Subtask5:\) 沒有額外限制
分析
\(Subtask1\)
與題目 248 相似。
設 \(dp[i][j]\) 表示區間 \([i,j]\) 合併能夠得到的最小值,顯然,有如下狀態轉移方程:
\[dp[i][j] = A_i,i = j \] \[dp[i][j] = min_{i\leq k<j}max(dp[i][k],dp[k+1][j]) + 1 \]時間複雜度 \(O(N^3)\)
\(Subtask2\)
在子任務 \(1\) 的基礎上,可以通過快速找到決策點 \(k\)
具體的,找到最大的 \(x\) 使得 \(dp[i][x]\leq dp[x+1][j]\)。之後,我們僅需要在 \(k\in\{ x, x+1\}\) 中做出決策即可。
考慮這樣做為什麼是對的。
顯然,隨著 \(x\) 的逐步增大,\(dp[i][x]\) 單調不降,\(dp[x+1][j]\) 單調不增,不難將轉移轉化為如下影象。
具體的,使用二分查詢可以快速找到決策點。
\(Subtask3\)
與題目 262144 相似。
考慮我們怎麼合併,當 \(N\) 為 \(2\) 的冪時,每次將 \((2 \times k - 1, 2 \times k)\)
設 \(f[i][k]\) 表示最大滿足區間 \([i,f[i][k]]\) 合併出 \(k\) 的右端點。則 \(f[i][k+1] = f[f[i][k]+1][k]\)。
即相當於區間 \([i,f[i][k]]\) 合併出 \(k\),區間 \([f[i][k] + 1, f[f[i][k] + 1][k]]\) 同樣合併出 \(k\),則合併兩個區間,即為如上狀態轉移方程。
需要注意的是,我們每次求的是滿足條件的最大右端點,以 \(f[i][k]+1\) 為左端點的區間合併出 \(k\) 的右端點顯然最大,所以只從這個位置更新。
最後統計答案列舉 \(i,k\) 即可,時間複雜度為 \(O(NmaxA_i)\)。
\(Subtask4\)
對於此類連續子段貢獻問題,我們可以考慮每次向右擴充套件一個數,再計算囊括這個數的連續子段的貢獻,這個子任務也可以通過這樣的方式解決。
我沒列舉右端點,由於 \(A_i\) 被排序,所以這些區間合併出來的值 \(v\) 必然滿足 \(v\in \{A_i,A_i+logi \}\),具體證明在子任務 \(3\) 中有簡單陳述。
我們將 \(1……i\) 劃分為多個連續子段,滿足任何一個連續子段再向左擴充套件一個元素就會導致其值超過 \(A_i\)。我們將一個子段看成一個元素,則有 \(\{ x,A_i,A_i,A_i……A_i\}\) 至多隻有從左往右第一個元素可能小於 \(A_i\),考慮反正,如果有兩個,則我們顯然可以向左合併一個子段,以得到更優的結果。而在這裡 \(x\) 實際上等價於 \(A_i\),用 \(x\) 合併和用 \(A_i\) 來合併實際上是一樣的。
假設我們想讓合併出來的結果為 \(v\),則我們需要的元素個數為 \(2^{v-A[i]}\),倍增的向右移動,即相當於列舉不同的結果,倍增統計答案每次的複雜度顯然是 \(logN\)。
如何維護連續子段?
每次在最後加入新的子段 \([i,i]\),顯然 \([i,i]\) 本身即滿足如上所述的向左最大性。當我們由 \(i - 1\) 向 \(i\) 更新時,每次將連續的兩個子段合併,合併 \(A_i - A_{i - 1}\) 次或者直到只剩下除 \([i,i]\) 外的一個連續子段。由於每次子段減少一半,這顯然也是 \(logN\) 的複雜度。
時間複雜度 \(O(NlogN)\)
\(Full\) \(Credit\)
考慮優化 \(dp\) 過程。稱一個子段是極大的,當且僅當其向左或向右擴充套件都會使這個子段合併出來的值增大。
引理:設 \(f(N)\) 表示大小為 \(N\) 的序列的極大連續子段的數量,則 \(f(N) = O(NlogN)\)。
證明:考慮原序列的笛卡爾樹,設序列的其中一個最大元素的位置為 \(p\),則有:
\[f(N)\leq f(p-1) + f(N - p) + C \]其中 \(C\) 為原序列包含位置 \(p\) 的最大子段數量,且 \(C\leq O(plog(\frac{N}{p}))\)。
具體證明如下:
包含位置 \(p\) 且合併值為 \(A_p + k\) 的連續子段個數\(\leq\) \(min(p, 2^k)\),限制 \(p\) 即來自所有具有固定值但具有不同左端點的區間在這裡最多有 \(p\) 個,而限制 \(2^k\) 是因為值從 \(A_p + k - 1\) 到 \(A_p + k\) 必然會經過擴充套件,而這個擴充套件會選擇向左或是向右,這裡我們需要從 \(p\) 開始擴充套件 \(k\) 次。
而 \(\sum_{k = 0}^{log_2N}\) [包含位置 \(p\) 且合併值為 \(A_p + k\) 的連續子段個數]
\(O(plog\frac{N}{p}) \leq O(log\frac{N!}{(p-1)!(N-p)})\)
則 \(f(N)\leq f(p-1)+f(N - p)+C\leq O(log(p-1)!)+O(log(N-p)!)+O(logN!-log(p-1)!-log(N-p)!)\leq O(logN)\)
之後,我們用並查集維護極長連續子段即可。
具體的,列舉值 \(v\),找出答案為 \(v\) 的連續子段的個數。
用 \(set\) 維護第 \(v-1\) 輪 的區間左端點。考慮如果當前一段連續的 \(1 1 1 1 1 1\) 我們應該怎麼維護。
事實上,我們發現,即使 \(11\) 是一個極長連續子段,但我們仍然不能直接將最開始的兩個 \(1\) 通過並查集合並在一起,因為同樣的道理,第二三兩個 \(1\) 也能組成一個最長連續子段,這樣做我們肯定會漏算。
所以只有從最後開始合併極長連續子段,再通過倍增記錄前面每一個位置能夠延伸至的最長連續子段的位置。比如,現將最後兩個 \(1\) 合併,然後將第一個 \(1\) 的指標指向第二個 \(1\),即第一個 \(1\) 和第二個 \(1\) 組成了一個最長連續子段,以此類推。那麼,我們發現,當我們的倍增觸碰到底部的時候,當前位置也同樣可以併入最後的極長連續子段,而噹噹前段落合併完成後,我們才從 \(set\) 中將其刪除。
但刪除過後,我們再在這一段數的最後一個位置的末尾打上一個標記,當 \(A_i = v\) 時,\(A_i\) 自成一個極長連續子段,我們將這個並查集提取出來,同樣放進 \(set\),重新合在一起更新。
總之,我們能夠通過這樣的操作,計算出答案為 \(v\) 的連續欄位的數量。
時間複雜度 \(O(Nlog^2N)\)
\(code\)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 10, M = 1e6 + 50;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
struct node{ //並查集
int fa[N], siz[N];
inline void initial(int n){
for(register int i = 0; i < n; i++) fa[i] = i, siz[i] = 1;
}
inline int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
inline void merge(int x, int y){
int fx = find(x), fy = find(y);
if(fx == fy) return; //已經合併過
if(siz[fx] > siz[fy]) swap(fx, fy);
fa[fx] = fy, siz[fy] += siz[fx], siz[fx] = 0;
}
}T;
int n, ans;
int L[N], R[N], arr[N];
set<int> s; //儲存極值為 v 的極長子段
vector<int> vec[M];
inline int Get_R(int x)
{
if(x == n) return n;
return R[T.find(x)];
}
signed main()
{
memset(L, -1, sizeof(L)), memset(R, -1, sizeof(R));
n = read();
for(register int i = 0; i < n; i++) arr[i] = read();
for(register int i = 0; i < n; i++) vec[arr[i]].push_back(i);
T.initial(n);
//M -> 最大值,值的極限
for(register int v = 1; v <= M - 1; v++){
vector<int> ed, tem;
int res = 0; //記錄有多少個值為 v 的區間
for(register int x : s){ //遍歷值為 v - 1 的極大區間的左端點
int r = Get_R(x); //找到右端點 + 1
int nexr = max(r, r == n ? -1 : Get_R(r)); //倍增標記是否觸底
if(nexr == r) ed.push_back(x); //為末端區間
else{
if(L[nexr] != -1) ed.push_back(x); //被標記過,需要被合併
else L[nexr] = x, tem.push_back(nexr); //未被標記過,標記
res += (nexr - r) * T.siz[T.find(x)], R[T.find(x)] = nexr;
}
}
for(register int x : ed){ //合併
s.erase(x);
if(L[Get_R(x)] == -1) L[Get_R(x)] = x;
else T.merge(L[Get_R(x)], x);
}
for(register int x : tem) L[x] = -1; //清空標記
for(register int x : vec[v]){ //放入 arr[x] = v 的 x
++res, R[x] = (x + 1), s.insert(x);
if(L[x] != -1) s.insert(L[x]);
L[x] = -1; //清空標記
}
ans = ans + res * v;
}
printf("%lld\n", ans);
return 0;
}
\(Solution2\)
由 \(Subtask4\) 優化得到的方法,大致思路如下:
建立笛卡爾樹,找到最大值,遞迴處理左右兩個區間,然後再計算包含最大值的貢獻。