打家劫舍Ⅱ 延伸学习笔记

打家劫舍Ⅱ 延伸学习笔记

一、问题回顾与核心约束

环形街道上的房屋偷窃问题,核心条件与基础打家劫舍一致:相邻房屋不能同时偷窃,否则触发报警;额外约束为房屋呈环形排列,即第一个房屋与最后一个房屋相邻,因此两者无法同时偷窃。给定非负整数数组nums,每个元素代表对应房屋的现金金额,目标是求解不触发警报前提下,能偷窃到的最高金额。

问题的核心难点在于"环形"带来的边界限制------常规线性结构的解法无法直接复用,需先拆解环形约束,再落地求解逻辑。此处先明确基础前提:nums为非负整数数组,长度n≥1;当n=1时,唯一房屋即为最优解,无需考虑环形约束,这是后续所有优化的基础边界条件。

二、常规解法梳理(从基础到可行)

2.1 环形约束的初步拆解

环形结构的核心矛盾是"首尾不能同时选",因此最直接的拆解思路的是将环形问题转化为两个独立的线性问题,这是常规解法的核心突破口,也是后续所有优化的基础框架:

  1. 不考虑第一个房屋,仅求解区间nums[1]~nums[n-1]的最高金额(此时首尾约束自然解除,因为第一个房屋不被选中,最后一个房屋可自由选择,符合线性结构);

  2. 不考虑最后一个房屋,仅求解区间nums[0]~nums[n-2]的最高金额(同理,最后一个房屋不被选中,第一个房屋可自由选择,同样转化为线性结构);

最终结果取上述两个线性区间解的最大值,即为环形问题的可行解。这一拆解思路无多余假设,符合问题约束,是后续所有优化的基石,也是工程实现中"先保证正确,再追求高效"的首要原则。

2.2 线性区间的常规动态规划实现

对于拆解后的线性区间,常规解法采用动态规划(DP)求解,核心逻辑围绕"状态定义+状态转移"展开,此处先梳理最基础的DP实现,不追求性能优化,仅保证逻辑正确:

状态定义:dp[i]表示前i间房屋(对应线性区间内的前i个元素)能偷窃到的最高金额;

状态转移方程:对于第i间房屋(线性区间内的第i个元素),有两种选择:

  • 不偷窃第i间房屋:此时最高金额与前i-1间房屋的最高金额一致,即dp[i] = dp[i-1];

  • 偷窃第i间房屋:此时不能偷窃第i-1间房屋,最高金额为前i-2间房屋的最高金额加上第i间房屋的金额,即dp[i] = dp[i-2] + nums[i];

因此,状态转移的核心是取两种选择的最大值:dp[i] = max(dp[i-1], dp[i-2] + nums[i])。

边界条件:对于线性区间的起始位置,当区间内只有1间房屋(即线性区间长度为1)时,dp[0] = nums[start](start为区间起始索引);当区间长度为2时,dp[1] = max(nums[start], nums[start+1])。

基于此,线性区间的常规DP实现(以Java为例)大致如下(未优化空间):

java 复制代码
// 计算线性区间 [start, end] 的最高金额(基础DP实现,未优化空间)
private int robRangeBasic(int[] nums, int start, int end) {
    int len = end - start + 1;
    if (len == 0) return 0;
    int[] dp = new int[len];
    dp[0] = nums[start];
    if (len == 1) return dp[0];
    dp[1] = Math.max(nums[start], nums[start+1]);
    for (int i = 2; i < len; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[start + i]);
    }
    return dp[len - 1];
}

此时,环形问题的常规解法即为调用两次上述方法,取最大值,整体代码逻辑完整、易理解,适合初期调试和逻辑验证,但存在可优化的空间------空间复杂度较高。

2.3 常规解法的性能瓶颈分析

上述常规实现的性能表现如下:

  • 时间复杂度:O(n),两个线性区间的遍历均为O(n),整体遍历次数为2n,属于线性时间,无明显时间瓶颈;

  • 空间复杂度:O(n),每个线性区间的DP求解需额外开辟长度为n的数组,用于存储状态值。

从问题规模来看,当n较小时(如n≤1e4),O(n)的空间复杂度完全可接受,工程实现中无需优先优化;但当n增大(如n≥1e5,甚至1e6),额外开辟的数组会占用较多内存,可能导致内存利用率下降,尤其在高性能计算场景中,内存带宽是关键瓶颈,多余的内存占用会间接影响程序执行效率,因此空间优化成为后续深入的核心方向。

三、逐步深入:从空间优化到最优实现

3.1 空间优化的核心思路(状态压缩)

回顾常规DP的状态转移方程,发现dp[i]的计算仅依赖于dp[i-1]和dp[i-2]两个前序状态,无需存储整个dp数组------这是状态压缩的核心前提,也是动态规划问题中常见的空间优化手段,符合"用时间换空间"或"用常数空间替代线性空间"的高性能优化思路。

具体优化逻辑:用两个常数变量替代dp数组,分别存储dp[i-1]和dp[i-2]的值,每次遍历更新这两个变量,即可完成状态转移,无需额外开辟数组。

变量定义调整:

  • first:对应dp[i-2],表示前i-2间房屋的最高金额;

  • second:对应dp[i-1],表示前i-1间房屋的最高金额;

遍历过程中,对于当前房屋nums[i],计算当前状态的最大值temp = max(second, first + nums[i]),随后更新first = second、second = temp,完成一次状态转移。遍历结束后,second即为当前线性区间的最高金额。

3.2 优化后的线性区间实现(空间O(1))

基于上述状态压缩思路,优化后的线性区间求解方法如下(Java为例),也是目前该问题的最优空间实现:

java 复制代码
// 计算线性区间 [start, end] 的最高金额(空间优化版,O(1)空间)
private int robRange(int[] nums, int start, int end) {
    int first = 0; // 对应dp[i-2]
    int second = 0; // 对应dp[i-1]
    for (int i = start; i <= end; i++) {
        int temp = second;
        second = Math.max(second, first + nums[i]);
        first = temp;
    }
    return second;
}

此时,环形问题的整体实现即为:判断数组长度n,若n=1则返回nums[0],否则返回max(robRange(nums, 0, n-2), robRange(nums, 1, n-1))。

优化后的性能表现:

  • 时间复杂度:仍为O(n),遍历次数未变,仅调整了状态存储方式,时间效率与常规DP一致;

  • 空间复杂度:降至O(1),仅使用3个常数变量(first、second、temp),无论n多大,额外内存占用均为常数级,这是该问题的最优空间复杂度,也是高性能实现的关键一步。

3.3 进一步优化:边界条件的精简与遍历效率

在上述最优空间实现的基础上,可进一步做细节优化,提升工程执行效率,尤其适配大规模数组场景,这也是高性能计算中"细节决定效率"的体现,优化方向围绕边界条件和遍历逻辑展开,不改变核心算法:

  1. 边界条件的提前判断:在robRange方法中,可提前判断start == end的情况(即线性区间长度为1),直接返回nums[start],避免进入循环,减少不必要的计算;

  2. 遍历变量的复用:在整体方法中,无需重复判断n的边界,可将n=1的情况与后续逻辑整合,但需保证代码可读性,避免过度优化导致逻辑混乱;

  3. 减少临时变量的重复创建:在循环内部,temp变量仅用于临时存储second的值,无法进一步精简,但可保证每次循环仅创建一次,避免冗余操作。

优化后的整体实现(Java为例):

java 复制代码
class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }
        // 复用robRange方法,避免代码冗余
        return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
    }
    
    private int robRange(int[] nums, int start, int end) {
        // 提前判断区间长度为1的情况,避免循环
        if (start == end) {
            return nums[start];
        }
        int first = 0;
        int second = 0;
        for (int i = start; i <= end; i++) {
            int temp = second;
            second = Math.max(second, first + nums[i]);
            first = temp;
        }
        return second;
    }
}

该优化属于"工程级细节优化",在小规模数组中性能提升不明显,但在大规模数组(n≥1e6)场景中,可减少循环次数和冗余判断,间接提升内存访问效率和执行速度,符合高性能计算中"优化每一处可复用逻辑"的思路。

四、工程实现中的思考(贴合高性能场景)

此处不刻意强调高性能,仅结合问题本身,梳理工程实现中需注意的细节,这些细节也是高性能计算中"正确性优先、效率为辅、可维护性并重"的核心原则体现:

4.1 输入合法性校验

工程场景中,输入数组nums可能存在空数组(n=0)的情况,虽然题目明确nums为非负整数数组,但实际实现中需考虑边界容错------若n=0,返回0(无房屋可偷),避免数组越界异常。这一校验不影响算法性能,但能提升代码的健壮性,避免线上异常,是工程实现的基础要求。

补充后的边界判断:

java 复制代码
public int rob(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int n = nums.length;
    if (n == 1) {
        return nums[0];
    }
    return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
}

4.2 内存访问效率的隐性优化

在高性能计算中,内存访问效率是影响程序执行速度的关键因素之一,尤其对于大规模数组,连续内存访问比随机访问效率高得多。本题中,nums数组为连续存储的数组,遍历过程中(i从start到end),访问nums[i]属于连续内存访问,无需额外优化;但需注意,避免在循环内部做无关的数组访问操作,减少缓存失效的概率。

例如,避免在循环中重复计算nums[start + i](常规DP中曾出现),改为直接访问nums[i](当前优化后的写法),虽然计算量一致,但减少了一次加法运算,同时保证了内存访问的连续性,这是隐性的性能优化,也是工程实现中"细节优化积累高效能"的体现。

4.3 算法的可扩展性与复用性

本题的核心是"环形约束拆解+线性区间DP求解",这一思路可扩展到类似的环形优化问题(如环形数组的最大子数组和等)。工程实现中,将线性区间的求解封装为独立的robRange方法,不仅提升了代码可读性,也实现了逻辑复用------若后续需求调整(如改为环形街道的其他约束),可直接复用robRange方法,仅修改整体逻辑的拆解方式,符合"模块化设计"的工程原则,也便于后续维护和优化。

4.4 性能与可读性的平衡

高性能计算并非"越优越好",而是要在性能、可读性、可维护性之间找到平衡。本题中,状态压缩后的O(1)空间实现,既保证了高性能,又未牺牲代码可读性;若进一步追求极致性能(如手动展开循环、使用寄存器变量),虽然能小幅提升执行速度,但会导致代码可读性下降,后续调试和维护成本增加,不符合工程实际需求。

因此,工程实现中,优先选择"逻辑清晰、性能达标"的实现方式,仅在大规模数据场景下,再做针对性的极致优化,这是高性能计算在工程实践中的核心准则之一。

五、总结与延伸

打家劫舍Ⅱ的求解过程,本质是"约束拆解+算法优化"的过程:先将环形约束拆解为两个线性约束,再通过动态规划求解线性区间,最后通过状态压缩实现空间优化,逐步从"可行"走向"最优"。

从高性能计算和工程实现的角度来看,本题的核心启示的是:

  1. 复杂问题的求解,优先拆解为已知的简单问题(环形→线性),再基于简单问题的解法做优化,避免从零构建复杂逻辑;

  2. 算法优化需循序渐进,先保证逻辑正确,再优化空间、时间效率,最后打磨工程细节,符合"迭代式优化"的思路;

  3. 高性能并非孤立存在,需结合内存访问、代码复用、可读性等工程因素,才能实现"高效且实用"的代码。

延伸思考:若房屋数量极大(如n≥1e7),单线程遍历可能无法满足性能需求,可考虑并行计算(如将两个线性区间的求解分配到两个线程,并行执行,最后取最大值),这是高性能计算中"并行优化"的思路,后续可进一步探索并行实现的细节的和线程安全问题。

相关推荐
weixin_448119942 小时前
Datawhale 硅基生物进化论 202602第2次笔记
笔记
Peter·Pan爱编程3 小时前
NVIDIA DKMS 驱动构建失败修复笔记
笔记·cuda
Дерек的学习记录10 小时前
C++:入门基础(下)
开发语言·数据结构·c++·学习·算法·visualstudio
半壶清水10 小时前
[软考网规考点笔记]-OSI参考模型与TCP/IP体系结构
网络·笔记·tcp/ip
前路不黑暗@11 小时前
Java项目:Java脚手架项目的公共模块的实现(二)
java·开发语言·spring boot·学习·spring cloud·maven·idea
哎呦 你干嘛~12 小时前
MODBUS_RTU485通讯主站(配置部分)
学习
myzzb12 小时前
纯python 最快png转换RGB截图方案 ——deepseek
开发语言·python·学习·开源·开发
被遗忘在角落的死小孩15 小时前
抗量子 Winternitz One Time Signature(OTS) 算法学习
学习·算法·哈希算法
浅念-15 小时前
C++ :类和对象(4)
c语言·开发语言·c++·经验分享·笔记·学习·算法