CF407E k-d-sequence
知识点:线段树,单调栈
原题面 Luogu
扯
30 min 写完代码,前后经历了静态查错,和标程对拍,拍 CF 的数据,花了将近 3h 才调出来= =
最后发现全是傻逼错误。
自己先把代码全都 rush 出来再集中调试的写法早就觉得有问题了了。
*必须要改变了*
*Luckyblock 上升到了新的高度*
最后,我们 CF 的机子真是太快了!
最后,dev 写 md 真是太好用了!
题意简述
给定长度为 \(n\) 的序列 \(a\),参数 \(k,d\)。
询问最长的子区间,使得该自区间加入至多 \(k\) 个任意的数后,排序后是一个公差为 \(d\) 的等差序列,输出区间左右端点。
\(1<n\le 2\times 10^5\),\(0\le k\le 2\times 10^5\), \(0\le d\le 10^9\),\(\left|a_i \right|\le 10^9\)。
分析题意
先特判掉 \(d=0\) 的情况,此时答案即最长的全部相等的子区间。
若子区间 \([l,r]\) 可在加数后组成等差数列的充要条件:
- 由等差数列通项公式 \(b_n = b_1+(n-1)d\), 则必定满足 \(a_i \equiv a_j \pmod d\ (i,j\in [l,r])\),即它们写成通项形式后常数项相等。
- 区间里没有重复的数。
- 它们之间的差值,都是 \(d\) 的倍数。
对于条件 1,考虑将序列分成若干段 \(\bmod d\) 相等的区间,分别进行处理。
以下讨论均在这样分割后的某区间内展开。
对于条件 2,维护每一位置 \(i\) 左侧第一个与之相同的数的位置 \(pre_i\)。
包含位置 \(i\) 的合法答案区间,左端点必 \(>pre_i\)。
对于条件 3,在一段 \(\bmod d\) 相等的区间内,考虑令 \(a_i = \left\lfloor\dfrac{a_i}{d}\right\rfloor = \left\lfloor\dfrac{(n-1)d+a_i \bmod d}{d}\right\rfloor=n-1\),去除首项 和 公差的影响。
此时加数后构成的等差数列,公差一定为 \(1\)。
对于一 \(\bmod d\) 相等的区间的某不合法子区间 \([l,r]\),考虑至少加几个数,使其成为一公差为 \(1\) 的等差数列。
显然,加最少的数,构成的等差数列一定为 \([\max\limits_{i=l}^{r} a_i, \max\limits_{i=l}^{r} a_i]\)。
最少需要的数即为:\((\max\limits_{i=l}^{r} a_i - \max\limits_{i=l}^{r} + 1) - (r - l + 1)\)。
则该子区间加 \(k\) 个数后合法的条件是:
即:
考虑枚举区间右端点 \(r\),固定了上述不等式右侧。
对于一确定 \(r\),设 \(w_l = \max\limits_{i=l}^{r} a_i - \max\limits_{i=l}^{r} + l\),
问题转化为找到最小的 \(l\),使\(\max\limits_{i=l}^{r} a_i - \max\limits_{i=l}^{r} + l \le k + r\)。
考虑用线段树维护 \(w_l\)。
查询是经典问题,即找到最小的位置 \(l\) 使 \(w_l\le k + r\)。
维护区间最小值,尽量向左子树递归,递归到叶返回位置,若区间最小值 \(> k + r\) 返回极大值。
考虑 \(r\) 的右移,对 \(w_l\) 的影响。
设 \(r\) 左侧第一个 \(>a_r\) 的位置为 \(j\),则 \(\max\limits_{i=j+1}^{r}a_i\) 会变成 \(a_r\)。
但不可直接区间加令 \(w_l+a_r(j<l<r)\)。对于 \(w_l(j<l<r)\),还需减去 \(\max\limits_{i=l}^{r-1} a_i\)。
而对于所有 \(\max\limits_{i=l}^{r-1} a_i\),它们并不相同。 怎么维护?
直接套线段树莽,复杂度不对。
发现要维护的东西具有单调性,考虑在 \(r\) 右移的过程中,对 \(a_r\) 维护一个单调递减的单调栈。
设插入 \(r\) 后,单调栈中一个元素为位置 \(p\),其前一个元素为位置 \(q\)。
可知,\([q+1,p]\) 中最大值为 \(a_p\)。
又栈中元素单调递减,区间 \([[q+1,p], r]\) 的最大值也为 \(a_p\)。
考虑插入 \(r\) 时单调栈的变化:
栈顶所有 \(<a_r\) 的位置 会弹栈,设当前栈顶为位置 \(p\),前一个元素为位置 \(q\)。
弹栈后,\([[q+1,p], r]\) 的最大值会变为 \(a_r\)。
此时对于 \(l\in [q+1, p]\),\(\max\limits_{i=l}^{r-1} a_i\) 即为 \(a_p\),可直接在线段树中减去其贡献。
同理,设 \(r\) 左侧第一个 \(<a_r\) 的位置为 \(j\),则 \(\min_{i=j+1}^{r}a_i\) 变为 \(a_r\)。
再对其维护一个单调递增的单调栈即可。
详见代码。
亿些细节
不要忘记 \(d=0\) 的特判。
答案的初始值应设为 \([1,1]\)。
保证区间内无重复元素,注意区间左端点对 \(\max\limits pre_r\) 取 \(max\)。
注意单调栈中加入极值元素。
若栈顶前一个元素为极值,则将操作的区间的左端点设为 第一步中分割成的区间左端点。
爆零小技巧
见下方注释。
哭哭
代码实现
//知识点:线段树,单调栈
/*
By:Luckyblock
*/
#include <algorithm>
#include <cstdio>
#include <ctype.h>
#include <cstring>
#include <map>
#define ll long long
#define ls (now_<<1)
#define rs (now_<<1|1)
const int kMaxn = 2e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
int maxlen = 1, ansl = 1, ansr = 1;
int n, k, d, data[kMaxn], a[kMaxn], pre[kMaxn];
int minnum[kMaxn << 2], tag[kMaxn << 2]; //写成 maxnum
int top_up, st_up[kMaxn], top_down, st_down[kMaxn];
std :: map <int, int> pre_equal;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = - 1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
void Pushup(int now_) {
minnum[now_] = std :: min(minnum[ls], minnum[rs]);
}
void Pushdown(int now_) {
if (! tag[now_]) return ;
minnum[ls] += tag[now_], minnum[rs] += tag[now_];
tag[ls] += tag[now_], tag[rs] += tag[now_];
tag[now_] = 0;
}
void Modify(int now_, int L_, int R_, int l_, int r_, int val_) {
if (l_ <= L_ && R_ <= r_) {
minnum[now_] += val_;
tag[now_] += val_;
return ;
}
Pushdown(now_);
int mid = (L_ + R_) >> 1;
if (l_ <= mid) Modify(ls, L_, mid, l_, r_, val_);
if (r_ > mid) Modify(rs, mid + 1, R_, l_, r_, val_);
Pushup(now_);
}
int Query(int now_, int L_, int R_, int ql_, int qr_, int k_) {
if (minnum[now_] > k_ || R_ < ql_ || L_ > qr_) return n + 1;
if (L_ == R_) return L_;
Pushdown(now_); //没写
int mid = (L_ + R_) >> 1, ret;
ret = Query(ls, L_, mid, ql_, qr_, k_); //没写赋值语句
if (ret == n + 1) ret = Query(rs, mid + 1, R_, ql_, qr_, k_); //少打_
return ret;
}
void StackInsert(int pos_, int minl_) {
int l, r;
for (; top_down && a[st_down[top_down]] <= a[pos_]; top_down --) { //没写指针增量
r = st_down[top_down], l = st_down[top_down - 1] + 1;
if (top_down == 1) l = minl_;
Modify(1, 1, n, l, r, - a[st_down[top_down]]);
}
st_down[++ top_down] = pos_;
r = pos_, l = top_down == 1 ? minl_ : st_down[top_down - 1] + 1;
Modify(1, 1, n, l, r, a[pos_]);
for (; top_up && a[st_up[top_up]] >= a[pos_]; top_up --) {
r = st_up[top_up], l = st_up[top_up - 1] + 1;
if (top_up == 1) l = minl_;
Modify(1, 1, n, l, r, a[st_up[top_up]]);
}
st_up[++ top_up] = pos_;
r = pos_, l = top_up == 1 ? minl_ : st_up[top_up - 1] + 1;
Modify(1, 1, n, l, r, - a[pos_]);
}
void Solve(int l_, int r_) {
top_down = top_up = 0;
st_down[0] = n + 1, st_up[0] = 0; //极值
for (int r = l_, l = l_; r <= r_; ++ r) { //l累计,不可每次新建
Modify(1, 1, n, r, r, r);
StackInsert(r, l_);
l = std :: max(l, pre[r] + 1);
int retl = Query(1, 1, n, l, r, k + r);
if (retl <= n && r - retl + 1 > maxlen) {
maxlen = r - retl + 1;
ansr = r, ansl = retl;
}
}
}
void Solve0() {
int secl = 1;
for (int i = 1; i <= n; ++ i) {
a[i] = read();
if (i > 1 && a[i] != a[i - 1]) {
if (i - 1 - secl + 1 > ansr - ansl + 1) {
ansl = secl, ansr = i - 1;
}
secl = i;
}
}
printf("%d %d\n", ansl, ansr);
}
//=============================================================
int main() {
n = read(), k = read(), d = read();
if (d == 0) {
Solve0();
return 0;
}
a[0] = - kInf, a[n + 1] = kInf;
int secl = 1;
for (int i = 1; i <= n; ++ i) {
data[i] = a[i] = read();
pre[i] = pre_equal[data[i]], pre_equal[data[i]] = i;
a[i] /= d;
if (i > 1 && (data[i] % d + d) % d != (data[i - 1] % d + d) % d) {
if (secl) Solve(secl, i - 1);
secl = i;
}
}
Solve(secl, n);
printf("%d %d\n", ansl, ansr);
return 0;
}
/*
10 5 0
1 1 1 1 4 4 4 1 1 1 2 2 2
*/