「动态 DP」学习笔记

\(\mathcal{Introduction}\)

\(\mathcal{Problem~1}\)

  给定序列 \(\{a_n\}\),其中 \(a_i\in\mathbb Z\),求其最大子段和(不能为空)。

  很显然的 DP——令 \(f_i\) 为以 \(i\) 为右端点的最大子段和,\(g_i\)\([1,i]\) 内的最大子段和,有:

\[\begin{cases} f_i=\begin{cases} a_i&i=1\\ \max\{f_{i-1}+a_i,a_i\}&\text{otherwise} \end{cases}\\ g_i=\begin{cases} a_i&i=1\\ \max\{g_{i-1},f_i\}&\text{otherwise} \end{cases} \end{cases} \]

  \(\mathcal O(n)\) 搞定。


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

\[\begin{bmatrix} a_i&a_i\\ -\infty&0 \end{bmatrix} \begin{bmatrix} f_{i-1}\\ 0 \end{bmatrix}= \begin{bmatrix} f_i\\ 0 \end{bmatrix} \]

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

\[\begin{bmatrix} a&b\\ c&d \end{bmatrix} \begin{bmatrix} e\\ f \end{bmatrix}= \begin{bmatrix} \max\{a+e,b+f\}\\ \max\{c+e,d+f\} \end{bmatrix} \]

扫描二维码关注公众号,回复: 11433154 查看本文章

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

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

\[(AB)\boldsymbol x=A(B\boldsymbol x) \]

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

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

\(\mathcal{Problem~2}\)

  给定序列 \(\{a_n\}\),其中 \(a_i\in\mathbb Z\),支持单点修改,询问区间最大子段和(不能为空)。

  状态定义和上一题完全一样,设询问区间 \((l,r)\),那么边界为 \(f_l=g_l=a_l\)。考虑转移的通项,我们用列向量 \(\begin{bmatrix}f_i\\g_i\\0\end{bmatrix}\) 表示一个状态,直接从矩乘的角度设计转移矩阵,那么:

\[\begin{bmatrix} f_i\\ g_i\\ 0 \end{bmatrix}= \begin{bmatrix} a_i&-\infty&a_i\\ a_i&0&a_i\\ -\infty&-\infty&0 \end{bmatrix} \begin{bmatrix} f_{i-1}\\ g_{i-1}\\ 0 \end{bmatrix} \]

  记 \(A_i=\begin{bmatrix}a_i&-\infty&a_i\\a_i&0&a_i\\-\infty&-\infty&0\end{bmatrix}\)。我们希望求到 \(\begin{bmatrix}f_r\\g_r\\0\end{bmatrix}\),那么不断用上述公式展开右侧最后一项直到到达边界,有:

\[\begin{bmatrix} f_r\\ g_r\\ 0 \end{bmatrix}= A_r \begin{bmatrix} f_{r-1}\\ g_{r-1}\\ 0 \end{bmatrix}= A_rA_{r-1} \begin{bmatrix} f_{r-2}\\ g_{r-2}\\ 0 \end{bmatrix}=\cdots= A_{r}A_{r-1}\cdots A_{l+1} \begin{bmatrix} a_l\\ a_l\\ 0 \end{bmatrix} \]

  注意到 \(\begin{bmatrix}a_l\\a_l\\0\end{bmatrix}=A_l\boldsymbol 0\),其中 \(\boldsymbol0\) 指零向量。那么进一步化简得:

\[\begin{bmatrix} f_r\\ g_r\\ 0 \end{bmatrix}= A_rA_{r-1}\cdots A_l\boldsymbol0 \]

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

  复杂度 \(\mathcal O(k^3n\log n)\),其中 \(k\) 为方阵的阶,\(k=3\)


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

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

  但是,我们需要的 \(A_rA_{r-1}\cdots A_l\) 是从右乘到左的积呀,我们所定义的矩乘在同阶方阵中真的具有交换律么?

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

\(\mathcal{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?)。

猜你喜欢

转载自www.cnblogs.com/rainybunny/p/13374842.html