[USACO22OPEN] 262144 Revisited P [题解]
题意
一个游戏规则如下:
给定一个长度为 的数组 ,每一次可以选择相邻的两个数合并,合并过后将其替换为一个大于两个数最大值的数字(例如 可以被合并为 )。显然,游戏会在 轮后结束,此时只会剩下一个数,你的目的是尽可能让这个数小。
在这个数组的所有连续子段上进行游戏,输出所有连续子段上的游戏结果的和。
对于所有数据:
单调不降
没有额外限制
分析
与题目 248 相似。
设 表示区间 合并能够得到的最小值,显然,有如下状态转移方程:
时间复杂度
在子任务 的基础上,可以通过快速找到决策点 使得时间复杂度优化为 。
具体的,找到最大的 使得 。之后,我们仅需要在 中做出决策即可。
考虑这样做为什么是对的。
显然,随着 的逐步增大, 单调不降, 单调不增,不难将转移转化为如下图像。
具体的,使用二分查找可以快速找到决策点。
与题目 262144 相似。
考虑我们怎么合并,当 为 的幂时,每次将 合并,不则每次个数减半,得到的答案最大为 。
设 表示最大满足区间 合并出 的右端点。则 。
即相当于区间 合并出 ,区间 同样合并出 ,则合并两个区间,即为如上状态转移方程。
需要注意的是,我们每次求的是满足条件的最大右端点,以 为左端点的区间合并出 的右端点显然最大,所以只从这个位置更新。
最后统计答案枚举 即可,时间复杂度为 。
对于此类连续子段贡献问题,我们可以考虑每次向右扩展一个数,再计算囊括这个数的连续子段的贡献,这个子任务也可以通过这样的方式解决。
我枚举右端点,由于 被排序,所以这些区间合并出来的值 必然满足 ,具体证明在子任务 中有简单陈述。
我们将 划分为多个连续子段,满足任何一个连续子段再向左扩展一个元素就会导致其值超过 。我们将一个子段看成一个元素,则有 至多只有从左往右第一个元素可能小于 ,考虑反正,如果有两个,则我们显然可以向左合并一个子段,以得到更优的结果。而在这里 实际上等价于 ,用 合并和用 来合并实际上是一样的。
假设我们想让合并出来的结果为 ,则我们需要的元素个数为 ,倍增的向右移动,即相当于枚举不同的结果,倍增统计答案每次的复杂度显然是 。
如何维护连续子段?
每次在最后加入新的子段 ,显然 本身即满足如上所述的向左最大性。当我们由 向 更新时,每次将连续的两个子段合并,合并 次或者直到只剩下除 外的一个连续子段。由于每次子段减少一半,这显然也是 的复杂度。
时间复杂度
考虑优化 过程。称一个子段是极大的,当且仅当其向左或向右扩展都会使这个子段合并出来的值增大。
引理:设 表示大小为 的序列的极大连续子段的数量,则 。
证明:考虑原序列的笛卡尔树,设序列的其中一个最大元素的位置为 ,则有:
其中 为原序列包含位置 的最大子段数量,且 。
具体证明如下:
包含位置 且合并值为 的连续子段个数 ,限制 即来自所有具有固定值但具有不同左端点的区间在这里最多有 个,而限制 是因为值从 到 必然会经过扩展,而这个扩展会选择向左或是向右,这里我们需要从 开始扩展 次。
而 [包含位置 且合并值为 的连续子段个数]
则
之后,我们用并查集维护极长连续子段即可。
具体的,枚举值 ,找出答案为 的连续子段的个数。
用 维护第 轮 的区间左端点。考虑如果当前一段连续的 我们应该怎么维护。
事实上,我们发现,即使 是一个极长连续子段,但我们仍然不能直接将最开始的两个 通过并查集合并在一起,因为同样的道理,第二三两个 也能组成一个最长连续子段,这样做我们肯定会漏算。
所以只有从最后开始合并极长连续子段,再通过倍增记录前面每一个位置能够延伸至的最长连续子段的位置。比如,现将最后两个 合并,然后将第一个 的指针指向第二个 ,即第一个 和第二个 组成了一个最长连续子段,以此类推。那么,我们发现,当我们的倍增触碰到底部的时候,当前位置也同样可以并入最后的极长连续子段,而当当前段落合并完成后,我们才从 中将其删除。
但删除过后,我们再在这一段数的最后一个位置的末尾打上一个标记,当 时, 自成一个极长连续子段,我们将这个并查集提取出来,同样放进 ,重新合在一起更新。
总之,我们能够通过这样的操作,计算出答案为 的连续字段的数量。
时间复杂度
#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;
}
由 优化得到的方法,大致思路如下:
建立笛卡尔树,找到最大值,递归处理左右两个区间,然后再计算包含最大值的贡献。
后记
本文作者:╰⋛⋋⊱๑落叶๑⊰⋌⋚╯
本文链接:https://www.cnblogs.com/Defoliation-ldlh/p/16282513.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步