题目大意
%
给定一棵
n
n
n 个点的树,点带点权。有
m
m
m 次操作,每次操作给定
x
,
y
x,y
x,y,表示修改点
x
x
x 的权值为
y
y
y,输出每次修改后树的最大权独立集大小。
%
数据范围
1
⩽
n
,
m
⩽
1
0
5
1\leqslant n,m\leqslant 10^5
1⩽n,m⩽105
题解
% 首先如果不考虑修改操作,很容易想到动态规划,首先是定义
%
f
u
,
0
f_{u,0}
fu,0 表示不取
u
u
u 节点时,以
u
u
u 为根的子树的最大独立集大小。
f
u
,
1
f_{u,1}
fu,1 表示取
u
u
u 节点时,以
u
u
u 为根的子树的最大独立集大小。
% 定义 u u u 的所有儿子的集合为 S u S_u Su,自身的权值为 a u a_u au,则转移如下(其实是废的)1
f u , 0 = ∑ v ∈ S u max ( f v , 0 , f v , 1 ) f u , 1 = a u + ∑ v ∈ S u f v , 0 \begin{aligned} f_{u,0}&=\sum_{v\in S_u}\max(f_{v,0},f_{v,1})\\ f_{u,1}&=a_u+\sum_{v\in S_u}f_{v,0} \end{aligned} fu,0fu,1=v∈Su∑max(fv,0,fv,1)=au+v∈Su∑fv,0
% 可以发现,每次的修改操作只会影响到某个点到根节点上所有点的 f f f 数组,这是树上的其中一条链,接下来我们可以考虑树链剖分。为了适应树链剖分的定义,我们再从 f f f 中分离出一部分信息,我们定义 u u u 的重儿子为 s o n ( u ) son(u) son(u),以它为根的子树为 T s o n ( u ) T_{son(u)} Tson(u),则可以定义
%
g
u
,
0
g_{u,0}
gu,0 表示不取
u
u
u 节点时,以
u
u
u 为根的子树,在不考虑子树
T
s
o
n
(
u
)
T_{son(u)}
Tson(u)的前提下,的最大独立集大小。
g
u
,
1
g_{u,1}
gu,1 表示取
u
u
u 节点时,以
u
u
u 为根的子树,在不考虑子树
T
s
o
n
(
u
)
T_{son(u)}
Tson(u)的前提下,的最大独立集大小。
% 简而言之, g g g 就是不考虑重儿子的贡献的 f f f。 g g g 的转移如下
g u , 0 = ∑ v ∈ S u , v ≠ s o n ( u ) max ( f v , 0 , f v , 1 ) g u , 1 = a u + ∑ v ∈ S u , v ≠ s o n ( u ) f v , 0 \begin{aligned} g_{u,0}=\sum_{v\in S_u,v\not=son(u)}\max(f_{v,0},f_{v,1})\\ g_{u,1}=a_u+\sum_{v\in S_u,v\not=son(u)}f_{v,0}\\ \end{aligned} gu,0=v∈Su,v=son(u)∑max(fv,0,fv,1)gu,1=au+v∈Su,v=son(u)∑fv,0
% 同样地, f f f 的转移可以更简单。
f u , 0 = g u , 0 + max ( f s o n ( u ) , 0 , f s o n ( u ) , 1 ) = max ( g u , 0 + f s o n ( u ) , 0 , g u , 0 + f s o n ( u ) , 1 ) f u , 1 = g u , 1 + f s o n ( u ) , 0 = max ( g u , 1 + f s o n ( u ) , 0 , − ∞ + f s o n ( u ) , 1 ) \begin{aligned} f_{u,0}&=g_{u,0}+\max(f_{son(u),0},f_{son(u),1})&&=\max(g_{u,0}+f_{son(u),0},g_{u,0}\ +f_{son(u),1})\\ f_{u,1}&=g_{u,1}+f_{son(u),0}&&=\max(g_{u,1}+f_{son(u),0},-\infty+f_{son(u),1})\\ \end{aligned} fu,0fu,1=gu,0+max(fson(u),0,fson(u),1)=gu,1+fson(u),0=max(gu,0+fson(u),0,gu,0 +fson(u),1)=max(gu,1+fson(u),0,−∞+fson(u),1)
%
上面的转移中,第一个等于号的右侧应该是显然的,第二个等于号后面是吃饱了撑的,但绝不是无聊之作。考虑树链剖分的操作,我们需要一种数据结构来维护这些信息,如果仍然采用线段树的话,我们需要一种满足结合律的运算,使得我们能保证结果的正确。
%
观察一下,第二个等于号后面的式子有取最大值,有相加操作,还设计到多个元素的运算,显然不可能为简单的四则运算,但其形式和矩阵乘法有些类似。于是,我们定义新矩阵乘法为
(
A
×
B
)
i
,
j
=
max
k
=
1
n
A
i
,
k
+
B
k
,
j
(A\times B)_{i,j}=\max_{k=1}^n A_{i,k}+B_{k,j}
(A×B)i,j=k=1maxnAi,k+Bk,j 可以证明,这样的定义满足结合律,仍然不满足交换率。那么上面的转移可以写成
[ g u , 0 g u , 0 g u , 1 − ∞ ] × [ f s o n ( u ) , 0 f s o n ( u ) , 1 ] = [ f u , 0 f u , 1 ] \begin{bmatrix}g_{u,0}&g_{u,0}\\g_{u,1}&-\infty\end{bmatrix} \times\begin{bmatrix}f_{son(u),0}\\f_{son(u),1}\end{bmatrix} =\begin{bmatrix}f_{u,0}\\f_{u,1}\end{bmatrix} [gu,0gu,1gu,0−∞]×[fson(u),0fson(u),1]=[fu,0fu,1]
%
定义上面方程左侧含有
g
g
g 的矩阵为矩阵
G
u
G_u
Gu,等式右侧的矩阵为
F
u
F_u
Fu,具体地,原树中每个非叶子节点存储的信息为
G
u
G_u
Gu,特别地,叶子节点存储的信息为
F
u
F_u
Fu。每个节点的信息均可以用一次普通树形DP求出。
如此,我们想要知道根节点的
F
F
F 矩阵,只需要将根节点所在的链上所有节点的矩阵全部乘起来(从深度小的乘到深度大的)。这个过程需要我们先对原树进行重链剖分,然后重编号,再用维护区间乘法的线段树维护每条重链上的矩阵的信息。
然后,我们就成功地把一个线性的算法优化到了带一个log的级别。 我们开始考虑修改操作。
我们定义
t
o
p
(
u
)
top(u)
top(u) 表示
u
u
u 所在的节点的重链的顶部的节点,
f
a
(
u
)
fa(u)
fa(u) 表示节点
u
u
u 的父亲,修改点
u
u
u 的权值后,
g
u
,
1
g_{u,1}
gu,1 的值会随之更改,这个修改十分简单,直接在线段树上修改,这样会改变
F
t
o
p
(
u
)
F_{top(u)}
Ftop(u) 矩阵,进而影响到
G
f
a
(
u
)
G_{fa(u)}
Gfa(u)。
每次都按照定义重新计算一次
G
f
a
(
u
)
G_{fa(u)}
Gfa(u) 显然不现实,于是我们考虑在修改
g
u
,
1
g_{u,1}
gu,1 之前,先将
F
t
o
p
(
u
)
F_{top(u)}
Ftop(u) 的信息存下来,然后完修改
g
u
,
1
g_{u,1}
gu,1 后,让
G
f
a
(
u
)
G_{fa(u)}
Gfa(u) 加上
F
t
o
p
(
u
)
F_{top(u)}
Ftop(u) 的变化量(先扣除原来的
F
t
o
p
(
u
)
F_{top(u)}
Ftop(u),再加上新的
F
t
o
p
(
u
)
F_{top(u)}
Ftop(u))。
令整颗树的根节点为
r
o
o
t
root
root,则最后的答案便是
max
(
f
r
o
o
t
,
0
,
f
r
o
o
t
,
1
)
\max(f_{root,0},f_{root,1})
max(froot,0,froot,1)。
代码
% 代码本身不算长,和普通树剖差不多,下面的代码只是注释和空行比较多。
#include<bits/stdc++.h>
using namespace std;
#define maxn 400010
struct matrix{//矩阵类
int mat[2][2];
matrix(){memset(mat,-63,sizeof mat);}//初始化为无穷小
inline int* operator[](const int x){return mat[x];}//重载中括号运算符
inline matrix operator*(const matrix &x)const{//新·矩阵乘法
matrix c;
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
for(int k=0;k<=1;k++)
c[i][j]=max(c[i][j],mat[i][k]+x.mat[k][j]);
return c;
}
};
struct edge{//邻接表
int v,next;
}edges[maxn<<1];
int head[maxn],cnte=0;
void ins(int u,int v){//建边
edges[++cnte]=(edge){v,head[u]};
head[u]=cnte;
}
int fa[maxn],size[maxn],son[maxn];
//父节点,子树大小,重儿子
void dfs(int u){//深搜求出基本信息
size[u]=1;
for(int i=head[u],v;i;i=edges[i].next)
if((v=edges[i].v)!=fa[u]){
fa[v]=u; dfs(v);
size[u]+=size[v];
if(size[son[u]]<size[v])
son[u]=v;
}
}
int n,q,a[maxn];
int idx[maxn],rank[maxn],cntv=0,top[maxn],end[maxn];
//idx:新编号 rank:原编号 top:重链顶部的原编号 end:重链尾的新编号
int f[maxn][2],g[maxn][2];
/*
f[u][0]表示不取点u时,以u为根的子树的最大独立集大小
f[u][1]表示取点u时,以u为根的子树的最大权独立集大小
g[u][0]表示不选点u,也不考虑重儿子所在子树的前提下,以u为根的子树的最大权独立集
g[u][1]表示选点u,不考虑重儿子所在子树的前提下,以u为根的子树的最大权独立集
g[u][0]=sigma{max(f[i][0],f[i][1])|i是u的儿子,i≠son[u]}
g[u][1]=a[i]+sigma{f[i][0]|i是u的儿子,i≠son[u]}
f[u][0]=max(g[u][0]+f[son[u]][0],g[u][0]+f[son[u]][1])
f[u][1]=max(g[i][1]+f[son[u]][0],-inf+f[son[u]][1])
|g[i][0] g[i][0] | * |f[son][0]| = |f[i][0]|
|g[i][1] -inf | |f[son][1]| |f[i][1]|
对于叶子节点,其没有重儿子,因而其f和g的值没有区别
g[leaf][0]=f[leaf][0]=0
g[leaf][1]=f[leaf][1]=a[leaf]
因而不需要特判
*/
matrix val[maxn];
//每个节点的矩阵
void recode(int u,int chain){//重编号+基本dp
rank[idx[u]=++cntv]=u; //标号
end[top[u]=chain]=cntv; //更新重链信息
g[u][0]=f[u][0]=0; //初始化
g[u][1]=f[u][1]=a[u];
if(son[u]){ //先遍历重儿子
recode(son[u],chain);
f[u][0]+=max(f[son[u]][1],f[son[u]][0]); //计算f数组
f[u][1]+=f[son[u]][0];
//g数组不考虑重儿子
}
for(int i=head[u],v;i;i=edges[i].next)
if((v=edges[i].v)!=fa[u]&&v!=son[u]){
recode(v,v);
//计算f和g数组
f[u][0]+=max(f[v][0],f[v][1]);
g[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
g[u][1]+=f[v][0];
}
//复制到val中
val[u][0][1]=val[u][0][0]=g[u][0];
val[u][1][0]=g[u][1];
val[u][1][1]=-1e9;
}
struct node{
node *left,*right;
int l,r;
matrix d;
node(int tl=0,int tr=0):l(tl),r(tr){//建树
if(tl==tr){
d=val[rank[tl]];//线段树叶子节点存储每个点储存的矩阵
left=right=NULL;
return;
}
int mid=(l+r)>>1;
left=new node(l,mid);
right=new node(mid+1,r);
d=left->d*right->d;
}
void change(int x){//更新
if(l==r){
d=val[rank[l]];//更新每个点储存的矩阵
return;
}
if(x<=left->r) left->change(x);
else right->change(x);
d=left->d*right->d;
}
matrix query(int x,int y){//查询
if(l==x&&r==y) return d;
if(y<=left->r) return left->query(x,y);
if(x>=right->l) return right->query(x,y);
return left->query(x,left->r)*right->query(right->l,y);
}
}t;
void change(int u,int d){//修改
val[u][1][0]+=d-a[u];//加上差值
a[u]=d;
while(u!=0){
matrix before=t.query(idx[top[u]],end[top[u]]);//计算出原来的
t.change(idx[u]);//修改
matrix after=t.query(idx[top[u]],end[top[u]]);//计算出后来的
u=fa[top[u]];//到另一条链上
val[u][0][0]+=max(after[0][0],after[1][0])-max(before[0][0],before[1][0]);//加上差值
val[u][0][1]=val[u][0][0];
val[u][1][0]+=after[0][0]-before[0][0];
}
}
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1,u,v;i<n;i++){
scanf("%d %d",&u,&v);
ins(u,v),ins(v,u);
}
dfs(1);
recode(1,1);
t=node(1,n);
for(int i=1,u,d;i<=q;i++){
scanf("%d %d",&u,&d);
change(u,d);
matrix ans=t.query(idx[1],end[top[1]]);
printf("%d\n",max(ans[0][0],ans[1][0]));
}
return 0;
}
有两条分割线分割的是最终使用的版本。 ↩︎