知識總結 動態 DP

2022-06-13 19:06:11 字數 3262 閱讀 9046

勾起了我悲傷的回憶 —— noip2018 316pts ……

主要思想:將 dp 過程分解為方便單點修改和乙個區間合併的操作(通常類似矩陣乘法),然後用資料結構(通常為線段樹)維護。

例:給定乙個長為 \(n\) 的整數序列,相鄰兩個數最多選乙個,有 \(m\) 次修改序列中的乙個數,求每次修改後選出數之和的最大值。

\(n,m\leq 10^5\) 。

如果不會做不帶修改的情況,請默默摁 ctrl + w 然後去學 dp 入門

如果不帶修改,明顯設 \(f_\) 表示當第 \(i\) 個點選 (0) / 不選 (1) 時,前 \(i\) 個點的和的最大值。於是有如下轉移方程:

\[f_=f_

\]\[f_=\max(f_,f_)+a_i

\]如果加入修改操作呢?只有這兩個 dp 方程比較難辦,因為修改乙個值就要重新計算後面的所有答案。gg

接下來是「動態 dp 」中最巧妙的部分:考慮用乙個矩陣來表示從 \(i-1\) 點向 \(i\) 點轉移,用某個表示「初始狀態」的矩陣依次乘上每個點的轉移就是答案。因為矩陣乘法有結合律,所以可以把答案表示成「初始狀態」乘上「修改點前面的矩陣乘積」乘上「當前位置修改後的矩陣」乘上「修改點後面的矩陣乘積」。這樣只需要用線段樹單點修改和查詢區間乘積(事實上這道題只需要查全域性乘積)即可。

然而,這道題中轉移的運算並不是加和乘,尤其是其中還有乙個礙眼的求最大值。但我們可以把矩陣乘法的定義稍加修改,把原來兩個整數的「乘法」改為兩個整數的加法,「加法」改為對兩個整數取最大值。這樣我們就構造如下轉移矩陣:

\[\begin

f_&f_

\end

\begin

0&a_i\\

0&-\infty\\

\end=

\begin

f_&f_\\

\end\]

還有乙個很多人沒考慮過的細節 (可能是大佬們認為這個問題太顯然不需要考慮) :這個「初始狀態」是什麼呢?對於這道題,前乙個數如果不選是不影響當前決策的,而如果選了的話就會造成乙個當前點不能選的「約束」。而第乙個點無論如何都不會受到這種「約束」,所以第乙個點的「前乙個點」應該被看作「沒有選」,即初始狀態為 \(\begin0&-\infty\end\) 。

我們把這個問題擴充套件到樹上,即每條邊的兩端點中至少選乙個點(洛谷 4719【模板】動態 dp )。考慮樹鏈剖分來轉化成序列問題。設 \(f_\) 表示 \(i\) 點選 / 不選時 \(i\) 點子樹中的最大權值和,\(g_\) 表示 \(i\) 點選 / 不選時 \(i\) 點子樹除 \(s_i\) 的子樹以外的部分中的最大權值和,其中 \(s_i\) 是 \(i\) 的重兒子。對於一條重鏈有如下方程:

\[\begin

f_&f_

\end

\begin

g_&g_\\

g_&-\infty\\

\end=

\begin

f_&f_\\

\end\]

這樣,每個點的答案是「初始狀態」乘上它到所在重鏈末尾的矩陣乘積。

至於具體實現,可以開始先一遍 dp 算出所有的 \(f\) 和 \(g\) 。每次修改時沿著重鏈向上爬,暴力修改鏈首父親的 \(g\) 值。鏈首到鏈首父親的邊是一條輕邊,所以這樣每次修改乙個點時要更新 \(g\) 值的點的數量約等於當前點到根的路徑上的輕邊數量(可能有加一減一之類的細節),是 \(o(\log n)\) 。因此總複雜度 \(o(mlog^2n)\) 。

和上面類似的分析,初始狀態(葉子節點那個不存在的重兒子的 \(f\) 值)是 \(\begin0&-\infty\end\) 。用這個東西去乘相當於取原矩陣的第一行,所以不需要「顯式」地乘。

**:很抱歉我**裡的矩陣行列和上文是反的,所有矩陣乘法的順序也是反的我也不知道怎麼回事 qaq 。

#include #include #include #include using namespace std;

namespace zyt

templateinline void write(t x)

const int n = 1e5 + 10, inf = 0x3f3f3f3f;

int n, m, head[n], ecnt, w[n], size[n], son[n], fa[n], dfn[n], dfncnt, top[n], f[n][2], g[n][2], end[n], pos[n];

struct edge

e[n << 1];

void add(const int a, const int b)

, head[a] = ecnt++;

} void dfs(const int u, const int f) }

void dfs2(const int u, const int t) }

void dfs3(const int u)

f[u][0] = g[u][0], f[u][1] = g[u][1];

if (son[u])

}struct matrix

matrix operator * (const matrix &b) const

}val[n];

namespace segment_tree

tree[n << 2];

void update(const int rot)

void build(const int rot, const int lt, const int rt)

void change(const int rot, const int lt, const int rt, const int p)

matrix query(const int rot, const int lt, const int rt, const int ls, const int rs)

}int work()

dfs(1, 0), dfs2(1, 1), dfs3(1);

for (int i = 1; i <= n; i++)

val[i].data[0][0] = val[i].data[0][1] = g[i][0], val[i].data[1][0] = g[i][1], val[i].data[1][1] = -inf;

build(1, 1, n);

while (m--)

matrix ans = query(1, 1, n, dfn[1], dfn[end[1]]);

write(max(ans.data[0][0], ans.data[1][0])), putchar('\n');

} return 0; }}

int main()

總結 動態DP學習筆記

學習了一下動態dp 首先乙個顯然的 o nm 的做法就是每次做一遍樹形dp 這也是我在noip考場上唯一拿到的部分分 直接考慮如何優化這個東西。簡化一下問題,假如這棵樹是一條鏈,那就變得很簡單了,可以直接拿線段樹維護矩陣加速。可是如果每個點不止有乙個兒子呢?我們首先樹剖一下。設 g i 0 sum ...

DP動態規劃部分學習總結

這幾天學習動態規劃,我的理解是dp大致可分為2類,一種是自下而上 也叫遞推 另一種是自上而下。這兩種方法都可以達到目的。仔細來講動態規劃是解決多階段決策問題的一種方法。並且每一步得出的結論都講影響下一階段的結果將每一階段都需要取出最佳結果然後一步步下去得出最終答案。並且動態規劃要秉承最優性原理。而最...

動態規劃1(DP)總結

遞迴是最簡單也是最直接的思路,分解小問題,然後得到最終問題的答案。但是遞迴 簡單,但是比較耗時。所以我們思路可以用遞迴,但是出於 效能考慮,還是要提公升效率。這當然是後期的優化了。動態規劃,回溯都是比較常用也比較常見的演算法。這篇來講動態規劃。動態規劃的核心 問題的最優解可以由子問題的最優解得到,那...