劍指Offer——動態規劃演算法
劍指Offer——動態規劃演算法
什麼是動態規劃?
和分治法一樣,動態規劃(dynamic programming)是通過組合子問題而解決整個問題的解。
分治法是將問題劃分成一些獨立的子問題,遞迴地求解各子問題,然後合併子問題的解。
動態規劃適用於子問題不是獨立的情況,也就是各子問題包含公共的子子問題。
此時,分治法會做許多不必要的工作,即重複地求解公共的子問題。動態規劃演算法對每個子問題只求解一次,將其結果儲存起來,從而避免每次遇到各個子問題時重新計算答案。
適用範圍
最優性原理體現為問題的最優子結構特性。當一個問題的最優解中包含了子問題的最優解時,則稱該問題具有最優子結構特性。
最優性原理是動態規劃的基礎。任何一個問題,如果失去了這個最優性原理的支援,就不可能用動態規劃設計求解。
1.問題中的狀態滿足最優性原理。
2.問題中的狀態必須滿足無後效性。
所謂無後效性是指:“下一時刻的狀態只與當前狀態有關,而和當前狀態之前的狀態無關,當前狀態是對以往決策的總結”。
動態規劃演算法的設計
兩種方法:
自頂向下(又稱記憶化搜尋、備忘錄):基本上對應著遞迴函式實現,從大範圍開始計算,要注意不斷儲存中間結果,避免重複計算
自底向上(遞推):從小範圍遞推計算到大範圍
動態規劃的重點
遞迴方程+邊界條件
爬樓梯問題
一個人每次只能走一層樓梯或者兩層樓梯,問走到第80層樓梯一共有多少種方法。
遞推公式:DP[i]=DP[i-1]+DP[i-2]
邊界條件:DP[1]=1 DP[2]=2
(a)自頂向下的解法:
long long dp[81] = {0};/*用於儲存中間結果否則會重複計算很多重複的子問題*/
long long DP(int n)
{
if(dp[n])
return dp[n];
if(n == 1)
return 1;
if(n == 2)
return 2;
dp[n] = DP(n-1) + DP(n-2);
return dp[n];
}
(b)自底向上的解法:
int i;
long long dp[81]; /* 注意當n超過75時,結果值將超過int範圍 */
dp[1] = 1;
dp[2] = 2;
for(i=3; i <= 80; i++)
dp[i] = dp[i-1] + dp[i-2];
最長上升子序列
對於序列:4 1 2 24,它的最長上升子序列是1 2 4,長度為3。
對於序列:4 2 4 25 6,它的最長上升子序列是2 4 5 6,長度為4。
設a[i]表示原序列,設DP[i]表示以第i個數結尾的最長上升序列的長度,那麼很顯然想匯出DP[i]的值,需要在DP[k](1<=k<i)中找出滿足a[k]<a[i]最大的一項。假設第kk項是我們找到的答案,那麼第i個數就可以接在第kk個數之後,成為以第i個數結尾的最長升序列。如果沒有找到答案,換言之第i個數比前面的數都要小,那麼DP[i]=1,也即生成了從自己開始又以自己結尾的最長升序列。綜上,我們很容易得出:
遞推公式:DP[i]=max(DP[k]+1,DP[i]) 1<=k<i
邊界條件:DP[i]=1 1<=i<=n
演算法複雜度為O(n^2)
void RiseSequence(int Array[], int num)
{
#define MAX_LENGTH 30
struct
{
int SequenceValue; /* max length ending with this num */
int PreviousIndex; /* record the previous number */
}ArrayInfo[MAX_LENGTH], temp;
int i;
for(i = 0; i < num; i++)
{
int j;
ArrayInfo[i].SequenceValue = 1;
ArrayInfo[i].PreviousIndex = -1;
for(j = 0; j < i; j++)
{
if(Array[j] < Array[i] && (ArrayInfo[j].SequenceValue + 1 > ArrayInfo[i].SequenceValue))
{
ArrayInfo[i].SequenceValue = ArrayInfo[j].SequenceValue + 1;
ArrayInfo[i].PreviousIndex = j;
}
}
}
temp.SequenceValue = ArrayInfo[0].SequenceValue;
for(i = 1; i < num; i++)
{
if(temp.SequenceValue < ArrayInfo[i].SequenceValue)
{
temp.SequenceValue = ArrayInfo[i].SequenceValue;
temp.PreviousIndex = i;
}
}
for(i = 0; i < temp.SequenceValue; i++)
{
printf("%d ", Array[temp.PreviousIndex]); /* in reverse order */
temp.PreviousIndex = ArrayInfo[temp.PreviousIndex].PreviousIndex;
}
printf("\nthe max rising sequence length is %d\n", temp.SequenceValue);
}
最長公共子序列
給定兩個序列X和Y,稱序列Z是X和Y的公共子序列如果Z既是X的一個子序列,又是Y的一個子序列。例如,如果X={a,b,c,b,d,a,b} Y={b,d,c,a,b,a} 那麼序列{b,c,a}就是X和Y的一個公共子序列,但是它並不是X和Y的最長公共子序列,因為它的長度為3。而同為X和Y公共子序列的{b,c,b,a},長度為4,因為找不到長度為5或更大的公共子序列,所以X和Y的最長公共子序列長度就為4。
假設兩個序列陣列分別為a,b。定義f(i,j)為計算到a陣列第i個數、b陣列第j個數時所得到的最長公共子序列的長度。這時有兩種情況:
1.假如a[i]=b[j],那麼f(i,j)=f(i-1,j-1)+1
2.假如a[i]!=b[j],那麼f(i,j)=max(f(i-1,j),f(i,j-1))
邊界條件為:f(i,0)=0 1<=i<=len(a)
f(0,j)=0 1<=j<=len(b)
演算法複雜度:O(n^2),len(a)表示陣列a的長度。
尾聲
動態規劃絕對不是一兩篇文章可以講清楚的。當然也不是通過一兩道題目可以完全學會。學習的關鍵是用動規的思想去想問題,去設計狀態轉移方程式。
動態規劃還有很多變形,如狀態壓縮,樹形等等。
雖然通常我們用遞迴的方式分析動態規劃問題,但最終都會基於迴圈去編碼。
美文美圖
再分享一下我老師大神的人工智慧教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智慧的隊伍中來!https://www.cnblogs.com/captainbed