一道入门 DP + 修改 = 动态 DP。

模板题为例,多次询问树的最大独立集,带修改。

先有 naive 的 DP,记 \(f_{u,0/1}\) 表示 \(u\) 点不选/选时以 \(u\) 为根的子树的最大独立集权值,有

\[\begin{cases}f_{u,0}=\sum\limits_{v\in \operatorname{son}u}\max\{f_{v,0},f_{v,1}\} \\ f_{u,1}=w_u+\sum\limits_{v\in\operatorname{son}u}f_{v,0}\end{cases}
\]

修改一个点的权值,只有这个点到根结点的 DP 值发生了变化,于是我们考虑重链剖分来快速转移。

说到快速转移,又想到了用矩阵。所以我们先来把转移方程改成可用矩阵转移的形式。再记两个 DP 值

\[\begin{cases}g_{u,0}=\sum\limits_{v\in\operatorname{lson}u}\max\{f_{v,0},f_{v,1}\} \\ g_{u,1}=w_u+\sum\limits_{v\in\operatorname{lson}u}f_{v,0}\end{cases}
\]

\(\operatorname{lson}u\) 表示 \(u\) 的所有轻儿子。这样做的目的是将轻重儿子的转移分开,更好维护。

现在重写转移方程

\[\begin{cases}f_{u,0}=g_{u,0}+\max\{f_{\operatorname{hson}u,0},f_{\operatorname{hson}u,1}\} \\ f_{u,1}=g_{u,1}+f_{\operatorname{hson}u,0}\end{cases}
\]

\(\operatorname{son}u\) 表示 \(u\) 的重儿子。这样我们就丢掉了烦人的 \(\sum\) 了。

上面的转移形式很像矩阵 \(A_{i,j}=\max\limits_{k}\{B_{i,k}+C_{k,j}\}\) 的转移形式,所以我们用这个新定义的矩阵乘法来重写刚才的式子(记 \(v=\operatorname{hson}u\)):

\[\begin{bmatrix}f_{u,0} \\ f_{u,1}\end{bmatrix}=\begin{bmatrix}g_{u,0} & g_{u,0} \\ g_{u,1} & -\infty\end{bmatrix}\begin{bmatrix}f_{v,0} \\ f_{v,1}\end{bmatrix}
\]

这样写后,一个点的 DP 值就等于这个点所在重链的叶子结点的矩阵乘这条链上的所有的转移矩阵,这个可以用线段树来维护。修改时沿着链向上跳,线段树上单点修改即可。

更多细节详见代码(注释自我感觉跟详细)

#include <bits/stdc++.h>
using namespace std; const int N=1e5+5;
struct matrix
{
int a[2][2];
matrix() {memset(a,0xcf,sizeof(a));}
int* const operator[](const int i) {return a[i];}
const int* const operator[](const int i) const {return a[i];}
matrix operator*(matrix b)
{
matrix c;
for(int i=0;i<2;++i)
for(int j=0;j<2;++j)
for(int k=0;k<2;++k)
c[i][j]=max(c[i][j],a[i][k]+b[k][j]);
return c;
}
}g[N],t[N<<2];//g[]是转移矩阵,t[]是线段树
int n,Q,son[N],siz[N],dfn[N],id[N],fa[N],ids,top[N],nd[N];
//id[]是这个点的dfs序,dfn[]是id[]的逆映射,nd[]只有在链顶有定义,代表这条链的底的dfs序
int f[N][2],w[N];//f[]是 DP 值
vector<int> G[N]; void dfs1(int u,int faz)
{
siz[u]=1,fa[u]=faz;
for(int v:G[u]) if(v!=faz)
{
dfs1(v,u),siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
} void dfs2(int u,int tp)
{
top[u]=tp,id[u]=++ids,dfn[ids]=u;
//按照定义初始化矩阵和 DP 值
g[u][0][0]=g[u][0][1]=0;
g[u][1][0]=f[u][1]=w[u],nd[tp]=max(nd[tp],ids);
if(son[u])
{
dfs2(son[u],tp);
f[u][0]+=max(f[son[u]][0],f[son[u]][1]);
f[u][1]+=f[son[u]][0];
}
for(int v:G[u]) if(v!=fa[u]&&v!=son[u])
{
dfs2(v,v);
f[u][0]+=max(f[v][0],f[v][1]);
g[u][0][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0],g[u][0][1]=g[u][0][0];
g[u][1][0]+=f[v][0];
//按照定义转移即可
}
} //下面的线段树维护的是链上的转移矩阵之积
void build(int rt,int l,int r)
{
if(l==r) {t[rt]=g[dfn[l]]; return;}
int mid=l+r>>1;
build(rt<<1,l,mid),build(rt<<1|1,mid+1,r);
t[rt]=t[rt<<1]*t[rt<<1|1];
} void upd(int rt,int lc,int rc,int p)
{
if(lc==rc) {t[rt]=g[dfn[lc]]; return;}
int mid=lc+rc>>1;
if(p<=mid) upd(rt<<1,lc,mid,p);
else upd(rt<<1|1,mid+1,rc,p);
t[rt]=t[rt<<1]*t[rt<<1|1];
} matrix query(int rt,int lc,int rc,int l,int r)
{
if(l<=lc&&r>=rc) return t[rt];
int mid=lc+rc>>1;
if(r<=mid) return query(rt<<1,lc,mid,l,r);
else if(l>mid) return query(rt<<1|1,mid+1,rc,l,r);
else return query(rt<<1,lc,mid,l,r)*query(rt<<1|1,mid+1,rc,l,r);
} void upd_val(int x,int y)
{
g[x][1][0]+=y-w[x],w[x]=y; //先将自己的值修改了
matrix bef,aft; //需要分别记修改前后的转移矩阵,靠这个差值更新
while(x)
{
bef=query(1,1,n,id[top[x]],nd[top[x]]);
upd(1,1,n,id[x]);
aft=query(1,1,n,id[top[x]],nd[top[x]]);
x=fa[top[x]]; //这个点是链顶的父亲,所以这条链对于父亲来说是一条轻链,所以父亲的矩阵必须修改
g[x][0][0]+=max(aft[0][0],aft[1][0])-max(bef[0][0],bef[1][0]);
g[x][0][1]=g[x][0][0],g[x][1][0]+=aft[0][0]-bef[0][0];
}
} int main()
{
scanf("%d%d",&n,&Q);
for(int i=1;i<=n;++i) scanf("%d",w+i);
for(int i=1,a,b;i<n;++i)
{
scanf("%d%d",&a,&b);
G[a].push_back(b),G[b].push_back(a);
}
dfs1(1,0),dfs2(1,1),build(1,1,n);
for(int i=1,x,y;i<=Q;++i)
{
scanf("%d%d",&x,&y);
upd_val(x,y);
matrix ans=query(1,1,n,id[1],nd[1]);
//注意到一个点的 DP 值就是这个点所在链的底到这个点的转移矩阵之积
//似乎我们在 dfs 后就一直没有用 f[] 了?因为每个叶子结点
//的转移矩阵刚好与其 f[] 值相等,所以我们不需要了
printf("%d\n",max(ans[0][0],ans[1][0]));
}
return 0;
}

动态 DP的更多相关文章

  1. 动态DP之全局平衡二叉树

    目录 前置知识 全局平衡二叉树 大致介绍 建图过程 修改过程 询问过程 时间复杂度的证明 板题 前置知识 在学习如何使用全局平衡二叉树之前,你首先要知道如何使用树链剖分解决动态DP问题.这里仅做一个简 ...

  2. Luogu P4643 【模板】动态dp

    题目链接 Luogu P4643 题解 猫锟在WC2018讲的黑科技--动态DP,就是一个画风正常的DP问题再加上一个动态修改操作,就像这道题一样.(这道题也是PPT中的例题) 动态DP的一个套路是把 ...

  3. 动态dp学习笔记

    我们经常会遇到一些问题,是一些dp的模型,但是加上了什么待修改强制在线之类的,十分毒瘤,如果能有一个模式化的东西解决这类问题就会非常好. 给定一棵n个点的树,点带点权. 有m次操作,每次操作给定x,y ...

  4. 洛谷P4719 动态dp

    动态DP其实挺简单一个东西. 把DP值的定义改成去掉重儿子之后的DP值. 重链上的答案就用线段树/lct维护,维护子段/矩阵都可以.其实本质上差不多... 修改的时候在log个线段树上修改.轻儿子所在 ...

  5. 动态 DP 学习笔记

    不得不承认,去年提高组 D2T3 对动态 DP 起到了良好的普及效果. 动态 DP 主要用于解决一类问题.这类问题一般原本都是较为简单的树上 DP 问题,但是被套上了丧心病狂的修改点权的操作.举个例子 ...

  6. 动态dp初探

    动态dp初探 动态区间最大子段和问题 给出长度为\(n\)的序列和\(m\)次操作,每次修改一个元素的值或查询区间的最大字段和(SP1714 GSS3). 设\(f[i]\)为以下标\(i\)结尾的最 ...

  7. [总结] 动态DP学习笔记

    学习了一下动态DP 问题的来源: 给定一棵 \(n\) 个节点的树,点有点权,有 \(m\) 次修改单点点权的操作,回答每次操作之后的最大带权独立集大小. 首先一个显然的 \(O(nm)\) 的做法就 ...

  8. UOJ268 [清华集训2016] 数据交互 【动态DP】【堆】【树链剖分】【线段树】

    题目分析: 不难发现可以用动态DP做. 题目相当于是要我求一条路径,所有与路径有交的链的代价加入进去,要求代价最大. 我们把链的代价分成两个部分:一部分将代价加入$LCA$之中,用$g$数组保存:另一 ...

  9. [复习]动态dp

    [复习]动态dp 你还是可以认为我原来写的动态dp就是在扯蛋. [Luogu4719][模板]动态dp 首先作为一个\(dp\)题,我们显然可以每次修改之后都进行暴力\(dp\),设\(f[i][0/ ...

  10. 【BZOJ4911】[SDOI2017]切树游戏(动态dp,FWT)

    [BZOJ4911][SDOI2017]切树游戏(动态dp,FWT) 题面 BZOJ 洛谷 LOJ 题解 首先考虑如何暴力\(dp\),设\(f[i][S]\)表示当前以\(i\)节点为根节点,联通子 ...

随机推荐

  1. 使用cookies,免密登录禅道(一)

    导言:在做自动化的过程中,很多时候都需要绕过登录验证码来进行测试,可使用cookie 绕过验证码进行登录. 以下以自己搭建的禅道环境登录为例(其他网站也可以同样道理): #coding=gbkimpo ...

  2. RabbitMQ由浅入深入门全总结(二)

    写在最前面 距离上一次发文章已经很久了,其实这段时间一直也没有停笔,只不过在忙着找工作还有学校结课的事情,重新弄了一下博客,后面也会陆陆续续会把文章最近更新出来~ 这篇文章有点长,就分了两篇 PS:那 ...

  3. 【NX二次开发】判断部件是否已修改(判断部件是否需要保存)UF_PART_is_modified();

    判断部件是否已修改(判断部件是否需要保存)UF_PART_is_modified(); 注意:函数需要输入原型,不要输入事例.事例转原型:UF_ASSEM_ask_prototype_of_occ() ...

  4. SQL Sever提权

    前言:渗透测试中提权是较为重要的环节,若以低权限身份进行后渗透,测试出的问题相对于高权限的质量会低很多,从一个普通用户,通过手段让自己变为管理员,也可利用操作系统或者应用程序中的错误,设计缺陷或者配置 ...

  5. 【题解】Luogu P1011 车站

    题目描述 火车从始发站(称为第1站)开出,在始发站上车的人数为a,然后到达第2站,在第2站有人上.下车,但上.下车的人数相同,因此在第2站开出时(即在到达第3站之前)车上的人数保持为a人.从第3站起( ...

  6. docker2-镜像原理及创建新的镜像

    1,镜像是什么 镜像是一种轻量级.可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码.运行时.库.环境变量和配置文件 在docker中所有应用 ...

  7. 『心善渊』Selenium3.0基础 — 14、Selenium对单选和多选按钮的操作

    目录 1.页面中的单选按钮和多选按钮 2.判断按钮是否选中is_selected() 3.单选按钮的操作 4.多选按钮的操作 5.选择部分多选按钮的操作 1.页面中的单选按钮和多选按钮 页面中的单选按 ...

  8. .obj : error LNK2019: 无法解析的外部符号

    记录一个报错 .obj : error LNK2019: 无法解析的外部符号 "public: void __thiscall 习惯上先去看看 | "#include"语 ...

  9. 01 JumpServer安装

    1.0.环境说明: 操作系统类型 主机名称 用户及密码 角色 eth0(Vmnet8) eth1(Vmnet1) 防火墙状态 selinux centos7.4 controlnode root:12 ...

  10. 洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法

    "为什么这个功能用不了?" 程序员:"清一下缓存" 上篇洞悉系列文章给大家详细介绍了MySQL的存储内幕:洞悉MySQL底层架构:游走在缓冲与磁盘之间.既然聊过 ...