1. 程式人生 > 其它 >[USACO22OPEN] 262144 Revisited P [題解]

[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\)

使得時間複雜度優化為 \(O(N^2logN)\)

具體的,找到最大的 \(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)\)

合併,不則每次個數減半,得到的答案最大為 \(max\) \(a_i+logN\)

\(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\) 優化得到的方法,大致思路如下:

建立笛卡爾樹,找到最大值,遞迴處理左右兩個區間,然後再計算包含最大值的貢獻。

後記