【Java数据结构与算法】动态规划从入门到入坟,思路、方法、技巧(二)

动态规划入门见:【Java数据结构与算法】动态规划从入门到入坟,思路、方法、技巧(一)
此篇主要为中等难度动态规划。

字符串动态规划

涉及字符串匹配子串问题可以优先考虑动态规划

交错字符串

题目: 给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。

在这里插入图片描述

输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出:true
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出:false

题目分析: s1 = “dbbca”,s2 = “aabcc”, s3 = “aadbbcbcac”,判断s3 是否由s1、s2交错组成。
在这里插入图片描述

dp[i][j] 表示 s1[0~i] s2[0~j] 能否交错等于 s3[0~ i + j]。
左上角一定为T,如果较短的s1 和 s2 不能组成s3前几个元素, 那么只加长s1的话依然不行,s2 同理。
所有先初始化第一行,第一列。前面为T,且第i位和s3相同则为T ,否则为F
dp[i][j] = dp[i - 1][j] && s1[i] = s3[i + j]   或
dp[i][j] =dp[i][j - 1] && s2[j] = s3[i + j]

解题思路:

  1. dp[i][j] 表示 s1[0~i] 和 s2[0~j] 能否组成s3[0, i + j ]
  2. 递推公式:dp[i][j] = (dp[i][j] = dp[i - 1][j] && s1[i] ==s3[i + j] ) || (dp[i][j - 1] && s2[j] == s3[i + j] )
  3. 初始化, 初始化dp[0][j] 和dp[i][0] ,dp[0][0] = T
  4. 从左至右
  5. dp举例如图所示

Java代码

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int len1 = s1.length();
        int len2 = s2.length();
        if (len2 + len1 != s3.length()) return false;
        //dp[i][j] 表示 s1[0~i]   s2[0~j] 能否交错等于 s3[0~ i + j]。
        boolean[][] dp = new boolean[len1 + 1][len2 + 1];
        //初始化 dp[0][j]
        dp[0][0] = true;
        for(int i = 1; i <= len1; i++) {
            dp[i][0] = dp[i - 1][0] && s1.charAt(i - 1) == s3.charAt(i - 1);
        }
        //初始化 dp[i][1]
        for(int j = 1; j <= len2; j++) {
            dp[0][j] = dp[0][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1);
        }
        
        for(int i = 1; i <= len1; i++) {
            for(int j = 1; j <= len2; j++) {
                dp[i][j] = (dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) ||
                (dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1)) ;
            }
        }
        return dp[len1][len2]; 
    }
}

最长回文子串

题目: 给你一个字符串 s,找到 s 中最长的回文子串。

题目分析:
方法一:可以采用中心扩散法,以字符为中心,或者以间隙为中心,往两边扩散找回文串,返回最长的。
方法二, 当 s[i ~ j] 是回文串的时候, s[i + 1 ~ j - 1]也一定是回文串。所以可以用动态规划。

解题思路
  1. dp[i][j] dp[i][j] 表示s[i ~ j] 是不是回文串
  2. dp[i][j] = dp[i + 1]dp[j - 1] && s[i] == s[j]
  3. 初始化: dp[i][i] = T , 单个字符一定是回文串
  4. 遍历顺序: 先知道较短的字符串是不是回文串,才能推出较长的回文串,所以得按子串长度由小到大遍历,
    也可以按照先知道较大的 i 和较小的 j ,i从大到小遍历,j从小到大遍历,当然j > i
Java代码
class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        char[] arr = s.toCharArray();
        boolean[][] dp = new boolean[n][n];
        //初始化长度为1的字符串dp
        for(int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        int bagin = 0;
        int maxLen = 1;//最小最长回文串长度为1
        //按长度由小到大的顺序遍历
        for(int L = 2; L <= n; L++) {
            //枚举左边界,再配合长度L可以计算出有边界
            for(int i = 0; i < n; i++) {
                int j = i + L - 1;
                if(j >= n) break;
                if(arr[i] == arr[j]) {
                    if(L == 2) {
                        dp[i][j] = true;
                    }else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }
                if(dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    bagin = i;
                }
            }
        }
        return s.substring(bagin, bagin + maxLen);
    }
}

分割回文串

题目:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]

题目: 听要求返回所有分割方案, 那就得枚举出所有回文串分割方案。采用递归回溯的方式遍历所有情况。
遍历到不是回文串的时候得进行剪枝。可以每次都用双指针的方法判断当前枚举的是不是回文串。但是这样肯定产生很多重复计算。所有现有动态规划预处理,得到所有子串是不是回文串的dp数组(参考上一题)。
在这里插入图片描述

解题思路:

  1. 使用动态规划预处理字符串,得到每个子串是否为回文串。
  2. 使用递归+回溯(类似深度优先搜索)遍历所有情况。是回文串就加入 list, 到最后得到一种分割顺序,如果不是回文串就回溯。

Java代码

class Solution {
    List<List<String>> ret = new ArrayList<>();
    public List<List<String>> partition(String s) {
        //动态规划预处理 dp[i][j] 表示字符串s[i - j] 是不是回文串
        int n = s.length();
        char[] arr = s.toCharArray();
        boolean[][] dp = new boolean[n][n];
        for(int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        for(int L = 2; L <= n; L++) {
            for(int i = 0; i < n; i++) {
                int j = i + L - 1;
                if(j >= n) break;
                if(arr[i] == arr[j]) {
                    if(L == 2) {
                        dp[i][j] = true;
                    }else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }
            }
        }
        //回溯
        List<String> list = new ArrayList<>();
        dfs(s, 0, n, dp, list);
        return ret;
    }

    void dfs(String s, int start, int n, boolean[][] dp, List<String> list ) {
        if(start == n) {
            ret.add(new ArrayList<>(list));
            return;
        }

        for(int i = start; i < n; i++) {
            if(dp[start][i]) {
                list.add(s.substring(start, i + 1));
                dfs(s,i + 1,n,dp,list);
                list.remove(list.size() - 1);
            }
        }
    }
}

数组动态规划

最大子数组和

题目:给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

题目分析: 对于第 i 个数字, 如果我们知道 以 第 i - 1 个数字为结尾的连续最大和大于0时, 那么以第 i个数字结尾的 连续最大和 就可以加上前面的。如果小于0就不加,只算自己 。 这样我们就能得到每个以每个数字结尾的最大和。返回最大的即可

class Solution {
    public int maxSubArray(int[] nums) {
        int dp = 0;
        int max = nums[0];
        for(int i = 0; i < nums.length; i++) {
            if(dp > 0) {
                nums[i] += dp;
            }
            if(nums[i] > max) {
                max = nums[i];
            }
            dp = nums[i];
        }
        return max;
    }
}

乘积最大子数组

题目:给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

输入: [2,3,-2,4]
输出: 6    解释: 子数组 [2,3] 有最大乘积 6。

题目分析: 求连续数组的最大乘积,和最大子数组和相比,这道题要求的是乘积。 数组中包含负数和0。显然遇见0,会使得乘积变为0
遇到负数,可能会暂时使得乘积编为负数,但是如果能再来一个负数就能变回来,所以也得保存。
[3,-1,4] -->4
[2,-5,-2,-4,3] --> 24
碰到正数,得乘前面为正数的最大乘积
碰到负数得乘前面为负数得最大乘积
所以用两个dp数组,一个保存最大值,一个保存最小值,最小值专门保存前面能给的最小的数(一般为负数),最大值专门保存前面能给的最大的数(一般为正数)。这样遇见正负数都能得到最大的数。
举例
[3,-1,4] -->[3,-3,-4] [3,-1,4]
[2,-5,-2,-4,3] --> [2,-10,-2,-4,-12] [2, -5, 20,8,24]

Java代码

class Solution {
    public int maxProduct(int[] nums) {
        int max = nums[0];
        int[] maxDp = new int[nums.length];
        int[] minDp = new int[nums.length];
        maxDp[0] = nums[0];
        minDp[0] = nums[0];
        for(int i = 1; i < nums.length; i++) {
            minDp[i] = Math.min(maxDp[i - 1] * nums[i], Math.min(minDp[i - 1] * nums[i], nums[i]));
            maxDp[i] = Math.max(maxDp[i - 1] * nums[i], Math.max(minDp[i - 1] * nums[i], nums[i]));
            if(maxDp[i] > max) {
                max = maxDp[i];
            }
        }
        return max;
    }
}

代码优化,dp优化,用两个变量代替两个dp数组。

class Solution {
    public int maxProduct(int[] nums) {
        int max = nums[0];
        int maxDp = nums[0];
        int minDp = nums[0];
        for(int i = 1; i < nums.length; i++) {
            int mx = maxDp, mn = minDp;
            minDp = Math.min(mx * nums[i], Math.min(mn * nums[i], nums[i]));
            maxDp = Math.max(mx * nums[i], Math.max(mn * nums[i], nums[i]));
            if(maxDp > max)  max = maxDp;
        }
        return max;
    }
}

打家劫舍

题目:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额

输入:[1,2,3,1]
输出:4 偷 nums[0] + nums[2] = 1 + 3 = 4

题目分析: 小偷偷钱,偷了一家不能立马偷下一家,最少隔一家。返回一遍过去偷的最多的钱数。对于nums[i] 有两种选择: 偷或者不偷
假设偷了nums[i] 就不能偷nums[i - 1] ,小偷能偷到的最大金额就是nums[i] + i-2时刻能偷到的最大金额
如果没偷nums[i], 小偷偷大的最大金额和 i - 1时刻相同。

所以用动态规划

解题思路

  1. dp[i] 表示到刚过了 第 i 家能偷到的最大金额
  2. 递推公式: 偷了第 i 家: dp[i] = nums[i] + dp[i - 2] 没偷第i家 dp[i] = dp[i - 1] , dp[i] 取两者较大的。自动分辨偷不偷第 i 家。
  3. 初始化: dp[0] = nums[0] dp[1] = max(dp[1], dp[0])。
  4. 遍历方向: 从左往右
  5. 举例dp:[1,2,3,1] --> dp[] = [1,2,4,4] 返回4

Java代码

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

代码优化:很明显,dp数组只用到了 dp[i], dp[i - 1] 和dp[i - 2] 用三个变量即可代替之。

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) return nums[0];
        if (n == 2) return Math.max(nums[0], nums[1]);
        int a = nums[0], b = Math.max(nums[0], nums[1]), c = 0;
        for(int i = 2; i < n; i++) {
            c = Math.max(a + nums[i], b);
            a = b;
            b = c;
        }
        return c;
    }
}

练习题目: 按摩师

打家劫舍 Ⅱ

题目:小偷偷钱,相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。现在所有房子是一个圈,最后一家和第一家连着。求今晚能偷的最大金额。

输入:nums = [2, 3, 2]
输出:3, 偷了第一个,不能偷第二个,也不能偷第三个, 所以最大的是偷第二个

题目分析: 变成了一个环, 可以先不考虑最后一个,这样可以得到前 n - 1个能偷得最大金额。
然后再不考虑第一个, 得到 后 n - 1个能偷到得最大值。取两者更大返回即可。

class Solution {
    public int rob(int[] nums) {
        if(nums == null || nums.length == 0) return 0;
        int len = nums.length;
        if(len == 1) return nums[0];
        if(len == 2) return Math.max(nums[0], nums[1]);
        return Math.max(rob1(nums, 0, len - 1), rob1(nums, 1, len));
    }

    public int rob1(int[] nums, int statr, int end) {
        int a = 0, b = 0, c = 0;
        for(int i = statr; i < end; i++) {
            c = Math.max(a + nums[i], b);
            a = b;
            b = c;
        }
        return c;
    }
}

打家劫舍Ⅲ

所有房屋的排列类似于一棵二叉树,今晚不能同时偷连着的房屋。求能偷的最大金额。

输入: [3,2,3,null,3,null,1]
 3
/  \
2   3
\    \
3    1
输出: 7 解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

题目分析: 偷了根节点就不能偷左右子节点, 偷了子节点也不能偷根节点
由于是二叉树,首先得知道二叉树的遍历方式:前中后序遍历(dfs)+层序遍历。(bfs)

对于一个节点,有偷和不偷两种状态,
不偷:能得得最大金额是=考虑左子节点偷的最大金额 + 考虑右子节点偷的最大金额。(考虑不代表一定偷,如果不偷钱更多就不偷。)
偷:左子节点不偷,右子节点不偷 的最大金额。

由于得先知道子节点偷与不偷对应金额,所以得对二叉树进行后续遍历,并保存每个节点偷不偷对应得状态。
在这里插入图片描述

class Solution {
    public int rob(TreeNode root) {
        int[] ret =  dfs(root);
        return Math.max(ret[0], ret[1]);
    }

    public int[] dfs(TreeNode root) {
        int ret[] = new int[2];
        if(root == null) return ret;
        int[] left = dfs(root.left);
        int[] right = dfs(root.right);

        ret[0] = root.val + left[1] + right[1];
        ret[1] = Math.max(left[1], left[0]) + Math.max(right[1], right[0]);
        return ret;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

甲 烷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值