首先我們可以發現,貪心是行不通的。見 hack:
1
10 1
10 1 1
0 0 0 100
假如每次都選可以走的點中最大的,那麼走出來的是 \(1 + 10 + 10 + 0 = 20\),但實際上,我們可以走 \(1 + 10 + 1 + 100 = 112\),這也是最優路徑。
所以,我們考慮動態規劃(dp)。
設 \(f_\) 表示從第 \(i\) 行第 \(j\) 列走到最後一行的最大的和。邊界根據定義(術語叫做狀態),就是 \(f_ = w_\)(\(w\) 是點的權值)。這樣,我們只需要從最後一層逆著往上走,推到 \(f_\) 即是答案。
考慮 \((i, j)\) 是如何被走到的,發現可以被 \((i + 1, j)\) 走到,也可以被 \((i + 1, j + 1)\) 走到(這裡是逆著走的)。那麼我們得到乙個柿子(術語叫做狀態轉移方程,也就是 \(f_\) 這個狀態是怎麼從別的狀態轉移過來的),\(f_ = \max(f_, f_) + w_\)(別忘了加上本身的權值)。
上面我們介紹了逆推,那麼,可不可以順推呢?
既然要順推,我們定義 \(f_\) 表示從 \((1, 1)\) 走到 \((i, j)\) 的最大和。邊界即 \(f_ = w_\)。
順著走,發現 \((i, j)\) 可以被 \((i - 1, j)\) 和 \((i - 1, j - 1)\) 走到。從而有 \(f_ = \max(f_, f_) + w_\)。
由於不能保證最後在**結束,所以答案為 \(\max_\\}\)(走到第 \(n\) 行的所有路徑的最大值)。
最長上公升子串行即 longest increasing subsequence(lis)。這裡我們介紹簡單的 \(o(n ^ 2)\) 做法。
設 \(f_i\) 表示前 \(i\) 個數中以 \(a_i\) 結尾(經常這麼設計狀態)的 lis 長度。初始 \(f_i = 1\)(\(a_i\) 自己就是乙個上公升子串行)。
考慮如何轉移。注意到前 \(i\) 個數中以 \(a_i\) 結尾的 lis 一定是乙個不以 \(a_i\) 結尾的 lis「拼上」\(a_i\)。那麼我們可以列舉這個不以 \(a_i\) 結尾的 lis 的結尾 \(j\)。從而有狀態轉移方程 \(f_i = \max_\\)(這個狀態轉移方程很重要,請細細體會)。
**如下:
#include using namespace std;
const int n = 1005;
int n, a[n], f[n], ans;
int main()
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j] < a[i] && f[j] + 1 > f[i])
f[i] = f[j] + 1;
for (int i = 1; i <= n; i++)
if (f[i] > f[ans]) ans = i;
cout << f[ans] << endl;
return 0;
}
那如果我們要輸出 lis 呢?其實也非常簡單。我們用乙個 \(pre\) 陣列來記錄,表示 \(i\) 是從 \(pre_i\) 「轉移」過來的(\(pre_i\) 下乙個就是 \(i\))。這樣,我們只需要在轉移的時候,加上pre[i] = j
即可。
輸出時,我們可以使用遞迴。**如下:
#include using namespace std;
const int n = 5005;
int n, a[n], pre[n], f[n], ans;
void print(int x)
int main()
for (int i = 2; i <= n; i++)
for (int j = 1; j < i; j++)
if (a[j] < a[i] && f[j] + 1 > f[i])
for (int i = 1; i <= n; i++)
if (f[i] > f[ans]) ans = i;
cout << f[ans] << endl;
print(ans);
return 0;
}
這種記錄前驅 \(pre\) 或者字尾 \(nxt\) 的輸出方法在 dp、搜尋中很常見,請讀者一定掌握。
ps:還有 \(o(n \log n)\) 的 lis 方法,使用二分,可以自行上網搜尋。
最長公共子串行,即 longest common subsequence(lcs)。
給出兩個長度為 \(n\) 和 \(m\) 的字串 \(a\) 和 \(b\),求即是 \(a\) 的子串行又是 \(b\) 的子串行的最大長度。
設 \(f_\) 表示 \(a_\) 與 \(b_\) 的 lcs 長度。當 \(a_i = b_j\),即它們是公共時,我們可以把它們作為乙個公共子串行,從而有 \(f_ = f_ + 1\);否則它們不公共,那麼「丟掉」\(a_i\) 或者 \(b_j\) 對 lcs 是沒有影響的,有 \(f_ = \max(f_, f_)\)。
綜上,得到狀態轉移方程
\[ f_ =\left\
& f_ + 1 \ (a_i = b_j)\\
& \max(f_, f_) \ \text
\end
\right.
\]**如下:
#include using namespace std;
const int n = 1005;
int n, m;
char a[n], b[n];
int dp[n][n];
int main()
經典DP學習總結(體驗)
貪心算法學完了,我們也開始了dp的學習,當然在我感覺現如今學習的兩種演算法都是幫助我們去解決問題的方法,就是直接性的公式套用的解題思路。就例如dp中的數塔問題,利用dp轉化成動態表達方程就很好的解決,不用再乙個個的情況列出來在排序這樣的解決問題。dp要實現的就是將複雜問題簡化,在我們不能直接將問題求...
經典DP問題
動態規劃5個經典問題解析 給定k個整數的序列,其任意連續子串行可表示為,其中 1 i j k。最大連續子串行是所有連續子序中元素和最大的乙個,例如給定序列,其最大連續子串行為,最大和為20。數塔問題 要求從頂層走到底層,若每一步只能走到相鄰的結點,則經過的結點的數字之和最大是多少?include s...
經典DP合集
1.數字三角形 數塔問題 dp入門題 有形如下圖所示的數塔,從頂部出發,在每一結點可以選擇向左走或是向右走,一起走到底層,要求找出一條路徑,使路徑上的值最大。樣例輸入 11 8 12 7 26 6 14 15 8 12 7 13 24 11 樣例輸出 86 13 8 26 15 24 狀態轉移方程 ...