1、到底什麼才叫「最優子結構」,和動態規劃什麼關係。
2、為什麼動態規劃遍歷dp
陣列的方式五花八門,有的正著遍歷,有的倒著遍歷,有的斜著遍歷。
目錄「最優子結構」是某些問題的一種特定性質,並不是動態規劃問題專有的。也就是說,很多問題其實都具有最優子結構,只是其中大部分不具有重疊子問題,所以我們不把它們歸為動態規劃系列問題而已。
我先舉個很容易理解的例子:假設你們學校有 10 個班,你已經計算出了每個班的最高考試成績。那麼現在我要求你計算全校最高的成績,你會不會算?當然會,而且你不用重新遍歷全校學生的分數進行比較,而是只要在這 10 個最高成績中取最大的就是全校的最高成績。
我給你提出的這個問題就符合最優子結構:可以從子問題的最優結果推出更大規模問題的最優結果。讓你算每個班的最優成績就是子問題,你知道所有子問題的答案後,就可以藉此推出全校學生的最優成績這個規模更大的問題的答案。
你看,這麼簡單的問題都有最優子結構性質,只是因為顯然沒有重疊子問題,所以我們簡單地求最值肯定用不出動態規劃。
再舉個例子:假設你們學校有 10 個班,你已知每個班的最大分數差(最高分和最低分的差值)。那麼現在我讓你計算全校學生中的最大分數差,你會不會算?可以想辦法算,但是肯定不能通過已知的這 10 個班的最大分數差推到出來。因為這 10 個班的最大分數差不一定就包含全校學生的最大分數差,比如全校的最大分數差可能是 3 班的最高分和 6 班的最低分之差。
這次我給你提出的問題就不符合最優子結構,因為你沒辦通過每個班的最優值推出全校的最優值,沒辦法通過子問題的最優值推出規模更大的問題的最優值。前文「動態規劃詳解」說過,想滿足最優子結,子問題之間必須互相獨立。全校的最大分數差可能出現在兩個班之間,顯然子問題不獨立,所以這個問題本身不符合最優子結構。
那麼遇到這種最優子結構失效情況,怎麼辦?策略是:改造問題。對於最大分數差這個問題,我們不是沒辦法利用已知的每個班的分數差嗎,那我只能這樣寫一段暴力**:
int result = 0;
for (student a : school)
}return result;
改造問題,也就是把問題等價轉化:最大分數差,不就等價於最高分數和最低分數的差麼,那不就是要求最高和最低分數麼,不就是我們討論的第乙個問題麼,不就具有最優子結構了麼?那現在改變思路,借助最優子結構解決最值問題,再回過頭解決最大分數差問題,是不是就高效多了?
當然,上面這個例子太簡單了,不過請讀者回顧一下,我們做動態規劃問題,是不是一直在求各種最值,本質跟我們舉的例子沒啥區別,無非需要處理一下重疊子問題。
前文「不同定義不同解法」和「高樓扔雞蛋高階」就展示了如何改造問題,不同的最優子結構,可能導致不同的解法和效率。
再舉個常見但也十分簡單的例子,求一棵二叉樹的最大值,不難吧(簡單起見,假設節點中的值都是非負數):
int maxval(treenode root)
你看這個問題也符合最優子結構,以root
為根的樹的最大值,可以通過兩邊子樹(子問題)的最大值推導出來,結合剛才學校和班級的例子,很容易理解吧。
當然這也不是動態規劃問題,旨在說明,最優子結構並不是動態規劃獨有的一種性質,能求最值的問題大部分都具有這個性質;但反過來,最優子結構性質作為動態規劃問題的必要條件,一定是讓你求最值的,以後碰到那種噁心人的最值題,思路往動態規劃想就對了,這就是套路。
動態規劃不就是從最簡單的 base case 往後推導嗎,可以想象成乙個鏈式反應,以小博大。但只有符合最優子結構的問題,才有發生這種鏈式反應的性質。
找最優子結構的過程,其實就是證明狀態轉移方程正確性的過程,方程符合最優子結構就可以寫暴力解了,寫出暴力解就可以看出有沒有重疊子問題了,有則優化,無則 ok。這也是套路,經常刷題的朋友應該能體會。
這裡就不舉那些正宗動態規劃的例子了,讀者可以翻翻歷史文章,看看狀態轉移是如何遵循最優子結構的,這個話題就聊到這,下面再來看另外個動態規劃迷惑行為。
我相信讀者做動態規問題時,肯定會對dp
陣列的遍歷順序有些頭疼。我們拿二維dp
陣列來舉例,有時候我們是正向遍歷:
int dp = new int[m][n];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
// 計算 dp[i][j]
有時候我們反向遍歷:
for (int i = m - 1; i >= 0; i--)
for (int j = n - 1; j >= 0; j--)
// 計算 dp[i][j]
有時候可能會斜向遍歷:
// 斜著遍歷陣列
for (int l = 2; l <= n; l++)
}
甚至更讓人迷惑的是,有時候發現正向反向遍歷都可以得到正確答案,比如我們在「團滅**問題」中有的地方就正反皆可。
那麼,如果仔細觀察的話可以發現其中的原因的。你只要把住兩點就行了:
1、遍歷的過程中,所需的狀態必須是已經計算出來的。
2、遍歷的終點必須是儲存結果的那個位置。
下面來具體解釋上面兩個原則是什麼意思。
比如編輯距離這個經典的問題,詳解見前文「編輯距離詳解」,我們通過對dp
陣列的定義,確定了 base case 是dp[..][0]
和dp[0][..]
,最終答案是dp[m][n]
;而且我們通過狀態轉移方程知道dp[i][j]
需要從dp[i-1][j]
,dp[i][j-1]
,dp[i-1][j-1]
轉移而來,如下圖:
// 通過 dp[i-1][j], dp[i][j - 1], dp[i-1][j-1]
// 計算 dp[i][j]
因為,這樣每一步迭代的左邊、上邊、左上邊的位置都是 base case 或者之前計算過的,而且最終結束在我們想要的答案dp[m][n]
。
再舉一例,回文子串行問題,詳見前文「子串行問題模板」,我們通過對dp
陣列的定義,確定了 base case 處在中間的對角線,dp[i][j]
需要從dp[i+1][j]
,dp[i][j-1]
,dp[i+1][j-1]
轉移而來,想要求的最終答案是dp[0][n-1]
,如下圖:
這種情況根據剛才的兩個原則,就可以有兩種正確的遍歷方式:
要麼從左至右斜著遍歷,要麼從下向上從左到右遍歷,這樣才能保證每次dp[i][j]
的左邊、下邊、左下邊已經計算完畢,得到正確結果。
現在,你應該理解了這兩個原則,主要就是看 base case 和最終結果的儲存位置,保證遍歷過程中使用的資料都是計算完畢的就行,有時候確實存在多種方法可以得到正確答案,可根據個人口味自行選擇。
第乙個問題,找最優子結構的過程,其實就是證明狀態轉移方程正確性的過程,方程符合最優子結構就可以寫暴力解了,寫出暴力解就可以看出有沒有重疊子問題了,有則優化,無則 ok。
。。沒看懂第二個問題,dp陣列遍歷方向問題
動態規劃學習篇
最近在看牛客網的校招題目,發現很多公司的程式設計題都考了動態規劃裡面的知識,所以,專門抽個時間來學習一下。首先,要用動態規劃演算法,得滿足以下幾個條件 1.最優化原理 最優子結構性質 最優化原理可這樣闡述 乙個最優化策略具有這樣的性質,不論過去狀態和決策如何,對前面的決策所形成的狀態而言,餘下的諸決...
動態規劃總結篇
對於同一型別問題的總結 leetcode 516.最長回文子串行 給定乙個字串s,找到其中最長的回文子串行。可以假設s的最大長度為1000。示例 1 輸入 bbbab 輸出 乙個可能的最長回文子串行為 bbbb 示例 2 輸入 cbbd 輸出 乙個可能的最長回文子串行為 bb class solut...
軟體測試 答疑篇
答疑篇 一 軟體測試的概念 二 軟體測試與研發的區別 1 測試與除錯的區別 2 難易程度 3 薪水 4 發展前景 5 技術要求 6 工作環境 三 軟體測試的職業規劃 四 乙個優秀的測試人員所具備的素質 驗證軟體功能是否滿足使用者的需求 測試的任務是發現程式中的缺陷 除錯的任務是定位並且解決程式中的問...