[学习笔记]动态 DP
前言#
不会 AC 自动机然而会 SAM 是正常的吗?
动态 DP#
动态 DP 用于在原本 DP 状态较容易转移情况下,维护动态修改权值。
带修最大子段和#
首先是最大子段和问题,从一个序列中取出任意长度的连续子段,最大化字段和。
令 表示以 结尾的最大子段和,那么有 :
结果为 ,如果要取至少一个数就再考虑要不要直接取最大的,因为可能序列元素全都是负数。
现在修改一下问题,多次单点修改权值与询问区间最大子段和,这个问题有一个经典的线段树做法,现在只考虑如何从原本 DP 的思路求解。
首先为了方便维护取 的 操作,规定一个 表示从 能取出的最大子段和,于是有 。最大子段和能使用线段树维护的一个原因是两段的信息可以快速合并,考虑一个有结合律的运算承担转移。
定义 广义矩阵乘法 如下:
可以发现这个玩意符合结合律,那么可以线段树维护一个区间内得到的矩阵。
考虑转移怎么写,我们需要构造一个矩阵 使得:
首先 里包含 ,代表取新的值。
然后 里包含 ,代表不能断开取数。
最后 里面包含 ,就是个占位符。
然后线段树维护一下就行了。
代码没有,太麻烦了,不如线段树/平衡树维护四个信息。
带修改树上最大独立集#
考虑不带修的情况, 表示在不选择 的情况下其子树最大独立集, 表示在选择 的情况下其子树内最大独立集。
然后结果就是,这个 DP 是比较简单的。
考虑如何设计为 矩阵可转移的方式,设 表示不选择 且只允许选择 的轻儿子所在子树的最大答案, 表示选择 的最大答案, 表示 的重儿子。
那么有:
然后这个玩意可以树剖或者 LCT 维护。
然后这题 LCT 不需要 Link 或者 Cut,而且初始辅助树所有边都是虚边。
然后维护的时候要维护虚子树信息所以记得在 access()
的时候修改充分。
然后 LCT 改一改,利用一下轻重链剖分,把 Splay 建立得更平衡,就变成了一个叫做全局平衡二叉树(?????)的小常数奇妙科技,然而我并不会。
代码#
最开始没删调试的 cerr
结果跑得飞慢吓我一跳……
struct Mat {
int val[2][2];
Mat() {
val[0][0] = val[0][1] = val[1][0] = val[1][1] = -INF;
}
inline friend Mat operator * (const Mat &a,const Mat &b) {
Mat res;
res.val[0][0] = std::max(a.val[0][0] + b.val[0][0],a.val[0][1] + b.val[1][0]);
res.val[0][1] = std::max(a.val[0][0] + b.val[0][1],a.val[0][1] + b.val[1][1]);
res.val[1][0] = std::max(a.val[1][0] + b.val[0][0],a.val[1][1] + b.val[1][0]);
res.val[1][1] = std::max(a.val[1][0] + b.val[0][1],a.val[1][1] + b.val[1][1]);
return res;
}
inline int* operator [] (int i) {
return val[i];
}
inline int GetAns() {
return std::max(val[0][0],val[0][1]);
}
};
int v[N];
Mat f[N],val[N];
struct LinkCutTree {
int fa[N],ch[N][2];
#define ls(x) (ch[x][0])
#define rs(x) (ch[x][1])
#define dir(x) (x == ch[fa[x]][1])
#define IsRoot(x) (x != ch[fa[x]][0] && x != ch[fa[x]][1])
inline void PushUp(int x) {
f[x] = f[rs(x)] * val[x] * f[ls(x)];
}
inline void rotate(int x) {
int y = fa[x],z = fa[y],d = dir(x),w = ch[x][d ^ 1];
if(!IsRoot(y)) ch[z][dir(y)] = x;
ch[y][d] = w,ch[x][d ^ 1] = y;
fa[x] = z,fa[y] = x;
if(w) fa[w] = y;
PushUp(y),PushUp(x);
}
inline void splay(int x) {
while(!IsRoot(x)) {
int y = fa[x],z = fa[y];
if(!IsRoot(y))
rotate((ls(y) == x) ^ (ls(z) == y) ? x : y);
rotate(x);
}
PushUp(x);
}
inline void access(int x) {
for(int p = 0;x;p = x,x = fa[x]) {
splay(x);
val[x][1][0] += f[rs(x)].GetAns();//,val[x][0][0] = val[x][1][0];
val[x][0][1] += f[rs(x)][0][0];
val[x][1][0] -= f[p].GetAns(),val[x][0][0] = val[x][1][0];
val[x][0][1] -= f[p][0][0];
rs(x) = p;
PushUp(x);
}
}
inline void modify(int x,int _v) {
access(x),splay(x);
val[x][0][1] -= v[x];
val[x][0][1] += (v[x] = _v);
PushUp(x);
}
}T;
int head[N],ecnt = -1;
struct Edge {
int nxt,to;
}e[N << 1];
inline void AddEdge(int st,int ed) {
e[++ecnt] = (Edge) {head[st],ed},head[st] = ecnt;
e[++ecnt] = (Edge) {head[ed],st},head[ed] = ecnt;
}
void dfs(int u,int _f) {
T.fa[u] = _f;
int sum1 = 0,sum2 = v[u];
repg(i,u) {
const int v = e[i].to;
if(v == _f) continue;
dfs(v,u);
sum1 += f[v].GetAns();
sum2 += f[v][0][0];
}
val[u][0][0] = val[u][1][0] = sum1;
val[u][0][1] = sum2;
f[u] = val[u];
}
int main() {
InitIO();
mems(head,-1);
f[0][0][0] = f[0][1][1] = 0;
int n = read(),m = read();
rep(i,1,n) v[i] = read();
repl(i,1,n) AddEdge(read(),read());
dfs(1,0);
while(m--) {
int x = read(),_v = read();
T.modify(x,_v);
T.splay(1);
write(f[1].GetAns()),enter;
}
EndIO();
return 0;
}
例题#
保卫王国#
最小权覆盖集 = 全集 - 最大权独立集。
强制选择 = 无限大权值,强制不选 = 负无限大权值。
然后就套板子罢。
DDP 这大常数做法能进最优解第一页就离谱
洪水#
每个点有点权,代表断掉这个点的代价,多次询问使得一个点子树全部叶子都与该点不连通最小代价,带动态点权修改。
不带修弱化版就是 P3931(3931 最小割能过就很牛)。
切树游戏#
猫老师博客里写过的奇妙题,然而还需要 FWT,可能学了 FWT 之后会去补上。
作者:AstatineAi
出处:https://www.cnblogs.com/AstatineAi/p/dynamic-DP.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本