最少硬币数
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值依然为unreachableif (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]`为结尾的最长递增子序列的长度。初始化为0int[] 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
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
动态规划
- 定义状态:
dp[i]
表示和为i
的完全平方数的最少数量 - 状态转移方程:
dp[i] = min{dp[i], dp[i - j*j] + 1}
,其中j
为<= i
的完全平方数,是一个需要遍历的变量;dp[i - j*j]
表示数i - j*j
能分解为最少几个完全平方数之和,用减法来衔接是可行的。 - 起始状态:
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, 然后再从下标 1 跳 3 步到达最后一个下标。
示例 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跳到当前下标iif (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 <= n
, dp[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”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j]
为0
或1
参考笔记
动态规划青铜:不同路径
错误方法
本方法假定第一个格子无障碍物的情况下,第一列均可达,而这有根本性错误:第一列的其他行可能存在障碍物,此时不可达。就比如测试用例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值设置为1Arrays.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
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 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开始) + 1for (int j = 0; j <= i; j++){//第一个和最后一个都为1if(j == 0 || j == i)cur.add(1);//新一行的中间元素 = 上 + 上左elsecur.add(pre.get(j) + pre.get(j - 1));}pre = cur; //上一行变更,继续滚动}return pre;
}