leetcode
[hot] 62. 不同路径
题目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
题解
动态规划解决问题,示例代码如下:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1));
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
// 极值节省空间
class Solution {
public:
int uniquePaths(int m, int n) {
if (m <= 0 || n <= 0) {
return 0;
}
vector<int> dp(n, 1);
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[j] += dp[j - 1];
}
}
return dp[n - 1];
}
};
复杂度
时间复杂度:
O
(
m
∗
n
)
O(m *n)
O(m∗n)
空间复杂度:
O
(
n
)
O(n)
O(n)
题解2
示例代码如下所示:
class Solution {
public:
int uniquePaths(int m, int n) {
long long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return ans;
}
};
63. 不同路径 II
题目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
题解
考虑障碍物对路径带来的影响,示例代码如下所示:
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
if (obstacleGrid[0][0] == 1) {
return 0;
}
vector<int> dp(n, 0);
dp[0] = 1;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0;
continue;
}
if (j >= 1 && obstacleGrid[i][j - 1] == 0) {
dp[j] += dp[j - 1];
}
}
}
return dp[n - 1];
}
};
关键点
障碍物只会影响当前节点和当前节点右侧节点的更新
[hot] 64. 最小路径和
题目
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
题解
动态规划解决问题,动态规划递推公式如下:
d
p
[
i
]
[
j
]
=
m
i
n
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
1
]
)
+
g
r
i
d
[
i
]
[
j
]
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
dp[i][j]=min(dp[i−1][j],dp[i][j−1])+grid[i][j]
另外,第一行和第一列要特殊处理,dp数组在这些位置上的取值为某个位置之前从左上角元素开始行或者列的累加值。示例代码如下:
// 二维dp数组
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
dp[0][0] = grid[0][0];
for (int i = 1; i < m; ++i) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < n; ++j) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
};
// 一维dp数组
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<int> dp(n);
dp[0] = grid[0][0];
for (int i = 1; i < n; ++i) {
dp[i] = dp[i - 1] + grid[0][i];
}
for (int i = 1; i < m; ++i) {
dp[0] += grid[i][0];
for (int j = 1; j < n; ++j) {
dp[j] = min(dp[j], dp[j - 1]) + grid[i][j];
}
}
return dp[n - 1];
}
};
复杂度
时间复杂度:
O
(
m
∗
n
)
O(m * n)
O(m∗n)
空间复杂度:
O
(
m
∗
n
)
O(m * n)
O(m∗n)
[拓展] 最小路径和 - 输出路径
题解
倒序输入路径,示例代码如下所示:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
dp[0][0] = grid[0][0];
for (int i = 1; i < m; ++i) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < n; ++j) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
// 输出路径
vector<vector<int>> dp_dirs(m, vector<int>(n, 0));
dp_dirs[0][0] = 1;
dp_dirs[m - 1][n - 1] = 1;
int i = m - 1;
int j = n - 1;
while (i || j) {
if (j == 0) {
dp_dirs[i][j] = 1;
--i;
continue;
}
if (i == 0) {
dp_dirs[i][j] = 1;
--j;
continue;
}
if (dp[i][j] == dp[i - 1][j] + grid[i][j]) {
dp_dirs[i - 1][j] = 1;
--i;
} else if (dp[i][j] == dp[i][j - 1] + grid[i][j]) {
dp_dirs[i][j - 1] = 1;
--j;
}
}
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
cout << dp_dirs[i][j] << " ";
}
cout << endl;
}
return dp[m - 1][n - 1];
}
};
[hot] 70. 爬楼梯
题目
假设你正在爬楼梯。需要 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 阶
题解
状态转移方程为 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n - 1) + f(n - 2) f(n)=f(n−1)+f(n−2),示例代码如下所示:
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) {
return n;
}
int a = 1;
int b = 2;
for (int i = 3; i <= n; ++i) {
int sum = a + b;
a = b;
b = sum;
}
return b;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
1
)
O(1)
O(1)
[hot] 72. 编辑距离
题目
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
题解
leetcode题解:题解链接
示例代码如下所示:
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size();
int n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 0; i < m + 1; ++i) {
dp[i][0] = i;
}
for (int j = 0; j < n + 1; ++j) {
dp[0][j] = j;
}
for (int i = 1; i < m + 1; ++i) {
for (int j = 1; j < n + 1; ++j) {
int left = dp[i][j - 1] + 1;
int down = dp[i - 1][j] + 1;
int l_d = dp[i - 1][j - 1];
if (word1[i - 1] != word2[j - 1]) {
++l_d;
}
dp[i][j] = min(min(left, down), l_d);
}
}
return dp[m][n];
}
};
复杂度
时间复杂度:
O
(
m
n
)
O(mn)
O(mn)
空间复杂度:
O
(
m
n
)
O(mn)
O(mn)
[hot] 96. 不同的二叉搜索树
题目
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
题解
直接套用leetcode解题思路,示例代码如下所示:
class Solution {
public:
int numTrees(int n) {
vector<int> G(n + 1, 0);
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
)
O(n)
O(n)
[hot] 118. 杨辉三角
题目
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
题解
直接解题思路,示例代码如下所示:
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> res(numRows);
for (int i = 0; i < numRows; ++i) {
res[i].resize(i + 1);
res[i][0] = res[i][i] = 1;
for (int j = 1; j < i; ++j) {
res[i][j] = res[i - 1][j] + res[i - 1][j - 1];
}
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
119. 杨辉三角 II
题目
给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 3
输出: [1,3,3,1]
题解
两层遍历解决问题,注意内层遍历需要从后向前遍历,这样才能保证后面的元素不影响前面元素,示例代码如下所示:
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> res(rowIndex + 1, 1);
for (int i = 0; i <= rowIndex; ++i) {
for (int j = i - 1; j > 0; --j) {
res[j] += res[j - 1];
}
}
return res;
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
1
)
O(1)
O(1)
关键点
最左和最右的边界条件
[hot] 121. 买卖股票的最佳时机
题目
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
题解
找到波谷min_value和波峰max_value,相减即得到最终结果。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() == 1) {
return 0;
}
int min_value = prices[0];
int max_profit = 0;
for (int i = 1; i < prices.size(); ++i) {
if (prices[i] < min_value) {
min_value = prices[i];
} else {
max_profit = max(prices[i] - min_value, max_profit);
}
}
// 如果股票价格一直走跌,这时根本没机会买入和卖出,因为只要操作就是亏
return max_profit;
}
};
关键点
能够成功找到位于左侧的波谷和位于右侧的波峰
122. 买卖股票的最佳时机 II
如果允许多次买卖,获得的最大收益是多少。直接累加大于0的收益,得到的就是最终收益。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.size() <= 1) {
return 0;
}
int max_profit = 0;
for (int i = 1; i < prices.size(); ++i) {
max_profit += max(0, prices[i] - prices[i - 1]);
}
return max_profit;
}
};
关键点
逐步累加的过程
123. 买卖股票的最佳时机 III
题目
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
题解
根据状态转移关系,有如下动态规划方程:
示例代码如下所示:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
int buy1 = -prices[0], sell1 = 0;
int buy2 = -prices[0], sell2 = 0;
for (int i = 1; i < n; ++i) {
buy1 = max(buy1, -prices[i]);
sell1 = max(sell1, buy1 + prices[i]);
buy2 = max(buy2, sell1 - prices[i]);
sell2 = max(sell2, buy2 + prices[i]);
}
return sell2;
}
};
[hot] 139. 单词拆分
题目
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
题解
定义数组dp,dp[i]表示s[0, i - 1]是否可以由wordDict中的单词组成,递推公式如下所示,其中 c h e c k s [ j , j + 1 , . . . , i − 1 ] check \ s[j, j + 1, ..., i - 1] check s[j,j+1,...,i−1]代表的含义是从s[j到i - 1]是否出现在wordDict中。
d p [ i ] = d p [ j ] & c h e c k s [ j , j + 1 , . . . , i − 1 ] dp[i] = dp[j] \ \& \ check \ s[j, j + 1, ..., i - 1] dp[i]=dp[j] & check s[j,j+1,...,i−1]
示例代码如下所示:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> set;
int length = s.size();
for (const auto& s : wordDict) {
set.insert(s);
}
vector<bool> dp(length + 1, false);
dp[0] = true;
for (int i = 1; i < length + 1; ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && set.count(s.substr(j, i - j))) {
dp[i] = true;
break;
}
}
}
return dp[length];
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
)
O(n)
O(n)
188. 买卖股票的最佳时机 IV
题目
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
题解
题解见 这里,二维dp数组写法如下,
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
k = min(k, n / 2);
vector<vector<int>> buy(n, vector<int>(k + 1));
vector<vector<int>> sell(n, vector<int>(k + 1));
buy[0][0] = -prices[0];
sell[0][0] = 0;
for (int i = 1; i <= k; ++i) {
buy[0][i] = sell[0][i] = INT_MIN / 2;
}
for (int i = 1; i < n; ++i) {
buy[i][0] = max(buy[i - 1][0], sell[i - 1][0] - prices[i]);
for (int j = 1; j <= k; ++j) {
buy[i][j] = max(buy[i - 1][j], sell[i - 1][j] - prices[i]);
sell[i][j] = max(sell[i - 1][j], buy[i - 1][j - 1] + prices[i]);
}
}
return *max_element(sell[n - 1].begin(), sell[n - 1].end());
}
};
一维dp数据示例代码如下所示:
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
k = min(k, n / 2);
vector<int> buy(k + 1, INT_MIN / 2);
vector<int> sell(k + 1, INT_MIN / 2);
buy[0] = -prices[0];
sell[0] = 0;
for (int i = 1; i < n; ++i) {
buy[0] = max(buy[0], sell[0] - prices[i]);
for (int j = 1; j <= k; ++j) {
buy[j] = max(buy[j], sell[j] - prices[i]);
sell[j] = max(sell[j], buy[j - 1] + prices[i]);
}
}
return *max_element(sell.begin(), sell.end());
}
};
[hot] 198. 打家劫舍
题目
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
题解
动态规划解决问题。数组dp每个元素代表的含义为小偷过了第i家之后所获得的最大收益,具体求解公式如下,
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
2
]
+
n
u
m
s
[
i
]
,
d
p
[
i
−
1
]
)
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
dp[i]=max(dp[i−2]+nums[i],dp[i−1])
即如果偷了当前这一家,意味着没偷前一家且在前前一家已经获得最大收益;如果没有偷当前这一家,则意味着偷了前一家且获得最大收益,示例代码如下所示:
class Solution {
public:
int rob(vector<int>& nums) {
int length = nums.size();
if (length <= 2) {
return *max_element(nums.begin(), nums.end());
}
vector<int> dp(length, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < length; ++i) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
};
// 不借助dp空间
class Solution {
public:
int rob(vector<int>& nums) {
int length = nums.size();
if (length <= 2) {
return *max_element(nums.begin(), nums.end());
}
int first = nums[0];
int second = max(nums[0], nums[1]);
for (int i = 2; i < length; ++i) {
int temp = second;
second = max(second, first + nums[i]);
first = temp;
}
return second;
}
};
复杂度
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n) or
O
(
1
)
O(1)
O(1)
213. 打家劫舍 II
题目
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
题解
错位分两种情况讨论,因为首尾不能同时抢,示例代码如下所示:
class Solution {
public:
int core(vector<int>& nums, int start, int end) {
int first = nums[start];
int second = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; ++i) {
int temp = second;
second = max(second, first + nums[i]);
first = temp;
}
return second;
}
int rob(vector<int>& nums) {
if (nums.size() <= 2) {
return *max_element(nums.begin(), nums.end());
}
int length = nums.size();
return max(core(nums, 0, length - 2), core(nums, 1, length - 1));
}
};
复杂度
时间:
O
(
n
)
O(n)
O(n)
空间:
O
(
1
)
O(1)
O(1)
[hot] 221.最大正方形
题目
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
题解
动态规划数组dp[i][j]的含义为以[i,j]为右下角的最大正方形的边长,递推公式如下所示:
d
p
[
i
]
[
j
]
=
{
m
i
n
(
d
p
[
i
,
j
−
1
]
,
d
p
[
i
−
1
,
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
)
+
1
i
f
m
a
t
r
i
x
[
i
]
[
j
]
=
=
1
0
i
f
m
a
t
r
i
x
[
i
]
[
j
]
=
=
0
dp[i][j]=\left\{\begin{matrix} min(dp[i, j -1], dp[i-1, j-1], dp[i-1][j]) + 1 \ \ \ if \ matrix[i][j] == 1\\ 0 \ \ \ if \ matrix[i][j] == 0 \end{matrix}\right.
dp[i][j]={min(dp[i,j−1],dp[i−1,j−1],dp[i−1][j])+1 if matrix[i][j]==10 if matrix[i][j]==0
可以想象如果dp[i][j-1]>dp[i - 1][j - 1],那么以[i, j - 1]为右下角的最大正方形肯定会涵盖以[i, j]为右下角的最大正方形,同理可以推断出其他情况。示例代码如下所示:
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) {
return 0;
}
int rows = matrix.size();
int cols = matrix[0].size();
vector<vector<int>> dp(rows, vector<int>(cols));
int matrix_side = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
if (matrix[i][j] == '0') {
continue;
}
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = min(dp[i][j - 1], min(dp[i - 1][j], dp[i - 1][j - 1])) + 1;
}
matrix_side = max(matrix_side, dp[i][j]);
}
}
return matrix_side * matrix_side;
}
};
复杂度
时间复杂度:
O
(
m
n
)
O(mn)
O(mn)
空间复杂度:
O
(
m
n
)
O(mn)
O(mn)
[hot] 279.完全平方数
题目
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
题解
定义动态规划数组dp[n + 1],其具体含义为数字i能够由完全平方数累加组成的最小数量,dp[i]初始化为i,因为最坏情况下i是由i个1组成。示例代码如下:
class Solution {
public:
int numSquares(int n) {
if (n <= 0) {
return 0;
}
vector<int> dp(n + 1, 0);
for (int i = 1; i <= n; ++i) {
dp[i] = i;
for (int j = 1; i - j * j >= 0; ++j) {
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
};
309. 买卖股票的最佳时机含冻结期
题目
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
题解
题解见 这里,示例代码如下。
class Solution {
public:
int maxProfit(vector<int>& prices) {
if (prices.empty()) {
return 0;
}
int n = prices.size();
// f[i][0]: 手上持有股票的最大收益
// f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
// f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
vector<vector<int>> f(n, vector<int>(3));
f[0][0] = -prices[0];
for (int i = 1; i < n; ++i) {
f[i][0] = max(f[i - 1][0], f[i - 1][2] - prices[i]);
f[i][1] = f[i - 1][0] + prices[i];
f[i][2] = max(f[i - 1][1], f[i - 1][2]);
}
return max(f[n - 1][1], f[n - 1][2]);
}
};
[hot] 322. 零钱兑换
题目
给定不同面额的硬币 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
题解
本题依然可以通过<组合总和>的思路来求解,不过会超时,因而采用动态规划解决问题。定义
F
(
i
)
F(i)
F(i)为组成金额i所需要的最小硬币数量,状态转移方程如下所示:
F
(
i
)
=
m
i
n
j
=
1
,
2
,
.
.
.
,
n
F
(
i
−
c
o
i
n
s
[
j
]
)
+
1
F(i) = min_{j=1, 2, ..., n}F(i - coins[j]) + 1
F(i)=minj=1,2,...,nF(i−coins[j])+1
示例代码如下所示:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
if (coins.empty() || amount < 0) {
return 0;
}
int length = coins.size();
vector<int> dp(amount + 1, amount + 1); // 初始化一定要注意,不能定义成INT_MAX,这样在如下代码运行过程中会有溢出的风险。
dp[0] = 0;
for (int i = 1; i <= amount; ++i) { // i的使命为遍历钱数
for (int j = 0; j < length; ++j) { // j的使命为遍历数组
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
复杂度
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
关键点
理解动态规划数组元素定义,是组成金额最少的硬币个数,所以F[0] = 0
[hot] 494. 目标和
题目
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
题解1
动态规划解决问题,数组元素总和为sum,带负号的元素之和为neg,因而带正号的元素之和为sum - neg,要想满足需求,则有 sum - neg - neg = target,即 neg = (sum - target) / 2,因而问题转换为在数组中找到非连续子数组,和为neg,示例代码如下:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum - target < 0 || (sum - target) % 2) {
return 0;
}
int neg = (sum - target) / 2;
vector<int> dp(neg + 1);
dp[0] = 1;
for (auto& num: nums) {
for (int i = neg; i >= num; --i) {
dp[i] += dp[i - num];
}
}
return dp[neg];
}
};
关键点
理解动态规划数组的含义,即和为x的数组元素组成种类数,因而dp[0] = 1
题解2
回溯法解决问题,示例代码如下所示:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
void backtrack(vector<int>& nums, int target, int idx, int s) {
if (idx == nums.size()) {
if (s == target) {
count++;
}
// 谨记else的条件为idx == nums.size()
} else {
backtrack(nums, target, idx + 1, s + nums[idx]);
backtrack(nums, target, idx + 1, s - nums[idx]);
}
}
private:
int count = 0;
};
518. 零钱兑换 II
题目
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
题解
思路比较直观,代码如下所示:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1);
dp[0] = 1;
for (auto coin : coins) {
for (int i = coin; i <= amount; ++i) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
};
关键点
理解动态规划数组元素定义,是组成指定金额的硬币组合数,所以F[0] = 1
714. 买卖股票的最佳时机含手续费
题目
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
题解
题解见 这里,示例代码如下:
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
int buy = prices[0] + fee;
int profit = 0;
for (int i = 1; i < n; ++i) {
if (prices[i] + fee < buy) {
buy = prices[i] + fee;
} else if (prices[i] > buy) {
profit += prices[i] - buy;
buy = prices[i];
}
}
return profit;
}
};
三角形最小路径和(leetcode_120)
题目
给定一个三角形 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
设置二维动态规划数组,示例代码如下所示:
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int rows = triangle.size();
vector<vector<int>> dp(rows, vector<int>(rows, 0));
dp[0][0] = triangle[0][0];
for (int i = 1; i < rows; ++i) {
for (int j = 0; j <= i; ++j) {
dp[i][j] = triangle[i][j];
if (j == 0) {
dp[i][j] += dp[i - 1][j];
} else if (j == i) {
dp[i][j] += dp[i - 1][j - 1];
} else {
dp[i][j] += min(dp[i - 1][j - 1], dp[i - 1][j]);
}
}
}
int result = dp[rows - 1][0];
for (int j = 1; j < dp[rows - 1].size(); ++j) {
result = min(result, dp[rows - 1][j]);
}
return result;
}
};
时间和空间复杂度都是O(N^2)
题解2
一维动态规划数组解决问题,注意内层循环遍历的顺序,示例代码如下所示:
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int rows = triangle.size();
vector<int> dp(rows, 0);
dp[0] = triangle[0][0];
for (int i = 1; i < rows; ++i) {
// 防止dp前面元素被覆盖的情况,因而需要从后向前遍历
for (int j = i; j >= 0; --j) {
int temp = triangle[i][j];
if (j == 0) {
temp += dp[j];
} else if (j == i) {
temp += dp[j - 1];
} else {
temp += min(dp[j], dp[j - 1]);
}
dp[j] = temp;
}
}
int result = dp[0];
for (int j = 1; j < dp.size(); ++j) {
result = min(result, dp[j]);
}
return result;
}
};
时间复杂度为O(N^2),空间复杂度为O(N)。
剪绳子(剑指offer_14)
题目
给你一根长度为 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。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
题解
对于的正整数 n,当 n≥2 时,可以拆分成至少两个正整数的和。令 k 是拆分出的第一个正整数,则剩下的部分是 n−k,n−k 可以不继续拆分,或者继续拆分成至少两个正整数的和。由于每个正整数对应的最大乘积取决于比它小的正整数对应的最大乘积,因此可以使用动态规划求解。
dp数组的含义: dp[i] 表示将正整数 i 拆分成至少两个正整数的和之后,这些正整数的最大乘积。
边界条件: 0 不是正整数,1 是最小的正整数,0 和 1 都不能拆分,因此 dp[0]=dp[1]=0。
状态转移方程:
当 i≥2 时,假设对正整数 i 拆分出的第一个正整数是 j(1≤j<i),则有以下两种方案:
将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j×(i−j);
将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j×dp[i−j]。
因此,当 j 固定时,有 dp[i]=max(j×(i−j),j×dp[i−j])。由于 j 的取值范围是 1 到 i−1,需要遍历所有的 j 得到 dp[i] 的最大值,因此可以得到状态转移方程如下:
d
p
[
i
]
=
m
a
x
1
≤
j
<
i
(
d
p
[
i
]
,
j
∗
(
i
−
j
)
,
j
∗
d
p
[
i
−
j
]
)
dp[i] = max_{1\le j \lt i} (dp[i], j * (i - j), j * dp[i - j])
dp[i]=max1≤j<i(dp[i],j∗(i−j),j∗dp[i−j])
示例代码如下:
class Solution {
public:
int cuttingRope(int n) {
vector<int> dp(n + 1, 0);
for (int i = 2; i <= n; ++i) {
for (int j = 1; j < i; ++j) {
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
};
把数字翻译成字符串(剑指offer_46)
题目
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
题解
以nums[i]开始组成的字符串个数s_num_[i]可以由两部分组成:
- required: nums[i]独自成为一个数字翻译成一个字母,这时组成字符串个数为s_num_[i + i]
- optional: nums[i]和nums[i + 1]共同组成一个数字翻译一个字母,这时组成字符串个数为s_num_[i + 2]
因而以nums[i]开始组成的字符串个数s_num_[i] = s_num_[i + i] + s_num_[i + 2](optional)
示例代码如下:
class Solution {
public:
int translateNum(int num) {
std::string s = std::to_string(num);
int length = s.size();
vector<int> dp(length + 1, 0);
dp[length] = 1;
dp[length - 1] = 1;
for (int i = length - 2; i >= 0; --i) {
int count = dp[i + 1];
int num_i = s[i] - '0';
int num_i1 = s[i + 1] - '0';
int translate_num = num_i * 10 + num_i1;
if (num_i > 0 && translate_num <= 25) {
count += dp[i + 2];
}
dp[i] = count;
}
return dp[0];
}
};
背包问题
题目
给定 n 种物品和一个容量为 c 的背包,物品 i 的重量是 w i w_i wi,其价值为 v i v_i vi,应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?
题解
声明一个 大小为 m[n][c] 的二维数组,m[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值 ,那么我们可以很容易分析得出 m[i][j] 的计算方法,
- 如果j<w[i],那么这个包容不下w[i],因而m[i][j]=m[i-1][j], 这个公式的意思是容量为j的包面对第i件物品,即使把包中所有的东西都拿出来,也不可能装得下,因而现在得到的结果和容量为j的包面对第i-1件物品时的情况是相同的。
- 如果 j ≥ w [ i ] j\geq w[i] j≥w[i], 那么现在可以装也可以不装,这时要么我选择不装,得到的结果就是m[i-1][j], 要么装,而如果选择装的话,得到的结果就和m[i-1][j-w[i]]装完之后再无脑的加上一个v[i]的状态是相同的,即m[i][j]=m[i-1][j-w[i]],
状态转移方程为
if(j>=w[i])
m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
else
m[i][j]=m[i-1][j];
示例代码如下所示:
void backpack() {
vector<int> v = { 0, 8, 10, 6, 3, 7, 2 }; // 这里人为添加一个0是为了一开始取得的元素为8,而不是10, 一定注意!
vector<int> w = { 0, 4, 6, 2, 2, 5, 1 };
int N = 6, C = 12;
vector<vector<int> > result(N + 1, vector<int>(C + 1, 0));
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= C; j++) {
// 注意这里是result[i - 1][j - w[i]]+v[i]
// 如果要装入w[i], 那么现在口袋中一定有i-1个物品,而不是i个
if (j >= w[i]) {
result[i][j] = max(result[i - 1][j], result[i - 1][j - w[i]]+v[i]);
} else {
result[i][j] = result[i - 1][j];
}
}
}
return;
}
礼物的最大价值(剑指offer_47)
题目
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
题解1
定义二维动态规划数组,由于本题较为简单,这里直接给出示例代码:
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size();
int cols = grid[0].size();
vector<vector<int>> dp(rows, vector<int>(cols, 0));
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; ++i) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < cols; ++j) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int row = 1; row < rows; ++row) {
for (int col = 1; col < cols; ++col) {
dp[row][col] = max(dp[row - 1][col], dp[row][col - 1]) + grid[row][col];
}
}
return dp[rows - 1][cols - 1];
}
};
题解2
将上述二维数组压缩为一维数组,示例代码如下所示:
class Solution {
public:
int maxValue(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0) {
return 0;
}
int rows = grid.size();
int cols = grid[0].size();
vector<int> dp(cols, 0);
for (int row = 0; row < rows; ++row) {
for (int col = 0; col < cols; ++col) {
int left = 0;
int up = 0;
if (row > 0) {
up = dp[col];
}
if (col > 0) {
left = dp[col - 1];
}
dp[col] = max(left, up) + grid[row][col];
}
}
return dp[cols - 1];
}
};