【七十八】【算法分析与设计】213. 打家劫舍 II,2560. 打家劫舍 IV,面试题 17.24. 最大子矩阵,压缩矩阵,子矩阵累加和转化为子数组累加和,求子数组最大累加和的区间

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 };  // 返回最大子矩阵的左上角和右下角的行列索引
        }

};

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

妖精七七_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值