[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]\) 合并能够得到的最小值,显然,有如下状态转移方程:
时间复杂度 \(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\),则有:
其中 \(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\) 优化得到的方法,大致思路如下:
建立笛卡尔树,找到最大值,递归处理左右两个区间,然后再计算包含最大值的贡献。