使数组互补的最少操作次数
问题描述
给你一个长度为 偶数 n 的整数数组 nums 和一个整数 limit 。每一次操作,你可以将 nums 中的任何整数替换为 1 到 limit 之间的另一个整数。
如果对于所有下标 i(下标从 0 开始),nums[i] + nums[n - 1 - i] 都等于同一个数,则数组 nums 是 互补的 。例如,数组 [1,2,3,4] 是互补的,因为对于所有下标 i ,nums[i] + nums[n - 1 - i] = 5 。
返回使数组 互补 的 最少 操作次数。
(真题链接:使数组互补的最少操作次数)
解题思路
本题要求将数组中的所有配对(nums[i], nums[n-1-i])的和调整到同一个目标值 S,每次操作可将任意一个数替换为[1, limit]之间的整数,目标是找到使总操作次数最少的目标值 S。
从暴力到差分
一个朴素的想法是枚举所有可能的 S(范围在 [2, 2 * limit]),对每个 S 计算所有配对所需的操作次数,取最小值。但这样做的时间复杂度为 O(n * limit),会超时。
我们需要一个更高效的方法。观察单个配对 (a, b)(令 a ≤ b),对于不同的目标值 S,该配对的修改次数呈现区间规律:
若 S 落在 [a + b, a + b],无需任何操作。
若 S 落在 [a + 1, a + b - 1] 或 [a + b + 1, b + limit],只需修改一个数。
若 S 落在 [2, a] 或 [b + limit + 1, 2 * limit],需要修改两个数。
也就是说,操作次数随 S 的变化是阶梯状的,这提示我们可以使用差分数组来高效地统计所有配对在每个目标值上的操作次数。
差分数组策略
初始化差分数组 res,长度 2 * limit + 3,初始所有目标值都需要 2 次操作(即每个 S 的基础操作次数为 n)。然后我们通过差分操作在区间端点进行加/减调整,来反映实际需要的操作次数。
对于每一对 (a, b):
在区间 [2, a] 和[b + limit + 1, 2 * limit]上需要 2 次操作------与基线一致,无需调整。
在区间 [a + 1, a + b - 1] 和 [a + b + 1, b + limit] 上只需 1 次操作,比基线少 1 次 → 在区间左端点 diff[l] -= 1,右端点 diff[r + 1] += 1。
在单点 [a + b, a + b] 上只需 0 次操作,比基线少 2 次 → 在 diff[a + b] -= 1,diff[a + b + 1] += 1(累积两次偏移)。
对差分数组求前缀和,即可得到每个 S 的实际操作次数,其中最小值即为答案。
代码实现
cpp
class Solution {
public:
int minMoves(vector<int>& nums, int limit) {
int n = nums.size();
vector<int> res(2 * limit + 2, 0);
for(int i = 0; i < n / 2; i++)
{
int a = min(nums[i], nums[n - 1 - i]);
int b = max(nums[i], nums[n - 1 - i]);
res[2] += 2;
res[a + 1] -= 1;
res[a + b] -= 1;
res[a + b + 1] += 1;
res[b + limit + 1] += 1;
}
int mini = n;
int cnt = 0;
for(int i = 2; i <= 2 * limit; i++)
{
cnt += res[i];
mini = min(mini, cnt);
}
return mini;
}
};
复杂度分析
| 复杂度 | 量级 |
|---|---|
| 时间复杂度 | O(n) |
| 空间复杂度 | O(n) |
总结
本题的核心在于将逐对统计转化为区间贡献的累积,利用差分数组在 O(n + limit) 时间内完成求解。差分数组的关键价值在于:
高效处理连续区间上的批量增减:每个配对只需 O(1) 时间更新区间的边界,极大地减少了暴力枚举的开销。
适用于类似"阶梯状"范围贡献问题:当操作次数随目标值分段常量变化时,差分数组是优化复杂度的利器。
掌握这一技巧,不仅能够解决本题,还能灵活应对其他需要批量区间更新后再求全局最优的算法问题。
距离最小相等元素查询
问题描述
给你一个 环形 数组 nums 和一个数组 queries。
对于每个查询 i ,你需要找到以下内容:
数组 nums 中下标 queries[i] 处的元素与 任意 其他下标 j(满足 nums[j] == nums[queries[i]])之间的 最小 距离。如果不存在这样的下标 j,则该查询的结果为 -1 。
返回一个数组 answer,其大小与 queries 相同,其中 answer[i] 表示查询i的结果。
(真题链接:距离最小相等元素查询)
解题思路
核心观察
对于环形数组中的某个位置,与其值相同的最近元素,一定是该位置左侧或右侧第一个相同值的元素。因此,我们只需要关注每个相同值在数组中的出现位置,并通过这些位置之间的间距来确定最小距离。
预处理:按值分组
使用哈希表 posMap,以元素值为键,存储该值在数组中出现的所有下标。由于遍历数组时下标天然递增,每个值的下标列表会自动保持有序。
环形距离的计算
数组是环形的,距离的计算要考虑两种情况:
直接距离:两个下标差的绝对值
绕环距离:从一端绕到另一端的距离,计算公式为 n - 直接距离
最终取两者的较小值。
对于每个值,其下标列表indices是有序的。对于列表中的某个下标 cur(对应查询位置):
左侧邻居:列表中的前一个元素(若当前是第一个元素,则左侧邻居为列表的最后一个元素,因为环形)
右侧邻居:列表中的后一个元素(若当前是最后一个元素,则右侧邻居为列表的第一个元素)
分别计算与左右邻居的环形距离,取最小值,即为该查询的答案。
实现细节
遍历 nums,将每个元素的下标存入对应的下标列表。
对于每个查询的下标 cur:
获取该值对应的下标列表 indices
若列表长度为 1,说明没有其他相同元素,答案为 -1
找到 cur 在列表中的位置(由于每个下标唯一,可以直接通过循环找到,或使用二分查找优化)
通过取模运算获取环形邻居,分别计算距离并取最小值
代码实现
cpp
class Solution {
public:
vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
int n = nums.size();
unordered_map<int, vector<int>> posMap;
// 记录每个值出现的所有下标
for (int i = 0; i < n; ++i) {
posMap[nums[i]].push_back(i);
}
// 预处理每个位置的最小距离
vector<int> minDist(n, -1);
for (auto& [val, indices] : posMap) {
int m = indices.size();
if (m == 1) continue;
for (int j = 0; j < m; ++j) {
int cur = indices[j];
int prev = indices[(j - 1 + m) % m];
int next = indices[(j + 1) % m];
int dist1 = min(abs(cur - prev), n - abs(cur - prev));
int dist2 = min(abs(cur - next), n - abs(cur - next));
minDist[cur] = min(dist1, dist2);
}
}
vector<int> ret;
for (int q : queries) {
ret.push_back(minDist[q]);
}
return ret;
}
};
复杂度分析
| 复杂度 | 量级 |
|---|---|
| 时间复杂度 | O(n+m) |
| 空间复杂度 | O(n) |
总结
本题的关键在于利用环形数组的特性和按值分组的思想:
环形距离的计算 :在环形数组中,两个位置的距离应为 min(直接距离, 绕环距离)。这要求我们在取模处理下标的同时,正确计算距离值。
只关注最近邻 :由于相同元素的下标列表是有序的,对于列表中的某个位置,距离它最近的相同元素只能是它的左邻居或右邻居(环形情况下,首尾也互为邻居)。因此无需遍历所有相同元素,只需检查这两个候选即可。
预处理的优势 :通过一次遍历预处理所有下标的最小距离,后续查询可直接在 O(1) 时间内得到答案,非常适合多查询场景。
边界处理 :当某个元素值的出现次数为 1 时,没有其他相同元素,直接返回 -1。
本题可以看作是"环形数组最近相同元素"问题的标准解法,掌握这一思路后,类似的问题(如环形区间查询、周期性数据的邻居查找)都可以触类旁通。