「笔记」可持久化线段树

写在前面

Q:什么时候需要可持久化?
A:想建一车线段树但是建不下的时候。

引入

给定一长度为 \(n\) 的数列,给定 \(m\) 次操作:

  1. 在某个历史版本上修改某一个位置上的值。
  2. 访问某个历史版本上的某一位置的值。

此外,每进行一次操作(对于操作2,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)
\(1\le n,m\le 10^6\)\(0\le |a_i|\le 10^9\)
1.5S,512MB。

介绍

对于上述问题,朴素的做法是维护 \(m\) 个版本指针。每一次修改后都另外开辟 \(O(n)\) 的空间,将整个数组复制一份,修改后将新版本的指针指向该数组。查询操作则将新版本指针指向被查询的版本。显然上述算法的时空复杂度都是 \(O(nm)\) 的。
但这样太蠢了。可以发现修改操作后新建的数组与原数组其它 \(n-1\) 个位置完全相同,对它们的复制与储存是做负功的。考虑找到一种维护手段,能够重复利用两数组相同的部分,来避免时空的浪费。

考虑把上述操作放到线段树上进行,同样维护 \(m\) 个版本指针,指向每个版本线段树的根。修改时把被操作版本大力复制一份,在新树上单点修改,版本指针更新。查询时直接在被查询版本上查询,并更新版本指针。
可以发现每次单点修改只会访问到自根向下的 \(O(\log n)\) 个节点,在新树中也仅有这 \(O(\log n)\) 个节点的信息与原版本不同。考虑仅新建这 \(O(\log n)\) 个节点,并重复利用原线段树中其它节点,如下图所示:

引用自:hyfhaha 的题解

具体地,从新的版本指针开始递归地新建节点,考虑每个节点的子节点维护的区间。对于不包含修改元素所在的区间,继承上个版本,直接将子节点指针指向上一个版本该位置的子节点即可。对于修改元素所在的区间,递归地新建节点,代码如下所示:

#define ls (lson[now_])
#define rs (rson[now_])
#define mid ((L_+R_)>>1)
void Modify(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
  now_ = ++ node_num; //每次都新建
  if (L_ == R_) { //到达根
    val[now_] = val_;
    return ;
  }
  ls = lson[pre_], rs = rson[pre_]; //继承儿子
  if (pos_ <= mid) Modify(ls, lson[pre_], L_, mid, pos_, val_); //修改
  else Modify(rs, rson[pre_], mid + 1, R_, pos_, val_);
}

代码

//知识点:可持久化线段树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e6 + 10;
//=============================================================
int n, m, a[kMaxn], root[kMaxn]; //root 为版本指针
//=============================================================
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 Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Seg {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid ((L_+R_)>>1)
  int node_num, lson[kMaxn << 5], rson[kMaxn << 5], val[kMaxn << 5];
  void Build(int &now_, int L_, int R_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      val[now_] = a[L_];
      return ;
    }
    Build(ls, L_, mid);
    Build(rs, mid + 1, R_);
  }
  void Modify(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      val[now_] = val_;
      return ;
    }
    ls = lson[pre_];
    rs = rson[pre_];
    if (pos_ <= mid) Modify(ls, lson[pre_], L_, mid, pos_, val_);
    else Modify(rs, rson[pre_], mid + 1, R_, pos_, val_);
  }
  int Query(int now_, int L_, int R_, int pos_) {
    if (L_ == R_) return val[now_];
    if (pos_ <= mid) return Query(ls, L_, mid, pos_);
    return Query(rs, mid + 1, R_, pos_);
  }
  #undef ls
  #undef rs
  #undef mid
}
//=============================================================
int main() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) a[i] = read();
  Seg::Build(root[0], 1, n);
  for (int i = 1; i <= m; ++ i) {
    int pre = read(), opt = read(), pos = read();
    if (opt == 1) {
      int val = read();
      Seg::Modify(root[i], root[pre], 1, n, pos, val); //新建版本
    } else {
      printf("%d\n", Seg::Query(root[pre], 1, n, pos));
      root[i] = root[pre]; //继承 root
    }
  }
  return 0;
}

区间修改

原题面:SP11470 TTM - To the moon

通过标记永久化实现的区间修改可持久化线段树。

给定一长度为 \(n\) 的数列,给定 \(m\) 次操作。有一初始为 0 的时间戳。

  • 区间加 \(d\),时间戳 + 1。
  • 查询当前时间戳区间和。
  • 查询给定时间戳区间和。
  • 将当前时间戳置为给定时间戳。

所有操作均合法。
\(1\le n,m\le 10^5\)\(0|a_i|\le 10^9\)\(|d|\le 10^4\)
829ms,1.46G。

区间修改区间查询的可持久化线段树。

考虑单点修改的可持久化线段树的原理:重用未被修改部分,仅新建被访问到的有变化的 \(\log n\) 个节点,并将新的版本指针指向新的根。
区间修改同理,一次区间修改至多会访问 \(2\log n\) 个节点,同样只新建这些节点,并更新版本指针即可。需要特别注意的是必须要标记永久化,否则下放标记时需要新建节点以保证版本之间互不影响,造成时空浪费。

总复杂度仍为 \(O(n\log n)\) 级别。

//知识点:可持久化线段树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, m, now, a[kN];
//=============================================================
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 Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Hjt {
  #define ls lson[now_]
  #define rs rson[now_]
  #define mid ((L_+R_)>>1)
  const int kMaxRoot = kN << 1;
  const int kMaxNode = kN << 5;
  int node_num, root[kMaxRoot], lson[kMaxNode], rson[kMaxNode];
  LL tag[kMaxNode], sum[kMaxNode];
  void Build(int &now_, int L_, int R_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      sum[now_] = a[L_];
      return ;
    }
    Build(ls, L_, mid), Build(rs, mid + 1, R_);
    sum[now_] = sum[ls] + sum[rs];
  }
  void Modify(int &now_, int pre_, int L_, int R_, int l_, int r_, int val_) {
    now_ = ++ node_num;
    lson[now_] = lson[pre_], rson[now_] = rson[pre_];
    tag[now_] = tag[pre_], sum[now_] = sum[pre_];
    sum[now_] += 1ll * (r_ - l_ + 1) * val_;
    if (l_ == L_ && R_ == r_) {
      tag[now_] +=  val_;
      return ;
    }
    if (r_ <= mid) Modify(ls, lson[pre_], L_, mid, l_, r_, val_);
    else if (l_ > mid) Modify(rs, rson[pre_], mid + 1, R_, l_, r_, val_);
    else {
      Modify(ls, lson[pre_], L_, mid, l_, mid, val_); 
      Modify(rs, rson[pre_], mid + 1, R_, mid + 1, r_, val_);
    }
  }
  LL Query(int now_, int L_, int R_, int l_, int r_, LL tag_) {
    if (l_ == L_ && R_ == r_) return sum[now_] + 1ll * (r_ - l_ + 1) * tag_;
    if (r_ <= mid) return Query(ls, L_, mid, l_, r_, tag_ + tag[now_]);
    else if (l_ > mid) return Query(rs, mid + 1, R_, l_, r_, tag_ + tag[now_]);
    else return Query(ls, L_, mid, l_, mid, tag_ + tag[now_]) + 
                Query(rs, mid + 1, R_, mid + 1, r_, tag_ + tag[now_]);
  }
}
//=============================================================
#define root Hjt::root
int main() { 
  n = read(), m = read(), now = 0;
  for (int i = 1; i <= n; ++ i) a[i] = read();
  Hjt::Build(root[0], 1, n);
  for (int i = 1; i <= m; ++ i) {
    char opt[5]; scanf("%s", opt);
    if (opt[0] == 'C') {
      ++ now;
      int l = read(), r = read(), val = read();
      Hjt::Modify(root[now], root[now - 1], 1, n, l, r, val);
    } else if (opt[0] == 'Q') {
      int l = read(), r = read();
      printf("%lld\n", Hjt::Query(root[now], 1, n, l, r, 0));
    } else if (opt[0] == 'H') {
      int l = read(), r = read(), t = read();
      printf("%lld\n", Hjt::Query(root[t], 1, n, l, r, 0));
    } else if (opt[0] == 'B') {
      now = read();
    }
  }
  return 0; 
}

主席树

P3834 【模板】可持久化线段树 2(主席树)

主席树,即可持久化权值线段树,维护“线段树的前缀和”。

给定一长度为 \(n\) 的数列 \(a\),给定 \(m\) 次询问。
每次询问给定参数 \(l,r,k\),查询数列 \(a\) 在闭区间 \([l,r]\) 内的第 \(k\) 小值。
\(1\le n,m\le 2\times 10^5\)\(|a_i|\le 10^9\)\(1\le l\le r\le n\)\(1\le k\le r-l+1\)
1S ~ 1.2S,256MB。

先离散化,设数列最大值为 \(A\)

考虑仅有一次询问的情况,yy 一下可得到这样一种解法:
对于区间 \([l,r]\),考虑排名 \(<k\) 的数的数量,考虑每次二分答案的值域 \([L, R]\),并检查区间 \([l,r]\) 中小于等于 \(\frac{L+R}{2}\) 的数,即左侧的数的个数 \(size\)。若 \(size<k\),则答案一定在值域 \([L, mid]\) 中,下调右端点。否则答案在 \([mid + 1, R]\) 中,且一定为其中排名第 \(k-size\) 的数,上调左端点。区间长度收缩至 \(1\) 时即得答案。
暴力实现的单次复杂度是 \(O(n\log A)\) 的。但这给出了一种思路:二分值域并检查数量。

看到二分值域检查数量,想到了权值线段树。假设已知区间 \([l,r]\) 建出的权值线段树,上面的二分完全可以在线段树上进行。
对于某节点维护的区间 \([L,R]\),其左右儿子分别维护的区间为 \(\left[L,\frac{L+R}{2}\right]\)\(\left[\frac{L+R}{2} + 1, R\right]\)。每次检查左儿子的 \(size\le k\) 是否成立。成立则答案必在左儿子,向左儿子递归。否则答案一定在右儿子,且一定是右儿子中排名 \(k-size\) 的数。令 \(k- size\),并向右儿子递归。递归至叶节点即得答案。
上述过程复杂度是 \(O(\log A)\) 的,非常喜人。

想办法怎么得到权值线段树。
考虑权值线段树构造的过程,在 \([l,r]\) 的权值线段树上插入 \(a_{r+1}\),即得 \([l,r+1]\) 的权值线段树。这玩意的形式和前缀和有些相似,不妨强行凑一凑。
考虑 \([1,l-1]\)\([1,r]\) 的权值线段树,将它们对应位置节点维护信息相减。上述过程相当于删除 \([1,l-1]\) 的信息,发现这可以得到 \([l,r]\) 的权值线段树。于是可以考虑将 \(1\sim n\) 的前缀权值线段树全建出来,树上二分时 再将对应位置节点维护信息相减。单次查询的复杂度可以保证,但建树的时间和空间消耗较大。

发现 \([1,l]\)\([1,l+1]\) 的权值线段树上,仅有 \(\log m\) 个节点不同。考虑可持久化线段树的套路,动态开点,并利用之前的线段树的信息。可以获得这样的结构:

引用自:主席树 - 孤独·粲泽 的博客

这就是主席树,即可持久化权值线段树,用来维护“线段树的前缀和”,前缀线段树也满足可减性,从而得到任意区间构成的权值线段树。算法总空间复杂度为 \(O(n\log A)\),时间复杂度 \(O(m\log A)\)

//知识点:主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 2e5 + 10;
//=============================================================
int n, m, d_num, a[kMaxn], data[kMaxn], map[kMaxn];
int root[kMaxn];
//=============================================================
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 Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Prepare() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++i) {
    a[i] = data[i] = read();
  }
  std::sort(data + 1, data + n + 1);
  data[0] = -0x3f3f3f3f;
  for (int i = 1; i <= n; ++i) {
    if (data[i] != data[i - 1]) d_num++;
    data[d_num] = data[i];
  }
  for (int i = 1; i <= n; ++i) {
    int ori = a[i];
    a[i] = std ::lower_bound(data + 1, data + d_num + 1, ori) - data;
    map[a[i]] = ori;
  }
}
namespace Hjt {
#define ls lson[now_]
#define rs rson[now_]
#define mid ((L_ + R_) >> 1)
int node_num, lson[kMaxn << 5], rson[kMaxn << 5], size[kMaxn << 5];
void Insert(int &now_, int pre_, int L_, int R_, int val_) {
  now_ = ++node_num;
  size[now_] = size[pre_] + 1;
  ls = lson[pre_], rs = rson[pre_];
  if (L_ == R_) return;
  if (val_ <= mid)
    Insert(ls, lson[pre_], L_, mid, val_);
  else
    Insert(rs, rson[pre_], mid + 1, R_, val_);
}
int Query(int r_, int l_, int L_, int R_, int k_) {
  if (L_ == R_) return L_;
  int sz = size[lson[r_]] - size[lson[l_]];
  if (k_ <= sz)
    return Query(lson[r_], lson[l_], L_, mid, k_);
  else
    return Query(rson[r_], rson[l_], mid + 1, R_, k_ - sz);
}
#undef ls
#undef rs
#undef mid
}  // namespace Hjt
//=============================================================
int main() {
  Prepare();
  for (int i = 1; i <= n; ++i) {
    Hjt::Insert(root[i], root[i - 1], 1, d_num, a[i]);
  }
  for (int i = 1; i <= m; ++i) {
    int l = read(), r = read(), k = read();
    printf("%d\n", map[Hjt::Query(root[r], root[l - 1], 1, d_num, k)]);
  }
  return 0;
}

带修主席树

P2617 Dynamic Rankings

主席树的本质是线段树的前缀和。前缀和的查询复杂度为 \(O(1)\) 级别,暴力单点修改复杂度 \(O(n)\) 级别。
考虑用树状数组平衡两者的复杂度。

给定一长度为 \(n\) 的数列 \(a\),给定 \(m\) 次操作。
操作有两种:

  1. 给定参数 \(x, y\),将 \(a_x\) 改为 \(y\)
  2. 给定参数 \(l,r,k\),查询数列 \(a\) 在闭区间 \([l,r]\) 内的第 \(k\) 小值。

\(1\le n,m\le 10^5\)\(|a_i|\le 10^9\)\(1\le l\le r\le n\)\(1\le k\le r-l+1\)
3S,512MB。

先离散化,设数的最大值为 \(m\)
主席树的本质是权值线段树的前缀和。如果暴力修改,需要每次修改所有的位置,查询复杂度不变,修改复杂度为 \(O(n\log A)\)

如何平衡前缀和的修改操作?考虑树状数组的思路,用二进制优化前缀和。
令第 \(k\) 棵权值线段树维护原来的 \([k-lowbit(k) + 1, k]\) 中的权值线段树。修改,查询时都查询 \(\log n\) 个权值线段树,每次查询修改复杂度为 \(O(\log^2 A)\)。空间复杂度不变,仍为 \(O(n\log A)\)

一份很久之前写的很丑的代码:

//知识点:主席树,树状数组
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kN = 1e6 + 10; 
//=============================================================
struct Opt {
  bool type;
  int l, r, k, pos, val;
} q[kN];
int n, m, d_num, data[kN], num[kN];
std::vector <int> tmp1, tmp2;
//=============================================================
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 Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace Hjt {
  #define ls lson[now_]
  #define rs rson[now_]
  #define mid ((L_+R_)>>1)
  int node_num, root[kN], sz[kN << 5], lson[kN << 5], rson[kN << 5];
  void Modify(int &now_, int L_, int R_, int pos_, int val_) {
    if (!now_) now_ = ++ node_num;
    sz[now_] += val_;
    if (L_ == R_) return ;
    if (pos_ <= mid) Modify(ls, L_, mid, pos_, val_);
    else Modify(rs, mid + 1, R_, pos_, val_);
  }
  int Query(int L_, int R_, int k_) {
    if (L_ == R_) return L_;
    int sizel = 0;
    for (int i = 0; i < tmp1.size(); ++ i) sizel -= sz[lson[tmp1[i]]];
    for (int i = 0; i < tmp2.size(); ++ i) sizel += sz[lson[tmp2[i]]];
    
    if (k_ <= sizel) {
      for (int i = 0; i < tmp1.size(); ++ i) tmp1[i] = lson[tmp1[i]];
      for (int i = 0; i < tmp2.size(); ++ i) tmp2[i] = lson[tmp2[i]];
      return Query(L_, mid, k_);
    }
    for (int i = 0; i < tmp1.size(); ++ i) tmp1[i] = rson[tmp1[i]];
    for (int i = 0; i < tmp2.size(); ++ i) tmp2[i] = rson[tmp2[i]];
    return Query(mid + 1, R_, k_ - sizel);
  }
}
namespace Bit {
  #define low(x) (x&-x)
  void Add(int pos_, int val_) {
    int p = std::lower_bound(data + 1, data + d_num + 1, num[pos_]) - data;
    for (int i = pos_; i <= n; i += low(i)) {
      Hjt::Modify(Hjt::root[i], 1, d_num, p, val_);
    }
  }
  int Query(int l_, int r_, int k_) {
    tmp1.clear(), tmp2.clear();
    for (int i = l_ - 1; i; i -= low(i)) tmp1.push_back(Hjt::root[i]);
    for (int i = r_; i; i -= low(i)) tmp2.push_back(Hjt::root[i]);
    return Hjt::Query(1, d_num, k_);
  }
}
void Init() {
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) data[++ d_num] = num[i] = read();
  for (int i = 1; i <= m; ++ i) {
    char opt[5]; scanf("%s", opt + 1);
    q[i].type = (opt[1] == 'Q');
    if (q[i].type) {
      q[i].l = read(), q[i].r = read(), q[i].k = read();
    } else {
      q[i].pos = read(), data[++ d_num] = q[i].val = read();
    }
  }
  std::sort(data + 1, data + d_num + 1);
  d_num = std::unique(data + 1, data + d_num + 1) - data - 1;
  for (int i = 1; i <= n; ++ i) Bit::Add(i, 1);
}
//=============================================================
int main() {
  Init();
  for (int i = 1; i <= m; ++ i) {
    if (q[i].type) {
      int l = q[i].l, r = q[i].r, k = q[i].k;
      printf("%d\n", data[Bit::Query(l, r, k)]);
    } else {
      int pos = q[i].pos, val = q[i].val;
      Bit::Add(pos, -1);
      num[pos] = val;
      Bit::Add(pos, 1);
    }
  }
  return 0;
}

在线静态二维数点

静态二维数点问题的模型:
给定平面上的 \(n\) 个点的坐标,第 \(i\) 个点的坐标为 \((xi,yi)\)\(m\) 组询问,每次给定一个子矩阵,求子矩阵内点的数量。

可以考虑使用主席树解决,先将所有节点按照 \(x_i\) 排序,并按照排序后顺序依次插入到主席树中的位置 \(y_i\) 并建立新版本。
查询以 \((a,b)\) 为左上角,\((c,d)\) 为右下角的子矩阵时,先二分找到 \(x_i\) 大于等于 \(a\),小于等于 \(c\) 的第一个点 \(l\) 和最后一个点 \(r\),取出 \([l,r]\) 的权值线段树,查询线段树区间 \([c,d]\) 中点的数量即可。

正确性显然,总复杂度 \(O(n\log n + m\log n)\) 级别。

例题

「CTSC2018」混合果汁

二分答案,主席树

给定 \(n\) 个物品,物品 \(i\) 的数量为 \(l_i\),单个花费为 \(p_i\),价值为 \(d_i\)
对于一个物品的选择方案,定义其总花费为所有物品的花费之和,其价值为方案中物品价值的最小值。
给定 \(m\) 个询问,每次询问给定参数 \(g,L\),求一个物品的选择方案,使得方案中物品数 \(\ge L\),花费 \(\le g\),且价值最大,输出最大的价值。
\(1\le n,m\le 10^5\)\(1\le d_i, p_i, l_i\le 10^5\)\(1\le g, L\le 10^{18}\)
2S,512MB。

显然对于每个询问,答案满足单调性,考虑二分 选择方案中价值最小的物品 \(mid\)。 问题变为判定仅使用价值 \(\ge d_{mid}\) 的物品,总花费 \(\le g\) 时,能否选择 \(\ge L\) 个物品。

先考虑如何暴力 Check。二分答案之后,所有 可选物品 贡献均变为 1。 为满足花费限制,贪心的想,肯定先选花费小的。
则可将可选物品按花费升序排序,从小到大选择物品,直至不能再选,判断选择的数量是否 \(\ge L\) 即可。
单次 Check 复杂度 \(O(n\log n + n)\),总复杂度 \(O(mn\log^2 n)\),期望得分 \(45\text{pts}\)


发现每次 Check 的过程中,我们仅关心某花费的物品的数量。考虑权值线段树维护对应权值区间内 物品的个数,与全部选择它们时的总花费。
每次 Check 时先将所有可选物品插入线段树中,再线段树上二分判断是否存在合法的方案。特别的,二分到叶节点后,注意特判叶节点选择的数量,因为叶节点对应的物品花费最高,可能不能全部选择。

单次 Check 复杂度变为 \(O(n\log n + \log n)\),总复杂度仍为 \(O(mn\log^2 n)\)

发现上述过程的瓶颈在于,每次 Check\(mid\) 不同导致可选物品都不同。必须每次重新构建 价值 \(\ge d_{mid}\) 的物品组成的权值线段树。
考虑可持久化,先将物品按 \(d_i\) 排序后 插入主席树中,Check 时直接取出对应部分即可,避免了重建线段树。

单次 Check 复杂度变为 \(O(\log^2 n)\),总复杂度 \(O(m\log^2 n)\),期望得分 \(100\text{pts}\)

由于每次询问的 \(g,L\le 10^{18}\),保证了不会乘爆,注意开 long long

//知识点:二分答案,主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 1e5 + 10;
//=============================================================
struct Juice {
  int d, p, l;
} a[kMaxn];
int n, m, maxp, ans, d[kMaxn], root[kMaxn];
ll g, L;
//=============================================================
inline ll read() {
  ll f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3ll) + (w << 1ll) + (ch ^ '0');
  return f * w;
}
bool CompareJuice(Juice fir, Juice sec) {
  return fir.d < sec.d;
}
void Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Hjt {
  #define ls lson[now_]
  #define rs rson[now_]
  #define mid ((L_+R_)>>1)
  int node_num, lson[kMaxn << 5], rson[kMaxn << 5];
  ll cnt[kMaxn << 5], val[kMaxn << 5];
  void Insert(int &now_, int pre_, int L_, int R_, int pos_, int cnt_) {
    now_ = ++ node_num;
    lson[now_] = lson[pre_];
    rson[now_] = rson[pre_];
    cnt[now_] = cnt[pre_] + 1ll * cnt_;
    val[now_] = val[pre_] + 1ll * pos_ * cnt_;
    if (L_ == R_) return ;
    if (pos_ <= mid) Insert(ls, lson[pre_], L_, mid, pos_, cnt_);
    else Insert(rs, rson[pre_], mid + 1, R_, pos_, cnt_);
  }
  ll Query(int lnow_, int rnow_, int L_, int R_, ll k_) {
    if (L_ == R_) {
      return std :: min((1ll * k_ / L_), cnt[rnow_] - cnt[lnow_]);
    }
    ll vall = val[lson[rnow_]] - val[lson[lnow_]];
    ll cntl = cnt[lson[rnow_]] - cnt[lson[lnow_]];
    if (vall < k_) return Query(rson[lnow_], rson[rnow_], mid + 1, R_, k_ - vall) + cntl;
    return Query(lson[lnow_], lson[rnow_], L_, mid, k_);
  }
  #undef mid
}
bool Check(int pos_) {
  return Hjt :: Query(root[pos_ - 1], root[n], 1, maxp, g) >= L;
}
//=============================================================
int main() {
  n = (int) read(), m = (int) read();
  for (int i = 1; i <= n; ++ i) {
    a[i] = (Juice) {(int) read(), (int) read(), (int) read()};
    Chkmax(maxp, a[i].p);
    d[i] = a[i].d;
  }
  std :: sort(a + 1, a + n + 1, CompareJuice);
  for (int i = 1; i <= n; ++ i) {
    Hjt :: Insert(root[i], root[i - 1], 1, maxp, a[i].p, a[i].l);
  }
  while (m --) {
    g = read(), L = read(), ans = - 1;
    for (int l = 1, r = n; l <= r; ) {
      int mid = (l + r) >> 1;
      if (Check(mid)) {
        ans = a[mid].d;
        l = mid + 1;
      } else {
        r = mid - 1;
      }
    }
    printf("%d\n", ans);
  }
  return 0;
}

CF1436E Complicated Computations

主席树维护区间 \(\operatorname{mex}\)

给定一长度为 \(n\) 的数列 \(a\),求其所有连续子序列的 \(\operatorname{mex}\)\(\operatorname{mex}\)
\(\operatorname{mex}(S)\) 定义为数集 \(S\) 中最小的没有出现的 正整数
\(1\le a_i\le n\le 10^5\)
1S,256MB。

设答案为 \(ans\),显然答案符合下列性质:

  • 没有连续子序列的 \(\operatorname{mex}\)\(ans\)
  • \(1\sim ans-1\) 都能够作为某连续子序列的 \(\operatorname{mex}\) 出现。

先考虑如何判断性质 1。
若某连续的子序列的 \(\operatorname{mex}\)\(ans\),显然该连续子序列中不包括数 \(ans\),且连续子序列越长,越有可能满足条件。
考虑枚举所有不含 \(ans\) 的极长连续子序列进行判断,发现这些连续子序列即为整个数列 被所有 \(ans\) 分割成的子段。查询这些子段的 \(\operatorname{mex}\),判断是否等于 \(ans\) 即可。

再考虑性质 2,发现只需要从小到大枚举答案,重复对性质 1 的判断即可。
枚举到的第一个不满足性质 1 的即为答案。

考虑实现。
对于一个在数列中出现了 \(k\) 次的数,它可将数列分为 \(k+1\) 段,则总查询 \(\operatorname{mex}\) 次数为 \(O(n)\) 级别。
使用主席树,总时间复杂度为 \(O(n\log n)\) 级别。
使用莫队 + 值域分块,总时间复杂度 \(O(n\sqrt m + m\sqrt n)\)

怎么用主席树实现啊/jk
\(i\) 棵主席树的叶节点 \(x\) 储存权值 \(x\)\(a_{1}\sim a_{i}\) 中最晚出现的位置。 插入时只有单点修改,因此可以可持久化。
查询时查询第 \(r\) 棵主席树中最小的,最晚出现位置 \(<l\) 的权值,即为区间 \([l,r]\)\(\operatorname{mex}\)。线段树维护区间最小值,主席树上二分即可。

//知识点:结论,主席树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, a[kN], root[kN];
std::vector <int> pos[kN];
//=============================================================
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 Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Hjt {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid (L_+R_>>1)
  int node_num, lson[kN << 5], rson[kN << 5], t[kN << 5];
  void Pushup(int now_) {
    t[now_] = std::min(t[ls], t[rs]);
  }
  void Insert(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      t[now_] = val_;
      return ;
    }
    ls = lson[pre_], rs = rson[pre_];
    t[now_] = t[pre_];
    if (pos_ <= mid) Insert(ls, lson[pre_], L_, mid, pos_, val_);
    else Insert(rs, rson[pre_], mid + 1, R_, pos_, val_);
    Pushup(now_);
  }
  int Query(int now_, int L_, int R_, int pos_) {
    if (L_ == R_) return L_;
    if (t[ls] < pos_) return Query(ls, L_, mid, pos_);
    return Query(rs, mid + 1, R_, pos_);
  }
  #undef ls
  #undef rs
  #undef mid
}
//=============================================================
int main() {
  n = read();
  for (int i = 1; i <= n; ++ i) {
    a[i] = read();
    pos[a[i]].push_back(i);
    Hjt::Insert(root[i], root[i - 1], 1, n + 1, a[i], i);
  }
  for (int i = 1; i <= n + 1; ++ i) pos[i].push_back(n + 1);
  for (int i = 1; i <= n + 2; ++ i) {
    int lim = pos[i].size(), flag = 0;
    for (int j = 0, las = 1; j < lim; ++ j) {
      int now = pos[i][j];
      if (las < now) {
        flag = (Hjt::Query(root[now - 1], 1, n + 1, las) == i);
        if (flag) break;
      }
      las = now + 1;
    }
    if (! flag) {
      printf("%d\n", i);
      break;
    }
  }
  return 0;
}

CF1422F Boring Queries

给定一长度为 \(n\) 的序列 \(a\),有 \(q\) 次询问。
每次询问给定区间 \([l,r]\),求 \(\operatorname{lcm}(a_l, \cdots,a_{r}) \bmod 10^9 + 7\)
强制在线。
\(1\le n,q\le 10^5\)\(1\le a_i\le 2\times 10^5\)
3S,512MB。

算法一

首先对 \(a\) 质因数分解,设 \(a_i\) 分解后为 \(\prod\limits_j p_j^{c_{i,j}}\)

根据小学奥数,有:

\[\large \operatorname{lcm}(a_l, \cdots,a_{r}) = \prod\limits_j p_j^{\max\limits_{l\le i\le r}(c_{i,j})} \]

如果不强制在线,用莫队维护下各质因子出现次数即可。
但恶心出题人强制在线,期望得分 0pts。


算法二

发现算法一中有区间取 \(\max\) 的操作,考虑对每个质因子都建立一棵线段树,维护区间该质因子次数的最大值。查询时枚举质因子,累乘贡献即可。
时空复杂度 \(O(kn\log n)\)\(k\) 为质因子个数,可知 \(k \approx \frac{n}{\ln n}\),时间空间均无法承受,期望得分 0pts。


算法三

发现数的值域比较小,又出现了质因子这样的字眼,考虑套路根号分治。
先将不大于 \(\sqrt{2\times 10^5}\) 的质因子筛出,发现仅有 92 个,于是仅建立 92 棵线段树,套用算法二,维护区间该质因子次数最大值。

再考虑大于 \(\sqrt{2\times 10^5}\) 的质因子,它们在每个 \(a_i\) 中的次数最多为 1,取 \(\max\) 后次数也只能为 1。
则每种质因子只能贡献一次,查询区间内它们的贡献,即为查询区间内 不同权值 的乘积。
这是个经典问题,可以看这里统计区间里有多少个不同的数__莫队__主席树__树状数组_dctika1584的博客。不强制在线可以用线段树 + 扫描线,通过单点修改维护。强制在线,套路地可持久化即可。

总时空间复杂度均为 \(O(kn\log n + n\log^2 n)\)\(k=92\),能过。

对于不大于 \(\sqrt{2\times 10^5}\) 的质因子,质因子的幂 \(\le 2\times 10^5\),因此代码中直接维护了该质因子的幂的最大值,而不是上述的次数。

//知识点:线段树,主席树 
/*
By:Luckyblock
注意在维护较小质因子的线段树中不能取模
合并的时候写的乘,并非取最大值。
我是,傻逼,哈哈。
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 1e5 + 10;
const int kMax = 2e5;
const int mod = 1e9 + 7;
//=============================================================
int p_num, p[kMax + 10];
bool vis[kMax + 10];
int n, m, ans, a[kN], last[kN], lastv[kMax];
int root[kN];
//=============================================================
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 Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
void Euler() {
  for (int i = 2; i <= kMax; ++ i) {
    if (! vis[i]) p[++ p_num] = i;
    for (int j = 1; j <= p_num && i * p[j] <= kMax; ++ j) {
      vis[i * p[j]] = true;
      if (i % p[j] == 0) break;
    }
  }
}
int QPow(int x_, int y_) {
  x_ %= mod;
  int ret = 1;
  for (; y_; y_ >>= 1) {
    if (y_ & 1) ret = 1ll * ret * x_ % mod;
    x_ = 1ll * x_ * x_ % mod;
  }
  return ret;
}
int Inv(int x_) {
  return QPow(x_, mod - 2);
}
namespace Hjt {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid ((L_+R_)>>1)
  int node_num, lson[kN << 6], rson[kN << 6], t[kN << 6];
  void Init() {
    t[0] = 1;
  }
  void Insert(int &now_, int pre_, int L_, int R_, int pos_, int val_) {
    now_ = ++ node_num;
    t[now_] = 1ll * t[pre_] * val_ % mod;
    if (L_ == R_) return ;
    ls = lson[pre_], rs = rson[pre_];
    if (pos_ <= mid) Insert(ls, lson[pre_], L_, mid, pos_, val_);
    else Insert(rs, rson[pre_], mid + 1, R_, pos_, val_);
  }
  int Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ && R_ <= r_) return t[now_];
    int ret = 1;
    if (l_ <= mid) ret = 1ll * ret * Query(ls, L_, mid, l_, r_) % mod;
    if (r_ > mid) ret = 1ll * ret * Query(rs, mid + 1, R_, l_, r_) % mod;
    return ret;
  }
  #undef ls
  #undef rs
  #undef mid
}
#define ls (now_<<1)
#define rs (now_<<1|1)
#define mid ((L_+R_)>>1)
struct Seg {
  int t[kN << 2];
  void Build(int now_, int L_, int R_) {
    t[now_] = 1;
    if (L_ == R_) return ;
    Build(ls, L_, mid);
    Build(rs, mid +1, R_);
  }
  void Pushup(int now_) {
    t[now_] = std::max(t[ls], t[rs]);
  }
  void Insert(int now_, int L_, int R_, int pos_, int val_) {
    if (L_ == R_) {
      t[now_] = val_;
      return ;
    }
    if (pos_ <= mid) Insert(ls, L_, mid, pos_, val_);
    else Insert(rs, mid + 1, R_, pos_, val_);
    Pushup(now_);
  }
  int Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ <= L_ && R_ <= r_) return t[now_]; 
    int ret = 1;
    if (l_ <= mid) Chkmax(ret, Query(ls, L_, mid, l_, r_));
    if (r_ > mid) Chkmax(ret, Query(rs, mid + 1, R_, l_, r_));
    return ret;
  }
} t[93];
#undef ls
#undef rs
#undef mid
void Insert(int pos_) {
  int val = a[pos_] = read();
  for (int i = 1; p[i] * p[i] <= kMax; ++ i) {
    int d = p[i], delta = 1;
    if (val == 1) break;
    if (val % d) continue ;
    while (val % d == 0) {
      val /= d;
      delta *= d;
    }
    t[i].Insert(1, 1, n, pos_, delta);
  }
  root[pos_] = root[pos_ - 1];
  Hjt::Insert(root[pos_], root[pos_], 1, n, pos_, val);
  if (lastv[val]) Hjt::Insert(root[pos_], root[pos_], 1, n, lastv[val], Inv(val));
  lastv[val] = pos_;
}
int Query(int l_, int r_) {
  int ret = 1;
  for (int i = 1; p[i] * p[i] <= kMax; ++ i) {
    ret = 1ll * ret * std::max(1, t[i].Query(1, 1, n, l_, r_)) % mod;
  }
  ret = 1ll * ret * Hjt::Query(root[r_], 1, n, l_, r_) % mod;
  return ret;
}
void Prepare() {
  Euler();
  Hjt::Init();
  n = read();
  for (int i = 1; i <= n; ++ i) Insert(i);
}
//=============================================================
int main() {
  Prepare();
  m = read();
  while (m --) {
    int l = (read() + ans) % n + 1, r = (read() + ans) % n + 1;
    if (l > r) std::swap(l, r);
    printf("%d\n", ans = Query(l, r));
  }
  return 0;
}

P2839 [国家集训队]middle

二分答案,可持久化对象的多样性。

对于一长度为 \(n\) 的数列 \(a\),定义其「中位数」为排序后数列的第 \(\frac{n}{2}\) 个数,下标从 0 开始
给定一长度为 \(n\) 的数列 \(a\)\(q\) 次询问,每次询问给定参数 \(a,b,c,d\),保证 \(a<b<c<d\)。求区间左端点在 \([a,b]\) 之间,右端点在 \([c,d]\) 之间的子区间中,最大的「中位数」,「中位数」的定义见上文。
强制在线
\(1\le n\le 2\times 10^4\)\(1\le q\le 2.5\times 10^4\)
2S,512MB。

国际惯例先离散化,以下均在离散化的前提下展开。
对于一个询问 \((a,b,c,d)\),可拆成三部分考虑:\([b,c]\) 内必选的部分和 \([a,b)\) 的一段后缀,\((c,d]\) 的一段前缀。仅需考虑什么样的前后缀 对答案有贡献即可。

假设已选出一段区间 \([l,r]\),有 \(a<l\le b\)\(c\le r<d\),设其中位数为 \(mid\)。显然,此时从左右两侧向区间内添加不小于 \(mid\) 的数可能会使中位数增大,添加小于 \(mid\) 的数可能会使中位数减小。
手玩一下可发现,当且仅当 添加的不小于 \(mid\) 的数的个数 大于等于 添加的小于 \(mid\) 的个数时,中位数才可能增大。不小于 \(mid\) 的数的个数 比小于 \(mid\) 的数的个数越多,贡献就越大。

下文将一个区间中不小于 \(mid\) 的个数与小于 \(mid\) 的个数 的差值称为该区间的贡献。


之后怎么做?动态维护中位数,找到左右两侧对 当前中位数 贡献最大的前后缀加入?发现不好搞,因为中位数变化后,贡献也在变化。

考虑枚举固定一个中位数的 下界,从而固定前后缀的贡献。以此下界为据,加入左右两侧贡献最大的前后缀。判断最后的中位数是否满足大于该下界。
若满足,说明答案 \(\ge\) 该下界,可调高下界,否则需要调低下界。发现该 中位数的下界 满足单调性,可二分答案枚举 \(mid\)

发现选择 1 个不小于 \(mid\) 的数 和 1 个小于 \(mid\) 的数贡献会相互抵消。更形象地,1 个不小于 \(mid\) 的数的 贡献 为 1,选择 1 个小于 \(mid\) 的数的 代价 也为 1。由此,枚举下界后,考虑条件转换:将区间 \([a,d]\) 内不小于 \(mid\) 的数变为 1,小于 \(mid\) 的数变为 -1。此时一段区间的贡献变为该区间转化后的和。

单次 Check 需要解决的问题变为:判断转化后的数列,\([b,c]\) 的和 + \([a,b)\) 的最大后缀和 + \((c,d]\) 的最大前缀和,是否不小于 \(0\)。暴力实现单次 Check 的复杂度为 \(O(n)\),总复杂度为 \(O(qn\log n)\),过不了。


地球人都知道最大前后缀和可以用线段树维护:SP1043 GSS1 - Can you answer these queries I

考虑预处理每一个二分的下界对应的数列,并建出线段树。这样单次 Check 的复杂度变为 \(O(\log n)\),询问的复杂度变为 \(O(q\log^2 n)\)
但预处理 \(n\) 棵线段树空间,时间都是 \(O(n^2 \log n)\) 的,还是很菜,过不了。

下面是一个很神的优化 (不愧是clj)
考虑二分答案枚举的下界 \(mid\),发现对于下界为 \(mid\)\(mid+1\) 时的数列,仅有 \(a_i = mid\) 的位置不同,由 1 变为了 -1。再观察上面建的 \(n\) 棵线段树,由上可知,对于第 \(mid\) 棵与第 \(mid+1\) 棵,仅有\(a_i = mid\) 的位置不同。
考虑对 二分值 进行可持久化,从而压缩线段树空间。 离散化后二分值最多仅有 \(n\) 种取值,预处理的空间时间均压缩为 \(O(n\log n)\) 级别。再套用上面的查询,总复杂度为 \(O(n\log n + q\log^2 n)\),可过。

注意可持久化线段树的实际含义。
root 的下标是 二分值,因此构建 root[i] 时,修改对象为 \(a_j<i\) 的位置。

//知识点:二分答案,可持久化线段树 
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <vector>
#define ll long long
const int kMaxn = 2e4 + 10;
//=============================================================
struct Node {
  int sum, lmax, rmax;
};
int n, q, ans, a[kMaxn], root[kMaxn];
int data_num, data[kMaxn], map[kMaxn];
//=============================================================
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 Chkmax(int &fir_, int sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
namespace Seg {
  #define ls (lson[now_])
  #define rs (rson[now_])
  #define mid ((L_+R_)>>1)
  int node_num, lson[kMaxn << 5], rson[kMaxn << 5];
  int sum[kMaxn << 5], lmax[kMaxn << 5], rmax[kMaxn << 5];
  void Debug(int now_, int L_, int R_) {
    printf("%d %d:%d %d %d\n", L_, R_, sum[now_], lmax[now_], rmax[now_]);
    if (L_ == R_) {
      return ;
    }
    Debug(ls, L_, mid);
    Debug(rs, mid + 1, R_);
  }
  void Pushup(int now_) {
    sum[now_] = sum[ls] + sum[rs];
    lmax[now_] = std::max(lmax[ls], sum[ls] + lmax[rs]);
    rmax[now_] = std::max(rmax[ls] + sum[rs], rmax[rs]);
  }
  void Build(int &now_, int L_, int R_) {
    now_ = ++ node_num;
    if (L_ == R_) {
      lmax[now_] = rmax[now_] = sum[now_] = 1;
      return ;
    }
    Build(ls, L_, mid);
    Build(rs, mid + 1, R_);
    Pushup(now_);
  }
  void Modify(int &now_, int pre_, int L_, int R_, int pos_) {
    now_ = ++ node_num;
    ls = lson[pre_], rs = rson[pre_];
    if (L_ == R_) {
      lmax[now_] = rmax[now_] = 0;
      sum[now_] = -1;
      return ;
    }
    if (pos_ <= mid) Modify(ls, lson[pre_], L_, mid, pos_);
    else Modify(rs, rson[pre_], mid + 1, R_, pos_);
    Pushup(now_);
  }
  Node Merge(Node l_, Node r_) {
    Node ret = (Node) {0, 0, 0};
    ret.sum = l_.sum + r_.sum;
    ret.lmax = std::max(l_.lmax, l_.sum + r_.lmax);
    ret.rmax = std::max(l_.rmax + r_.sum, r_.rmax);
    return ret;
  }
  Node Query(int now_, int L_, int R_, int l_, int r_) {
    if (l_ > r_) return (Node) {0, 0, 0};
    if (l_ <= L_ && R_ <= r_) {
      return (Node) {sum[now_], lmax[now_], rmax[now_]};
    }
    if (r_ <= mid) return Query(ls, L_, mid, l_, r_); 
    if (l_ > mid) return Query(rs, mid + 1, R_, l_, r_);
    return Merge(Query(ls, L_, mid, l_, mid), Query(rs, mid + 1, R_, mid + 1, r_));
  }
  #undef ls
  #undef rs
  #undef mid 
}
void Prepare() {
  n = read();
  for (int i = 1; i <= n; ++ i) a[i] = data[i] = read();
  
  std::sort(data + 1, data + n + 1);
  data_num = 1;
  map[1] = data[1];
  for (int i = 2; i <= n; ++ i) {
    if (data[i] != data[i - 1]) {
      map[++ data_num] = data[i];
    }
    data[data_num] = data[i]; 
  }
  
  std :: vector <int> pos[kMaxn];
  for (int i = 1; i <= n; ++ i) {
    a[i] = std::lower_bound(data + 1, data + data_num + 1, a[i]) - data;
    pos[a[i]].push_back(i);
  }
  
  Seg::Build(root[1], 1, n);
  for (int i = 2; i <= data_num; ++ i) {
    root[i] = root[i - 1];
    for (int j = 0, lim = pos[i - 1].size(); j < lim; ++ j) {
      Seg::Modify(root[i], root[i], 1, n, pos[i - 1][j]);
    }
  }
}
bool Check(int ll_, int lr_, int rl_, int rr_, int lim_) {
  int sum = Seg::Query(root[lim_], 1, n, lr_, rl_).sum;
  int suml = Seg::Query(root[lim_], 1, n, ll_, lr_ - 1).rmax;
  int sumr = Seg::Query(root[lim_], 1, n, rl_ + 1, rr_).lmax;
  return (sum + suml + sumr) >= 0;
}
void Query(int ll_, int lr_, int rl_, int rr_) {
  for (int l = 1, r = data_num; l <= r; ) {
    int mid = ((l + r) >> 1);
    if (Check(ll_, lr_, rl_, rr_, mid)) {
      ans = map[mid];
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
}
//=============================================================
int main() {
  Prepare();
  q = read();
  for (int i = 1; i <= q; ++ i) {
    int opt[4];
    for (int j = 0; j < 4; ++ j) {
      opt[j] = (read() + ans) % n;
    }
    std::sort(opt, opt + 4);
    Query(opt[0] + 1, opt[1] + 1, opt[2] + 1, opt[3] + 1);
    printf("%d\n", ans);
  }
  return 0;
}

「TJOI / HEOI2016」字符串

二分答案,SA + 主席树

给定一长度为 \(n\) 的字符串,\(m\) 个询问。
每次询问给定参数 \(a,b,c,d\),求子串 \(S[a:b]\)所有子串,与子串 \(S[c:d]\) 的最长公共前缀的最大值。
\(1\le n,m\le 10^5, 1\le a\le b\le n, 1\le c\le d\le n\)

对于每一个询问,答案满足单调性,考虑二分答案。 设 \(l\) 为当前二分到的最长的,子串 \(S[a:b]\)所有子串,与子串 \(S[c:d]\) 的最长公共前缀。

分析可知,若 \(l\) 合法,那么会存在至少一个后缀 \(x\) 满足:

  • 开头在 \([a:b-l+1]\) 中。
  • \(\operatorname{lcp}(x, c)\ge l\)

对于第二个限制,考虑 \(\operatorname{lcp}\) 的单调性。\(\operatorname{lcp}(x,c)\) 是一个在 \(c\) 处取极值的单峰函数。
则满足条件的 \(x\) 的取值,一定是 \(sa\) 数组上连续的一段。
套路地对 \(\operatorname{height}\) 建立 st 表,即可 \(O(1)\) 查询 \(\operatorname{lcp}\),于是可以通过二分排名快速得到后缀 \(x\) 排名的取值范围,将限制二也转化为了区间限制形式。

限制一限制了后缀的区间,限制二限制了 \(rk\) 的区间。查询这样的后缀的存在性,变成了一个静态二维数点问题。
\(sa\) 数组建立主席树维护即可。

//知识点:SA,二分答案,主席树
/*
By:Luckyblock
*/
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <ctype.h>
#define ll long long
const int kMaxn = 1e5 + 10;
//=============================================================
char S[kMaxn];
int n, m, ans, cnt[kMaxn], id[kMaxn], rkid[kMaxn];
int sa[kMaxn], rk[kMaxn << 1], oldrk[kMaxn << 1], height[kMaxn];
int MaxHeight[kMaxn][20], Log2[kMaxn];;
int lson[kMaxn << 5], rson[kMaxn << 5], size[kMaxn << 5];
int node_num, root[kMaxn];
//=============================================================
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;
}
bool cmp(int x, int y, int w) { //判断两个子串是否相等。
  return oldrk[x] == oldrk[y] && 
         oldrk[x + w] == oldrk[y + w]; 
}
void GetHeight() {
  for (int i = 1, k = 0; i <= n; ++ i) {
    if (rk[i] == 1) k = 0;
    else {
      if (k > 0) k --;
      int j = sa[rk[i] - 1];
      while (i + k <= n && j + k <= n && 
             S[i + k] == S[j + k]) {
        ++ k;
      }
    }
    height[rk[i]] = k;
  }
}
int QueryLcp(int l_, int r_) {
  int k = Log2[r_ - l_ + 1];
  return std :: min(MaxHeight[l_][k], MaxHeight[r_ - (1 << k) + 1][k]);
}
void MakeSt() {
  for (int i = 2; i <= n; ++ i) MaxHeight[i][0] = height[i];
  for (int i = 2; i <= n; ++ i) {
    Log2[i] = Log2[i >> 1] + 1;
  }
  for (int j = 1; j < 20; ++ j) {
    for (int i = 1; i + (1 << j) - 1 <= n; ++ i) {
      MaxHeight[i][j] = std :: min(MaxHeight[i][j - 1], 
                                   MaxHeight[i + (1 << (j - 1))][j - 1]);
    }
  }
}
void SuffixSort() {
  int M = 300;
  for (int i = 1; i <= n; ++ i) ++ cnt[rk[i] = S[i]];
  for (int i = 1; i <= M; ++ i) cnt[i] += cnt[i - 1];
  for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;
  for (int p, w = 1; w < n; w <<= 1) {
    p = 0;
    for (int i = n; i > n - w; -- i) id[++ p] = i;
    for (int i = 1; i <= n; ++ i) {
      if (sa[i] > w) id[++ p] = sa[i] - w;
    }
    memset(cnt, 0, sizeof (cnt));
    for (int i = 1; i <= n; ++ i) ++ cnt[(rkid[i] = rk[id[i]])];
    for (int i = 1; i <= M; ++ i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; -- i) sa[cnt[rkid[i]] --] = id[i];
    std ::swap(rk, oldrk);
    M = 0;
    for (int i = 1; i <= n; ++ i) {
      M += (cmp(sa[i], sa[i - 1], w) ^ 1);
      rk[sa[i]] = M;
    }
  }
  GetHeight();
  MakeSt();
}
void Insert(int pre_, int &now_, int L_, int R_, int val_) {
  now_ = ++ node_num;
  size[now_] = size[pre_] + 1;
  lson[now_] = lson[pre_], rson[now_] = rson[pre_];
  if(L_ >= R_) return ;
  int mid = (L_ + R_) >> 1;
  if(val_ > mid) Insert(rson[pre_], rson[now_], mid + 1, R_, val_);
  else Insert(lson[pre_], lson[now_], L_, mid, val_);
}
int Query(int u_, int v_, int L_, int R_, int ql_, int qr_) {
  if (ql_ <= L_ && R_ <= qr_) return size[v_] - size[u_];
  int mid = (L_ + R_) >> 1, ret = 0;
  if (ql_ <= mid) ret += Query(lson[u_], lson[v_], L_, mid, ql_, qr_);
  if (qr_ > mid) ret += Query(rson[u_], rson[v_], mid + 1, R_, ql_, qr_);
  return ret;
}
bool Judge(int len_, int a_, int b_, int c_) {
  int l = 1, r = rk[c_], up, down;
  while (l < r) {
    int mid = (l + r) >> 1;
    if (QueryLcp(mid + 1, rk[c_]) < len_) l = mid + 1;
    else r = mid;
  }
  up = r, l = rk[c_], r = n;
  while (l < r) {
    int mid = (l + r + 1) >> 1;
    if (QueryLcp(rk[c_] + 1, mid) < len_) r = mid - 1;
    else l = mid;
  }
  down = r;
  return Query(root[up - 1], root[down], 1, n, a_, b_ - len_ + 1) > 0;
}
int Solve(int a_, int b_, int c_, int d_) {
  int l = 0, r = std :: min(b_ - a_ + 1, d_ - c_ + 1);
  while (l < r) {
    int len = (l + r + 1) >> 1;
    if (Judge(len, a_, b_, c_)) l = len;
    else r = len - 1;
  }
  return r;
}
//=============================================================
int main() {
  n = read(), m = read();
  scanf("%s", S + 1);
  SuffixSort();
  for (int i = 1; i <= n; ++ i) Insert(root[i - 1], root[i], 1, n, sa[i]);
  for (int i = 1; i <= m; ++ i) {
    int a = read(), b = read(), c = read(), d = read();
    printf("%d\n", Solve(a, b, c, d));
  }
  return 0;
}

「十二省联考 2019」字符串问题

SA,可持久化线段树优化建图,DAGDP

\(T\) 组数据,每次给定一字符串 \(S\)
\(S\) 中存在 \(n_a\) 个 A 类子串 \((la_i, ra_i)\)\(n_b\) 个 B 类子串 \((lb_i,rb_i)\)。且存在 \(m\) 组支配关系,支配关系 \((x,y)\) 表示第 \(x\) 个 A 类串支配第 \(y\) 个 B 类串。
要求构造一个目标串 \(T\),满足:

  • \(T\) 由若干 A 类串拼接而成。
  • 对于分割中所有相邻的串,后一个串存在一个被前一个串支配的前缀。

求该目标串的最大长度,若目标串可以无限长输出 \(-1\)
\(1\le T\le 100\)\(n_a,n_b,|S|,m\le 2\times 10^5\)
6S,1G。

首先建立图论模型,从每个 A 类子串向其支配的 B 串连边,从每个 B 串向以它为前缀的 A 串连边。A 串节点的权值为其长度,B 串节点权值为 0。
在图上 DP 求得最长路即为答案,若图中存在环则无解。
第一类边有 \(m\) 条,但第二类边数可以达到 \(n_an_b\) 级别,考虑优化建图。

对于某 A 串 \((la_i, ra_i)\),它以 B 串 \((lb_j, rb_j)\) 作为一个前缀的充要条件是 \(\operatorname{lcp}(S[la_i:n],S[lb_j:n]) \ge rb_j-lb_j+1\)\(ra_i - la_i + 1\ge rb_j-lb_j+1\)
对于限制一,考虑求得 \(S\) 的 SA,对 \(\operatorname{height}\) 建立 ST 表,可在 \(sa\) 上二分求得满足 \(\operatorname{lcp}\ge rb_j-lb_j+1\) 的区间的左右边界,满足条件的 A 串一定都在这段区间内。第二类边转化为区间连边问题。
此时不考虑限制二直接线段树优化建图,可以拿到 80 分的好成绩。

限制二实际上限定了 B 连边的对象的长度。
考虑将所有 A,B 串按长度递减排序,按长度递减枚举 A 串并依次加入可持久化线段树。
对于每一个 B 串,先找到使得 A 串长度大于其长度的最晚的历史版本,此时线段树上的所有 A 串长度都大于其长度,再向这棵线段树上的节点连边。

时间复杂度 \(O((|S| + n_a + n_b)\log n)\),空间复杂度 \(O(m + (|S| + n_a + n_b)\log n)\),不需要刻意卡常就能稳过。


看见上面轻描淡写的是不是觉得这题太傻逼了?以下是菜鸡 Lb 在代码实现上的小问题。

边数在极限数据下可以达到 \(10^7\) 级别,不注意空间大小和清空时的实现会被卡到 60。这个时空限制显然就是给选手乱搞的,数组往大了开就行。

在线段树优化建图中,实点会在建树操作中就与虚点连好边。本题的实点是代表 A,B 串的节点,在本题的可持久化线段树优化中,实点与虚点的连边发生在动态开点的插入过程中。
在新建节点时,需要将该节点连向上一个版本中对应位置的节点。
对于 A 串 \((la_i, ra_i)\),它应该被插入到线段树中 \(rk_{la_i}\) 的位置,即叶节点 \(rk_{la_i}\) 与该实点相连。

\(\operatorname{height}_1\) 没有意义。

注意二分时的初始值。

long long

函数传参顺序 是通过栈传递的,因此是从右向左的。

//知识点:SA,可持久化线段树,优化建图,DAGDP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
struct Str {
  int l, r, lth, id;
} subs[kN << 1];
int node_num, n, na, nb, m, into[kN <<5];
int e_num, head[kN << 5], v[50 * kN], ne[50 * kN];
LL val[kN << 5], f[kN << 5];
char s[kN];
//=============================================================
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 Chkmax(LL &fir_, LL sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(int &fir_, int sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool cmp(Str fir_, Str sec_) {
  if (fir_.lth != sec_.lth) return fir_.lth > sec_.lth;
  return fir_.id < sec_.id;
}
void Add(int u_, int v_) {
  v[++ e_num] = v_, ne[e_num] = head[u_], head[u_] = e_num;
  into[v_] ++;
}
namespace ST {
  int Minn[kN][21], Log2[kN];
  void MakeST(int *a_) {
    for (int i = 1; i <= n; ++ i) Minn[i][0] = a_[i];
    for (int i = 2; i <= n; ++ i) Log2[i] = Log2[i >> 1] + 1;
    for (int j = 1; j <= 20; ++ j) {
      for (int i = 1; i + (1 << j) - 1 <= n; ++ i) { //
        Minn[i][j] = std::min(Minn[i][j - 1], Minn[i + (1 << (j - 1))][j - 1]);
      }
    }
  }
  int Query(int l_, int r_) {
    int k = Log2[r_ - l_ + 1];
    return std::min(Minn[l_][k], Minn[r_ - (1 << k) + 1][k]);
  }
}
namespace SA {
  int sa[kN], rk[kN << 1];
  int oldrk[kN << 1], cnt[kN], id[kN];
  int height[kN];
  void SuffixSort() {
    int rknum = std::max(n, 300);
    memset(cnt, 0, sizeof (cnt));
    for (int i = 1; i <= n; ++ i) cnt[rk[i] = s[i]] ++;
    for (int i = 1; i <= rknum; ++ i) cnt[i] += cnt[i - 1];
    for (int i = n; i >= 1; -- i) sa[cnt[rk[i]] --] = i;
    
    for (int w = 1, p; w < n; w <<= 1) {
      p = 0;
      for (int i = n; i > n - w; -- i) id[++ p] = i;
      for (int i = 1; i <= n; ++ i) {
        if (sa[i] > w) id[++ p] = sa[i] - w;
      }
      
      memset(cnt, 0, sizeof (cnt));
      for (int i = 1; i <= n; ++ i) ++ cnt[rk[id[i]]];
      for (int i = 1; i <= rknum; ++ i) cnt[i] += cnt[i - 1];
      for (int i = n; i >= 1; -- i) sa[cnt[rk[id[i]]] --] = id[i];
      
      for (int i = 1; i <= n; ++ i) oldrk[i] = rk[i];
      rknum = 0;
      for (int i = 1; i <= n; ++ i) {
        rknum += (oldrk[sa[i]] == oldrk[sa[i - 1]] && 
                  oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]) ^ 1;
        rk[sa[i]] = rknum;
      }
    }
  }
  void GetHeight() {
    for (int i = 1, k = 0; i <= n; ++ i) {
      if (rk[i] == 1) {
        k = 0;
      } else {
        if (k) -- k;
        int j = sa[rk[i] - 1];
        while (i + k <= n && j + k <= n && 
               s[i + k] == s[j + k]) {
          ++ k;
        }
      }
      height[rk[i]] = k;
    }
  }
  int Lcp(int x_, int y_) {
    if (x_ > y_) std::swap(x_, y_);
    return ST::Query(x_ + 1, y_);
  }
  void Init() {
    SuffixSort();
    GetHeight();
    ST::MakeST(SA::height);
  }
}
namespace Hjt {
  #define ls lson[now_]
  #define rs rson[now_]
  #define mid ((L_+R_)>>1)
  int root[kN], lson[kN << 5], rson[kN << 5];
  void Insert(int &now_, int pre_, int L_, int R_, int pos_, int id_) {
    now_ = ++ node_num;
    ls = lson[pre_], rs = rson[pre_];
    if (pre_) Add(now_, pre_);
    if (L_ == R_) {
      Add(now_, id_);
      return ;
    }
    if (pos_ <= mid) {
      Insert(ls, lson[pre_], L_, mid, pos_, id_);
      Add(now_, ls);
    } else {
      Insert(rs, rson[pre_], mid + 1, R_, pos_, id_);
      Add(now_, rs);
    }
  }
  void AddEdge(int now_, int L_, int R_, int l_, int r_, int id_) {
    if (! now_) return ;
    if (l_ <= L_ && R_ <= r_) {
      Add(id_, now_);
      return ;
    }
    if (l_ <= mid) AddEdge(ls, L_, mid, l_, r_, id_);
    if (r_ > mid) AddEdge(rs, mid + 1, R_, l_, r_, id_);
  }
  #undef ls
  #undef rs
  #undef mid
}
void Init() {
  e_num = 0;
  for (int i = 0; i <= node_num; ++ i) {
    head[i] = val[i] = into[i] = f[i] = 0;
  }
  
  scanf("%s", s + 1);
  n = strlen(s + 1);
  SA::Init();
  na = read();
  for (int i = 1; i <= na; ++ i) {
    int l_ = read(), r_ = read();
    subs[i] = (Str) {l_, r_, r_ - l_ + 1, i};
    val[i] = subs[i].lth;
  }
  nb = read();
  for (int i = 1; i <= nb; ++ i) {
    int l_ = read(), r_ = read();
    subs[na + i] = (Str) {l_, r_, r_ - l_ + 1, na + i};
  }
  m = read();
  for (int i = 1; i <= m; ++ i) {
    int u_ = read(), v_ = read();
    Add(u_, v_ + na); //Add(read(), read()+na) 会倒着读
  }
  node_num = na + nb;
}
bool Check(int x_, int y_, int lth_) {
  return SA::Lcp(x_, y_) >= lth_;
}
void AddEdgeB(int id_, int now_) {
  int pos = SA::rk[subs[id_].l], l_ = pos, r_ = pos; //l_,r_ 初始值
  for (int l = 1, r = pos - 1; l <= r; ) {
    int mid = (l + r) >> 1;
    if (Check(mid, pos, subs[id_].lth)) {
      r = mid - 1;
      l_ = mid;
    } else {
      l = mid + 1;
    }
  }
  for (int l = pos + 1, r = n; l <= r; ) {
    int mid = (l + r) >> 1;
    if (Check(pos, mid, subs[id_].lth)) {
      l = mid + 1;
      r_ = mid;
    } else {
      r = mid - 1; 
    }
  }
  Hjt::AddEdge(Hjt::root[now_], 1, n, l_, r_, subs[id_].id);
}
void Build() {
  node_num = na + nb;
  std::sort(subs + 1, subs + na + nb + 1, cmp);
  for (int now = 0, i = 1; i <= na + nb; ++ i) {
    if (subs[i].id > na) {
      AddEdgeB(i, now);
      continue;
    }
    ++ now;
    Hjt::Insert(Hjt::root[now], Hjt::root[now - 1], 1, n, SA::rk[subs[i].l], 
                subs[i].id);
  }
}
void TopSort() {
  std::queue <int> q;
  for (int i = 1; i <= node_num; ++ i) {
    if (!into[i]) {
      f[i] = val[i];
      q.push(i);
    }
  }
  while (! q.empty()) {
    int u_ = q.front(); q.pop();
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Chkmax(f[v_], f[u_] + val[v_]);
      -- into[v_];
      if (!into[v_]) q.push(v_);
    }
  }
  LL ans = 0;
  for (int i = 1; i <= node_num; ++ i) {
    Chkmax(ans, f[i]);
    if (into[i]) {
      printf("-1\n");
      return ;
    }
  }
  printf("%lld\n", ans);
}
//=============================================================
int main() {
  int T = read();
  while (T --) {
    Init();
    Build();
    TopSort();
  }
  return 0;
}
posted @ 2020-05-01 10:54  Luckyblock  阅读(475)  评论(3编辑  收藏  举报