专栏文章
题解:P12646 [KOI 2024 Round 1] 升序 & P12642 [KOI 2024 Round 1] 加倍
P12646题解参与者 3已保存评论 2
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 2 条
- 当前快照
- 1 份
- 快照标识符
- @mio8iyam
- 此快照首次捕获于
- 2025/12/02 15:06 3 个月前
- 此快照最后确认于
- 2025/12/02 15:06 3 个月前
首先先来看一下这道题的弱化版:P12642 [KOI 2024 Round 1] 加倍。
考虑对于每个 预处理出一个 数组,表示最小的 使得:。
我们发现这个 是可以继承的,即对于某个(另一个),在预处理出最小的 使得 后, 应该等于 。这是因为之前 乘的影响也会作用于此。
所以我们要对 处理一个前缀和。
但还没完,我们会发现一个问题:对于原本就符合条件的 ,如果 足够大,大到 ,那么我们之前的修改前缀和相当于是“多了”。
所以我们还要考虑处理出一个大小为负的 用于抵消之前并不必要的影响。具体地,找到最大的 满足 ,则 。
然后考虑如何求答案,我们要先用前缀和“还原” 数组(因为之前说过 数组的影响是顺延的):。然后再对 数组求和:。
还有一个细节:可能会出现某个 ,这属于没有贡献的情况,不能胡乱计入。所以前缀和应该再和 取 :。
CPPvoid solve()
{
cin >> n;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
for(int i = 1; i <= n - 1; i ++){
if(a[i] > a[i + 1]){
int t = a[i + 1];
while(a[i] > t) t *= 2, c[i] ++;
}
else{
int x = 0, t = a[i];
while(t * 2 <= a[i + 1]){
t *= 2, x ++;
}
c[i] = -x;
}
s[i] = max(0LL, s[i - 1] + c[i]);
}
int ans = 0;
for(int i = 1; i <= n; i ++){
ans += s[i];
}
cout << ans << '\n';
}
下面是加强版(本题)。
还是接着弱化版思路中的 数组考虑。对于区间 ,我们最终的答案是这样的:
或
和 取 其实很烦人。这里先考虑如果没有这一层取 的话应该怎么算,可以交换求和符号并结合前缀和进行化简:
解释一下第一步:根据初始公式可知 且 ,把 挪到外面后内层 的求和范围就出来了。
显然只要预处理出 的前缀和与 的前缀和就能做到 求。
注:在计算时请搞清楚左右端点的包含关系,以及我们当前的计算公式的针对情况、当前算的 数组的下标范围等边界情况。在我的代码里会注明。
然后看正常情况。我们的瓶颈就在于和 取 实际上会导致求和中断一段(这一段里的前缀和都小于等于 ),然后再接着求。所以我们会很自然地想到能否把非零段都提取出来,然后用上面说的方法快速计算。
考虑预处理出对于所有的 右边第一个会求和中断的位置 ,这样就能从有值的位置跳到距离最近的断点(最后一个前缀和不为 的位置),就能做了。这其实是一个类似“跳表”的结构。
但时间复杂度呢?或者说, 对于每个询问的 最多能跳多少次?感觉像是 的,实际也的确如此。考虑什么时候会出现 ,一定是前一个数比后一个数大了至少两倍,这样才会出现 是负数用来抵消的情况。而 数组的值域是 ,也就是说我们最多只会跳 次。
时间复杂度 , 是 的值域。
CPP#include <bits/stdc++.h>
#define int long long // ll!!!
using namespace std;
const int maxn = 3e5 + 7;
int n, q, a[maxn], c[maxn], p[maxn], s[maxn], s2[maxn];
stack<int> st;
void solve()
{
cin >> n >> q;
for(int i = 1; i <= n; i ++){
cin >> a[i];
}
// 统计c数组
for(int i = 1; i <= n - 1; i ++){
if(a[i] > a[i + 1]){
int t = a[i + 1];
while(a[i] > t) t *= 2, c[i] ++;
}
else{
int x = 0, t = a[i];
while(t * 2 <= a[i + 1]){
t *= 2, x ++;
}
c[i] = -x;
}
}
// 预处理前缀和
for(int i = 1; i <= n; i ++){
s[i] = s[i - 1] + c[i];
s2[i] = s2[i - 1] + i * c[i];
}
// 预处理p[i]
for(int i = n; i >= 0; i --){
while(!st.empty() && s[st.top()] >= s[i]) st.pop(); // 找右边第一个小于s[i]的元素
p[i] = !st.empty()? st.top() : n + 1; // 如果没有,就赋值n+1
st.push(i);
}
auto sum = [&](int l, int r){ // 快速计算非零的区间前缀和
return (r + 1) * (s[r] - s[l - 1]) - (s2[r] - s2[l - 1]);
};
while(q --){
int l, r; cin >> l >> r;
if(l == r){
cout << 0 << '\n'; continue;
}
int ans = 0;
int i = l - 1; // 我们默认i是前缀和<0的位置,也就是i+1才是前缀和非0的位置。所以注意i从l-1开始。
while(i <= r - 1){ // 有效的范围应该是l ~ r-1,因为c[r]指的是位置r对r+1的操作次数,不计入在内
ans += sum(i + 1, min(p[i] - 1, r - 1));
i = p[i];
}
cout << ans << '\n';
}
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solve();
return 0;
}
相关推荐
评论
共 2 条评论,欢迎与作者交流。
正在加载评论...