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;
}
};

浙公网安备 33010602011771号