题目描述
给你两个数组 nums
和 andValues
,长度分别为 n
和 m
。
数组的 值 等于该数组的 最后一个 元素。
你需要将 nums
划分为 m
个 不相交的连续 子数组,对于第 i
个子数组 [l_i, r_i]
,子数组元素的按位 AND
运算结果等于 andValues[i]
,换句话说,对所有的 1 <= i <= m
,nums[l_i] & nums[l_i + 1] & ... & nums[r_i] == andValues[i]
,其中 &
表示按位 AND
运算符。
返回将 nums
划分为 m
个子数组所能得到的可能的 最小 子数组 值 之和。如果无法完成这样的划分,则返回 -1
。
样例
输入: nums = [1,4,3,3,2], andValues = [0,3,3,2]
输出: 12
解释:
唯一可能的划分方法为:
[1,4] 因为 1 & 4 == 0
[3] 因为单元素子数组的按位 AND 结果就是该元素本身
[3] 因为单元素子数组的按位 AND 结果就是该元素本身
[2] 因为单元素子数组的按位 AND 结果就是该元素本身
这些子数组的值之和为 4 + 3 + 3 + 2 = 12
输入: nums = [2,3,5,7,7,7,5], andValues = [0,7,5]
输出: 17
解释:
划分 nums 的三种方式为:
[[2,3,5],[7,7,7],[5]] 其中子数组的值之和为 5 + 7 + 5 = 17
[[2,3,5,7],[7,7],[5]] 其中子数组的值之和为 7 + 7 + 5 = 19
[[2,3,5,7,7],[7],[5]] 其中子数组的值之和为 7 + 7 + 5 = 19
子数组值之和的最小可能值为 17
输入: nums = [1,2,3,4], andValues = [2]
输出: -1
解释:
整个数组 nums 的按位 AND 结果为 0。
由于无法将 nums 划分为单个子数组使得元素的按位 AND 结果为 2,因此返回 -1。
限制
1 <= n == nums.length <= 10^4
1 <= m == andValues.length <= min(n, 10)
1 <= nums[i] < 10^5
0 <= andValues[j] < 10^5
算法1
(动态规划,二分,ST 表) O(nmlogn)
- 先思考暴力动态规划的做法。设状态 f(j,i) 表示前 i 个数字,划分为了 j 个连续子数组的最小值。其中 j 的范围是 [1,m],i 的范围是 [1,n]。
- 初始值 f(0,0)=0,其余为 +∞ 待定。
- 转移时,对于 f(j,i),倒序枚举前序划分点 k∈[1,i],并动态维护区间 [k,i] 的按位
AND
值。当满足区间 [k,i] 的按位AND
的值为 andValues(j) 时,转移 f(j,i)=min。 - 最终答案为 f(m, n)。
- 上面暴力动态规划的状态数为 O(mn),转移时间为 O(n),总时间复杂度为 O(n^2m),不满足要求。
- 状态数是没办法减少的,只能想办法减少转移时间。注意到,区间 [k, i] 的按位
AND
值是随着 k 值的减小而不增的,这里可以通过二分快速定位到满足要求的 k 的范围。 - 二分的判定是需要能快速的求出任意区间的按位
AND
值,由于AND
可重复操作且满足结合律,这里可以使用 ST 表来在对数时间预处理,在常数时间内查询。 - 假设对于 f(j, i) 的可转移区间为 k \in [p_1, p_2-1],还需要能快速找到这个区间内的最小的 f(j - 1, k) 值,这一部分也可以通过 ST 表在上一层中预处理,并在常数时间内查询得到。
- 优化后,转移的时间复杂度为二分的时间 O(\log n),同时还需要维护 ST 表,也是对数的时间。
- ST 表是一种基于倍增的数据结构,st(j, i) 表示从 i 开始的 2^j 个元素的组合值。查询时,将区间 [l, r] 分为可能重叠的两部分,用这两部分的 st 值做组合得到最终结果。
时间复杂度
- 预处理
AND
值 ST 表的时间复杂度为 O(n \log n)。 - 动态规划的状态数为 O(mn),转移时间为 O(\log n)。
- 动态规划中维护最小值 ST 表的总时间复杂度为 O(nm \log n)。
- 故算法的总时间复杂度为 O(nm \log n)。
空间复杂度
- 需要 O(n(m + \log n)) 的额外空间存储动态规划的状态和 ST 表。
C++ 代码
const int N = 10001, L = 18, M = 11;
const int INF = 1000000000;
class Solution {
private:
int v[L][N], log[N], mi[L][N];
int f[N];
int get_v(int l, int r) {
int z = log[r - l];
return v[z][l] & v[z][r - (1 << z) + 1];
}
int binary_search(int ed, int target) {
int l = 1, r = ed + 1;
while (l < r) {
int mid = (l + r) >> 1;
if (get_v(mid, ed) < target)
l = mid + 1;
else
r = mid;
}
return l;
}
int get_min(int l, int r) {
int z = log[r - l];
return min(mi[z][l], mi[z][r - (1 << z) + 1]);
}
public:
int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
const int n = nums.size(), m = andValues.size();
log[0] = 0;
for (int i = 1; i <= n; i++) {
v[0][i] = nums[i - 1];
log[i] = (i >> (log[i - 1] + 1)) ? log[i - 1] + 1 : log[i - 1];
}
for (int j = 1; (1 << j) <= n; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
v[j][i] = v[j - 1][i] & v[j - 1][i + (1 << (j - 1))];
for (int i = 1; i <= n; i++)
mi[0][i] = f[i] = INF;
mi[0][0] = f[0] = 0;
for (int k = 1; (1 << k) <= n; k++)
for (int i = 0; i + (1 << k) - 1 <= n; i++)
mi[k][i] = min(mi[k - 1][i], mi[k - 1][i + (1 << (k - 1))]);
for (int j = 0; j < m; j++) {
f[0] = INF;
for (int i = 1; i <= n; i++) {
int p1 = binary_search(i, andValues[j]);
int p2 = binary_search(i, andValues[j] + 1);
if (p1 < p2)
f[i] = get_min(p1 - 1, p2 - 2) + nums[i - 1];
else
f[i] = INF;
}
for (int i = 0; i <= n; i++) {
mi[0][i] = f[i];
f[i] = INF;
}
for (int k = 1; (1 << k) <= n; k++)
for (int i = 0; i + (1 << k) - 1 <= n; i++)
mi[k][i] = min(mi[k - 1][i], mi[k - 1][i + (1 << (k - 1))]);
}
if (mi[0][n] >= INF)
return -1;
return mi[0][n];
}
};
算法2
(暴力动态规划) O(nm \log U)
- 换一种方式进行暴力动态规划。设状态 f(i, j, k) 表示前 i 个数字,划分为了 j 个连续子数组,且最后一段连续子数组的
AND
值为 k 的最小值。其中 i 的范围是 [0, n - 1],j 的范围是 [0, m - 1],k 这一维定义为哈希表。 - 初始值 f(0, 0, nums(0)) = 0,其余为 +\infty 待定。
- 转移时,对于 f(i, j),如果 j > 0 且 andValues(j - 1) 存在于 f(i - 1, j - 1) 中,则第 i 个数字可以新开一个子数组,转移 f(i, j, nums(i)) = f(i - 1, j - 1, andValues(j - 1)) + nums(i - 1)。其它情况下,将第 i 个数字和前面的连续子数组合并,转移 f(i, j, k \text{ AND } nums(i)) = \min(f(i, j, k \text{ AND } nums(i)), f(i - 1, j, k))。
- 最终答案为 f(n - 1, m - 1, andValues(m - 1)) + nums(n - 1)。注意,如果 andValues(m - 1) 在 f(n - 1, m - 1) 中不存在,则返回 -1。
- 可以使用滚动数组优化掉第一维,提升性能。
时间复杂度
- 动态规划的状态数为 O(nm \log U),其中 U 是最大可能的数字。
- 这是因为对于一个固定起始位置的子数组,其向后最多可以产生 \log U 个不同的
AND
值,转移时间为常数,故总时间复杂度为 O(nm \log U)。
空间复杂度
- 使用滚动数组,需要 O(m \log U) 的额外空间存储动态规划的状态。
C++ 代码
const int M = 11;
class Solution {
public:
int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
const int n = nums.size(), m = andValues.size();
unordered_map<int, int> f[M];
f[0][nums[0]] = 0;
for (int i = 1; i < n; i++) {
unordered_map<int, int> g[M];
for (int j = 0; j < min(m, i + 1); j++) {
if (j > 0 && f[j - 1].count(andValues[j - 1]))
g[j][nums[i]] = f[j - 1][andValues[j - 1]] + nums[i - 1];
for (const auto &[k, _] : f[j]) {
int t = k & nums[i];
if (g[j].count(t) == 0 || g[j][t] > f[j][k])
g[j][t] = f[j][k];
}
}
for (int j = 0; j < min(m, i + 1); j++)
f[j] = g[j];
}
if (f[m - 1].count(andValues[m - 1]) == 0)
return -1;
return f[m - 1][andValues[m - 1]] + nums[n - 1];
}
};