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

建立笛卡尔树,找到最大值,递归处理左右两个区间,然后再计算包含最大值的贡献。

后记

posted @ 2022-05-17 21:37  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(420)  评论(1编辑  收藏  举报