LeetCode - 3117. 划分数组得到最小的值之和

题目链接

考察知识点

  • 动态规划 - 线性DP

思路分析

一般来说这种划分求最值的题可以用DP做,古话说得好:

暴力出奇迹,打表出省一

再加上此题的动态规划状态表示和转移方程并不是那么好想,所以我们先打出一份爆搜加剪枝的代码,注意:

  • 剪枝一:剩余元素数量不足以匹配剩余的AND结果时直接终止

  • 剪枝二:若当前新AND值已小于目标值,后续不可能达到目标,终止当前分支

对于剪枝二,与运算有一个很重要的性质:\(a \vee x \le a\),因此剪枝成立。

爆搜代码

class Solution {
public:
    // 存储最终答案,初始化为整数最大值,表示尚未找到有效解
    int ans = INT_MAX;
    
    /**
     * 深度优先搜索函数,用于探索所有可能的分组方式
     * @param u 当前处理到nums数组的索引位置
     * @param v 当前需要匹配的andValues数组的索引位置
     * @param curAnd 当前分组累积的AND运算结果
     * @param curSum 当前已经选择的元素的总和
     * @param nums 输入的数组(常量引用,避免拷贝)
     * @param andValues 需要匹配的AND结果数组(常量引用,避免拷贝)
     * @param n nums数组的长度
     * @param m andValues数组的长度
     */
    void dfs(int u, int v, int curAnd, int curSum, const vector<int> &nums, const vector<int> &andValues, int n, int m) {
        // 终止条件1:已经匹配完所有需要的AND结果
        if (v == m) {
            // 此时必须处理完nums中所有元素才是有效解
            if (u == n) {
                // 更新答案为当前总和与已有答案的最小值
                ans = min(ans, curSum);
            }
            return; // 结束当前递归分支
        }
        
        // 终止条件2:nums元素已处理完毕但还未匹配完所有AND结果,此分支无效
        if (u >= n) return;
        
        // 剪枝优化1:剩余元素数量不足以匹配剩余的AND结果
        // 若nums剩余元素(n-u)小于还需匹配的andValues数量(m-v),直接终止
        if (n - u < m - v) return;
        
        // 计算将当前nums[u]加入当前分组后的新AND值
        // 初始curAnd为-1(二进制全1),首次运算等价于直接取nums[u]
        int newAnd = curAnd & nums[u];
        
        // 剪枝优化2:AND运算结果具有单调性(只会变小或不变)
        // 若当前新AND值已小于目标值,后续不可能达到目标,终止当前分支
        if (newAnd < andValues[v]) return;
        
        // 分支1:不结束当前分组,继续添加下一个元素到当前分组
        // 递归处理下一个元素,保持当前分组索引v不变,更新AND值
        dfs(u + 1, v, newAnd, curSum, nums, andValues, n, m);
        
        // 分支2:若当前新AND值恰好等于目标值,可以选择结束当前分组
        if (newAnd == andValues[v]) {
            // 递归处理下一个元素,分组索引v+1(开始新分组),重置AND值为-1
            // 同时将当前元素值加入总和(因为它是当前分组的最后一个元素)
            dfs(u + 1, v + 1, -1, curSum + nums[u], nums, andValues, n, m);
        }
    }
    
    /**
     * 主函数:计算满足条件的最小元素和
     * @param nums 输入的数组
     * @param andValues 需要匹配的AND结果数组
     * @return 最小和;若无法满足条件则返回-1
     */
    int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
        // 调用深度优先搜索,初始参数:
        // 从nums[0]开始(u=0),匹配andValues[0](v=0),初始AND为-1,初始总和为0
        dfs(0, 0, -1, 0, nums, andValues, nums.size(), andValues.size());
        
        // 若答案仍为INT_MAX,说明没有有效解,返回-1;否则返回找到的最小和
        return ans == INT_MAX ? -1 : ans;
    }
};

过了468/512个测试用例。

左神总结过动态规划大致过程(其很好说明了爆搜和动态规划之间不可分割的关系):

爆搜 -> 记忆化搜索 -> 严格位置依赖的动态规划 -> 进一步优化空间 -> 进一步优化枚举也就是优化时间

又因为记忆化搜索是动态规划的一种,我们考虑把前面的爆搜代码代为记忆化搜索版本。

缓存表要开多少维?每一维度代表什么?易得,dfs函数中所有除去要处理的数据和中间答案不需要挂到缓存表之外,其余的每个参数都要成为缓存表中的一个维度。

dp[N][M][MAX_VALUE]

表示处理到nums的第u个元素,匹配到andValues的第v个元素,当前累积的AND结果为a时的最小和。

但很可惜,这么定义dp数组因为使用空间过大而通过0/512个测试样例。

注意到dp数组中有大量用不到的状态,所以考虑用map存储每个状态对应的值,对于每个状态有三个变量:u,v,And,注意到\(u \le N,v \le M,And \le MaxValue\),可以把这三个变量压成一个变量,这样就能存储了。

至此,我们解决了这道题

时间复杂度

\(O(\lvert nums \rvert)\)

C++代码

// 定义一个极大值,用于表示不可行的状态或初始值
const int inf = 1e9;

class Solution {
public:
    // 成员变量,分别存储nums数组长度和andValues数组长度
    int n, m;
    // 哈希表,用于记忆化存储递归过程中的中间结果,避免重复计算
    unordered_map<int, int> dp;

    /**
     * 递归函数,用于计算满足条件的最小和
     * @param u 当前处理到nums数组的索引
     * @param v 当前需要匹配的andValues数组的索引
     * @param And 当前累积的AND运算结果
     * @param nums 输入的数组
     * @param andValues 需要匹配的AND结果数组
     * @return 从当前状态开始,满足条件的最小和;若不可行则返回inf
     */
    int f(int u, int v, int And, vector<int> &nums, vector<int> &andValues) {
        // 剪枝:若nums剩余元素数量小于andValues未匹配的数量,不可能完成匹配,返回inf
        if (n - u < m - v) return inf;
        
        // 若andValues已全部匹配(v == m)
        // 此时需检查nums是否也已全部处理(u == n),是则返回0(无需再添加元素),否则返回inf(不合法)
        if (v == m) return u == n ? 0 : inf;
        
        // 更新当前累积的AND值:将当前nums[u]与现有And做AND运算
        // (初始And为-1,二进制全1,首次运算等价于直接取nums[u])
        And &= nums[u];
        
        // 剪枝:AND运算具有单调性(结果只会减小或不变),若当前And已小于目标值,后续不可能达标,返回inf
        if (And < andValues[v]) return inf;
        
        // 构造哈希表的键值,将u、v、And三个状态变量合并为一个整数
        // (通过不同数量级分离,确保不同状态对应唯一key)
        int key = u * 100000 + v * 10000 + And;
        
        // 若当前状态已计算过,直接返回缓存的结果
        if (dp.count(key)) return dp[key];
        
        // 递归方案1:不将当前nums[u]作为第v组的结尾,继续累积AND,处理下一个元素
        int ans = f(u + 1, v, And, nums, andValues);
        
        // 若当前累积的And等于目标值andValues[v],可以选择将nums[u]作为第v组的结尾
        // 此时进入下一组(v+1),重置And为-1(以便计算新组的AND),并将nums[u]的值加入总和
        // 取两种方案的最小值作为当前状态的结果
        if (And == andValues[v]) {
            ans = min(ans, f(u + 1, v + 1, -1, nums, andValues) + nums[u]);
        }
        
        // 将当前状态的结果存入哈希表,并返回
        return dp[key] = ans;
    }

    /**
     * 主函数,计算满足条件的最小和
     * @param nums 输入的数组
     * @param andValues 需要匹配的AND结果数组
     * @return 最小和;若无法满足条件则返回-1
     */
    int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
        // 初始化n和m分别为两个数组的长度
        n = nums.size(), m = andValues.size();
        
        // 调用递归函数,初始状态:从nums[0]开始,匹配andValues[0],初始And为-1
        int ans = f(0, 0, -1, nums, andValues);
        
        // 若结果为inf,说明无合法方案,返回-1;否则返回计算出的最小和
        return ans == inf ? -1 : ans;
    }
};
posted @ 2025-08-22 23:12  九三青梧  阅读(10)  评论(0)    收藏  举报