满二叉树

节点个数必为奇数个

香港icpc2019

给定一棵树,两个人轮流进行游戏,一个人每次能从树上取走一棵符合满二叉树性质的子树,询问谁能必胜。

解:满二叉树节点个数必为奇数个。故节点个数为奇数时先手必胜,否则后手必胜。

#include <cstdio>

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        int n;
        scanf("%d", &n);
        for (int i = 0; i < n - 1; ++i) scanf("%*d%*d");
        puts(n & 1 ? "Alice" : "Bob");
    }
    return 0;
}

dfs求两点距离

int dis;
void get_dis(int cur, int end, int father, int depth) {
    if (cur == end) {
        dis = depth;
        return;
    }
    for (auto i : tree[cur]) {
        if (i != father) {
            get_dis(i, end, cur, depth + 1);
        }
    }
}

DFS求每个点的高度

int height[1000006];

void get_height(int cur, int depth) {
    if (sons[cur].empty()) {
        height[cur] = 0;
        return;
    }
    int h = 0;
    for (auto &i : sons[cur]) {
        get_height(i, depth + 1);
        h = std::max(h, height[i]);
    }
    height[cur] = h + 1;
}

倍增求LCA

/**
 * luogu3379
 */

#include <cstdio>
#include <algorithm>

using namespace std;

struct Edge {
    int v;
    int next;
} edges[500002 << 1];
int heads[500002];
int total = 0;

int depth[500002] = {0};
int ancestors[500002][22] = {0};

int LOG_2[500002];

void add_edge(int u, int v) {
    edges[++total] = {v, heads[u]};
    heads[u] = total;
}

void dfs(int cur_v, int parent) {
    ancestors[cur_v][0] = parent;
    depth[cur_v] = depth[parent] + 1;

    for (int i = 1; i <= LOG_2[depth[cur_v]]; ++i) {
        ancestors[cur_v][i] = ancestors[ancestors[cur_v][i-1]][i-1];
    }

    for (int i = heads[cur_v]; i; i = edges[i].next) {
        if (edges[i].v != parent) {
            dfs(edges[i].v, cur_v);
        }
    }
}

int lca(int a, int b) {
    if (depth[a] < depth[b]) {
        swap(a, b);
    }
    while (depth[a] > depth[b]) {
        a = ancestors[a][LOG_2[depth[a]-depth[b]]];
    }
    if (a == b) {
        return a;
    }
    for (int i = LOG_2[depth[a]]; i >= 0; --i) {
        if (ancestors[a][i] != ancestors[b][i]) {
            a = ancestors[a][i];
            b = ancestors[b][i];
        }
    }
    return ancestors[a][0];
}

void get_log_2() {
    LOG_2[1] = 0;
    for (int i = 2; i <= 500001; ++i) {
        LOG_2[i] = LOG_2[i>>1] + 1;
    }
}

int main() {
    get_log_2();

    int n, m, s;
    scanf("%d %d %d", &n, &m, &s);

    int x, y;
    for (int i = 0; i < n - 1; ++i) {
        scanf("%d %d", &x, &y);
        add_edge(x, y);
        add_edge(y, x);
    }

    dfs(s, 0);

    int a, b;
    for (int i = 0; i < m; ++i) {
        scanf("%d %d", &a, &b);
        printf("%d\n", lca(a, b));
    }

    return 0;
}

Tarjan求LCA

Tarjan算法求LCA的本质是用并查集对向上标记法进行优化,是一种离线算法,时间复杂度 $$O(n+m)$$

求LCA的Tarjan算法主体由dfs实现,并用并查集进行优化。对于每个节点,我们增加一个标记:

  • 若该节点没有访问过,则初值为0
  • 若该节点已访问但还没有回溯,则标记为1
  • 若该节点已访问且已回溯,则标记为2

显然,对于当前访问的节点 x,它到根节点的路径一定都被标记为1。因此对于任意一个与 x相关的询问,设询问的另一个节点为 y,则 LCA(x,y)即为 y到根节点的路径中第一个,也就是最深的标记为1的节点。

求这个节点的方法可以用并查集优化。当一个节点的标记改为2的同时,将它合并到其父节点的集合当中。显然,此时它的父节点的标记一定为1,并且单独构成一个集合,因为这个父节点还没有进行过回溯操作。

在合并过后,遍历关于当前节点 x的所有询问,对于任意一个询问,若 y的标记为2,说明其已经被访问过,并且它的并查集指向的那个节点,也就是 y到根节点的路径中最深的还没有回溯的节点,一定就是 LCA(x,y)。

对于询问,我们可以用一个不定长数组存储与每个节点相关的询问,并且每个询问用一个二元组表示,第一维存储该询问的另一个节点,第二维存储该询问输入的次序,以便按顺序输出

这样,Tarjan算法求LCA的步骤就很明了了:

  1. 从根节点开始进行dfs

  2. 将当前节点标记为1

  3. 遍历当前节点的所有出边;若当前边的终点还没有访问过,则访问它,访问过后将该节点合并到当前节点的集合中;

  4. 遍历与当前节点相关的所有询问;若当前询问的另一个节点的标记为2,则该询问的答案即为另一个节点所在集合的代表元素

  5. 将当前节点标记为2

    题目描述

    如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

    输入格式

    第一行包含三个正整数 N,M,S,分别表示树的结点个数、询问的个数和树根结点的序号。

    接下来 N-1 行每行包含两个正整数 x, y,表示 xx 结点和 y 结点之间有一条直接连接的边(数据保证可以构成树)。

    接下来 M 行每行包含两个正整数 a, b,表示询问 a结点和 b 结点的最近公共祖先。

    输出格式

    输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。

/* input
5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5
*/

/* output
4
4
1
4
4
*/

// 
// Created by Jiang Yinzuo on 2020/10/28.
// Tarjan Algorithm O(n + m)

#include <cstdio>
#include <vector>

constexpr int MAX_N = 500000;
constexpr int MAX_M = 500000;
std::vector<int> tree[MAX_N + 1];
/// queries[x] = {y, id}
std::vector<std::pair<int, int> > queries[MAX_N];

///// Union Find /////
int father[MAX_N + 1];

int get_father(int v) {
    return father[v] == v ? v : father[v] = get_father(father[v]);
}

int answer[MAX_M + 1];

void tarjan(int cur_v) {
    static int tag[MAX_N + 1] = {0};
    tag[cur_v] = 1;
    for (auto i : tree[cur_v]) {
        if (tag[i] == 0) {
            tarjan(i);
            father[i] = cur_v; // merge child vertex to self
        }
    }
    for (auto &q : queries[cur_v]) {
        int y = q.first, id = q.second;
        if (tag[y] == 2) answer[id] = get_father(y);
    }
    tag[cur_v] = 2;
}

int main() {
    int n, m, root;
    scanf("%d %d %d", &n, &m, &root);
    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        scanf("%d %d", &u, &v);
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    for (int i = 0; i < m; ++i) {
        int a, b;
        scanf("%d %d", &a, &b);
        queries[a].emplace_back(b, i);
        queries[b].emplace_back(a, i);
    }

    for (int i = 0; i <= n; ++i) father[i] = i;

    tarjan(root);
    for (int i = 0; i < m; ++i) printf("%d\n", answer[i]);
    return 0;
}

树上差分

根据每条边两边的点的情况,计算边的贡献的dfs

CF1401D

2017 ICPC 沈阳 Tree

2019 CCPC 威海 C

给一棵带边权的树$$<E, V, W>$$,给3个树上的点集$$V_1, V_2, V_3 \subseteq V$$。记$$v_1 \in V_1, v_2 \in V_2, v_3 \in V_3$$,求$$\mathbb{E}[{f(v_1, v_2, v_3)}]$$。$$f(v_1, v_2, v_3) = \min \limits_{v \in V}(\sum_{i=1}^3 distance(v_i, v))$$

解:观察得,$$f(v_1, v_2, v_3) = \frac{1}{2}(distance(v_1, v_2) + distance(v_2, v_3) + distance(v_3, v_1))$$

故 $$ \mathbb{E}[{f(v_1, v_2, v_3)}] = \mathbb{E}[\frac{1}{2}(distance(v_1, v_2) + distance(v_2, v_3) + distance(v_3, v_1))] \ = \frac{1}{2}( \mathbb{E}[distance(v_1, v_2)] + \mathbb{E}[distance(v_2, v_3)] + \mathbb{E}[distance(v_1, v_3)]) $$ 对一棵树,计算每条边的贡献:一条边的两侧分别有多少个$$v_1, v_2, v_3$$

#include <cstdio>
#include <vector>

constexpr int MAX_N = 200001;
struct Vertex {
    int to;
    unsigned long long weight;
};
std::vector<Vertex> tree[MAX_N];
long long sci_cnt[3][MAX_N] = {0};
long long tot_cnt[3];
bool has[3][MAX_N] = {false};

unsigned long long ans[3] = {0};

void dfs(int cur_v, int father, unsigned long long fa_cur_weight) {
    for (int i = 0; i < 3; ++i) {
        sci_cnt[i][cur_v] += has[i][cur_v];
    }
    for (auto &v : tree[cur_v]) {
        if (v.to != father) {
            dfs(v.to, cur_v, v.weight);
            for (int i = 0; i < 3; ++i) {
                sci_cnt[i][cur_v] += sci_cnt[i][v.to];
            }
        }
    }
    for (int i = 0; i < 3; ++i) {
        ans[i] += fa_cur_weight * (sci_cnt[i][cur_v] *
                                   (tot_cnt[(i + 1) % 3] - sci_cnt[(i + 1) % 3][cur_v]) +
                                   sci_cnt[(i + 1) % 3][cur_v] * (tot_cnt[i] - sci_cnt[i][cur_v]));
    }
}

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        unsigned long long w;
        scanf("%d %d %llu", &u, &v, &w);
        tree[u].push_back({v, w});
        tree[v].push_back({u, w});
    }
    int i = 0;
    for (auto &ha : has) {
        int plc;
        scanf("%d", &tot_cnt[i]);
        for (int j = 0; j < tot_cnt[i]; ++j) {
            scanf("%d", &plc);
            ha[plc] = true;
        }
        ++i;
    }
    dfs(1, 0, 0);
    double answer = 0;
    for (i = 0; i < 3; ++i) {
        answer += ans[i] / (double )(tot_cnt[i] * tot_cnt[(i+1)%3]);
    }
    printf("%.12lf\n", answer / (double )2);
    return 0;
}

树的直径

两次dfs

先从任意一点P出发,找离它最远的点Q,再从点Q出发,找离它最远的点W,W到Q的距离就是是的直径

int max_depth = 0;
int end1;
void get_diameter(int cur, int father, int depth) {
    if (tree[cur].size() == 1 && father != -1 && depth > max_depth) {
        max_depth = depth;
        end1 = cur;
    }
    for (auto i : tree[cur]) {
        if (i != father) {
            get_diameter(i, cur, depth + 1);
        }
    }
}

int main() {
    max_depth = 0;
    get_diameter(1, -1, 1);
    get_diameter(end1, -1, 1);
    int diameter = max_depth;
    return 0;
}

树形DP

树的中心

树的直径的中点称为树的中心。以树的中心为整棵树的根时,从该根到每个叶子节点的最长路径最短

例题:给一棵树(n个点),求最少移去几个点,使得剩下的图仍然构成一棵树,且直径为k

解:设直径为D。

k为偶数时,一棵树上存在一点V使得树上任意一点到V的距离小于等于D/2。

k为奇数时,存在一条边E使得树上任意一点到E的其中一个端点距离小于等于$\lfloor D/2 \rfloor$

故只需枚举点V或者边E,dfs求出距离小于等于D/2的点的个数,并维护最大值即可。

//
// Created by Jiang Yinzuo on 2020/12/11.
//

#include <cstdio>
#include <vector>

constexpr int MaxN = 2001;

std::vector<int> tree[MaxN];
std::vector<std::pair<int, int>> edges;

int ans;
void dfs(int cur_v, int father, int depth, const int max_depth) {
    if (depth > max_depth) {
        return;
    }
    --ans;
    for (auto i : tree[cur_v]) {
        if (i != father) {
            dfs(i, cur_v, depth + 1, max_depth);
        }
    }
}

int main() {
    int n, k;
    scanf("%d %d", &n, &k);
    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        scanf("%d %d", &u, &v);
        tree[u].push_back(v);
        tree[v].push_back(u);
        (void)edges.emplace_back(u, v);
    }

    int min_ans = n;
    if (k % 2 == 0) {
        for (int i = 1; i <= n; ++i) {
            ans = n;
            dfs(i, 0, 0, k / 2);
            min_ans = min_ans < ans ? min_ans : ans;
        }
    } else {
        for (auto &p : edges) {
            ans = n;
            dfs(p.first, p.second, 0, k / 2);
            dfs(p.second, p.first, 0, k / 2);
            min_ans = min_ans < ans ? min_ans : ans;
        }
    }

    printf("%d\n", min_ans);
    return 0;
}

树的重心

定义

对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心。

(这里以及下文中的“子树”都是指无根树的子树,即包括“向上”的那棵子树,并且不包括整棵树自身。)

根据定义求树的重心

//
// Created by jiang on 2020/8/15.
// poj1655

#include <cstdio>
#include <vector>

std::vector<int> tree[200001];
int size[200001];
int max_size[200001];
int n;
int centroid, min_max_size = 0x7fffffff;

void dfs(int cur_v, int father) {
    size[cur_v] = max_size[cur_v] = 1;
    for (auto &i : tree[cur_v]) {
        if (i != father) {
            dfs(i, cur_v);
            size[cur_v] += size[i];
            max_size[cur_v] = std::max(max_size[cur_v], size[i]);
        }
    }
    max_size[cur_v] = std::max(max_size[cur_v], n - size[cur_v]);
    if (min_max_size > max_size[cur_v]) {
        centroid = cur_v;
        min_max_size = max_size[cur_v];
    }
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        int u, v;
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) tree[i].clear();
        for (int i = 0; i < n - 1; ++i) {
            scanf("%d %d", &u, &v);
            tree[u].push_back(v);
            tree[v].push_back(u);
        }
        min_max_size = 0x7fffffff;
        dfs(1, 0);
        printf("%d %d\n", centroid, min_max_size);
    }
    return 0;
}

性质

  1. 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。
  2. 树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。
  3. 把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上。
  4. 在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
  5. 一棵树最多有两个重心,且这两个重心相邻。

求所有子树的重心(徐州ICPC2019)

//
// Created by jiang on 2020/8/15.
// https://nanti.jisuanke.com/t/42552

#include <cstdio>
#include <vector>

#define MAXN 200001
std::vector<int> tree[MAXN];

/**
 * fathers: 每个节点的父亲
 * depth: 每个节点的深度
 * centroids: 每个子树深度较深的重心
 * size: 每个子树的节点数量
 */
int fathers[MAXN], depth[MAXN], centroids[MAXN], size[MAXN];

/**
 * 合并两棵子树的重心时,新的重心一定在子树重心的连线上
 * @param cur_v 合并后的根节点
 * @param c1 子树1的重心
 * @param c2 子树2的重心
 */
void merge(int cur_v, int c1, int c2) {
    while (depth[c1] > depth[cur_v] && size[c1] < size[cur_v] - size[c1])
        c1 = fathers[c1];
    while (depth[c2] > depth[cur_v] && size[c2] < size[cur_v] - size[c2])
        c2 = fathers[c2];
    centroids[cur_v] = depth[c1] > depth[c2] ? c1 : c2;
}

void dfs(int cur_v) {
    size[cur_v] = 1;
    centroids[cur_v] = cur_v;
    for (auto &i : tree[cur_v]) {
        if (i != fathers[cur_v]) {
            fathers[i] = cur_v;
            depth[i] = depth[cur_v] + 1;
            dfs(i);
            size[cur_v] += size[i];
            merge(cur_v, centroids[cur_v], centroids[i]);
        }
    }
}

int main() {
    int n;
    scanf("%d", &n);

    int u, v;
    for (int i = 0; i < n - 1; ++i) {
        scanf("%d %d", &u, &v);
        tree[u].push_back(v);
        tree[v].push_back(u);
    }
    fathers[1] = 0;
    depth[1] = 1;
    dfs(1);

    // 一棵树最多2个重心
    for (int i = 1; i <= n; ++i) {
        if (fathers[centroids[i]] && size[centroids[i]] * 2 == size[i]) {
            if (centroids[i] < fathers[centroids[i]])
                printf("%d %d\n", centroids[i], fathers[centroids[i]]);
            else
                printf("%d %d\n", fathers[centroids[i]], centroids[i]);
        } else {
            printf("%d\n", centroids[i]);
        }
    }
    return 0;
}

cf1406c:把有两个重心的树变成一个重心

找到一个重心的一边的叶子节点,和另一个重心相连。

//
// Created by jiang on 2020/9/17.
// 求树的重心

#include <cstdio>
#include <vector>

std::vector<int> tree[200001];
int size[200001];
int max_size[200001];
int n;
std::vector<int> centroids;
int min_max_size;

void dfs(int cur_v, int father) {
    size[cur_v] = max_size[cur_v] = 1;
    for (auto &i : tree[cur_v]) {
        if (i != father) {
            dfs(i, cur_v);
            size[cur_v] += size[i];
            max_size[cur_v] = std::max(max_size[cur_v], size[i]);
        }
    }
    max_size[cur_v] = std::max(max_size[cur_v], n - size[cur_v]);
    if (min_max_size >= max_size[cur_v]) {
        if (min_max_size > max_size[cur_v]) centroids.clear();
        centroids.push_back(cur_v);
        min_max_size = max_size[cur_v];
    }
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        int u, v;
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) tree[i].clear();
        for (int i = 0; i < n - 1; ++i) {
            scanf("%d %d", &u, &v);
            tree[u].push_back(v);
            tree[v].push_back(u);
        }
        min_max_size = 0x7fffffff;
        dfs(1, 0);
        if (centroids.size() == 1) {
            printf("%d %d\n", tree[1][0], 1);
            printf("%d %d\n", tree[1][0], 1);
        } else {
            int c1 = centroids[0], c2 = centroids[1];
            int child, cur = c1, father = c2;
            while (tree[cur].size() > 1) {
                for (int i = 0; (child = tree[cur][i]) == father; ++i);
                father = cur;
                cur = child;
            }
            printf("%d %d\n", cur, father);
            printf("%d %d\n", cur, c2);
        }
    }
    return 0;
}

启发式合并

洛谷U41492:树上每个节点有一个颜色,求任意子树的颜色数量。

如果给每个子树记录颜色,可能会超内存。因此需要将轻儿子的颜色合并到重儿子上。

//
// Created by Jiang Yinzuo on 2020/12/7.
//

#include <cstdio>
#include <unordered_set>
#include <vector>

std::vector<int> tree[100001];
int color[100001];

int heavy_child[100001];
int size[100001];

/*
 * 求重儿子
 */
void dfs1(int cur_v, int father) {

    size[cur_v] = 1;
    int max_size = 0, heaviest_child = 0;
    for (auto &i : tree[cur_v]) {
        if (i != father) {
            dfs1(i, cur_v);
            size[cur_v] += size[i];
            if (size[i] > max_size) {
                max_size = size[i];
                heaviest_child = i;
            }
        }
    }
    heavy_child[cur_v] = heaviest_child;
}

int color_cnt[100001];

std::unordered_set<int> dfs2(int cur_v, int father) {
    if (tree[cur_v].size() == 1 && father != 0) {
        std::unordered_set<int> exist;
        color_cnt[cur_v] = 1;
        exist.insert(color[cur_v]);
        return exist;
    }

    auto heavy_exist = dfs2(heavy_child[cur_v], cur_v);
    for (auto &i : tree[cur_v]) {
        if (i != father && i != heavy_child[cur_v]) {
            auto light_exist = dfs2(i, cur_v);
            heavy_exist.merge(light_exist);
        }
    }

    heavy_exist.insert(color[cur_v]);
    color_cnt[cur_v] = heavy_exist.size();
    return heavy_exist;
}

int main() {
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        scanf("%d %d", &u, &v);
        tree[u].push_back(v);
        tree[v].push_back(u);
    }
    for (int i = 1; i <= n; ++i) {
        scanf("%d", color + i);
    }
    dfs1(1, 0);
    dfs2(1, 0);
    int m;
    scanf("%d", &m);
    for (int i = 0; i < m; ++i) {
        int q;
        scanf("%d", &q);
        printf("%d\n", color_cnt[q]);
    }
    return 0;
}

树链剖分

定义

1.重儿子:一个节点的儿子节点里面,以重儿子为根的子树大小最大。

2.轻儿子:一个节点的儿子节点,除了唯一一个重儿子,其余儿子为轻儿子。

3.重边:连接父亲和重儿子的边称为重边。

4.轻边:连接父亲和轻儿子的边称为轻边。

5.重链:只由重边构成的链称为重链。

将树从x到y结点最短路径上所有节点的值都加上z

树上差分O(n+m)

求树从x到y结点最短路径上所有节点的值之和

LCA:dfs O(n)预处理每个节点的dis(即到根节点的最短路径长度)

然后对于每个询问,求出x,y两点的lca,利用lca的性质distance ( x , y ) = dis ( x ) + dis ( y ) - 2 * dis ( lca )求出结果

时间复杂度O(mlogn+n)

洛谷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为根节点的子树内所有节点值之和

输入格式

第一行包含 44 个正整数 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 取模)。

//
// Created by Jiang Yinzuo on 2020/10/28.
//

#include <cstdio>
#include <vector>

constexpr int MAX_N = 200004;

std::vector<int> tree[MAX_N + 1];

////////////// tree split
int father[MAX_N];
int depth[MAX_N];
int size[MAX_N];
int heavy_son[MAX_N] = {0};
int chain_top[MAX_N]; // chain_top[v] =  top of the heavy chain containing v
int dfn[MAX_N], dfn_cnt = 0; // dfs order
int rank[MAX_N]; // rank[dfn[v]] = v

int weight[MAX_N];
int w[MAX_N];

/// process father, depth, size and heavy_son
void dfs1(int cur_v, int fa, int dep) {
    father[cur_v] = fa;
    depth[cur_v] = dep;
    size[cur_v] = 1;
    for (auto i : tree[cur_v]) {
        if (i != fa) {
            dfs1(i, cur_v, dep + 1);
            size[cur_v] += size[i];
            if (size[i] >= size[heavy_son[cur_v]]) {
                heavy_son[cur_v] = i;
            }
        }
    }
}

void dfs2(int cur_v, int top) {
    chain_top[cur_v] = top;
    dfn[cur_v] = ++dfn_cnt;
    weight[dfn_cnt] = w[cur_v];
    rank[dfn_cnt] = cur_v;
    if (!heavy_son[cur_v]) return;
    dfs2(heavy_son[cur_v], top);

    for (auto i : tree[cur_v]) {
        if (i != heavy_son[cur_v] && i != father[cur_v]) {
            dfs2(i, i); // top of a heavy chain
        }
    }
}
///////////////////

///// segment tree
int arr[(MAX_N << 2) + 1], lazy_tag[(MAX_N << 2) + 1] = {0};
int mod;

void push_down(int cur_v, int len) {
    lazy_tag[cur_v << 1] += lazy_tag[cur_v];
    lazy_tag[cur_v << 1 | 1] += lazy_tag[cur_v];
    arr[cur_v << 1] += lazy_tag[cur_v] * (len - (len >> 1));
    arr[cur_v << 1 | 1] += lazy_tag[cur_v] * (len >> 1);
    arr[cur_v << 1] %= mod;
    arr[cur_v << 1 | 1] %= mod;
    lazy_tag[cur_v] = 0;
}

void build(int cur_v, int l, int r) {
    if (l == r) {
        arr[cur_v] = weight[l] % mod;
        return;
    }
    int mid = (l + r) / 2;
    build(cur_v << 1, l, mid);
    build(cur_v << 1 | 1, mid + 1, r);
    arr[cur_v] = (arr[cur_v << 1] + arr[cur_v << 1 | 1]) % mod;
}

int query(int cur_v, int l, int r, int L, int R) {
    int res = 0;
    if (L <= l && r <= R) {
        return arr[cur_v] % mod;
    }
    if (lazy_tag[cur_v]) push_down(cur_v, r - l + 1);
    int mid = (l + r) / 2;
    if (L <= mid) res = (res + query(cur_v << 1, l, mid, L, R)) % mod;
    if (R > mid) res = (res + query(cur_v << 1 | 1, mid + 1, r, L, R)) % mod;
    return res;
}

void update(int cur_v, int l, int r, const int L, const int R, int val) {
    if (L <= l && r <= R) {
        lazy_tag[cur_v] += val;
        arr[cur_v] += val * (r - l + 1);
    } else {
        if (lazy_tag[cur_v])push_down(cur_v, r - l + 1);
        int mid = (l + r) / 2;
        if (L <= mid)update(cur_v << 1, l, mid, L, R, val);
        if (R > mid)update(cur_v << 1 | 1, mid + 1, r, L, R, val);
        arr[cur_v] = (arr[cur_v << 1] + arr[cur_v << 1 | 1]) % mod;
    }
}

////////////
int n, m, r;

int query_range(int x, int y) {
    int ans = 0;

    // 当两个点不在同一条链上
    while (chain_top[x] != chain_top[y]) {
        // 把x点改为所在链顶端的深度更深的那个点
        if (depth[chain_top[x]] < depth[chain_top[y]])
            std::swap(x, y);
        // ans加上x点到x所在链顶端 这一段区间的点权和
        ans = (ans + query(1, 1, n, dfn[chain_top[x]], dfn[x])) % mod;
        // 把x跳到x所在链顶端的那个点的上面一个点
        x = father[chain_top[x]];
    }
    // 直到两个点处于一条链上
    // 把x跳到x所在链顶端的那个点的上面一个点
    if (depth[x] > depth[y]) std::swap(x, y);
    // 把x跳到x所在链顶端的那个点的上面一个点
    return (ans + query(1, 1, n, dfn[x], dfn[y])) % mod;
}

void update_range(int x, int y, int val) {
    val %= mod;
    while (chain_top[x] != chain_top[y]) {
        if (depth[chain_top[x]] < depth[chain_top[y]])std::swap(x, y);
        update(1, 1, n, dfn[chain_top[x]], dfn[x], val);
        x = father[chain_top[x]];
    }
    if (depth[x] > depth[y])std::swap(x, y);
    update(1, 1, n, dfn[x], dfn[y], val);
}

int query_subtree(int x) {
    // 把x跳到x所在链顶端的那个点的上面一个点
    return query(1, 1, n, dfn[x], dfn[x] + size[x] - 1);
}

void update_subtree(int x, int val) {
    update(1, 1, n, dfn[x], dfn[x] + size[x] - 1, val);
}


int main() {
    scanf("%d %d %d %d", &n, &m, &r, &mod);

    for (int i = 1; i <= n; ++i) {
        scanf("%d", w + i);
    }
    for (int i = 0; i < n - 1; ++i) {
        int u, v;
        scanf("%d %d", &u, &v);
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    dfs1(r, 0, 1); // r:树的根
    dfs2(r, r);
    build(1, 1, n);
    for (int i = 0; i < m; ++i) {
        int op, x, y, z;
        scanf("%d", &op);
        if (op == 1) {
            scanf("%d %d %d", &x, &y, &z);
            update_range(x, y, z);
        } else if (op == 2) {
            scanf("%d %d", &x, &y);
            printf("%d\n", query_range(x, y));
        } else if (op == 3) {
            scanf("%d %d", &x, &z);
            update_subtree(x, z);
        } else {
            scanf("%d", &x);
            printf("%d\n", query_subtree(x));
        }
    }
    return 0;
}