划分数组得到最小的值之和

2024-08-16

题目:

给你两个数组 numsandValues,长度分别为 nm。数组的 等于该数组的 最后一个 元素。

你需要将 nums 划分为 m不相交的连续 子数组,对于第 ith 个子数组 [li, ri],子数组元素的按位 AND 运算结果等于 andValues[i],换句话说,对所有的 1 <= i <= mnums[li] & nums[li + 1] & ... & nums[ri] == andValues[i] ,其中 & 表示按位 AND 运算符。

返回将 nums 划分为 m 个子数组所能得到的可能的 最小 子数组 之和。如果无法完成这样的划分,则返回 -1

示例 1:

输入: nums = [1,4,3,3,2], andValues = [0,3,3,2]

输出: 12

解释:

唯一可能的划分方法为:

  1. [1,4] 因为 1 & 4 == 0
  2. [3] 因为单元素子数组的按位 AND 结果就是该元素本身
  3. [3] 因为单元素子数组的按位 AND 结果就是该元素本身
  4. [2] 因为单元素子数组的按位 AND 结果就是该元素本身

这些子数组的值之和为 4 + 3 + 3 + 2 = 12

示例 2:

输入: nums = [2,3,5,7,7,7,5], andValues = [0,7,5]

输出: 17

解释:

划分 nums 的三种方式为:

  1. [[2,3,5],[7,7,7],[5]] 其中子数组的值之和为 5 + 7 + 5 = 17
  2. [[2,3,5,7],[7,7],[5]] 其中子数组的值之和为 7 + 7 + 5 = 19
  3. [[2,3,5,7,7],[7],[5]] 其中子数组的值之和为 7 + 7 + 5 = 19

子数组值之和的最小可能值为 17

示例 3:

输入: nums = [1,2,3,4], andValues = [2]

输出: -1

解释:

整个数组 nums 的按位 AND 结果为 0。由于无法将 nums 划分为单个子数组使得元素的按位 AND 结果为 2,因此返回 -1

提示:

  • 1 <= n == nums.length <= 104
  • 1 <= m == andValues.length <= min(n, 10)
  • 1 <= nums[i] < 105
  • 0 <= andValues[j] < 105

思路:

寻找子问题

看示例 1nums = [1,4,3,3,2], andValues = [0,3,3,2]

我们要解决的问题是,把 nums 划分成 4 个子数组所能得到的最小子数组值之和,其中每个子数组的 AND 值与 andValues 中的值一一对应。

 nums[0] 开始。考虑是否要把 nums[0] 作为子数组的最后一个数,分类讨论:
	不把 nums[0] 作为子数组的最后一个数,也就是 nums[0] 和后续元素在同一个子数组中,那么接下来需要解决的问题为:把 [4,3,3,2] 划分成 4 个子数组,且第一个子数组与 nums[0] = 1 计算 AND 的值恰好等于 andValues[0] = 0,其余子数组的 AND 值分别等于 3, 3, 2,在满足该条件的情况下,所能得到的最小子数组值之和。
	 nums[0] 作为子数组的最后一个数,但子数组 AND 等于 1,不等于 andValues[0] = 0,不符合题目要求,无法划分。

继续。考虑是否要把 nums[1] 作为子数组的最后一个数,分类讨论:
	不把 nums[1] 作为子数组的最后一个数,也就是 nums[1] 和后续元素在同一个子数组中,那么接下来需要解决的问题为:把 [3,3,2] 划分成 4 个子数组,且第一个子数组与 nums[0] & nums[1] 计算 AND 的值恰好等于 andValues[0] = 0,其余子数组的 AND 值分别等于 3, 3, 2,在满足该条件的情况下,所能得到的最小子数组值之和。注意剩余元素只有 3 个,没法分成 4 个子数组。
	 nums[1] 作为子数组的最后一个数,注意我们并不需要知道这个子数组的前面具体有哪些数,只需要知道前面的元素的 AND 值等于 1。由于 nums[0] & nums[1] = 1 & 4 = 0 = andValues[0],符合题目要求,可以划分。接下来需要解决的问题为:把 [3, 3, 2] 划分成 3 个子数组,子数组的 AND 值分别等于 3, 3, 2,在满足该条件的情况下,所能得到的最小子数组值之和。
是否划分都会把原问题变成一个和原问题相似的、规模更小的子问题,都是把一些元素划分成若干段,且每一段的 AND 值与 andValues 中的元素匹配。这可以用递归解决。

 1:为方便把子数组的最后一个元素加入答案,本题适合从左到右思考。

 2:动态规划有「选或不选」和「枚举选哪个」两种基本思考方式。在做题时,可根据题目要求,选择适合题目的一种来思考。本题用到的是「选或不选」。

状态定义与状态转移方程

递归需要哪些参数?
	需要知道当前考虑到 nums 的哪个数,其下标记作 i。
	需要知道当前划分的子数组对应着 andValues 的哪个数,其下标记作 j。也可以理解为前面已经划分了 j 段。
	需要知道当前划分的子数组,在 i 左边的那些元素的 AND 值,记作 and。再次强调,我们并不需要知道 i 左边具体有哪些数,只需要知道左边那些数的 AND 值是多少即可。

于是,定义 dfs(i, j, nd) 表示从左往右划分,目前考虑到 nums[i],已经划分了 j 段,且当前待划分的这一段已经参与 AND 运算的结果为 and,在这种情况下,剩余元素划分得到的最小和。

首先把 and 与 nums[i] 计算 AND。

用「选或不选」的思想分类讨论:
	不划分:继续向右递归 dfs(i + 1, j, and)。
	划分:如果 and = andValues[j],那么可以划分,即 dfs(i + 1, j + 1, −1) + nums[i]。这里令 and = −1 是因为 −1 的二进制全为 1,与任何数 x 的 AND 都是 x,适合用来计算新子数组的 AND 值。
	这两种情况取最小值,就得到了 dfs(i,j,and)。

递归边界:
	如果 n − i < m − j,那么剩余元素不足,无法划分,返回 ∞。
	如果 j = m 且 i < n,还有元素没有划分,返回 ∞。
	如果 j = m 且 i = n,划分成功,返回 0。

递归入口:dfs(0, 0, −1),即答案。如果答案是 ∞ 则返回 −1。

代码:

class Solution {
public:
    int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
        const int INF = INT_MAX / 2; // 除 2 防止下面 + nums[i] 溢出
        int n = nums.size(), m = andValues.size();
        unordered_map<long long, int> memo;
        auto dfs = [&](auto&& dfs, int i, int j, int and_) -> int {
            if (n - i < m - j) { // 剩余元素不足
                return INF;
            }
            if (j == m) { // 分了 m 段
                return i == n ? 0 : INF;
            }
            and_ &= nums[i];
            // 三个参数压缩成一个 long long
            long long mask = (long long) i << 36 | (long long) j << 32 | and_;
            if (memo.contains(mask)) { // 之前计算过
                return memo[mask];
            }
            int res = dfs(dfs, i + 1, j, and_); // 不划分
            if (and_ == andValues[j]) { // 划分,nums[i] 是这一段的最后一个数
                res = min(res, dfs(dfs, i + 1, j + 1, -1) + nums[i]);
            }
            return memo[mask] = res; // 记忆化
        };
        int ans = dfs(dfs, 0, 0, -1);
        return ans < INF ? ans : -1;
    }
};