1. 程式人生 > >動態規劃演算法筆記整理

動態規劃演算法筆記整理

動態規劃:

  • 將一個複雜的問題分解成若干個子問題,通過綜合子問題的最優解來得到原問題的最優解
  • 動態規劃會將每個求解過的子問題的解記錄下來,這樣下一次碰到同樣的子問題時,就可以直接使用之前記錄的結果,而不是重複計算
  • 可以用遞迴或者遞推的寫法實現,遞迴的寫法又叫記憶化搜尋
  • 重疊子問題:如果一個問題可以被分解成若干個子問題,且這些子問題會重複出現,就稱這個問題擁有重疊子問題。 一個問題必須擁有重疊子問題,才能用動態規劃去解決。
  • 最優子結構:如果一個問題的最優解可以由其子問題的最優解有效地構造出來,那麼稱為這個問題擁有的最有子結構。最優子結構保證了動態規劃中的原問題的最優解可以由子問題的最優解推導而來
  • 動態規劃與分治的區別:都是分解為子問題然後合併子問題得到解,但是分治分解出的子問題是不重疊
  • 動態規劃與貪心的區別:都有最優子結構,但是貪心直接選擇一個子問題去求解,會拋棄一些子問題,這種選擇的正確性需要用歸納法證明,而動態規劃會考慮所有的子問題
  • DP:考慮且僅僅考慮由前一階段狀態轉移到當前狀態後,遞推並選取出當前狀態的最優解,具有無後效性和最優子結構的基本特徵,其中所謂的無後效性是指:“下一時刻的狀態只與當前狀態有關,而和當前狀態之前的狀態無關,當前的狀態是對以往決策的總結”。因此DP是由前一階段的一系列階段轉移並選取最優而來,即抵達當前狀態路徑不唯一,僅是最終最優結果唯一。
  • 貪心
    :對於尚未加入解集的元素按固定策略(最大或最小)選取狀態轉移,妄圖從區域性最優達到整體最優,也具有最優子結構性質,但轉移路徑單一,一旦確定貪心策略後就是一條路走到底,沒有多餘的狀態供給選擇。顯然,最終結果也必定唯一,具有後效性,即當前最優狀態與之前的選取路徑中全部節點都有關。
  •  相對DP來講,貪心演算法效率要高,但是有些問題用貪心往往無法求解,因為一些限制(如0-1揹包中的揹包不一定要裝滿)貪心無法在滿足的條件下且同時達到最優,這樣一來就只能使用DP列舉全部可轉移的狀態,從而遞推出最優解。個人覺得,若限制條件較多,應該偏向於DP,若限制條件較少(一般為一個)則可以考慮貪心演算法。

動態規劃的遞迴和遞推寫法

  • 遞迴寫法
//不使用動態規劃
int F(int n) {
  if(n == 0 || n == 1) return 1;
  else return F(n - 1) + F(n - 2);
}
// 此時F(5) = F(4) + F(3), F(4) = F(3) + F(2),3會被計算兩次
// 採用動態規劃的方法(記憶化搜尋)
int dp[10000];
memeset(dp, -1, sizeof(dp));
int F(int n) {
  if(n == 0 || n == 1) return 1;
  if(dp[n] != -1) return dp[n];
  else {
    dp[n] = F(n-1) + F(n - 2);
    return dp[n];
  }
}
  • 遞推寫法
// 數塔為例
// 遞推方程:f[i][j] += max(f[i+1][j], f[i+1][j+1])
// 如果非要建立dp陣列,先要初始化dp[n][j] = f[n][j]   [(j從1~n)]
// 然後dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + f[i][j]
// f[i][j]為第i行j列數字的大小
// 採用自底向上遞推的方法
// 陣列從1開始作為下標儲存
for(int i = n - 1; i >= 1; i--) {
  for(int j = 1; j <= i; j++)
    f[i][j] += max(f[i+1][j], f[i+1][j+1]);
}
return f[1][1];

最大連續子序列和

  • 給定序列,求連續的子序列要求和最大,求最大的和為多少
  • dp[i]表示以a[i]作為末尾的連續序列的最大和(a[i]必須是末尾被選的數啊啊),dp陣列中所有的資料的最大值就是所求
  • 因為a[i]一定是所選序列的末尾,所以分為兩種情況:
    • a[i]開始,a[i]結束
    • 某數開始,到a[i]結束(最大和是dp[i-1] + a[i])
  • 所以遞推方程為dp[i] = max(a[i], dp[i-1]+a[i])
// a陣列從下標0開始
dp[0] = a[0];
for(int i = 1; i < n; i++)
  dp[i] = max(a[i], dp[i-1]+a[i]);
int maxn = dp[0];
for(int i = 1; i < n; i++)
  maxn = max(dp[i], maxn);
printf("%d", maxn);

最長不下降子序列(LIS)

  • 求一個序列的最長的子序列(可以不連續),使得這個子序列是不下降的
  • dp[i]表示必須以a[i]結尾的最長不下降子序列的長度
  • dp[i] = max{1, dp[j] + 1}; // j從1 ~ i-1 且必須滿足a[j] <= a[i]
int ans = 0;
for(int i = 0; i < n; i++) {
  for(int j = 1; j < i; j++) {
    if(a[i] >= a[j])
      dp[i] = max(1, dp[j] + 1);
  }
  ans = max(dp[i], ans);
}
printf("%d", ans);

最長公共子序列(LCS)

  • 給定兩個字串或者數字序列A和B,求一個字串,使得這個字串是A和B的最長公共部分(子序列可以不連續)
  • dp[i][j]表示A的第i位之前和B的第i位之前的這兩個序列的LCS最長公共子序列的長度(下標從1開始)
  • 那麼dp[lena][lenb]即為所求
  • 遞推方程:
    • 當a[i] == b[j] : dp[i][j] = dp[i-1][j-1] + 1
    • 當a[i] != b[j] : dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    • 邊界:dp[i][0] = dp[0][j] = 0(0 <= i <= n, 1 <= j <= m)
char a[100], b[100];
scanf("%s", a+1);
scanf("%s", b+1);
int lena = strlen(a + 1);
int lenb = strlen(b + 1);
for(int i = 0; i <= lena; i++)
  dp[i][0] = 0;
for(int j = 0; j <= lenb; j++)
  dp[0][j] = 0;
for(int i =1; i <= lena; i++) {
  for(int j - 1; j <= lenb; j++) {
    if(a[i] == b[j])
      dp[i][j] = dp[i-1][j-1] + 1;
    else
      dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
  }
}
printf("%d", dp[lena][lenb]);

最長迴文子串

  • 給出一個字串s,求s的最長迴文字串的長度
  • dp[i][j]表示s[i]到s[j]所表示的字串是否是迴文字串。只有0和1
  • 遞推方程:
    • 當s[i] == s[j] : dp[i][j] = dp[i+1][j-1]
    • 當s[i] != s[j] : dp[i][j] =0
    • 邊界:dp[i][j] = 1, dp[i][i+1] = (s[i] == s[i+1]) ? 1 : 0
  • 因為i、j如果從小到大的順序來列舉的話,無法保證更新dp[i][j]的時候dp[i+1][j-1]已經被計算過。因此不妨考慮按照字串的長度和子串的初試位置進行列舉,即第一遍將長度為3的子串的dp的值全部求出,第二遍通過第一遍結果計算出長度為4的子串的dp的值…這樣就可以避免狀態無法轉移的問題
int len = s.length();
//先把1和2長度的都初始化了
int ans = 1;
for(int i = 0; i < len; i++) {
  dp[i][i] = 1;
  if(i < len - 1 && s[i] == s[i+1]) {
    dp[i][i+1] = 1;
    ans = 2;
  }
}
//狀態轉移方程
for(int L = 3; L <= len; L++) {
  for(int i = 0; i + L - 1 < len; i++) {
    int j = i + L - 1;
    if(s[i] == s[j] && dp[i+1][j-1] == 1) {
      dp[i][j] = 1;
      ans = L;
    }
  }
}
printf("%d", ans);

DAG最長路

揹包問題

  • 多階段動態規劃問題:有一類動態規劃可解的問題,它可以描述成若干個有序的階段,且每個階段的狀態有關,一般把這類問題稱為多階段動態規劃問題

01揹包問題

  • 有n件物品,每件物品的重量為w[i],價值為c[i]。現有一個重量為V的揹包,問如何選取物品放入揹包,使得揹包內物品的總價值最大。其中每種物品只有1件
  • dp[i][j]表示前i件物品恰好裝入容量為j的揹包所能獲得的最大價值
    • 不放第i件物品,則dp[i][j] = dp[i-1][j]
    • 放第i件物品,那麼問題轉化為前i – 1件物品恰好裝入容量j – w[i]的揹包中所能獲得的最大價值dp[i-1][j-w[i]] + c[i]
  • 遞推方程dp[i][j] = max{ dp[i-1][j], dp[i-1][j-w[i]]+c[i] }
for(int i = 1; i <= n; i++) {
  for(int j = 1, j <= v; j++)
    if(j < w[i])
      dp[i][j] = dp[i-1][j];
   else
      dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + c[i]);
}
  • 一維:
for(int i = 1; i <= n; i++) {
  for(int j = v; j >= w[i]; j--)
    dp[v] = max(dp[v], dp[v[w[i]]] + c[i]);
}

完全揹包問題

  • 有n種物品,每種物品的單件重量為w[i],價值為c[i]。現有一個容量為v的揹包,問如何選取物品放入揹包,使得揹包內物品的總價值最大。其中每種物品有無窮件
  • 遞推方程:dp[i][j] = max(dp[i-1][v], dp[i][j-w[i]] + c[i])
  • 和01揹包不同,這裡的j的列舉順序為正向列舉,而且是dp[i][j-w[i]]
for(int i =1; i <= n; i++) {
  for(int j = w[i]; j <= v; j++)
   dp[j] = max(dp[i], dp[j-w[j]]+ c[i]);
}