树链剖分/重链剖分
<h2 id="什么是树链剖分重链剖分">什么是树链剖分/重链剖分</h2><p>我们可以弄一道例题来看看:<br>
现在给定一棵 <span class="math inline">\(n(1 \le n \le 10^5)\)</span> 节点的树,每个节点上有一个数值,现在你可以进行 $m ( 1 \le m \le 10^5) $ 次操作。格式如下:</p>
<ul>
<li><code>1 x z</code> 表示将 <span class="math inline">\(x\)</span> 到 <span class="math inline">\(y\)</span> 最短路径上的节点值加上 <span class="math inline">\(z\)</span> 。</li>
<li><code>2 x y</code> 表示树求 <span class="math inline">\(x\)</span> 到 <span class="math inline">\(y\)</span> 最短路径上的权值和。</li>
<li><code>3 x y</code> 将子树 <span class="math inline">\(x\)</span> 下的每个节点的权值加上 <span class="math inline">\(y\)</span> 。</li>
<li><code>4 x</code> 求子树 <span class="math inline">\(x\)</span> 下每个节点的权值之和。<br>
怎么写呢?当然,你需要前置知识线段树。接下来,且听我慢慢道来。</li>
</ul>
<h2 id="先从构造一个完美的剖分数组开始">先从构造一个完美的剖分数组开始</h2>
<p>我们知道,一棵树可以通过DFS构造成一个序列,其中对于任意一棵子树(包括根节点),我们把他的节点个数叫做 <span class="math inline">\(siz\)</span> 。子树 <span class="math inline">\(x\)</span> 的大小就是 <span class="math inline">\(siz_x\)</span> 。那这对剖分有什么帮助呢?对于一棵树的DFS序举例如下:</p>
<pre><code> 1
/ \
2 3
/
4
\
5
</code></pre>
<p>那么DFS后,序列如下:</p>
<pre><code>1 2 4 5 3
</code></pre>
<p>可以看到,子树 <span class="math inline">\(x\)</span> 到 <span class="math inline">\(x+siz_x\)</span> ,就是整个子树 <span class="math inline">\(x\)</span> 。我们暂时叫这个数组为剖分数组 <span class="math inline">\(tag\)</span>。但这样并不是最优的,有的的时候无法保证前面阐述的规律,这个时候怎么办呢?我们可以分析一下,在这棵树中,<span class="math inline">\(1\)</span> 有两个孩子,分别是 <span class="math inline">\(2\)</span> 和 <span class="math inline">\(3\)</span> ,而且 <span class="math inline">\(2\)</span> 的子树大小比 <span class="math inline">\(3\)</span> 大,那么我们就规定 <span class="math inline">\(2\)</span> 是 <span class="math inline">\(1\)</span> 的 <strong>重孩子</strong> 。从 <span class="math inline">\(1\)</span> 到 <span class="math inline">\(2\)</span> 的这条边我们叫做<strong>重链</strong>。可以得知,对于任意一个非叶子节点,都有其独有的重孩子,我们就可以根据重孩子来构造DFS的先后,因为我们优先遍历一棵树的所有重孩子,所以所有重孩子一定是连在一起的,上面就是一个说明。那优先从重孩子开始还有什么好处呢?我们知道,如果从轻孩子开始遍历去构造这个数组,那么时间复杂度可能会达到 <span class="math inline">\(O(n)\)</span>(见后文)。但是如果从重孩子开始的话,就不一样了,我们可以证明其时间复杂度可以来到 <span class="math inline">\(O(\log_2n)\)</span> 的单次询问复杂度,证明如下:<br>
对于节点 <span class="math inline">\(u\)</span> ,其重孩子 <span class="math inline">\(v\)</span> 必然是所有子节点中<strong>子树最大</strong>的那个,即为:</p>
<p></p><div class="math display">\[size(v) \ge size(w) \forall w \in children(u)
\]</div><p></p><p>又因为重孩子的子树大小 <span class="math inline">\(\ge\)</span> 其他所有子节点的子树大小,因此对于任意轻孩子 <span class="math inline">\(w\)</span> 有:</p>
<p></p><div class="math display">\[size(w)\le size(v)
\]</div><p></p><p>又又因为所有节点子树个数+1等于其父亲节点的包含的节点个数(有点拗口),即为:</p>
<p></p><div class="math display">\[size(v)+\sum_{轻孩子w} size(w)+1 = size(u)
\]</div><p></p><p>根据上面的第二,三条定理,解不等式方程,可以得到对于:</p>
<p></p><div class="math display">\[\sum_{轻孩子w}size(w) \le size(u)-size(v)-1
\]</div><p></p><p>所以可以知道:如果某个轻孩子节点<span class="math inline">\(w\)</span> 的子树大小大于 <span class="math inline">\(\frac{1}{2} size(u)\)</span> , 则以上定理不成立,违背重孩子的定义,所以,必然有对于任意 <span class="math inline">\(w\)</span>:</p>
<p></p><div class="math display">\[size(w)\le \frac{size(u)}{2}
\]</div><p></p><p>因此,从根节点到任意节点得到路径上,最多只有 <span class="math inline">\(O(\log n)\)</span> 条轻边(因为每次碰到轻边子树大小随见到原来的 <span class="math inline">\(\frac{1}{2}\)</span> ,所以至多 <span class="math inline">\(\log^2n\)</span> 就会到达根节点)<br>
而又因为重孩子是连续的一段区间,所以一次线段树修改就可以,再加上轻边修改是每次 <span class="math inline">\(O(\log n)\)</span>,最多 <span class="math inline">\(\log n\)</span> 条,所以是 <span class="math inline">\(O(\log n)\)</span> 时间复杂度,证毕。<br>
至此,我们的树链剖分的关键部分就已经完成。<br>
Code如下:</p>
<pre><code class="language-cpp">void dfs1(int root, int fa) {
fath = fa;
siz = 1;
dep = dep + 1;
for(auto i : tree) {
if(i == fa) continue;
dfs1(i, root);
siz += siz;
if(siz > siz])
son = i;
}
}
void dfs2(int root, int tp) {
top = tp;
dfn = ++cnt;
rev = root;
if(!son) return;
dfs2(son, tp);
for(auto i : tree) {
if(i == fath || i == son) continue;
dfs2(i, i);
}
}
</code></pre>
<p>其中 <code>siz</code> 是表示大小,<code>top</code>表示链头(见后文),<code>dfn</code> 表示对于 <span class="math inline">\(root\)</span> 节点,在剖分数组里的位置。<code>rev</code> 是反的 <code>dfn</code>,<code>cnt</code> 全局计数器,<code>son</code> 记录重孩子编号,<code>dep</code> 表示深度。</p>
<h2 id="利用链表的思想完成perfect的查询修改">利用链表的思想,完成perfect的查询/修改</h2>
<p>我们已经知道,怎么分割一个有效的剖分数组。就是不会用,怎么办呢?<br>
还是拿出一棵树:</p>
<pre><code> 1
/ \
2 5
/ \ \
3 7 6
\ /
4 8
/ \
910
</code></pre>
<p>构造出来的序列如下(<code>-</code>表示重链,<code> </code> 表示轻边)</p>
<pre><code>1-2-3-4-9 10 7 5-6-8
</code></pre>
<p>现在我们将 <span class="math inline">\(10\)</span> 到 <span class="math inline">\(8\)</span> 之间的,过程如下:</p>
<ul>
<li>先使所有重链的链头为深度最小的节点,比如 <span class="math inline">\(8,6,5\)</span> 的链头都是 <span class="math inline">\(5\)</span>,<span class="math inline">\(9,4,3,2,1\)</span> 的链头都是 <span class="math inline">\(1\)</span></li>
<li>令所有轻边的链头就是其本身<br>
那么修改的时候,每次选择深度大的条跳跃,直到两者链头相同。那么此时只需要修改当前 <span class="math inline">\(\min{u,v}\)</span> 到 <span class="math inline">\(\max{u,v}\)</span> (询问/操作的两个端点)就可以了!。<br>
到这里我们也可以知道了吧,按照轻边来的话时间复杂度是 <span class="math inline">\(O(n)\)</span> 的哦。千万小心!<br>
因为查询和修改代码雷同,就不详细撰述了。</li>
</ul>
<h2 id="code-for-luogu-p3384">Code For Luogu P3384</h2>
<pre><code class="language-cpp">#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e5+100;
int num,fath,top,son,dep,siz;
int lay,w,tag1,tag2,cnt,n,m,r,p;
vector<vector<int> > tree;
inline void dfs1(int root,int fa){
fath=fa;
siz=1;
dep=dep+1;
for(auto i : tree){
if(i == fa)continue;
dfs1(i,root);
siz+=siz;
if(siz>siz])
son=i;
}
}
inline void dfs2(int root,int Top){
tag1=++cnt;
tag2=root;
top=Top;
if(son){
dfs2(son,Top);
for(auto i : tree){
if(i == son || i==fath)continue;
dfs2(i,i);
}
}
}
void pushup(int root){
w=(w+w)%p;
}
void maketag(int root,int l,int r,int lz){
lay=(lay+lz)%p;
w=(w+(r-l+1)*lz)%p;
}
void pushdn(int root,int l,int r){
if(lay==0 || l==r) return;
int mid=(l+r)>>1;
maketag(root<<1,l,mid,lay);
maketag(root<<1|1,mid+1,r,lay);
lay=0;
}
void build(int root,int l,int r){
if(l==r){
w=num]%p;
return ;
}
int mid=(l+r)>>1;
build(root<<1,l,mid);
build(root<<1|1,mid+1,r);
pushup(root);
}
int query(int root,int l,int r,int L,int R){
if(R<l || L>r) return 0;
if(L<=l && r<=R) return w;
pushdn(root,l,r);
int mid=(l+r)>>1;
return (query(root<<1,l,mid,L,R)+query(root<<1|1,mid+1,r,L,R))%p;
}
void update(int root,int l,int r,int L,int R,int val){
if(R<l || L>r) return;
if(L<=l && r<=R){
maketag(root,l,r,val);
return;
}
pushdn(root,l,r);
int mid=(l+r)>>1;
update(root<<1,l,mid,L,R,val);
update(root<<1|1,mid+1,r,L,R,val);
pushup(root);
}
void udp(int x,int y,int val){
while(top!=top){
if(dep]<dep]) swap(x,y);
update(1,1,n,tag1],tag1,val);
x=fath];
}
if(dep>dep) swap(x,y);
update(1,1,n,tag1,tag1,val);
}
int qry(int x,int y){
int ans=0;
while(top!=top){
if(dep]<dep]) swap(x,y);
ans=(ans+query(1,1,n,tag1],tag1))%p;
x=fath];
}
if(dep>dep) swap(x,y);
ans=(ans+query(1,1,n,tag1,tag1))%p;
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>r>>p;
tree.resize(n+1);
for(int i=1;i<=n;i++) cin>>num;
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
tree.push_back(y);
tree.push_back(x);
}
dfs1(r,0);
dfs2(r,r);
build(1,1,n);
for(int i=1;i<=m;i++){
int op,x,y,z;
cin>>op;
if(op==1){
cin>>x>>y>>z;
udp(x,y,z);
}
else if(op==2){
cin>>x>>y;
cout<<qry(x,y)<<endl;
}
else if(op==3){
cin>>x>>z;
update(1,1,n,tag1,tag1+siz-1,z);
}
else if(op==4){
cin>>x;
cout<<query(1,1,n,tag1,tag1+siz-1)<<endl;
}
}
return 0;
}
</code></pre>
<p>线段树部分请自学哦!</p>
<h2 id="完美的习题">完美的习题</h2>
<ul>
<li>P3384 【模板】重链剖分/树链剖分</li>
<li>P3258 松鼠的新家</li>
</ul><br><br>
来源:https://www.cnblogs.com/CheeseFunction/p/18847884
頁:
[1]