文章目录

题目描述
今日每日一题 :3740. 三个相等元素之间的最小距离 I
明日每日一题:3741. 三个相等元素之间的最小距离 II
注:题目描述完全相同但是数据范围不同,我直接贴的是3741的数据范围

示例 1:
输入: nums = [1,2,1,1,3]
输出: 6
解释:
最小距离对应的有效三元组是 (0, 2, 3) 。
(0, 2, 3) 是一个有效三元组,因为 nums[0] == nums[2] == nums[3] == 1。它的距离为 abs(0 - 2) + abs(2 - 3) + abs(3 - 0) = 2 + 1 + 3 = 6。
示例 2:输入: nums = [1,1,2,3,2,1,2]
输出: 8
解释:
最小距离对应的有效三元组是 (2, 4, 6) 。
(2, 4, 6) 是一个有效三元组,因为 nums[2] == nums[4] == nums[6] == 2。它的距离为 abs(2 - 4) + abs(4 - 6) + abs(6 - 2) = 2 + 2 + 4 = 8。
示例 3:输入: nums = [1]
输出: -1
解释:
不存在有效三元组,因此答案为 -1。
提示:1 <= n == nums.length <= 105
1 <= nums[i] <= n
思路解析
对于3740题来说,数组长度的上限仅为100,我们可以通过三层循环的暴力枚举解法直接AC(不过个人感觉暴力要比哈希的接哈麻烦),还完全无法适配3741题1e5级别的大数据范围,因此我们直接采用更高效、同时能兼容两道题的哈希表解法来进行AC。
这道题的核心难点在于,如果我们通过暴力枚举所有可能的三元组来统计元素与下标的对应关系,会带来极高的时间成本,在数据量较大时会直接触发超时。想要优化这个问题,核心思路就是先归类、再计算:我们可以先把数组中相同元素对应的所有下标整理归集到一起,再针对有足够出现次数的元素,去计算它能形成的有效三元组的最小距离,从根源上减少无效的计算量。
具体来说,我们可以维护一个unordered_map<int, vector<int>>类型的哈希表,哈希表的键是原数组中的元素值,而键对应的value,就是原数组中所有等于该键的元素的下标,我们遍历数组把这些下标依次存入vector中,这样由于我们是按照顺序遍历原数组下标,得到的下标数组天然就是升序排列的。
当哈希表构建完成后,我们只需要遍历哈希表中的每一组数据:只有当某个元素对应的下标数组长度大于等于3时,它才能形成符合要求的有效三元组,才有后续计算的必要。我们只需要对这些符合条件的下标数组逐一计算,最终就能统计出所有有效三元组中的最小距离。
这里还有一个核心的优化逻辑:对于升序排列的下标数组来说,想要得到三元组的最小距离,我们只需要遍历所有连续的三个下标即可。因为如果跳过中间的下标去选择非连续的三个下标,首尾的间距一定会更大,最终计算出来的距离也只会更大,不可能得到比连续三元组更小的结果。这也是我们的算法能保证高效且结果正确的关键逻辑。
代码实现
cpp
class Solution {
public:
int minimumDistance(vector<int>& nums) {
int size = nums.size();
// 数组长度不足3,不可能存在有效三元组,提前剪枝直接返回-1
if(size < 3)
return -1;
// 哈希表:键为数组元素值,值为该元素在原数组中出现的所有下标(天然升序排列)
unordered_map<int, vector<int>> hash;
// 一次遍历原数组,填充哈希表,完成相同元素的下标归集
for(int i = 0; i < size; i++)
{
hash[nums[i]].push_back(i);
}
int ret = INT_MAX;
// 遍历哈希表中的每一组元素与对应的下标集合
for(auto [i , num] : hash)
{
// 下标数量不足3,无法形成有效三元组,直接跳过当前元素
if(num.size() < 3)
continue;
int tmp = INT_MAX;
// 三指针滑动窗口,遍历当前下标数组中所有连续的三个下标
int a = 0, b = 1, c = 2;
while(c < num.size())
{
// 计算当前连续三个下标形成的三元组的距离,更新当前元素的最小距离
tmp = min(tmp, abs(num[a] - num[b]) + abs(num[b] - num[c]) + abs(num[c] - num[a]));
// 滑动窗口整体右移一位,遍历下一组连续下标
c++;
a++;
b++;
}
// 用当前元素的最小三元组距离,更新全局最小距离
if(tmp != INT_MAX)
ret = min(tmp,ret);
}
// 最终判断:若全局最小值仍为初始值,说明无有效三元组,返回-1,否则返回最小距离
return ret == INT_MAX ? -1 : ret;
}
};
时间复杂度
- 时间复杂度:O(n)
我们只需要遍历一次原数组来构建哈希表,这个过程的时间开销为O(n)。
后续遍历哈希表时,所有元素的下标数组的总长度之和就是原数组的长度n,每个下标最多被访问一次,在计算时也仅仅遍历常数长度,因此整体的时间复杂度为O(n)。 - 空间复杂度:O(n)
哈希表需要存储原数组中所有元素的下标,最坏情况下所有元素都相同,需要存储n个下标,因此空间复杂度为O(n)。
踩坑记录
- 关于数值溢出的问题:虽然3741题的数据范围是
1e5,int类型完全可以覆盖下标和距离的计算结果,但如果遇到更大的数据范围,一定要注意计算三元组距离时,不要把多个绝对值相加的操作写在同一行导致溢出,建议分步骤计算并做好类型提升,避免出现数值越界的问题。
这里其实可以再次优化,题目中定义的距离公式为:
abs(i - j) + abs(j - k) + abs(k - i)对于任意三个不同的下标i、j、k,我们先将其按从小到大排序,得到
p < q < r(即p=min(i,j,k),r=max(i,j,k),q为中间值)。
此时我们对公式进行展开化简:
abs(p - q) = q - p(因q>p,绝对值可直接去掉)
abs(q - r) = r - q(因r>q,绝对值可直接去掉)
abs(r - p) = r - p(因r>p,绝对值可直接去掉)将三者相加合并:
(q - p) + (r - q) + (r - p) = 2r - 2p = 2 * (r - p)这样也可以将三指针遍历优化
-
滑动窗口的指针更新:在遍历连续三个下标的循环中,一定要记得在每次计算完成后,同步把a、b、c三个指针都向右移动一位,很容易出现只更新c指针,忘记更新a和b的情况,导致结果计算错误甚至出现数组越界。
-
最终结果的合法性判断:我们初始化的全局最小距离ret是
INT_MAX,如果整个数组中都没有能形成有效三元组的元素,最终ret的值不会发生变化,因此在return的时候一定要判断ret是否还是初始值,如果是则返回-1,否则返回计算得到的最小距离,这里很容易忘记判断直接返回ret,导致无有效三元组时输出错误结果。
如果这篇博客对你有帮助,别忘了点赞支持一下~也可以收藏起来,方便后续刷题复习时随时翻看。要是能顺手点个关注,爱弥斯还能得到漂泊者批准的游戏时间哦!
