1. 题目来源
2. 题目解析
依旧是自闭的一天…连续四天,四道 hard
。
本题按照 dp
分类的思想,在其中又贪心套 dp
可还行。
看一下是怎么从暴力 dp
优化到贪心+ dp
的,十分有价值。
以下是学习笔记:
容易找到性质,得到周期性,得到要将每一列数变成一样的。
不考虑异或和为 0 的话就直接找每列的众数即可。
考虑异或和为 0 的话,就得考虑用 dp
了,f[i][j]
为前 i
列异或和为 j
的最小操作数。答案即为 f[k][0]
表示前 k
列异或和为 0 的最小操作数。
很明显其转移方程就是 f[i][j] = max(f[i][j], f[i-1][j ^ t]);
这个 t
就是这一列要改变到的哪个数,因为数据范围是 0~2^10
,则 t
的范围就在 0~1023
中选即可:
则暴力 dp
即为:
// 其中 len[i-1] 表示第 i-1 列的数字个数
// son[i-1][j^t] 表示第 i-1 列,值为 j^t 的数字个数
// 两个相减就是第 i-1 列所有数变为 j^t 的操作数
for (int i = 1; i <= k; i ++ )
for (int j = 0; j < 1024; j ++ )
for (int t = 0; t < 1024; t ++ )
f[i][j] = min(f[i][j], f[i-1][j ^ t] + (len[i-1] - son[i-1][j ^ t]));
这个方法足够暴力,时间复杂度到了 O ( 2 10 ∗ 2 10 ∗ k ) O(2^{10} * 2^{10} * k) O(210∗210∗k) 的复杂度,必然是超时了。
考虑怎么优化呢?得将一个循环优化掉啊。
我们是考虑了第 i
列的变为所有数的情况。这个情况其实可以再细分一下:
- 某一列变为一个全新的数
- 每一列变为原列中的一个数
如果是每一列都变为一个全新的数的话,肯定没有有某一列变为一个全新的数好。每一列全新,操作数必然是所有数,而某一列全新,是其他列的众数就不必操作!
故,这样就将暴力情况分解为两个情况,且优化掉了不可能的情况。
这样就不必枚举 t :0~1023
了。仅需要枚举本列中的数即可。
再和贪心解求个 min
即可。
这样问题就转化为上述的手写思考过程了!
时间复杂度: O ( n 2 ) O(n^2) O(n2) 三重循环,不过枚举列、再枚举行,其实就是枚举了整个二维空间,其实就是 O ( n 2 ) O(n^2) O(n2) 的复杂度。
空间复杂度: O ( n 2 ) O(n^2) O(n2)
代码:
class Solution {
public:
int s[1024]; // 异或值最多是 1023,0~1023 共 1024 个数
int minChanges(vector<int>& nums, int k) {
// m 为分成长度为 k 的区间段个数,也是一列数的数量,注意上取整,最后一行可能不足 k 个数
int n = nums.size(), m = (n + k - 1) / k;
vector<vector<int>> f(k + 1, vector<int>(1024, 1e8)); // f[i][j] 表示前 i 列异或和为 j 的最小操作数
// 下标从 1 开始,初始化状态 0 列 长度为 0 异或值为 0 为合法方案,其余为非法方案
f[0][0] = 0;
int sum = 0, minv = 1e8; // sum贪心解的操作数,minv 为所有列的最小众数
for (int i = 1; i <= k; i ++ ) { // 枚举每一列
memset(s, 0, sizeof s);
int len = m; // 求该列数的数量,可能不足 k 个数
if (n % k && n % k < i) len -- ; // 如果最后一列不足,则数量减一
for (int j = 0; j < len; j ++ ) // 枚举行号,求第 i 列的数映射
s[nums[j * k + i - 1]] ++ ; // i 从 1 开始
// 求众数 maxv
int maxv = 0;
for (int j = 0; j < 1024; j ++ ) maxv = max(maxv, s[j]);
// sum 求第 i 列的最小操作次数, minv 求最小众数
sum += len - maxv, minv = min(minv, maxv);
// dp, j u 的循环顺序改变不受影响
for (int j = 0; j < 1024; j ++ ) // 枚举状态,前一列可能有 1024 种情况
for (int u = 0; u < len; u ++ ) { // 枚举选哪个数,从上往下选
int x = nums[u * k + i - 1], cost = len - s[x]; // 修改操作数是 len-s[x]
f[i][j] = min(f[i][j], f[i - 1][j ^ x] + cost);
}
}
// 每列都是减去了众数的个数的,再补上最小众数的个数,它就是全新的一列
return min(sum + minv, f[k][0]);
}
};