Leetcode題解——算法思想之動態規劃

斐波那契數列html

遞歸和動態規劃都是將原問題拆成多個子問題而後求解,他們之間最本質的區別是,動態規劃保存了子問題的解,避免重複計算。java

斐波那契數列

1. 爬樓梯

70. Climbing Stairs (Easy)程序員

題目描述:有 N 階樓梯,每次能夠上一階或者兩階,求有多少種上樓梯的方法。面試

定義一個數組 dp 存儲上樓梯的方法數(爲了方便討論,數組下標從 1 開始),dp[i] 表示走到第 i 個樓梯的方法數目。算法

第 i 個樓梯能夠從第 i-1 和 i-2 個樓梯再走一步到達,走到第 i 個樓梯的方法數爲走到第 i-1 和第 i-2 個樓梯的方法數之和。數組

 

考慮到 dp[i] 只與 dp[i - 1] 和 dp[i - 2] 有關,所以能夠只用兩個變量來存儲 dp[i - 1] 和 dp[i - 2],使得原來的 O(N) 空間複雜度優化爲 O(1) 複雜度。優化

public int climbStairs(int n) { if (n <= 2) { return n; } int pre2 = 1, pre1 = 2; for (int i = 2; i < n; i++) { int cur = pre1 + pre2; pre2 = pre1; pre1 = cur; } return pre1; }

2. 強盜搶劫

198. House Robber (Easy)this

題目描述:搶劫一排住戶,可是不能搶鄰近的住戶,求最大搶劫量。spa

定義 dp 數組用來存儲最大的搶劫量,其中 dp[i] 表示搶到第 i 個住戶時的最大搶劫量。3d

因爲不能搶劫鄰近住戶,若是搶劫了第 i -1 個住戶,那麼就不能再搶劫第 i 個住戶,因此

 

public int rob(int[] nums) { int pre2 = 0, pre1 = 0; for (int i = 0; i < nums.length; i++) { int cur = Math.max(pre2 + nums[i], pre1); pre2 = pre1; pre1 = cur; } return pre1; }

3. 強盜在環形街區搶劫

213. House Robber II (Medium)

public int rob(int[] nums) { if (nums == null || nums.length == 0) { return 0; } int n = nums.length; if (n == 1) { return nums[0]; } return Math.max(rob(nums, 0, n - 2), rob(nums, 1, n - 1)); } private int rob(int[] nums, int first, int last) { int pre2 = 0, pre1 = 0; for (int i = first; i <= last; i++) { int cur = Math.max(pre1, pre2 + nums[i]); pre2 = pre1; pre1 = cur; } return pre1; }

4. 信件錯排

題目描述:有 N 個 信 和 信封,它們被打亂,求錯誤裝信方式的數量。

定義一個數組 dp 存儲錯誤方式數量,dp[i] 表示前 i 個信和信封的錯誤方式數量。假設第 i 個信裝到第 j 個信封裏面,而第 j 個信裝到第 k 個信封裏面。根據 i 和 k 是否相等,有兩種狀況:

  • i==k,交換 i 和 k 的信後,它們的信和信封在正確的位置,可是其他 i-2 封信有 dp[i-2] 種錯誤裝信的方式。因爲 j 有 i-1 種取值,所以共有 (i-1)*dp[i-2] 種錯誤裝信方式。
  • i != k,交換 i 和 j 的信後,第 i 個信和信封在正確的位置,其他 i-1 封信有 dp[i-1] 種錯誤裝信方式。因爲 j 有 i-1 種取值,所以共有 (i-1)*dp[i-1] 種錯誤裝信方式。

綜上所述,錯誤裝信數量方式數量爲:

 

5. 母牛生產

程序員代碼面試指南-P181

題目描述:假設農場中成熟的母牛每一年都會生 1 頭小母牛,而且永遠不會死。第一年有 1 只小母牛,從第二年開始,母牛開始生小母牛。每隻小母牛 3 年以後成熟又能夠生小母牛。給定整數 N,求 N 年後牛的數量。

第 i 年成熟的牛的數量爲:

 

矩陣路徑

1. 矩陣的最小路徑和

64. Minimum Path Sum (Medium)

[[1,3,1],
 [1,5,1],
 [4,2,1]]
Given the above grid map, return 7. Because the path 1→3→1→1→1 minimizes the sum.

題目描述:求從矩陣的左上角到右下角的最小路徑和,每次只能向右和向下移動。

public int minPathSum(int[][] grid) { if (grid.length == 0 || grid[0].length == 0) { return 0; } int m = grid.length, n = grid[0].length; int[] dp = new int[n]; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (j == 0) { dp[j] = dp[j]; // 只能從上側走到該位置 } else if (i == 0) { dp[j] = dp[j - 1]; // 只能從左側走到該位置 } else { dp[j] = Math.min(dp[j - 1], dp[j]); } dp[j] += grid[i][j]; } } return dp[n - 1]; }

2. 矩陣的總路徑數

62. Unique Paths (Medium)

題目描述:統計從矩陣左上角到右下角的路徑總數,每次只能向右或者向下移動。

 

 

public int uniquePaths(int m, int n) { int[] dp = new int[n]; Arrays.fill(dp, 1); for (int i = 1; i < m; i++) { for (int j = 1; j < n; j++) { dp[j] = dp[j] + dp[j - 1]; } } return dp[n - 1]; }

也能夠直接用數學公式求解,這是一個組合問題。機器人總共移動的次數 S=m+n-2,向下移動的次數 D=m-1,那麼問題能夠當作從 S 中取出 D 個位置的組合數量,這個問題的解爲 C(S, D)。

public int uniquePaths(int m, int n) { int S = m + n - 2; // 總共的移動次數 int D = m - 1; // 向下的移動次數 long ret = 1; for (int i = 1; i <= D; i++) { ret = ret * (S - D + i) / i; } return (int) ret; }

數組區間

1. 數組區間和

303. Range Sum Query - Immutable (Easy)

Given nums = [-2, 0, 3, -5, 2, -1]

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

求區間 i ~ j 的和,能夠轉換爲 sum[j + 1] - sum[i],其中 sum[i] 爲 0 ~ i - 1 的和。

class NumArray { private int[] sums; public NumArray(int[] nums) { sums = new int[nums.length + 1]; for (int i = 1; i <= nums.length; i++) { sums[i] = sums[i - 1] + nums[i - 1]; } } public int sumRange(int i, int j) { return sums[j + 1] - sums[i]; } }

2. 數組中等差遞增子區間的個數

413. Arithmetic Slices (Medium)

A = [0, 1, 2, 3, 4]

return: 6, for 3 arithmetic slices in A:

[0, 1, 2],
[1, 2, 3],
[0, 1, 2, 3],
[0, 1, 2, 3, 4],
[ 1, 2, 3, 4],
[2, 3, 4]

dp[i] 表示以 A[i] 爲結尾的等差遞增子區間的個數。

當 A[i] - A[i-1] == A[i-1] - A[i-2],那麼 [A[i-2], A[i-1], A[i]] 構成一個等差遞增子區間。並且在以 A[i-1] 爲結尾的遞增子區間的後面再加上一個 A[i],同樣能夠構成新的遞增子區間。

dp[2] = 1
    [0, 1, 2]
dp[3] = dp[2] + 1 = 2
    [0, 1, 2, 3], // [0, 1, 2] 以後加一個 3
    [1, 2, 3]     // 新的遞增子區間
dp[4] = dp[3] + 1 = 3
    [0, 1, 2, 3, 4], // [0, 1, 2, 3] 以後加一個 4
    [1, 2, 3, 4],    // [1, 2, 3] 以後加一個 4
    [2, 3, 4]        // 新的遞增子區間

綜上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 時,dp[i] = dp[i-1] + 1。

由於遞增子區間不必定以最後一個元素爲結尾,能夠是任意一個元素結尾,所以須要返回 dp 數組累加的結果。

public int numberOfArithmeticSlices(int[] A) { if (A == null || A.length == 0) { return 0; } int n = A.length; int[] dp = new int[n]; for (int i = 2; i < n; i++) { if (A[i] - A[i - 1] == A[i - 1] - A[i - 2]) { dp[i] = dp[i - 1] + 1; } } int total = 0; for (int cnt : dp) { total += cnt; } return total; }

分割整數

1. 分割整數的最大乘積

343. Integer Break (Medim)

題目描述:For example, given n = 2, return 1 (2 = 1 + 1); given n = 10, return 36 (10 = 3 + 3 + 4).

public int integerBreak(int n) { int[] dp = new int[n + 1]; dp[1] = 1; for (int i = 2; i <= n; i++) { for (int j = 1; j <= i - 1; j++) { dp[i] = Math.max(dp[i], Math.max(j * dp[i - j], j * (i - j))); } } return dp[n]; }

2. 按平方數來分割整數

279. Perfect Squares(Medium)

題目描述:For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.

public int numSquares(int n) { List<Integer> squareList = generateSquareList(n); int[] dp = new int[n + 1]; for (int i = 1; i <= n; i++) { int min = Integer.MAX_VALUE; for (int square : squareList) { if (square > i) { break; } min = Math.min(min, dp[i - square] + 1); } dp[i] = min; } return dp[n]; } private List<Integer> generateSquareList(int n) { List<Integer> squareList = new ArrayList<>(); int diff = 3; int square = 1; while (square <= n) { squareList.add(square); square += diff; diff += 2; } return squareList; }

3. 分割整數構成字母字符串

91. Decode Ways (Medium)

題目描述:Given encoded message "12", it could be decoded as "AB" (1 2) or "L" (12).

public int numDecodings(String s) { if (s == null || s.length() == 0) { return 0; } int n = s.length(); int[] dp = new int[n + 1]; dp[0] = 1; dp[1] = s.charAt(0) == '0' ? 0 : 1; for (int i = 2; i <= n; i++) { int one = Integer.valueOf(s.substring(i - 1, i)); if (one != 0) { dp[i] += dp[i - 1]; } if (s.charAt(i - 2) == '0') { continue; } int two = Integer.valueOf(s.substring(i - 2, i)); if (two <= 26) { dp[i] += dp[i - 2]; } } return dp[n]; }

最長遞增子序列

已知一個序列 {S1, S2,...,Sn},取出若干數組成新的序列 {Si1, Si2,..., Sim},其中 i一、i2 ... im 保持遞增,即新序列中各個數仍然保持原數列中的前後順序,稱新序列爲原序列的一個 子序列

若是在子序列中,當下標 ix > iy 時,Six > Siy,稱子序列爲原序列的一個 遞增子序列

定義一個數組 dp 存儲最長遞增子序列的長度,dp[n] 表示以 Sn 結尾的序列的最長遞增子序列長度。對於一個遞增子序列 {Si1, Si2,...,Sim},若是 im < n 而且 Sim < Sn,此時 {Si1, Si2,..., Sim, Sn} 爲一個遞增子序列,遞增子序列的長度增長 1。知足上述條件的遞增子序列中,長度最長的那個遞增子序列就是要找的,在長度最長的遞增子序列上加上 Sn 就構成了以 Sn 爲結尾的最長遞增子序列。所以 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。

由於在求 dp[n] 時可能沒法找到一個知足條件的遞增子序列,此時 {Sn} 就構成了遞增子序列,須要對前面的求解方程作修改,令 dp[n] 最小爲 1,即:

 

對於一個長度爲 N 的序列,最長遞增子序列並不必定會以 SN 爲結尾,所以 dp[N] 不是序列的最長遞增子序列的長度,須要遍歷 dp 數組找出最大值纔是所要的結果,max{ dp[i] | 1 <= i <= N} 即爲所求。

1. 最長遞增子序列

300. Longest Increasing Subsequence (Medium)

public int lengthOfLIS(int[] nums) { int n = nums.length; int[] dp = new int[n]; for (int i = 0; i < n; i++) { int max = 1; for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) { max = Math.max(max, dp[j] + 1); } } dp[i] = max; } return Arrays.stream(dp).max().orElse(0); }

使用 Stream 求最大值會致使運行時間過長,能夠改爲如下形式:

int ret = 0; for (int i = 0; i < n; i++) { ret = Math.max(ret, dp[i]); } return ret;

以上解法的時間複雜度爲 O(N2),可使用二分查找將時間複雜度下降爲 O(NlogN)。

定義一個 tails 數組,其中 tails[i] 存儲長度爲 i + 1 的最長遞增子序列的最後一個元素。對於一個元素 x,

  • 若是它大於 tails 數組全部的值,那麼把它添加到 tails 後面,表示最長遞增子序列長度加 1;
  • 若是 tails[i-1] < x <= tails[i],那麼更新 tails[i] = x。

例如對於數組 [4,3,6,5],有:

tails      len      num
[]         0        4
[4]        1        3
[3]        1        6
[3,6]      2        5
[3,5]      2        null

能夠看出 tails 數組保持有序,所以在查找 Si 位於 tails 數組的位置時就可使用二分查找。

public int lengthOfLIS(int[] nums) { int n = nums.length; int[] tails = new int[n]; int len = 0; for (int num : nums) { int index = binarySearch(tails, len, num); tails[index] = num; if (index == len) { len++; } } return len; } private int binarySearch(int[] tails, int len, int key) { int l = 0, h = len; while (l < h) { int mid = l + (h - l) / 2; if (tails[mid] == key) { return mid; } else if (tails[mid] > key) { h = mid; } else { l = mid + 1; } } return l; }

2. 一組整數對可以構成的最長鏈

646. Maximum Length of Pair Chain (Medium)

Input: [[1,2], [2,3], [3,4]]
Output: 2
Explanation: The longest chain is [1,2] -> [3,4]

題目描述:對於 (a, b) 和 (c, d) ,若是 b < c,則它們能夠構成一條鏈。

public int findLongestChain(int[][] pairs) { if (pairs == null || pairs.length == 0) { return 0; } Arrays.sort(pairs, (a, b) -> (a[0] - b[0])); int n = pairs.length; int[] dp = new int[n]; Arrays.fill(dp, 1); for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (pairs[j][1] < pairs[i][0]) { dp[i] = Math.max(dp[i], dp[j] + 1); } } } return Arrays.stream(dp).max().orElse(0); }

3. 最長擺動子序列

376. Wiggle Subsequence (Medium)

Input: [1,7,4,9,2,5]
Output: 6
The entire sequence is a wiggle sequence.

Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].

Input: [1,2,3,4,5,6,7,8,9]
Output: 2

要求:使用 O(N) 時間複雜度求解。

public int wiggleMaxLength(int[] nums) { if (nums == null || nums.length == 0) { return 0; } int up = 1, down = 1; for (int i = 1; i < nums.length; i++) { if (nums[i] > nums[i - 1]) { up = down + 1; } else if (nums[i] < nums[i - 1]) { down = up + 1; } } return Math.max(up, down); }

最長公共子序列

對於兩個子序列 S1 和 S2,找出它們最長的公共子序列。

定義一個二維數組 dp 用來存儲最長公共子序列的長度,其中 dp[i][j] 表示 S1 的前 i 個字符與 S2 的前 j 個字符最長公共子序列的長度。考慮 S1i 與 S2j 值是否相等,分爲兩種狀況:

  • 當 S1i==S2j 時,那麼就能在 S1 的前 i-1 個字符與 S2 的前 j-1 個字符最長公共子序列的基礎上再加上 S1i 這個值,最長公共子序列長度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
  • 當 S1i != S2j 時,此時最長公共子序列爲 S1 的前 i-1 個字符和 S2 的前 j 個字符最長公共子序列,或者 S1 的前 i 個字符和 S2 的前 j-1 個字符最長公共子序列,取它們的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。

綜上,最長公共子序列的狀態轉移方程爲:

 

對於長度爲 N 的序列 S1 和長度爲 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最長公共子序列長度。

與最長遞增子序列相比,最長公共子序列有如下不一樣點:

  • 針對的是兩個序列,求它們的最長公共子序列。
  • 在最長遞增子序列中,dp[i] 表示以 Si 爲結尾的最長遞增子序列長度,子序列必須包含 Si ;在最長公共子序列中,dp[i][j] 表示 S1 中前 i 個字符與 S2 中前 j 個字符的最長公共子序列長度,不必定包含 S1i 和 S2j
  • 在求最終解時,最長公共子序列中 dp[N][M] 就是最終解,而最長遞增子序列中 dp[N] 不是最終解,由於以 SN 爲結尾的最長遞增子序列不必定是整個序列最長遞增子序列,須要遍歷一遍 dp 數組找到最大者。
public int lengthOfLCS(int[] nums1, int[] nums2) { int n1 = nums1.length, n2 = nums2.length; int[][] dp = new int[n1 + 1][n2 + 1]; for (int i = 1; i <= n1; i++) { for (int j = 1; j <= n2; j++) { if (nums1[i - 1] == nums2[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } return dp[n1][n2]; }

0-1 揹包

有一個容量爲 N 的揹包,要用這個揹包裝下物品的價值最大,這些物品有兩個屬性:體積 w 和價值 v。

定義一個二維數組 dp 存儲最大價值,其中 dp[i][j] 表示前 i 件物品體積不超過 j 的狀況下能達到的最大價值。設第 i 件物品體積爲 w,價值爲 v,根據第 i 件物品是否添加到揹包中,能夠分兩種狀況討論:

  • 第 i 件物品沒添加到揹包,整體積不超過 j 的前 i 件物品的最大價值就是整體積不超過 j 的前 i-1 件物品的最大價值,dp[i][j] = dp[i-1][j]。
  • 第 i 件物品添加到揹包中,dp[i][j] = dp[i-1][j-w] + v。

第 i 件物品可添加也能夠不添加,取決於哪一種狀況下最大價值更大。所以,0-1 揹包的狀態轉移方程爲:

 

public int knapsack(int W, int N, int[] weights, int[] values) { int[][] dp = new int[N + 1][W + 1]; for (int i = 1; i <= N; i++) { int w = weights[i - 1], v = values[i - 1]; for (int j = 1; j <= W; j++) { if (j >= w) { dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v); } else { dp[i][j] = dp[i - 1][j]; } } } return dp[N][W]; }

空間優化

在程序實現時能夠對 0-1 揹包作優化。觀察狀態轉移方程能夠知道,前 i 件物品的狀態僅與前 i-1 件物品的狀態有關,所以能夠將 dp 定義爲一維數組,其中 dp[j] 既能夠表示 dp[i-1][j] 也能夠表示 dp[i][j]。此時,

 

由於 dp[j-w] 表示 dp[i-1][j-w],所以不能先求 dp[i][j-w],以防將 dp[i-1][j-w] 覆蓋。也就是說要先計算 dp[i][j] 再計算 dp[i][j-w],在程序實現時須要按倒序來循環求解。

public int knapsack(int W, int N, int[] weights, int[] values) { int[] dp = new int[W + 1]; for (int i = 1; i <= N; i++) { int w = weights[i - 1], v = values[i - 1]; for (int j = W; j >= 1; j--) { if (j >= w) { dp[j] = Math.max(dp[j], dp[j - w] + v); } } } return dp[W]; }

沒法使用貪心算法的解釋

0-1 揹包問題沒法使用貪心算法來求解,也就是說不能按照先添加性價比最高的物品來達到最優,這是由於這種方式可能形成揹包空間的浪費,從而沒法達到最優。考慮下面的物品和一個容量爲 5 的揹包,若是先添加物品 0 再添加物品 1,那麼只能存放的價值爲 16,浪費了大小爲 2 的空間。最優的方式是存放物品 1 和物品 2,價值爲 22.

id w v v/w
0 1 6 6
1 2 10 5
2 3 12 4

變種

  • 徹底揹包:物品數量爲無限個

  • 多重揹包:物品數量有限制

  • 多維費用揹包:物品不只有重量,還有體積,同時考慮這兩種限制

  • 其它:物品之間相互約束或者依賴

1. 劃分數組爲和相等的兩部分

416. Partition Equal Subset Sum (Medium)

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].

能夠當作一個揹包大小爲 sum/2 的 0-1 揹包問題。

public boolean canPartition(int[] nums) { int sum = computeArraySum(nums); if (sum % 2 != 0) { return false; } int W = sum / 2; boolean[] dp = new boolean[W + 1]; dp[0] = true; for (int num : nums) { // 0-1 揹包一個物品只能用一次 for (int i = W; i >= num; i--) { // 從後往前,先計算 dp[i] 再計算 dp[i-num] dp[i] = dp[i] || dp[i - num]; } } return dp[W]; } private int computeArraySum(int[] nums) { int sum = 0; for (int num : nums) { sum += num; } return sum; }

2. 改變一組數的正負號使得它們的和爲一給定數

494. Target Sum (Medium)

Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

There are 5 ways to assign symbols to make the sum of nums be target 3.

該問題能夠轉換爲 Subset Sum 問題,從而使用 0-1 揹包的方法來求解。

能夠將這組數當作兩部分,P 和 N,其中 P 使用正號,N 使用負號,有如下推導:

                  sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
                       2 * sum(P) = target + sum(nums)

所以只要找到一個子集,令它們都取正號,而且和等於 (target + sum(nums))/2,就證實存在解。

public int findTargetSumWays(int[] nums, int S) { int sum = computeArraySum(nums); if (sum < S || (sum + S) % 2 == 1) { return 0; } int W = (sum + S) / 2; int[] dp = new int[W + 1]; dp[0] = 1; for (int num : nums) { for (int i = W; i >= num; i--) { dp[i] = dp[i] + dp[i - num]; } } return dp[W]; } private int computeArraySum(int[] nums) { int sum = 0; for (int num : nums) { sum += num; } return sum; }

DFS 解法:

public int findTargetSumWays(int[] nums, int S) { return findTargetSumWays(nums, 0, S); } private int findTargetSumWays(int[] nums, int start, int S) { if (start == nums.length) { return S == 0 ? 1 : 0; } return findTargetSumWays(nums, start + 1, S + nums[start]) + findTargetSumWays(nums, start + 1, S - nums[start]); }

3. 01 字符構成最多的字符串

474. Ones and Zeroes (Medium)

Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4

Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0"

這是一個多維費用的 0-1 揹包問題,有兩個揹包大小,0 的數量和 1 的數量。

public int findMaxForm(String[] strs, int m, int n) { if (strs == null || strs.length == 0) { return 0; } int[][] dp = new int[m + 1][n + 1]; for (String s : strs) { // 每一個字符串只能用一次 int ones = 0, zeros = 0; for (char c : s.toCharArray()) { if (c == '0') { zeros++; } else { ones++; } } for (int i = m; i >= zeros; i--) { for (int j = n; j >= ones; j--) { dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1); } } } return dp[m][n]; }

4. 找零錢的最少硬幣數

322. Coin Change (Medium)

Example 1:
coins = [1, 2, 5], amount = 11
return 3 (11 = 5 + 5 + 1)

Example 2:
coins = [2], amount = 3
return -1.

題目描述:給一些面額的硬幣,要求用這些硬幣來組成給定面額的錢數,而且使得硬幣數量最少。硬幣能夠重複使用。

  • 物品:硬幣
  • 物品大小:面額
  • 物品價值:數量

由於硬幣能夠重複使用,所以這是一個徹底揹包問題。徹底揹包只須要將 0-1 揹包中逆序遍歷 dp 數組改成正序遍歷便可。

public int coinChange(int[] coins, int amount) { if (amount == 0 || coins == null || coins.length == 0) { return 0; } int[] dp = new int[amount + 1]; for (int coin : coins) { for (int i = coin; i <= amount; i++) { //將逆序遍歷改成正序遍歷 if (i == coin) { dp[i] = 1; } else if (dp[i] == 0 && dp[i - coin] != 0) { dp[i] = dp[i - coin] + 1; } else if (dp[i - coin] != 0) { dp[i] = Math.min(dp[i], dp[i - coin] + 1); } } } return dp[amount] == 0 ? -1 : dp[amount]; }

5. 找零錢的硬幣數組合

518. Coin Change 2 (Medium)

Input: amount = 5, coins = [1, 2, 5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

徹底揹包問題,使用 dp 記錄可達成目標的組合數目。

public int change(int amount, int[] coins) { if (amount == 0 || coins == null || coins.length == 0) { return 0; } int[] dp = new int[amount + 1]; dp[0] = 1; for (int coin : coins) { for (int i = coin; i <= amount; i++) { dp[i] += dp[i - coin]; } } return dp[amount]; }

6. 字符串按單詞列表分割

139. Word Break (Medium)

s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".

dict 中的單詞沒有使用次數的限制,所以這是一個徹底揹包問題。該問題涉及到字典中單詞的使用順序,所以可理解爲涉及順序的徹底揹包問題。

求解順序的徹底揹包問題時,對物品的迭代應該放在最裏層。

public boolean wordBreak(String s, List<String> wordDict) { int n = s.length(); boolean[] dp = new boolean[n + 1]; dp[0] = true; for (int i = 1; i <= n; i++) { for (String word : wordDict) { // 對物品的迭代應該放在最裏層 int len = word.length(); if (len <= i && word.equals(s.substring(i - len, i))) { dp[i] = dp[i] || dp[i - len]; } } } return dp[n]; }

7. 組合總和

377. Combination Sum IV (Medium)

nums = [1, 2, 3]
target = 4

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.

涉及順序的徹底揹包。

public int combinationSum4(int[] nums, int target) { if (nums == null || nums.length == 0) { return 0; } int[] maximum = new int[target + 1]; maximum[0] = 1; Arrays.sort(nums); for (int i = 1; i <= target; i++) { for (int j = 0; j < nums.length && nums[j] <= i; j++) { maximum[i] += maximum[i - nums[j]]; } } return maximum[target]; }

股票交易

1. 須要冷卻期的股票交易

309. Best Time to Buy and Sell Stock with Cooldown(Medium)

題目描述:交易以後須要有一天的冷卻時間。

 

public int maxProfit(int[] prices) { if (prices == null || prices.length == 0) { return 0; } int N = prices.length; int[] buy = new int[N]; int[] s1 = new int[N]; int[] sell = new int[N]; int[] s2 = new int[N]; s1[0] = buy[0] = -prices[0]; sell[0] = s2[0] = 0; for (int i = 1; i < N; i++) { buy[i] = s2[i - 1] - prices[i]; s1[i] = Math.max(buy[i - 1], s1[i - 1]); sell[i] = Math.max(buy[i - 1], s1[i - 1]) + prices[i]; s2[i] = Math.max(s2[i - 1], sell[i - 1]); } return Math.max(sell[N - 1], s2[N - 1]); }

2. 須要交易費用的股票交易

714. Best Time to Buy and Sell Stock with Transaction Fee (Medium)

Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:
Buying at prices[0] = 1
Selling at prices[3] = 8
Buying at prices[4] = 4
Selling at prices[5] = 9
The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

題目描述:每交易一次,都要支付必定的費用。

 

public int maxProfit(int[] prices, int fee) { int N = prices.length; int[] buy = new int[N]; int[] s1 = new int[N]; int[] sell = new int[N]; int[] s2 = new int[N]; s1[0] = buy[0] = -prices[0]; sell[0] = s2[0] = 0; for (int i = 1; i < N; i++) { buy[i] = Math.max(sell[i - 1], s2[i - 1]) - prices[i]; s1[i] = Math.max(buy[i - 1], s1[i - 1]); sell[i] = Math.max(buy[i - 1], s1[i - 1]) - fee + prices[i]; s2[i] = Math.max(s2[i - 1], sell[i - 1]); } return Math.max(sell[N - 1], s2[N - 1]); }

3. 只能進行兩次的股票交易

123. Best Time to Buy and Sell Stock III (Hard)

public int maxProfit(int[] prices) { int firstBuy = Integer.MIN_VALUE, firstSell = 0; int secondBuy = Integer.MIN_VALUE, secondSell = 0; for (int curPrice : prices) { if (firstBuy < -curPrice) { firstBuy = -curPrice; } if (firstSell < firstBuy + curPrice) { firstSell = firstBuy + curPrice; } if (secondBuy < firstSell - curPrice) { secondBuy = firstSell - curPrice; } if (secondSell < secondBuy + curPrice) { secondSell = secondBuy + curPrice; } } return secondSell; }

4. 只能進行 k 次的股票交易

188. Best Time to Buy and Sell Stock IV (Hard)

public int maxProfit(int k, int[] prices) { int n = prices.length; if (k >= n / 2) { // 這種狀況下該問題退化爲普通的股票交易問題 int maxProfit = 0; for (int i = 1; i < n; i++) { if (prices[i] > prices[i - 1]) { maxProfit += prices[i] - prices[i - 1]; } } return maxProfit; } int[][] maxProfit = new int[k + 1][n]; for (int i = 1; i <= k; i++) { int localMax = maxProfit[i - 1][0] - prices[0]; for (int j = 1; j < n; j++) { maxProfit[i][j] = Math.max(maxProfit[i][j - 1], prices[j] + localMax); localMax = Math.max(localMax, maxProfit[i - 1][j] - prices[j]); } } return maxProfit[k][n - 1]; }

字符串編輯

1. 刪除兩個字符串的字符使它們相等

583. Delete Operation for Two Strings (Medium)

Input: "sea", "eat"
Output: 2
Explanation: You need one step to make "sea" to "ea" and another step to make "eat" to "ea".

能夠轉換爲求兩個字符串的最長公共子序列問題。

public int minDistance(String word1, String word2) { int m = word1.length(), n = word2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); } } } return m + n - 2 * dp[m][n]; }

2. 編輯距離

72. Edit Distance (Hard)

Example 1:

Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation:
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')
Example 2:

Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation:
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')

題目描述:修改一個字符串成爲另外一個字符串,使得修改次數最少。一次修改操做包括:插入一個字符、刪除一個字符、替換一個字符。

public int minDistance(String word1, String word2) { if (word1 == null || word2 == null) { return 0; } int m = word1.length(), n = word2.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) { dp[i][0] = i; } for (int i = 1; i <= n; i++) { dp[0][i] = i; } for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { if (word1.charAt(i - 1) == word2.charAt(j - 1)) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; } } } return dp[m][n]; }

3. 複製粘貼字符

650. 2 Keys Keyboard (Medium)

題目描述:最開始只有一個字符 A,問須要多少次操做可以獲得 n 個字符 A,每次操做能夠複製當前全部的字符,或者粘貼。

Input: 3
Output: 3
Explanation:
Intitally, we have one character 'A'.
In step 1, we use Copy All operation.
In step 2, we use Paste operation to get 'AA'.
In step 3, we use Paste operation to get 'AAA'.
public int minSteps(int n) { if (n == 1) return 0; for (int i = 2; i <= Math.sqrt(n); i++) { if (n % i == 0) return i + minSteps(n / i); } return n; }
public int minSteps(int n) { int[] dp = new int[n + 1]; int h = (int) Math.sqrt(n); for (int i = 2; i <= n; i++) { dp[i] = i; for (int j = 2; j <= h; j++) { if (i % j == 0) { dp[i] = dp[j] + dp[i / j]; break; } } } return dp[n]; }
相關文章
相關標籤/搜索