目录
一维动态规划:
137. 爬楼梯 ①
138. 打家劫舍 ② ×
139. 单词拆分 ② ×
140. 零钱兑换 ② ×
141. 最长递增子序列 ②
多维动态规划:
142. 三角形最小路径和 ②
143. 最小路径和 ②
144. 不同路径 II ②
145. 最长回文子串 ②
146. 交错字符串 ②
147. 编辑距离 ②
148. 买卖股票的最佳时机 III ③
149. 买卖股票的最佳时机 IV ③
150. 最大正方形 ②
二十二、一维动态规划:
137. 爬楼梯 ①
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入:n = 3 输出:3 解释:有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
方法1:(用递归会超时)
public int climbStairs(int n) {
int[] ints = new int[46];
ints[1] = 1;
ints[2] = 2;
for (int i = 3; i <= n; i++) {
ints[i] = ints[i - 1] + ints[i - 2];
}
return ints[n];
}
详细图解参见:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
138. 打家劫舍 ② ×
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
解题思路:. - 力扣(LeetCode)
方法2:(0ms)
public int rob(int[] nums) {
int prev = 0;
int curr = 0;
// 每次循环,计算“偷到当前房子为止的最大金额”
for (int i : nums) {
// 循环开始时,curr 表示 dp[k-1],prev 表示 dp[k-2]
// dp[k] = max{ dp[k-1], dp[k-2] + i }
int temp = Math.max(curr, prev + i);
prev = curr;
curr = temp;
// 循环结束时,curr 表示 dp[k],prev 表示 dp[k-1]
}
return curr;
}
139. 单词拆分 ② ×
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true 解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。注意,你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出: false
提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s
和wordDict[i]
仅由小写英文字母组成wordDict
中的所有字符串 互不相同
// BFS
public boolean wordBreak(String s, List<String> wordDict) { // 2ms
Queue<Integer> queue = new LinkedList<>();
queue.add(0);
int slength = s.length();
boolean[] visited = new boolean[slength + 1];
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int start = queue.poll().intValue();
for (String word : wordDict) {
int nextStart = start + word.length();
if (nextStart > slength || visited[nextStart]) {
continue;
}
if (s.indexOf(word, start) == start) {
if (nextStart == slength) {
return true;
}
queue.add(nextStart);
visited[nextStart] = true;
}
}
}
}
return false;
}
// DFS
public boolean wordBreak(String s, List<String> wordDict) { // 1ms
boolean[] visited = new boolean[s.length() + 1];
return dfs(s, 0, wordDict, visited);
}
private boolean dfs(String s, int start, List<String> wordDict, boolean[] visited) {
for (String word : wordDict) {
int nextStart = start + word.length();
if (nextStart > s.length() || visited[nextStart]) {
continue;
}
if (s.indexOf(word, start) == start) {
if (nextStart == s.length() || dfs(s, nextStart, wordDict, visited)) {
return true;
}
visited[nextStart] = true;
}
}
return false;
}
// DP
public boolean wordBreak(String s, List<String> wordDict) { // 1ms
int maxWordLength = 0;
Set<String> wordSet = new HashSet<>(wordDict.size());
for (String word : wordDict) {
wordSet.add(word);
if (word.length() > maxWordLength) {
maxWordLength = word.length();
}
}
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i < dp.length; i++) {
for (int j = (i - maxWordLength < 0 ? 0 : i - maxWordLength); j < i; j++) {
if (dp[j] && wordSet.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[dp.length - 1];
}
140. 零钱兑换 ② ×
给你一个整数数组 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
解题思路:
. - 力扣(LeetCode)
12ms:
class Solution {
public int coinChange(int[] coins, int amount) {
// 自底向上的动态规划
if(coins.length == 0){
return -1;
}
// memo[n]的值: 表示的凑成总金额为n所需的最少的硬币个数
int[] memo = new int[amount+1];
// 给memo赋初值,最多的硬币数就是全部使用面值1的硬币进行换
// amount + 1 是不可能达到的换取数量,于是使用其进行填充
Arrays.fill(memo,amount+1);
memo[0] = 0;
for(int i = 1; i <= amount;i++){
for(int j = 0;j < coins.length;j++){
if(i - coins[j] >= 0){
// memo[i]有两种实现的方式,
// 一种是包含当前的coins[i],那么剩余钱就是 i-coins[i],这种操作要兑换的硬币数是 memo[i-coins[j]] + 1
// 另一种就是不包含,要兑换的硬币数是memo[i]
memo[i] = Math.min(memo[i],memo[i-coins[j]] + 1);
}
}
}
return memo[amount] == (amount+1) ? -1 : memo[amount];
}
}
作者:sugar
链接:https://leetcode.cn/problems/coin-change/solutions/137661/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
18ms:
class Solution {
public int coinChange(int[] coins, int amount) {
// 自底向上的动态规划
if(coins.length == 0){
return -1;
}
// memo[n]的值: 表示的凑成总金额为n所需的最少的硬币个数
int[] memo = new int[amount+1];
memo[0] = 0;
for(int i = 1; i <= amount;i++){
int min = Integer.MAX_VALUE;
for(int j = 0;j < coins.length;j++){
if(i - coins[j] >= 0 && memo[i-coins[j]] < min){
min = memo[i-coins[j]] + 1;
}
}
// memo[i] = (min == Integer.MAX_VALUE ? Integer.MAX_VALUE : min);
memo[i] = min;
}
return memo[amount] == Integer.MAX_VALUE ? -1 : memo[amount];
}
}
作者:sugar
链接:https://leetcode.cn/problems/coin-change/solutions/137661/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
36ms:
class Solution {
int[] memo;
public int coinChange(int[] coins, int amount) {
if(coins.length == 0){
return -1;
}
memo = new int[amount];
return findWay(coins,amount);
}
// memo[n] 表示钱币n可以被换取的最少的硬币数,不能换取就为-1
// findWay函数的目的是为了找到 amount数量的零钱可以兑换的最少硬币数量,返回其值int
public int findWay(int[] coins,int amount){
if(amount < 0){
return -1;
}
if(amount == 0){
return 0;
}
// 记忆化的处理,memo[n]用赋予了值,就不用继续下面的循环
// 直接的返回memo[n] 的最优值
if(memo[amount-1] != 0){
return memo[amount-1];
}
int min = Integer.MAX_VALUE;
for(int i = 0;i < coins.length;i++){
int res = findWay(coins,amount-coins[i]);
if(res >= 0 && res < min){
min = res + 1; // 加1,是为了加上得到res结果的那个步骤中,兑换的一个硬币
}
}
memo[amount-1] = (min == Integer.MAX_VALUE ? -1 : min);
return memo[amount-1];
}
}
作者:sugar
链接:https://leetcode.cn/problems/coin-change/solutions/137661/javadi-gui-ji-yi-hua-sou-suo-dong-tai-gui-hua-by-s/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
141. 最长递增子序列 ②
给你一个整数数组 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
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
- 你能将算法的时间复杂度降低到
O(n log(n))
吗?
方法1:
public int lengthOfLIS(int[] nums) {
int[] records = new int[nums.length];
Arrays.fill(records, 1);
int max = records[0];
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] > nums[i]){
records[j] = Math.max(records[i] + 1, records[j]);
}
}
max = Math.max(records[i], max);
}
return max;
}
图解参见:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
方法2:(0ms)
public int lengthOfLIS(int[] nums) {
if(nums.length == 1) return 1;
int n = nums.length;
int[] dp = new int[n];
int len = 0;
for(int i: nums){
int index = Arrays.binarySearch(dp, 0, len, i);
if(index < 0) index = -(index+1);
dp[index] = i;
if(index == len) len++;
}
return len;
}
方法3:(2ms)
public int lengthOfLIS(int[] nums) {
int ans = 0;
int[] end = new int[nums.length];
for(int num : nums){
int l = 0, r = ans; // ans就是现在的个数
while(l < r){
int mid = l + (r - l)/2;
if(end[mid] < num) l = mid + 1;
else r = mid;
}
end[l] = num;
if(l == ans) ans++; // 刚好处在边界
}
return ans;
}
二十三、多维动态规划:
142. 三角形最小路径和 ②
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 输出:11 解释:如下面简图所示: 2 3 4 6 5 7 4 1 8 3 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
示例 2:
输入:triangle = [[-10]] 输出:-10
提示:
1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-104 <= triangle[i][j] <= 104
进阶:
- 你可以只使用
O(n)
的额外空间(n
为三角形的总行数)来解决这个问题吗?
方法2:动态规划(3ms)
定义二维 dp 数组,将解法二中「自顶向下的递归」改为「自底向上的递推」。
1、状态定义:
2、状态转移:
3、代码实现:
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
// dp[i][j] 表示从点 (i, j) 到底边的最小路径和。
int[][] dp = new int[n + 1][n + 1];
// 从三角形的最后一行开始递推。
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
}
}
return dp[0][0];
}
作者:Sweetiee 🍬
链接:https://leetcode.cn/problems/triangle/solutions/329394/di-gui-ji-yi-hua-dp-bi-xu-miao-dong-by-sweetiee/
时间复杂度:O(N^2),N 为三角形的行数。
空间复杂度:O(N^2),N 为三角形的行数。
方法3:空间优化
作者:Sweetiee 🍬
链接:https://leetcode.cn/problems/triangle/solutions/329394/di-gui-ji-yi-hua-dp-bi-xu-miao-dong-by-sweetiee/
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[] dp = new int[n + 1];
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j);
}
}
return dp[0];
}
作者:Sweetiee 🍬
链接:https://leetcode.cn/problems/triangle/solutions/329394/di-gui-ji-yi-hua-dp-bi-xu-miao-dong-by-sweetiee/
时间复杂度:O(N^2),N 为三角形的行数。
空间复杂度:O(N),N 为三角形的行数。
递归见:. - 力扣(LeetCode)
0ms答案:
public int minimumTotal(List<List<Integer>> triangle) {
int[] a = new int[triangle.size()];
for (int i = 0; i < triangle.size(); ++i) minimumTotal(triangle, a, i);
int min = Integer.MAX_VALUE;
for (int i : a) if (i < min) min = i;
return min;
}
private static void minimumTotal(List<List<Integer>> triangle, int[] last, int i) {
List<Integer> line = triangle.get(i);
if (i == 0) {
last[0] = line.get(0);
return;
}
last[i] = last[i - 1] + line.get(i);
last[i - 1] += line.get(i - 1);
for (int j = i - 2; j > -1; --j) {
int sum = last[j] + line.get(j + 1);
if (sum < last[j + 1]) last[j + 1] = sum;
last[j] += line.get(j);
}
}
1ms答案:
public int minimumTotal(List<List<Integer>> triangle) {
memo = new Integer[triangle.size()][triangle.size()];
return dfs(triangle, 0, 0);
}
Integer[][] memo;
private int dfs(List<List<Integer>> triangle, int i, int j) {
if (i == triangle.size()) {
return 0;
}
if (memo[i][j] != null) { //计算过的最小路径就无需再计算,直接返回
return memo[i][j];
}
return memo[i][j] = Math.min(dfs(triangle, i + 1, j), dfs(triangle, i + 1, j + 1)) + triangle.get(i).get(j); //当前元素加下层中最短的路径和
}
2ms答案:
public int minimumTotal(List<List<Integer>> triangle) {
int[] minSum = new int[triangle.size()];
// for(int i = 0; i < minSum.length; ++i) minSum[i] = Integer.MAX_VALUE-1444;
minSum[0] = triangle.get(0).get(0);
for(int i = 1; i < minSum.length; ++i){
List<Integer> p = triangle.get(i);
int last = minSum[0], pum = 0;
for(int j = 0; j < i+1; ++j){
if(j == 0) pum = last+p.get(j);
else if(j == i) pum = last+p.get(j);
else pum = Math.min(last, minSum[j])+p.get(j);
last = minSum[j];
minSum[j] = pum;
}
}
int min = Integer.MAX_VALUE;
for(int i: minSum){
if (i < min) min=i;
}
return min;
}