动态规划

337 阅读6分钟

小偷问题

 * 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,
 * 影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,
 * 如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,
 能够偷窃到的最高金额。

 示例 1:

 输入: [1,2,3,1]
 输出: 4
 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
      偷窃到的最高金额 = 1 + 3 = 4 。
  • 动态规划一般都是考虑三步走
1.确定dp数组,明确dp[i]所表示的含义
2.初始化dp数组
3.转移方程
public int rob(int[] nums) {
        int n = nums.length;
        if(n == 0){
            return 0;
        }
        if(n == 1){
            return nums[0];
        }
        //确定数组的含义(截止到第i家所获得的最大利润)
        int[] dp = new int[n];
        // 初始化dp 
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0],nums[1]);
        for (int i = 2; i < n; i++) {
            // 动态转移:是否偷第i家
            dp[i] = Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[n-1];
    }

正整数的拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。
 返回你可以获得的最大乘积。
 示例 1:
 输入: 2
 输出: 1
 解释: 2 = 1 + 1, 1 × 1 = 1。
public int integerBreak(int n) {

        if(n < 1)
            throw new IllegalArgumentException("n should be greater than zero");

        int[] memo = new int[n+1];
        memo[1] = 1;
        for(int i = 2 ; i <= n ; i ++)
            // 求解memo[i]
            for(int j = 1 ; j <= i - 1 ; j ++)
                memo[i] = Math.max(memo[i],Math.max(j*(i-j),j*memo[i-j]));

        return memo[n];
    }

LCS(最长公共子序列)

求两个字符串的最长公共子序列
public String lcs(String s1, String s2){

        int m = s1.length();
        int n = s2.length();

        // memo 是 (m + 1) * (n + 1) 的动态规划表格
        // memo[i][j] 表示s1的前i个字符和s2前j个字符的最长公共子序列的长度
        // 其中memo[0][j] 表示s1取空字符串时, 和s2的前j个字符作比较
        // memo[i][0] 表示s2取空字符串时, 和s1的前i个字符作比较
        // 所以, memo[0][j] 和 memo[i][0] 均取0
        // 我们不需要对memo进行单独的边界条件处理 :-)
        int[][] memo = new int[m + 1][n + 1];

        // 动态规划的过程
        // 注意, 由于动态规划状态的转变, 下面的i和j可以取到m和n
        for(int i = 1 ; i <= m ; i ++)
            for(int j = 1 ; j <= n ; j ++)
                if(s1.charAt(i - 1) == s2.charAt(j - 1))
                    memo[i][j] = 1 + memo[i - 1][j - 1];
                else
                    memo[i][j] = Math.max(memo[i - 1][j], memo[i][j - 1]);

        // 通过memo反向求解s1和s2的最长公共子序列
        m = s1.length();
        n = s2.length();
        StringBuilder res = new StringBuilder("");
        while(m > 0 && n > 0)
            if(s1.charAt(m - 1) == s2.charAt(n - 1)){
                res.insert(0, s1.charAt(m - 1));
                m --;
                n --;
            }
            else if(memo[m - 1][n] > memo[m][n - 1])
                m --;
            else
                n --;

        return res.toString();
    }

LIS(最长上升子序列)

一个字符串递增的字符串长度
输入: [10,9,2,5,3,7,101,18]
 输出: 4
 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
/**
     *
     * @param w :重量
     * @param v :价值
     * @param C :背包容量
     * @return
     */
public int lengthOfLIS(int[] nums) {
        if(nums.length == 0){
            return 0;
        }
        // 保存截止到该数字的递增数量
        int[] dp = new int[nums.length];
        Arrays.fill(dp,1);
        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);
                }
            }

        }
        int res = 1;
        // 寻找整个数组中的
        for (int i = 0; i < nums.length; i++) {
            res = Math.max(res,dp[i]);
        }
        return res;
    }

背包问题

public int knapsack01(int[] w, int[] v, int C){

        if(w == null || v == null || w.length != v.length)
            throw new IllegalArgumentException("Invalid w or v");

        if(C < 0)
            throw new IllegalArgumentException("C must be greater or equal to zero.");

        int n = w.length;
        if(n == 0 || C == 0)
            return 0;
        // 这处的C+1可以类比链表中的虚拟头节点
        // 这样在判断首位是也可以带入后面的转移方程
        // 不然需要额外的进行判断首位
        int[][] memo = new int[n][C + 1];
        // 初始化第一行
        for(int j = 0 ; j <= C ; j ++)
            // 第一行只有一个物品,是否可以放下
            memo[0][j] = (j >= w[0] ? v[0] : 0 );

        for(int i = 1 ; i < n ; i ++)
            for(int j = 0 ; j <= C ; j ++){
                memo[i][j] = memo[i-1][j];
                if(j >= w[i])
                    // 取舍到底放不放这个w[i]
                    memo[i][j] = Math.max(memo[i][j], v[i] + memo[i - 1][j - w[i]]);
            }

        return memo[n - 1][C];
    }
    // 压缩优化解法
    public int knapsack02(int[] w, int[] v, int C){

        if(w == null || v == null || w.length != v.length)
            throw new IllegalArgumentException("Invalid w or v");

        if(C < 0)
            throw new IllegalArgumentException("C must be greater or equal to zero.");

        int n = w.length;
        if(n == 0 || C == 0)
            return 0;

        int[] memo = new int[C+1];

        for(int j = 0 ; j <= C ; j ++)
            memo[j] = (j >= w[0] ? v[0] : 0);

        for(int i = 1 ; i < n ; i ++)
            // 这里需要从后往前遍历,防止元素被覆盖
            // 因为此时的状态仅与上一次的状态有关
            // 从前往后遍历造成本次修改也会影响后序
            for(int j = C ; j >= w[i] ; j --)
                memo[j] = Math.max(memo[j], v[i] + memo[j - w[i]]);

        return memo[C];
    }

变种的背包问题(完全背包问题)

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
 注意:
 每个数组中的元素不会超过 100
 数组的大小不会超过 200
 示例 1:

 输入: [1, 5, 11, 5]

 输出: true

 解释: 数组可以分割成 [1, 5, 5] 和 [11].
  • 可以看成在sum/2的背包中是否可以正好装满数组中的部分元素
public class PatitionEqualSubsetSum {
    // 在 n 个物品中选出物品,填满sum/2的背包
    /**
     * 1、不选择 nums[i],如果在 [0, i - 1] 这个子区间内已经有一部分元素,
     * 使得它们的和为 j,那么 dp[i][j] = true;

     2、选择 nums[i],如果在 [0, i - 1] 这个子区间内就得找到一部分元素
     使得它们的和为 j - nums[i]。

     状态转移方程是:

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

     */

    public boolean canPartition(int[] nums) {
        int len = nums.length;
        if (len == 0) {
            return false;
        }

        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        // 特判:如果是奇数,就不符合要求
        if ((sum & 1) == 1) {
            return false;
        }

        int target = sum / 2;
        boolean[][] dp = new boolean[len][target+1];
        if(nums[0]<=target){
            dp[0][nums[0]] = true;
        }
        for (int i = 1; i < len; i++) {
            for(int j = 0;j<=target;j++){
                dp[i][j] = dp[i-1][j];
                // 单个元素已经满足
                if(nums[i] == j){
                    dp[i][j] = true;
                    continue;
                }
                if(nums[i]<j){
                    // 该元素要不要
                    dp[i][j] = dp[i-1][j]||dp[i-1][j-nums[i]];
                }
            }
        }
        return dp[len-1][target];


    }


    public boolean canPartition1(int[] nums) {

        int sum = 0;
        for(int i = 0 ; i < nums.length ; i ++){
            if(nums[i] <= 0)
                throw new IllegalArgumentException("numbers in nums must be greater than zero.");
            sum += nums[i];
        }

        if(sum % 2 == 1)
            return false;

        int n = nums.length;
        int C = sum / 2;

        boolean[] memo = new boolean[C + 1];
        for(int i = 0 ; i <= C ; i ++)
            memo[i] = (nums[0] == i);

        for(int i = 1 ; i < n ; i ++)
            //避免新值的干扰
            for(int j = C; j >= nums[i] ; j --)
                memo[j] = memo[j] || memo[j - nums[i]];

        return memo[C];
    }
}