动态规划小结

这类题是真的多,这里总结几个大类,还有一些小类只能靠多刷题积累经验了,做过了可能就会,没遇到过可能就真的是想不出来。

动态规划的题一般来说问得是什么设什么dp[i]就好,如果从问题中看不出来设什么那就需要将问题进行转换了,这种题一般也比较难想。

真的是太多了,少贴几个,整理有点头皮发麻。

一、斐波那切数列

这一类是比较简单和基础的一类了。

状态转方程基本是:dp[i] = dp[i-1] + dp [i-2]  i>=2 这个模型了。

70. 爬楼梯

思路:设dp[i]为爬到第i阶有多少种爬法。到第i阶可以从第i-1阶爬一步也可以从第i-2阶爬两步。

因此状态转换方程:dp[i] = dp[i-1] + dp[i-2]

class Solution {
    public int climbStairs(int n) {
        int dp_i1 = 1;
        int dp_i2 = 2;
        int dp_i = 0;
        if(n == 1) return 1;
        if(n == 2) return 2;
        for(int i = 3; i <= n ; i++) {
            dp_i = dp_i1 + dp_i2;
            dp_i1 = dp_i2;
            dp_i2 = dp_i;
        }
        return dp_i;
    }
}

题目:有N个信和信封,它们被打乱,求错误装信方式的数量。

思路:设dp[n]为n封信的错误装信方式数量。

情况①第n封信错误的装在了1~n-1中的k位置,而k位置的信又正好装在n位置上,则有(n-1)*dp[n-2]

情况②第n封信错误的装在了1~n-1中的k位置,但是k位置的信不可以装在n位置上,则有(n-1)*dp[n-1]

状态转换方程:dp[n] = (n-1)*(dp[n-1] + dp[n-2])

二、矩阵路径

这类题目和走台阶的比较类似

设dp[i][j]为从开始点走到(i,j)点有几种走法。

情况①:边界位置只能够从左或者上其中一个方向走来来,故有dp[i][j]  = 1;

情况②:中间位置可以从左或者上两个方向走来,故有dp[i][j] = dp[i-1][j] + dp[i][j-1];

62. 不同路径

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(i == 0 || j == 0) {
                    dp[i][j] = 1;
                }else {
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }
            }
        }
        return dp[m-1][n-1];
    }
}

64. 最小路径和

class Solution {
    public int minPathSum(int[][] grid) {
        int[][] dp = new int[grid.length][grid[0].length];
        for(int i = 0; i < grid.length; i++) {
            for(int j = 0; j < grid[0].length; j++) {
                if(i == 0 && j == 0) {
                    dp[0][0] = grid[i][j];
                    continue;
                }
                if(i == 0) {
                    dp[i][j] = dp[i][j-1] + grid[i][j];
                    continue;
                }
                if(j == 0) {
                    dp[i][j] = dp[i-1][j] + grid[i][j];
                    continue;
                }
                dp[i][j] = Math.min(dp[i][j-1],dp[i-1][j]) + grid[i][j];
            }
        }
        return dp[grid.length-1][grid[0].length-1];
    }
}

三、各种序列问题

碰到序列问题首选动态规划,动态规划解决序列问题简直不要太好用。

413. 等差数列划分

设dp[i]为以i为结尾的等差子数组个数

if(A[i] - A[i-1] == A[i-1] - A[i-2]) dp[i] = dp[i-1] + 1;

如果i能够加到以i-1为等差子数组的序列中,则只是比前者多一个长度+1的情况

class Solution {
    //动态规划定义dp_i为以i为结尾的等差数列个数
    private int sum = 0;
    public int numberOfArithmeticSlices(int[] A) {
        count(A,0);
        return sum;
    }
    //递归计算以i为开始的等差数列
    private int count(int[] A, int i) {
        if(A.length - i < 3) return 0;
        else {
            int count = 0;
            if(A[i] - A[i+1] == A[i+1] - A[i+2]) {
                count = 1 + count(A,i+1);
                sum = sum + count;
            }else {
                count(A,i+1);
            }
            return count;
        }
    }
}

91. 解码方法

这个题和上一个比较相似,但是处理边界有点棘手。

设dp[i]为以下标i结尾的字符串的解码方式

s[i]同s[i-1]组不成26以内的数字dp[i] = dp[i-1]

s[i]同s[i-1]可以组成26以内的数字dp[i] = dp[i-1] + dp[i-2](好像斐波那契哦)

class Solution {
    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];
    }
}

最长递增子序列

300. 最长上升子序列

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length <= 1) return nums.length;
        int dp[] = new int[nums.length];
        Arrays.fill(dp,1);
        int ans = 0;
        for(int i = 1; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                if(nums[j] < nums[i]) dp[i] = Math.max(dp[i],dp[j]+1);
            }
            ans = Math.max(ans,dp[i]);
        }
        return ans;
    }
}

最长公共子序列

设dp[i][j]为以i,j为结尾的字符串的最长公共子序列。

情况①:Si = Sj dp[i][j] = dp[i-1][j-1] + 1;

情况②:Si != Sj dp[i][j] = max{dp[i-1][j], dp[i][j-1]} 要么i不在公共子序列中,要么j不在公共子序列中

583. 两个字符串的删除操作

class Solution {
    public int minDistance(String word1, String word2) {
        int l1 = word1.length();
        int l2 = word2.length();
        int[][] dp = new int[l1+1][l2+1];
        for(int i = 1; i <= l1; i++) 
            dp[i][0] = i;
        for(int i = 1; i <= l2; i++)
            dp[0][i] = i;
        for(int i = 1; i <= l1; i++) {
            for(int j = 1; j <= l2; 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,dp[i][j-1]+1);
            }
        }
        return dp[l1][l2];
    }
}

注意与最长公共子字符串的区分

设dp[i][j]为以i,j为结尾的最长公共子串长度

情况①:Si = Sj dp[i][j] = dp[i-1][j-1] + 1;

情况②:Si != Sj dp[i][j] = 0 说明不存在以i,j为结尾的公共子串

ans = max{dp[0][0] ~ dp[i][j]}

class Solution {
    public int findLength(int[] A, int[] B) {
        int m = A.length;
        int n = B.length;
        int max = 0;
        int[][] dp = new int[m+1][n+1];
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) {
                if(A[i-1] == B[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
                else dp[i][j] = 0;
                max = Math.max(max,dp[i][j]);
            }
        }
        return max;
    }
}

 四、整数分割

 343. 整数拆分

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

279. 完全平方数

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n+1];
        dp[0] = 0;
        dp[1] = 1;
        for(int i = 2; i <= n; i++) {
            int m = (int)Math.pow(i,0.5);
            int min = Integer.MAX_VALUE;
            for(int j = 1; j <= m; j++) {
                int temp = i - j*j;
                if(dp[temp] < min) min = dp[temp];
            }
            dp[i] = min + 1;
        }
        return dp[n];
    }
}

 五、背包问题

(1)0/1背包

问题:描述有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。

设dp[i][j]在j的空间里装前i个物品所达到的最大价值。

情况①:不装第i件物品,dp[i][j] = dp[i-1][j]

情况②:装第i件物品,那么就需要付出相应体积的代价 dp[i][j] = dp[i-1][j - w] + v

综上 dp[i][j] = max{dp[i-1][j], dp[i-1][j - w] + v}

这里有个需要特别注意的地方,我们这样去设置其实是默认了先装第1,2,3 ....i件物品,即装入物品有序。

416. 分割等和子集

还是老样子问什么设什么就好

设dp[i][j]装前i件物品在j的空间里是否能恰好装满。

dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]]

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int temp : nums) {
            sum = sum + temp;
        }
        int c = 0;
        if(sum % 2 == 1) return false;
        else c = sum/2;
        boolean[] dp = new boolean[c+1];
        dp[0] = true;
        for(int i = 0; i < nums.length; i++) {
            for(int j = c; j >= nums[i]; j--) {
                dp[j] = dp[j] || dp[j-nums[i]];
            }
        }
        return dp[c];
    }
}

494. 目标和

Sum正 + Sum负 = S

Sum + Sum正 + Sum负 = S + Sum

2Sum = S + Sum

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int sum = 0;
        for(int num : nums) {
            sum+=num;
        }
        if((S+sum)%2==1 || S > sum) return 0;
        int target = (S+sum)/2;
        int[][] dp = new int[nums.length+1][target+1];
        dp[0][0] = 1;
        for(int i = 1; i < nums.length+1; i++) {
            dp[i][0] = 1;
            for(int j = 1; j < target+1; j++) {
                if(j >= nums[i-1]) {
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
                }else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[nums.length][target];
    }
}

(2)多维背包

多维背包问题不但有体积的限制而且还有重量的限制,解决问题的方式同0/1背包只是dp数组多加一维罢了

474. 一和零

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        if(strs.length == 0) return 0;
        if(m < 0 || n < 0) return 0;
        int[][] dp = new int[m+1][n+1];
        for(String str : strs) {
            for(int i = m; i >= count0(str); i--) {
                for(int j = n; j >= count1(str); j--) {
                    dp[i][j] = Math.max(dp[i][j],dp[i-count0(str)][j-count1(str)] + 1);
                }
            }
        }
        return dp[m][n];
    }
    private int count0(String s) {
        int count = 0;
        for(int i = 0; i < s.length(); i++) {
            if(s.charAt(i) == '0') count++;
        }
        return count;
    }
    private int count1(String s) {
        int count = 0;
        for(int i = 0; i < s.length(); i++) {
            if(s.charAt(i) == '1') count++;
        }
        return count;
    }
}

 (3)完全背包问题

完全背包问题的物品可以重复,只要稍微改一下0/1背包即可,但是注意这样装入仍然是有序的

dp[i][j] = max{dp[i-1][j], dp[i][j - w] + v}

我们改了一下方程,即表示装过i之后还可以装i。就是把0/1背包的反向遍历背包空间改成正向遍历

322. 零钱兑换

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 coin : coins) {
            for(int j = coin; j <= amount; j++) {
                dp[j] = Math.min(dp[j],dp[j-coin]+1);
            }
        }
        return dp[amount] == amount+1 ? -1 : dp[amount];
    }
}

518. 零钱兑换 II

class Solution {
    public int change(int amount, int[] coins) {
        if(amount < 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] + dp[i-coin];
            }
        }
        return dp[amount];
    }
}

377. 组合总和 Ⅳ

注意观察这道题和上题的区别,这道题包含重复的解,而上一题不包含重复解,我们上面说方法的装入是有序的,所以是不包含重复解的。

这里我们来介绍另一种方式。

f(n) = ∑f(n - nums[i])   n ≥ nums[i] 

class Solution {
    public int combinationSum4(int[] nums, int target) {
        if(target < 0) return 0;
        int[] dp = new int[target+1];
        dp[0] = 1;
        for(int i = 1; i <= target; i++) {
            int sum = 0;
            for(int num : nums) {
                if(num <= i) {
                    sum = sum + dp[i-num];
                }
            }
            dp[i] = sum;
        }
        return dp[target];
    }
}

139. 单词拆分

这题同样要求装入的方式无序

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length()+1];
        dp[0] = true;
        for(int i = 1; i <= s.length(); i++) {
            for(String word : wordDict) {
                if(i>=word.length())
                    if(s.substring(i-word.length(),i).equals(word))
                        dp[i] = dp[i] || dp[i-word.length()];
            }
        }
        return dp[s.length()];
    }
}

六、强盗问题

强盗问题也是一类0/1抉择问题

198. 打家劫舍

设dp[i][0]表示第i间不偷的最大利益,则有 dp[i][0] = max{dp[i-1][1], dp[i-1][0]}

设dp[i][1]表示第i间偷的最大利益,则有 dp[i][1] = dp[i-1][0] + nums[i]

class Solution {
    public int rob(int[] nums) {
        if(nums.length == 0) return 0;
        if(nums.length == 1) return nums[0];
        int dp_i_0 = 0;
        int dp_i_1 = nums[0];
        int predp_i_0 = dp_i_0;
        int predp_i_1 = dp_i_1;
        for(int i = 1; i < nums.length; i++) {
            dp_i_0 = Math.max(predp_i_0,predp_i_1);
            dp_i_1 = predp_i_0 + nums[i];
            predp_i_0 = dp_i_0;
            predp_i_1 = dp_i_1;
        }
        return Math.max(dp_i_1,dp_i_0);
    }
}

213. 打家劫舍 II

开头和结尾肯定有一家不能偷,我们假设开头不能偷或者结尾不能偷然后就变成了两个线性的问题。

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]);
        return Math.max(helprob(nums,0,nums.length-2),helprob(nums,1,nums.length-1));
    }
    
    private int helprob(int[] nums, int start, int end) {
        int length = end - start + 1;
        if(length == 0) return 0;
        if(length == 1) return nums[start];
        int dp_i_0 = 0;
        int predp_i_0 = dp_i_0;
        int dp_i_1 = nums[start];
        int predp_i_1 = dp_i_1;
        for(int i = start + 1; i <= end; i++) {
            dp_i_0 = Math.max(predp_i_0,predp_i_1);
            dp_i_1 = predp_i_0 + nums[i];
            predp_i_0 = dp_i_0;
            predp_i_1 = dp_i_1;
        }
        return Math.max(dp_i_0,dp_i_1);
    }
}

 七、股票问题

 121. 买卖股票的最佳时机 (只能够做一次交易)

设dp[i][0]表示第i天不持有股票的最大利益,dp[i][1]表示第i天持有股票的最大利益。

dp[i][0] = max{dp[i-1][0], dp[i-1][1] + prices[i]}

dp[i][1] = max{dp[i-1][1], -prices[i]} (-prices[i]就表示今天是第一笔交易)

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length <= 1) return 0;
        int dp_i_0 = 0;
        int dp_i_1 = - prices[0];
        int predp_i_0 = dp_i_0;
        int predp_i_1 = dp_i_1;
        for(int i = 1; i < prices.length; i++) {
            dp_i_0 = Math.max(predp_i_0,predp_i_1 + prices[i]);
            dp_i_1 = Math.max(predp_i_1,-prices[i]);
            predp_i_0 = dp_i_0;
            predp_i_1 = dp_i_1;
        }
        return dp_i_0;
    }
}

122. 买卖股票的最佳时机 II(可以多次买卖)

 dp[i][0] = max{dp[i-1][0], dp[i-1][1] + prices[i]}

 dp[i][1] = max{dp[i-1][1], dp[i-1][0] - prices[i]}

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length <= 1) return 0;
        int dp_i_0 = 0;
        int dp_i_1 = -prices[0];
        int predp_i_0 = dp_i_0;
        int predp_i_1 = dp_i_1;
        for(int i = 1; i < prices.length; i++) {
            dp_i_0 = Math.max(predp_i_0,predp_i_1 + prices[i]);
            dp_i_1 = Math.max(predp_i_1,predp_i_0 - prices[i]);
            predp_i_0 = dp_i_0;
            predp_i_1 = dp_i_1;
        }
        return dp_i_0;
    }
}

 123. 买卖股票的最佳时机 III(只能做两次交易)

这里我们多加一维变量来体现两次

dp[i][k][0] = max{dp[i-1][k][0], dp[i-1][k][1] + prices[i]}

dp[i][k][1] = max{dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]}

188. 买卖股票的最佳时机 IV (可以做K笔交易)

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        if(n <= 1 || k <= 0) return 0;
        if(k >= 2*n) return f(prices);
        int[][][] dp = new int[n][k+1][2];
        for(int i = 0; i < n; i++) {
            for(int j = 1; j <= k; j++) {
                if(i == 0) dp[i][j][1] = -prices[i];
                else {
                    dp[i][j][1] = Math.max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i]);
                    dp[i][j][0] = Math.max(dp[i-1][j][0],dp[i-1][j][1]+prices[i]);
                }
            }
        }
        return dp[n-1][k][0];
    }
    private int f(int[] prices) {
        int[][] dp = new int[prices.length+1][2];
        dp[1][1] = -prices[0];
        for(int i = 2; i < prices.length+1; i++) {
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i-1]);
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i-1]);
        }
        return dp[prices.length][0];
    }
}

309. 最佳买卖股票时机含冷冻期

dp[i][0] = max {dp[i-1][0], dp[i-1][1] + prices[i]}

dp[i][1] = max {dp[i-1][1], dp[i-2][0] - prices[i]} 表示今天不持股票可能是持有昨天持有的,也可能是昨天在冷冻期前天没有股票今天买入了

这是四种不同情况每一天只可能发生其中一种,不要去相互关联他们

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        if(n <= 1) return 0;
        int[] dp_0 = new int[n+1];
        int[] dp_1 = new int[n+1];
        dp_1[1] = -prices[0];
        for(int i = 2; i <= n; i++) {
            dp_1[i] = Math.max(dp_1[i-1],dp_0[i-2]-prices[i-1]);
            dp_0[i] = Math.max(dp_0[i-1],dp_1[i-1]+prices[i-1]);
        }
        return dp_0[n];
    }
}

714. 买卖股票的最佳时机含手续费

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        if(n<=1) return 0;
        int[] dp_0 = new int[n+1];
        int[] dp_1 = new int[n+1];
        dp_1[1] = -prices[0];
        for(int i = 2; i <= n; i++) {
            dp_1[i] = Math.max(dp_1[i-1],dp_0[i-1]-prices[i-1]);
            dp_0[i] = Math.max(dp_0[i-1],dp_1[i-1]+prices[i-1]-fee);
        }
        return dp_0[n];
    }
}

 

 

posted @ 2019-12-02 13:29  卑微芒果  Views(274)  Comments(0Edit  收藏  举报