树状数组上的二分

题目大意,给你 nn 个数,两种操作,第一种是修改 ax=da_x = d
第二种是查询最大的 T,T[0,n]T, T \in [0, n] 满足 i=1Tais\displaystyle \sum_{i = 1}^{T} a_i \leqslant s

树状数组二分算法描述如下,从高位往低位去遍历树状数组
用一个 pos\text{pos} 去记录一下当前在树状数组 C[]C[\cdots] 的哪个位置
用一个 tt 去记录前缀和

forjlogn0\displaystyle \textbf{for} \quad j \in \left\lceil \log n\right\rceil \to 0
pospos+2j\displaystyle \quad pos' \leftarrow pos + 2^j
ift+C[pos]s:pospos,t+=C[pos]\displaystyle \quad \textbf{if} \quad t + C[pos'] \leqslant s: pos \leftarrow pos', t += C[pos']

算法实现原理
pospos 在第 jj 位的时候,此时下标满足第 jj 位以后都是 00,即有形式
pos:? ? ? 0 00pos:\quad ?\ ?\ ? \ 0 \ 0\cdots 0,将 pospos+2jpos' \leftarrow pos + 2^j, 相当于将第 jj 位置为 11
pos:? ? ? 1 00pos': \quad ?\ ? \ ? \ 1 \ 0 \cdots 0,可以看到 C[pos]C[pos'] 恰好对应了原数组下标
[(? ? ? 0 01)(? ? ? 1 00)][(? \ ? \ ? \ 0 \ 0 \cdots 1 ) \to ( ? \ ? \ ? \ 1 \ 0 \cdots 0)]

换句话说,C[pos]C[pos'] 恰好对应原来数组中下标 [pos+1pos][pos+1\cdots pos'] 的和
如果加到 pospos' 仍然不超过 ss,那么就将这部分和加入到答案中

二维树状数组

与一维类似,二维树状数组的 C(i,j)C(i, j) 维护的是
原数组下标范围 a([ilowbit(i)+1i],[jlowbit(j)+1j])\displaystyle a([i-\text{lowbit}(i)+1\cdots i], [j - \text{lowbit}(j)+1 \cdots j]) 的和
对一维数组稍作修改,两个 for\text{for} 循环求和即可

线段树维护最大子段和

带区间修改的最大子段和维护,可以用线段树
假设线段树节点 pp 对应的区间最大子段和记为 mss\text{mss}

那么它的最大子段只可能来自 33 个部分,分别是
左边的最大子段和,右边的最大子段和,左边的最大后缀加上右边的最大前缀

mss=max(l.mss,r.mss,l.msuf+r.mpre)\text{mss} = \max(l.\text{mss}, \quad r.\text{mss}, \quad l.\text{msuf} + r.\text{mpre})
其中 msuf,mpre\text{msuf}, \text{mpre} 也要分别维护出来
一个节点的最大后缀,它要么是右边的最大后缀,要么是右边一整段的和加上左边的最大后缀

msuf=max(r.msuf,r.sum+l.msuf)\text{msuf} = \max(r.\text{msuf}, \quad r.sum + l.\text{msuf})
mpre=max(l.mpre,l.sum+r.mpre)\text{mpre} = \max(l.\text{mpre}, \quad l.sum + r.\text{mpre})
sum=l.sum+r.sumsum = l.sum + r.sum

这些信息分别维护出来就可以

线段树懒标记编程技巧

线段树处理懒标记的时候,应该让标记尽量少
比如假设有 33 种操作,分别是区间 +d+d,区间 ×d\times d,以及区间赋值成 dd

那么可以考虑将这三种操作统一起来

假设用一个标记 tag(a,b)\textbf{tag}(a, b),类似矩阵变换 tagx\textbf{tag} \cdot x 表示将 tag\textbf{tag} 作用在 xx
那么 xxa+bx \leftarrow x \cdot a + b

这样区间 +d+d 的操作打标记的时候,相当于 tag(1,d)\textbf{tag}(1, d)
区间 ×d\times d 的操作打标记,相当于 tag(d,0)\textbf{tag}(d, 0)
区间整体赋值成 dd,相当于 tag(0,d)\textbf{tag}(0, d)

接着考虑两件事情,分别是标记合并和信息更新
首先看信息更新,假设是对当前区间 pp 作用了 tag(a,b)\textbf{tag}(a, b)
那么每一个数都变成 xxa+bx \leftarrow x \cdot a + b,维护的区间和变成

(x)a+p.sz×b\displaystyle \left(\sum x \right) a + p.sz \times b,即 sumsuma+p.sz×bsum \leftarrow sum \cdot a + p.sz \times b

考虑标记合并,假设当前两个标记 tg1(a1,b1),tg2(a2,b2)\text{tg1}(a_1, b_1), \text{tg2}(a_2, b_2),因为矩阵乘法是不能交换
不妨设原来标记是 tg1(a1,b1)\text{tg1}(a_1, b_1),新标记是 tg2(a2,b2)\text{tg2}(a_2, b_2),那么

x(x×a1+b1)×a2+b2x \leftarrow (x \times a_1 + b_1) \times a_2 + b_2,合并之后
xx×(a1a2)+b1a2+b2x \leftarrow x \times (a_1 \cdot a_2) + b_1 \cdot a_2 + b_2
由此新的标记是 tg(a1a2,b1a2+b2)\text{tg}(a_1 \cdot a_2, \quad b_1 \cdot a_2 + b_2)

最后注意的是,标记清空的标志是 tag=tag(1,0)tag = \textbf{tag}(1, 0)
线段树建树的时候,也要初始化 tag(1,0)\textbf{tag}(1, 0)

线段树上二分

问题描述,给你一个序列,支持单点修改,能够修改 ax=da_x = d
给你 qq 个询问,问在 [l,r][l, r]第一个 d\geqslant d 的位置 pospos,不存在就返回 1-1

可以用线段树维护一个区间最大值 maxvmaxv,对于每个询问,在线段树上查询 [l,r][l, r]

如果线段树当前区间和 [l,r][l, r] 重合,并且当前区间的 maxv<dmaxv < d,那么当前区间是不可能有 d\geqslant d 的数的
直接返回 1-1,否则的话一定能找到这样的 pospos
接着优先去左边找,左边没有就去右边找

如果当前区间 l=rl = r,那么直接返回 ll,否则的话,继续递归
如果左儿子的 l.maxvdl.maxv \geqslant d,那么递归左儿子,返回 search(p1,l,mid,d)\text{search}(p\ll 1, l, mid, d)
否则返回 search(p1+1,mid+1,r,d)\text{search}(p \ll 1+1, mid+1, r, d)

如果当前区间需要分裂,同时递归左右子树,那么先递归左子树
记一下 pos=search(p1,l,mid,d)pos = \text{search}(p \ll 1, l, mid, d),如果 pos1pos \neq -1 立即返回 pospos
否则就返回 search(p1+1,mid+1,r,d)\text{search}(p \ll 1 + 1, mid+1, r, d)

动态开点线段树

NOIP2017 列队

算法分析
本例中需要支持如下操作,在 n×mn \times m 的方阵中,如果让 (x,y)(x, y) 出方阵,那么

  • 找到标号第 xx 大的行,并且在该行中找到第 yy 大的列,将对应的元素 (x,y)(x, y) 取出
    然后把该行中 [y+1m][y+1\cdots m] 整体往前挪一位
  • 然后操作第 mm 列,将第 mm[x+1n][x+1\cdots n] 整体往上挪一位
  • 最后把取出来的元素插入 (n,m)(n, m)

注意到我们需要对 nn 行,以及第 mm 列求前缀第 kk
可以考虑一开始建 n+1n+1 棵线段树,其中每一棵线段树初始只有一个根节点
这样第 [1n][1\cdots n] 棵线段树维护 [1n][1 \to n] 行,第 n+1n+1 棵线段树维护第 mm

将线段树写成动态开点,这样就可以尝试维护操作了,维护区间内被删除了多少个数 cntcnt

  • 对于删除 (x,y)(x, y),就是在第 xx 棵线段树 tr(x)tr(x) 中找到第 yy 大的数 pp,然后返回 pp
    如果要对应到方阵中的编号,那么就是 id=(x1)m+pid = (x-1)\cdot m + p
    然后删除这个点,采用懒惰删除的方法,对线段树这个点标记为删除,即 u.cnt+1u.\text{cnt} + 1
  • 值得注意的是,懒惰删除的时候,和一般线段树不一样,我们 pull(u)\text{pull}(u) 的时候
    因为是动态开点的,uu 的左子树或者右子树可能不存在,要加判一下子树存在
    以左子树为例,如果 u.lnullu.l \neq \text{null},那么直接 u.cnt+=u.l.cntu.\text{cnt} += u.l.\text{cnt}
    否则的话左子树不存在,也就是说我们并没有修改过左子树对应的区间,u.cnt+=0u.\text{cnt} += 0
  • 修改的时候,比如要出列第 yy 个人,只需要把 rootyroot \to y 路径上的点都开出来就可以
    唯一需要注意的是,递归子树的时候,如果子树是 null\textbf{null},那么要先 new\textbf{new} 出来节点然后递归
  • 接着考虑查询第 kk 大,同样需注意,递归子树遇到 null\textbf{null} 要先将其 new\textbf{new} 出来
    假设当前递归到 (u,l,r)(u, l, r),那么左子树中的元素个数是 tot=midl+1tot = mid - l + 1
    如果左子树非空,那么 tottot 还要扣除掉已经删除的元素,即 tot=u.l.cnttot -= u.l.\text{cnt}
    否则,说明左子树对应的区间我们没有修改过,自然对应的 cnt=0\text{cnt} = 0
  • 然后执行很常见的二分查找逻辑,如果 ktotk \leqslant tot,那么在左子树查找第 kk
    否则去右子树找第 ktotk - tot

综上所述

  • 通过线段树维护,可以得到要出列的点编号,假设是 id=(x1)m+yid = (x-1) \cdot m + y
    可以用一个 vector\text{vector} 存储被取出的点,vector[x]{y}\text{vector}[x] \leftarrow \{y\} 表示
    xx 棵线段树被删除点的情况就存储到 vector[x]\text{vector}[x]
  • 接着是查询第 xx 大的下标 pp,如果是叶子节点,就直接返回 p=lp = l,否则二分查询左右子树
  • 接下来有一些坑点,注意到对 xx 行修改,得到的前 yy[1,m1]\in [1, m-1]
    最后一列我们是单独拿出来处理的,如果 pmp \geqslant m,那么查询到的这个人是
    “向左看齐” 之后由最后一列第 xx 大填补空缺补充上来的
  • 由此对 (x,y)(x, y) 出列操作,还需要额外维护最后一列填补空缺的人的编号
    即在第 n+1n+1 棵线段树中查找第 xx 大,此时这个编号的人不需要出列,而是填补空缺

由此程序实现需要注意的

  • 对最后一列操作,即第 n+1n+1 棵线段树,维护两种情况
  • 第一种是 solve(1)solve(1)
    这一列的第 xx 个人出列,此时 vector[n+1]{x}\text{vector}[n+1] \leftarrow \{x\},这个人要重新插入到末尾
  • 另外一种是 solve(0)solve(0)
    m1m-1 列的人出队,假设说是 (x,y)(x, y),在第 n+1n+1 棵线段树找到第 xx 大之后
    这个人是要填补空缺的,不需要真正出列,不需要将其放在 vector[n+1]\text{vector}[n+1] 的末尾
    而是要放到 vector[x]\text{vector}[x] 的末尾
  • 对其他列操作,加入是 (x,y)(x, y),在第 xx 棵线段树中找到第 yy 大的编号 idid,做两件事情
    第一是在将 idid 加入到第 n+1n+1 棵线段树对应的 vector\text{vector} 末尾,即 vector[n+1]{id}\text{vector}[n+1] \leftarrow \{id\}
    第二就是填补空缺,在第 n+1n+1 棵线段树中找到第 xx 大的编号 id2id2vector[x]{id2}\text{vector}[x] \leftarrow \{id2\}

这样所有信息都维护好了

  • 如果对第 mm 列操作,查询第 xx 大的下标是 pppnp \leqslant n 直接返回 pp
    否则是出列过之后又进来的,返回 vector[n+1][pn1]\text{vector}[n+1][p-n-1]
  • 如果对让 (x,y)(x, y) 出列,xm1x \leqslant m-1,那么同样查询第 yy 大的下标 pp
    pm1p \leqslant m-1 直接返回,否则返回 vector[x][pm]\text{vector}[x][p-m]

珂朵莉树

珂朵莉树可以较快地实现

  • 区间加
  • 区间赋值
  • 区间第 kk
  • 求区间 nn 次方的和

珂朵莉树特别擅长处理区间 [l,r][l, r] 之间数的值都是 vv 的情况
因为区间处理会把大量的数变成同一个数
用一个三元组 <l,r,v>\left<l, r, v \right> 表示区间 [l,r][l, r] 的元素值都是 vv

对三元组按区间左端点排序

1
2
3
4
5
6
7
8
struct Node {
int l, r;
mutable long long v;
Node(int l, int r, long long v) : l(l), r(r), v(v) {}
bool operator< (const Node &rhs) const {
return l < rhs.l;
}
};

区间分裂
区间操作常常需要把原本连续的区间断开,<l,r,v><l,pos1,v><pos,r,v>\displaystyle \left<l, r, v\right> \to \left<l, pos-1, v \right> \cup \left<pos, r, v \right>
将 Node 用 set 来维护的话很容易实现,我们按左端点排序
只需要找到右边第一个 pos\geqslant pos 的元素 itit

  • 如果已经存在以 pospos 为左端点的区间,直接返回
  • 否则,令 itit1it\leftarrow it-1,则此时 itit 代表的区间就是 [lposr][l\cdots pos \cdots r],为待分裂的区间
    itit 从 set 中删除,并且加入 <l,pos1,v>,<pos,r,v>\left<l, pos-1, v \right>, \left<pos, r, v \right>
1
2
return tr.insert(Node(pos, r, v)).first
返回插入的位置

区间赋值
[l,r][l, r] 从原序列中分裂出来,然后在 set 中删掉 [l,r][l, r]
将新的值 <l,r,now>\left<l, r, now \right> 插入 set

区间加
同样采用暴力的方式,将 [l,r][l, r] 从序列中断开,挨个加

1
2
3
4
5
6
void add(int l, int r, long long v) {
auto end = split(r+1);
for (auto it = split(l); it != end; it++) {
it->v += v;
}
}

区间第 k 大
同样先将 [l,r][l, r] 分离出来,具体分离的操作如下

1
2
auto end = split(r+1);
for (auto it = split(l); it != end; it++)

这样每一次迭代器 itit 就分离出区间的第一个节点,用一个二元组 (v,len)(v, len) 来表示
区间的值都是 vv,并且区间的长度为 lenlen,将二元组放入到一个 vector\text{vector}

二元组按值 vv 排序,求区间第 kk 大就很容易了

1
2
3
4
for (auto u : v) {
k -= u.second;
if (k <= 0) return u..first;
}

求区间所有数的 n 次方的和
同样分离区间,然后对 x=power(x,n)x = \text{power}(x, n),再乘上区间长度即可

1
2
3
4
5
6
long long sum = 0;
auto end = split(r+1);

for (auto it = split(l); it != end; it++) {
tot = tot + power(it->v, n) * (it->r - it->l + 1);
}

CF915E Physical Education Lessons
题目大意,现在到学期结束还有 nn 天,他们一开始都是工作日,接下来工作人员发出 qq 个指令
每个指令用 (l,r,k)(l, r, k) 描述
如果 k=1k = 1,那么 [l,r][l, r] 所有日子都变成非工作日
如果 k=2k = 2,那么 [l,r][l, r] 所有日子都是工作日
统计每个指令下发后,剩余工作日的天数

算法分析
一开始记一下所有日子都是工作日 sum=nsum = n
对于操作 11,相当于区间 [l,r][l, r] 赋值成 kk,同时 split\text{split} 的时候记一下 lenlen 表示区间内一共有多少个数
编程实现的时候用到了一个技巧
工作日赋值成 11,非工作日赋值成 00,假设区间长度是 lenlen
这样 vlenv \cdot len 恰好就能表示区间内有多少个工作日了

这样对于每一个询问可以维护出 [l,r][l, r] 中有多少个数 lenlen

  • 对于 k=2k = 2,将所有日子都变成工作日,我们把 [l,r][l, r] 区间所有数都赋值成 11
    那么我们需要知道哪些数原本就是 11(工作日),而原来就是工作日的数有 tot=vlentot = \sum v \cdot len
    这样 lenvlen\sum len - \sum v\cdot len 就是新增加的工作日天数,将其加入到答案中
  • 对于 k=1k = 1,将所有日子变成非工作日,区间内所有 11 的数有多少个呢?恰好就是 tottot
    于是在答案中减去 tottot 即可