线段树的各种扩展

线段树的各种扩展

前情提要线段树, 算法竞赛掌管区间的神, 权值数据结构水各种题.

小技巧

动态开点

这篇博客所有的线段树扩展都基于动态开点, 所以先讲一下.

先申请一个很长的数组, 需要新结点就从数组里申请.

这是一种内存池思想, 可以避免内存的多次申请与释放 (更多的是可以避免指针), 在有文字记载的首任指针神教教主一扶苏一的代码中, 甚至会直接把这个很长的数组称为 \(pool\).

int rt = 0, cntt = 0;
int ls[NN << 5], rs[NN << 5];
T val[NN << 5]; // 这个按照需要来写类型, 下面所有T都是同样

inline // 这个函数是申请结点用的, 一般直接写++cntt, 不写函数
int newnode() {
  return ++cntt;
}

为什么是 \(32\) 倍空间呢?

每次新建都需要从根到叶子建一串结点, 就是 \(\log V\) 个, 所以开\(32\) (int的最大长度) 倍空间即可 (当然 long long\(64\)).

很显然修改可能要新建, 但是询问和线段树上二分是不需要新建的.

放一下代码, 单点修改区间查询和线段树上二分 (这篇博客介绍的东西都是只需要单点修改的, 涉及区间修改也只需要打上标记即可).

传入指针是因为C语言没有引用这种东西, 有条件建议是传引用.

传指针是因为不想传引用, 不代表是指针线段树. --指针神教教主Defad

void update(int *x, int l, int r, int I, T k) {
  if (!*x) *x = ++cntt;
  if (l == r) {
    val[*x] += k;
    return;
  } else {
    int m = l + r >> 1;
    if (I <= m)
      update(ls + *x, l, m, I, k);
    else
      update(rs + *x, m + 1, r, I, k);
    return pushup(*x);
  }
}
T query(int x, int l, int r, int L, int R) {
  if (L <= l && r <= R) {
    return val[x];
  } else {
    int m = l + r >> 1; T s = 0; // 置空
    if (L <= m)
      s += query(ls[x], l, m, L, R);
    if (m + 1 <= R)
      // 3F曾经某次At-ABC因为这里写成 s == query 导致罚时, 希望同学们引以为戒
      s += query(rs[x], m + 1, r, L, R);
    return s;
}
int kth(int x, int l, int r, T k) {
  if (l == r) {
    return l;
  } else {
    int m = l + r >> 1;
    if (k <= val[ls[x]])
      return kth(ls[x], l, m, k);
    else if (k <= val[x])
      return kth(rs[x], m + 1, r, k - val[ls[x]]);
    else // 这个根据题目来, 有时是 -1, 有时是 r 或 r + 1
      return -1;
  }
}

不知有没有同学想过为什么这里访问了 \(ls\), 但是有可能 \(ls\) 没有用过就是 \(0\), 反正Defad是想过的.

因为 \(0\) 是始终都不会被修改的, 所以一直有 \(ls_{0} = 0, rs_{0} = 0, val_{0} = 0\), 所以不会出问题.

但是如果用指针线段树就要注意了, 没有初始化的是 NULL, 访问空指针就是RE, 直接挂大分.

标记永久化

线段树上的一种技巧, 不需要进行 pushdown 就能快很多, 最重要的是在标记难以下传时可以直接维护标记.

首先区间修改的时候打标记, 这里用普通线段树举例.

inline
void pushup(int x, int l, int r) {
  val[x] = val[x << 1] + val[x << 1 | 1] + tag[x] * (r - l + 1);
}
void update(int x, int l, int r, int L, int R, ll k) {
  if (L <= l && r <= R) {
    val[x] += k * (r - l + 1);
    tag[x] += k;
    return;
  } else {
    int m = l + r >> 1;
    if (L <= m)
      update(x << 1, l, m, L, R, k);
    if (m + 1 <= R)
      update(x << 1 | 1, m + 1, r, L, R, k);
    return pushup(x, l, r);
  }
}

然后查询时带着标记查询.

ll query(int x, int l, int r, int L, int R, ll k) {
  if (L <= l && r <= R) {
    return val[x] + k * (r - l + 1);
  } else {
    int m = l + r >> 1; ll s = 0LL;
    if (L <= m)
      s += query(x << 1, l, m, L, R, k + tag[x]);
    if (m + 1 <= R)
      s += query(x << 1 | 1, m + 1, r, L, R, k + tag[x]);
    return s;
  }
}

动态开点权值线段树

好消息是不需要离散化, 坏消息是容易写挂.

以上次的平衡树举例.

VJudge LuoGu

直接把区间长度开到 \([-1 * 10^{7}, 1 * 10^{7}]\), 然后把上次的代码的离散化删掉, 提交.

线段树分裂合并

一般用于集合的分裂合并, 所以多数是权值线段树.

线段树合并

合并两个可重集, 这两个可重集分别是用权值线段树维护的.

首先, 肯定要递归整个线段树.

其次, 如果当前可重集 \(x\)\(y\) 的这个结点为空, 就返回非空的.

void merge(int x, int y, int l, int r) {
  if (!x || !y) return x ^ y; // 返回非空的
  if (l == r) {
    val[x] += val[y];
    return x;
  } else {
    int m = l + r >> 1;
    ls[x] = merge(ls[x], ls[y], l, m);
    rs[x] = merge(rs[x], rs[y], m + 1, r);
    pushup(x);
    return x;
  }
}

线段树分裂

把可重集 \(y\) 的所有在 \([L, R]\) 区间内的数放到新的可重集 \(x\), 并在 \(y\) 中删掉这些数.

首先肯定要申请 \(x\) 的结点.

然后重要的, 在区间内就 \(ls, rs, val\) 都要传递, 清空可以偷懒直接给这个结点的父结点的这个儿子变成 \(0\).

int split(int *y, int l, int r, int L, int R) {
  int x = ++cntt;
  if (L <= l && r <= R) {
    ls[x] = ls[*y];
    rs[x] = rs[*y];
    val[x] = val[*y];
    *y = 0;
    return x;
  } else {
    int m = l + r >> 1;
    if (L <= m)
      ls[x] = split(ls + *y, l, m, L, R);
    if (m + 1 <= R)
      rs[x] = split(rs + *y, m + 1, r, L, R);
    pushup(*y);
    pushup(x);
    return x;
  }
}

板子

VJudge LuoGu

需要记录多个根, 使用一个 \(cntr\) 记录当前有多少个根, 分裂出来直接 \(cntr := cntr + 1\) 然后给到 \(rt_{cntr}\) 即可.

持久化线段树

VJudge LuoGu

本期唯一不涉及权值的线段树, 只需要在历史版本上单点修改单点查询.

首先这次如果有初始化就必须建树了, 不然就不是版本 \(0\) 了.

inline
void pushup(int x) {
  val[x] = val[ls[x]] + val[rs[x]];
}
void build(int *x, int l, int r) {
  if (!*x) *x = ++cntt;
  if (l == r) {
    val[*x] = a[l];
    return;
  } else {
    int m = l + r >> 1;
    build(ls + *x, l, m);
    build(rs + *x, m + 1, r);
    return pushup(*x);
  }
}

然后考虑修改.

A. 把原来的线段树复制一遍.
B. 只把要修改的一串修改, 其他的用原来的.

显然, 前者极易MLE, 而后者较省空间.

void update(int *x, int y, int l, int r, int I, ll k) {
  if (!*x) *x = ++cntt;
  if (l == r) {
    val[*x] = k;
    return;
  } else {
    int m = l + r >> 1;
    if (I <= m) {
      rs[*x] = rs[y];
      update(ls + *x, ls[y], l, m, I, k);
    } else {
      ls[*x] = ls[y];
      update(rs + *x, rs[y], m + 1, r, I, k);
    }
    return pushup(*x);
  }
}

查询如果要新建相同版本, 可以传递一下 \(rt_{h}\)\(rt_{i}\), 把历史版本的根直接给到当前版本.

普通的动态开点线段树单点查询.

ll query(int x, int l, int r, int I) {
  if (l == r) {
    return val[x];
  } else {
    int m = l + r >> 1;
    if (I <= m)
      return query(ls[x], l, m, I);
    else
      return query(rs[x], m + 1, r, I);
  }
}

主席树

VJudge LuoGu 双倍经验

动态开点权值线段树, 并不是持久化的, 因为持久化需要支持修改操作, 而主席树的修改操作并不是对于值进行修改, 而是维护数组的.

第一步, 离散化.

C语言离散化

第二步, 建树.

这个建树和普通线段树的建树不一样, 要建立 \(N\) 个权值线段树 (显然要动态开点并且每次只新建一串), 第 \(i\) 个权值线段树维护数组 \(a\)\(1\)\(i\) 中出现的所有的数.

指针神教上一任教主Wild-Donkey形容主席树为"批判的继承".

inline
void pushup(int x) {
  val[x] = val[ls[x]] + val[rs[x]];
}
void update(int *x, int y, int l, int r, int I, int k) {
  if (!*x) *x = ++cntt;
  if (l == r) {
    val[*x] = val[y] + k;
    return;
  } else {
    int m = l + r >> 1;
    if (I <= m) {
      rs[*x] = rs[y];
      update(ls + *x, ls[y], l, m, I, k);
    } else {
      ls[*x] = ls[y];
      update(rs + *x, rs[y], m + 1, r, I, k);
    }
    return pushup(*x);
  }
}
f1 (i, 1, S, 1) { // 这是main里的离散化后的建树部分
  update(rt + i, rt[i - 1], 1, N, a[i], 1);
}

查询.

线段树上二分, 注意要二分的是 \(x - 1\)\(y\) , 至于为什么可以想一想前缀和.

int kth(int x, int y, int l, int r, int k) {
  if (l == r) {
    return l;
  } else {
    int m = l + r >> 1;
    if (k <= val[ls[y]] - val[ls[x]])
      return kth(ls[x], ls[y], l, m, k);
    else if (k <= val[y] - val[x])
      return kth(rs[x], rs[y], m + 1, r, k - (val[ls[y]] - val[ls[x]]));
    else
      return -1;
  }
}

最后输出的是?
A. a[query(rt[x - 1], rt[y], 1, N, k)]
B. b[query(rt[x - 1], rt[y], 1, N, k)]

显然要选B, 但是Defad在SPOJ上交主席树时在这里也出过逝, 所以也要引以为戒, a 是离散化后的, b 是原数组去重的.

posted @ 2024-11-11 23:36  指针神教教主Defad  阅读(18)  评论(0编辑  收藏  举报