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]\) 可在加数后组成等差数列的充要条件:

  1. 由等差数列通项公式 \(b_n = b_1+(n-1)d\), 则必定满足 \(a_i \equiv a_j \pmod d\ (i,j\in [l,r])\),即它们写成通项形式后常数项相等。
  2. 区间里没有重复的数。
  3. 它们之间的差值,都是 \(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\) 个数后合法的条件是:

\[(\max\limits_{i=l}^{r} a_i - \max\limits_{i=l}^{r} + 1) - (r - l + 1)\le k \]

即:

\[\max\limits_{i=l}^{r} a_i - \max\limits_{i=l}^{r} + l \le k + r \]


考虑枚举区间右端点 \(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
*/
posted @ 2020-08-25 14:53  Luckyblock  阅读(186)  评论(1编辑  收藏  举报