Tips:预封装约束的状态定义

预先封装约束的状态定义

.引言

\;\;\;\;\;\;\;\; 这个Tips专栏是新开的专栏,主要是因为,有一些思考感觉很重要,且深入,但是又不是一个专门的主题,且不好分类,所以将遇到的小知识都放在这个专栏。今天是2025年10月17日。正好今天在解决Leetcode题目的打家劫舍这道题的时候,使用分治法解决,发现同样是分治法,但是状态转移却截然不同,可谓天差地别。详细见下面。

.打家劫舍

\;\;\;\;\;\;\;\; 先不论标题这一长串是什么意思。先来看一个问题。这个问题是Leetcode里面很经典的问题:打家劫舍

简化点链接的时间,我将题目截图放这里:



假设a[l,r]表示区间[l,r]的最大值。

要求得 a[0,n-1] 得最大值,等价于求的a[0,mid]的最大值+a[mid+1,n-1]的最大值。而a[0,mid]的最大值等于a[0,mid/2]的最大值+a[mid/2+1,n-1]最大值。一直往下分,很显然这就是典型的分治法。


.没有限制的状态

为了从头说起,先使用最简单的没有状态限制定义 的分治法:

我认为代码很直观看出原理:

cpp 复制代码
class Solution 
{
public:
    int rob(vector<int>& nums) 
    {
        int n=nums.size();
        if(n==0)return 0;
        auto dfs=[&](this auto&&dfs,int l,int r)->int
        {
            if(l==r) //只有一个元素,当然选这个元素
                return nums[l];
            if(l+1==r)  //只有两个元素,选两个之间大的一个
            {
                return max(nums[l],nums[r]);
            }
            if(l>r) //不存在区间,返回0
                return 0;
            int mid=(l+r)>>1;
            int res=0;
            //选nums[mid]和不选nums[mid]两种取最大值
            res=dfs(l,mid-1)+dfs(mid+1,r);  //不选nums[mid]
            res=max(res,dfs(l,mid-2)+dfs(mid+2,r)+nums[mid]);     //选nums[mid],那么nums[mid+1],nums[mid-1]都不能选。
            return res;
        };  

        return dfs(0,n-1);
    }
};

代码可以优化一下,添加记忆化

cpp 复制代码
class Solution 
{
public:
    int rob(vector<int>& nums) 
    {
        int n=nums.size();
        if(n==0)return 0;
        vector<vector<int>>memo(n,vector<int>(n,-1));
        auto dfs=[&](this auto&&dfs,int l,int r)->int
        {
            if(l==r) //只有一个元素,当然选这个元素
                return nums[l];
            if(l+1==r)  //只有两个元素,选两个之间大的一个
            {
                return max(nums[l],nums[r]);
            }
            if(l>r) //不存在区间,返回0
                return 0;
            int &res=memo[l][r];
            if(res!=-1)return res;
            int mid=(l+r)>>1;
            //选nums[mid]和不选nums[mid]两种取最大值
            //不选nums[mid]
            res=dfs(l,mid-1)+dfs(mid+1,r);  
            //选nums[mid],那么nums[mid+1],nums[mid-1]都不能选。
            res=max(res,dfs(l,mid-2)+dfs(mid+2,r)+nums[mid]);     
            return res;
        };  

        return dfs(0,n-1);
    }
};

上面的分治法非常简单,同时通过固定枚举nums[mid],将当前区间分为两段。然后比较取nums[mid]和不取两种方案取最大值。这样得方法优点就是省略了边界处理(消除左边和右边相邻两个数组的限制,比如左数组取尾,右数组不能取首)。如何处理的?就是通过nums[mid]隔断,然后单独考虑左右两个数组 ,这两个数组不相邻,自然没有边界约 束,此时这两个子数组又是相同的子问题,所以可以继续分治下去。合并的时候也不需要考虑约束


.带限制的状态

\;\;\;\;\;\;\;\; 但是,对于区间[L,R],直接考虑左右两个数组[L,mid]和[mid+1,R]呢?此时两个子数组相邻,那么分别解决这两个数组后,合并的时候就有问题了,因为要求不能选取相邻的两个元素,合并两个最大值可能出错,因为左边数组可能取了最后一个元素而右边的数组取了第一个元素。所以子问题[L,mid]和[mid+1,R]不能仅仅表示区间的最大值这单一结论了,而应该添加约束,将这个大结论拆分为几个带约束的结论。

当然,从结构性来说,带约束的结论是不带约束结论的子集。
带限制结论 ∈ 不带限制结论 带限制结论\in 不带限制结论 带限制结论∈不带限制结论

\;\;\;\;\;\;\;\; 两个不带约束结论却受到"不能有相邻元素"的约束从而无法合并。因为合并是带有约束的。因此,我们要将状态设置限制,下面四个状态定义是完全互斥且穷尽所有可能的(首 / 尾选或不选的组合共 4 种),合并时需严格确保 "左区间尾" 和 "右区间首" 不同时被选(相邻冲突)。以下是完整的状态转移逻辑,每个状态的合并都基于左区间(L~mid)和右区间(mid+1~R)的合法组合:

复制代码
a[L,R][0]表示[L,R]中第一个元素不选,最后一个元素也不选的最大值
a[L,R][1]表示[L,R]中第一个元素不选,最后一个元素选的最大值
a[L,R][2]表示[L,R]中第一个元素选,最后一个元素也选的最大值
a[L,R][3]表示[L,R]中第一个元素选,最后一个元素不选的最大值

上面四个状态就将所有的可能列举出来。那么合并左右两个数组的时候,就要分类讨论合并。

比如已经求得了 a[L,mid][0~3] 和 a[mid+1,R][0~3] 的值。下面是合并过程中的状态转移

cpp 复制代码
a[L,R][0] = max(
{
    a[L,mid][0] + a[mid+1,R][0],
    a[L,mid][0] + a[mid+1,R][3],
    a[L,mid][1] + a[mid+1,R][0]
});

a[L,R][1] = max({
    a[L,mid][0] + a[mid+1,R][1],
    a[L,mid][0] + a[mid+1,R][2],
    a[L,mid][1] + a[mid+1,R][1]
});

a[L,R][2] = max({
    a[L,mid][2] + a[mid+1,R][1],
    a[L,mid][3] + a[mid+1,R][1],
    a[L,mid][3] + a[mid+1,R][2]
});

a[L,R][3] = max({
    a[L,mid][2] + a[mid+1,R][0],
    a[L,mid][3] + a[mid+1,R][0],
    a[L,mid][3] + a[mid+1,R][3]
});

代码实现为:

cpp 复制代码
class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) return 0;
        // 数组大小使用 2 << bit_width(n-1) 是足够的(覆盖所有节点)
        vector<array<int, 4>> a(2 << bit_width(static_cast<unsigned int>(n-1)));
        // a[i][0]: 第一个不选,最后一个不选
        // a[i][1]: 第一个不选,最后一个选
        // a[i][2]: 第一个选,最后一个不选
        // a[i][3]: 第一个选,最后一个选

        auto maintain = [&](int idx) -> void {
            auto& b = a[idx];
            auto& c = a[idx*2+1]; // 左区间 [l, mid]
            auto& d = a[idx*2+2]; // 右区间 [mid+1, r]

            // 合并逻辑:严格保证 mid 和 mid+1 不同时被选
            b[0] = max({
                c[0] + d[0],  // 左都不选,右都不选
                c[0] + d[2],  // 左都不选,右首选尾不选
                c[1] + d[0],  // 左首不选尾选,右都不选(mid选,mid+1不选)
                c[1] + d[2]   // 左首不选尾选,右首选尾不选(错误,mid和mid+1都选,排除)
                // 注意:c[1](mid选)和d[2](mid+1选)相邻,需排除
            });
            b[0] = max({c[0]+d[0], c[0]+d[2], c[1]+d[0]});

            b[1] = max({
                c[0] + d[1],  // 左都不选,右首不选尾选
                c[0] + d[3],  // 左都不选,右首选尾选(mid不选,mid+1选,合法)
                c[1] + d[1],  // 左首不选尾选,右首不选尾选(mid选,mid+1不选,合法)
                c[1] + d[3]   // 左首不选尾选,右首选尾选(mid和mid+1都选,排除)
            });
            b[1] = max({c[0]+d[1], c[0]+d[3], c[1]+d[1]});

            b[2] = max({
                c[2] + d[0],  // 左首选尾不选,右都不选(mid不选,mid+1不选)
                c[2] + d[2],  // 左首选尾不选,右首选尾不选(mid不选,mid+1选,合法)
                c[3] + d[0],  // 左首选尾选,右都不选(mid选,mid+1不选,合法)
                c[3] + d[2]   // 左首选尾选,右首选尾不选(mid和mid+1都选,排除)
            });
            b[2] = max({c[2]+d[0], c[2]+d[2], c[3]+d[0]});

            b[3] = max({
                c[2] + d[1],  // 左首选尾不选,右首不选尾选(mid不选,mid+1不选,合法)
                c[2] + d[3],  // 左首选尾不选,右首选尾选(mid不选,mid+1选,合法)
                c[3] + d[1],  // 左首选尾选,右首不选尾选(mid选,mid+1不选,合法)
                c[3] + d[3]   // 左首选尾选,右首选尾选(mid和mid+1都选,排除)
            });
            b[3] = max({c[2]+d[1], c[2]+d[3], c[3]+d[1]});
        };

        auto div = [&](this auto&& self, int idx, int l, int r) -> void {
            if (l == r) {
                // 单个元素:修正状态初始化
                a[idx][0] = 0;          // 都不选
                a[idx][1] = 0;          // 首不选尾选(矛盾,为0)
                a[idx][2] = 0;          // 首选尾不选(矛盾,为0)
                a[idx][3] = nums[l];    // 首选尾选(合法)
                return;
            }
            int mid = (l + r) >> 1;
            self(idx*2+1, l, mid);
            self(idx*2+2, mid+1, r);
            maintain(idx);
        };

        div(0, 0, n-1);
        // 最终结果取所有合法状态的最大值
        return max({a[0][0], a[0][1], a[0][2], a[0][3]});
    }
};

.预封装约束的状态

上面的带约束状态转移方程逻辑非常清楚完善。是穷举了合并时约束的所有可能。这当然可以,但是写代码的时候非常冗余不说,有时候也会漏掉一些状态转移。有什么办法呢?

\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; 预先封装好约束的状态转移

为了区别两种方法,我先说第一种方法的本质:

第一种方法(4 个独立状态)
操作: 合并的时候,穷举4个状态所有的组合(4*4=16),然后根据约束条件删除不合法的组合。
性质: 状态描述"当前的选择结果"。

现在来说第二种方法:

定义:

f00: 首必不选 + 尾必不选

f01: 首必不选 + 尾可选可不选

f10: 首可选可不选 + 尾必不选

f11: 首可选可不选 + 尾可选可不选

代码:

cpp 复制代码
class Solution 
{
public:
    // 定义4个状态的返回值:{f00, f01, f10, f11}
    using State = tuple<int, int, int, int>;

    State dfs(const vector<int>& nums, int l, int r) 
    {
        if (l == r) 
        {
            // 边界:长度为1,按定义返回
            return {0, 0, 0, max(nums[l], 0)};
        }
        int mid = l + (r - l) / 2;
        auto [p00, p01, p10, p11] = dfs(nums, l, mid);    // 左区间 p = [l, mid]
        auto [q00, q01, q10, q11] = dfs(nums, mid + 1, r); // 右区间 q = [mid+1, r]

        // 计算当前区间的4个状态
        int f00 = max(p00 + q10, p01 + q00);
        int f01 = max(p00 + q11, p01 + q01);
        int f10 = max(p10 + q10, p11 + q00);
        int f11 = max(p10 + q11, p11 + q01);

        return {f00, f01, f10, f11};
    }

    int rob(vector<int>& nums)
     {
        if (nums.empty()) return 0;
        auto [f00, f01, f10, f11] = dfs(nums, 0, nums.size() - 1);
        return f11; // f11就是无约束的原始问题答案
    }
};

发现了,第二种方法的合并操作简单很少,大大简化了代码量。那么第二种方法为什么可以这样呢?

性质: 隐式约束传递。状态包含选择结果 和对边界的约束信息
约束方式: 状态包含选择结果和对边界的约束信息。
本质: 状态本身携带约束信息,合并时通过约束的自然匹配来避免相邻冲突,无需显式检查。

区别:

特性 第一种 第二种
状态语义 记录具体选择结果 记录选择结果+边界约束
约束保证 显式排除非法组合 隐式通过约束匹配
合并逻辑 复杂,需要枚举所有可能 简洁,只有2种组合 per state
代码可读性 较低,容易出错 较高,逻辑清晰
思维模型 "我有什么选择?排除哪些?" "在什么约束下我能得到什么?"
相关推荐
三川69811 分钟前
排序算法介绍
数据结构·算法·排序算法
智驱力人工智能5 小时前
基于视觉分析的人脸联动使用手机检测系统 智能安全管理新突破 人脸与手机行为联动检测 多模态融合人脸与手机行为分析模型
算法·安全·目标检测·计算机视觉·智能手机·视觉检测·边缘计算
2301_764441335 小时前
水星热演化核幔耦合数值模拟
python·算法·数学建模
循环过三天5 小时前
3.4、Python-集合
开发语言·笔记·python·学习·算法
priority_key7 小时前
排序算法:堆排序、快速排序、归并排序
java·后端·算法·排序算法·归并排序·堆排序·快速排序
不染尘.8 小时前
2025_11_7_刷题
开发语言·c++·vscode·算法
来荔枝一大筐9 小时前
力扣 寻找两个正序数组的中位数
算法
算法与编程之美9 小时前
理解Java finalize函数
java·开发语言·jvm·算法
地平线开发者10 小时前
LLM 训练基础概念与流程简介
算法·自动驾驶
点云SLAM10 小时前
弱纹理图像特征匹配算法推荐汇总
人工智能·深度学习·算法·计算机视觉·机器人·slam·弱纹理图像特征匹配