打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 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
,其中 dp[i]
表示 [0, i]
区间可以抢夺的最大值,对当前i
来说,有抢和不抢两种互斥的选择,不抢即为 dp[i-1]
(等价于去掉 nums[i]
只抢 [0, i-1]
区间最大值),抢即为 dp[i-2] + nums[i]
(等价于去掉 nums[i-1]
)。
2 | 7 | 9 | 3 | 1 | |
---|---|---|---|---|---|
dp | 2 | 7 | 11 | 11 | 12 |
可以得到状态转移方程 dp[i] = max(num[i] + dp[i - 2]
, dp[i - 1]
), 且需要初始化 dp[0]
和 dp[1]
,其中 dp[0]
即为 num[0]
,dp[1]
此时应该为 max(num[0], num[1]
),代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() <= 1) return nums.empty() ? 0 : nums[0];
vector<int> dp = {nums[0], max(nums[0], nums[1])};
for (int i = 2; i < nums.size(); ++i) {
dp.push_back(max(nums[i] + dp[i - 2], dp[i - 1]));
}
return dp.back();
}
};
分别维护两个变量 robEven
和 robOdd
,
robEven
就是要抢偶数位置的房子,
robOdd
就是要抢奇数位置的房子。
所以在遍历房子数组时,如果是偶数位置,那么 robEven
就要加上当前数字,然后和 robOdd
比较,取较大的来更新 robEven
。这里就看出来了,robEven
组成的值并不是只由偶数位置的数字,只是当前要抢偶数位置而已。
同理,当奇数位置时,robOdd
加上当前数字和 robEven
比较,取较大值来更新 robOdd
,这种按奇偶分别来更新的方法,可以保证组成最大和的数字不相邻,最后别忘了在 robEven
和 robOdd
种取较大值返回,代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
int robEven = 0, robOdd = 0, n = nums.size();
for (int i = 0; i < n; ++i) {
if (i % 2 == 0) {
robEven = max(robEven + nums[i], robOdd);
} else {
robOdd = max(robEven, robOdd + nums[i]);
}
}
return max(robEven, robOdd);
}
};
打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
动态规划
因为首尾相连了,所以第一家和最后一家只能抢其中的一家,或者都不抢,那这里变通一下,如果把第一家和最后一家分别去掉,各算一遍能抢的最大值,然后比较两个值取其中较大的一个即为所求。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() <= 1) return nums.empty() ? 0 : nums[0];
return max(rob(nums, 0, nums.size() - 1), rob(nums, 1, nums.size()));
}
int rob(vector<int> &nums, int left, int right) {
if (right - left <= 1) return nums[left];
vector<int> dp(right, 0);
dp[left] = nums[left];
dp[left + 1] = max(nums[left], nums[left + 1]);
for (int i = left + 2; i < right; ++i) {
dp[i] = max(nums[i] + dp[i - 2], dp[i - 1]);
}
return dp.back();
}
};
使用两个变量来代替整个 DP 数组,分别维护两个变量 robEven
和 robOdd
,
robEven
就是要抢偶数位置的房子,robOdd
就是要抢奇数位置的房子。所以在遍历房子数组时,如果是偶数位置,那么 robEven
就要加上当前数字,然后和 robOdd
比较,取较大的来更新 robEven
。
这里就看出来了,robEven
组成的值并不是只由偶数位置的数字,只是当前要抢偶数位置而已。同理,当奇数位置时,robOdd
加上当前数字和 robEven
比较,取较大值来更新 robOdd
,这种按奇偶分别来更新的方法,可以保证组成最大和的数字不相邻,
最后别忘了在 robEven
和 robOdd
种取较大值返回,代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size() <= 1) return nums.empty() ? 0 : nums[0];
return max(rob(nums, 0, nums.size() - 1), rob(nums, 1, nums.size()));
}
int rob(vector<int> &nums, int left, int right) {
int robEven = 0, robOdd = 0;
for (int i = left; i < right; ++i) {
if (i % 2 == 0) {
robEven = max(robEven + nums[i], robOdd);
} else {
robOdd = max(robEven, robOdd + nums[i]);
}
}
return max(robEven, robOdd);
}
};
打家劫舍 III
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/
2 3
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/
4 5
/
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
DFS
典型的递归问题,可以利用回溯法来做,因为当前的计算需要依赖之前的结果,那么对于某一个节点,
如果其左子节点存在,通过递归调用函数,算出不包含左子节点返回的值,
同理,如果右子节点存在,算出不包含右子节点返回的值,那么此节点的最大值可能有两种情况,
-
一种是该节点值加上不包含左子节点和右子节点的返回值之和,
-
另一种是左右子节点返回值之和不包含当期节点值,取两者的较大值返回即可,
但是这种方法无法通过 OJ,超时了,所以必须优化这种方法,这种方法重复计算了很多地方,
比如要完成一个节点的计算,就得一直找左右子节点计算,可以把已经算过的节点用 HashMap
保存起来,
以后递归调用的时候,现在 HashMap
里找,如果存在直接返回,如果不存在,等计算出来后,保存到 HashMap
中再返回,这样方便以后再调用,
参见代码如下:
class Solution {
public:
int rob(TreeNode* root) {
unordered_map<TreeNode*, int> m;
return dfs(root, m);
}
int dfs(TreeNode *root, unordered_map<TreeNode*, int> &m) {
if (!root) return 0;
if (m.count(root)) return m[root];
int val = 0;
if (root->left) {
val += dfs(root->left->left, m) + dfs(root->left->right, m);
}
if (root->right) {
val += dfs(root->right->left, m) + dfs(root->right->right, m);
}
val = max(val + root->val, dfs(root->left, m) + dfs(root->right, m));
m[root] = val;
return val;
}
};
下面的递归函数返回一个大小为2的一维数组 res,
其中 res[0]
表示不包含当前节点值的最大值,res[1]
表示包含当前值的最大值,
那么在遍历某个节点时,首先对其左右子节点调用递归函数,
分别得到包含与不包含左子节点值的最大值,和包含于不包含右子节点值的最大值,
则当前节点的 res[0] 就是左子节点两种情况的较大值加上右子节点两种情况的较大值,
res[1] 就是不包含左子节点值的最大值加上不包含右子节点值的最大值,和当前节点值之和,返回即可,
参见代码如下:
class Solution {
public:
int rob(TreeNode* root) {
vector<int> res = dfs(root);
return max(res[0], res[1]);
}
vector<int> dfs(TreeNode *root) {
if (!root) return vector<int>(2, 0);
vector<int> left = dfs(root->left);
vector<int> right = dfs(root->right);
vector<int> res(2, 0);
res[0] = max(left[0], left[1]) + max(right[0], right[1]);
res[1] = left[0] + right[0] + root->val;
return res;
}
};
买卖股票的最佳时机
给定一个数组,它的第 i
个元素是一支给定股票第 i
天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
最简方法
遍历一次数组,用一个变量记录遍历过数中的最小值,然后每次计算当前值和这个最小值之间的差值最为利润,然后每次选较大的利润来更新。当遍历完成后当前利润即为所求,代码如下:
C++
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0, buy = INT_MAX;
for (int price : prices) {
buy = min(buy, price);
res = max(res, price - buy);
}
return res;
}
};
Java
public class Solution {
public int maxProfit(int[] prices) {
int res = 0, buy = Integer.MAX_VALUE;
for (int price : prices) {
buy = Math.min(buy, price);
res = Math.max(res, price - buy);
}
return res;
}
}
动态规划
Java
int n = prices.length;
int[][] dp = new int[n][2];
for (int i = 0; i < n; i++) {
if (i - 1 == -1) {
dp[i][0] = 0;
// 解释:
// dp[i][0]
// = max(dp[-1][0], dp[-1][1] + prices[i])
// = max(0, -infinity + prices[i]) = 0
dp[i][1] = -prices[i];
//解释:
// dp[i][1]
// = max(dp[-1][1], dp[-1][0] - prices[i])
// = max(-infinity, 0 - prices[i])
// = -prices[i]
continue;
}
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];
买卖股票的最佳时机 II
给定一个数组,它的第 i
个元素是一支给定股票第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释:
在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 =5-1=4 。
在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 =6-3=3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释:
在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 =5-1=4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
最简方法
从第二天开始,如果当前价格比之前价格高,则把差值加入利润中,因为我们可以昨天买入,今日卖出,若明日价更高的话,还可以今日买入,明日再抛出。以此类推,遍历完整个数组后即可求得最大利润。代码如下:
C++
class Solution {
public:
int maxProfit(vector<int>& prices) {
int res = 0, n = prices.size();
for (int i = 0; i < n - 1; ++i) {
if (prices[i] < prices[i + 1]) {
res += prices[i + 1] - prices[i];
}
}
return res;
}
};
Java
public class Solution {
public int maxProfit(int[] prices) {
int res = 0;
for (int i = 0; i < prices.length - 1; ++i) {
if (prices[i] < prices[i + 1]) {
res += prices[i + 1] - prices[i];
}
}
return res;
}
}
动态规划
Java
int maxProfit_k_inf(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}
return dp_i_0;
}
买卖股票的最佳时机 III
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释:
在第4天(股票价格=0)的时候买入,在第6天(股票价格=3)的时候卖出,这笔交易所能获得利润=3-0=3。
在第7天(股票价格=1)的时候买入,在第8天(股票价格=4)的时候卖出,这笔交易所能获得利润 =4-1=3。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释:
在第1天(股票价格=1)的时候买入,在第5天(股票价格=5)的时候卖出,这笔交易所能获得利润=5-1=4。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。
动态规划
求至少k次交易的最大利润,找到通解后可以设定 k = 2。
我们定义local[i][j]
为在到达第i
天时最多可进行j
次交易并且最后一次交易在最后一天卖出的最大利润,此为局部最优。
然后我们定义global[i][j]
为在到达第i天时最多可进行j
次交易的最大利润,此为全局最优。它们的递推式为:
local[i][j] = max(global[i - 1][j - 1] + max(diff, 0), local[i - 1][j] + diff)
global[i][j] = max(local[i][j], global[i - 1][j])
其中局部最优值是比较前一天并少交易一次的全局最优加上大于0的差值,和前一天的局部最优加上差值中取较大值,而全局最优比较局部最优和前一天的全局最优,代码如下:
C++
class Solution {
public:
int maxProfit(vector<int> &prices) {
if (prices.empty()) return 0;
int n = prices.size(), g[n][3] = {0}, l[n][3] = {0};
for (int i = 1; i < prices.size(); ++i) {
int diff = prices[i] - prices[i - 1];
for (int j = 1; j <= 2; ++j) {
l[i][j] = max(g[i - 1][j - 1] + max(diff, 0), l[i - 1][j] + diff);
g[i][j] = max(l[i][j], g[i - 1][j]);
}
}
return g[n - 1][2];
}
};
用一维数组来代替二维数组,可以极大的节省了空间,由于覆盖的顺序关系,我们需要j
从2到1,这样可以取到正确的g[j-1]
值,而非已经被覆盖过的值,参见代码如下:
C++
class Solution {
public:
int maxProfit(vector<int> &prices) {
if (prices.empty()) return 0;
int g[3] = {0};
int l[3] = {0};
for (int i = 0; i < prices.size() - 1; ++i) {
int diff = prices[i + 1] - prices[i];
for (int j = 2; j >= 1; --j) {
l[j] = max(g[j - 1] + max(diff, 0), l[j] + diff);
g[j] = max(l[j], g[j]);
}
}
return g[2];
}
};
三维动态规划
Java
int max_k = 2;
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++) {
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) {
dp[-1][k][0] = dp[i][0][0] = 0;
dp[-1][k][1] = dp[i][0][1] = Math.MIN_INT;
}
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
}
// 穷举了 n × max_k × 2 个状态。
return dp[n - 1][max_k][0];
买卖股票的最佳时机 IV
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [2,4,1], k = 2
输出: 2
解释:
在第1天 (股票价格=2)的时候买入,在第2天(股票价格=4)的时候卖出,这笔交易所能获得利润=4-2=2。
示例 2:
输入: [3,2,6,5,0,3], k = 2
输出: 7
解释:
在第2天(股票价格=2)的时候买入,在第3天(股票价格=6)的时候卖出, 这笔交易所能获得利润=6-2=4。
在第5天(股票价格=0)的时候买入,在第6天(股票价格=3)的时候卖出, 这笔交易所能获得利润=3-0=3。
动态规划
求至少k次交易的最大利润。我们定义local[i][j]
为在到达第i
天时最多可进行j
次交易并且最后一次交易在最后一天卖出的最大利润,此为局部最优。然后我们定义global[i][j]
为在到达第i
天时最多可进行j
次交易的最大利润,此为全局最优。它们的递推式为:
local[i][j] = max(global[i - 1][j - 1] + max(diff, 0), local[i - 1][j] + diff)
global[i][j] = max(local[i][j], global[i - 1][j]),
其中局部最优值是比较前一天并少交易一次的全局最优加上大于0的差值,和前一天的局部最优加上差值后相比,两者之中取较大值,而全局最优比较局部最优和前一天的全局最优。
但这道题还有个坑,就是如果k的值远大于prices的天数,比如k是好几百万,而prices的天数就为若干天的话,上面的DP解法就非常的没有效率
C++
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
if (prices.empty()) return 0;
if (k >= prices.size()) return solveMaxProfit(prices);
int g[k + 1] = {0};
int l[k + 1] = {0};
for (int i = 0; i < prices.size() - 1; ++i) {
int diff = prices[i + 1] - prices[i];
for (int j = k; j >= 1; --j) {
l[j] = max(g[j - 1] + max(diff, 0), l[j] + diff);
g[j] = max(g[j], l[j]);
}
}
return g[k];
}
int solveMaxProfit(vector<int> &prices) {
int res = 0;
for (int i = 1; i < prices.size(); ++i) {
if (prices[i] - prices[i - 1] > 0) {
res += prices[i] - prices[i - 1];
}
}
return res;
}
};
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
//Naive solution:
/*int n=prices.size();
if(n==0) return 0;
int dp[n][k+1][2];
dp[0][k][0]=0;
dp[0][k][1]=-prices[0];
for(int i=k-1;i>=0;i--){
dp[0][i][0]=-9999;
dp[0][i][1]=-9999;
}
for(int i=1;i<n;i++){
for(int j=k;j>=0;j--){
if(j==k)
dp[i][k][0]=0;
else
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j+1][1]+prices[i]);
dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j][0]-prices[i]);
}
}
int max=-9999;
for(int i=0;i<=k;i++){
if(dp[n-1][i][0]>max){
max=dp[n-1][i][0];
}
}
return max ;
}*/
//Loop array solution:
//我們只需要前一次的數據,因此可通過循環數組節省空間
long n=prices.size();
if(n==0) return 0;
int ret;//相當於是無限次購買的情況
if(k >= n / 2)
{
ret = 0;
for(int i = 1; i < n; ++i)
{
if(prices[i] > prices[i - 1])
ret += prices[i] - prices[i - 1];
}
return ret;
}
int dp[2][k+1][2];
dp[0][k][0]=0;
dp[0][k][1]=-prices[0];
for(int i=k-1;i>=0;i--){
dp[0][i][0]=-9999;
dp[0][i][1]=-9999;
}
for(long i=1;i<n;i++){
for(int j=k;j>=0;j--){
if(j==k)
dp[i%2][k][0]=0;
else
dp[i%2][j][0]=max(dp[(i-1)%2][j][0],dp[(i-1)%2][j+1][1]+prices[i]);
dp[i%2][j][1]=max(dp[(i-1)%2][j][1],dp[(i-1)%2][j][0]-prices[i]);
}
}
long max=-9999;
for(long i=0;i<=k;i++){
if(dp[(n-1)%2][i][0]>max){
max=dp[(n-1)%2][i][0];
}
}
return (int)max ;
}
};
三维动态规划
Java
int maxProfit_k_any(int max_k, int[] prices) {
int n = prices.length;
if (max_k > n / 2)
return maxProfit_k_inf(prices);
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++)
for (int k = max_k; k >= 1; k--) {
if (i - 1 == -1) {
dp[-1][k][0] = dp[i][0][0] = 0;
dp[-1][k][1] = dp[i][0][1] = Math.MIN_INT;
}
dp[i][k][0] = Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
dp[i][k][1] = Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
}
return dp[n - 1][max_k][0];
}
int maxProfit_k_inf(int[] prices) {
int n = prices.length;
int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}
return dp_i_0;
}