樹鏈剖分題集

樹鏈剖分常用於處理靜態樹上操作,效率很高,寫起來也就是固定的輕重邊拆分+線段樹,但是代碼一般都是100行+,容易出錯。
本題集中的樹鏈剖分練習題,在解決思路上沒什麼難點,主要是如何設計線段樹以及如何更新+詢問,也就是說要想好怎麼維護線段樹,其它地方沒什麼太難的,但是代碼長就容易出bug,多寫寫就好了。最後一題是線段樹動態開點,這和可持久化權值線段樹(動態主席樹)一樣的思想,只需要將普通的線段樹給稍微修改一下即可。

樹的統計

題意簡述
原題來自:ZJOI 2008
一樹上有 n 個節點,編號分別爲 1 到 n,每個節點都有一個權值 w。我們將以下面的形式來要求你對這棵樹完成一些操作:

1.CHANGE u t :把節點 u 權值改爲 t;

2.QMAX u v :詢問點 u 到點 v 路徑上的節點的最大權值;

3.QSUM u v :詢問點 u 到點 v 路徑上的節點的權值和。

注意:從點 u 到點 v 路徑上的節點包括 u 和 v 本身。

解題思路
樹鏈剖分+線段樹上單點修改,區間查詢,需要注意查詢操作,利用到了LCA倍增思想,樹鏈剖分中很多區間修改和查詢操作都是這樣完成的。

代碼示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int N = 3e4+10;
const int M = 2*N;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,q,a[N];
/*父親,深度,子樹大小,重兒子,重路徑頂部節點,
    樹中節點在線段樹中下標,線段樹中節點對應樹中位置*/
int par[N],deep[N],size[N],son[N],top[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    /*利用深搜更新par,size,deep,son數組*/
    vis[x] = true; size[x] = 1;
    par[x] = fa; deep[x] = deep[fa]+1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(y == fa || vis[y]) continue;
        dfs1(y,x);
        size[x] += size[y]; //累加子樹大小
        if(size[y] > size[son[x]]) son[x] = y;//求重兒子
    }
}
void dfs2(int x,int fa){
    if(son[x]){ //先走重兒子,使得重路徑在線段樹中連續
        seg[son[x]] = ++seg[0];//0位置用不到,利用來計數
        top[son[x]] = top[x];
        rev[seg[0]] = son[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        /*若y沒有被遍歷過,即y不是x的重兒子或者父親*/
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
        /*如果x-->y是輕邊,那麼y就是其所在重路徑頂部節點*/
    }
}
struct SegmentTree{
    int l,r;
    ll mx, sum;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define mx(x) t[x].mx
    #define sum(x) t[x].sum
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l, r(rt) = r;
    if(l == r){
        mx(rt) = sum(rt) = a[rev[l]]; //線段樹上l節點對應着樹上rev[l]點
        return;
    }
    int mid = l+r>>1;
    BuildTree(rt*2,l,mid); BuildTree(rt*2+1,mid+1,r);
    mx(rt) = max(mx(rt<<1),mx(rt<<1|1));
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0); //我們以1號節點爲根
    /*根節點所在重路徑的頂部節點也是根節點,賦初值*/
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
ll tmx,tsum;//利用全局變量同時統計2個答案
void query(int rt,int l,int r){
    /*將以rt爲根的區間內屬於[l,r]部分的和累加到tsum上,並更新tmx*/
    if(l <= l(rt) && r(rt) <= r){
        tsum += sum(rt); tmx = max(tmx,mx(rt));
        return ;
    }
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) query(rt<<1,l,r);
    if(r > mid) query(rt<<1|1,l,r);
}
void ask(int x,int y){
    /*返回x與y之間路徑上權值最大點的權值*/
    int fx = top[x] , fy = top[y];
    while(fx != fy){//先將x和y條整到同一個重鏈上
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        query(1,seg[fx],seg[x]);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);//路徑淺的編號小
    query(1,seg[x],seg[y]); //再更新一次
}
void change(int rt,int x,int val){
    /*把線段樹節點x的權值改爲val*/
    if(l(rt) == r(rt)){
        mx(rt) = sum(rt) = val;
        return;
    }
    int mid = l(rt) + r(rt) >> 1;
    if(x > mid) change(rt<<1|1,x,val);
    else change(rt<<1,x,val);
    mx(rt) = max(mx(rt<<1),mx(rt<<1|1));
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
int main(){
    #ifdef LOCAL
        freopen("123.txt","r",stdin);
        freopen("222.txt","w",stdout);
    #endif
    scanf("%d",&n);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y);
        addEdge(x,y,1); addEdge(y,x,1);
    }
    for(int i = 1;i <= n;i++) scanf("%d",a+i);
    scanf("%d",&q);
    preHandle();//樹鏈剖分預處理
    char op[20];
    for(int i = 1,x,y;i <= q;i++){
        scanf("%s%d%d",op,&x,&y);
        if(op[0] == 'C') change(1,seg[x],y);
        else{
            tmx = -INF; tsum = 0;
            ask(x,y);//同時更新最大值與路徑和
            if(op[1] == 'M') printf("%lld\n",tmx);
            else printf("%lld\n",tsum);
        }
    }
    return 0;
}

「HAOI2015」樹上操作

題意簡述
有一棵點數爲 N 的樹,以點 1 爲根,且樹有點權。然後有 M 個操作,分爲三種:

1、把某個節點 x 的點權增加 a 。

2、把某個節點 x 爲根的子樹中所有點的點權都增加 a 。

3、詢問某個節點 x 到根的路徑中所有點的點權和。

解題思路
樹鏈剖分+線段樹上區間修改,區間查詢。用到了線段樹上的延遲標記,其實和普通線段樹沒啥區別,就是查詢和修改時候要用LCA倍增思想將樹上的鏈轉化爲線段樹上的區間。只要理解了樹上節點和線段樹下標是通過seg[x] 和 rev[seg[x]]來轉換的即可。

另外還需要明白線段樹上重鏈的seg[]值是連續的,同一個子樹上的所有節點下標也是連續的,這個結論可以通過觀察dfs2的實現得到。

代碼示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y,edge[tot] = z;
    nex[tot] = head[x]; head[x] = tot;
}
int n,m,a[N];
int getInt() {
    int ans = 0;
    bool neg = false;
    char c = getchar();
    while (c!='-' && (c<'0' || c>'9')) c = getchar();
    if (c == '-') neg = true, c = getchar();
    while (c>='0' && c<='9')
        ans = ans*10 + c-'0', c = getchar();
    return neg ? -ans : ans; 
}
int deep[N],par[N],top[N],size[N],son[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    vis[x] = true; par[x] = fa;
    deep[x] = deep[fa]+1; size[x] = 1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y] || y == fa) continue;
        dfs1(y,x);
        size[x] += size[y]; 
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    int l,r;
    ll sum,add;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define sum(x) t[x].sum
    #define add(x) t[x].add
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l,r(rt) = r,add(rt) = 0;
    if(l == r){
        sum(rt) = a[rev[l]]; return;
    }
    int mid = l+r>>1;
    BuildTree(rt<<1,l,mid); BuildTree(rt<<1|1,mid+1,r);
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
void spread(int rt){
    if(!add(rt)) return;
    sum(rt<<1) += (r(rt<<1)-l(rt<<1)+1)*add(rt);
    sum(rt<<1|1) += (r(rt<<1|1)-l(rt<<1|1)+1)*add(rt);
    add(rt<<1) += add(rt); add(rt<<1|1) += add(rt);
    add(rt) = 0;
}
void Add(int rt,int l,int r,ll val){
    if(l <= l(rt) && r(rt) <= r){
        sum(rt) += (r(rt) - l(rt)+1)*val;
        add(rt) += val; return;
    }
    spread(rt);
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) Add(rt<<1,l,r,val);
    if(r > mid) Add(rt<<1|1,l,r,val);
    sum(rt) = sum(rt<<1)+sum(rt<<1|1);
}
ll query(int rt,int l,int r){
    if(l <= l(rt) && r(rt) <= r) return sum(rt);
    spread(rt); ll res = 0;
    int mid = l(rt) + r(rt) >> 1;
    if(l <= mid) res += query(rt<<1,l,r);
    if(r > mid) res += query(rt<<1|1,l,r);
    return res;
}
ll ask(int x){
    /*從節點x到根節點的路徑和*/
    ll res = 0;
    while(x){
        res += query(1,seg[top[x]],seg[x]);
        x = par[top[x]];
    }
    return res;
}
int main(){
    n = getInt(); m = getInt();
    for(int i = 1;i <= n;i++) a[i] = getInt();
    for(int i = 1,x,y;i < n;i++){
        x = getInt(); y = getInt();
        addEdge(x,y,1); addEdge(y,x,1);
    }
    preHandle();
    for(int i = 1,op,x,y;i <= m;i++){
        op = getInt();
        if(op == 1){
            x = getInt(); y = getInt();
            Add(1,seg[x],seg[x],y);
        }else if(op == 2){
            x = getInt(); y = getInt();
            Add(1,seg[x],seg[x]+size[x]-1,y);
        }else{
            x = getInt(); printf("%lld\n",ask(x));
        }
    }
    return 0;
}

「NOI2015」軟件包管理器

題意簡述
Linux 用戶和 OSX 用戶一定對軟件包管理器不會陌生。通過軟件包管理器,你可以通過一行命令安裝某一個軟件包,然後軟件包管理器會幫助你從軟件源下載軟件包,同時自動解決所有的依賴(即下載安裝這個軟件包的安裝所依賴的其它軟件包),完成所有的配置。Debian/Ubuntu 使用的 apt-get,Fedora/CentOS 使用的 yum,以及 OSX 下可用的 Homebrew 都是優秀的軟件包管理器。

你決定設計你自己的軟件包管理器。不可避免地,你要解決軟件包之間的依賴問題。如果軟件包 A 依賴軟件包 B,那麼安裝軟件包 A 以前,必須先安裝軟件包 B。同時,如果想要卸載軟件包 B,則必須卸載軟件包 A。現在你已經獲得了所有的軟件包之間的依賴關係。而且,由於你之前的工作,除 0 號軟件包以外,在你的管理器當中的軟件包都會依賴一個且僅一個軟件包,而 0 號軟件包不依賴任何一個軟件包。依賴關係不存在環(若有m(m≥2) 個軟件包A1,A2,A3,…,Am ,其中 A1 依賴 A2,A2依賴 A3 依賴 A4 ,……,Am−1 依賴 Am ,而 Am 依賴 A1 ,則稱這 m 個軟件包的依賴關係構成環),當然也不會有一個軟件包依賴自己。

現在你要爲你的軟件包管理器寫一個依賴解決程序。根據反饋,用戶希望在安裝和卸載某個軟件包時,快速地知道這個操作實際上會改變多少個軟件包的安裝狀態(即安裝操作會安裝多少個未安裝的軟件包,或卸載操作會卸載多少個已安裝的軟件包),你的任務就是實現這個部分。注意,安裝一個已安裝的軟件包,或卸載一個未安裝的軟件包,都不會改變任何軟件包的安裝狀態,即在此情況下,改變安裝狀態的軟件包數爲 0。

解題思路
要畫圖理解題意,若A依賴B,則安裝A之前要安裝B,若卸載B要先卸載A。也就是說要安裝A,就要確保A到根節點路徑上軟件都已安裝;若卸載A,就要確保 A 的子樹爲空。

於是就是樹上操作,若卸載一個軟件 x ,就是查詢它子樹上已安裝的軟件個數(包括它自己),輸出後再刪除即可;若安裝一個軟件 x ,就是查詢 x 到根節點路徑上未安裝的軟件個數(包括它自己),輸出後再安裝。因爲軟件安裝和卸載只是改變其狀態(已安裝:1,未安裝:0),而不改變結構(徹底刪除節點),所以可以用樹鏈剖分+線段樹解決。

但是需要注意的是,怎麼把整棵子樹都清零,怎麼把到根節點的路徑都置 1,顯然要用延遲標記,但是延遲標記卻不應該累加,那要怎麼處理?
置 1 時延遲標記add = 1,置0時延遲標記add = -1,每一個節點如果延遲標記不爲0,那麼該節點爲根的子樹所有軟件狀態相同。我們下傳延遲標記操作封裝在spread函數內,可以參考如何實現延遲標記的更新。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2e5+10;
typedef int ll;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,m;
int par[N],deep[N],size[N],son[N],top[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    vis[x] = true; par[x] = fa;
    deep[x] = deep[fa]+1; size[x] = 1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y]) continue;
        dfs1(y,x);
        size[x] += size[y];
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    int l,r,sum,add;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define sum(x) t[x].sum
    #define add(x) t[x].add
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l,r(rt) = r,add(rt) = sum(rt) = 0;
    if(l == r) return;
    int mid = l+r>>1;
    BuildTree(rt<<1,l,mid); BuildTree(rt<<1|1,mid+1,r);
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
void spread(int rt){
    if(!add(rt)) return; 
    if(add(rt) == -1){
        sum(rt<<1) = sum(rt<<1|1) = 0;
        add(rt<<1) = add(rt<<1|1) = add(rt);
        add(rt) = 0; return;
    }
    sum(rt<<1) = (r(rt<<1)-l(rt<<1)+1)*add(rt);
    sum(rt<<1|1) = (r(rt<<1|1)-l(rt<<1|1)+1)*add(rt);
    add(rt<<1) = add(rt); add(rt<<1|1) = add(rt);
    add(rt) = 0;
}
void Add(int rt,int l,int r,ll val){
    if(l <= l(rt) && r(rt) <= r){
        if(val == -1) sum(rt) = 0;
        else sum(rt) = (r(rt) - l(rt)+1);
        add(rt) = val; 
        return;
    }
    spread(rt);
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) Add(rt<<1,l,r,val);
    if(r > mid) Add(rt<<1|1,l,r,val);
    sum(rt) = sum(rt<<1)+sum(rt<<1|1);
}
ll query(int rt,int l,int r){
    if(l <= l(rt) && r(rt) <= r) return sum(rt);
    spread(rt); ll res = 0;
    int mid = l(rt) + r(rt) >> 1;
    if(l <= mid) res += query(rt<<1,l,r);
    if(r > mid) res += query(rt<<1|1,l,r);
    return res;
}
int ask1(int x){
    return query(1,seg[x],seg[x]+size[x]-1);
}
int ask2(int x){
    int res = 0;
    while(x){
        res += query(1,seg[top[x]],seg[x]);
        x = par[top[x]];
    }
    return res;
}
void Add2(int x){
    while(x){
        Add(1,seg[top[x]],seg[x],1);
        x = par[top[x]];
    }
}
int main(){
    scanf("%d",&n);
    for(int i = 1,x;i < n;i++){
        scanf("%d",&x);
        addEdge(x+1,i+1,1); addEdge(i+1,x+1,1);
    }
    preHandle();
    scanf("%d",&m); char op[22];
    for(int i = 1,x;i <= m;i++){
        scanf("%s%d",op,&x); x++;
        if(op[0] == 'u'){
            printf("%d\n",ask1(x));
            Add(1,seg[x],seg[x]+size[x]-1,-1);
        }else{
            printf("%d\n",deep[x]-ask2(x));
            Add2(x);
        }
    }
    return 0;
}

染色

題意簡述
原題來自:SDOI 2011
給定一棵有 n 個節點的無根樹和 m 個操作,操作共兩類。
1、將節點 a 到節點 b 路徑上的所有節點都染上顏色;
2、詢問節點 a 到節點 b 路徑上的顏色段數量,連續相同顏色的認爲是同一段,例如 112221 由三段組成:11 、 222、1。

請你寫一個程序依次完成操作。

解題思路
如果不是在樹上,是在區間上,該如何操作?用線段樹解決。線段樹上每個節點有lc:左端點顏色,rc:右端點顏色,same:區間顏色是否唯一(1表示唯一,0表示不唯一),sum:區間內顏色段數量。
那麼更新顯然要用延遲標記,標記在same上,而合併操作需要考慮中間相接的端點顏色是否相同,若相同則顏色段數量要-1。於是區間上“染色”問題就可以用線段樹解決了。

在樹上顯然類似,因爲我們可以通過樹鏈剖分來將樹給轉化爲區間。唯一麻煩的地方就是端點顏色問題,在區間上,我們只需要判斷左子樹的右端點、右子樹的左端點是否顏色相同即可,但是樹上的路徑是通過倍增來找的,顯然需要記錄每一條鏈的端點顏色,麻煩在於如何記錄並判斷。

樹鏈剖分後節點越淺,其seg越小,於是需要判斷顏色是否相同的是seg[fx]和 seg[par[fx]],這是由於我們在倍增調整深度時候是自底向上的,自然要判斷深度淺的節點。爲了美觀以及簡便,設 getcol(x) 返回該節點的顏色。

代碼示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2e5+10;
const int INF = 0x3f3f3f3f;
int head[N],edge[M],ver[M],nex[M], tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int deep[N],seg[N],rev[N],top[N],son[N],size[N],par[N];
int vis[N],a[N];
void dfs1(int x,int fa){
    vis[x] = true; par[x] = fa;
    deep[x] = deep[fa]+1; size[x] = 1;
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y] || y == fa) continue;
        dfs1(y,x);
        size[x] += size[y];
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    /*lc:區間左端點顏色,rc:區間右端點顏色,sum:區間內顏色段數*/
    int l,r,lc,rc,sum;
    int same;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define lc(x) t[x].lc
    #define rc(x) t[x].rc
    #define sum(x) t[x].sum
    #define same(x) t[x].same
}t[N*4];
void Updata(int rt){//更新節點rt的same、rc、lc和sum
    same(rt) = same(rt<<1) && same(rt<<1|1) && rc(rt<<1) == lc(rt<<1|1);
    sum(rt) = sum(rt<<1)+sum(rt<<1|1);
    if(rc(rt<<1) == lc(rt<<1|1)) sum(rt)--;
    lc(rt) = lc(rt<<1); rc(rt) = rc(rt<<1|1);
}
void BuildTree(int rt,int l,int r){
    l(rt) = l,r(rt) = r,same(rt) = 0;
    if(l == r){
        lc(rt) = rc(rt) = a[rev[l]];
        sum(rt) = 1; return;
    }
    int mid = l + r >> 1;
    BuildTree(rt<<1,l,mid); BuildTree(rt<<1|1,mid+1,r);
    Updata(rt);
}
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = rev[1] = top[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
void spread(int rt){
    if(!same(rt)) return;//將懶惰標記下傳,即對子樹染相同顏色
    sum(rt<<1) = sum(rt<<1|1) = 1;
    lc(rt<<1) = rc(rt<<1) = lc(rt<<1|1) = rc(rt<<1|1) = lc(rt);
    same(rt<<1) = same(rt<<1|1) = 1; same(rt) = 0;
}
void modify(int rt,int l,int r,int col){
    if(l <= l(rt) && r(rt) <= r){
        sum(rt) = 1; lc(rt) = rc(rt) = col;
        same(rt) = 1; return;
    }
    spread(rt);//懶惰標記
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) modify(rt<<1,l,r,col);
    if(r > mid) modify(rt<<1|1,l,r,col);
    Updata(rt);
}
void change(int x,int y,int z){
    int fx = top[x],fy = top[y];
    while(fx != fy){
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        modify(1,seg[fx],seg[x],z);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);
    modify(1,seg[x],seg[y],z);
}
int query(int rt,int l,int r){
    if(l <= l(rt) && r(rt) <= r) return sum(rt);
    spread(rt);
    int mid = l(rt)+r(rt)>>1 ,res= 0;
    if(l <= mid) res += query(rt<<1,l,r);
    if(r > mid) res += query(rt<<1|1,l,r);
    /*合併時中間顏色相同,要減去一段*/
    if(l <= mid && mid < r && rc(rt<<1) == lc(rt<<1|1)) res--;
    return res;
}
int getcol(int rt,int x){//這個函數太好用了,哭哭
    if(l(rt) == r(rt)) return lc(rt);
    spread(rt);
    int mid = l(rt)+r(rt)>>1;
    if(x <= mid) return getcol(rt<<1,x);
    else return getcol(rt<<1|1,x);
}
int ask(int x,int y){
    int fx = top[x], fy = top[y], res = 0;
    while(fx != fy){
        if(deep[fx] < deep[fy]) swap(fx,fy),swap(x,y);
        res += query(1,seg[fx],seg[x]);
        if(getcol(1,seg[fx]) == getcol(1,seg[par[fx]])) res--;
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);
    res += query(1,seg[x],seg[y]);
    return res;
}
int n,m;
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) scanf("%d",a+i);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y); 
        addEdge(x,y,1); addEdge(y,x,1);
    }
    preHandle(); char op[4];
    for(int i = 1,x,y,z;i <= m;i++){
        scanf("%s",op);
        if(op[0] == 'Q'){
            scanf("%d%d",&x,&y);
            printf("%d\n",ask(x,y));
        }else{
            scanf("%d%d%d",&x,&y,&z);
            change(x,y,z);
        }
    }
    return 0;
}

「SDOI2014」旅行

題意簡述
S 國有 N 個城市,編號從 1 到 N。城市間用 N−1 條雙向道路連接,滿足從一個城市出發可以到達其它所有城市。每個城市信仰不同的宗教,如飛天麪條神教、隱形獨角獸教、絕地教都是常見的信仰。爲了方便,我們用不同的正整數代表各種宗教,S 國境內總共有 C 種不同的宗教。
S 國的居民常常旅行。旅行時他們總會走最短路,並且爲了避免麻煩,只在信仰和他們相同的城市留宿。當然旅程的終點也是信仰與他相同的城市。S 國政府爲每個城市標定了不同的旅行評級,旅行者們常會記下途中(包括起點和終點)留宿過的城市的評級總和或最大值。
在 S 國的歷史上常會發生以下幾種事件:

1、CC x c:城市 x 的居民全體改信了 c 教;

2、CW x w:城市 x 的評級調整爲 w;

3、QS x y:一位旅行者從城市 x 出發,到城市 y,並記下了途中留宿過的城市的評級總和;

4、QM x y:一位旅行者從城市 x 出發,到城市 y,並記下了途中留宿過的城市的評級最大值。
由於年代久遠,旅行者記下的數字已經遺失了,但記錄開始之前每座城市的信仰與評級,還有事件記錄本身是完好的。請根據這些信息,還原旅行者記下的數字。

爲了方便,我們認爲事件之間的間隔足夠長,以致在任意一次旅行中,所有城市的評級和信仰保持不變。
解題思路
如果沒有信仰,那就是簡單的樹鏈剖分模板題,但這題多了個信仰,正常思路是開數組存放對應信仰的評級,但是顯然不現實,從空間角度來看。
這題用到了動態開點,和可持久化線段樹類似,只需要稍微改一下線段樹模板即可。

代碼示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y,edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,m;
int deep[N],par[N],size[N],top[N],seg[N],rev[N],son[N];
int vis[N],a[N],c[N];//a:初始評級 c:初始信仰
void dfs1(int x,int fa){
    deep[x] = deep[fa]+1;par[x] = fa;
    size[x] = 1;vis[x] = true;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y] || y == fa) continue;
        dfs1(y,x);
        size[x] += size[y];
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i],z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    int ls,rs;
    ll mx,sum;
    SegmentTree(){
        ls = rs = mx = sum = 0;
    }
    #define ls(x) t[x].ls
    #define rs(x) t[x].rs
    #define mx(x) t[x].mx
    #define sum(x) t[x].sum
}t[N*20];
int root[N],sz = 1;//最多有多少個根節點
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = rev[1] = top[1] = 1;
    dfs2(1,0);
}
void Add(int &rt,int l,int r,int pos,ll val){
    if(!rt) rt = ++sz;
    mx(rt) = max(mx(rt),val); sum(rt) += val;
    if(l == r) return; int mid = l+r>>1;
    if(pos <= mid) Add(ls(rt),l,mid,pos,val);
    else Add(rs(rt),mid+1,r,pos,val);
}
void Delete(int rt,int l,int r,int pos){
    if(l == r){
        sum(rt) = mx(rt) = 0; return;
    }
    int mid = l+r>>1;
    if(pos <= mid) Delete(ls(rt),l,mid,pos);
    else Delete(rs(rt),mid+1,r,pos);
    sum(rt) = sum(ls(rt)) + sum(rs(rt));
    mx(rt) = max(mx(ls(rt)),mx(rs(rt)));
}
ll mx,sum;
void query(int rt,int L,int R,int l,int r){
  //  printf("%d %d %d %d %d\n",rt,L,R,l,r);
    if(!rt) return;
    if(l <= L && R <= r){
      //  printf("%d %d\n",mx(rt),sum(rt));
        mx = max(mx,mx(rt)); sum += sum(rt); return;
    }
    int mid = L+R>>1;
    if(l <= mid) query(ls(rt),L,mid,l,r);
    if(r > mid) query(rs(rt),mid+1,R,l,r);
}
void ask(int rt,int x,int y){
    /*返回x到y路徑上信仰rt的評級最大值以及總和*/
    int fx = top[x],fy = top[y];
    while(fx != fy){
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        query(rt,1,seg[0],seg[fx],seg[x]);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);
    query(rt,1,seg[0],seg[x],seg[y]);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) scanf("%d%d",a+i,c+i);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y);
        addEdge(x,y,1); addEdge(y,x,1);
    }
    preHandle();
    for(int i = 1;i <= n;i++)
        Add(root[c[i]],1,seg[0],seg[i],a[i]);
    char op[10];
    for(int i = 1,x,y;i <= m;i++){
        scanf("%s%d%d",op,&x,&y);
        if(op[1] == 'C'){
            Delete(root[c[x]],1,seg[0],seg[x]);
            Add(root[c[x] = y],1,seg[0],seg[x],a[x]);
        }else if(op[1] == 'W'){
            Delete(root[c[x]],1,seg[0],seg[x]);
            Add(root[c[x]],1,seg[0],seg[x],a[x] = y);
        }else{
            mx = sum = 0;//一起更新,少寫代碼,美滋滋
            ask(root[c[x]],x,y);
            if(op[1] == 'S') printf("%lld\n",sum);
            else printf("%lld\n",mx);
        }
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章