「動態 DP」學習筆記 (updating)
\(\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} \]
不過這看似突發奇想的定義有什麼實際作用呢?
聯想到矩陣快速冪,但快速冪需要保證矩陣具有結合律,即對於任意矩陣 \(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\}\)
狀態定義和上一題完全一樣,設詢問區間 \((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;
}
諸如此類,定義矩陣乘法進行 DP 轉移,繼而動態維護轉移矩陣的演算法,就是所謂動態 DP(DDP?)。