树上动态DP(树上DDP)

动态DP

oi-wiki的动态DP本来讲的就是树上DP,是一种解决带单点修改操作的DP问题的方法。树上DDP首先需要掌握重链剖分,因此有一点小门槛,可以先看看线性DDP。对线性DDP有了一定的了解,再将其结合到树链剖分中,即可完成树上DDP。

树上DDP

如果是从线性DDP入手的话,当来到树上DP时,会发现无法下手。因为通常而言 D i D_i Di肯定与 i i i的所有子节点有关,而不是仅与 D i − 1 D_{i-1} Di1有关。特别是在树上 i − 1 i-1 i1 i i i大概率是不相邻的。那么在树链剖分中,与节点 i i i线性相邻的点是哪一个点呢?显然是 i i i的重儿子。因此提示我们可以把 D i D_i Di写成与 D h i D_{h_i} Dhi有关的形式,其中 h i h_i hi表示 i i i的重儿子。

考虑一个极简的题目,牛客204871

给定一个树,节点有权值。有两类操作,修改一个点的权值,或者查询某个子树的权值和。

这个题是一个标准的模板题,有很多做法,树链剖分dfs序、以及树上启发式合并均可。这里用DDP做一下。

很明显, D i = A i + ∑ j 是 i 的儿子 D j D_i=A_i+\sum_{j是i的儿子}D_j Di=Ai+ji的儿子Dj

将重儿子单独拿出来,写成 D i = A i + D h i + ∑ j 是 i 的轻儿子 D j D_i=A_i+D_{h_i}+\sum_{j是i的轻儿子}D_j Di=Ai+Dhi+ji的轻儿子Dj

将重儿子项剔除,剩下的记作: L i = A i + ∑ j 是 i 的轻儿子 D j L_i=A_i+\sum_{j是i的轻儿子}D_j Li=Ai+ji的轻儿子Dj

于是: D i = D h i + L i D_i=D_{h_i}+L_i Di=Dhi+Li

写成矩阵的形式:
[ D i 1 ] = [ 1 L i 0 1 ] × [ D h i 1 ] \begin{bmatrix} D_i \\ 1 \end{bmatrix}=\begin{bmatrix} 1 & L_i \\ 0 & 1 \end{bmatrix}\times{\begin{bmatrix} D_{h_i} \\ 1 \end{bmatrix}} [Di1]=[10Li1]×[Dhi1]

仿照线性DDP,DP方程改为了线性相邻的两个规划目标的矩阵乘积形式,且系数矩阵只与 i i i有关。用线段树维护重链上的矩阵乘积,可以很容易的求出任意 D i D_i Di。当然,需要知道 i i i所在重链的尾节点(即叶子节点)。

对于单点修改操作,假设修改的是节点 i i i,则 i i i所在的重链是作为轻儿子挂在某个节点下的,因此要修改对应的 L p a r e n t t o p L_{parent_{top}} Lparenttop,其中 t o p top top i i i所在重链的顶节点, p a r e n t t o p parent_{top} parenttop则是 t o p top top的父亲, t o p top top重链显然是 p a r e n t parent parent的一个轻儿子。当然还没有完,还需要往上跳 p a r e n t parent parent所在的重链。总之沿着重链往上跳,修改相应的 L L L即可。这个时间是 O ( log ⁡ N ) O(\log{N}) O(logN)的。

初始化的具体实现,在第一遍dfs可以求出 D D D,第二遍dfs可以求出 L L L

这个矩阵形式稍微推导一下,即可发现维护矩阵乘积实际上就是维护 L i L_i Li的累加和,因此线段树直接维护和就行了。

#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;

struct HLD{ // 重链剖分

using llt = long long;

using value_type = llt;
vector<value_type> data; // 线段树

using lazy_type = llt;
vector<lazy_type> lazy; // 延迟标记

/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {
    return ls + rs;
}

/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {
    data[t] += delta;
}

static value_type mkValue(llt data){
    return data;
}

/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {
    static const lazy_type LAZY0 = 0;
    return LAZY0; 
}

/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {
    static const value_type VALUE0 = 0;
    return VALUE0;
}

/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {
    if(a <= s and e <= b) {
        return data[t];
    }

    int mid = (s + e) >> 1;
    value_type ans = value_zero();
    if(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));
    if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));
    return ans;
}

/// 单点修改
void _modify(int t, int s, int e, int pos, const lazy_type & delta){
    if(s == e){
        _dn_(t, s, e, delta);
        return;
    }
    int mid = (s + e) >> 1;
    if(pos <= mid) _modify(lson(t), s, mid, pos, delta);
    else _modify(rson(t), mid + 1, e, pos, delta);
    _pushUp(t);
    return;
}

/// 这个函数不用动
void _pushUp(int t) {
    data[t] = _up_(data[lson(t)], data[rson(t)]);
}

/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}

int N;
/// 树结构, 1-index
vector<vector<int>> g;
/// 点权值
vector<llt> weight;

/// 建单向边
void mkDiEdge(int a, int b){
    g[a].push_back(b);
}
/// 建双向边
void mkBiEdge(int a, int b){
    mkDiEdge(a, b); mkDiEdge(b, a);
}

/// 树链剖分结构
struct node_t{
    int parent; // 父节点
    int hson;   // 重儿子
    int depth;  // 该节点的深度, 根节点深度为0
    int size;   // 本节点所领子树的节点总数
    int top;    // 本节点所在重链的顶,采用的是原树编号
    int bot;    // 本节点所在重链的底,但是采用的是新编号
    int nid;    // 本节点在线段树中的编号, 即dfs序
    int mdes;   // 本节点所领子树的线段树编号均在[nid, mdes]中,采用的是新编号
};

int root; // 树根
vector<int> nid2old; // nid2old[i]表示线段树中第i个节点在原树中的编号
int timestamp; // 辅助变量
vector<node_t> nodes;

vector<value_type> D;
vector<value_type> L;

/// 递归找重边
void _dfsHeavyEdge(int u, int p, int d){
    auto & n = nodes[u];
    n.parent = p;
    n.depth = d;
    n.size = 1;
    D[u] = weight[u];

    for(auto v : g[u]){
        if(v == p) continue;
        _dfsHeavyEdge(v, u, d + 1);
        n.size += nodes[v].size;
        if(nodes[n.hson].size < nodes[v].size) n.hson = v;
        D[u] += D[v];
    }
    return;
}

/// 递归找重链
void _dfsHeavyPath(int u, int top){
    auto & n = nodes[u];
    n.top = top;
    nid2old[n.mdes = n.nid = ++timestamp] = u;
    L[u] = weight[u];

    if(0 == n.hson) return (void)(n.bot = n.nid);

    _dfsHeavyPath(n.hson, top);
    n.mdes = max(n.mdes, nodes[n.hson].mdes);
    n.bot = nodes[n.hson].bot;

    for(auto v : g[u]){
        if(v != n.parent and v != n.hson){
            _dfsHeavyPath(v, v);
            n.mdes = max(n.mdes, nodes[v].mdes);
            L[u] += D[v];
        }
    }
    return;
}
/// 递归建线段树
void _build(int t, int s, int e) {
    if(s == e) {
        data[t] = mkValue(L[nid2old[s]]); // 注意线段树编号与原树编号存在转换
        return; 
    }
    int mid = (s + e) >> 1;
    _build(lson(t), s, mid);
    _build(rson(t), mid + 1, e);
    _pushUp(t);
}

/// 初始化, n是树的点数
void init(int n){
    N = n;
    timestamp = 0;
    /// 初始化树结构
    g.assign(N + 1, {});
    weight.assign(N + 1, 0);
    /// 初始化树链结构
    nodes.assign(N + 1, {0, 0, 0, 0, 0, 0, 0});
    nid2old.assign(N + 1, 0);
    /// 初始化线段树结构
    data.assign(N + 1 << 2, value_zero());
    lazy.assign(N + 1 << 2, lazy_zero());    
    /// 初始化DP数据
    D.assign(N + 1, {});
    L.assign(N + 1, {});
    return;
}

/// 在输入所有数据以后构建
void build(int root){
    /// 建树链
    _dfsHeavyEdge(this->root = root, 0, 0);
    _dfsHeavyPath(root, root);
    /// 建线段树
    _build(1, 1, N);
}

/// 求原树上x和y的LCA
int lca(int x, int y){
    while(nodes[x].top != nodes[y].top){
        if(nodes[nodes[x].top].depth < nodes[nodes[y].top].depth) y = nodes[nodes[y].top].parent;
        else x = nodes[nodes[x].top].parent;
    }
    return nodes[x].depth <= nodes[y].depth ? x : y;
}

/// 查询原树上x的信息,就是线段树上[nid, bot]的区间信息
value_type query(int x){
    return _query(1, 1, N, nodes[x].nid, nodes[x].bot);
}

/// 原树上的单点修改
void modify(int x, const lazy_type & delta){
    while(x){
        _modify(1, 1, N, nodes[x].nid, delta);
        x = nodes[nodes[x].top].parent;
    }
    return;
}

};

HLD Tree;
int N, Q, Root;

void work(){
    cin >> N >> Q >> Root;
    Tree.init(N);
    for(int i=1;i<=N;++i) cin >> Tree.weight[i];
    for(int a,b,i=1;i<N;++i){
        cin >> a >> b;
        Tree.mkBiEdge(a, b);
    }    
    Tree.build(Root);
    for(int cmd,a,x,q=1;q<=Q;++q){
        cin >> cmd >> a;
        // cout << q << ": " << cmd << ", " << a << endl;
        if(2 == cmd){
            auto ans = Tree.query(a);
            cout << ans << "\n";
        }else{
            cin >> x;
            Tree.modify(a, x);
        }
    }
    return;
}

int main(){
#ifndef ONLINE_JUDGE
    freopen("z.txt", "r", stdin);
#endif
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int nofkase = 1;
    // cin >> nofkase;
    while(nofkase--) work();
    return 0;
}

也可以用洛谷B3861练习初始化与查询操作。不过这两道题都只能用来熟悉将DP方程转为矩阵维护的初始化与查询操作。这两个例子太过简单,一个是没有修改操作,一个是修改操作太过简易(因为实际上没有用到矩阵乘法,只用到了单一运算)。因此这两个例子并没有体现出树上DDP修改操作的一般流程。

修改操作的一般流程

树上DDP的修改,显然是要通过重链一路向上,直到树根。关于修改操作的一般流程,有几点:

  1. 时刻维护 L L L数组与权值数组,因此其中的值是实时的;
  2. 不维护 D D D数组,因此其中的值只是最初的规划值;
  3. D D D的实时值是通过线段树算出来的。

简要的说,就是只维护 L L L,不维护 D D D。当然,根据规划方程的具体内容,有时候也能实时维护 D D D,例如下文要提到的ABC351G,不过实时维护 D D D并不一定总是可行。这里说的是一般流程。一般而言,总需要获取新旧 L L L值,与新旧 D D D值。

# x为要修改的节点
# delta为本次修改x的权值的增加量
def modify(x, delta):
    if 0 == delta: return
    
    A[x] += delta # 修改权值,只有这一个权值需要修改
    
    oldL = L[x] # 获取旧的L值,如果不时刻维护L数组,就要用线段树查询来获取oldL
    newL        # 根据delta与旧L值确定新的L值
    while True:
        # 令top是x重链的顶
        oldTop = query(top) # 查询旧的Dtop值
        _modify(1, 1, N, x的nid, L[x] = newL) # 修改x位置,此时该重链修改完毕。为了统一起见,线段树的修改总是视为设置操作
        
        curTop = quety(top) # 查询新的Dtop值
        if(curTop == oldTop) break
        
        # 令parent是top的父节点
        if 0 == parent: break

        oldL = L[parent] # 获取旧的L值
        newL             # 根据oldTop,curTop与oldL确定newL

        x = parent    

这其中根据规划方程的具体内容,有些地方可以简化。例如上一题中,完全不需要什么新旧 D D D值与 L L L值,只需要delta就可以确定每条重链要修改的内容。不过,这是一个一般流程,应该总是可行的。

洛谷P4719

这是oi-wiki DDP使用的模板题,当然洛谷本身也是作为模板题使用的。单点修改,查询最大权独立集。令 D i , 0 D_{i,0} Di,0表示 i i i子树不选节点 i i i的最大值, D i , 1 D_{i,1} Di,1为选 i i i的最大值,则
{ D i , 0 = ∑ j 是 i 的儿子 max ⁡ ( D j , 0 , D j , 1 ) D i , 1 = A i + ∑ j 是 i 的儿子 D j , 0 \begin{cases}D_{i,0}=\sum_{j是i的儿子}{\max(D_{j,0},D_{j,1})} \\ D_{i,1}=A_i+\sum_{j是i的儿子}{D_{j,0}}\end{cases} {Di,0=ji的儿子max(Dj,0,Dj,1)Di,1=Ai+ji的儿子Dj,0

仿照一般做法,令 L i L_i Li分别表示不选 i i i i i i的轻儿子能够提供的最大权值以及选 i i i时轻儿子能够提供的最大,有
D = [ max ⁡ ( D h , 0 + L i , 0 , D h , 1 + L i , 0 ) D h , 0 + L i , 1 ] D=\begin{bmatrix}\max(D_{h,0}+L_{i,0} ,D_{h,1}+L_{i,0}) \\ D_{h,0}+L_{i,1} \end{bmatrix} D=[max(Dh,0+Li,0,Dh,1+Li,0)Dh,0+Li,1]

其中:
L = [ ∑ j 是 i 的轻儿子 max ⁡ ( D j , 0 , D j , 1 ) A i + ∑ j 是 i 的轻儿子 D j , 0 ] L=\begin{bmatrix}\sum_{j是i的轻儿子}{\max(D_{j,0},D_{j,1})} \\ A_i+\sum_{j是i的轻儿子}{D_{j,0}} \end{bmatrix} L=[ji的轻儿子max(Dj,0,Dj,1)Ai+ji的轻儿子Dj,0]

写成广义矩阵乘法的形式有:
[ D i , 0 D i , 1 ] = [ L i , 0 L i , 0 L i , 1 − ∞ ] × [ D h i , 0 D h i , 1 ] \begin{bmatrix}D_{i,0} \\ D_{i, 1}\end{bmatrix} = \begin{bmatrix}L_{i,0} & L_{i,0} \\ L_{i,1} & -\infty \end{bmatrix} \times\begin{bmatrix}D_{h_i,0} \\ D_{h_i, 1}\end{bmatrix} [Di,0Di,1]=[Li,0Li,1Li,0]×[Dhi,0Dhi,1]

这里的 × \times ×表示矩阵的类乘操作或者说是矩阵的广义乘法操作,定义如下:令矩阵 C = A × B C=A\times{B} C=A×B,则
C i , j = max ⁡ k = 1 3 ( A i , k + B k , j ) C_{i,j}=\max_{k=1}^{3}{(A_{i,k}+B_{k,j})} Ci,j=k=1max3(Ai,k+Bk,j)

于是,树链剖分之后用线段树维护这个矩阵乘法即可。

#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;

using llt = long long;
llt const INF = 0x7F1F2F3F4F5F6F7F;
llt const NINF = -INF;

llt chkadd(llt a, llt b){
    if(NINF == a or NINF == b) return NINF;
    return a + b;
}

struct HLD{ // 重链剖分

using llt = long long;

using value_type = array<llt, 4>; // 2 * 2 的矩阵 
vector<value_type> data; // 线段树

using lazy_type = array<llt, 2>; // L0, L1
vector<lazy_type> lazy; // 延迟标记

/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {
    return {
        max(chkadd(ls[0], rs[0]), chkadd(ls[1], rs[2])),
        max(chkadd(ls[0], rs[1]), chkadd(ls[1], rs[3])),
        max(chkadd(ls[2], rs[0]), chkadd(ls[3], rs[2])),
        max(chkadd(ls[2], rs[1]), chkadd(ls[3], rs[3]))
    };
}

/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {
    data[t][0] = data[t][1] = delta[0];
    data[t][2] = delta[1];
}

/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {
    static const lazy_type LAZY0 = {0LL, 0LL};
    return LAZY0; 
}

/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {
    static const value_type VALUE0 = {0LL, NINF, NINF, 0LL};
    return VALUE0;
}

/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {
    if(a <= s and e <= b) {
        return data[t];
    }
 
    int mid = (s + e) >> 1;
    value_type ans = value_zero();
    if(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));
    if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));
    return ans;
}

/// 几乎不用动
void _modify(int t, int s, int e, int pos, const lazy_type & delta) {
    if(s == e) {
        _dn_(t, s, e, delta);
        return;
    }

    int mid = (s + e) >> 1;
    if(pos <= mid) _modify(lson(t), s, mid, pos, delta);
    else _modify(rson(t), mid + 1, e, pos, delta);
    _pushUp(t);
    return;
}

/// 这个函数不用动
void _pushUp(int t) {
    data[t] = _up_(data[lson(t)], data[rson(t)]);
}

/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}

int N;
/// 树结构, 1-index
vector<vector<int>> g;
/// 点权值
vector<llt> weight;

/// 建单向边
void mkDiEdge(int a, int b){
    g[a].push_back(b);
}
/// 建双向边
void mkBiEdge(int a, int b){
    mkDiEdge(a, b); mkDiEdge(b, a);
}

/// 树链剖分结构
struct node_t{
    int parent; // 父节点
    int hson;   // 重儿子
    int depth;  // 该节点的深度, 根节点深度为0
    int size;   // 本节点所领子树的节点总数
    int top;    // 本节点所在重链的顶,采用的是原树编号
    int bot;    // 本节点所在重链的底,但是采用的是新编号
    int nid;    // 本节点在线段树中的编号, 即dfs序
    int mdes;   // 本节点所领子树的线段树编号均在[nid, mdes]中,采用的是新编号
};

int root; // 树根
vector<int> nid2old; // nid2old[i]表示线段树中第i个节点在原树中的编号
int timestamp; // 辅助变量
vector<node_t> nodes;

/// DP的数据结构
vector<array<llt, 2>> D, L;

/// 递归找重边
void _dfsHeavyEdge(int u, int p, int d){
    auto & n = nodes[u];
    n.parent = p;
    n.depth = d;
    n.size = 1;

    auto & d0 = D[u][0];
    auto & d1 = D[u][1];
    d0 = 0, d1 = weight[u];

    for(auto v : g[u]){
        if(v == p) continue;
        _dfsHeavyEdge(v, u, d + 1);
        n.size += nodes[v].size;
        if(nodes[n.hson].size < nodes[v].size) n.hson = v;

        d0 += max(D[v][0], D[v][1]);
        d1 += D[v][0];
    }
    return;
}

/// 递归找重链
void _dfsHeavyPath(int u, int top){
    auto & n = nodes[u];
    n.top = top;
    nid2old[n.mdes = n.nid = ++timestamp] = u;

    auto & L0 = L[u][0];
    auto & L1 = L[u][1];
    L0 = 0, L1 = weight[u];

    if(0 == n.hson) return (void)(n.bot = n.nid);
    _dfsHeavyPath(n.hson, top);
    n.mdes = max(n.mdes, nodes[n.hson].mdes);
    n.bot = nodes[n.hson].bot;

    for(auto v : g[u]){
        if(v != n.parent and v != n.hson){
            _dfsHeavyPath(v, v);
            n.mdes = max(n.mdes, nodes[v].mdes);

            L0 += max(D[v][0], D[v][1]);
            L1 += D[v][0];
        }
    }
    return;
}
/// 递归建线段树
void _build(int t, int s, int e) {
    if(s == e) {
        data[t] = {
            L[nid2old[s]][0], L[nid2old[s]][0], 
            L[nid2old[s]][1], NINF
        };
        return; 
    }
    int mid = (s + e) >> 1;
    _build(lson(t), s, mid);
    _build(rson(t), mid + 1, e);
    _pushUp(t);
}

/// 初始化, n是树的点数
void init(int n){
    N = n;
    timestamp = 0;
    /// 初始化树结构
    g.assign(N + 1, {});
    weight.assign(N + 1, 0);
    /// 初始化树链结构
    nodes.assign(N + 1, {0, 0, 0, 0, 0, 0, 0});
    nid2old.assign(N + 1, 0);
    /// 初始化线段树结构
    data.assign(N + 1 << 2, value_zero());
    lazy.assign(N + 1 << 2, lazy_zero()); 
    /// 初始化DP
    D.assign(N + 1, {0LL, 0LL});
    L.assign(N + 1, {0LL, 0LL});
    return;   
}

/// 在输入所有数据以后构建
void build(int root){
    /// 建树链
    _dfsHeavyEdge(this->root = root, 0, 0);
    _dfsHeavyPath(root, root);
    /// 建线段树
    _build(1, 1, N);
}

/// 求原树上x和y的LCA
int lca(int x, int y){
    while(nodes[x].top != nodes[y].top){
        if(nodes[nodes[x].top].depth < nodes[nodes[y].top].depth) y = nodes[nodes[y].top].parent;
        else x = nodes[nodes[x].top].parent;
    }
    return nodes[x].depth <= nodes[y].depth ? x : y;
}

/// 查询原树上x子树的信息
value_type query(int x){
    return _query(1, 1, N, nodes[x].nid, nodes[x].bot);
}


void modify(int x, llt delta){
    if(0 == delta) return;

    /// 修改权值,只有一个权值需要修改
    weight[x] += delta;

    /// 查询oldL,确定newL
    auto oldL = L[x];
    array<llt, 2> newL {oldL[0], oldL[1] + delta};
    while(1){
        auto top = nodes[x].top;
        /// 查询老的Dtop
        auto oldTop = query(top);
        /// 修改Lx
        _modify(1, 1, N, nodes[x].nid, L[x] = newL);

        /// 查询新的Dtop
        auto curTop = query(top);
        if(oldTop == curTop) break;

        /// 处理父节点
        auto parent = nodes[top].parent;
        if(0 == parent) break;

        auto old0 = max(oldTop[0], oldTop[1]);
        auto old1 = max(oldTop[2], oldTop[3]);
        auto old = max(old0, old1);

        auto cur0 = max(curTop[0], curTop[1]);
        auto cur1 = max(curTop[2], curTop[3]);
        auto cur = max(cur0, cur1);

        auto cha1 = cur0 - old0;
        auto cha0 = cur - old;
        
        /// 查询oldL,确定newL
        oldL = L[parent];
        newL = {oldL[0] + cha0, oldL[1] + cha1};

        x = parent;
    }

    return;
}

};


HLD Tree;
int N, Q, Root;

void work(){
    cin >> N >> Q;
    Tree.init(N);
    for(int i=1;i<=N;++i) cin >> Tree.weight[i];
    for(int a,b,i=1;i<N;++i){
        cin >> a >> b;
        Tree.mkBiEdge(a, b);
    }
    Tree.build(1);

    for(int x,a,q=1;q<=Q;++q){
        cin >> x >> a;
        if(a != Tree.weight[x]){
            Tree.modify(x, a - Tree.weight[x]);
        }        
        auto ans = Tree.query(1);
        cout << *max_element(ans.begin(), ans.end()) << "\n";
    }
    return;
}

int main(){
#ifndef ONLINE_JUDGE
    freopen("z.txt", "r", stdin);
#endif
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int nofkase = 1;
    // cin >> nofkase;
    while(nofkase--) work();
    return 0;
}

ABC351G

标程使用了static top tree的做法,但这个题目很明显是树上DDP的模板题。令: D i = A i + ∏ j 是 i 的儿子 D j D_i=A_i+\prod_{j是i的儿子}D_j Di=Ai+ji的儿子Dj
这是题目所求,将轻重儿子分开,写作: D i = A i + L i × D h i D_i=A_i+L_i\times{D_{h_i}} Di=Ai+Li×Dhi
其中: L i = ∏ j 是 i 的轻儿子 D j L_i=\prod_{j是i的轻儿子}{D_j} Li=ji的轻儿子Dj
写成矩阵形式:
[ D i 1 ] = [ L i A i 0 1 ] × [ D h i 1 ] \begin{bmatrix} D_i \\ 1 \end{bmatrix}=\begin{bmatrix} L_i & A_i \\ 0 & 1 \end{bmatrix}\times{\begin{bmatrix} D_{h_i} \\ 1 \end{bmatrix}} [Di1]=[Li0Ai1]×[Dhi1]

这样就可以进行维护了。显然并不需要维护完整的矩阵,只需要维护两个数即可。这里要维护的是标准的矩阵乘法,既有加法又有乘法,因此修改操作要注意。总体而言,修改操作也是顺着重链往上,依次修改topparentL。此处修改需要知道L原来的值,可以通过查询D[top]获取。

本题的修改流程大概长这样,对原始的x,其L显然不变,因此修改A即可。对以后的topparent,其A显然不变,需要修改L。对于某个parent节点而言,除去x的其他轻儿子的积等于 L p a r e n t D x \frac{L_{parent}}{D_x} DxLparent,这里分母的 D x D_x Dx指修改之前的值,可以通过查询 D D D数组获取。然后再乘以新的 D x D_x Dx即可,而新的 D x D_x Dx可以通过查询 x x x所在重链的矩阵累乘积得到。

/// 原树上的单点修改
void modify(int x, llt delta){
    weight[x] = delta;
    /// 修改
    _modify(1, 1, N, nodes[x].nid, {L[x], delta});

    while(1){
        auto top = nodes[x].top;
        auto parent = nodes[top].parent;
        if(0 == parent) break;

        /// 查询当前top的值
        auto curTop = query(top);
        // if(D[top] == curTop[1]) break;

        auto oL = L[parent];
        llt curL = oL * inv(D[top]) % MOD * curTop[1];        
        D[top] = curTop[1];
        L[parent] = curL;

        /// 修改parent
        _modify(1, 1, N, nodes[parent].nid, {curL, weight[parent]});
        x = parent;
    }
    return;
}

然而这段代码有个问题,会wa。问题在于如果有 D x D_x Dx为零,就无法正常得到parent节点新的L值。需要特判。特判之后的方程变为:
D i = { A i + L i × D h i 轻儿子的 D 全不为 0 A i 存在某个轻儿子 j 满足 D j = 0 D_i=\begin{cases}A_i+L_i\times{D_{h_i}} & 轻儿子的D全不为0 \\ A_i & 存在某个轻儿子j满足D_j=0 \end{cases} Di={Ai+Li×DhiAi轻儿子的D全不为0存在某个轻儿子j满足Dj=0

其中: L i = ∏ j 是 i 的轻儿子 ∧ D j ≠ 0 D j L_i=\prod_{j是i的轻儿子\land{D_j\neq{0}}}{D_j} Li=ji的轻儿子Dj=0Dj
写成矩阵形式:
[ D i 1 ] = [ L i 或 0 A i 0 1 ] × [ D h i 1 ] \begin{bmatrix} D_i \\ 1 \end{bmatrix}=\begin{bmatrix} L_i或0 & A_i \\ 0 & 1 \end{bmatrix}\times{\begin{bmatrix} D_{h_i} \\ 1 \end{bmatrix}} [Di1]=[Li00Ai1]×[Dhi1]

也就是说线段树维护的矩阵,其左上角位置可能是0也可能是L,而L值专门用来维护那些非零轻儿子的乘积。再用一个计数器,用来记录节点的零轻儿子的数量,用于判断矩阵元素是写零还是写L。判断情况比较多,将if-else写整齐,免得遗漏,因此modify的代码比较长。

#include <bits/stdc++.h>
#include <bits/extc++.h>
using namespace std;

using llt = long long;
llt const MOD = 998244353LL;

llt qpow(llt a, llt n){
    a %= MOD;
    llt ret = 1;
    while(n){
        if(n & 1) ret = ret * a % MOD;
        a = a * a % MOD;
        n >>= 1; 
    }
    return ret;
}

llt inv(llt a){return qpow(a, MOD - 2LL);}

struct HLD{ // 重链剖分

using llt = long long;

using value_type = array<llt, 2>; // [Li, Ai]
vector<value_type> data; // 线段树

using lazy_type = array<llt, 2>;
vector<lazy_type> lazy; // 延迟标记

/// 从下往上计算信息,要变动
value_type _up_(const value_type & ls, const value_type & rs) {
    // return {rs[0] * ls[0] % MOD, (rs[0] * ls[1] % MOD + rs[1]) % MOD};
    return {ls[0] *rs[0] % MOD, (ls[0] * rs[1] % MOD + ls[1]) % MOD};
}

/// 从上往下计算信息,要变动
void _dn_(int t, int s, int e, const lazy_type & delta) {
    data[t] = delta;
}

/// 辅助函数,视延迟的类型而变动
static const lazy_type & lazy_zero() {
    static const lazy_type LAZY0 = {1LL, 0LL};
    return LAZY0; 
}

/// 辅助函数,视线段树信息类型而变动
static const value_type & value_zero() {
    static const value_type VALUE0 = {1LL, 0LL};
    return VALUE0;
}

/// 几乎不用动
value_type _query(int t, int s, int e, int a, int b) {
    if(a <= s and e <= b) {
        return data[t];
    }

    int mid = (s + e) >> 1;
    value_type ans = value_zero();
    if(a <= mid) ans = _up_(ans, _query(lson(t), s, mid, a, b));
    if(mid < b) ans = _up_(ans, _query(rson(t), mid + 1, e, a, b));
    return ans;
}

/// 单点修改
void _modify(int t, int s, int e, int pos, const lazy_type & delta){
    if(s == e){
        _dn_(t, s, e, delta);
        return;
    }
    int mid = (s + e) >> 1;
    if(pos <= mid) _modify(lson(t), s, mid, pos, delta);
    else _modify(rson(t), mid + 1, e, pos, delta);
    _pushUp(t);
    return;
}

/// 这个函数不用动
void _pushUp(int t) {
    data[t] = _up_(data[lson(t)], data[rson(t)]);
}

/// 这两个函数不用变动
static int lson(int x) {return x << 1;}
static int rson(int x) {return lson(x) | 1;}

int N;
/// 树结构, 1-index
vector<vector<int>> g;
/// 点权值
vector<llt> weight;

/// 建单向边
void mkDiEdge(int a, int b){
    g[a].push_back(b);
}
/// 建双向边
void mkBiEdge(int a, int b){
    mkDiEdge(a, b); mkDiEdge(b, a);
}

/// 树链剖分结构
struct node_t{
    int parent; // 父节点
    int hson;   // 重儿子
    int depth;  // 该节点的深度, 根节点深度为0
    int size;   // 本节点所领子树的节点总数
    int top;    // 本节点所在重链的顶,采用的是原树编号
    int bot;    // 本节点所在重链的底,但是采用的是新编号
    int nid;    // 本节点在线段树中的编号, 即dfs序
    int mdes;   // 本节点所领子树的线段树编号均在[nid, mdes]中,采用的是新编号
};

int root; // 树根
vector<int> nid2old; // nid2old[i]表示线段树中第i个节点在原树中的编号
int timestamp; // 辅助变量
vector<node_t> nodes;

vector<llt> D;
vector<llt> L;
vector<int> Z; // Zi只是i的轻儿子中D为零的数量

/// 递归找重边
void _dfsHeavyEdge(int u, int p, int d){
    auto & n = nodes[u];
    n.parent = p;
    n.depth = d;
    n.size = 1;
    D[u] = weight[u];
    llt tmp = 1;

    for(auto v : g[u]){
        if(v == p) continue;
        _dfsHeavyEdge(v, u, d + 1);
        n.size += nodes[v].size;
        if(nodes[n.hson].size < nodes[v].size) n.hson = v;
        tmp = tmp * D[v] % MOD;       
    }
    if(n.hson) D[u] = (D[u] + tmp) % MOD;
    return;
}

/// 递归找重链
void _dfsHeavyPath(int u, int top){
    auto & n = nodes[u];
    n.top = top;
    nid2old[n.mdes = n.nid = ++timestamp] = u;
    L[u] = 1;

    if(0 == n.hson) return (void)(n.bot = n.nid);

    _dfsHeavyPath(n.hson, top);
    n.mdes = max(n.mdes, nodes[n.hson].mdes);
    n.bot = nodes[n.hson].bot;

    for(auto v : g[u]){
        if(v != n.parent and v != n.hson){
            _dfsHeavyPath(v, v);
            n.mdes = max(n.mdes, nodes[v].mdes);
            if(D[v]){
                L[u] = (L[u] * D[v]) % MOD;
            }else{
                Z[u] += 1;
            }            
        }
    }

    return;
}
/// 递归建线段树
void _build(int t, int s, int e) {
    if(s == e) {
        data[t] = {L[nid2old[s]], weight[nid2old[s]]}; // 注意线段树编号与原树编号存在转换
        return; 
    }
    int mid = (s + e) >> 1;
    _build(lson(t), s, mid);
    _build(rson(t), mid + 1, e);
    _pushUp(t);
}

/// 初始化, n是树的点数
void init(int n){
    N = n;
    timestamp = 0;
    /// 初始化树结构
    g.assign(N + 1, {});
    weight.assign(N + 1, 0);
    /// 初始化树链结构
    nodes.assign(N + 1, {0, 0, 0, 0, 0, 0, 0});
    nid2old.assign(N + 1, 0);
    /// 初始化线段树结构
    data.assign(N + 1 << 2, value_zero());
    lazy.assign(N + 1 << 2, lazy_zero());    
    /// 初始化DP数据
    D.assign(N + 1, {});
    L.assign(N + 1, {});
    Z.assign(N + 1, 0);
    return;
}

/// 在输入所有数据以后构建
void build(int root){
    /// 建树链
    _dfsHeavyEdge(this->root = root, 0, 0);
    _dfsHeavyPath(root, root);
    /// 建线段树
    _build(1, 1, N);
}

/// 求原树上x和y的LCA
int lca(int x, int y){
    while(nodes[x].top != nodes[y].top){
        if(nodes[nodes[x].top].depth < nodes[nodes[y].top].depth) y = nodes[nodes[y].top].parent;
        else x = nodes[nodes[x].top].parent;
    }
    return nodes[x].depth <= nodes[y].depth ? x : y;
}

/// 查询原树上x的信息,就是线段树上[nid, bot]的区间信息
value_type query(int x){
    return _query(1, 1, N, nodes[x].nid, nodes[x].bot);
}

/// 原树上的单点修改
void modify(int x, llt delta){
    if(weight[x] == delta) return;
    
    weight[x] = delta;
    /// 修改
    _modify(1, 1, N, nodes[x].nid, {L[x], delta});

    while(1){
        auto top = nodes[x].top;
        auto parent = nodes[top].parent;
        if(0 == parent) break;

        /// 查询当前top的值
        auto curTop = query(top);
        if(D[top] == curTop[1]) break;

        // auto tt = _query(1, 1, N, nodes[parent].nid, nodes[parent].nid);

        if(D[top]){
            if(curTop[1]){
                auto oL = L[parent];
                llt curL = oL * inv(D[top]) % MOD * curTop[1] % MOD;        
                D[top] = curTop[1];
                L[parent] = curL;    
                if(0 == Z[parent]){ // 没有0儿子才需要修改
                    // if(not (tt[0] == oL)) while(1);
                    /// 修改parent
                    _modify(1, 1, N, nodes[parent].nid, {curL, weight[parent]});                     
                }else{
                    // assert(tt[0] == 0);
                    break;
                }                           
            }else{
                auto oL = L[parent];
                llt curL = oL * inv(D[top]) % MOD;        
                D[top] = curTop[1];
                L[parent] = curL; 
                if(1 == (Z[parent] += 1)){
                    // assert(tt[0]);
                    /// 修改parent
                    _modify(1, 1, N, nodes[parent].nid, {0, weight[parent]});                      
                }else{
                    // assert(tt[0] == 0);
                    break;
                }
            }
        }else{
            // assert(tt[0] == 0);
            // assert(curTop[1]);
            if(--Z[parent]){
                auto oL = L[parent];
                llt curL = oL * curTop[1] % MOD;
                L[parent] = curL;     
                D[top] = curTop[1];     
                break;           
            }else{
                auto oL = L[parent];
                llt curL = oL * curTop[1] % MOD;
                L[parent] = curL;     
                D[top] = curTop[1];
                /// 修改parent
                _modify(1, 1, N, nodes[parent].nid, {curL, weight[parent]}); 
            }
        }
        x = parent;
    }
    return;
}

};

HLD Tree;
int N, Q, Root;

void work(){
    cin >> N >> Q;
    Tree.init(N);
    for(int p,i=2;i<=N;++i){
        cin >> p;
        Tree.mkDiEdge(p, i);
    }
    for(int i=1;i<=N;++i) cin >> Tree.weight[i];
    Tree.build(Root = 1);

    for(int v,x,q=1;q<=Q;++q){
        cin >> v >> x;
        Tree.modify(v, x);
        auto ans = Tree.query(1);
        cout << ans[1] << "\n";
    }
    return;
}

int main(){
#ifndef ONLINE_JUDGE
    freopen("z.txt", "r", stdin);
#endif
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int nofkase = 1;
    // cin >> nofkase;
    while(nofkase--) work();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值