天天看點

資料結構與算法(十二)——算法-動态規劃

一、青蛙跳台階&斐波那契數列

1、問題

  一隻青蛙跳台階,每次可以跳 1 層或 2 層。青蛙跳到 n 層一共有多少種跳法?

2、思想

  先把問題規模縮小,考慮 n = 1時,n = 2的解。那麼,顯然有:

  (1)邊界條件:dp[1] = 1、dp[2] = 2

  (2)再考慮 n = 3時,逆向思維一下,要跳 3 層,是不是隻能是從第 2 階跳 1 層到或者是從第 1 階跳 2 層到。是以dp[3] = dp[2] + dp[1]。

  (3)同理n = 4時,是不是也是隻能是從第 3 階跳 1 層到或者是從第 3 階跳 2 層到。是以dp[4] = dp[3] + dp[2]。

  (3)……

  (4)同理可得,狀态轉移方程:dp[n] = dp[n - 1] + dp[n - 2]。

資料結構與算法(十二)——算法-動态規劃

3、代碼示例

1 // 遞歸.時間複雜度:O(2^n).空間複雜度:O(1)
 2 public int jumpFloor(int target) {
 3     if (target == 1) {
 4         return 1;
 5     }
 6     if (target == 2) {
 7         return 2;
 8     }
 9 
10     return jumpFloor(target - 1) + jumpFloor(target - 2);
11 }
12 
13 // 循環(dp數組).時間複雜度:O(n).空間複雜度:O(n)
14 public int jumpFloor2(int target) {
15     if (target == 1) {
16         return 1;
17     }
18 
19     int[] dp = new int[target + 1];
20     dp[1] = 1;
21     dp[2] = 2;
22     for (int i = 3; i <= target; i++) {
23         dp[i] = dp[i - 1] + dp[i - 2];
24     }
25     return dp[target];
26 }
27 
28 // 滾動數組.時間複雜度:O(n).空間複雜度:O(1)
29 public int jumpFloor3(int target) {
30     if (target == 1) {
31         return 1;
32     }
33     int num1 = 1, num2 = 2, temp = num2;
34 
35     for (int i = 3; i <= target; i++) {
36         temp = num1 + num2;
37         num1 = num2;
38         num2 = temp;
39     }
40 
41     return temp;
42 }      

二、網格路徑

1、問題

  有一個 m*n 的網格,從 [1, 1] 出發,每次隻能向右或向下,抵達 [m, n] 一共有多少種方案?如圖:

資料結構與算法(十二)——算法-動态規劃

2、思想

  先把問題規模縮小,考慮從 [1, 1]到 [1, 2],到 [1, 3],到 [1, 4]……和從 [1, 1]到[2, 1],到 [3, 1]……,是不是隻能一直向右和向下,那麼,顯然有:

  (1)邊界條件:dp[1][n]  = 1、dp[m][1] = 1

  (2)再考慮到 [2, 2] ,是不是隻有 [1, 2] 向下或者 [2, 1] 向右。是以dp[2][2] = dp[1][2] + dp[2][1]。

  (3)同理可得,狀态轉移方程:dp[m][n] = dp[m - 1][n] + dp[m][n - 1]。

3、代碼示例

1 // 3*6. m=3,n=6
 2 public static int gridPath(int m, int n) {
 3     int[][] dp = new int[m + 1][n + 1];
 4     for (int i = 1; i <= n; i++) {
 5         dp[1][i] = 1;
 6     }
 7 
 8     for (int i = 1; i <= m; i++) {
 9         dp[i][1] = 1;
10     }
11 
12     // 周遊二維數組
13     for (int i = 2; i <= m; i++) {
14         for (int j = 2; j <= n; j++) {
15             dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
16         }
17     }
18 
19     return dp[m][n];
20 }      

三、最長遞增子序列

1、問題

  問題很好了解。例如:[3,6,2,7] 是數組 [0,3,1,6,2,2,7] 的子序列,但它不是遞增的。

  nums = [10,9,2,5,3,7,101,18] --> 4 [2,3,7,101] 或者 [2,3,7,18]

  nums = [0,1,0,3,2,3] --> 4 [0,1,2,3]

  nums = [7,7,7,7,7,7,7] --> 1 [7]

2、思想

  定義:dp[i] 表示以 nums[i] 這個數結尾的最長遞增子序列的長度。為了了解這個定義,比如:dp[3] 就是以 nums[3] = 4 結尾的最長遞增子序列(1,3,4)的長度,也就是3。

  根據這個定義,最終結果(子序列的最大長度)應該是 dp 數組中的最大值。

資料結構與算法(十二)——算法-動态規劃
資料結構與算法(十二)——算法-動态規劃

  過程動圖

資料結構與算法(十二)——算法-動态規劃

  那麼,顯然有:

  (1)邊界條件:dp[*] = 1

  (2)動态轉移方程:dp[i] = nums[i] > nums[j] && max(dp[i], dp[j] + 1)

3、代碼示例

1 // 時間複雜度 O(N^2)
 2 public int lis(int[] nums) {
 3     int[] dp = new int[nums.length];
 4     // dp數組全部初始化為 1
 5     Arrays.fill(dp, 1);
 6 
 7     for (int i = 0; i < nums.length; i++) {
 8         for (int j = 0; j < i; j++) {
 9             if (nums[i] > nums[j]) {
10                 dp[i] = Math.max(dp[i], dp[j] + 1);
11             }
12         }
13     }
14 
15     int res = 0;
16     for (int i : dp) {
17         res = Math.max(res, i);
18     }
19 
20     return res;
21 }      

四、最長回文子串

1、問題

  給定一個字元串 s,長度為 n,找到 s 中最長的回文子串。

  s = "abbac" --> "abba"

  s = "cbbd" --> "bb"

  s = "babad" --> "aba"

2、思想

  定義:dp[i][j]表示 s 中 i 到 j 的字元串是否是回文串。那麼,顯然有:

  (1)邊界條件:dp[i][i] = true;dp[i][j] = true 當 s[i] = s[j] && j - i = 1

  (2)狀态轉移方程:dp[i][j] = ( s[i] = s[j] && dp[i + 1][j - 1] )

3、代碼示例

1 public int getLongestPalindrome(String s, int n) {
 2     boolean[][] dp = new boolean[n][n];
 3     int max = 1;
 4 
 5     // 字元串首尾字母長度差 (len = j - i) 0~n-1
 6     for (int len = 0; len < n; len++) {
 7 
 8         for (int i = 0; i < n - len; i++) {
 9             int j = i + len;
10 
11             // 如果字元串 i 到 j 的首尾相等,再根據字元串 i+1 到 j-1 來确定,即得到遞推公式
12             if (s.charAt(i) == s.charAt(j)) {
13                 // 邊界條件
14                 if (len == 0 || len == 1) {
15                     dp[i][j] = true;
16                 } else {
17                     // 狀态轉移方程
18                     dp[i][j] = dp[i + 1][j - 1];
19                 }
20 
21                 if (dp[i][j]) {
22                     // 更新最大長度
23                     max = Math.max(max, len + 1);
24                 }
25             }
26         }
27     }
28     return max;
29 }      

五、兌換硬币

1、問題

  給定一個正整數數組 coins,表示不同面值的硬币,整數 amount 代表總金額。求可以湊成總金額所需的最少硬币個數,不能組成總金額,傳回 -1。硬币數量無限。

  coins = [1,3,5], amout = 11 -> 3(11 = 5 + 5 + 1)

  coins = [2,5], amout = 3 -> -1

2、思想

  用dp[i] = j 表示湊夠 i 元最少需要 j 個硬币。先把問題規模縮小,考慮,如果有面值為1元、3元和5元的硬币若幹枚,如何用最少的硬币湊夠11元?答案很簡單是 3(5+5+1)。則:

  i = 0,表示湊夠0元最少需要的硬币數。當然就是不選硬币,需要0個硬币。

  dp[0] = 0

  i = 1,隻有1元可用,選擇 1 元硬币,金額變為 i - 1 = 0,接下來隻需要湊夠 0 元即可。

  dp[1] = dp[i - 1] + 1 = dp[0] + 1 = 0 + 1 = 1

  i = 2,隻有1元可用,選擇 1 元硬币,金額變為 i - 1 = 1,接下來隻需要湊夠 1 元即可。

  dp[2] = dp[i - 1] + 1 = dp[1] + 1 = 1 + 1 = 2

  i = 3,可選硬币有 1 和 3,則分兩種情況

  ①選擇 1 元硬币,金額變為 i - 1 = 2,接下來隻需要湊夠 2 元即可。

  dp[3] = dp[i - 1] + 1 = dp[2] + 1 = 3 (3個1元的)

  ②選擇 3 元硬币,金額變為 i - 3 = 0,接下來隻需要湊夠 0 元即可。

  dp[3] = dp[i - 3] + 1 = dp[0] + 1 = 1 (1個3元的)

  顯然,上述情況取小:

  dp[3] = min{dp[i - 1] + 1, dp[i - 3] + 1} = 1

  i = 4~amount,同理可得。

  歸納一下:在已知dp[0]、dp[1]、dp[2]、……dp[i - 1]求 dp[i] 時,在可取的硬币面值(i >= coins[j],coins[j]表示第 j 個硬币的面值),都取一次,則問題就可降級為 dp[i - coins[j]],而這個值是已知,最後取小即可。

  案例說明:

資料結構與算法(十二)——算法-動态規劃

  不妨:coins = [1,3,5,20],求dp[11].

  目的是湊成11,那麼,從可選的硬币裡選擇一枚,所有情況如下:

  選擇coins[0]=1,則還需要湊11-1=10,即,在arr中湊10,是以dp[11]=dp[10] + 1。

  選擇coins[1]=3,則還需要湊11-3=8,即,在arr中湊8,是以dp[11]=dp[11-3] + 1。

  選擇coins[2]=5,則還需要湊11-5=6,即,在arr中湊6,是以dp[11]=dp[11-5] + 1。

  注意:coins[3]=20 > 11,是不可選的。是以可選條件是 i >= coins[j]。

  最後,在上述情況取小即可。

  那麼,有:

  (1)邊界條件:dp[0] = 0

  (2)狀态轉移方程:dp[i] = min(dp[i], dp[i - coins[j]] + 1)。i - coins[j] >= 0

3、代碼示例

1 // 兌換硬币
 2 public int minMoney(int[] coins, int amount) {
 3     // 數組下标從 0 開始
 4     int[] dp = new int[amount + 1];
 5 
 6     // 若最小硬币是 1,那麼兌換 amount 最多用 amount 個硬币。則 dp[amount] 最大等于 amount
 7     // 由于下面要取小,是以給一個越界值
 8     Arrays.fill(dp, amount + 1);
 9 
10     // 邊界條件
11     dp[0] = 0;
12     // 依次求dp[1]、dp[2]……dp[amount]
13     for (int i = 1; i < amount + 1; i++) {
14 
15         // 周遊硬币的面值
16         for (int coin : coins) {
17             // 該硬币面值可取
18             if (i - coin >= 0) {
19                 dp[i] = Math.min(dp[i], dp[i - coin] + 1);
20             }
21         }
22     }
23 
24     // 要是流程走下來 dp 值是非法值.說明換不出來
25     return dp[amount] == amount + 1 ? -1 : dp[amount];
26 }      

  注:貪心不正确

  coins = [1,4,5,7], amout=17

  貪心結果:7+7+1+1+1 -> 5

  實際:7+5+4+1 ->4

六、0-1背包問題

1、問題

  一共有N件物品,第i(i從1開始)件物品的重量為w[i],價值為v[i]。在總重量不超過背包容量C的情況下,能夠裝入背包的最大價值是多少?

  通俗的說,就是你有一個麻袋,你去超市順便拿東西,在不超出麻袋容量的情況下,盡可能的拿的物品價值和最大。

  要求:①裝入背包的總價值最大,且重量不超出。②物品要麼拿,要麼不拿,不可重複拿。

2、思想

  舉一個案例,有一個背包,容量為4kg,現有物品:

物品 重量 價值
吉他(G) 1kg 1500
音響(S) 4kg 3000
電腦(L) 3kg 2000

  顯然:可取方案有G+L,價值3500;或S,價值3000。最佳方案:G+L,3500。

  定義:dp[i][j]表示在前 i 個物品中選擇,能夠裝入容量為 j 的背包中的最大價值。即:

物品\重量 0kg 1kg 2kg 3kg 4kg
吉他(G)
音響(S)
電腦(L)

  先把問題規模縮小,按一行一行(必須),背包容量為0、1、2、……C=4的順序,填寫上表。

  表示物品隻有吉他,有:

物品\重量 0kg 1kg 2kg 3kg 4kg
吉他(G) 1500 1500 1500 1500
音響(S)
電腦(L)

  表示物品有吉他、音響,有:

物品\重量 0kg 1kg 2kg 3kg 4kg
吉他(G) 1500 1500 1500 1500
音響(S) 1500 1500 1500 3000
電腦(L)

  表示物品有吉他、音響、電腦,有:

物品\重量 0kg 1kg 2kg 3kg 4kg
吉他(G) 1500 1500 1500 1500
音響(S) 1500 1500 1500 3000
電腦(L) 1500 1500 2000 3500

  那麼,有:

  (1)邊界條件:dp[i][0] = dp[0][j] = 0。上表格第一列、第一行為0

  (2)狀态轉移方程:dp[i][j] = max(dp[i - 1][j], v[i] + dp[i - 1][j - w[i]])

3、代碼示例

資料結構與算法(十二)——算法-動态規劃
資料結構與算法(十二)——算法-動态規劃
1 public class Knapsack {
 2     private Goods[] goods;
 3     // 背包的容量
 4     private int c;
 5     // dp[i][j]:表示在前i個物品中選擇,能夠裝入容量為j的背包中的最大價值
 6     private int[][] dp;
 7     private int[][] path;
 8 
 9     public Knapsack(Goods[] goods, int c) {
10         if (goods == null || goods.length == 0 || c <= 0) {
11             return;
12         }
13         this.goods = goods;
14         this.c = c;
15 
16         this.dp = new int[goods.length][c + 1];
17         this.path = new int[goods.length][c + 1];
18 
19         // 初始化第一列、第一行為0.可不寫,預設就是0
20 //        for (int i = 0; i < dp.length; i++) {
21 //            dp[i][0] = 0;
22 //        }
23 //        for (int i = 0; i < dp[0].length; i++) {
24 //            dp[0][i] = 0;
25 //        }
26     }
27 
28     // 動态規劃
29     public void dp() {
30         for (int i = 1; i < dp.length; i++) {
31             for (int j = 1; j < dp[0].length; j++) {
32                 // 表明目前物品放不了背包
33                 if (goods[i].weight > j) {
34                     dp[i][j] = dp[i - 1][j];
35                 } else {
36                     dp[i][j] = Math.max(dp[i - 1][j], goods[i].value + dp[i - 1][j - goods[i].weight]);
37 
38                     // 用于記錄物品選擇政策
39                     if (dp[i - 1][j] < goods[i].value + dp[i - 1][j - goods[i].weight]) {
40                         path[i][j] = 1;
41                     }
42                 }
43             }
44         }
45 
46     }
47 
48     // 擷取物品的選擇政策
49     public List<Goods> getPlan() {
50         // 1.檢視 dp 填寫情況
51         for (int i = 0; i < dp.length; i++) {
52             for (int j = 0; j < dp[i].length; j++) {
53                 System.out.print(dp[i][j] + " ");
54             }
55             System.out.println();
56         }
57 
58         // 2.物品選擇政策
59         List<Goods> result = new ArrayList<>();
60         int i = path.length - 1;
61         int j = path[0].length - 1;
62         // 從path的最後一個開始找
63         while (i > 0 && j > 0) {
64             if (path[i][j] == 1) {
65                 result.add(goods[i]);
66 
67                 j = j - goods[i].weight;
68             }
69             i--;
70         }
71         return result;
72     }
73 }
74 
75 // 物品
76 class Goods {
77     public String name;
78     // 物品重量
79     public int weight;
80     // 物品價值
81     public int value;
82 
83     public Goods(String name, int weight, int value) {
84         this.name = name;
85         this.weight = weight;
86         this.value = value;
87     }
88 
89     @Override
90     public String toString() {
91         return "Goods{" +
92                 "name='" + name + '\'' +
93                 ", weight=" + weight +
94                 ", value=" + value +
95                 '}';
96     }
97 }      

0-1背包

資料結構與算法(十二)——算法-動态規劃
資料結構與算法(十二)——算法-動态規劃
1 public class Main {
 2     public static void main(String[] args) {
 3         Goods[] goods = new Goods[4];
 4 
 5         // 為了使得 goods[1] 是第1個物品
 6         goods[0] = null;
 7         goods[1] = new Goods("吉他", 1, 1500);
 8         goods[2] = new Goods("音響", 4, 3000);
 9         goods[3] = new Goods("電腦", 3, 2000);
10 
11         // 背包容量
12         final int C = 4;
13         Knapsack knapsack = new Knapsack(goods, C);
14         System.out.println("動态規劃dp:");
15         knapsack.dp();
16 
17         final List<Goods> plan = knapsack.getPlan();
18         System.out.println("物品選擇政策:");
19         System.out.println(plan);
20     }
21 }
22 
23 // 結果
24 動态規劃dp:
25 0 0 0 0 0 
26 0 1500 1500 1500 1500 
27 0 1500 1500 1500 3000 
28 0 1500 1500 2000 3500 
29 物品選擇政策:
30 [Goods{name='電腦', weight=3, value=2000}, Goods{name='吉他', weight=1, value=1500}]      

測試類

4、說明

  0-1背包問題不太好了解。下面用一些圖文幫助讀者了解。

  首先,我們深刻了解一下0-1背包的問題,如圖:

資料結構與算法(十二)——算法-動态規劃

  不妨假設,在這前 i 個物品中,選取總重量不超過背包容量 C 的物品,且使得物品總價值最大。選擇政策叫方案A,此時的最大總價值設為 maxValue。這裡了解一下前 i 個物品,由于動态規劃問題是規模逐漸增大的,是以,要姑且把物品看成是"有序的"。

  那麼,我們思考一個問題,如果此時來了第 i + 1個物品(價值:V,重量:W),會怎麼樣呢?如圖:

資料結構與算法(十二)——算法-動态規劃

  主要分為兩種情況談論:

  1、W > C :第 i + 1 個物品(鑽石)不可取。那麼,此時(總體物品是 i + 1個,但是)是不是就相當于沒有第 i + 1 這個物品(因為在i + 1個物品中選擇,不會取到第 i + 1這個物品),問題的解,是不是就等價于圖一(在 i 個物品中選擇,背包容量為C),是以,就是方案A,最大總價值就是 maxValue。

  2、W <= C :第 i + 1個物品(鑽石)可取。又分為兩種情況:即拿或不拿。(注:這是0-1背包,一個物品要麼不選擇,要麼隻選擇一次)

    2.1:不拿。同樣,此時(這種情況下)問題的解,與上述(W > C)相同,即方案A,最大總價值就是 maxValue

    2.2:拿。那麼,第 i + 1 個物品(鑽石)已經被選擇了,此時背包容量還有(C - W)。由于第 i + 1個物品已經被選擇了,此時問題規模,就是在前 i 個物品中,選取總重量不超過背包容量 C - W 的物品。價值就是dp[i][c-w]。

  由于2.1和2.2屬于一種情況。是以整個問題的解就是 max( maxValue, V + dp[i][c-w] )。

  下面,再對上面填表的過程,以及代碼做一些解釋:

資料結構與算法(十二)——算法-動态規劃
資料結構與算法(十二)——算法-動态規劃