【刷题】动态规划

系列汇总:《刷题系列汇总》



——————《剑指offeer》———————

1. 跳台阶扩展问题

  • 题目描述:一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
  • 我的思路:递归
public class Solution {
    int num = 0;
    public int jumpFloorII(int target) {
        for(int i = 1;i <= target;i++){
            jump(i,target);
        }
        return num;
    }
    private void jump(int step,int rest){
        if(step == rest){
            num++;
            return;
        }
        rest -= step;
        for(int i = 1;i <= rest;i++){
            jump(i,rest);
        }
    }
}
  • 优秀思路:【动态规划】若f(n)表示青蛙跳到第n级台阶可能的跳法数量,则
    在这里插入图片描述
public class Solution {
    public int jumpFloorII(int target) {
        if (target <= 0) return 0;
        return (int) Math.pow(2, target - 1);
    }
}

2. 矩形覆盖

  • 题目描述:我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n2*1的小矩形无重叠地覆盖一个2*n的大矩形,从同一个方向看总共有多少种不同的方法?
  • 优秀思路:【动态规划】配合图形理解。① 首先明确 n<=3时,摆放方式刚好是n种;② 结合图形理解其他情况

若第一次摆放一个2*1的块(竖着放),则后续的摆放方式有f(n-1)种(少了1列);
在这里插入图片描述
若第一次摆放一个1*2的块(横着放),则后续的摆放方式有f(n-2)种(摆了上面下面的摆法也定了,故少了2列);
在这里插入图片描述
所以 f(n) = f(n-1)+f(n-2)

public class Solution {
    public int rectCover(int target) {
        if(target <= 3) return target;
        return rectCover(target-1)+rectCover(target-2);
    }
}

——————《LeectCode》———————

1. 跳跃游戏 II

  • 题目描述:给你一个非负整数数组 nums ,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。假设你总是可以到达数组的最后一个位置。
    在这里插入图片描述
  • 我的思路(超时):正向递归
class Solution {
    int res = Integer.MAX_VALUE;
    public int jump(int[] nums) {
        if(nums.length == 1) return 0;
        helper(nums,0,0);
        return res;
    }
    /**
    cur: 当前位置
    nStep:当前步数
     */
    private void helper(int[] nums,int cur,int nStep){
        if(cur >= nums.length) return; // 超出范围
        if(cur == nums.length-1){ // 到达末端
            res = Math.min(res,nStep);
            return;
        }
        if(nums[cur] == 0) return; //走不动了
        for(int i = 1;i <= nums[cur];i++){
            helper(nums,cur+i,nStep+1);
        }
    }   
}
  • 优秀思路1(21%):【反向贪心】从结束点开始,每次反向找到上一个可以跳跃到当前点的距离最长的那个点,为了保证距离最长,故每轮从左往右遍历找到即停止
class Solution {
   public int jump(int[] nums) {
        if (nums.length <= 1) return 0;
        // 贪心策略:反向找到每次跳跃距离最长的那个点(正向跳不能保证一定能跳到终点,所以要搜索很多次)
        int position = nums.length-1;
        int res = 0;
        while(position > 0){ 
            for(int i = 0;i < position;i++){ // 从左往右遍历,搜索到可以跳跃到position的点即停止
                if(nums[i] >= position-i){
                    position = i;
                    res++;
                    break;
                }
            }
        }
        return res;
    }
}
  • 优秀思路2:【正向贪心】正向查找,每次找到可到达的最远位置。实现很巧妙,维护当前能够到达的最大下标位置,记为边界。我们从左到右遍历数组,到达边界时,更新边界并将跳跃次数增加 1。
class Solution {
    public int jump(int[] nums) {
        int length = nums.length;
        int end = 0; // 当前点所能达到的所有点中所能跳达的最远边界
        int maxPosition = 0; 
        int steps = 0;
        for (int i = 0; i < length - 1; i++) {
            // 记录当前遍历点的所能达到最远的下一位置
            maxPosition = Math.max(maxPosition, i + nums[i]); 
            // 小于end的所有点都是上一个点所能跳跃到的地方
            if (i == end) { // 遇到上一个点的边界时,步数加1,更新边界
                end = maxPosition;
                steps++;
            }
        }
        return steps;
    }
}

2. 爬楼梯

  • 题目描述:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。
  • 优秀思路1:【动态规划】两种方式,①(100%)建立dp数组存储所有时刻的状态;② **(100%)**本题中由于f(n) = f(n-1)+f(n-2),故可以利用滚动数组的方式,用两个变量记录前两个状态,用于计算后一状态
class Solution {
    public int climbStairs(int n) {
        if(n == 1) return 1;
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; ++i) {
            dp[i] = dp[i-1]+dp[i-2]; // 数组自带记忆功能
        }
        return dp[n];
    }
}
class Solution {
    public int climbStairs(int n) {
        int lastOfLast = 0, last = 0, cur = 1;
        for (int i = 1; i <= n; ++i) {
            // 滚动数组的方式记录 n-1 和 n-2 时的状态
            lastOfLast = last; 
            last = cur; 
            cur = lastOfLast + last;
        }
        return cur;
    }
}

3. 编辑距离(需重写)

  • 题目描述:给你两个单词 word1word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:
    • 插入一个字符
    • 删除一个字符
    • 替换一个字符
  • 优秀思路:【动态规划】分析过程如下
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
class Solution {
    public int minDistance(String word1, String word2) {
        int n = word1.length();
        int m = word2.length();

        // 有一个字符串为空串
        if (n * m == 0) {
            return n + m;
        }

        // DP 数组
        // D[i][j] 表示 A 的前 i 个字母和 B 的前 j 个字母之间的编辑距离。
        int[][] dp = new int[n + 1][m + 1];

        // 边界状态初始化
        for (int i = 0; i < n + 1; i++) { // 此时A与B只存在纯删除A / 纯插入B
            dp[i][0] = i;
        }
        for (int j = 0; j < m + 1; j++) { // 此时A与B只存在纯删除B / 纯插入A
            dp[0][j] = j;
        }

        // 计算所有 DP 值
        for (int i = 1; i < n + 1; i++) {
            for (int j = 1; j < m + 1; j++) {
                int nDelete = dp[i - 1][j] + 1; // 表示删除操作
                int nInsert = dp[i][j - 1] + 1; // 表示插入操作
                int nReplace = dp[i - 1][j - 1]; // 表示替换操作
                if (word1.charAt(i - 1) != word2.charAt(j - 1)) { // 最后一位不相同时,替换操作增加1
                    nReplace += 1;
                }
                dp[i][j] = Math.min(nDelete, Math.min(nInsert, nReplace));
            }
        }
        return dp[n][m];
    }
}

4. 分割回文串

  • 题目描述:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。回文串 是正着读和反着读都一样的字符串。在这里插入图片描述
  • 我的思路:hashmap+回溯,相似思路见我的题解
    在这里插入图片描述
class Solution {
    List<List<String>> res;
    public List<List<String>> partition(String s) {
        res = new ArrayList<>();
        if(s == null) return res;

        // 1.找出所有的回文子串下标组合
        char[] arr = s.toCharArray();
        Map<Integer,ArrayList<Integer>> map = new HashMap<>();
        for(int i = 0;i < arr.length;i++){
            ArrayList<Integer> list = new ArrayList<>();
            for(int j = i;j < arr.length;j++){
                if(judge(arr,i,j)) list.add(j);
            }
            map.put(i,list);
        }

        // 2.寻找所有的分割组合
        find(s,map,new ArrayList<String>(),0);
        return res;

    }

    // 判断是否为回文子串
    private boolean judge(char[] arr,int start,int end){
        while(start < end){
            if(arr[start] != arr[end]) return false;
            start++;
            end--;
        }
        return true;
    }

    // 递归寻找所有的分割组合
    private void find(String s,Map<Integer,ArrayList<Integer>> map,ArrayList<String> tempRes,int cur){
        if(cur >= s.length()){
            res.add(new ArrayList<>(tempRes)); 
            return;
        }
        for(int i:map.get(cur)){
            tempRes.add(s.substring(cur,i+1));
            find(s,map,tempRes,i+1);
            tempRes.remove(tempRes.size()-1); // 恢复原状态
        }
    }
}
  • 优秀思路:【动态规划,待补充】

5. 二叉树中的最大路径和(需重写)

  • 题目描述:路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。路径和 是路径中各节点值的总和。给你一个二叉树的根节点 root ,返回其 最大路径和 。
  • 优秀思路:【递归】定义某节点的最大贡献值为“该节点值 + max(左子树贡献值,右子树贡献值)”,
    递归计算左右子节点的最大贡献值+本节点值进行返回,注意在最大贡献值大于 0 时,才会选取对应子节点。
class Solution {
    int maxSum = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }
    //
    public int maxGain(TreeNode node) {
        if (node == null) {
            return 0;
        }
        // 递归计算左右子节点的最大贡献值
        // 只有在最大贡献值大于 0 时,才会选取对应子节点
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);
        // 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        int priceNewpath = node.val + leftGain + rightGain;
        // 更新答案
        maxSum = Math.max(maxSum, priceNewpath);
        // 返回节点的最大贡献值
        return node.val + Math.max(leftGain, rightGain);
    }
}

6. 最长公共子序列

  • 题目描述:给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的子序列:是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 优秀思路:【动态规划】

    • 状态定义:定义 dp[i][j] 表示 text1[0:i-1] 和 text2[0:j-1] 的最长公共子序列。 (注:text1[0:i-1] 表示的是 text1 的 第 0 个元素到第 i - 1 个元素,两端都包含)之所以 dp[i][j] 的定义不是 text1[0:i] 和 text2[0:j] ,是为了方便当 i = 0 或者 j = 0 的时候,dp[i][j]表示的为空字符串和另外一个字符串的匹配,这样 dp[i][j] 可以初始化为 0.
    • 状态转移方程:在这里插入图片描述在这里插入图片描述
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int M = text1.length();
        int N = text2.length();
        int[][] dp = new int[M + 1][N + 1];
        // 下标从1开始,自然地,i=0和j=0对应的所有dp都为0
        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] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[M][N];
    }
}

7. 322 300 198 139 53

  • 题目描述:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。

  • 优秀思路


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值