代码从一定会补正式挪到一定不补了
CF710F
根号个串一个自动机的做法,不够时暴力重构,精细一点确实可以 \(\Theta(n\sqrt n)\) 的查询而且常数比较小,每次来新串就重构最后一个不满的自动机,看看给长度还是个数开根号可能有不一样的效果
但是复杂度能到 \(\Theta(\rm{n\log n})\)
加入删除本质一样,只是权值赋成 \(\pm 1\),对已经有的串维护若干个自动机,加删串都建一个自动机,比较最新的自动机和前一个自动机串的数量/长度,如果前一个自动机的长度/数量小于当前的 \(\alpha\) 倍就合并两个自动机并重构 \(fail\) 树,同时维护点权
查询就是朴素的 \(\rm{AC}\) 自动机的操作,在树上跑并更新权值
不难发现按照数量合并的复杂度是每个串合并不超过 \(\log\) 次且每次查询在不超过 \(\log\) 个现存自动机上跑,所以复杂度是一个 \(\log\),但是注意每次合并自动机只能重构增量而不是完全重构
按照长度重构时可以避免整个问题,复杂度 \(\Theta(L\log L)\),表达式忽略 \(\alpha=2\) 时的常数部分
Luogu6177
强制在线的树分块一般有对 dfn 序分块;随机标记 \(\sqrt n\) 个点,每个点归到最近的被标记祖先所代表的块;看父亲节点在的块里面点的数量是不是不够 \(\sqrt n\) 个,是就加如当前点,否则新开一块
回到本题使用第二种方法,标记点不用随机,dfs 时检查子树中最远没有被加入块中的点距离当前点是不是超过 \(S\),是则新建一块
路径询问最后统计答案的方式是使用 std::bitset
,那么维护每个标记点到根的链上块和块之间的 std::bitset
,预处理时维护一个根链的栈,遇到被标记点暴力跑栈顶和当前点的这段链,那么到栈中元素的信息直接推即可
之后每次询问找到 \(\rm{LCA}\),最下面不属于完整链的部分直接跳父亲并向 bitset
中添加信息,完整块的直接合并 bitset
,跨过 \(\rm{LCA}\) 不属于一个完整块的部分也直接或
\(S=\sqrt n\) 的时候列式计算复杂度就是 \(\frac{nm}{\omega}\) 的,有博客提醒空间复杂度是 \(\frac{n^3}{S^2\omega}\),那么调大 \(S\) 来减小空间消耗是个不错的选择
Luogu5163
逆序操作则删边变加边所以暴力怎么做?暴力是不是加边!加边!加边!然后并查集查询
好像还真是这题暴力做法诶
得到每两个点被加入一个强连通分量的时间那么就可以线段树合并,来支持前 \(k\) 大和查询,由于基于原强连通分量而形成的新强连通分量是 \(\Theta(m)\) 级别的,那么也就是每个边被“缩起来” 的最早时间
不知道怎么就 一脸的整体二分,考虑一下整体二分的过程,先加入 \(\rm{[l,mid]}\) 的边,如果一条边的两个端点处于一个强连通分量中则合并时间在 \(\rm{[l,mid]}\) 中
这时候显然先递归右边可以让信息更完整地被利用,不同于传统的缩点题,要使用可撤销并查集维护每个点现在属于的强连通分量,之所以可撤销是因为在递归左边的时候要清除
这里看起来复杂度非常容易假,比如 \(tarjan\) 只能遍历有边的点等
CF566C
考察一个数列上的 \(f(x)\),不难发现每个点的贡献都是关于 \(x\) 的下凸函数,一个序列在树上就是一条路径,又因为下凸函数和下凸函数还是下凸函数,那么树上的贡献和也是关于点的下凸函数
这其实对应着贡献总和是朝向一个方向下降的,且方向最多有 \(1\) 个,至此可以从 \(1\) 开始比较自己的代价和所有儿子的代价,朝向代价小于自己的儿子走
时间复杂度 \(\Theta(n^2)\) 考虑优化:
走儿子可以使用点分树优化,每次走向值小的那个联通块,走儿子的操作次数降至 \(\log\) 次,但是对于多度数点还是束手无策
考察原贡献函数:
当其挪向它的子树 \(y\) 时新函数为:
对其求导数也就是(后面由于自变量减小了,导数前面要加负号)
由于将所有点上的贡献定义成了连续的函数,所以根据导数小于 \(0\) 则原函数减少,所以找到小于 \(0\) 的儿子继续递归即可
如何找到导数 \(<0\) 的儿子的方法比较简单,如果对当前分治中心所有儿子求子树导数和,减去两倍的某个儿子子树导数和就是其增量导数的值,这个方法还是挺巧妙的
本题主要是将贡献强设成函数并用导数观察增减性的方式是不容易想到的,奇怪的是,为什么很多博主都写了 容易想到 呢?
CTSC2018 暴力写挂
经典题,看起来就比通道简单不少,考虑化式子:
在第一棵树上求出来进行边分治,将分治结构存下来,这个也就是边分树
由于边分治结构优美,每条边只连接两个联通块,所以边分树也就是一个二叉树,区分左右子树的方式也就是在 \(x\) 这侧就是左子树,\(y\) 侧联通块就是右子树,也可以理解成一个 \(0/1\ trie\),每位 \(0/1\) 表示在左还是右儿子 ,那么不难发现边分树叶子是 \(n\) 个原树上的点
设当前分治边为 \((x,y)\),让边分树每个节点维护 \(vl,vr\) 表示左右子树里面 \(dis(x,t)+dep[t]\) 的最大值
\(n\) 个节点一开始将信息拉成一条单链,注意边分树每层维护的信息是不同的,所以并不能 push_up
来用子节点更新父亲的,但是边分树可以合并,对于两个单链合并的时候使用 \(vl_x+vr_y\) 或者 \(vl_y+vr_x\) 来更新答案,不难发现这样每个点只会在 \(\rm{LCA}\) 处合并一次
本题剩下的工作是在枚举第二棵树上的每个点作为第二个树上的 \(\rm{LCA}\),同时合并边分树即可
在 WC2018 通道 一题中使用边分树合并也要简洁的多,那题需要在每层维护不同的点权和第二棵树上的直径,而端点点权+路径长度有别于传统定义的直径也能满足传统合并方式
另外的 CF757F 一题是边分树可持久化的代表作,然而这又有什么难的呢?树上维护的信息是点的数量和到该点长度就行了,查询跳儿子,是右儿子就加上左边的贡献
细心的你一定发现了这就是开店,确实,这就是点分树边分树本质相同的体现了!
WC2014 紫荆花之恋
出于某种角度考虑,建议枪毙根号做法发明人,不写 \(\rm poly\log\) 感觉这题就失去了很大的 \(\rm DS\) 意义?
当然,我没写过点分治重构所以来写这题,替罪羊是不写的,万万不能写的
其实就是动态维护点分树,如果子树大小大于阀值就重构这个子树,同时在点分树每个点维护一个平衡树,将式子拆成 \(dis_x-r_x\le r_y-dis_y\) 即可
重构性点分我的做法是记录每个点在点分树上的深度来判断这个点在不在联通块里面,这样子需要注新加入的点需要直接赋成 dep[fa]+1
传统做法需要书写一个平衡树,重构时进行垃圾回收等操作,确实说不上简洁,但是近三年一种做法迅速占领了 UOJ 提交速度榜首页,代码长度大多不到 3kb
其实就是将平衡树换成两个 std:vector
即可,往其中之一加,大小够根号就排序并和另一个归并,让它全放到另一个中
在查询的时候,有序的里面二分,无序的暴力跑即可
本做法截止目前大概占据了 UOJ 最快排行榜第一页的多数,这大概也就是时代的发展吧
Luogu5439
两棵树题被时代潮流后浪拍走了,好耶!
在第一棵树上点分治,统计答案方式仍然是容斥而非合并子树,对于一条跨过当前分治中心但并不以分治中心为端点的路径,将当前连通块里面的点在第二棵树上建立虚树
考察贡献形式不难发现对于第二棵树上一个特定的 \(\rm LCA\) 而言,贡献是其深度乘子树中点对的 \(siz\) 乘积之和
如果认为这里的 \(siz\) 表示在分治连通块里面的子树大小就输干净了,因为连通块不是完全扩展过的,本质上是以 \(t\) 为根时的子树大小
求 \(siz\) 大可使用最基础的换根 \(dp\) 做:现在原树上 \(dfs\) 一遍求出以 \(1\) 为根时的子树大小,换成的根在子树里面时新的子树大小就是 \(n-siz[\rm sideson]\),而 \(\rm sideson\) 在点分治过程中被记录了,就是 \(dfs\) 时的父亲,很难不是水到渠成
那么虚树上的计算是最基础的 \(\rm dp\),同时以分治重心为端点的路径也可以直接计算,毋需多言
最后考虑虚树构建的过程,尝试优化每次排序的过程:归并已经排好序的子连通块。做法是将子连通块分治归并,每个子连通块的每个元素最多被归并 \(\log \rm deg\) 次
如果使用 $ O(1)\ \rm LCA$,复杂度是 \(\Theta(n\log n)\) 的,解释是刻画成一个 \(f(n)=n\log_c n\log_2c\),使用换底公式得到
注意这种快速建立虚树的方法也被上面写过的 通道 和 暴力写挂 所使用
CF526G
不难找到一个 \(\Theta(n\sum y)\) 的暴力,先找到一个跨过 \(x\) 的带权直径(可以用树形 \(\rm DP\) ),即每次找到一条带权直径之后将直径上的边的边权置 \(0\),每个询问重复 \(y\) 次
不难发现一个跨过 \(x\) 的最长链的一个端点必然是直径的一个端点,证明仍然可以使用传统直径性质来做,那么维护两个直径端点为根的树
那么取出 \(y\) 条链等价于取出根和另外的 \(2y-1\) 个叶子,最大化叶子到根的路径并,而这个问题等价于取出树上最长的 \(2y-1\) 个边无交长链,求长度和
求前 \(2y-1\) 的长链的长度和可以直接带权长剖来做,注意链顶到父亲的边权也要计算,也要求长链长度前缀和
但是这样的取用方法可能不保证过 \(x\),那么找到 \(x\) 子树里面最深的点进行替换
第一种替换方式是取出这个点所在的长链并替换掉第 \(2y-1\) 长的链,可以通过维护每个点最深儿子(也就是所在的长链)以及每个长链的长度实现
另一种是让最深儿子向上跳,找到第一个已经被取用的长链,删掉该链后半截,接上这半截,实现可以倍增,不难发现如果记录每个点所在长链长度的排名,一个点的根链上该排名是递减的
简述题解专栏
LOJ6022
仔细考察每个 access
操作实际上是是对若干实链的底部的子树加一,这里的子树因为存在换根操作所以无意义增加代码量
因为题设了换根要 access
正好和代码里面更新了这段链,区间操作均使用线段树维护区间和,回答询问直接区间和除大小即可
Luogu5360
处理前后缀生成树,询问时建立 \([n,1]\sim [n,n]\) 在后缀生成树上的虚树 和 \([1,1]\sim[1,n]\) 在前缀树上的虚树,虚树边权是路径上边权最值
这样子点数简化到了最多 \(4n\) 那么对着跑 \(Kruskal\) 就行了,如果预处理出来虚树再归并可能有更好的效果,否则时间复杂度 \(\Theta(n(m+q)\log n)\)
Luogu7422
原来朴素无向图上整的都叫广义圆方树
直接建立广义圆方树,必经就是选定 \(A\) 之后只能选子树里的点,互不影响也就是切掉之后选择不同子树里面的点
对同种颜色对应的点建立虚树,虚树上出现不同颜色的点直接 \(dp\),对于一条边上的异色点,贡献是虚树中点权大小因为只有这一个子树能选而且 \(K\ge 1\)
LOJ6515
熟悉的回忆!离线直接线段树分治暴力扫复杂度就很合理
在线做法是维护一个分界点,计算分界点向左进行的背包和向右进行的背包,删头删尾挪指针,加头加尾直接扩展,把分界点一边删空了就取另一侧的中点作为分界点重构,询问直接合并信息,一遍枚举固定值,另一边单调队列
直观感受复杂度就是 \(\Theta(p\sum_i \lfloor\frac{n}{2^i}\rfloor)\),而这就是 \(\Theta(np)\),证明考虑第二次重构左侧 \(\frac 14\) 时可视作在右侧铺了一个 \(\frac 14\),类推发现到最后也铺不满
和联赛模拟中【棋盘】一题做法是一致的
LOJ3277
按照高度存星星和楼房,维护每个横坐标放一个星星的代价,初值全是 \(0\) ,依次处理每个高度的星星,如果当前横坐标的代价超过了删掉的代价,不难发现后面再放星星时不会使得这个位置代价减小,所以直接删掉,否则先加入坐标代价
考虑加入星星给维护横坐标代价带来了什么影响:找到这个大于 \(h_x\) 的最靠近之的楼房,如果再在里面放星星就就要删除这个星星,是一个区间加法。如何找到最靠近之还最低的楼房是容易的,单调栈并查集都能做
Luogu3273
传统的左偏树打标记方式不能支持根链点权和查询,但是如果只有堆顶有标记那么大可不必担心,所以使用启发式合来做:每次合并堆的时候将大小较小的堆的标记暴力下放,注意减去大堆的标记值
所以删除节点可以直接得到数值,使用 multiset
维护每个大根堆堆顶即可,另附左偏树模板代码
Code Display
int ls[N],rs[N],fa[N],val[N],dis[N];
inline void push_up(int x){
if(dis[rs[x]]>dis[ls[x]]) swap(ls[x],rs[x]);
if(dis[x]!=dis[rs[x]]+1){
dis[x]=dis[rs[x]]+1;
if(fa[x]) push_up(fa[x]);
} return ;
}
inline int merge(int x,int y){
if(!x||!y) return x+y;
if(val[x]<val[y]) swap(x,y);
fa[rs[x]]=0;
fa[rs[x]=merge(rs[x],y)]=x;
return push_up(x),fa[x]=0,x;
}