前言

最后一届NOIPTG的day2T3对于动态DP的普及起到了巨大的作用。然而我到现在还不会

开始

SP1716 GSS3 - Can you answer these queries III

题解位置

这道题的题目大意就是维护动态序列最大子段和。一个比较显然的想法就是用线段树维护\(lmax,rmax,sum,max\)即可。但是我们不想放弃DP的优良性质,于是就有了优良的动态DP。

对于这道题目,如果不考虑修改操作,那么DP就是这样的:

令\(F[i]\)表示以\(A[i]\)为结尾的最大子段和,\(G[i]\)表示到\(i\)为止的答案,那么不难发现

\[F[i]=A[i]+\max\{F[i-1],0\}\\
G[i]=\max\{G[i-1],F[i]\}
\]

下一步就是把转移改写为矩乘的形式。能够改写是因为矩乘基于乘法对加法的分配律。而max同样对加法有分配律。也就是说:

\[a(b+c)=ab+ac\\
a+\max\{b,c\}=\max\{a+b,a+c\}
\]

这样,上面的Dp就可以变成这个样子:

\[F[i]=\max\{A[i]+F[i-1],A[i]\}\\
G[i]=\max\{G[i-1],F[i-1]+A[i],A[i]\}
\]

那么转移矩阵就应该是一个\(3\times3\)的矩阵。

\[\begin{aligned}
\left [\begin{matrix}
A[i]&-\infty&A[i]\\
A[i]&0&A[i]\\
-\infty & -\infty & 0
\end{matrix}\right]
\left [\begin{matrix}
F[i-1]\\
G[i-1]\\
0
\end{matrix}\right]
=
\left [\begin{matrix}
F[i]\\
G[i]\\
0
\end{matrix}\right]
\end{aligned}
\]

只是这个矩阵乘法是

\[C_{i,j}=\max\{A_{i,k}+B_{k,j}\}
\]

那么根据矩阵乘法的结合律,用线段树维护即可。

更进一步

真正展示动态DP厉害的地方在树上。

【模板】动态 DP

首先明确最大权独立集的含义:选择若干的点,他们互不相邻,使得点权和最大。

对付这道题目,我们同样先把最朴素的Dp写出来(\(F[u][0]\)代表不选当前点,\(F[u][1]\)代表选当前点):

void Dp( int u, int Fa ) {
F[ u ][ 1 ] = A[ u ];
for( int t = Start[ u ]; t; t = Edge[ t ].Next ) {
int v = Edge[ t ].To;
if( v == Fa ) continue;
Dp( v, u );
F[ u ][ 1 ] += F[ v ][ 0 ];
F[ u ][ 0 ] += max( F[ v ][ 0 ], F[ v ][ 1 ] );
}
return;
}

好像不知道怎么改成矩乘是吧。

不妨先看一看一条链的情况。

\[F[i][0]=\max\{F[i-1][0],F[i-1][1]\}\\
F[i][1]=A[i]+F[i-1][0]\\
\]

然后改写成矩乘

\[\begin{aligned}
\left[\begin{matrix}
0 & 0\\
A[i]&-\infty
\end{matrix}\right]
\left[\begin{matrix}
F[i-1][0]\\
F[i-1][1]
\end{matrix}\right]
=
\left[\begin{matrix}
F[i][0]\\
F[i][1]
\end{matrix}\right]
\end{aligned}
\]

既然可以在链上做,那么就可以考虑一下树剖。

现在剩余的问题在于轻儿子的转移。

不妨令\(G[i][0]\)表示不选\(i\)时去掉\(i\)的重儿子的答案,相对应的\(G[i][1]\)表示选\(i\)时去掉\(i\)的重儿子的答案。

如果用\(Son[ i ]\)表示\(i\)的重儿子,那么不难发现

\[\begin{aligned}
\left[\begin{matrix}
G[i][0] & G[i][0]\\
G[i][1] &-\infty
\end{matrix}\right]
\left[\begin{matrix}
F[Son[i]][0]\\
F[Son[i]][1]
\end{matrix}\right]
=
\left[\begin{matrix}
F[i][0]\\
F[i][1]
\end{matrix}\right]
\end{aligned}
\]

那么点\(u\)的\(F[u]\)可以通过这个点到这条链的底部的矩阵乘积之和。(这里的矩阵指的就是上面那个含有\(G\)的矩阵。)注意这里的矩阵是左乘,所以顺序应该是从深度小的一端乘到深度大的一端。

最后一步,维护\(G\)的值。其实修改也很简单。考虑到最原始的Dp,如果修改了轻儿子\(v\)的值,那么只要

G[i][0] += -max( Last[v][0], Last[v][1] ) + max( New[v][0], New[v][1] );
G[i][1] += -Last[v][0] + New[v][0];

然后在线段树上更新一下就好了。就是用新的值直接修改就好。这样所有的地方都走通了。

为了降低代码可读性,所以没有删去调试信息,看起来爽【滑稽】

#include <bits/stdc++.h>
#include <unistd.h>
//#define Debug
using namespace std; const int Maxn = 100010;
const int INF = 1000000000;
struct matrix {
int A[ 2 ][ 2 ];
matrix() {
A[ 0 ][ 0 ] = A[ 0 ][ 1 ] = A[ 1 ][ 0 ] = A[ 1 ][ 1 ] = -INF;
return;
}
matrix( int x, int y ) {
A[ 0 ][ 0 ] = A[ 0 ][ 1 ] = x;
A[ 1 ][ 0 ] = y;
A[ 1 ][ 1 ] = -INF;
return;
}
matrix( int a, int b, int c, int d ) {
A[ 0 ][ 0 ] = a; A[ 0 ][ 1 ] = b; A[ 1 ][ 0 ] = c; A[ 1 ][ 1 ] = d;
return;
}
inline matrix operator * ( const matrix Other ) const {
matrix Ans = matrix();
for( int i = 0; i < 2; ++i )
for( int j = 0; j < 2; ++j )
for( int k = 0; k < 2; ++k )
Ans.A[ i ][ j ] = max( Ans.A[ i ][ j ], A[ i ][ k ] + Other.A[ k ][ j ] );
#ifdef Debug
printf( "%15d %15d %15d %15d %15d %15d\n", A[ 0 ][ 0 ], A[ 0 ][ 1 ], Other.A[ 0 ][ 0 ], Other.A[ 0 ][ 1 ], Ans.A[ 0 ][ 0 ], Ans.A[ 0 ][ 1 ] );
printf( "%15d %15d %15d %15d %15d %15d\n", A[ 1 ][ 0 ], A[ 1 ][ 1 ], Other.A[ 1 ][ 0 ], Other.A[ 1 ][ 1 ], Ans.A[ 1 ][ 0 ], Ans.A[ 1 ][ 1 ] );
#endif
return Ans;
}
};
struct edge {
int To, Next;
edge() {}
edge( int _To, int _Next ) : To( _To ), Next( _Next ) {}
};
edge Edge[ Maxn << 1 ];
int Start[ Maxn ], UsedEdge;
int n, m, A[ Maxn ];
int Deep[ Maxn ], Father[ Maxn ], Size[ Maxn ], Son[ Maxn ], Top[ Maxn ], Dfn[ Maxn ], Ind[ Maxn ], Ref[ Maxn ], Used;
int Dp[ Maxn ][ 2 ], LDp[ Maxn ][ 2 ];
matrix G[ Maxn ], Tree[ Maxn << 2 ]; inline void AddEdge( int x, int y ) { Edge[ ++UsedEdge ] = edge( y, Start[ x ] ); Start[ x ] = UsedEdge; return; } void Dfs1( int u, int Fa ) {
Deep[ u ] = Deep[ Fa ] + 1; Father[ u ] = Fa; Size[ u ] = 1;
for( int t = Start[ u ]; t; t = Edge[ t ].Next ) {
int v = Edge[ t ].To;
if( v == Fa ) continue;
Dfs1( v, u );
Size[ u ] += Size[ v ];
if( Size[ v ] > Size[ Son[ u ] ] ) Son[ u ] = v;
}
} void Dfs2( int u, int Fa ) {
if( Son[ u ] ) {
Top[ Son[ u ] ] = Top[ u ]; Ind[ Son[ u ] ] = ++Used; Ref[ Used ] = Son[ u ];
Dfs2( Son[ u ], u );
}
for( int t = Start[ u ]; t; t = Edge[ t ].Next ) {
int v = Edge[ t ].To;
if( v == Fa || v == Son[ u ] ) continue;
Top[ v ] = v; Ind[ v ] = ++Used; Ref[ Used ] = v;
Dfs2( v, u );
}
return;
} void Build( int u, int Fa ) {
LDp[ u ][ 1 ] = A[ u ];
for( int t = Start[ u ]; t; t = Edge[ t ].Next ) {
int v = Edge[ t ].To;
if( v == Fa || v == Son[ u ] ) continue;
Build( v, u );
LDp[ u ][ 0 ] += max( Dp[ v ][ 0 ], Dp[ v ][ 1 ] );
LDp[ u ][ 1 ] += Dp[ v ][ 0 ];
}
if( Son[ u ] ) Build( Son[ u ], u );
Dp[ u ][ 0 ] = LDp[ u ][ 0 ] + max( Dp[ Son[ u ] ][ 0 ], Dp[ Son[ u ] ][ 1 ] );
Dp[ u ][ 1 ] = LDp[ u ][ 1 ] + Dp[ Son[ u ] ][ 0 ];
G[ u ] = matrix( LDp[ u ][ 0 ], LDp[ u ][ 1 ] );
if( Son[ u ] ) G[ u ] = G[ u ] * G[ Son[ u ] ];
return;
} void BuildTree( int Index, int Left, int Right ) {
if( Left == Right ) {
Tree[ Index ] = matrix( LDp[ Ref[ Left ] ][ 0 ], LDp[ Ref[ Left ] ][ 1 ] );
return;
}
int Mid = ( Left + Right ) >> 1;
if( Left <= Mid ) BuildTree( Index << 1, Left, Mid );
if( Right > Mid ) BuildTree( Index << 1 | 1, Mid + 1, Right );
#ifdef Debug
printf( " %d %d <-- %d %d + %d %d\n", Left, Right, Left, Mid, Mid + 1, Right );
#endif
Tree[ Index ] = Tree[ Index << 1 ] * Tree[ Index << 1 | 1 ];
return;
} matrix Query( int Index, int Left, int Right, int L, int R ) {
if( L <= Left && Right <= R ) return Tree[ Index ];
int Mid = ( Left + Right ) >> 1;
if( R <= Mid ) return Query( Index << 1, Left, Mid, L, R );
if( L > Mid ) return Query( Index << 1 | 1, Mid + 1, Right, L, R );
return Query( Index << 1, Left, Mid, L, R ) * Query( Index << 1 | 1, Mid + 1, Right, L, R );
} void Update( int Index, int Left, int Right, int Pos ) {
if( Left == Right ) {
Tree[ Index ] = matrix( LDp[ Ref[ Left ] ][ 0 ], LDp[ Ref[ Left ] ][ 1 ] );
return;
}
int Mid = ( Left + Right ) >> 1;
if( Pos <= Mid ) Update( Index << 1, Left, Mid, Pos );
if( Pos > Mid ) Update( Index << 1 | 1, Mid + 1, Right, Pos );
Tree[ Index ] = Tree[ Index << 1 ] * Tree[ Index << 1 | 1 ];
return;
} void Change( int u, int Key ) {
LDp[ u ][ 1 ] += -A[ u ] + Key; A[ u ] = Key;
matrix Last = G[ Top[ u ] ];
Update( 1, 1, n, Ind[ u ] );
G[ Top[ u ] ] = Query( 1, 1, n, Ind[ Top[ u ] ], Dfn[ Top[ u ] ] );
#ifdef Debug
printf( " u = %d, %d %d\n", u, LDp[ u ][ 0 ], LDp[ u ][ 1 ] );
printf( " Tu = %d, %d %d\n", Top[ u ], G[ Top[ u ] ].A[ 0 ][ 0 ], G[ Top[ u ] ].A[ 1 ][ 0 ] );
#endif
int Son = Top[ u ];
u = Father[ Top[ u ] ];
while( u ) {
LDp[ u ][ 0 ] += -max( Last.A[ 0 ][ 0 ], Last.A[ 1 ][ 0 ] ) + max( G[ Son ].A[ 0 ][ 0 ], G[ Son ].A[ 1 ][ 0 ] );
LDp[ u ][ 1 ] += -Last.A[ 0 ][ 0 ] + G[ Son ].A[ 0 ][ 0 ];
Last = G[ Top[ u ] ];
#ifdef Debug
printf( "Update %d\n", u );
#endif
Update( 1, 1, n, Ind[ u ] );
#ifdef Debug
printf( "Query %d %d\n", Ind[ Top[ u ] ], Dfn[ Top[ u ] ] );
#endif
G[ Top[ u ] ] = Query( 1, 1, n, Ind[ Top[ u ] ], Dfn[ Top[ u ] ] );
#ifdef Debug
printf( " u = %d, %d %d\n", u, LDp[ u ][ 0 ], LDp[ u ][ 1 ] );
printf( " Tu = %d, %d %d\n", u, G[ Top[ u ] ].A[ 0 ][ 0 ], G[ Top[ u ] ].A[ 1 ][ 0 ] );
#endif
Son = Top[ u ];
u = Father[ Top[ u ] ];
}
return;
} int main() {
scanf( "%d%d", &n, &m );
for( int i = 1; i <= n; ++i ) scanf( "%d", &A[ i ] );
for( int i = 1; i < n; ++i ) {
int x, y; scanf( "%d%d", &x, &y );
AddEdge( x, y ); AddEdge( y, x );
}
Dfs1( 1, 0 );
Top[ 1 ] = 1; Ind[ 1 ] = ++Used; Ref[ Used ] = 1;
Dfs2( 1, 0 );
for( int i = 1; i <= n; ++i ) Dfn[ Top[ i ] ] = max( Dfn[ Top[ i ] ], Ind[ i ] );
Build( 1, 0 );
BuildTree( 1, 1, n );
#ifdef Debug
printf( "Used : %d\n", Used );
printf( "Top : " ); for( int i = 1; i <= n; ++i ) printf( "%d ", Top[ i ] ); printf( "\n" );
printf( "Son : " ); for( int i = 1; i <= n; ++i ) printf( "%d ", Son[ i ] ); printf( "\n" );
printf( "Ind : " ); for( int i = 1; i <= n; ++i ) printf( "%d ", Ind[ i ] ); printf( "\n" );
printf( "Ref : " ); for( int i = 1; i <= n; ++i ) printf( "%d ", Ref[ i ] ); printf( "\n" );
printf( "Dfn : " ); for( int i = 1; i <= n; ++i ) printf( "%d ", Dfn[ i ] ); printf( "\n" );
printf( "Fa : " ); for( int i = 1; i <= n; ++i ) printf( "%d ", Father[ i ] ); printf( "\n" );
printf( "%d ", max( Dp[ 1 ][ 0 ], Dp[ 1 ][ 1 ] ) );
printf( "%d\n", max( G[ 1 ].A[ 0 ][ 0 ], G[ 1 ].A[ 1 ][ 0 ] ) );
for( int i = 1; i <= n; ++i ) printf( " ( %d, %d, %d, %d )\n", LDp[ i ][ 0 ], LDp[ i ][ 1 ], Dp[ i ][ 0 ], Dp[ i ][ 1 ] );
printf( "Testing...\n" );
int IsOk = 1;
for( int i = 1; i <= n; ++i ) {
printf( " %d : Check %d To %d\n", i, Ind[ i ], Dfn[ Top[ i ] ] );
matrix Temp = Query( 1, 1, n, Ind[ i ], Dfn[ Top[ i ] ] );
printf( " ---> %d - %d %d - %d\n", Temp.A[ 0 ][ 0 ], Dp[ i ][ 0 ], Temp.A[ 1 ][ 0 ], Dp[ i ][ 1 ] );
if( Temp.A[ 0 ][ 0 ] != Dp[ i ][ 0 ] || Temp.A[ 1 ][ 0 ] != Dp[ i ][ 1 ] ) printf( "Err %d\n", i ), IsOk = 0;
}
if( IsOk ) printf( "Ok.\n" ); else {
printf( "Ooooops.\n" );
return 0;
}
for( int i = 1; i <= n; ++i ) {
printf( " %d : %d - %d, %d - %d\n", i, Dp[ i ][ 0 ], G[ i ].A[ 0 ][ 0 ], Dp[ i ][ 1 ], G[ i ].A[ 1 ][ 0 ] );
if( Dp[ i ][ 0 ] != G[ i ].A[ 0 ][ 0 ] || Dp[ i ][ 1 ] != G[ i ].A[ 1 ][ 0 ] ) {
printf( "Err\n" );
IsOk = 0;
}
}
if( IsOk ) printf( "Ok.\n" ); else {
printf( "Ooooops.\n" );
return 0;
}
#endif
for( int i = 1; i <= m; ++i ) {
int x, y; scanf( "%d%d", &x, &y );
Change( x, y );
printf( "%d\n", max( G[ 1 ].A[ 0 ][ 0 ], G[ 1 ].A[ 1 ][ 0 ] ) );
#ifdef Debug
sleep( 1 );
#endif
}
return 0;
}

动态DP教程的更多相关文章

  1. 【学习笔记】动态 dp 入门简易教程

    序列 dp 引入:最大子段和 给定一个数列 \(a_1, a_2, \cdots, a_n\)(可能为负),求 \(\max\limits_{1\le l\le r\le n}\left\{\sum_ ...

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

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

  3. Luogu P4643 【模板】动态dp

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

  4. 动态dp学习笔记

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

  5. 洛谷P4719 动态dp

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

  6. 动态 DP 学习笔记

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

  7. 动态dp初探

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

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

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

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

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

随机推荐

  1. LeetCode:181.超过经理收入的员工

    题目链接:https://leetcode-cn.com/problems/employees-earning-more-than-their-managers/ 题目 Employee 表包含所有员 ...

  2. Delphi 10.3.2试用报告

    感谢朋友们,如此之快就发了注册机,209321818群里有下载,感兴趣可以去. 安装前,需要先卸载Delphi 10.3.1,然后就是正常的安装过程,非常顺利,现在差不多半小时就安装完成. 安装后,启 ...

  3. MongoDB 各个位版本下载地址

    官网首页下载需要填写资料 windows版本 Linux版本

  4. 如何在cmd命令行中运行Java程序

    cmd运行java 有萌新问我怎么用cmd运行Java,他运行报错了,怎么办?如图是他的执行过程: 他说就这一个类,里面包含了main方法怎么会没有加载主类呢. 其实很简单,因为你执行的时候并不能直接 ...

  5. 【3】Zookeeper中的角色

    一.Zookeeper角色的分类 领导者(leader) Leader服务器为客户端提供读服务和写服务. 学习者(learner) 跟随者(follower) Follower服务器为客户端提供读服务 ...

  6. Win10系统的开机启动项如何去关闭?

    我们在使用电脑时会安装许多的应用程序,而有些应用程序会默认在电脑开机时自行启动,不仅影响开机速度,还会在开机后占用电脑内存资源. 那么在Win10系统中,我们该如何查看有哪些开机启动项呢?我们又该怎么 ...

  7. 并发编程: GIL锁、GIL与互斥锁区别、进程池与线程池的区别

    一.GIL 二.关于GIL性能的讨论 三.计算密集测试 四.IO密集测试 五.GIL与互斥锁 六.TCP客户端 七.进程池 八.进程什么时候算是空闲 九.线程池 一.GIL GIL Global In ...

  8. 1.python环境安装

    一:安装Python与环境配置 二:安装pip 三:Anaconda安装和使用 3.1 什么是 Anaconda? Anaconda是专注于数据分析的Python发行版本,支持 Linux, Mac, ...

  9. Linux根文件系统和目录结构及bash特性3

    bash的基础特性: 命令补全:        shell程序在接收到用户执行命令的请求,分析完成之后,最左侧的字符串会被当作命令        命令查找机制:            查找内部命令   ...

  10. [工具] fierce--子域收集

    简介 fierce 是使用多种技术来扫描目标主机IP地址和主机名的一个DNS服务器枚举工具.运用递归的方式来工作.它的工作原理是先通过查询本地DNS服务器来查找目标DNS服务器,然后使用目标DNS服务 ...