动态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} Di−1有关。特别是在树上 i − 1 i-1 i−1与 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+j是i的儿子∑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+j是i的轻儿子∑Dj
将重儿子项剔除,剩下的记作: L i = A i + ∑ j 是 i 的轻儿子 D j L_i=A_i+\sum_{j是i的轻儿子}D_j Li=Ai+j是i的轻儿子∑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的修改,显然是要通过重链一路向上,直到树根。关于修改操作的一般流程,有几点:
- 时刻维护 L L L数组与权值数组,因此其中的值是实时的;
- 不维护 D D D数组,因此其中的值只是最初的规划值;
- 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=∑j是i的儿子max(Dj,0,Dj,1)Di,1=Ai+∑j是i的儿子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=[∑j是i的轻儿子max(Dj,0,Dj,1)Ai+∑j是i的轻儿子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+j是i的儿子∏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=j是i的轻儿子∏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]
这样就可以进行维护了。显然并不需要维护完整的矩阵,只需要维护两个数即可。这里要维护的是标准的矩阵乘法,既有加法又有乘法,因此修改操作要注意。总体而言,修改操作也是顺着重链往上,依次修改top
的parent
的L
。此处修改需要知道L
原来的值,可以通过查询D[top]
获取。
本题的修改流程大概长这样,对原始的x
,其L
显然不变,因此修改A
即可。对以后的top
的parent
,其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=j是i的轻儿子∧Dj=0∏Dj
写成矩阵形式:
[
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]=[Li或00Ai1]×[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;
}