前言
解決問題的一個重要思想就是歸納,即減小問題的規模。如果小規模在一定範圍內可解,則求解返回,若還是太大,繼續往小分解。在返回大問題的過程中,有個弊端,多個大問題與同一個小問題有聯繫,這個小問題反覆求解。解決辦法就是動態規劃,存下這個小問題的解,不就省事多了。
動態規劃
維基百科說得好,動態規劃就是通過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。
說白了就是:
1. 這個問題可以分解爲子問題
2. 原問題與子問題有關係
3. 子問題可以求解並記錄
說到這裏,你是不是想到了遞歸,想到了數學歸納法,其實他們都是相通的。(哇哦,瞬間覺得學科之間原來真的是有聯繫的)
看到這裏,你是不是覺得動態規劃挺簡單的嘛,就是三個步驟嘛:
1. 把原問題分解爲子問題
2. 找到原問題與子問題的關係
3. 記錄下子問題的解
emmm,就是這三個簡單的步驟,難倒了多少好漢啊(也包括我),下面我們就通過分析一些使用動態規劃解決的問題來摸索這三步怎麼找。
動態規劃的常見應用:
1. 斐波那契數列
1 1 2 3 5 8 13。。。
我們要求第n個數,即f(n)
a. 分解問題:求f(n)就是求f(n - 1) 與 f(n - 2)
b. 找關係:f(n) = f(n - 1) + f(n - 2)
c.找個數組把f(n)存起來,這樣調用時就不用再次計算了
這樣,在f(1) 與 f(2) 已知的情況下,我們便可以求得f(n)。
看到這裏,你可能會說這不是明顯的遞歸嗎,怎麼是動態規劃呢?這裏便要提出動態規劃與遞歸的不同,若用遞歸,求f(100)就要求f(99) 與 f(98), 求f(99)就要求f(98) 與f(97),此時f(98)算了兩遍,而使用動態規劃,它存儲了f(98),後面用的時候便不需要再重新計算,能夠省下來許多時間,這便是傳說中動態規劃記憶化存儲的性質。
2. 鋼條切割問題
設f(n)爲長度爲n的鋼條切割後得到的最大價值
a.分解問題:
求f(n)即需要知道f(n - 1),f(n - 2), ....及p(n), p(n -1), p(n -2)....
b.找關係:
f(n) = max(p(n), f(1) + f(n -1), f(2) + f(n -2), ......., f(n/2) + f(n/2) )
c.存儲f(n),節省時間
這樣便可找到最值。
int cut(int []p) { int *r=new int[p.length+1]; r[1] = p[1]; for(int i = 1; i <= p.length; i++) { int q = p[i]; for(int j=1;j<=i;j++){ q = max(q, r[j] + r[i-j]); } r[i]=q; } return r[p.length]; }
3. 小朋友過河問題
在一個夜黑風高的晚上,有n(n <= 50)個小朋友在橋的這邊,現在他們需要過橋,但是由於橋很窄,每次只允許不大於兩人通過,他們只有一個手電筒,所以每次過橋的兩個人需要把手電筒帶回來,i號小朋友過橋的時間爲T[i],兩個人過橋的總時間爲二者中時間長者。問所有小朋友過橋的總時間最短是多少。
輸入:
兩行數據:第一行爲小朋友個數n
第二行有n個數,用空格隔開,分別是每個小朋友過橋的時間。
輸出:
一行數據:所有小朋友過橋花費的最少時間。
a. 分解問題
i個人過河可看作是前i-1個人過河的情況再多一個人,或是前i-2個人再做兩個人情況,這兩種的時間不一樣,需要分開討論。
b. 找關係
我們先將所有人按花費時間遞增進行排序,假設前i個人過河花費的最少時間爲opt[i]。
(1)考慮前i-1個人過河的情況,即河這邊還有1個人,河那邊有i-1個人,並且這時候手電筒肯定在對岸,所以opt[i] = opt[i-1] + a[1] + a[i] (讓花費時間最少的人把手電筒送過來,然後和第i個人一起過河)
(2)如果河這邊還有兩個人,一個是第i號,另外一個無所謂,河那邊有i-2個人,並且手電筒肯定在對岸,所以opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2] (讓花費時間最少的人把電筒送過來,然後第i個人和另外一個人一起過河,由於花費時間最少的人在這邊,所以下一次送手電筒過來的一定是花費次少的,送過來後花費最少的和花費次少的一起過河,解決問題)
所以 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }。
c.記錄每一個opt[n], 節省時間。
#include<iostream> #include<vector> #include<algorithm> using namespace std; int main() { int childNum, i; cin >> childNum; int *child = new int[childNum]; for (i = 0;i < childNum; i++) { cin >> child[i]; } sort(child, child + childNum); vector<int> vect(childNum); vect[0] = 0; vect[1] = child[1]; for (i = 2; i < childNum; i++) { vect[i] = min(vect[i-1] + child[0] + child[i],vect[i-2] + child[0] + child[i]+2 * child[1]); } cout << vect[childNum - 1]; return 0; }
4. 求T和P的最長公共子序列(LCS)
定義C[i][j]表示X[i]與Y[j]的最大子序列的長度
其中X[i]表示A序列前i個
其中Y[j]表示B序列前j個
Algorithm LCS Input: A[N], B[N] Output: length begin int c[i][j] for(i = 0; i <= N; i++){ c[i][0] = 0; c[0][i] = 0; } for(i = 1; i <= N; i++){ for(j = 1; j <= N; j++){ if(A[i] == B[j]){ c[i][j] = c[i - 1][j - 1]; } else{ c[i][j] = max{c[i - 1][j], c[i][j - 1]}; } } } end
5. 計算最短公共超序列SCS
最短公共超序列(SCS)S定義爲所有以T和P爲子序列的序列中最短的一個。
定義a[i][j]表示s1[i]與s2[j]的最大子序列的長度
其中s1[i]表示A序列前i個
其中s2[j]表示B序列前j個
Algorithm SCS Input: A[N], B[N] Output: length begin int c[i][j] for(i = 0; i <= N; i++){ c[i][0] = 0; c[0][i] = 0; } for(i = 1; i <= N; i++){ for(j = 1; j <= N; j++){ int case1, case2, case3; case1 = c[i - 1][j] + 1; case2 = c[i][j - 1] + 1; if(A[i] == B[j]){ case3= c[i - 1][j - 1] + 1; } else{ case3 = c[i - 1][j - 1] + 2; } c[i][j] = min(case1, case2, case3); } } return c[m][n]; end
總結
動態規劃算法在歸納的基礎上對數值小問題的解進行存儲,這樣可以在重複使用的時候減小開銷,一般動態規劃在解決日常問題時很常見。