动态规划高频问题(算法村第十九关白银挑战)

最少硬币数

322. 零钱兑换 - 力扣(LeetCode)

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

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

动态规划

public int coinChange(int[] coins, int amount)
{
    //dp数组的索引表示零钱面额,数组元素表示兑换零钱所需的最少硬币数
    int[] dp = new int[amount +1];	
    int unreachable = amount + 1;
    //unreachable表示无法用硬币兑换该面额,因为极端情况是全用一元的硬币兑换,此时最少硬币数应为 amount。另外,一些在1~amount之间的面额,也是无法用硬币兑换的
    Arrays.fill(dp, unreachable);	

    dp[0] = 0;	//特例,同时也是递推的起点
    for (int curAmount = 1; curAmount <= amount; curAmount++)
        for (int coin : coins)	//逐一用不同的硬币兑换,取所用的最小硬币数;或者换不了,dp值依然为unreachable
            if (coin <= curAmount)
                dp[curAmount] = Math.min(dp[curAmount], dp[curAmount - coin] + 1);	//后面的兑换结果,由前面的兑换结果确定

    return dp[amount] == unreachable ? -1 : dp[amount];
}

当然,分析问题时,首先是from top to down

在这里插入图片描述

设计动态规划算法时,再from botton to up

最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4

示例 2:

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

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

动态规划

public int lengthOfLIS(int[] nums)
{
    int ans = 0;
    //确定状态,即`dp[i]`的值表示以`nums[i]`为结尾的最长递增子序列的长度。初始化为0
    int[] dp = new int[nums.length];

    for (int right = 0; right < nums.length; right++)
    {
        for (int left = 0; left < right; left++)
            //**跳过非递增的元素**这导致了结尾靠后的递增子序列的长度不一定大于结尾靠前的
            //以nums[left]为结尾的最长递增子序列,是以nums[right]为结尾的最长递增子序列的一部分
            if(nums[left] < nums[right])
                //保持或更新以nums[right]为结尾的最长递增子序列的长度
                dp[right] = Math.max(dp[right], dp[left]);
        	    //遍历所有在当前nums[right]之前的nums[left],确定以nums[right]为结尾的最长递增子序列的长度

        //将结尾元素计入最大递增子序列长度,然后维护答案
        ans = Math.max(ans, ++dp[right]); 
        //分别计算不同结尾的递增子序列的结果
    }

    return ans;
}

最少完全平方个数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

动态规划

  1. 定义状态:dp[i]表示和为 i的完全平方数的最少数量
  2. 状态转移方程:dp[i] = min{dp[i], dp[i - j*j] + 1} ,其中j<= i的完全平方数,是一个需要遍历的变量;dp[i - j*j]表示数i - j*j能分解为最少几个完全平方数之和,用减法来衔接是可行的。
  3. 起始状态:dp[0] = 0。边界条件:dp[n],即和为 n 的完全平方数的最少数量。
public int numSquares(int n)
{
    int[] dp = new int[n  + 1];
    dp[0] = 0;

    for(int i = 1; i <= n; i++)
    {
        dp[i] = Integer.MAX_VALUE;
        for (int j = 1; j * j <= i; j++)
            dp[i] = Math.min(dp[i], dp[i - j*j] + 1);
    }

    return dp[n];
}

再论青蛙跳

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 13 步到达最后一个下标。

示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:

  • 1 <= nums.length <= 104
  • 0 <= nums[i] <= 105

动态规划

状态:布尔值dp[i]表示能否跳到第i个下标。

public boolean canJump(int[] nums)
{
    boolean[] dp = new boolean[nums.length];
    dp[0] = true;   //起点是下标0,可以到达

    for (int i = 1; i < nums.length; i++)
    {
        for (int j = 0; j < i; j++)
            //能从前面的某个能到达的下标j跳到当前下标i
            if (dp[j] && j + nums[j] >= i)
            {
                dp[i] = true;
                break;
            }
    }

    return dp[nums.length - 1];
}

只要修路的速度比人走的速度快

public boolean canJump(int[] nums)
{
    int road = 0;
    for (int man = 0; man < nums.length; man++)
    {
        //人走到前面了,路还没修好
        if (man > road)
            return false;

        //从man的位置继续修路
        road = Math.max(road, man + nums[man]);
    }

    //修路的速度一直比人走的速度快,则人一定能走路到达终点
    return true;
}

解码方法

一条包含字母 A-Z 的消息通过以下映射进行了 编码

'A' -> "1"
'B' -> "2"
...
'Z' -> "26"

解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:

  • "AAJF" ,将消息分组为 (1 1 10 6)
  • "KJF" ,将消息分组为 (11 10 6)

注意,消息不能分组为 (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6""06" 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数

题目数据保证答案肯定是一个 32 位 的整数。

示例 1:

输入:s = "12"
输出:2
解释:它可以解码为 "AB"1 2)或者 "L"12)。

示例 2:

输入:s = "226"
输出:3
解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6)

示例 3:

输入:s = "06"
输出:0
解释:"06" 无法映射到 "F" ,因为存在前导零("6""06" 并不等价)。

提示:

  • 1 <= s.length <= 100
  • s 只包含数字,并且可能包含前导零。

动态规划

定义状态:1 <= i <= ndp[i]表示前i个数字所能构成的解码方案数。数组初始化为0。

初始状态:dp[0] = 1,“前0个数字”即空串。根据下面的状态转移方程,dp[1] = dp[0] = 1,合理,因为用一个数字只能构成一种解码方案。

public int numDecodings(String s)
{
    int n = s.length();
    int[] dp = new int[n + 1];
    dp[0] = 1;

    for (int i = 1; i <= n; i++)
    {
        //拿一位数来解码
        if(s.charAt(i - 1) != '0')
            dp[i] = dp[i - 1];

        //拿两位数来解码
        if (i >= 2 && s.charAt(i - 2) != '0')
        {
            int num = (s.charAt(i - 2) - '0') * 10 + s.charAt(i - 1) - '0';

            if (num <= 26)
                //衔接总解码数
                dp[i] += dp[i - 2];
        }
    }

    return dp[n];
}

路径中存在障碍物

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

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

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

img

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

img

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j]01

参考笔记

动态规划青铜:不同路径

错误方法

本方法假定第一个格子无障碍物的情况下,第一列均可达,而这有根本性错误:第一列的其他行可能存在障碍物,此时不可达。就比如测试用例obstacleGrid = {{0},{1}}

所以,对每个格子,都要先判断是否有障碍物,再对其dp赋值(dp[i]表示从起点开始到达当前位置的路径数)。具体实现见“正确方法”

public static void main(String[] args)
    {
//        int[][] obstacleGrid = {{0,0,0},{0,1,0},{0,0,0}};
//        int[][] obstacleGrid = {{0,1},{0,0}};
//        int[][] obstacleGrid = {{0,1}};
        int[][] obstacleGrid = {{0},{1}};
        int ans = uniquePathsWithObstacles(obstacleGrid);
        System.out.println(ans); //预期:0;结果:1
    }

    public static int uniquePathsWithObstacles(int[][] obstacleGrid)
    {
        int row = obstacleGrid.length;
        int col = obstacleGrid[0].length;
        int[] dp = new int[col];

        //确定第一行的障碍物位置
        int flag = -1;
        for (int j = 0; j < col; j++)
            if (obstacleGrid[0][j] == 1)
            {
                flag = j;
                break;
            }

        //因为第一行只能往右走,所以往后均不可达到
        if (flag == -1)     //第一行无障碍物
            Arrays.fill(dp, 1);
        else                //将索引0到flag - 1 的dp值设置为1
            Arrays.fill(dp, 0, flag, 1);

        //从第二行滚动数组开始
        for (int i = 1; i < row; i++)
            for (int j = 1; j < col; j++)
            {
                if (obstacleGrid[i][j] == 1)
                {
                    dp[j] = 0;  //没法到达有障碍物的格子
                    continue;
                }

                dp[j] = dp[j] + dp[j - 1];
            }

        return dp[col - 1];
    }

正确方法

public static int uniquePathsWithObstacles_2(int[][] obstacleGrid)
{
    int row = obstacleGrid.length;
    int col = obstacleGrid[0].length;
    int[] dp = new int[col];
	
    //设置初始状态
    if (obstacleGrid[0][0] == 0)
        dp[0] = 1;
    else	
		return 0;	//开局即被堵死

    for (int i = 0; i < row; i++)
        for (int j = 0; j < col; j++)
        {
            if (obstacleGrid[i][j] == 1)
            {
                dp[j] = 0;
                continue;
            }
			
            //第一列的格子的dp值由初始状态和obstacleGrid[i][0]共同确定
            //这里值需考虑其他列的dp值:当前 <- 左 + 上
            if (j >= 1 && obstacleGrid[i][j] == 0)
                dp[j] = dp[j] + dp[j - 1];
        }

    return dp[col - 1];
}

杨辉三角Ⅱ

给定一个非负索引 rowIndex,返回「杨辉三角」的第 rowIndex 行。

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

img

示例 1:

输入: rowIndex = 3
输出: [1,3,3,1]

示例 2:

输入: rowIndex = 0
输出: [1]

示例 3:

输入: rowIndex = 1
输出: [1,1]

提示:

  • 0 <= rowIndex <= 33

进阶:

你可以优化你的算法到 *O*(*rowIndex*) 空间复杂度吗?

动态规划

计算dp[i]的时,还需要上一轮的dp[i-1],但是dp[i-1]已经被覆盖,故至少需要两个数组进行滚动,第一个数组用于完整保留上一轮的dp

public static List<Integer> getRow(int rowIndex)
{
    //保留前一行的所有元素
    ArrayList<Integer> pre = new ArrayList<Integer>();
    pre.add(1);

    for (int i = 1; i <= rowIndex; i++)
    {
        //每次生成一个新的一维数组(列表)
        ArrayList<Integer> cur = new ArrayList<>();

        //每行的长度 = 行索引(从0开始) + 1
        for (int j = 0; j <= i; j++)
        {
            //第一个和最后一个都为1
            if(j == 0 || j == i)
                cur.add(1);
            //新一行的中间元素 = 上 + 上左
            else
                cur.add(pre.get(j) + pre.get(j - 1));
        }

        pre = cur;  //上一行变更,继续滚动
    }

    return pre;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值