動態樹概述
一、適用問題
動態樹主要用於解決操作中帶有加邊、刪邊、換根的一系列問題,即樹結構發生變化的問題,理論上來說,樹鏈剖分的問題都能用 進行解決。
二、函數解析
本質是上對樹進行實鏈剖分,實鏈剖分的意思就是將一棵樹分成多條鏈,鏈中的邊稱爲實邊,鏈與鏈之間的邊則稱爲虛邊,每條鏈都是一個 ,在 中進行中序遍歷即可還原原來的樹結構。而 就是不斷進行虛邊、實邊轉換的一個算法。
中一共有 、、、、、、、、、、、 等函數,下面大致介紹一下每個函數的具體作用以及一些坑點,更多的是提綱挈領的作用,想要從最基礎的地方開始學的話,推薦 oiwiki。
簡單函數(僅操作單個 的函數)
- :清除一個點的信息,如父親、左右兒子、標記、維護信息等信息。
- :由左右兒子的信息更新父節點的信息,與線段樹的 函數沒有太大差別。
- :將當前節點的標記下放到兒子節點,如加、減、翻轉等標記。
- :一直遞歸到根節點,然後把標記信息不斷下放,沒有涉及任何虛實邊的轉換。
void update(int p){ //遞歸地從上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
- :將當前節點向上旋轉一層,可以自己模擬一下。此處改變了 的內部結構,即子節點發生了改變,因此需要進行 ,但是仍然沒有進行任何虛實邊的轉換。
inline void rotate(int x){ //將x向上旋轉一層的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
- :將當前點旋轉到 的根節點, 到根節點作用在於不需要在向上進行更新。比如你現在要修改 的點權,但是每個節點還要維護子樹 的信息,如果 不是其所在 的根節點,那麼修改 的點權勢必影響到其祖先節點的 信息,因此需要將 旋轉爲其所在 的根後再進行單點修改。注意 函數也沒有進行實邊和虛邊的轉換。
inline void splay(int x){ //把x旋轉到當前splay的根
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
以上函數都屬於 函數中的簡單函數,因爲這些函數都只是在單個 中進行操作,不涉及任何虛實邊的轉換。
複雜函數(涉及多個 的操作,進行虛實邊轉換)
- :將點 到根的路徑經過的點放入同一個 中,且這個 中僅包含從 到根路徑上經過的點。具體操作即是將 點不斷轉成其所在 的根,然後再進行虛實邊轉換一直到根。此處 函數有返回值,返回值爲最後構成的 的根節點。
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
int p; //每次改變右兒子的值,因爲整棵樹是中序遍歷,放入右兒子才能保證先遍歷父親再遍歷兒子
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
- :換根操作,將點 變成當前樹的根。具體過程爲先 點 ,然後再將點 旋轉爲其 所在根,然後將所有節點的左右兒子翻轉即可。
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
- :從樹中拎出 的路徑,返回該路徑的 根節點,可以查詢路徑最大值、點權和、邊權和等信息。
inline int split(int x,int y){
makeRoot(x);
return access(y);
}
- :即返回點 所在樹的根節點,不是所在 中的根節點,用於判斷兩點是否連通。
inline int find(int p){ //找到x所在樹的根節點編號
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
- :連接樹中 、 兩點之間的邊,無邊變虛邊,如果題目中沒有保證操作一定合法,則需要自行判斷 、 是否已經連通。
inline void link(int x,int y){ //在x、y兩點間連一條邊,連接了虛邊
if (find(x) != find(y)) makeRoot(x), f[x] = y;
}
- :斷開樹中 、 兩點之間的實邊,兩個點同時斷開即可。
inline void cut(int x,int p){ //把x、y兩點間邊刪掉,此處刪除的是實邊,注意實邊和虛邊的區別
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
三、具體細節
單點修改
由於 中維護了多個 ,因此單點修改需要把該點修改的信息不斷上傳,所以我們需要先將點 旋轉到 的根或者整棵樹的根,然後再進行單點修改。
如果題中只需要維護實鏈信息,則只需要旋轉到 的根,如果需要同時維護實鏈和虛鏈信息,即整棵子樹的信息的話,則需要令該點爲樹根,即調用 函數。
維護邊權
由於 是不斷地進行虛邊、實邊轉換,因此沒有固定的邊結構,所以直接維護邊權十分困難,因此我們將邊轉成點,邊 成爲一個點 ,、 即可。
維護子樹信息
普通 只能維護具有可減性的子樹信息,比如子樹大小,子樹貢獻等,而子樹 、 等問題則不具有可減性,難以維護。
維護子樹信息主要在於維護實邊信息和虛邊信息,而進行實虛轉換的函數只有 、、 三個函數,只需要在該三個函數進行一定的修改即可,下面習題中包含了該問題可供參考。
動態樹習題
1. [國家集訓隊] Tree II(模板題)
題意:
個點一棵樹,支持四種操作。
- ,將 到 的路徑上的點的權值都加上 。
- ,將樹中原有的邊 刪除,加入一條新邊 ,保證操作完之後仍然是一顆樹。
- \ ,將 到 的路徑上的點的權值都乘上 。
- / ,詢問 到 的路徑上的點的權值和,答案 。
思路:
三個涉及到路徑的操作,都是先把 變成樹根,然後 ,即拉起一條 到 的路徑,使得 到 路徑上的點都在一個 中,然後獲得這個 的根節點,即可對根節點打標記完成。
刪邊則是令 爲根,拉起 到 的路徑,將 旋轉成 的根,然後兒子與父親雙向斷開聯繫。加邊則是令 爲根,然後使 的父親變成 。
總結:
這題應該算是 的模板題,涉及的操作都是基礎操作,沒有太多思維上的難點。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
#define int long long
const int N = 100010;
const int mod = 51061;
int n, q, u, v, c;
char op;
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], sum[N], val[N], siz[N], rev[N], add[N], mul[N];
inline void clear(int p){ //清除這個點的信息
ch[p][0] = ch[p][1] = f[p] = siz[p] = val[p] = sum[p] = rev[p] = add[p] = 0;
mul[p] = 1;
}
inline int isRoot(int p){
clear(0);
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
clear(0);
siz[p] = (siz[ls] + 1 + siz[rs]) % mod;
sum[p] = (sum[ls] + val[p] + sum[rs]) % mod;
}
inline void pushDown(int p){
clear(0);
if(mul[p] != 1){ //乘法
if(ls){ //左兒子
mul[ls] = (mul[ls] * mul[p]) % mod;
val[ls] = (val[ls] * mul[p]) % mod;
sum[ls] = (sum[ls] * mul[p]) % mod;
add[ls] = (add[ls] * mul[p]) % mod;
}
if(rs){ //右兒子
mul[rs] = (mul[rs] * mul[p]) % mod;
val[rs] = (val[rs] * mul[p]) % mod;
sum[rs] = (sum[rs] * mul[p]) % mod;
add[rs] = (add[rs] * mul[p]) % mod;
}
mul[p] = 1;
}
if(add[p]){
if(ls){
add[ls] = (add[ls] + add[p]) % mod;
val[ls] = (val[ls] + add[p]) % mod;
sum[ls] = (sum[ls] + add[p] * siz[ls] % mod) % mod;
}
if(rs){
add[rs] = (add[rs] + add[p]) % mod;
val[rs] = (val[rs] + add[p]) % mod;
sum[rs] = (sum[rs] + add[p] * siz[rs] % mod) % mod;
}
add[p] = 0;
}
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //遞歸地從上到下pushDown信息
//沒有將實邊變成虛邊
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //將x向上旋轉一層的操作
//沒有將實邊變成虛邊
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋轉到當前splay的根
//沒有將實邊變成虛邊
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
//進行了邊的虛實變換
int p; //每次改變右兒子的值,因爲整棵樹是中序遍歷,放入右兒子才能保證先遍歷父親再遍歷兒子
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y兩點間連一條邊,連接了虛邊
if (find(x) != find(y)) makeRoot(x), f[x] = y;
}
inline void cut(int x,int p){ //把x、y兩點間邊刪掉,此處刪除的是實邊,注意實邊和虛邊的區別
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
inline int find(int p){ //找到x所在樹的根節點編號
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
//中序遍歷即可還原樹結構
void print(int p){
if(!p) return;
pushDown(p);
print(ls);
printf("%lld ",p);
print(rs);
}
}st;
signed main() {
scanf("%lld%lld", &n, &q);
for (int i = 1; i <= n; i++) st.val[i] = 1;
for (int i = 1; i < n; i++) {
scanf("%lld%lld", &u, &v);
st.link(u,v);
}
while (q--) {
scanf(" %c%lld%lld", &op, &u, &v);
if (op == '+') { //+ u v c, u到v的路徑上的點權值+c
scanf("%lld", &c);
//u變成樹根,拉起v到u的鏈,把v旋到splay的根
st.makeRoot(u); v = st.access(v);
st.val[v] = (st.val[v] + c) % mod;
st.sum[v] = (st.sum[v] + st.siz[v] * c % mod) % mod;
st.add[v] = (st.add[v] + c) % mod;
}
if (op == '-') { //- u1 v1 u2 v2, 刪除(u1,v1), 加上(u2,v2)
st.cut(u,v);
scanf("%lld%lld", &u, &v);
st.link(u,v);
}
if (op == '*') { //* u v c, u到v的路徑乘上c
scanf("%lld", &c);
st.makeRoot(u); v = st.access(v);
st.val[v] = st.val[v] * c % mod;
st.sum[v] = st.sum[v] * c % mod;
st.mul[v] = st.mul[v] * c % mod;
}
if (op == '/'){ //u v, 詢問u到v路徑權值和
st.makeRoot(u); v = st.access(v);
printf("%lld\n", st.sum[v]);
}
}
return 0;
}
2. Query on a tree
題意:
個點一棵樹,支持兩種操作。
- ,將第 條邊的邊權改爲 。
- ,查詢樹中點 到點 的路徑中的邊權最大值。
思路:
邊權 ,需要對於每一條邊建一個節點,即樹中一共有 個節點,每個邊節點與上下兩個節點連邊。
建邊需要先確定每個節點的邊權之後再進行 ,因爲點修改會對該節點的祖先節點產生影響,需要將該點旋至 端點後才能進行修改。
總結:
總結一下構建 構建的關鍵,構建 需要先對各個頂點賦值然後再進行 操作,若是 之後再賦值相當於點修改,而點修改需要將點旋爲 根之後才能進行更改。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 20000+10;
int n,val[N];
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], maxn[N], val[N], siz[N], rev[N];
inline void clear(int p){ //清除這個點的信息
ch[p][0] = ch[p][1] = f[p] = siz[p] = val[p] = maxn[p] = 0;
}
inline int isRoot(int p){
clear(0);
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
clear(0);
siz[p] = siz[ls] + 1 + siz[rs];
maxn[p] = max(val[p],max(maxn[ls],maxn[rs]));
}
inline void pushDown(int p){
clear(0);
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //遞歸地從上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //將x向上旋轉一層的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋轉到當前splay的根
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
int p;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y兩點間連一條邊
makeRoot(x), f[x] = y; //dfs建樹, 每條邊都是有效的, 因此不需要判斷是否有效
}
}st;
int main(){
int _; scanf("%d",&_);
while(_--){
scanf("%d",&n);
rep(i,0,2*n) st.clear(i);
rep(i,1,n-1){
int a,b,c; scanf("%d%d%d",&a,&b,&c);
st.val[i+n] = c;
st.link(a,i+n);
st.link(i+n,b);
}
while(1){
char s[20]; scanf("%s",s);
if(s[0] == 'D') break;
else if(s[0] == 'C'){
int a,b; scanf("%d%d",&a,&b);
st.splay(a+n); //先轉爲splay根節點
st.val[a+n] = b;
}
else{
int a,b; scanf("%d%d",&a,&b);
st.makeRoot(a);
b = st.access(b);
printf("%d\n",st.maxn[b]);
}
}
}
return 0;
}
3. Can you answer these queries VII
題意:
個點一棵樹,每個節點有一個值,支持兩種操作。
- ,查詢樹中點 到點 的路徑中最大連續和
- ,將樹中點 到點 路徑中所有點的值改爲
思路:
對每一個點維護一個 、、 表示點 子樹中左連續的最大值、右連續的最大值以及整棵子樹中的最大連續值。
需要注意一點,交換左右兒子的時候,還需要把每個節點的 和 進行交換,其餘細節見代碼。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const ll inf = 1e9+100;
const int N = 1e5+10;
int n,Q;
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N];
ll maxn[N], sum[N], lc[N], rc[N], val[N], siz[N], lazy[N];
bool rev[N];
inline void clear(int p){ //清除這個點的信息
ch[p][0] = ch[p][1] = f[p] = val[p] = maxn[p] = lc[p] = sum[p] = rc[p] = siz[p] = 0;
}
inline int isRoot(int p){
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
siz[p] = siz[ls] + 1 + siz[rs];
sum[p] = val[p] + sum[ls] + sum[rs]; //ls、rs可能爲0
maxn[p] = max(maxn[ls],max(maxn[rs],rc[ls]+lc[rs]+val[p]));
lc[p] = max(lc[ls],sum[ls]+val[p]+lc[rs]);
rc[p] = max(rc[rs],sum[rs]+val[p]+rc[ls]);
}
inline void pushDown(int p){
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
swap(lc[ls],rc[ls]); swap(lc[rs],rc[rs]); //交換左右兒子時還要交換左右連續最大值
rev[p] = 0;
}
if(lazy[p] != -inf){
if(ls){
sum[ls] = siz[ls]*lazy[p];
val[ls] = lazy[ls] = lazy[p];
lc[ls] = rc[ls] = maxn[ls] = lazy[p] > 0 ? sum[ls]:0;
}
if(rs){
sum[rs] = siz[rs]*lazy[p];
val[rs] = lazy[rs] = lazy[p];
lc[rs] = rc[rs] = maxn[rs] = lazy[p] > 0 ? sum[rs]:0;
}
lazy[p] = -inf;
}
}
void update(int p){ //遞歸地從上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //將x向上旋轉一層的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋轉到當前splay的根
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
int p;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p);
splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y兩點間連一條邊
makeRoot(x), f[x] = y; //dfs建樹, 每條邊都是有效的, 因此不需要判斷是否有效
}
}st;
int main(){
scanf("%d",&n);
rep(i,1,n){
scanf("%lld",&st.val[i]);
st.siz[i] = 1; st.sum[i] = st.val[i];
st.lazy[i] = -inf;
st.lc[i] = st.rc[i] = st.maxn[i] = st.val[i] > 0 ? st.val[i]:0;
}
rep(i,1,n-1){
int a,b; scanf("%d%d",&a,&b);
st.link(a,b);
}
scanf("%d",&Q);
while(Q--){
int op; scanf("%d",&op);
if(op == 1){ //a->b max
int a,b; scanf("%d%d",&a,&b);
st.makeRoot(a);
b = st.access(b);
printf("%lld\n",st.maxn[b]);
}
else{ //a->b to c
int a,b; ll c; scanf("%d%d%lld",&a,&b,&c);
st.makeRoot(a);
b = st.access(b);
st.val[b] = st.lazy[b] = c;
st.sum[b] = st.siz[b]*c;
st.lc[b] = st.rc[b] = st.maxn[b] = c > 0 ? st.sum[b]:0;
}
}
return 0;
}
4. [ZJOI2008] 樹的統計 Count
題意:
一棵樹上有 個節點,每個節點都有一個權值 。支持三種操作: :把結點 的權值改爲 ; :詢問從點 到點 的路徑上的節點的最大權值; :詢問從點 到點 的路徑上的節點的權值和。
思路:
單點查詢 + 路徑最大值 + 路徑 和,非常裸的題目,純當練習。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const int inf = 1e9+100;
const int N = 40000+10;
int n,Q,A[N],B[N];
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N];
int maxn[N], sum[N], val[N];
bool rev[N];
inline void clear(int p){ //清除這個點的信息
ch[p][0] = ch[p][1] = f[p] = val[p] = maxn[p] = 0;
}
inline int isRoot(int p){
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
sum[p] = val[p] + sum[ls] + sum[rs]; //ls、rs可能爲0
maxn[p] = max(val[p],max(maxn[ls],maxn[rs]));
}
inline void pushDown(int p){
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //遞歸地從上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //將x向上旋轉一層的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋轉到當前splay的根
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
int p;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p);
splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y兩點間連一條邊
makeRoot(x), f[x] = y; //dfs建樹, 每條邊都是有效的, 因此不需要判斷是否有效
}
}st;
int main(){
scanf("%d",&n);
st.maxn[0] = -inf;
rep(i,1,n-1) scanf("%d%d",&A[i],&B[i]);
rep(i,1,n){
int hp; scanf("%d",&hp);
st.val[i] = st.maxn[i] = st.sum[i] = hp;
}
rep(i,1,n-1) st.link(A[i],B[i]);
scanf("%d",&Q);
while(Q--){
char s[20]; int u,v;
scanf("%s%d%d",s,&u,&v);
if(s[0] == 'C'){
st.splay(u);
st.val[u] = v;
st.pushUp(u);
}
else if(s[1] == 'M'){
st.makeRoot(u);
v = st.access(v);
printf("%d\n",st.maxn[v]);
}
else{
st.makeRoot(u);
v = st.access(v);
printf("%d\n",st.sum[v]);
}
}
return 0;
}
5. 最小差值生成樹
題意:
個點, 條邊的一個無向圖,求邊權最大值與最小值的差值最小的生成樹。
思路:
關於這類特殊生成樹問題,一般考慮用 動態維護樹結構然後更新答案。
此題也可以這樣考慮。將邊按邊權從小到大排序,如果 兩點不連通,則加上該邊,如果 兩點連通,則將 路徑上邊權最小的邊去除,然後連上當前的邊。維護過程不斷更新最大值與最小值的差值,不斷取 即可。
因此只需要維護一個邊權 ,並且維護路徑最小值以及最小值點的編號,然後動態加邊刪邊即可。還需要對在樹中的邊打上標記,去除的時候刪去標記,用於查找整棵樹中的最小邊權。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 2e5+5e4+100;
const int M = 2e5+10;
int n, m, vis[N], pos = 1, num, ans = 1e9;
struct Edge{
int a,b,w;
bool operator < (Edge xx) const {
return w < xx.w;
}
}e[N];
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], val[N], minn[N], mpos[N], rev[N];
inline void clear(int p){ //清除這個點的信息
ch[p][0] = ch[p][1] = f[p] = val[p] = mpos[p] = minn[p] = 0;
}
inline int isRoot(int p){
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
minn[p] = val[p]; mpos[p] = p;
if(ls && minn[ls] < minn[p]) minn[p] = minn[ls], mpos[p] = mpos[ls];
if(rs && minn[rs] < minn[p]) minn[p] = minn[rs], mpos[p] = mpos[rs];
}
inline void pushDown(int p){
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //遞歸地從上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //將x向上旋轉一層的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋轉到當前splay的根
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
int p = 0;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y兩點間連一條邊
// if (find(x) != find(y))
makeRoot(x), f[x] = y;
}
inline void cut(int x,int p){ //把x、y兩點間邊刪掉
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
inline int find(int p){ //找到x所在樹的根節點編號
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
}st;
signed main() {
scanf("%d%d", &n, &m);
rep(i,0,n) st.val[i] = st.minn[i] = 1e5;
rep(i,1,m) scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].w);
sort(e+1,e+1+m);
rep(i,1,m) st.val[i+n] = st.minn[i+n] = e[i].w, st.mpos[i+n] = i+n;
rep(i,1,m){
if(e[i].a == e[i].b) continue;
if(st.find(e[i].a) != st.find(e[i].b)){
st.link(e[i].a,i+n); st.link(i+n,e[i].b);
num++; vis[i] = 1;
while(!vis[pos]) pos++;
if(num == n-1) ans = min(ans,e[i].w-e[pos].w);
}
else{
st.makeRoot(e[i].a);
int p1 = st.access(e[i].b);
p1 = st.mpos[p1];
st.cut(e[p1-n].a,p1); st.cut(p1,e[p1-n].b); vis[p1-n] = 0;
st.link(e[i].a,i+n); st.link(i+n,e[i].b); vis[i] = 1;
while(!vis[pos]) pos++;
if(num == n-1) ans = min(ans,e[i].w-e[pos].w);
}
}
printf("%d\n",ans);
return 0;
}
6. [BJOI2014] 大融合
題意:
個點,一共 次操作。一共有兩種操作類型, 表示連通 ,保證操作合法,且始終是棵森林。 表示查詢去除 邊之後, 所在樹的節點數 所在樹的節點數。
思路:
我們一般遇到的都是維護鏈上節點個數的問題,而此題要求這顆樹上的節點個數,因此我們需要同時維護虛邊和實邊的信息。
我們令 表示節點 子樹中節點個數, 表示節點 虛兒子的節點個數和。因此 ,而這也正是 函數。
因此我們只需要維護 即可,然後觀察哪些函數會改變 的值,不難發現,只有 、、、 會改變邊的虛實關係,其中 主要修改在於調用了 函數,而 只是刪除實邊不會修改虛邊,因此真正關鍵的函數即爲 和 函數,具體操作見代碼,不難思考。
這裏主要講解 中修改 信息時爲什麼需要將節點 ,原因在於單點修改之後,其祖先節點維護的信息都會發生變化,因此一般的問題需要 ,因爲一般問題只需要維護實鏈信息。然後在該問題中還維護了虛鏈信息,因此需要 而不是 。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const int N = 100010;
int n, q;
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], siz[N], siz2[N], rev[N];
inline void clear(int p){ //清除這個點的信息
ch[p][0] = ch[p][1] = f[p] = siz[p] = siz2[p] = rev[p] = 0;
}
inline int isRoot(int p){
clear(0);
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
clear(0);
siz[p] = siz[ls] + 1 + siz[rs] + siz2[p];
}
inline void pushDown(int p){
clear(0);
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //遞歸地從上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //將x向上旋轉一層的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋轉到當前splay的根
update(x); //將上面的標記完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把從根到x的所有點放在一條實鏈裏, 返回這個splay的根
int p; //每次改變右兒子的值,因爲整棵樹是中序遍歷,放入右兒子才能保證先遍歷父親再遍歷兒子
for(p = 0; x; p = x, x = f[x]){
splay(x), siz2[x] += siz[ch[x][1]]-siz[p], ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x點成爲整棵樹的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整條鏈反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y兩點間連一條邊
//makeRoot(x)的作用是使得x無父親
//makeRoot(y)的作用是使得y無父親,因此可以修改y的信息,不用去更新y的祖先
makeRoot(x), makeRoot(y), f[x] = y, siz2[y] += siz[x]; pushUp(y);
}
inline void cut(int x,int p){ //把x、y兩點間邊刪掉,此處刪除的是實邊,注意實邊和虛邊的區別
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
inline int find(int p){ //找到x所在樹的根節點編號
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
}st;
signed main() {
scanf("%d%d", &n, &q);
rep(i,1,n) st.siz[i] = 1;
while (q--) {
char op[10]; int x,y; scanf("%s%d%d",op,&x,&y);
if(op[0] == 'A') st.link(x,y);
else{
st.cut(x,y);
st.makeRoot(x); st.splay(x);
int a1 = st.siz[x];
st.makeRoot(y); st.splay(y);
int a2 = st.siz[y];
st.link(x,y);
printf("%lld\n",(ll)a1*(ll)a2);
}
}
return 0;
}
7. Sone1
題意:
支持 種操作,包括鏈 、、,子樹 、、,換根,換邊,子樹和鏈的修改與加值。
思路:
典型例題,主要思路是對於每個點維護了一個 ,詳情看 的題解。
代碼:
貼上 的代碼。
/*
Toptree即爲可以維護子樹信息的lct升級版
*/
#include<cstdio>
#define N 200010
const int inf=~0U>>1;
inline void swap(int&a,int&b){int c=a;a=b;b=c;}
inline int max(int a,int b){return a>b?a:b;}
inline int min(int a,int b){return a<b?a:b;}
inline void read(int&a){
char c;bool f=0;a=0;
while(!((((c=getchar())>='0')&&(c<='9'))||(c=='-')));
if(c!='-')a=c-'0';else f=1;
while(((c=getchar())>='0')&&(c<='9'))(a*=10)+=c-'0';
if(f)a=-a;
}
struct tag{
int a,b;//ax+b
tag(){a=1,b=0;}
tag(int x,int y){a=x,b=y;}
inline bool ex(){return a!=1||b;}
inline tag operator+(const tag&x){return tag(a*x.a,b*x.a+x.b);}
};
inline int atag(int x,tag y){return x*y.a+y.b;}
struct data{
int sum,minv,maxv,size;
data(){sum=size=0,minv=inf,maxv=-inf;}
data(int x){sum=minv=maxv=x,size=1;}
data(int a,int b,int c,int d){sum=a,minv=b,maxv=c,size=d;}
inline data operator+(const data&x){return data(sum+x.sum,min(minv,x.minv),max(maxv,x.maxv),size+x.size);}
};
inline data operator+(const data&a,const tag&b){return a.size?data(a.sum*b.a+a.size*b.b,atag(a.minv,b),atag(a.maxv,b),a.size):a;}
//son:0-1:重鏈兒子,2-3:AAA樹兒子
int f[N],son[N][4],a[N],tot,rt,rub,ru[N];bool rev[N],in[N];
int val[N];
data csum[N],tsum[N],asum[N];
tag ctag[N],ttag[N];
inline bool isroot(int x,int t){
if(t)return !f[x]||!in[f[x]]||!in[x];
return !f[x]||(son[f[x]][0]!=x&&son[f[x]][1]!=x)||in[f[x]]||in[x];
}
inline void rev1(int x){
if(!x)return;
swap(son[x][0],son[x][1]);rev[x]^=1;
}
inline void tagchain(int x,tag p){
if(!x)return;
csum[x]=csum[x]+p;
asum[x]=csum[x]+tsum[x];
val[x]=atag(val[x],p);
ctag[x]=ctag[x]+p;
}
inline void tagtree(int x,tag p,bool t){
if(!x)return;
tsum[x]=tsum[x]+p;
ttag[x]=ttag[x]+p;
if(!in[x]&&t)tagchain(x,p);else asum[x]=csum[x]+tsum[x];
}
inline void pb(int x){
if(!x)return;
if(rev[x])rev1(son[x][0]),rev1(son[x][1]),rev[x]=0;
if(!in[x]&&ctag[x].ex())tagchain(son[x][0],ctag[x]),tagchain(son[x][1],ctag[x]),ctag[x]=tag();
if(ttag[x].ex()){
tagtree(son[x][0],ttag[x],0),tagtree(son[x][1],ttag[x],0);
tagtree(son[x][2],ttag[x],1),tagtree(son[x][3],ttag[x],1);
ttag[x]=tag();
}
}
inline void up(int x){
tsum[x]=data();
for(int i=0;i<2;i++)if(son[x][i])tsum[x]=tsum[x]+tsum[son[x][i]];
for(int i=2;i<4;i++)if(son[x][i])tsum[x]=tsum[x]+asum[son[x][i]];
if(in[x]){
csum[x]=data();
asum[x]=tsum[x];
}else{
csum[x]=data(val[x]);
for(int i=0;i<2;i++)if(son[x][i])csum[x]=csum[x]+csum[son[x][i]];
asum[x]=csum[x]+tsum[x];
}
}
inline int child(int x,int t){pb(son[x][t]);return son[x][t];}
inline void rotate(int x,int t){
int y=f[x],w=(son[y][t+1]==x)+t;
son[y][w]=son[x][w^1];
if(son[x][w^1])f[son[x][w^1]]=y;
if(f[y])for(int z=f[y],i=0;i<4;i++)if(son[z][i]==y)son[z][i]=x;
f[x]=f[y];f[y]=x;son[x][w^1]=y;up(y);
}
inline void splay(int x,int t=0){
int s=1,i=x,y;a[1]=i;
while(!isroot(i,t))a[++s]=i=f[i];
while(s)pb(a[s--]);
while(!isroot(x,t)){
y=f[x];
if(!isroot(y,t)){if((son[f[y]][t]==y)^(son[y][t]==x))rotate(x,t);else rotate(y,t);}
rotate(x,t);
}
up(x);
}
inline int newnode(){
int x=rub?ru[rub--]:++tot;
son[x][2]=son[x][3]=0;in[x]=1;
return x;
}
inline void setson(int x,int t,int y){son[x][t]=y;f[y]=x;}
inline int pos(int x){for(int i=0;i<4;i++)if(son[f[x]][i]==x)return i;return 4;}
inline void add(int x,int y){//從x連出一條虛邊到y
if(!y)return;
pb(x);
for(int i=2;i<4;i++)if(!son[x][i]){
setson(x,i,y);
return;
}
while(son[x][2]&&in[son[x][2]])x=child(x,2);
int z=newnode();
setson(z,2,son[x][2]);
setson(z,3,y);
setson(x,2,z);
splay(z,2);
}
inline void del(int x){//將x與其虛邊上的父親斷開
if(!x)return;
splay(x);
if(!f[x])return;
int y=f[x];
if(in[y]){
int s=1,i=y,z=f[y];a[1]=i;
while(!isroot(i,2))a[++s]=i=f[i];
while(s)pb(a[s--]);
if(z){
setson(z,pos(y),child(y,pos(x)^1));
splay(z,2);
}
ru[++rub]=y;
}else{
son[y][pos(x)]=0;
splay(y);
}
f[x]=0;
}
inline int fa(int x){//x通過虛邊的父親
splay(x);
if(!f[x])return 0;
if(!in[f[x]])return f[x];
int t=f[x];
splay(t,2);
return f[t];
}
inline int access(int x){
int y=0;
for(;x;y=x,x=fa(x)){
splay(x);
del(y);
add(x,son[x][1]);
setson(x,1,y);
up(x);
}
return y;
}
inline int lca(int x,int y){
access(x);
return access(y);
}
inline int root(int x){
access(x);
splay(x);
while(son[x][0])x=son[x][0];
return x;
}
inline void makeroot(int x){
access(x);
splay(x);
rev1(x);
}
inline void link(int x,int y){
makeroot(x);
add(y,x);
access(x);
}
inline void cut(int x){
access(x);
splay(x);
f[son[x][0]]=0;
son[x][0]=0;
up(x);
}
inline void changechain(int x,int y,tag p){
makeroot(x);
access(y);
splay(y);
tagchain(y,p);
}
inline data askchain(int x,int y){
makeroot(x);
access(y);
splay(y);
return csum[y];
}
inline void changetree(int x,tag p){
access(x);
splay(x);
val[x]=atag(val[x],p);
for(int i=2;i<4;i++)if(son[x][i])tagtree(son[x][i],p,1);
up(x);
splay(x);
}
inline data asktree(int x){
access(x);
splay(x);
data t=data(val[x]);
for(int i=2;i<4;i++)if(son[x][i])t=t+asum[son[x][i]];
return t;
}
int n,m,x,y,z,k,i,ed[N][2];
int main(){
read(n);read(m);
tot=n;
for(i=1;i<n;i++)read(ed[i][0]),read(ed[i][1]); //連邊
for(i=1;i<=n;i++)read(val[i]),up(i); //先賦點權,再連邊
for(i=1;i<n;i++)link(ed[i][0],ed[i][1]); //每個點的權值
read(rt); //給出根
makeroot(rt);
while(m--){
read(k);
if(k==1){//換根,x變成根
read(rt);
makeroot(rt);
}
if(k==9){//x的父親變成y,x父親換成y
read(x),read(y);
if(lca(x,y)==x)continue;
cut(x);
link(y,x);
makeroot(rt);
}
if(k==0){//子樹賦值,以x爲根的子樹點權值改爲y
read(x),read(y);
changetree(x,tag(0,y));
}
if(k==5){//子樹加,x爲根子樹點權值加上y
read(x),read(y);
changetree(x,tag(1,y));
}
if(k==3){//子樹最小值,x爲根子樹中點權值求min
read(x);
printf("%d\n",asktree(x).minv);
}
if(k==4){//子樹最大值,x爲根子樹中點權值求max
read(x);
printf("%d\n",asktree(x).maxv);
}
if(k==11){//子樹和,x爲根子樹中點權sum
read(x);
printf("%d\n",asktree(x).sum);
}
if(k==2){//鏈賦值,x-y路徑上點權值改爲z
read(x),read(y),read(z);
changechain(x,y,tag(0,z));
makeroot(rt);
}
if(k==6){//鏈加,x-y路徑上點權值加上z
read(x),read(y),read(z);
changechain(x,y,tag(1,z));
makeroot(rt);
}
if(k==7){//鏈最小值,x-y路徑上點權值求min
read(x),read(y);
printf("%d\n",askchain(x,y).minv);
makeroot(rt);
}
if(k==8){//鏈最大值,x-y路徑上點權值求max
read(x),read(y);
printf("%d\n",askchain(x,y).maxv);
makeroot(rt);
}
if(k==10){//鏈和,x-y路徑上點權值求sum
read(x),read(y);
printf("%d\n",askchain(x,y).sum);
makeroot(rt);
}
}
return 0;
}
後記
本篇博客到這裏就結束了,祝大家 愉快,一起愛上 把!(๑•̀ㅂ•́)و✧