1. 程式人生 > >洛谷 P3384 樹鏈剖分(詳解)

洛谷 P3384 樹鏈剖分(詳解)

題目描述

如題,已知一棵包含N個結點的樹(連通且無環),每個節點上包含一個數值,需要支援以下操作:
操作1: 格式: 1 x y z 表示將樹從x到y結點最短路徑上所有節點的值都加上z
操作2: 格式: 2 x y 表示求樹從x到y結點最短路徑上所有節點的值之和
操作3: 格式: 3 x z 表示將以x為根節點的子樹內所有節點值都加上z
操作4: 格式: 4 x 表示求以x為根節點的子樹內所有節點值之和

輸入格式:

第一行包含4個正整數N、M、R、P,分別表示樹的結點個數、操作個數、根節點序號和取模數(即所有的輸出結果均對此取模)。

接下來一行包含N個非負整數,分別依次表示各個節點上初始的數值。

接下來N-1行每行包含兩個整數x、y,表示點x和點y之間連有一條邊(保證無環且連通)

接下來M行每行包含若干個正整數,每行表示一個操作,格式如下:

操作1: 1 x y z

操作2: 2 x y

操作3: 3 x z

操作4: 4 x

輸出格式:

輸出包含若干行,分別依次表示每個操作2或操作4所得的結果(對P取模

輸入樣例#1

5 5 2 24
7 3 7 8 0 
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3

輸出樣例#1

2
21

說明

時空限制:1s,128M

資料規模:

對於30%的資料: N10,M10N10,M10

對於70%的資料: N103,M103N103,M103

對於100%的資料:N105,M105N105,M105

其實,純隨機生成的樹LCA+暴力是能過的,可是,你覺得可能是純隨機的麼233

樣例說明:

樹的結構如下:

img

各個操作如下:

img

故輸出應依次為2、21(重要的事情說三遍:記得取模)

思路

這是一道樹鏈剖分的模板題:

因為樹的狀態是不變的,因此樹鏈剖分的策略是將這些點按某種方式組織起來,剖分為若干條鏈,每條鏈就相當於一個序列。這樣,操作的路徑可以拆分為某幾條鏈,也就是若干個完整序列或是某個序列上的一段區間,此時可以用線段樹等處理序列上區間新操作的資料結構去解決問題。樹鏈剖分的核心是如何恰當的將樹剖分為若干條鏈,當鏈的劃分方式確定時,只需要將他們看作一個個序列,將所有的序列按照順序拼接起來,每條鏈就形成了一個區間,而序列上的區間問題使我們擅長解決的.

關於樹鏈剖分,有如下幾個定義:

  • 重兒子:以任意點為根,記size(u)為以u為根的子樹的節點個數,令v為u的所有兒子size值最大的一個,則v為重兒子
  • 輕兒子:除了重兒子,其餘都為輕兒子
  • 重邊:令v為u的所有兒子size值最大的一個,則(u,v)為重邊
  • 輕邊:重邊之外的邊
  • 重鏈:一條鏈為重鏈,當且僅當他全部由重邊組成(一個點也算一條重鏈)

在剖分過程中,要計算如下7個值:

  • fa[u]:u在樹中的父親
  • dep[u]:u節點的深度
  • size[u]:u的子樹節點數(子樹大小)
  • son[u]:u的重兒子
  • top[u]:u所在重鏈的頂部節點
  • id[x]:新的編號
  • wt[x]:新編號的點權

第一遍dfs時可以計算前4個值,第二遍dfs後可以計算後3個值,在計算id時,同一條重鏈上的點需要按順序排在連繼續的一段位置,也就是一段區間.
將一條路徑(u,v)拆分成若干重鏈的過程,實際上就是尋找最近公共祖先的過程。我們會選擇u,v中深度較大的點向上走一步,知道u==v.現在有了重鏈,由於我們記錄了重鏈的頂部節點top[x],還記錄了每個點在序列中的位置,因為我們不需要一步步走。假定top[u]和top[v]不同,那麼他們的最近公共祖先可能在其中的一條重鏈上,也可能在其他重鏈上。因為LCA顯然不可能在top深度較大的那條重鏈上,所以我們先處理top深度較大的。首先我們找出u,v中top深度較大的點,假設是u,則可以直接調到fa[top[u]]處,且跳過的這一段,線上段樹中是一段區間,若我們按照深度從小到大來儲存點,則這段區間為:[id[top[x]],id[x]]。當u,v中的top相同時,說明他們走到了同一條重鏈上,這時他們之間的路徑也是序列上的一段區間,且u,v中深度較小的點是原路徑的LCA.這樣我們就可以將給出的任意路徑拆分為若干個重鏈,也就是若干個區間,可以用線段樹來處理操作。

程式碼

#include <bits/stdc++.h>
using namespace std;
#define mem(a, b) memset(a, b, sizeof(a))
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
const int N = 2e5 + 10;
int sum[N << 2], lazy[N << 2]; //線段樹求和
int n, m, r, mod;              //節點數,運算元,根節點,模數
int first[N], tot;             //鄰接表
//分別為:重兒子,每個節點新編號,父親,編號,深度,子樹個數,所在重鏈的頂部
int son[N], id[N], fa[N], cnt, dep[N], siz[N], top[N];
int w[N], wt[N]; //初始點權,新編號點權
int res = 0;     //查詢答案
struct edge
{
    int v, next;
} e[N];
void add_edge(int u, int v)
{
    e[tot].v = v;
    e[tot].next = first[u];
    first[u] = tot++;
}
void init()
{
    mem(first, -1);
    tot = 0;
    cnt = 0;
}
int pushup(int rt)
{
    sum[rt] = (sum[rt << 1] + sum[rt << 1 | 1]) % mod;
}
void pushdown(int rt, int m) //下放lazy標記
{
    if (lazy[rt])
    {
        lazy[rt << 1] += lazy[rt];                 //給左兒子下放lazy
        lazy[rt << 1 | 1] += lazy[rt];             //給右兒子下放lazy
        sum[rt << 1] += lazy[rt] * (m - (m >> 1)); //更新sum
        sum[rt << 1] %= mod;
        sum[rt << 1 | 1] += lazy[rt] * (m >> 1);
        sum[rt << 1 | 1] %= mod;
        lazy[rt] = 0;
    }
}
void build(int l, int r, int rt)
{
    lazy[rt] = 0;
    if (l == r)
    {
        sum[rt] = wt[l]; //新的編號點權
        sum[rt] %= mod;
        return;
    }
    int m = (l + r) >> 1;
    build(lson);
    build(rson);
    pushup(rt);
}
void update(int L, int R, int c, int l, int r, int rt)
{
    if (L <= l && r <= R)
    {
        lazy[rt] += c;
        sum[rt] += c * (r - l + 1);
        sum[rt] %= mod;
        return;
    }
    pushdown(rt, r - l + 1);
    int m = (l + r) >> 1;
    if (L <= m)
        update(L, R, c, lson);
    if (R > m)
        update(L, R, c, rson);
    pushup(rt);
}
void query(int L, int R, int l, int r, int rt)
{
    if (L <= l && r <= R)
    {
        res += sum[rt];
        res %= mod;
        return;
    }
    pushdown(rt, r - l + 1);
    int m = (l + r) >> 1;
    if (L <= m)
        query(L, R, lson);
    if (R > m)
        query(L, R, rson);
}
//----------------------------------------------------------------
//處理出fa[],dep[],siz[],son[]
void dfs1(int u, int f, int deep)
{
    dep[u] = deep;   //標記深度
    fa[u] = f;       //標記節點的父親
    siz[u] = 1;      //記錄每個節點子樹大小
    int maxson = -1; //記錄重兒子數量
    for (int i = first[u]; ~i; i = e[i].next)
    {
        int v = e[i].v;
        if (v == f)
            continue;
        dfs1(v, u, deep + 1);
        siz[u] += siz[v];
        if (siz[v] > maxson) //兒子裡最多siz就是重兒子
        {
            son[u] = v; //標記u的重兒子為v
            maxson = siz[v];
        }
    }
}

//處理出top[],wt[],id[]
void dfs2(int u, int topf)
{
    id[u] = ++cnt;  //每個節點的新編號
    wt[cnt] = w[u]; //新編號的對應權值
    top[u] = topf;  //標記每個重鏈的頂端
    if (!son[u])    //沒有兒子時返回
        return;
    dfs2(son[u], topf); //搜尋下一個重兒子
    for (int i = first[u]; ~i; i = e[i].next)
    {
        int v = e[i].v;
        if (v == fa[u] || v == son[u]) //處理輕兒子
            continue;
        dfs2(v, v); //每一個輕兒子都有一個從自己開始的鏈
    }
}
void updrange(int x, int y, int k)
{
    k %= mod;
    while (top[x] != top[y])
    {
        if (dep[top[x]] < dep[top[y]]) //使x深度較大
            swap(x, y);
        update(id[top[x]], id[x], k, 1, n, 1);
        x = fa[top[x]];
    }
    if (dep[x] > dep[y]) //使x深度較小
        swap(x, y);
    update(id[x], id[y], k, 1, n, 1);
}
int qrange(int x, int y)
{
    int ans = 0;
    while (top[x] != top[y]) //當兩個點不在同一條鏈上
    {
        if (dep[top[x]] < dep[top[y]]) //使x深度較大
            swap(x, y);
        res = 0;
        query(id[top[x]], id[x], 1, n, 1);
        //ans加上x點到x所在鏈頂端這一段區間的點權和
        ans += res;
        ans %= mod;
        x = fa[top[x]]; //x跳到x所在鏈頂端的這個點的上面一個點
    }
    //當兩個點處於同一條鏈
    if (dep[x] > dep[y]) //使x深度較小
        swap(x, y);
    res = 0;
    query(id[x], id[y], 1, n, 1);
    ans += res;
    return ans % mod;
}
void upson(int x, int k)
{
    update(id[x], id[x] + siz[x] - 1, k, 1, n, 1); //子樹區間右端點為id[x]+siz[x]-1
}
int qson(int x)
{
    res = 0;
    query(id[x], id[x] + siz[x] - 1, 1, n, 1);
    return res;
}
int main()
{
    // freopen("in.txt", "r", stdin);
    int u, v;
    scanf("%d%d%d%d", &n, &m, &r, &mod);
    init();
    for (int i = 1; i <= n; i++)
        scanf("%d", &w[i]);
    for (int i = 1; i <= n - 1; i++)
    {
        scanf("%d%d", &u, &v);
        add_edge(u, v);
        add_edge(v, u);
    }
    dfs1(r, 0, 1);
    dfs2(r, r);
    build(1, n, 1); //用新點權建立線段樹
    while (m--)
    {
        int op, x, y, z;
        scanf("%d", &op);
        if (op == 1)
        {
            scanf("%d%d%d", &x, &y, &z);
            updrange(x, y, z);
        }
        else if (op == 2)
        {
            scanf("%d%d", &x, &y);
            printf("%d\n", qrange(x, y));
        }
        else if (op == 3)
        {
            scanf("%d%d", &x, &z);
            upson(x, z);
        }
        else if (op == 4)
        {
            scanf("%d", &x);
            printf("%d\n", qson(x));
        }
    }
    return 0;
}