最難啃的骨頭開始啃了
文章目錄
斐波拉契數組變形系列
70.爬樓梯
dp方程:
dp[i] = dp[i-1] + dp[i-2]
自底向上迭代
class Solution {
public int climbStairs(int n) {
if (n == 0) return 1;
if (n < 3) return n;
int dp[] = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
自頂向下記憶化遞歸:
class Solution {
private int[] cache;
public int climbStairs(int n) {
if (n == 0) return 1;
cache = new int[n + 1];
cache[0] = 1;
return helper(n);
}
private int helper(int n) {
if (n < 3) {
if (cache[n] == 0) {
cache[n] = n;
}
}
if (cache[n] == 0) {
cache[n] = helper(n - 1) + helper(n - 2);
}
return cache[n];
}
}
322.零錢兌換
思路類似於爬樓梯和斐波拉契數列
求f(n)
問題可以轉換爲求f(n-coin[i])的最小值+1
的問題
自頂向下,帶緩存的遞歸
class Solution {
static int cache[];
public int coinChange(int[] coins, int amount) {
if (amount < 1) return 0;
cache = new int[amount + 1];
return helper(coins, amount);
}
private int helper(int[] coins, int amount) {
if (amount == 0) return 0;
if (amount < 0) return -1;
if (cache[amount] == 0) {
int curmin = Integer.MAX_VALUE;
for (int i = 0; i < coins.length; i++) {
int res = helper(coins, amount - coins[i]);
if (res >= 0 && curmin > res)
curmin = res+1;
}
cache[amount] = (curmin == Integer.MAX_VALUE) ? -1 : curmin;
}
return cache[amount];
}
}
自底向上,dp迭代
因爲求最小值,所以先初始化爲比最大還大,如果迭代完了小於等於最大值,那麼就找到了,如果依然大於最大值,說明沒找到
class Solution {
public int coinChange(int[] coins, int amount) {
int dp[] = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
不同路徑系列
62.不同路徑
子問題:右側點的路徑數+下側點的路徑數
得到dp方程:dp[i][j] = dp[i+1][j]+dp[i][j+1]
我們求的是(0,0)點的可能到達的路徑數,所以從最後開始遍歷,推出(0,0)的總路徑數
最後一行和最後一列比較固定,按這個走法只能有一條路能到達,全部初始化爲1
class Solution {
//dp方程
//opt[i][j] = opt[i+1][j]+opt[i][j+1]
public int uniquePaths(int m, int n) {
int dp[][] = new int[m][n];
for (int k = 0; k < m; k++) {
dp[k][n - 1] = 1;
}
for (int k = 0; k < n; k++) {
dp[m - 1][k] = 1;
}
for (int k = m - 2; k >= 0; k--) {
for (int l = n - 2; l >= 0; l--) {
dp[k][l] = dp[k+1][l]+dp[k][l+1];
}
}
return dp[0][0];
}
}
優化:將dp數組簡化成一維的
每次迭代只需要暫存最後一排的就夠了,暫存數組多開一維,保證最後一列沒有初始化也可以使用
class Solution {
public int uniquePaths(int m, int n) {
//定義一維的暫存空間就夠了,從最後一排開始存儲
int dp[] = new int[n + 1];
//初始化最後一排,數組大小爲n-1,最後一個保持爲0,避免迭代時數組越界
for (int k = 0; k < n; k++) {
dp[k] = 1;
}
for (int k = m - 2; k >= 0; k--) {
for (int l = n - 1; l >= 0; l--) {
dp[l] = dp[l]+dp[l+1];
}
}
return dp[0];
}
}
63.不同路徑II
相比上一個題,增加了障礙物
首先還是初始化dp數組的最後一行和最後一列,然後最後往前遞推,遇到障礙物就跳過
初始化發生變化,不再是全賦1,而是從最後一個開始推:如果遇到障礙物就跳過沒如果沒有障礙物當前值就等於後一個值,一旦遇到了障礙物跳過了,障礙物位置爲0,之前的都爲0
遍歷時,如果遇到障礙物就跳過,障礙物位置就爲0
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if (obstacleGrid == null || obstacleGrid.length == 0) return 0;
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int dp[][] = new int[m][n];
if (obstacleGrid[m - 1][n - 1] == 0) dp[m - 1][n - 1] = 1;
//從最後一個點開始,如果不是石頭就和前一個一樣,如果是石頭就初始化爲0
for (int k = m - 2; k >= 0; k--) {
if (obstacleGrid[k][n - 1] == 0) dp[k][n - 1] = dp[k + 1][n - 1];
}
for (int k = n - 2; k >= 0; k--) {
if (obstacleGrid[m - 1][k] == 0) dp[m - 1][k] = dp[m - 1][k + 1];
}
for (int k = m - 2; k >= 0; k--) {
for (int l = n - 2; l >= 0; l--) {
if (obstacleGrid[k][l] == 0) dp[k][l] = dp[k + 1][l] + dp[k][l + 1];
}
}
return dp[0][0];
}
}
優化:將dp數組簡化爲一維的
思路和上方一致,有一點需要小心,使用二維時如果遇到障礙物可以直接跳過,因爲初始化爲0,而使用一維時不行,必須對障礙物進行處理,將障礙物出手動置爲1
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if (obstacleGrid == null || obstacleGrid.length == 0) return 0;
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int dp[] = new int[n + 1];
if (obstacleGrid[m - 1][n - 1] == 0) dp[n - 1] = 1;
for (int k = n - 2; k >= 0; k--) {
if (obstacleGrid[m - 1][k] == 0) dp[k] = dp[k + 1];
}
for (int k = m - 2; k >= 0; k--) {
for (int l = n - 1; l >= 0; l--) {
if (obstacleGrid[k][l] == 0) dp[l] = dp[l] + dp[l + 1];
else dp[l] = 0;
}
}
return dp[0];
}
}
120.三角形最小路徑和
和不同路徑類似,只是只跑了一半的圖,如果不求最小路徑而是求路徑條數就和不同路徑基本一致了
將集合格式改變一下就比較清楚了
[2],
[3,4],
[6,5,7],
[4,1,8,3]
從倒數第二層開始,找到下一層下面的數和右斜下的數中的小的一個,加上當前位置,就是局部最小了
dp方程:dp[j] = Math.min(dp[j], dp[j + 1]) + num[i][j]
dp初始化爲最後一行的下一行,是一個空白行
然後依次向上迭代,最後只剩下第一行第一個就是最小路徑的值
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int dp[] = new int[triangle.size() + 1];
for (int i = triangle.size() - 1; i >= 0; i--) {
for (int j = 0; j < triangle.get(i).size(); j++) {
dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j);
}
}
return dp[0];
}
}
不開闢新數組,複用原集合
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
for (int i = triangle.size() - 2; i >= 0; i--) {
for (int j = 0; j < triangle.get(i).size(); j++) {
triangle.get(i).set(j, Math.min(triangle.get(i + 1).get(j), triangle.get(i + 1).get(j + 1)) + triangle.get(i).get(j));
}
}
return triangle.get(0).get(0);
}
}
自頂向下記憶化遞歸
class Solution {
Integer[][] cache;
public int minimumTotal(List<List<Integer>> triangle) {
cache = new Integer[triangle.size()][triangle.size()];
return helper(0, 0, triangle);
}
private int helper(int level, int c, List<List<Integer>> triangle) {
if (cache[level][c] != null) {
return cache[level][c];
}
if (level == triangle.size() - 1) {
return cache[level][c] = triangle.get(level).get(c);
}
int left = helper(level + 1, c, triangle);
int right = helper(level + 1, c + 1, triangle);
return cache[level][c] = Math.min(left, right) + triangle.get(level).get(c);
}
}
64.最小路徑和
二維的dp
class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;
int m = grid.length;
int n = grid[0].length;
int dp[][] = new int[m][n];
dp[0][0] = grid[0][0];
//初始化dp的第一行和第一列
for (int i = 1; i < m; i++) {
dp[i][0] += grid[i][0] + dp[i - 1][0];
}
for (int i = 1; i < n; i++) {
dp[0][i] = grid[0][i] + dp[0][i - 1];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
}
壓縮dp數組到一維
dp[0]設置成Integer.MAX_VALUE;從dp[1]開始使用
class Solution {
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) return 0;
int m = grid.length;
int n = grid[0].length;
int dp[] = new int[n + 1];
dp[0] = Integer.MAX_VALUE;
dp[1] = grid[0][0];
//初始化dp的第一行和第一列
for (int i = 1; i < n; i++) {
dp[i + 1] += grid[0][i] + dp[i];
}
for (int i = 1; i < m; i++) {
for (int j = 0; j < n; j++) {
dp[j + 1] = grid[i][j] + Math.min(dp[j], dp[j + 1]);
}
}
return dp[n];
}
}
子序列系列
53.最大子序和(子序列最大和)
dp方程:dp[i] += dp[i-1]
如果前一個值大於零,說明加上它當前值會增大,就加上,如果小於等於零,就不加
每一個位置都求一下最大和,遍歷完成獲得全局最大和
class Solution {
public int maxSubArray(int[] nums) {
int resMax = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i - 1] > 0) nums[i] += nums[i - 1];
resMax = Math.max(resMax, nums[i]);
}
return resMax;
}
}
152.乘積最大子數組(子序列最大積)
維護兩個變量,當前最大值和當前最小值, 如果當前值爲負數,就交換最小值和最大值
class Solution {
public int maxProduct(int[] nums) {
int max = Integer.MIN_VALUE;
int curmax = 1;
int curmin = 1;
for (int i = 0; i < nums.length; i++) {
if (nums[i] < 0) {
int tmp = curmax;
curmax = curmin;
curmin = tmp;
}
curmax = Math.max(nums[i], curmax * nums[i]);
curmin = Math.min(nums[i], curmin * nums[i]);
max = Math.max(max, curmax);
}
return max;
}
}
解法二:兩次遍歷
將數組按0分成n個子數組,求不含0的子數組的最大積
正序和逆序分別遍歷一次,兩個全局最大積的較大值就是最大積
class Solution {
public int maxProduct(int[] nums) {
int max = 1;
int resmax = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
max *= nums[i];
resmax = Math.max(resmax, max);
if (max == 0) max = 1;
}
max = 1;
for (int i = nums.length - 1; i >= 0; i--) {
max *= nums[i];
resmax = Math.max(resmax, max);
if (max == 0) max = 1;
}
return resmax;
}
}
1143.最長公共子序列(子序列最大公共序列)
將一維變成二維操作
自頂向下:從最後開始看,如果最後兩個字符一樣的,結果等於各自減去最後一個後的長度+1,如果最後兩個字符不相同,等於任選一個減去最後一個字符後的計算長度中大的那一個
自底向上:從第一個字符開始,數組從1下標開始,如果兩個字符相同,則長度等於各自減一的後計算長度+1,如果不相同,則任選一個減去最後一個字符後的計算長度中大的那一個。自底向上迭代,最後位置就是最終答案
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
if (text1 == null || text2 == null) return 0;
int m = text1.length();
int n = text2.length();
int dp[][] = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
}
}
}
return dp[m][n];
}
}
打家劫舍系列
198.打家劫舍
新加一個維度,偷還是不偷
子問題:
- 如果這一家偷,那麼收入等於前一家不偷+這一家偷的
dp[i][1] = nums[i] + dp[i - 1][0];
- 如果這一家不偷,那麼收入等於前一家偷或者不偷的較大值
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for (int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = nums[i] + dp[i - 1][0];
}
return Math.max(dp[n - 1][0], dp[n - 1][1]);
}
}
不加維度,問題轉換爲求dp[i-2]+num[i]和dp[i-1]的較大值
的問題
dp方程轉換爲:dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
if (nums.length == 2) return Math.max(nums[0], nums[1]);
int dp[] = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.length - 1];
}
}
爲了避免判斷長度爲0 1 2,採用length+2長度的dp數組
class Solution {
public int rob(int[] nums) {
int dp[] = new int[nums.length + 2];
for (int i = 0; i < nums.length; i++) {
dp[i + 2] = Math.max(dp[i + 1], dp[i] + nums[i]);
}
return dp[nums.length + 1];
}
}
213.打家劫舍II
與打家劫舍I相比變成了環了,意味着還要考慮如果偷了第一家最後一家不能偷,如果偷了最後一家第一家不能偷
那麼問題可以簡化成:從第一家到倒數第二家遍歷一次
;從第二家到最後一家遍歷一次
。這樣就破除了環了,然後取較大值返回
在第一個題的基礎上進行改造:
一維:
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
if (nums.length == 2) return Math.max(nums[0], nums[1]);
int dp[] = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length - 1; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
dp[0] = dp[nums.length-2];//暫存第一個遍歷結果
dp[1] = nums[1];
dp[2] = Math.max(nums[1], nums[2]);
for (int i = 3; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return Math.max(dp[nums.length - 1], dp[0]);
}
}
簡化:
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int dp[] = new int[nums.length + 2];
for (int i = 0; i < nums.length - 1; i++) {
dp[i + 2] = Math.max(dp[i + 1], dp[i] + nums[i]);
}
int res1 = dp[nums.length];//暫存第一個遍歷結果
Arrays.fill(dp, 0);
for (int i = 1; i < nums.length; i++) {
dp[i + 2] = Math.max(dp[i + 1], dp[i] + nums[i]);
}
return Math.max(dp[nums.length + 1], res1);
}
}
二維:
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int dp[][] = new int[nums.length][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for (int i = 1; i < nums.length - 1; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = nums[i] + dp[i - 1][0];
}
int res1 = Math.max(dp[nums.length - 2][0], dp[nums.length - 2][1]);
dp[1][0] = 0;
dp[1][1] = nums[1];
for (int i = 2; i < nums.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = nums[i] + dp[i - 1][0];
}
int res2 = Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]);
return Math.max(res1, res2);
}
}
337.打家劫舍III
自頂向下
的傻遞歸
class Solution {
public int rob(TreeNode root) {
if (root == null) return 0;
//當前層和下下層的和
int val1 = root.val;
if (root.left!=null)val1+=rob(root.left.left)+rob(root.left.right);
if (root.right!=null)val1+=rob(root.right.left)+rob(root.right.right);
//下一層的和
int val2 = rob(root.left)+rob(root.right);
return Math.max(val1,val2);
}
}
自頂向下
的帶緩存的
遞歸
class Solution {
Map<TreeNode, Integer> cache;
public int rob(TreeNode root) {
cache = new HashMap<TreeNode, Integer>();
return helper(root);
}
private int helper(TreeNode root) {
if (root == null) return 0;
if (cache.containsKey(root)) return cache.get(root);
//當前層和下下層的和
int val1 = root.val;
if (root.left != null) val1 += helper(root.left.left) + helper(root.left.right);
if (root.right != null) val1 += helper(root.right.left) + helper(root.right.right);
//下一層的和
int val2 = helper(root.left) + helper(root.right);
cache.put(root, Math.max(val1, val2));
return cache.get(root);
}
}
自底向上
,後序遍歷加升維
和前面兩個打家劫舍一樣,可以升一個維度,用偷或者不偷來描述一次操作
採用後序遍歷保證自底向上逐層搜索
class Solution {
public int rob(TreeNode root) {
int[] helper = helper(root);
return Math.max(helper[0], helper[1]);
}
//返回結果是偷或者不偷的值
private int[] helper(TreeNode root) {
if (root == null) return new int[]{0, 0};
int[] left = helper(root.left);
int[] right = helper(root.right);
//選擇偷當前節點,值爲當前節點+下一層不偷
int choose = root.val + left[1] + right[1];
//選擇不偷當前節點,值爲下一層偷的最大值的和
int noChoose = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
return new int[]{choose, noChoose};
}
}
買賣股票系列
121.買賣股票的最佳時機
動態規劃:
因爲最多交易一次,狀態一共只有三種
- 一次都不交易
- 買了一次
- 買了一次 賣了一次
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int dp[][] = new int[prices.length][3];
dp[0][0] = 0;//不操作
dp[0][1] = -prices[0];//買入
dp[0][2] = Integer.MIN_VALUE;//賣出,不可能存在
for (int i = 1; i < prices.length; i++) {
//可能是當前位置操作的,可也能上一次就操作了
dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
}
return Math.max(0, dp[prices.length - 1][2]);
}
}
這個題動態規劃就比較晦澀,效率不高,最好還是用貪心來
一次遍歷,如果小於最小值更新最小值,如果大於最小值假設賣出得收益,更新最大收益
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int min = Integer.MAX_VALUE;
int resMax = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < min) {
min = prices[i];
} else {
resMax = Math.max(resMax, prices[i] - min);
}
}
return resMax;
}
}
122.買賣股票的最佳時機II
動態規劃
因爲可以無限次買賣,定義兩種狀態。持有和賣出
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int dp[][] = new int[prices.length][2];
dp[0][0] = 0;//不持有
dp[0][1] = -prices[0];//持有,只能是這次買入的
for (int i = 1; i < prices.length; i++) {
//當前不持有,可能上次有這次賣了,可能上次就不持有這次不操作
dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
//當前持有,可能上次沒有這次買入的,可能上次就持有這次不操作
dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
}
return dp[prices.length - 1][0];
}
}
這個題動態規劃就比較晦澀,效率不高,最好還是用貪心來
一次遍歷,如果比前一天價格高就前一天買當天賣
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int res = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0) {
res += prices[i] - prices[i - 1];
}
}
return res;
}
}
123.買賣股票的最佳時機III
因爲最多交易兩次,狀態一共只有五種
- 一次都不交易
- 買了一次
- 買了一次 賣了一次
- 買了一次 賣了一次 買了一次
- 買了一次 賣了一次 賣了一次 賣了一次
針對這五種狀態進行規劃。
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
//二個維度:第幾天,買賣狀態
int dp[][] = new int[prices.length][5];
/**
* j = 0:還未開始交易;
* j = 1:第 1 次買入一支股票;
* j = 2:第 1 次賣出一支股票;
* j = 3:第 2 次買入一支股票;
* j = 4:第 2 次賣出一支股票。
*/
dp[0][0] = 0;
//第一次買入
dp[0][1] = -prices[0];
//第一次買入兩個,不可能
dp[0][3] = Integer.MIN_VALUE;
for (int i = 1; i < prices.length; i++) {
//可能是當前位置操作的,可也能上一次就操作了
dp[i][1] = Math.max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2]);
dp[i][3] = Math.max(dp[i - 1][2] - prices[i], dp[i - 1][3]);
dp[i][4] = Math.max(dp[i - 1][3] + prices[i], dp[i - 1][4]);
}
return Math.max(0, Math.max(dp[prices.length - 1][2], dp[prices.length - 1][4]));
}
}
188.買賣股票的最佳時機IV
當k是一個變量時,我們簡單的使用二維枚舉狀態就不夠用了,所以需要在問題1和問題3的基礎上升級到三維
三個維度:
- 第i天
- 第j次交易
- 是否持有股票
如果只是簡單的把問題1和問題3的代碼拿去升維然後循環弄,會發現超時過不了
再次觀察,如果k大於天數的一半
,說明最大次數滿足無限買賣,問題退化成問題2
了
class Solution {
public int maxProfit(int k, int[] prices) {
if (prices.length < 2 || k < 1) return 0;
//k比prices.length的一半大,說明可以轉化爲問題2,無限次交易
if (k > prices.length >> 1) {
return maxProfit(prices);
}
//三個維度:第幾天、交易次數、是否持有股票
int dp[][][] = new int[prices.length][k + 1][2];
//初始化第一天
for (int i = 1; i < k + 1; i++) {
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < prices.length; i++) {
for (int j = 1; j <= k; j++) {
dp[i][j][0] = Math.max(dp[i - 1][j][1] + prices[i], dp[i - 1][j][0]);
dp[i][j][1] = Math.max(dp[i - 1][j - 1][0] - prices[i], dp[i - 1][j][1]);
}
}
return dp[prices.length - 1][k][0];
}
//貪心實現無限次交易
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int res = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] - prices[i - 1] > 0) {
res += prices[i] - prices[i - 1];
}
}
return res;
}
}
309.最佳買賣股票時機含冷凍期
如果沒有凍結期,那就是無限買賣問題,類似於問題2,所以在問題2的持有和不持有狀態上再加一個凍結期狀態
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) return 0;
int dp[][] = new int[prices.length][3];
//初始化第一天
dp[0][0] = 0;//不持股
dp[0][1] = -prices[0];//持股
dp[0][0] = 0;//冷凍期
for (int i = 1; i < prices.length; i++) {
//今天不持股,可能是昨天就不持股或者昨天持股今天賣了
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
//今天持股,可能昨天就持股或者昨天空窗期今天買入的
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][2] - prices[i]);
//今天空窗期,說明昨天不持股
dp[i][2] = dp[i - 1][0];
}
return dp[prices.length - 1][0];
}
}
714.買賣股票的最佳時機含手續費
如果沒有手續費,那就是無限買賣問題,和問題2一樣,加上手續費後,貪心就不行了,要交易的次數儘量少利潤儘量多
採用動態規劃只需要在每次買入或者賣出時扣一次手續費就可以了
代碼和思路同問題2基本一致
class Solution {
public int maxProfit(int[] prices, int fee) {
if (prices.length < 2) return 0;
int dp[][] = new int[prices.length][2];
//初始化第一天
dp[0][0] = 0;//不持有
dp[0][1] = -prices[0] - fee;//持有(一次交易扣一次手續費,所以統一在買入時扣)
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee);
}
return dp[prices.length - 1][0];
}
}
尚未分類
32.最長有效括號
很容易想到暴力法,O(N*2),對暴力進行剪枝,代碼如下:
從第一個開始枚舉起始,內存循環枚舉結尾
如果左括號+1,如果右括號-1
如果計數器小於1說明除成對括號外出現了右括號前沒有左括號,退出這一層遍歷
如果計數器值爲0,更新最大成對的數目
class Solution {
public int longestValidParentheses(String s) {
int max = 0;
int count = 0;
for (int i = 0; i < s.length()-1; i++) {
count = 0;
for (int j = i; j < s.length(); j++) {
if (s.charAt(j) == '(') count++;
else count--;
if (count < 0) break;
if (count == 0) max = Math.max(max, j - i + 1);
}
}
return max;
}
}
暴力的時間和空間複雜度都比較高,採用動態規劃進行解決:
方法過於巧妙,好不容易理解透徹記錄下
一次遍歷字符串,如果是(
就跳過,如果是)
就進行操作
-
如果
)
的前一個字符是(
,剛好組成一對,dp[i-2]+2
舉例:index = 3
時,dp[i-2] = 2,index = 2
和index = 3
組成一對,dp[3] = dp[1]+2 = 4
-
如果
)
的前一個字符不是(
,則查找除掉當前成對的字符後的前一個字符是否是(
,如果不是就放棄了,當前置爲0.如果是dp[i]的值至少爲dp[i-1[+2,如果新匹配的字符的前一個字符(i - dp[i - 1] -2)屬於上一個符合的子序列,那麼兩個子序列就可以合併成一個子序列,長度再加上前面的
舉例:index = 7時,dp[i-1]所屬的子序列爲index5到index6
,除去子序列的前一個是index = 4
,滿足條件,所以dp[7]至少爲dp[i-1]+2
,然後判斷index = 5的前一個index = 4,發現index = 4這個下標存在,就加上dp[4]的值,得到dp[7] = dp]6]+2+dp[4]
class Solution {
public int longestValidParentheses(String s) {
int max = 0;
int dp[] = new int[s.length()];
//一次遍歷字符串,如果連續成對,更新連續值,如果斷了連續,重新開始記錄
for (int i = 1; i < s.length(); i++) {
//左括號跳過,右括號開始判斷
if (s.charAt(i) == ')') {
//如果右括號的前一個就是左括號
if (s.charAt(i - 1) == '(') {
dp[i] = 2 + (i >= 2 ? dp[i - 2] : 0);
//如果去除掉當前有效段之後前的一個字符爲(,說明前後能再組成一對
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
//dp[i]的長度至少等於dp[i-1]+2,如果新插入的那個符號前一位還有符號就加入那個位置的dp值(如果那個位置屬於前一個段就加上了前一段的長度,如果是單獨的就相當於加0
dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] >= 2 ? dp[i - dp[i - 1]- 2] : 0);
}
max = Math.max(max, dp[i]);
}
}
return max;
}
}
343.整數拆分
初始化dp數組爲1
dp[i]表示i可以拆分後的最大乘積
dp[i]的值是dp[i]、拆分爲兩個數乘積最大值,拆分爲兩個數其中一個數被拆分的最大積乘另一個數的積
class Solution {
public int integerBreak(int n) {
int dp[] = new int[n + 1];
Arrays.fill(dp, 1);
for (int i = 3; i <= n; i++) {
for (int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[i - j] * j));
}
}
return dp[n];
}
}