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 。
示例 3:
输入:nums = [1,2,3] 输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
1.
递归函数,只需要走一步,如果定义f函数考虑偷取[0,i]区间中的房屋使得能够偷取的最高金额.
思考能不能走一步使得出现重复子问题,也就是使得区间缩小.
如果要考虑偷取[0,i]区间中的房屋使得能够偷取的最高金额,我们可以关注某一个位置,i位置.
如果偷取i位置元素,[0,i]区间能能够偷取的最高金额就是[0,i-2]区间能够偷取的最大金额加上nums[i]房间的金额数.
如果不偷取i位置元素,[0,i]区间能够偷取的最高金额数是[0,i-1]区间能够偷取的最大金额.
走一步出现了重复的子问题.
考虑迭代递归.
using namespace std; // 使用标准命名空间std
class Solution { // 定义一个解决方案类
public:
vector<int> nums; // 存储每个房屋的金额
int n; // 房屋的数量
vector<int> dp; // 用于动态规划的数组,不包括最后一间房
vector<int> dp1; // 用于动态规划的数组,不包括第一间房
void solveinit() { // 初始化动态规划数组
n = nums.size(); // 获取房屋数量
dp.clear(), dp1.clear(); // 清空动态规划数组
dp.resize(n, -1), dp1.resize(n, -1); // 重新设定大小并初始化为-1
}
int dfs(int i) { // 第一个动态规划函数,考虑从第一间房开始,不偷最后一间房
if (i < 0) // 边界条件,如果索引小于0,则返回0
return 0;
if (dp[i] != -1) // 如果当前值已经被计算过,直接返回该值
return dp[i];
dp[i] = max(dfs(i - 2) + nums[i], dfs(i - 1)); // 状态转移方程
return dp[i]; // 返回计算结果
}
int dfs1(int i) { // 第二个动态规划函数,考虑从第二间房开始,不偷第一间房
if (i < 1) // 边界条件,如果索引小于1,则返回0
return 0;
if (dp1[i] != -1) // 如果当前值已经被计算过,直接返回该值
return dp1[i];
dp1[i] = max(dfs1(i - 2) + nums[i], dfs1(i - 1)); // 状态转移方程
return dp1[i]; // 返回计算结果
}
int rob(vector<int>& _nums) { // 主函数,计算最高可偷金额
nums = _nums; // 初始化房屋金额数组
solveinit(); // 调用初始化函数
int ret = INT_MIN; // 初始化最大金额为最小整数
if(n==1) return dfs(0); // 如果只有一间房,直接返回该房间的金额
for (int i = 0; i < n - 1; i++) { // 遍历第一个动态规划数组的每个元素
ret = max(ret, dfs(i)); // 更新最大金额
}
for (int i = 1; i < n; i++) { // 遍历第二个动态规划数组的每个元素
ret = max(dfs1(i), ret); // 更新最大金额
}
return ret; // 返回最高可偷金额
}
};
2560. 打家劫舍 IV
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组
nums
表示每间房屋存放的现金金额。形式上,从左起第i
间房屋中放有nums[i]
美元。另给你一个整数
k
,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少k
间房屋。返回小偷的 最小 窃取能力。
示例 1:
输入:nums = [2,3,5,9], k = 2 输出:5 解释: 小偷窃取至少 2 间房屋,共有 3 种方式: 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。 因此,返回 min(5, 9, 9) = 5 。
示例 2:
输入:nums = [2,7,9,3,1], k = 2 输出:2 解释:共有 7 种窃取方式。窃取能力最小的情况所对应的方式是窃取下标 0 和 4 处的房屋。返回 max(nums[0], nums[4]) = 2 。
提示:
1 <= nums.length <= 10(5)
1 <= nums[i] <= 10(9)
1 <= k <= (nums.length + 1)/2
1.
我们需要求最小的窃取能力,也就是查找目标的窃取能力,窃取能力有一个区间,上界和下界,目标窃取能力一定是在这个上界和下界之间,于是我们可以利用二分查找的思想进行查找.
2.
查找的过程固定窃取能力,使得能够满足题意要求,也就是能够偷取k个房屋,因此可以构造一个f函数,[0,j]区间以窃取能力i偷取最多能够偷取的房间数.如果大于等于k表示这个窃取能力是有效的,如果小于k表示这个窃取能力是无效的.
3.
要求[0,j]区间以i窃取能力窃取能够偷取的房间数,走一步,使得出现重复的子问题.
我们只需要关注某一个细节,点.比如是否需要偷取j位置的房屋.
如果偷取j位置的房屋,需要有一个条件,首先我的窃取能力需要大于该房屋的金额数.
此时[0,j]区间以i窃取能力偷取房屋能够最大偷取的房屋数是1+f(j-2,i).
如果不偷取j位置的房屋,此时[0,j]区间以i窃取能力偷取房屋能够最大偷取的房屋数是f(j-1,i).
class Solution { // 定义一个解决方案类
public:
int n; // 定义一个整数n用于存储数组nums的长度
vector<int> nums; // 定义一个整型向量nums用于存储每间房屋中的现金金额
int k; // 定义一个整数k用于表示小偷至少窃取的房屋数量
vector<int> dp; // 定义一个动态规划数组dp
void solveinit() { // 定义一个初始化函数
n = nums.size(); // 获取数组nums的长度并赋值给n
dp.clear(), dp.resize(n, -1); // 清空dp数组并重新设置大小为n,初始值设为-1
}
int dfs(int i, int ability) { // 定义一个深度优先搜索函数,i为当前考虑的房屋索引,ability为当前能力值
if (i < 0) // 如果索引i小于0,说明已经没有房屋可以考虑了
return 0; // 返回0表示不窃取任何房屋
if (dp[i] != -1) // 如果dp[i]已经被计算过
return dp[i]; // 直接返回已经计算的结果
if (nums[i] <= ability) { // 如果当前房屋的金额小于或等于能力值
dp[i] = dfs(i - 2, ability) + 1; // 递归调用dfs计算跳过一个房屋后的结果,并增加当前房屋
}
dp[i] = max(dfs(i - 1, ability), dp[i]); // 计算不包括当前房屋的情况,取最大值
dp[i] = max(0, dp[i]); // 确保dp[i]不为负数
return dp[i]; // 返回计算结果
}
int minCapability(vector<int>& _nums, int _k) { // 定义主函数计算小偷的最小窃取能力
nums = _nums, k = _k; // 初始化nums和k
solveinit(); // 调用初始化函数
int l, r; // 定义两个整数l和r用于二分搜索的上下界
l = nums[0], r = nums[n - 1]; // 初始设定l为第一个元素值,r为最后一个元素值
for (int i = 0; i < n; i++) { // 遍历nums数组
if (nums[i] < l) // 找到数组中的最小值
l = nums[i];
if (nums[i] > r) // 找到数组中的最大值
r = nums[i];
}
int ans; // 定义一个整数ans用于存储结果
while (l <= r) { // 当l小于等于r时进行循环
int mid = (l + r) >> 1; // 计算中点值
solveinit(); // 重新初始化dp数组
if (dfs(n - 1, mid) >= k) { // 如果以mid为能力值,能窃取的房屋数大于等于k
ans = mid; // 更新答案
r = mid - 1; // 调整上界
} else {
l = mid + 1; // 调整下界
}
}
return ans; // 返回最终的最小能力值
}
};
面试题 17.24. 最大子矩阵
给定一个正整数、负整数和 0 组成的 N × M 矩阵,编写代码找出元素总和最大的子矩阵。
返回一个数组
[r1, c1, r2, c2]
,其中r1
,c1
分别代表子矩阵左上角的行号和列号,r2
,c2
分别代表右下角的行号和列号。若有多个满足条件的子矩阵,返回任意一个均可。注意:本题相对书上原题稍作改动
示例:
输入: [ [-1,0], [0,-1] ]输出:[0,1,0,1] 解释:输入中标粗的元素即为输出所表示的矩阵
说明:
1 <= matrix.length, matrix[0].length <= 200
1.
子矩阵的最大累加和,可以利用矩阵压缩的思想.
x,x,x,x,x,x,
x,x,x,x,x,x,
是一个子矩阵,那么将这个矩阵压缩成一维的矩阵,
y,y,y,y,y,y,
也就是将两行元素,压缩在一起,每一列的元素是对应行元素的累加形式.
第一列的所有元素累加和构成压缩后的第一列,第二列的所有元素累加和构成压缩后的第二列.以此类推
原子矩阵累加和等价于压缩之后的子矩阵的子数组和.
因此我们只需要考虑可能的行然后求区子数组和即可.
2.
dp表存储压缩矩阵每一个子数组结尾位置的累加和以及开头下标.
子数组进行分区,以某一个位置结尾的子数组.然后考虑这个小的集合的子数组中的累加和.
class Solution { // 定义一个解决方案类
public:
vector<vector<int>> matrix; // 用于存储输入的矩阵
vector<int> nums; // 用于存储处理过程中的列和
vector<vector<int>> dp; // 动态规划数组,用于存储最大子数组和及其开始索引
int row, col; // 矩阵的行数和列数
int a, b, c, d; // 存储最大子矩阵的左上角和右下角的行列索引
int up, down; // 当前考虑的行的上界和下界
void solveinit() { // 初始化函数
row = matrix.size(), col = matrix[0].size(); // 获取矩阵的行数和列数
nums.clear(), nums.resize(col, 0); // 初始化nums数组为列数大小的0
dp.clear(), dp.resize(col, vector<int>(2, INT_MIN)); // 初始化dp数组为列数大小,每个元素是包含两个INT_MIN的数组
}
void dfsinit() { // 另一个初始化函数,用于每次考虑新的行组合时调用
nums.clear(), nums.resize(col, 0); // 重新初始化nums数组
dp.clear(), dp.resize(col, vector<int>(2, INT_MIN)); // 重新初始化dp数组
for (int i = 0; i < col; i++) { // 遍历每一列
for (int j = up; j <= down; j++) { // 对于每一列,加上当前考虑的行范围内该列的值
nums[i] += matrix[j][i];
}
}
}
int dfs(int i) { // 动态规划递归函数,用于计算最大子数组和
if (i < 0) // 边界条件,当索引小于0时返回0
return 0;
if (dp[i][0] != INT_MIN) // 如果当前列的结果已计算过,则直接返回
return dp[i][0];
int prev_sum = dfs(i - 1); // 递归调用,计算前一列的最大和
if (prev_sum > 0) { // 如果前一列的最大和大于0,表示可以将当前列加到前一列的子数组中
dp[i][0] = prev_sum + nums[i];
dp[i][1] = dp[i - 1][1]; // 更新子数组的起始索引为前一列的起始索引
} else {
dp[i][0] = nums[i]; // 否则当前列自身作为一个新的子数组的开始
dp[i][1] = i;
}
return dp[i][0]; // 返回当前列的最大子数组和
}
vector<int> getMaxMatrix(vector<vector<int>>& _matrix) { // 主函数,输入矩阵并返回最大子矩阵的边界索引
matrix = _matrix; // 初始化矩阵
solveinit(); // 调用初始化函数
int ans = INT_MIN; // 初始化最大和为最小整数
a = b = c = d = -1; // 初始化最大子矩阵的索引为-1
for (up = 0; up < row; up++) { // 遍历每一个可能的行上界
nums.clear(), nums.resize(col, 0); // 重新初始化nums数组
for (down = up; down < row; down++) { // 遍历每一个可能的行下界
dp.clear(), dp.resize(col, vector<int>(2, INT_MIN)); // 重新初始化dp数组
for (int i = 0; i < col; i++) { // 遍历每一列
nums[i] += matrix[down][i]; // 更新当前列的累加和
int cur_sum = dfs(i); // 调用动态规划递归函数计算当前列的最大子数组和
if (cur_sum > ans) { // 如果当前计算得到的子数组和大于之前记录的最大和
ans = cur_sum; // 更新最大和
a = up, b = dp[i][1]; // 更新最大子矩阵的左上角坐标
c = down, d = i; // 更新最大子矩阵的右下角坐标
}
}
}
}
return { a, b, c, d }; // 返回最大子矩阵的左上角和右下角的行列索引
}
};
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!