Live2D

Note -「动态 DP」学习笔记

Introduction

Problem 1

  给定序列 {an},其中 aiZ,求其最大子段和(不能为空)。

  很显然的 DP——令 fi 为以 i 为右端点的最大子段和,gi[1,i] 内的最大子段和,有:

{fi={aii=1max{fi1+ai,ai}otherwisegi={aii=1max{gi1,fi}otherwise

  O(n) 搞定。


  不过我们来深究一下这个转移形式。以 f 的转移为例,我们把它写成“矩阵乘法”:

[aiai0][fi10]=[fi0]

  当然啦,这不是传统意义的矩乘,我们实际上定义:

[abcd][ef]=[max{a+e,b+f}max{c+e,d+f}]

  不过这看似突发奇想的定义有什么实际作用呢?

  联想到矩阵快速幂,但快速幂需要保证矩阵具有结合律,即对于任意矩阵 A,B 和向量 x 都应满足:

(AB)x=A(Bx)

  把上面的定义代入,就会发现这种矩乘仍满足结合律!而本质上,就是由于 + 运算对于 max 运算具有分配率a+max{b,c}=max{a+b,a+c})。

  所以到底有什么用嘛 qwq!我们走进下一题。

Problem 2

  给定序列 {an},其中 aiZ,支持单点修改,询问区间最大子段和(不能为空)。

  状态定义和上一题完全一样,设询问区间 (l,r),那么边界为 fl=gl=al。考虑转移的通项,我们用列向量 [figi0] 表示一个状态,直接从矩乘的角度设计转移矩阵,那么:

[figi0]=[aiaiai0ai0][fi1gi10]

  记 Ai=[aiaiai0ai0]。我们希望求到 [frgr0],那么不断用上述公式展开右侧最后一项直到到达边界,有:

[frgr0]=Ar[fr1gr10]=ArAr1[fr2gr20]==ArAr1Al+1[alal0]

  注意到 [alal0]=Al0,其中 0 指零向量。那么进一步化简得:

[frgr0]=ArAr1Al0

  相当于求区间矩阵的乘积,而在上文中已经得出,这种矩阵乘法具有结合律!所以可以用线段树维护区间矩阵乘积,单点修改时暴力修改单个矩阵和 O(logn) 个乘积即可。

  复杂度 O(k3nlogn),其中 k 为方阵的阶,k=3


  这里有必要阐明一个许多动态 DP 入门讲解没有提到的细节。在线段树维护时,我们自然而然地维护了区间左 × 右的积。以 pushup 函数为例:

void pushup ( const int rt ) { mt[rt] = mt[rt << 1] * mt[rt << 1 | 1]; }

  但是,我们需要的 ArAr1Al 是从右乘到左的积呀,我们所定义的矩乘在同阶方阵中真的具有交换律么?

  答案是否定的!而这样做的正确性来源于题目本身——翻转整个区间,其最大子段和不变!如果某些题目不满足翻转区间答案不变的性质,是不能交换乘法顺序的!

Code

#include <cstdio>
#include <cstring>
#include <assert.h>

const int MAXN = 5e4, NINF = 0xc0c0c0c0; // NINF即-INF。 
int n, m, a[MAXN + 5];

inline int max_ ( const int a, const int b ) { return a < b ? b : a; }

struct Matrix {
	int n, m, mat[3][3];
	Matrix () {}
	Matrix ( const int tn, const int tm ): n ( tn ), m ( tm ), mat {} {}
	inline int* operator [] ( const int key ) { return mat[key]; }
	inline Matrix operator * ( Matrix t ) {
		assert ( m == t.n ); 
		Matrix ret ( n, t.m );
		memset ( ret.mat, 0xc0, sizeof ret.mat );
		// 这里注意,根据乘法定义,零矩阵的所有元素为-INF。 
		for ( int i = 0; i < n; ++ i ) {
			for ( int k = 0; k < m; ++ k ) {
				for ( int j = 0; j < t.m; ++ j ) {
					ret[i][j] = max_ ( ret[i][j], mat[i][k] + t[k][j] );
				}
			}
		}
		return ret;
	}
} zero ( 3, 1 ); // zero是真正意义上的零向量,注意与零矩阵区别。 

inline void makeMat ( Matrix& a, const int v ) { // 构造 Ai。 
	a[0][0] = a[0][2] = v, a[0][1] = NINF;
	a[1][0] = a[1][2] = v;
	a[2][0] = a[2][1] = NINF;
}

struct SegmentTree {
	Matrix mt[MAXN << 2];
	inline void pushup ( const int rt ) { mt[rt] = mt[rt << 1] * mt[rt << 1 | 1]; }
	inline void init ( const int rt, const int l, const int r ) {
		mt[rt] = Matrix ( 3, 3 );
		if ( l == r ) return makeMat ( mt[rt], a[l] );
		int mid = l + r >> 1;
		init ( rt << 1, l, mid ), init ( rt << 1 | 1, mid + 1, r );
		pushup ( rt );
	}
	inline void update ( const int rt, const int l, const int r, const int x, const int v ) {
		if ( l == r ) return makeMat ( mt[rt], v );
		int mid = l + r >> 1;
		if ( x <= mid ) update ( rt << 1, l, mid, x, v );
		else update ( rt << 1 | 1, mid + 1, r, x, v );
		pushup ( rt );
	}
	inline Matrix query ( const int rt, const int l, const int r, const int ql, const int qr ) {
		if ( ql <= l && r <= qr ) return mt[rt];
		Matrix ret ( 3, 3 ); // 注意这里ret并不是单位矩阵,所以第一次更新应当直接赋值。 
		int mid = l + r >> 1, f = 0;
		if ( ql <= mid ) ret = query ( rt << 1, l, mid, ql, qr ), f = 1;
		if ( mid < qr ) {
			if ( ! f ) ret = query ( rt << 1 | 1, mid + 1, r, ql, qr );
			else ret = ret * query ( rt << 1 | 1, mid + 1, r, ql, qr ); 
		}
		return ret;
	}
} sgt;

int main () {
	zero[0][0] = zero[1][0] = NINF;
	scanf ( "%d", &n );
	for ( int i = 1; i <= n; ++ i ) scanf ( "%d", &a[i] );
	sgt.init ( 1, 1, n );
	scanf ( "%d", &m );
	for ( int i = 1, op, l, r; i <= m; ++ i ) {
		scanf ( "%d %d %d", &op, &l, &r );
		if ( ! op ) sgt.update ( 1, 1, n, l, r );
		else printf ( "%d\n", ( sgt.query ( 1, 1, n, l, r ) * zero )[1][0] );
	}
	return 0;
}

  前两题链接:Problem 1Problem 2


  诸如此类,定义矩阵乘法进行 DP 转移,继而动态维护转移矩阵的算法,就是所谓动态 DP(DDP?)。

Training

「CF 750E」New Year and Old Subsequence

Description

  Link.

  给定一个长度为 n 的数字串 sq 次询问 s[l..r] 需要删除多少个字符使得 "2017" 是其子串而 "2016" 不是。

  n,q2×105

Solution

  先考虑整个串,设 f(i,04) 表示第 1i 内,已经与 "2017" 匹配了长度为 04 的子串时,最小的删除次数。分转移为 2,0,1,7,6 和其它数字设计转移矩阵即可。

  这道题体现了“不满足翻转区间答案不变的性质,不能交换乘法顺序”这一点。

「洛谷 P4719」「模板」"动态 DP" & 动态树分治

Description

  Link.

  给定一棵 n 个结点的带权树,m 次单点点权修改,求出每次修改后的带权最大独立集。

  n,m105

Solution

  不考虑修改,显然 DP。令 f(u,0/1) 表示选 / 不选结点 uu 子树内的带权最大独立集。那么:

{f(u,0)=vmax{f(v,0),f(v,1)}f(u,1)=vf(v,0)+au

  引入修改,我们自然想用数据结构维护转移。那么就需要进行树链剖分(不一定是重链剖分,这里以复杂度更小的 LCT 为例)。假设 u 的实儿子是 su,我们单独维护 su 的贡献,而把其它儿子一起考虑。设 g(u,0/1) 表示选 / 不选结点 u结点 u 及其虚儿子的子树们的带权最大独立集。有:

{g(u,0)=vsumax{f(v,0),f(v,1)}g(u,1)=vsuf(v,0)+au

  用它来表示 f

{f(u,0)=g(u,0)+max{f(su,0),f(su,1)}f(u,1)=g(u,1)+f(su,0)

  写成矩乘:

[f(u,0)f(u,1)]=[g(u,0)g(u,0)g(u,1)][f(su,0)f(su,1)]

  令 Gu=[g(u,0)g(u,0)g(u,1)],每次修改,仅会影响 O(logn) 个链头,也即是 O(logn) 个虚实交替的位置的 G 需要修改,就可以维护了。

  详细题解:my solution(含实现细节说明)。

「洛谷 P6021」洪水

Description

  Link.

  给定一棵 n 个点的带点权树,删除 u 点的代价是该点点权 aum 次操作:

  • 修改单点点权。
  • 询问让某棵子树的根不可到达子树内任意一片叶子的代价。

  n,m2×105

Solution

  还是不考虑修改啦,列出 DP:

f(u)={auu is leafmin{au,vf(v)}otherwise

  单独拿出实儿子 su

g(u)={+u is leafvsuf(v)otherwisef(u)=min{au,f(su)+g(u)}

  定义矩乘的 + 为加法,× 为取 min,有:

[f(u)0]=[g(u)au+0][f(su)0]

  仍可用 LCT / 树剖维护。若使用 LCT,询问 u 子树时,应 access 原树上 u 的父亲,再 splay u,就能保证当前 u 的实链全部在子树内,输出 u 维护的矩乘答案即可。

  详细题解:my solution

「SP 6779」GSS7

Description

  Link.

  给定一棵 n 个点的带点权树,q 次操作:

  • 路径点权赋值。
  • 询问路径最大子段和(可以为空)。

  n,q105

Solution

  嘛……其实就是引例的题搬到树上 qwq。应该可以熟练地列出转移矩阵了叭,设 f(u) 为以 u 为端点的最大子段和,g(u) 为前缀最大子段和,suu 的重儿子(这题来练练树剖 www),有:

[g(u)f(u)0]=[0au0au00][g(su)f(su)0]

  注意在树剖跳重链求答案的时候,必须注意矩乘顺序。例如对于路径 (u,v),钦定 u 为路径起点,当 u 向上跳时,转移矩阵按 DFN 降序;当 v 向上跳时转移矩阵按 DFN 升序,所以线段树应维护两个乘法顺序的矩阵积。

  这道题有些卡常(而且我常数貌似很大 qwq),所以手玩一下转移矩阵的幂,发现:

[0vv00]k=[0max{v,kv}max{0,kv}kvmax{0,kv}0]

  就可以 O(1) 求出矩阵幂了。

  详细题解:my solution

「NOIP 2018」「洛谷 P5024」保卫王国

Description

  Link.

  给定一棵 n 个点的带点权树,q 次询问,每次钦定两个点必须选或不选,求此时点权和最小的选点方案,使得一条边两端至少有一个点被选择。

  n,q105

Solution

  板不板?哪里需要什么倍增?

  f(u,0/1) 为不选 / 选 u 点时子树最优方案,g(u,0/1) 为不选 / 选 u 点时除实儿子 su 以外的最优方案,有:

[f(u,0)f(u,1)]=[+g(u,0)g(u,1)g(u,1)][f(su,0)f(su,1)]

  考虑表示“钦定”:将不选的点的点权增加 +,将必选的点的点权增加 ,显然最优方案必选此时点权极小的“必选点”,必不选点权极大的“不选点”。那么正常地 splay(root) 得到整棵树的 DP 结果即可。注意要将点权复原。

  详细题解:懒了 www,需要参考代码见云剪切板

posted @   Rainybunny  阅读(193)  评论(1编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示