线段树分治学习笔记

也许更好的阅读体验

线段树分治其实个人更愿意认为它更像一种技巧而不是一个算法,即带撤销操作的时间分治。操作形式多为维护一些信息,操作可能有询问/执行操作/撤回操作,其操作的执行较为容易,但撤回困难。所以把操作,询问一起离线下来,每个操作只在时间轴上的一个区间生效,那么就可以在线段树上的这个区间打上操作的标记,维护信息,在叶子结点计算答案。

模板题P5757:一张 \(n\) 个点的图,有 \(m\) 条无向边会在 \([l_i,r_i]\) 时刻存在,求在 \([1,k]\) 时刻中每个时刻整张图是否是一张二分图。

\(1\leq n\leq 10^5\)\(1\leq l_i\leq r_i\leq k\leq 10^5\)\(1\leq m\leq 2\times 10^5\)

前置知识:扩展域并查集判定二分图,可撤销并查集,线段树。

考虑二分图中一条边的意义为其两边端点不在同一部,则用 \(x,x+n\) 代表 \(x\)\(x\) 的对立点,和 \(x\) 的对立点处于同一集合的则和 \(x\) 不在同一部,那么二分图中一条边就是互相和对方的对立点相连,若出现 \(x\)\(x+n\) 处于同一个集合则说明不是二分图,并且若 \(x,y\) 是图变为非二分图加的最后一条边,那么 \(x,x+n\)\(y,y+n\) 之间会分别在同一个集合中。

可撤销并查集的原理是考虑并查集常见的两种优化,路径压缩和按秩合并,这里的秩可以是 size,depth,甚至是 rand,单独用 size 作为秩进行按秩合并的好处是,把 \(x\) 并到 \(y\) 上的操作只有累加 \(sz_x\)\(sz_y\),更新 \(fa_x\)\(y\),不难发现,若最近一次操作就是把 \(x\) 并到 \(y\),那么这个并上去的操作可以把 \(sz\) 减回去,\(fa\) 恢复,则做到了撤销。其限制在于只能按照并的顺序倒着撤销,即按照栈的顺序,否则后来并到 \(x\) 上的贡献不能仅减去 \(sz_x\) 来消除。

接下来我们考虑对于时间轴建立线段树,每个节点维护一个 vector,类似标记永久化地,把边放入其存在时间的线段树节点上,则叶子节点到根路径上所有 vector 里的边就是在其对应时刻存在的边。于是很自然地发现在线段树上 dfs,进入节点时把它 vector 中的元素插入并查集,离开的时候倒序撤销,则 dfs 栈默认就有按照顺序来撤销的性质,则到叶子节点的时候可以直接判断是否仍是二分图。

于是总复杂度 \(O(m\log k\log n)\)一份参考代码实现

例题CF576E:一张 \(n\) 个点 \(m\) 条边的无向图,颜色从 \(1\)\(k\) 编号,初始边上颜色均为 \(0\)\(q\) 次修改操作把第 \(e_i\) 条边的颜色修改为 \(c_i\),定义一个合法状态为 \(1\cdots k\) 每个颜色的边集分别构成的子图都是二分图,若修改之后由合法状态变为非法状态,则此次修改无效,求每次修改是否有效。

\(1\leq n,m,q\leq 5\times 10^5\)\(1\leq k\leq 50\)

考虑这题的形式和模板题非常像,数据范围也支持我们对于 \(k\) 种颜色分别维护,所以考虑如何进行转化使得可以让我们类似地解决这个问题。

现在的困难在于,我们没有上一题一样得到一条边在某个时间内属于某个颜色的信息,于是我们考虑每条边的颜色情况。若边 \(e_i\) 相邻的两次颜色修改时刻为 \(x<y\),则我们可以看作,它在 \([x,x]\) 时刻修改了颜色,并且由于是单点变更,我们可以到叶子节点的时候尝试修改并判断是否合法,以此我们就能得到其在 \((x,y)\) 时间中的颜色,就和上面一题完全一致了。这里的逻辑是其递归到底层之后是没有被前面或后面的修改所包含的,而且由于是先处理左儿子,到 \((x,y)\) 的时候一定已经经过了叶子 \(x\),可以知道其对应的颜色并加入,而且由于是在时间范围内加入,也并没有删除的操作。

于是总复杂度 \(O(q\log q\log n+nk)\)一份参考代码实现

例题CF603E:一个 \(n\) 个点的无向图,\(m\) 次加入一条带权边 \((u,v,w)\),求每次加入后是否能选出一个边集使得点 \(\operatorname{deg}_{1\cdots n}\) 均为奇数,如果可以,最小化该边集的最大权值并输出。

\(1\leq n\leq 10^5\)\(1\leq m\leq 3\times 10^5\)

这道题首先需要分析一下性质,首先选出来的边集中若存在环,我们显然可以把环删掉以得到不劣的答案,所以构成的每个联通块都是树。考虑一棵什么样的树可以满足性质,不难发现除了根有奇数个儿子,其它点都只有偶数个儿子,这启发我们通过对一棵任意树进行调整来控制根和非根的叶子数量:若一个点有偶数个儿子,则保留它与父亲的边,否则删掉,使其成为子树的根节点。如果能完整经过这样的调整,新形成的若干棵树就应该是合法的。考虑从叶子节点来看,一个非根节点的子树为其自身加上偶数个儿子的子树,则其 size 是奇数;一个根节点是它本身加上奇数个 size 为奇数的儿子的子树,所以它的 size 是偶数;从另一个角度,每个点度数都是奇数,而总度数是偶数(每条边贡献 \(2\)),也要求其 size 是偶数。则一棵 size 是偶数的树,若其每个非根节点 size 都为奇数,则一定合法,否则割掉所有 size 的偶数的子树就合法了,于是联通块 size 是偶数即为每个点度数为奇数的充要条件。

那么可以得到,若一个联通块点数为偶数,我们就可以通过一些调整,删除一些联通块中的边使其满足每个点的度数都是奇数,至此便得到了这个问题静态版解决方法:按边权从小到达加入并查集,直到不存在 size 为奇数的联通块。

但是对于动态的问题,我们如果沿用上面的做法,那么不按边权顺序加边的后果是,可能一次修改会导致整张图的结构发生巨大变化,这不是用目前的简单做法可以维护的。所以我们需要挖掘性质,找到其它做法。首先考虑这样一件事情,一条边可能存在于边集中的时间是一个区间或不存在,由于答案是单调不升的,观察一下可以得到,第 \(i\) 条边可能产生贡献的时间为 \([i,j]\),其中 \(j\) 是最后一个满足 \(ans_j\ge val_i\) 的时刻。我们希望能得到这个区间,而答案又是单调不升的,所以我们把边按边权从小到大排序,并维护一个从前往后不退的指针,线段树分治到叶子节点 \([l,l]\) 之后,如果还没有满足不存在奇数大小联通块,则暴力推进指针并加入编号 \(<l\) 的边直到合法。那么 \([i,j]\)\(j\) 怎么求呢?发现我们每次推进指针并加入边 \(i\),其对应的 \(j\) 就是 \(l\),那么由于我们又是从后往前遍历的,则此时把 \(i\) 放到 \([i,l)\) 上就好了,即这是一个一边分治一边覆盖的过程,覆盖的位置是根据分治的过程来决定的。

总复杂度依然是两只 \(\log\)一份参考代码实现

同样是两只 \(\log\) 的复杂度,这题还可以用整体二分来解决,下面介绍一下整体二分的做法。

因为答案是单调的,我们用函数 \(\text{solve}(l,r,L,R)\) 表示 \(ans_{l\cdots r}\in [L,R]\),并且 \(i<l\)\(val<L\) 的边已经加入可撤销并查集,接下来考虑如何分治。

和普通整体二分一样地,我们计算出 \(m=\lfloor \frac{l+r}2 \rfloor\)\(ans_{mid}\) 的值 \(p\),并递归处理 \(\text{solve}(l,m-1,p,R),\text{solve}(m+1,r,L,p)\),那么现在的问题就在于如何求 \(ans_{m}\)

现在已经加入的边满足 \(i<l\)\(val<L\),那么还需要加入的边有:

  1. \(l\leq i\leq m\)\(val< L\) 的边,对答案无影响,直接加入;
  2. \(L\leq val\leq R\)\(i\leq m\) 的边,按 \(val\) 从小到大加入,直到满足条件,则此时的边权就是答案;

于是就可以得到 \(p\),再在递归之前恢复/加边到满足递归前置要求(\(i<l\)\(val<L\) 的边已经加入)再递归处理即可。

总复杂度两只 \(\log\),实测跑得比线段树分治略快,一份参考代码实现

posted @ 2022-01-26 14:05  摸鱼酱  阅读(1351)  评论(2编辑  收藏  举报