*题目编号为Leetcode中对应的题号。
某位大佬的Leetcode题解参考链接
动态规划
-
斐波那契数列
F(0)=1, F(1)=1, F(n)=F(n-1)+F(n-2)
// 基础版递归实现 int calcFib(int n){ assert(n>=0); if(n==0) return 1; if(n==1) return 1; return calcFib(n-1)+calcFib(n-2); }
上述程序中存在大量的重复计算,如下图,使得时间复杂度达到了指数级!
// 优化递归实现:记忆化搜索 vector<int> memo; int fib(int n) { assert(n >= 0); if (n == 0 || n == 1) memo[n] = 1; if (!memo[n])// 如果fib(n)之前未被计算过 memo[n] = fib(n - 1) + fib(n - 2); return memo[n]; } int calcFib(int n) { memo = vector<int>(n + 1, 0); return fib(n); }
通过记忆化搜索,时间复杂度仅为O(n).
记忆化搜索属于自上而下的解决问题。
// 动态规划实现版本1
int calcFib(int n){
assert(n>=0);
vector<int> memo(n+1, 0);// 注意这里是n+1
memo[0]=1;
memo[1]=1;
for(int i=2;i<=n;i++){
memo[i]=memo[i-1]+memo[i-2];
}
return memo[n];
}
// 动态规划实现版本2
int calcFib(int n){
assert(n>=0);
int res;
int memo[2];
for(int i=0;i<=n;i++){
if(i==0 || i==1){
memo[i]=1;
res=1;
continue;
}
res=(memo[0]+memo[1]);
memo[0]=memo[1];
memo[1]=res;
}
return res;
}
动态规划属于自下而上的解决问题。
相比于记忆化搜索,动态规划解决问题性能更优,一是因为没有递归调用,调用函数是有额外的时间开销的,此外也不会占用系统的栈空间。
动态规划是将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。

-
(70 爬楼梯) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
// 记忆化搜索:自顶向下实现 class Solution { private: vector<int> memo; int takeOneStep(int n){ if(n==1) return 1; if(n==2) return 2; if(!memo[n]) memo[n]=takeOneStep(n-1)+takeOneStep(n-2); return memo[n]; } public: int climbStairs(int n) { assert(n>0); memo=vector<int>(n+1, 0); return takeOneStep(n); } }; // 动态规划实现 class Solution { public: int climbStairs(int n) { vector<int> memo(n+2, 0);// 注意这里是n+2 memo[1]=1; memo[2]=2; for(int i=3;i<=n;i++){ memo[i]=memo[i-1]+memo[i-2]; } return memo[n]; } };
-
(120三角形最小路径和) 给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是
下标
与上一层结点下标
相同或者等于上一层结点下标 + 1
的两个结点。示例
[ [2], [3,4], [6,5,7], [4,1,8,3] ] 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
说明:
如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
/// Memory Search /// Time Complexity: O(n^2) /// Space Complexity: O(1) class Solution { public: int minimumTotal(vector<vector<int>>& triangle) { int n = triangle.size(); vector<vector<int>> dp(n, vector<int>(n, -1)); for(int i = 0 ; i < n ; i ++) go(triangle, n - 1, i, dp); return *min_element(dp[n-1].begin(), dp[n-1].end()); } private: int go(const vector<vector<int>>& triangle, int i, int j, vector<vector<int>>& dp){ if(dp[i][j] != -1) return dp[i][j]; if(i == 0) return dp[i][j] = triangle[i][j]; if(j == 0) return dp[i][j] = triangle[i][j] + go(triangle, i - 1, 0, dp); if(j == i) return dp[i][j] = triangle[i][j] + go(triangle, i - 1, i - 1, dp); return dp[i][j] = triangle[i][j] + min(go(triangle, i - 1, j - 1, dp), go(triangle, i - 1, j, dp)); } }; /// Dynamic Programming /// Time Complexity: O(n^2) /// Space Complexity: O(1) class Solution { public: int minimumTotal(vector<vector<int>>& triangle) { int n = triangle.size(); for(int i = 1 ; i < n ; i ++){ triangle[i][0] += triangle[i-1][0]; triangle[i][i] += triangle[i-1][i-1]; for(int j = 1 ; j < i ; j ++) triangle[i][j] += min(triangle[i-1][j-1], triangle[i-1][j]); } return *min_element(triangle[n-1].begin(), triangle[n-1].end()); } };
-
(64最小路径和) 给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明: 每次只能向下或者向右移动一步。
示例:
输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。
/// Dynamic Programming /// with O(n^2) space(空间复杂度可以进一步优化) /// /// Time Complexity: O(n^2) /// Space Complexity: O(n^2) class Solution { public: int minPathSum(vector<vector<int>>& grid) { int n = grid.size(); assert(n > 0); int m = grid[0].size(); assert(m > 0); vector<vector<int>> res = grid; for(int j = 1 ; j < m ; j ++) res[0][j] += res[0][j-1]; for(int i = 1 ; i < n ; i ++) res[i][0] += res[i-1][0]; for(int i = 1 ; i < n ; i ++) for(int j = 1 ; j < m ; j ++) res[i][j] += min(res[i-1][j], res[i][j-1]); return res[n-1][m-1]; } };
-
(343 整数拆分) 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
// 记忆化搜索实现 class Solution { private: vector<int> memo; int max3int(int num1, int num2, int num3){ return max(num1, max(num2, num3)); } int breakInt(int n) { if (n == 1) return 1; // 对于每一个数都有两种选择,可以继续拆下去,也可以不拆,状态转移方程中,仍然需要考虑其余所有的状态 if (!memo[n]) { int max_res = 0; for (int i = 1; i < n; i++) { max_res=max3int(max_res, (n - i) * i, breakInt(n - i) * i); } memo[n] = max_res; } return memo[n]; } public: int integerBreak(int n) { assert(n > 0); memo = vector<int>(n + 1, 0); return breakInt(n); } }; // 动态规划实现 class Solution { private: vector<int> memo; int max3int(int num1, int num2, int num3){ return max(num1, max(num2, num3)); } public: int integerBreak(int n) { assert(n > 0); memo = vector<int>(n + 2, 0); memo[2]=1; for(int i=3;i<=n;i++){ // 求解memo[i] int max_res=0; for(int j=1;j<i;j++){ // j+(i-j) max_res=max3int(max_res, j*(i-j), memo[j]*(i-j)); } memo[i]=max_res; } return memo[n]; } };
-
最优子结构:通过求子问题的最优解,来得到原问题的最优解
-
(279完全平方数) 给定正整数 n,找到若干个完全平方数(比如
1, 4, 9, 16, ...
)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。示例:
输入: n = 12 输出: 3 解释: 12 = 4 + 4 + 4. 输入: n = 13 输出: 2 解释: 13 = 4 + 9.
/// Memory Search /// Time Complexity: O(n) /// Space Complexity: O(n) class Solution { public: int numSquares(int n) { vector<int> mem(n + 1, -1); return numSquares(n, mem); } private: int numSquares(int n, vector<int>& mem){ if(n == 0) return 0; if(mem[n] != -1) return mem[n]; int res = INT_MAX; for(int i = 1; n - i * i >= 0; i ++ ) res = min(res, 1 + numSquares(n - i * i, mem)); return mem[n] = res; } }; /// Dynamic Programming /// Time Complexity: O(n) /// Space Complexity: O(n) class Solution { public: int numSquares(int n) { vector<int> mem(n + 1, INT_MAX); mem[0] = 0; for(int i = 1; i <= n ; i ++) for(int j = 1 ; i - j * j >= 0 ; j ++) mem[i] = min(mem[i], 1 + mem[i - j * j]); return mem[n]; } };
-
(91解码方法) 一条包含字母
A-Z
的消息通过以下方式进行了编码:'A' -> 1 'B' -> 2 ... 'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
示例 1:
输入: "12" 输出: 2 解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
/// Memory Search /// Time Complexity: O(n) /// Space Complexity: O(n) class Solution { private: int n; vector<int> dp; public: int numDecodings(string s) { n = s.size(); dp.resize(n, -1); return dfs(s, 0); } private: int dfs(const string& s, int start){ if(start >= s.size()) return 1; if(s[start] == '0') return 0; if(dp[start] != -1) return dp[start]; int res = dfs(s, start + 1); if(start + 1 < n && s.substr(start, 2) <= "26") res += dfs(s, start + 2); return dp[start] = res; } }; /// Dynamic Programming /// Time Complexity: O(n) /// Space Complexity: O(n) class Solution { public: int numDecodings(string s) { int n = s.size(); if(n == 1 || s[0] == '0') return s[0] != '0'; vector<int> dp(n + 1, 0); dp[n] = 1; for(int i = n - 1; i >= 0; i --) if(s[i] != '0'){ dp[i] = dp[i + 1]; if(i + 1 < n && s.substr(i, 2) <= "26") dp[i] += dp[i + 2]; } return dp[0]; } };
-
(62不同路径) 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个7 x 3 的网格。有多少可能的路径?
示例:
输入: m = 3, n = 2 输出: 3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向右 -> 向下 2. 向右 -> 向下 -> 向右 3. 向下 -> 向右 -> 向右
/// Memory Search /// Time Complexity: O(m * n) /// Space Complexity: O(m * n) class Solution { public: int uniquePaths(int m, int n) { vector<vector<int>> dp(m, vector<int>(n, 0)); return dfs(m - 1, n - 1, dp); } private: int dfs(int x, int y, vector<vector<int>>& dp){ if(x == 0 || y == 0) return 1; if(dp[x][y]) return dp[x][y]; int res = dfs(x - 1, y, dp) + dfs(x, y - 1, dp); return dp[x][y] = res; } }; /// Dynamic Programming /// Time Complexity: O(m * n) /// Space Complexity: O(m * n) 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]; } };
(63不同路径II) 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用
1
和0
来表示。说明: m 和 n 的值均不超过 100。
示例 1:
输入: [ [0,0,0], [0,1,0], [0,0,0] ] 输出: 2 解释: 3x3 网格的正中间有一个障碍物。 从左上角到右下角一共有 2 条不同的路径: 1. 向右 -> 向右 -> 向下 -> 向下 2. 向下 -> 向下 -> 向右 -> 向右
/// Dynamic Programming /// Time Complexity: O(m*n) /// Space Complexity: O(m*n) class Solution { public: int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) { int m = obstacleGrid.size(); if(!m) return 0; int n = obstacleGrid[0].size(); if(!n || obstacleGrid[0][0]) return 0; vector<vector<long long>> dp(m, vector<long long>(n, 1ll)); dp[0][0] = 1; for(int j = 1; j < n; j ++) if(obstacleGrid[0][j]) dp[0][j] = 0; else dp[0][j] = dp[0][j - 1]; for(int i = 1; i < m; i ++) if(obstacleGrid[i][0]) dp[i][0] = 0; else dp[i][0] = dp[i - 1][0]; for(int i = 1; i < m; i ++) for(int j = 1; j < n; j ++) if(obstacleGrid[i][j]) dp[i][j] = 0; else dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; return dp[m - 1][n - 1]; } };
-
(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 。

- 状态定义了函数要做什么,状态转移方程定义了该怎么做
// 记忆化搜索实现版本1
class Solution {
private:
vector<int> memo;
// 考虑抢劫nums[index, nums.size()-1]这个范围中的一个房子
int tryRob(vector<int>& nums, int index) {
if (index >= nums.size())
return 0;
// 对于每一间房屋有两种选择,可以偷也可以不偷,状态转移方程中,仍然需要考虑其余所有的状态
if (memo[index] == -1) {
int maxMoney = 0;
for (int i = index; i < nums.size(); i++) {
maxMoney = max(maxMoney, nums[i] + tryRob(nums, i + 2));
}
memo[index] = maxMoney;
}
return memo[index];
}
public:
int rob(vector<int>& nums) {
if (nums.size() == 0)
return 0;
memo = vector<int>(nums.size(), -1);
return tryRob(nums, 0);
}
};
// 记忆化搜索实现版本2
class Solution {
private:
vector<int> memo;
int tryRob(vector<int>& nums, int index) {
if (index < 0)
return 0;
if (memo[index] == -1) {
int res = 0;
for (int i = index; i >= 0; i--) {
res = max(res, nums[i] + tryRob(nums, i - 2));
}
memo[index] = res;
}
return memo[index];
}
public:
int rob(vector<int>& nums) {
if (nums.size() == 0)
return 0;
memo = vector<int>(nums.size(), -1);
return tryRob(nums, nums.size() - 1);
}
};
// 动态规划实现版本1
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0)
return 0;
vector<int> memo(nums.size()+1, -1);
memo[0]=nums[0];
if(nums.size()>=2)
memo[1]=max(nums[0],nums[1]);
for(int i=2; i<nums.size(); i++){
memo[i]=max(memo[i-1], memo[i-2]+nums[i]);
}
return memo[nums.size()-1];
}
};
// 动态规划实现版本2
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() == 0)
return 0;
int n=nums.size();
vector<int> memo(n, -1);
memo[n-1]=nums[n-1];
for(int i=n-2; i>=0; i--){
for(int j=i;j++;j<n){
memo[i]=max(memo[i], nums[j]+(j+2>n ? 0 : memo[j+2]));
}
}
return memo[0];
}
};
(213打家劫舍II) 现在考虑这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
/// Dynamic Programming
/// Two Pass House Robber I Problem
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0)
return 0;
if(n == 1)
return nums[0];
if(n == 2)
return max(nums[0], nums[1]);
return max(rob(nums, 0, nums.size() - 2), rob(nums, 1, nums.size() - 1));
}
private:
int rob(const vector<int>& nums, int start, int end){
int preMax = nums[start];
int curMax = max(preMax, nums[start+1]);
for(int i = start + 2 ; i <= end ; i ++){
int temp = curMax;
curMax = max(nums[i] + preMax, curMax);
preMax = temp;
}
return curMax;
}
};
(337打家劫舍III) 小区成二叉树结构,如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
选择二叉树中不相邻的节点,使得节点之和为最大。
/// Memory Search
/// Time Complexity: O(n), where n is the nodes' number in the tree
/// Space Complexity: O(n)
class Solution {
public:
int rob(TreeNode* root) {
return rob(root, true);
}
private:
int rob(TreeNode* root, bool include){
if(root == NULL)
return 0;
int res = rob(root->left, true) + rob(root->right, true);
if(include)
res = max(root->val + rob(root->left, false) + rob(root->right, false),
res);
return res;
}
};
/// Redefine the recursive function and return a two-element array
/// represent the include and exclude maximum result for robbing the node
///
/// Time Complexity: O(n), where n is the nodes' number in the tree
/// Space Complexity: O(1)
class Solution {
public:
int rob(TreeNode* root) {
vector<int> result = tryRob(root);
return max(result[0], result[1]);
}
private:
vector<int> tryRob(TreeNode* root){
if(root == NULL)
return vector<int>(2, 0);
vector<int> resultL = tryRob(root->left);
vector<int> resultR = tryRob(root->right);
vector<int> res(2, 0);
res[0] = resultL[1] + resultR[1];
res[1] = max(res[0], root->val + resultL[0] + resultR[0]);
return res;
}
};
-
(309最佳买卖股票时机含冷冻期) 给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。示例:
输入: [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
/// Memory Search /// Time Complexity: O(n) /// Space Complexity: O(n) class Solution { private: int n; public: int maxProfit(vector<int>& prices) { n = prices.size(); if(n <= 1) return 0; vector<int> buy(n, -1), sell(n, -1); return _sell(prices, n - 1, buy, sell); } private: int _sell(const vector<int>& prices, int index, vector<int>& buy, vector<int>& sell){ if(index == 0) return 0; if(index == 1) return max(0, prices[1] - prices[0]); if(sell[index] != -1) return sell[index]; return sell[index] = max(_sell(prices, index - 1, buy, sell), _buy(prices, index - 1, buy, sell) + prices[index]); } int _buy(const vector<int>& prices, int index, vector<int>& buy, vector<int>& sell){ if(index == 0) return -prices[0]; if(index == 1) return max(-prices[0], -prices[1]); if(buy[index] != -1) return buy[index]; return buy[index] = max(_buy(prices, index - 1, buy, sell), _sell(prices, index - 2, buy, sell) - prices[index]); } }; /// Dynamic Programming /// Time Complexity: O(n) /// Space Complexity: O(n) class Solution { public: int maxProfit(vector<int>& prices) { if(prices.size() <= 1) return 0; vector<int> buy(prices.size(), 0); vector<int> sell(prices.size(), 0); buy[0] = -prices[0]; buy[1] = max(-prices[0], -prices[1]); sell[1] = max(0, buy[0] + prices[1]); for(int i = 2 ; i < prices.size() ; i ++){ sell[i] = max(sell[i-1], buy[i-1] + prices[i]); buy[i] = max(buy[i-1], sell[i-2] - prices[i]); } return sell.back(); } };
-
(0-1背包问题) 有一个背包,它的容量为C,现在有n种不同的物品,编号为0……n-1,其中每一件物品的重量为w(i),价值为v(i)。问可以向这个背包中盛放那些物品,使得在不超过这个背包的基础上,物品的总价值最大。
- 两个约束条件:在n个物品里选,重量的最大值小于等于C。
- 状态:F(n,c)考虑将n个物品放入容量为C的背包中,使得价值最大
- 状态转移方程:
F(i,c) = F(i-1,c)// 第i个物品不放入背包中 = v(i) + F(i-1, c-w(i))// 第i个物品放入背包中 // 在这两者中选最大值 F(i,c) = max(F(i-1,c), v(i) + F(i-1,c-w(i)))
class Knapsack01 { private: // 考虑将第index物品放入容量为C的背包中,返回最大的价值 // [0,index] int tryPutIn(const vector<int>& w, const vector<int>& v, int index, int C) { if (index < 0 || C <= 0) return 0; // 对于每一件物品有两种选择,放入背包或者不放入,状态转移方程中只需要考虑相邻的状态,不用考虑其余状态 return max(tryPutIn(w, v, index - 1, C), v[index] + tryPutIn(w, v, index - 1, C - w[index])); } public: int knapsack01(const vector<int>& w, const vector<int>& v, int C) { if (w.empty() || v.empty() || C <= 0) return 0; int n = w.size(); return tryPutIn(w, v, n - 1, C); } };
- 由于每一个状态是有两种约束条件,所以记忆化搜索的记忆数组需要是二维数组。
// 使用记忆化搜索实现 class Knapsack01 { private: vector<vector<int>> memo;// memo[i][j]表示背包容积为j时,考虑[0,i]的物品时价值的最大值 // 考虑将第index物品放入容量为C的背包中,返回最大的价值 // [0,index] int tryPutIn(const vector<int>& w, const vector<int>& v, int index, int C) { if (index < 0 || C <= 0) return 0; if(memo[index][C]==-1){ memo[index][C]=max(tryPutIn(w, v, index - 1, C), v[index] + tryPutIn(w, v, index - 1, C - w[index])); } return memo[index][C]; } public: int knapsack01(const vector<int>& w, const vector<int>& v, int C) { if (w.empty() || v.empty() || C <= 0) return 0; int n = w.size(); memo = vector<vector<int>>(n, vector<int>(C+1, -1)); return tryPutIn(w, v, n - 1, C); } };
- 动态规划实现版本:
竖轴0-2表示第i个物品,横轴0-5表示背包的容积,二维数组的(i,j)表示背包容量为j时,考虑第i个物品时,价值的最大值。
当背包容积为2,考虑第i=1个物品时,分两种情况:
- 不放入该物品,则价值的最大值为(0,2)=6
- 放入该物品,则背包的剩余容量变为2-2=0,则此时的价值为(0,0)+ value(1)=10
因为,10大于6,所以(2,1)处填入10
以此类推,完整的表格如下:
(2,5)的推理如下:
- 若不放入第i=2个物品,则价值的最大值为(1,5)=16
- 若放入该物品,则背包的剩余容量变为5-3=2,则此时的价值为(2,1)+ value(2)=22
因为22大于16,所以(2,5)处应填入22
class Knapsack01 { public: int knapsack01(const vector<int>& w, const vector<int>& v, int C) { if (w.empty() || v.empty() || C <= 0) return 0; int n = w.size(); vector<vector<int>> memo(n, vector<int>(C + 1, -1)); // 考虑第i=0件物品 for (int i = 0; i < C + 1; i++) memo[0][i] = (i >= w[0] ? v[0] : 0); // 考虑第i=1……n-1件物品 for (int i = 1; i < n; i++) { for (int j = 0; j < C + 1; j++) { if (j >= w[i]) { memo[i][j] = max(memo[i - 1][j], v[i] + memo[i - 1][j - w[i]]); } else memo[i][j] = memo[i - 1][j]; } } return memo[n - 1][C]; } };
- 时间复杂度为O(n*C)
- 空间复杂度为O(n*C),实际上,第i行元素只依赖第i1行元素,所以只需要保存两行元素即可。优化后的空间复杂度为O(2C)=O©
class Knapsack01 { public: int knapsack01(const vector<int>& w, const vector<int>& v, int C) { if (w.empty() || v.empty() || C <= 0) return 0; int n = w.size(); vector<vector<int>> memo(2, vector<int>(C + 1, -1)); // 考虑第i=0件物品 for (int i = 0; i < C + 1; i++) { if (i >= w[0]) memo[0][i] = v[0]; else memo[0][i] = 0; } // 考虑第i=1……n-1件物品 for (int i = 1; i < n; i++) { for (int j = 0; j < C + 1; j++) { int p=i%2, q=(i-1)%2; if (j >= w[i]) { memo[p][j] = max(memo[q][j], v[i] + memo[q][j - w[i]]); } else memo[p][j] = memo[q][j]; } } return memo[(n - 1)%2][C];// 注意最后一个元素也要改 } };
- 背包问题的变种:
- 多重背包问题:每个物品有num(i)个
- 完全不背包问题:每个物品的数量无限
- 多维费用问题:每个物品有体积和重量两个维度,并且背包也重量和体积的最大约束,要记录的数组应为三维
- 物品间加入一些约束:物品间可以相互排斥、相互依赖
-
(416 分割等和子集) 给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
- 每个数组中的元素不会超过 100
- 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5] 输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11].
在n个物品中选出一定的物品,填满sum/2的背包。
F(n,c)//状态:考虑将n个物品填满容量为C的背包 F(i,c) = F(i-1,c) // 第i个物品不放入背包中 || F(i-1, c-w(i)) // 第i个物品放入背包中
// 基础递归实现 class Solution { private: bool tryPart(vector<int> &nums, int half, int index){ if(index<0 || half<0) return false; if(half==0) return true; return (tryPart(nums, half, index-1) || tryPart(nums, half-nums[i], index-1)) } public: bool canPartition(vector<int>& nums) { int sum = 0; for (int i = 0; i < nums.size(); i++) sum += nums[i]; if (sum % 2 != 0) return false; int half = sum / 2; return tryPart(nums, half, n-1); } }; // 记忆化搜索 class Solution { private: // memo[i][c]表示使用[0,i]的物品是否可以填充容量为c的背包 // -1:未计算,0:不能填充,1:可以填充 vector<vector<int>> memo; bool tryPart(vector<int> &nums, int half, int index){ if(index<0 || half<0) return false; if(half==0) return true; if(memo[index][half] == -1){ if(tryPart(nums, half, index-1) || tryPart(nums, half-nums[index], index-1)) memo[index][half]=1; else memo[index][half]=0; } return memo[index][half]; } public: bool canPartition(vector<int>& nums) { int sum = 0; for (int i = 0; i < nums.size(); i++) sum += nums[i]; if (sum % 2 != 0) return false; int half = sum / 2; memo=vector<vector<int>>(nums.size(),vector<int>(half+1, -1)); return tryPart(nums, half, nums.size()-1); } }; // 动态规划实现(超出思维极限) class Solution { public: bool canPartition(vector<int>& nums) { int sum = 0; for (int i = 0; i < nums.size(); i++) sum += nums[i]; if (sum % 2 != 0) return false; int half = sum / 2; vector<bool> memo(half + 1, false); for (int i = 0; i < half + 1; i++) { memo[i] = (i == nums[0]); } for (int i = 1; i < nums.size(); i++) { for (int j = half; j >= nums[i]; j--) { memo[j] = memo[j] || memo[j - nums[i]]; } } return memo[half]; } };
-
(322零钱兑换) 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例:
输入: coins = [1, 2, 5], amount = 11 输出: 3 解释: 11 = 5 + 5 + 1
/// Memory Search /// /// Time Complexity: O(coins_size * amount) /// Space Complexity: O(amount) class Solution { private: vector<int> dp; int max_amount; public: int coinChange(vector<int>& coins, int amount) { max_amount = amount + 1; dp = vector<int>(amount+1, -1); int res = search(coins, amount); return res == max_amount ? -1 : res; } private: int search(const vector<int>& coins, int amount){ if(amount == 0) return 0; if(dp[amount] != -1) return dp[amount]; int res = max_amount; for(int coin: coins) if(amount - coin >= 0) res = min(res, 1 + search(coins, amount -coin)); return dp[amount] = res; } }; /// Dynamic Problem /// 0-1 backpack problem /// /// Time Complexity: O(coins_size * amount) /// Space Complexity: O(amount) class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int> dp(amount+1, amount + 1); dp[0] = 0; for(int coin: coins) for(int j = coin ; j <= amount ; j ++) dp[j] = min(dp[j], dp[j-coin] + 1); return dp[amount] == amount + 1 ? -1 : dp[amount]; } }; /// Dynamic Problem /// 0-1 backpack problem /// /// Time Complexity: O(coins_size * amount) /// Space Complexity: O(amount) class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int> dp(amount + 1, amount + 1); dp[0] = 0; for(int i = 1 ; i <= amount ; i ++) for(int coin: coins) if(i - coin >= 0) dp[i] = min(dp[i], dp[i-coin] + 1); return dp[amount] == amount + 1 ? -1 : dp[amount]; } };
-
(39组合总和) 给定一个无重复元素的数组
candidates
和一个目标数target
,找出candidates
中所有可以使数字和为target
的组合。candidates
中的数字可以无限制重复被选取。说明:
- 所有数字(包括
target
)都是正整数。 - 解集不能包含重复的组合。
示例:
输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]
/// Backtrack /// Time Complexity: O(n^n) /// Space Complexity: O(target) class Solution { public: vector<vector<int>> combinationSum(vector<int> &candidates, int target) { vector<vector<int>> res; vector<int> cur_res; solve(candidates, 0, target, cur_res, res); return res; } private: void solve(const vector<int> &candidates, int index, int target, vector<int>& cur_res, vector<vector<int>>& res){ if(target == 0){ res.push_back(cur_res); return; } for(int i = index ; i < candidates.size() ; i ++) if(target >= candidates[i]){ cur_res.push_back(candidates[i]); solve(candidates, i, target - candidates[i], cur_res, res); cur_res.pop_back(); } return; } };
(40组合总和II)
candidates
中的每个数字在每个组合中只能使用一次。示例:
输入: candidates = [10,1,2,7,6,1,5], target = 8, 所求解集为: [ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
/// Backtrack /// Time Complexity: O(n^n) /// Space Complexity: O(target) class Solution { public: vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { sort(candidates.begin(), candidates.end()); vector<vector<int>> res; vector<int> cur_res; solve(candidates, 0, target, cur_res, res); return res; } private: void solve(const vector<int> &candidates, int index, int target, vector<int>& cur_res, vector<vector<int>>& res){ if(target == 0){ res.push_back(cur_res); return; } for(int i = index ; i < candidates.size() ; i ++){ if(i > index && candidates[i] == candidates[i-1]) continue; if(target >= candidates[i]){ cur_res.push_back(candidates[i]); solve(candidates, i + 1, target - candidates[i], cur_res, res); cur_res.pop_back(); } } return; } };
(216组合总和III) 找出所有相加之和为 n 的 k* 个数的组合**。***组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
示例:
输入: k = 3, n = 7 输出: [[1,2,4]]
class Solution { public: vector<vector<int>> combinationSum3(int k, int n) { vector<vector<int>> res; vector<int> cur_res; solve(1, k, n, cur_res, res); return res; } private: void solve(int index, int k, int n, vector<int>& cur_res, vector<vector<int>>& res){ if(n == 0 && k == 0){ res.push_back(cur_res); return; } if(k == 0) return; for(int i = index ; i <= 9 ; i ++) if(n >= i){ cur_res.push_back(i); solve(i + 1, k - 1, n - i, cur_res, res); cur_res.pop_back(); } return; } };
(377组合总和IV) 给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3] target = 4 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。 因此输出为 7。
/// Memory Search /// Time Complexity: O(n * target) /// Space Complexity: O(n * target) class Solution { private: vector<int> memo; public: int combinationSum4(vector<int>& nums, int target) { if(nums.size() == 0) return 0; memo = vector<int>(target + 1, -1); solve(nums, target); return memo[target]; } private: int solve(const vector<int>& nums, int target){ if(target == 0) return 1; if(memo[target] != -1) return memo[target]; int res = 0; for(int i = 0; i < nums.size() ; i ++) if(target >= nums[i]) res += solve(nums, target - nums[i]); return memo[target] = res; } }; /// Dynamic Programming /// Time Complexity: O(n * target) /// Space Complexity: O(target) class Solution { public: int combinationSum4(vector<int>& nums, int target) { int n = nums.size(); if(n == 0) return 0; vector<int> memo(target + 1, 0); memo[0] = 1; for(int i = 1; i <= target; i++) for(int j = 0; j < n; j ++) if(nums[j] <= i){ if(memo[i] == -1 || memo[i - nums[j]] == -1 || (long long)memo[i] + (long long)memo[i - nums[j]] > INT_MAX) memo[i] = -1; else memo[i] += memo[i - nums[j]]; } assert(memo[target] != -1); return memo[target]; } };
- 所有数字(包括
-
(474一和零) 在计算机界中,我们总是追求用有限的资源获取最大的收益。
现在,假设你分别支配着 m 个
0
和 n 个1
。另外,还有一个仅包含0
和1
字符串的数组。你的任务是使用给定的 m 个
0
和 n 个1
,找到能拼出存在于数组中的字符串的最大数量。每个0
和1
至多被使用一次。示例:
输入: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出: 4 解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
/// 0-1 backsack problem /// Recursion Implimentation (Memory Search) /// Time Complexity: O(sizeof(array)*m*n) /// Space Complexity: O(sizeof(array)*m*n) class Solution { public: int findMaxForm(vector<string>& strs, int m, int n) { vector<int> mcost(strs.size(), 0), ncost(strs.size(), 0); for(int i = 0 ; i < strs.size() ; i ++) for(char c: strs[i]) if(c == '0') mcost[i] ++; else ncost[i] ++; vector<vector<vector<int>>> dp(strs.size(), vector<vector<int>>(m + 1, vector<int>(n + 1, -1))); return findMaxForm(strs.size() - 1, m, n, dp, mcost, ncost); } private: int findMaxForm(int k, int m, int n, vector<vector<vector<int>>>& dp, const vector<int>& mcost, const vector<int>& ncost){ if(k < 0) return 0; if(dp[k][m][n] != -1) return dp[k][m][n]; dp[k][m][n] = findMaxForm(k - 1, m, n, dp, mcost, ncost); if(m >= mcost[k] && n >= ncost[k]) dp[k][m][n] = max(1 + findMaxForm(k - 1, m - mcost[k], n - ncost[k], dp, mcost, ncost), dp[k][m][n]); return dp[k][m][n]; } }; /// 0-1 backsack problem /// Dynamic Programming /// Time Complexity: O(sizeof(array)*m*n) /// Space Complexity: O(sizeof(array)*m*n) class Solution { public: int findMaxForm(vector<string>& strs, int m, int n) { vector<int> mcost(strs.size(), 0), ncost(strs.size(), 0); for(int i = 0 ; i < strs.size() ; i ++) for(char c: strs[i]) if(c == '0') mcost[i] ++; else ncost[i] ++; vector<vector<vector<int>>> dp(strs.size(), vector<vector<int>>(m + 1, vector<int>(n + 1, 0))); for(int u = mcost[0]; u <= m ; u ++) for(int v = ncost[0] ; v <= n ; v ++) dp[0][u][v] = 1; for(int i = 1 ; i < strs.size() ; i ++) for(int u = 0 ; u <= m ; u ++) for(int v = 0 ; v <= n ; v ++){ dp[i][u][v] = dp[i - 1][u][v]; if(u >= mcost[i] && v >= ncost[i]) dp[i][u][v] = max(dp[i][u][v], 1 + dp[i - 1][u - mcost[i]][v - ncost[i]]); } return dp[strs.size() - 1][m][n]; } };
-
(139单词拆分) 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
示例:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
-
(494目标和) 给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入:nums: [1, 1, 1, 1, 1], S: 3 输出:5 解释: -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 一共有5种方法让最终目标和为3。
提示:
- 数组非空,且长度不会超过 20 。
- 初始的数组的和不会超过 1000 。
- 保证返回的最终结果能被 32 位整数存下。
/// Backtracking /// Time Complexity: O(2^n) /// Space Complexity: O(n) class Solution { public: int findTargetSumWays(vector<int>& nums, int S) { return dfs(nums, 0, 0, S); } private: int dfs(const vector<int>& nums, int index, int res, int S){ if(index == nums.size()) return res == S; int ret = 0; ret += dfs(nums, index + 1, res - nums[index], S); ret += dfs(nums, index + 1, res + nums[index], S); return ret; } }; /// Memory Search /// Using TreeSet /// /// Time Complexity: O(n * maxNum * log(n * maxNum)) /// Space Complexity: O(n * maxNum) class Solution { public: int findTargetSumWays(vector<int>& nums, int S) { map<pair<int, int>, int> dp; return dfs(nums, 0, S, dp); } private: int dfs(const vector<int>& nums, int index, int S, map<pair<int, int>, int>& dp){ if(index == nums.size()) return S == 0; pair<int, int> p = make_pair(index, S); if(dp.count(p)) return dp[p]; int ret = 0; ret += dfs(nums, index + 1, S - nums[index], dp); ret += dfs(nums, index + 1, S + nums[index], dp); return dp[p] = ret; } }; /// Dynamic Programming /// Using 2D-Array /// /// Time Complexity: O(n * maxNum) /// Space Complexity: O(n * maxNum) class Solution { public: int findTargetSumWays(vector<int>& nums, int S) { vector<vector<int>> dp(nums.size(), vector<int>(2001, 0)); dp[0][1000 - nums[0]] += 1; dp[0][1000 + nums[0]] += 1; for(int i = 1; i < nums.size(); i ++) for(int j = 0; j < 2001; j ++){ if(j - nums[i] >= 0 && j - nums[i] < 2001) dp[i][j] += dp[i - 1][j - nums[i]]; if(j + nums[i] >= 0 && j + nums[i] < 2001) dp[i][j] += dp[i - 1][j + nums[i]]; } if(1000 + S < 0 || 1000 + S >= 2001) return 0; return dp[nums.size() - 1][1000 + S]; } };
-
(300 最长上升子序列) 给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
状态: LIS(i)表示以第i个数字为结尾的最长上升子序列的长度 LIS(i)表示[0,i]范围内,选择数字nums[i]可以获得的最长上升子序列的长度(第i个数字在这个序列中) 状态转移方程: LIS(i)=max(1+LIS(j) if nums[i]>nums[j])
// 记忆化搜索 class Solution { private: vector<int> memo; int LIS(vector<int> &nums, int index){ if(index==0) return 1; if(memo[index]==1){ for(int i=0;i<index;i++){ if(nums[i]<nums[index]) memo[index]=max(memo[index], 1+LIS(nums, i)); } } return memo[index]; } public: int lengthOfLIS(vector<int>& nums) { memo=vector<int>(nums.size(), 1); // 不能直接返回LIS(nums, nums.size()-1) // 因为最长上升子序列并不一定是以最后一个元素结尾的 int maxLen=0; for (int i = 0; i < nums.size(); i++) { maxLen=max(maxLen,LIS(nums, i)); } return maxLen; } }; // 动态规划 class Solution { public: int lengthOfLIS(vector<int>& nums) { vector<int> memo(nums.size(), 1); for (int i = 0; i < nums.size(); i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) memo[i] = max(memo[i], 1+memo[j]); } } int maxLen=0; for(int i=0;i<memo.size();i++){ maxLen = max(maxLen, memo[i]); } return maxLen; } };
-
(376摆动序列) 如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为**摆动序列。**第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
例如,
[1,7,4,9,2,5]
是一个摆动序列,因为差值(6,-3,5,-7,3)
是正负交替出现的。相反,[1,4,7,2,5]
和[1,7,4,5,5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
示例:
输入: [1,17,5,10,13,15,10,5,16,8] 输出: 7 解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。
-
(最长公共子序列) 给出两个字符串s1和s2,求这两个字符串的最长公共子序列的长度。
示例:
输入: [A,E,B,D] [A,B,C,D] 输出: 3 解释: 最长的公共子序列是 [A,B,D],它的长度是 3。
LCS(m,n)为s1[0,m]和s2[0,n]最长公共子序列的长度 // 状态 LCS(m,n)=1+LCS(m-1,n-1) if(s1[m]==s2[n]) // 状态转移 =max(LCS(m,n-1),LCS(m-1,n)) if(s1[m]!=s2[n])
// 记忆化搜索实现 class Solution{ private: vector<vector<int>>memo; int LCS(string &s1, string &s2, int m ,int n){ if(m<0 || n<0) return 0; if(memo[m][n]==-1){ if(s1[m]==s2[n]) memo[m][n]=1+LCS(s1,s2,m-1,n-1); else memo[m][n]=max(LCS(s1,s2,m,n-1),LCS(s1,s2,m-1,n)); } return memo[m][n] } public: int lengthOfLIS(string &s1, string &s2){ memo.clear(); memo=vector<vector<int>>(s1.size(),vector<int>(s2.size(),-1)); return LCS(s1, s2, s1.size()-1, s2.size()-1); } }; // 动态规划实现1 class Solution{ public: int lengthOfLIS(string &s1, string &s2){ vector<vector<int>> memo(s1.size(),vector<int>(s2.size(),0)); // 对第0行第0列初始化 for(int i=0;i<s1.size();i++){ if(s1[i]==s2[0]){ for(int j=i;j<s1.size();j++) memo[0][j]=1; break; } } for(int i=0;i<s2.size();i++){ if(s2[i]==s1[0]){ for(int j=i;j<s2.size();j++) memo[j][0]=1; break; } } for(int i=1;i<s1.size();i++){ for(int j=1;j<s2.size();j++){ if(s1[i]==s2[j]) memo[i][j]=1+memo[i-1][j-1]; else memo=max(memo[i-1][j],memo[i][j-1]); } } }; // 动态规划实现2 class Solution{ public: int lengthOfLIS(string &s1, string &s2){ vector<vector<int>> memo(s1.size()+1,vector<int>(s2.size()+1,0)); for(int i=1;i<=s1.size();i++){ for(int j=1;j<=s2.size();j++){ if(s1[i-1]==s2[j-1]) memo[i][j]=1+memo[i-1][j-1]; else memo=max(memo[i-1][j],memo[i][j-1]); } } };