动态规划&回溯各种变形题

30 篇文章 0 订阅
6 篇文章 0 订阅

剪绳子

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

class Solution {
    public int cuttingRope(int n) {
        /*
        dp五部曲:
        1.状态定义:dp[i]为长度为i的绳子剪成m段最大乘积为dp[i]
        2.状态转移:dp[i]有两种途径可以转移得到
            2.1 由前一个dp[j]*(i-j)得到,即前面剪了>=2段,后面再剪一段,此时的乘积个数>=3个
            2.2 前面单独成一段,后面剩下的单独成一段,乘积为j*(i-j),乘积个数为2
            两种情况中取大的值作为dp[i]的值,同时应该遍历所有j,j∈[1,i-1],取最大值
        3.初始化:初始化dp[1]=1即可
        4.遍历顺序:显然为正序遍历
        5.返回坐标:返回dp[n]
        */
        // 定义dp数组
        int[] dp = new int[n + 1];
        // 初始化
        dp[1] = 1;  // 指长度为1的单独乘积为1
        // 遍历[2,n]的每个状态
        for(int i = 2; i <= n; i++) {
            for(int j = 1; j <= i - 1; j++) {
                // 求出两种转移情况(乘积个数为2和2以上)的最大值
                int tmp = Math.max(dp[j] * (i - j), j * (i - j));
                dp[i] = Math.max(tmp, dp[i]);
            }
        }
        return dp[n];
    }
}

买卖股票

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
        int min = prices[0];
        int res = 0;
        for (int i = 1; i < prices.length; i++) {
            //计算最大差
            res = Math.max(res, (prices[i]-min));
            //记录最小值
            min = Math.min(min, prices[i]);
        }
        return res;
    }
}

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        //下标为0时 不持有股票的收益为0
        dp[0][0] = 0;
        //下标为0时,持有股票的收益为-prices[0]
        dp[0][1] = -prices[0];

        for (int i = 1; i < prices.length; i++) {
            //不持有股票的最大利润 = max(前一天不持有的最大利润,前一天持有+今天卖掉)
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            //持有股票的最大利润 = max(前一天持有的最大利润,前一天不持有+今天买入)
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        }
        //最大利润一定是手中没有股票
        return dp[prices.length-1][0];
    }
}

硬币组合

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

每个硬币无限个, 求组合数。

class Solution {
    public int change(int amount, int[] coins) {
        //代表 总和为i的方法总数有dp[i]个
        int[] dp = new int[amount+1];
        dp[0] = 1;
        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                //方法总数 = 每个硬币的方法数之和
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

最少硬币数

class Solution {
    public int coinChange(int[] coins, int amount) {
        //和为i的最少硬币数为dp[i]
        int[] dp = new int[amount+1];
        //因为递推公式是求min,因此初始化为最大值
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                //因为+1会溢出 所以需要判断
                if (dp[j-coins[i]] != Integer.MAX_VALUE)
                dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
            }
        }
        //如果是最大值说明没有组成成功,因此返回-1;
        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }
}

最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

解法: 因为要求最大和 所以用dp

1. dp[i] 表示前i个元素的最大连续子数组和 2. 进行边界定义 dp[0] = a[0], max = a[0]; 3. 最大和 = max(dp[i-1]+a[i], a[i]);

class Solution {
    public int maxSubArray(int[] nums) {
        int len = nums.length;
        if (len == 0) return 0;
        int[] dp = new int[len];
        int max = nums[0];
        dp[0] = nums[0];
        for (int j = 1; j < len; j++) {
                //max(前j-1的和不是负数,前j-1的和是负数) 负数直接丢弃
                dp[j] = Math.max(dp[j-1] + nums[j], nums[j]);
                max = Math.max(max, dp[j]);
        }
		return max;
	}
}

求最值

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

1. 定义dp数组,到达i,j 位置的最小和为dp[i][j]  2. dp[0][0] 3 dp[0][i] dp[i][0] 4. 递推公式

public class Solution {
   public int minPathSum(int[][] grid) {
    
    int m = grid.length;
    int n = grid[0].length;
    int[][] dp = new int[m][n];
    dp[0][0] = grid[0][0];
    for(int i = 1; i < n; i++) {
        dp[0][i] = dp[0][i-1] + grid[0][i];
    }
    for (int i = 1; i < m; i++) {
        dp[i][0] = dp[i-1][0] + grid[i][0];
    }
    for(int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            //最小值 = min(上边的最小值,左边的最小值) + 当前值
            dp[i][j] = Math.min(dp[i][j-1], dp[i-1][j]) + grid[i][j];
        }
    }
    return dp[m-1][n-1];
   }
}

求总数

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

问总共有多少条不同的路径?

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 if(i == 0 && j != 0) dp[i][j] = 1;
                else 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];
    }
}

机器人走方格

有一个XxY的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。请设计一个算法,计算机器人有多少种走法。注意这次的网格中有些障碍点是不能走的。

给定一个int[][] map(C++ 中为vector >),表示网格图,若map[i][j]为1则说明该点不是障碍点,否则则为障碍。另外给定int x,int y,表示网格的大小。请返回机器人从(0,0)走到(x - 1,y - 1)的走法数,为了防止溢出,请将结果Mod 1000000007。保证x和y均小于等于50

int countWays(int[][] map, int x, int y) {
        int[][] dp = new int[x][y];
        for(int i = 0; i < x; i ++){
            for(int j = 0; j < y; j ++){
                //有障碍就跳过
                if(map[i][j] != 1) continue;
                if(i == 0 && j == 0) dp[0][0] = 1;
                //当前(i,0)的值根据(i-1,0)的值判断
                else if(i != 0 && j == 0) dp[i][0] = dp[i-1][0] ;
                else if(i == 0 && j != 0) dp[0][j] = dp[0][j-1] ;
                else{
                    dp[i][j] = (dp[i][j-1] + dp[i-1][j])%1000000007;
                }
            }
        }
        return dp[x-1][y-1];
    }

求具体结果

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0) return res;
  
        Stack<Integer> context = new Stack<>();
        back(candidates, target, 0 ,context);
        return res;
    }
    private void back(int[] candidates, int yushu, int s, Stack<Integer> context) {
        if (yushu == 0) {
            res.add(new ArrayList(context));
            return;
        }
        for (int i = s; i < candidates.length && yushu >= candidates[i]; i++) {
            context.push(candidates[i]);
            //因为每个数字可以选择多次,因此传递i
            back(candidates, (yushu - candidates[i]), i, context);
            context.pop();
        }
    }
}

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次, 且有重复数据.

class Solution {
    List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0) return res;
        //因为有重复数据 需要先排序
        Arrays.sort(candidates);
        back(candidates, target, 0, new Stack<Integer>());
        return res;
    }
    private void back(int[] candidates, int yushu, int s, Stack<Integer> context){
        if (yushu == 0){
            res.add(new ArrayList(context));
            return;
        }
        for (int i = s; i < candidates.length && yushu >= candidates[i]; i++){
            //遇到重复数据 跳过
            if (i > s && candidates[i] == candidates[i-1]) continue;
            context.push(candidates[i]);
            //i+1 是因为每个数字只能选择一次
            back(candidates, (yushu-candidates[i]), i+1, context);
            context.pop();
        }
    }
}

给定一个没有重复数字的序列,返回其所有可能的全排列。


class Solution1 {
  List<List<Integer>> res = new ArrayList<>();
  public List<List<Integer>> permute(int[] nums) {
    if (nums == null || nums.length == 0) return res;
    int len = nums.length;
    back(nums, new boolean[len], new Stack<Integer>());
    return res;
  }
  private void back(int[] nums, boolean[] used, Stack<Integer> context) {
    int len = nums.length;
    //count == len 代表数字已经选择完了
    if (context.size() == len){
      res.add(new ArrayList(context));
      return;
    }
    for (int i = 0; i < len; i++) {
      //如果没有用过再进行使用
      if (!used[i]) {
        context.push(nums[i]);
        used[i] = true;
        back(nums,  used, context);
        context.pop();
        used[i] = false;
      }
    }
  }
}

给定一个可包含重复数字的序列,返回所有不重复的全排列。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        if (nums == null || nums.length == 0) return res;
        Arrays.sort(nums);
        back(nums, 0, new boolean[nums.length], new Stack<Integer>());
        return res;
    }
    private void back(int[] nums, int count, boolean[] used, Stack<Integer> context) {
        int len = nums.length;
        if (count == len) {
            res.add(new ArrayList(context));
            return;
        }
        for (int i = 0; i < len; i++) {
            if (!used[i]) {
                //i > 0 重复的数字不会出现在第一个
                //当当前数和前一个数一样, 前一个数在本分支中还没有用过,则一定会出现和前一个分支相同的结果
                if(i > 0 && nums[i] == nums[i-1] && !used[i-1])continue;
                context.push(nums[i]);
                used[i] = true;
                back(nums, count+1, used,context);
                context.pop();
                used[i] = false;
            }
        }
    }
}

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        if (nums.length == 0) return res;
        back(nums, 0, new Stack<Integer>());
        return res;
    }
    private void back(int[] nums, int s, Stack<Integer> context) {
        //[], [1], [1,2], [1,2,3], [1,3], [2],[2,3],[3]
        res.add(new ArrayList(context));
        for (int i = s; i < nums.length; i++) {
            context.push(nums[i]);
            back(nums, i+1, context);
            context.pop();
        }
    }
}

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if (nums.length == 0) return res;
        Arrays.sort(nums);
        back(nums, 0, new Stack<Integer>());
        return res;
    }
    private void back(int[] nums, int s, Stack<Integer> context) {
        res.add(new ArrayList(context));
        for (int i = s; i < nums.length; i++) {
            if (i > s && nums[i] == nums[i-1]) continue;
            context.push(nums[i]);
            back(nums, i+1, context);
            context.pop();
        }
    }
}

树形dp

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1;
        if (n == 0) return 0;
        // 总次数 = 每个数字为根的 (左子树的可能数 * 右子树的可能数) 之和
        // dp[i] 表示 i 个数字组成二叉树的所有可能数
        for (int i = 1; i <= n; i++){
            // dp[j-1] : 以j为根左子树的组合数  dp[i-j]: 以j为根的右子树的组合数
            for (int j = 1; j <= i; j++) {
                dp[i] += dp[j-1] * dp[i-j];
            }
        }
        return dp[n];
    }
}

二叉树

给定一个非空二叉树,返回其最大路径和。

本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点

class Solution {
    int max = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        help(root);
        return max;
    }
    private int help(TreeNode root) {
        if (root == null) return 0;
        int left = Math.max(0, help(root.left));
        int right = Math.max(0, help(root.right));
        max = Math.max(max, root.val+left+right);
        return root.val + Math.max(left,right);
    }
}

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

思路: 4中情况 1. 本身就是公共祖先 2. 一个在左边一个在右边 3. 2个都在左边 4. 2个都在右边

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //情况1
        if (root == null || root == p || root == q) return root;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //情况2
        if (left != null && right != null) return root;
        //情况3, 4
        return left != null ? left : right;
    }
}

后继节点

后继节点:就是中序遍历排序中相对的后一个

思路: 左根右 1. 先判断右子树不为空, 则下一个是右子树的最左节点

2. 如果右子树为空, 如果当前节点是左节点 则下一个是当前节点的跟节点.

3. 当前节点是右节点, 则下一个是当前节点的根节点的根节点

4. 如果当前节点是右子树的最右节点, 则下一个节点是null

public TreeNode findNext(TreeNode node) {
        if (node == null) return null;
        TreeNode tmp = null;
        if (node.right != null) {
            tmp = tmp.right;
            while (tmp.left != null) {
                tmp = tmp.left;
            }
            return tmp;
        }else {
            tmp = node;
            //tmp.parent.left != tmp 这里主要针对左子树最左侧节点
            while (tmp.parent != null && tmp.parent.left != tmp) {
                tmp = tmp.parent;
            }
            return tmp.parent;
        }
    }

前驱节点

1. 当前节点的左子树不为null, 则前一个是左子树的最右节点

2. 否则如果为null, 则是当前节点的根节点

3. 如果当前节点是左子树的最左节点,则前一个为null

public TreeNode findPre(TreeNode node) {
        if (node == null) return null;
        TreeNode tmp = null;
        if (node.left != null) {
            tmp = node.left;
            while (tmp.right != null) {
                tmp = tmp.right;
            }
            return tmp;
        }else {
            tmp = node;
            //tmp.parent.right != tmp 主要针对右子树最右测节点
            while (tmp.parent != null && tmp.parent.right != tmp) {
                tmp = tmp.parent;
            }
            return tmp.parent;
        }
    }

判断是否是完全二叉树

1. 层次遍历

2. 如果右孩子不为null 左孩子为null 则返回false

3. 如果右孩子为null, 但是以后的左孩子或者右孩子有不为null 则返回false

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public boolean isCompleteTree(TreeNode node) {
        if (node == null) return false;
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.add(node);
        boolean flag = false;
        while (!queue.isEmpty()) {
            TreeNode cur = queue.poll();
            //右孩子不为null 左孩子为null
            //右孩子为null 以后的节点左孩子或右孩子必须为null
            if ((cur.right != null && cur.left == null) || (flag && (cur.right != null || cur.left != null))) {
                return false;
            }
            if (cur.left != null) {
                queue.add(cur.left);
            }
            if (cur.right != null) {
                queue.add(cur.right);
            }else {
                flag = true;
            }
        }
        return true;
    }
}

求二叉树从根节点到叶子节点的最大路径和对应的路径

1. 如果是叶子节点, 当前和 大于 最大和, 则更新最大和 及对应的路径

2. 否则将左节点递归, 回溯, 右节点递归回溯

    int max = 0;

    List<TreeNode> res = new ArrayList<TreeNode>();
    public List<TreeNode> getMax(TreeNode node) {
        if (node == null) return null;
        help(node, 0, new Stack<TreeNode>());
        return res;
    }

    private void help(TreeNode node, int sum, Stack<TreeNode> context) {

        if (node.left == null && node.right == null) {
            context.push(node);
            sum += node.val;
            if (sum > max) {
                max = sum;
                res = new ArrayList<TreeNode>(context);
            }
            return;
        }
        context.push(node);
        help(node.left, sum+root.val, context);
        context.pop();
        help(node.right, sum+root.val, context);
        context.pop();
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值