LeetCode430周赛T3
题目描述
给定一个只包含正整数的数组 nums
,我们需要找到其中的特殊子序列。特殊子序列是一个长度为4的子序列,用下标 (p, q, r, s)
表示,它们满足以下条件:
- 索引顺序:
p < q < r < s
,且相邻坐标之间至少间隔一个数字,即q - p > 1
,r - q > 1
,s - r > 1
。 - 乘积关系:
nums[p] * nums[r] == nums[q] * nums[s]
。
子序列指的是从原数组中删除零个或多个元素后,剩下元素不改变顺序组成的序列。
示例 1:
输入:nums = [1,2,3,4,3,6,1]
输出:1
解释:
nums 中只有一个特殊子序列。
(p, q, r, s) = (0, 2, 4, 6) :对应的元素为 (1, 3, 3, 1)。
nums[p] * nums[r] = 1 * 3 = 3
nums[q] * nums[s] = 3 * 1 = 3
示例 2:
输入:nums = [3,4,3,4,3,4,3,4]
输出:3
解释:
nums 中共有三个特殊子序列。
1. (p, q, r, s) = (0, 2, 4, 6) :对应元素为 (3, 3, 3, 3)。
nums[p] * nums[r] = 3 * 3 = 9
nums[q] * nums[s] = 3 * 3 = 9
2. (p, q, r, s) = (1, 3, 5, 7) :对应元素为 (4, 4, 4, 4)。
nums[p] * nums[r] = 4 * 4 = 16
nums[q] * nums[s] = 4 * 4 = 16
3. (p, q, r, s) = (0, 2, 5, 7) :对应元素为 (3, 3, 4, 4)。
nums[p] * nums[r] = 3 * 4 = 12
nums[q] * nums[s] = 3 * 4 = 12
提示:
7 <= nums.length <= 1000
1 <= nums[i] <= 1000
解题思路
为了高效地解决这个问题,我们需要利用数学和数据结构的结合。以下是详细的解题步骤:
-
问题转换:
题目要求
nums[p] * nums[r] == nums[q] * nums[s]
,我们用a,b,c,d定义nums[p],nums[q],nums[r],num[s]
,可以将其转换为分数的形式:
a b = d c \frac{a}{b} = \frac{d}{c} ba=cd这样,
a
和b
都在c
和d
的左边,便于使用前缀和后缀的方法进行分解和统计。 -
统计后缀中的最简分数:
首先,统计下标
[4, n−1]
中所有满足间隔至少一个数的数对(c, d)
的最简分数,并记录到一个哈希表suf
中。最简分数是指分子和分母互质的分数。如果分子和分母不互质,可以通过它们的最大公约数(GCD)进行约简。例如,分数4/6
的最简分数是2/3
。 -
枚举并统计前缀中的最简分数:
然后,枚举
b
的位置,以及b
左边满足间隔条件的a
,计算a/b
的最简分数,并在suf
中查找相等的最简分数的数量,将其累加到结果中。 -
动态更新后缀哈希表:
在从左向右枚举
b
的过程中,动态地维护和更新suf
中的最简分数的个数,以保证统计的准确性。
代码实现
以下是基于上述思路的C++代码实现:
typedef long long LL;
class Solution {
public:
long long numberOfSubsequences(vector<int>& nums) {
int n = nums.size();
LL res = 0;
unordered_map<int,int> mp;
// 预先统计所有 (c, d) 的最简分数
for(int c = 4; c < n - 2; c ++) {
for(int d = c + 2; d < n; d ++) {
int gcd_val = __gcd(nums[c], nums[d]);
int x = nums[c] / gcd_val;
int y = nums[d] / gcd_val;
mp[y << 16 | x ] ++;
}
}
// 枚举 b,并统计满足条件的 a
for(int b = 2; b <= n - 5; b ++) {
for(int a = 0; a <= b - 2; a ++) {
int gcd_val = __gcd(nums[a], nums[b]);
int x = nums[a] / gcd_val;
int y = nums[b] / gcd_val;
res += mp[x << 16 | y];
}
// 更新后缀哈希表,移除已经不满足间隔条件的 (c, d)
for(int d = b + 4; d < n; d ++) {
int gcd_val = __gcd(nums[b + 2], nums[d]);
int x = nums[b + 2] / gcd_val;
int y = nums[d] / gcd_val;
mp[y << 16 | x] --;
}
}
return res;
}
};
代码解析
-
哈希表
mp
的使用:我们使用一个
unordered_map<int, int>
来存储后缀中所有(c, d)
对应的最简分数。为了高效存储分数,我们将分子和分母合并成一个整数,其中分母占高16位,分子占低16位。 -
预处理后缀部分:
在初始阶段,我们遍历数组中满足条件的
(c, d)
对,并将其最简分数存入哈希表mp
。 -
枚举
b
并统计符合条件的a
:对于每一个可能的
b
,我们枚举所有满足条件的a
,计算其最简分数,并在哈希表mp
中查找相等的最简分数的数量,将其累加到结果res
中。 -
动态更新哈希表
mp
:随着
b
的增大,一些(c, d)
对将不再满足间隔条件,需要从哈希表中移除。这通过减小相应分数的计数来实现。
复杂度分析
- 时间复杂度:O(n²),其中 n 是数组的长度。主要来自于两层嵌套循环,分别用于预处理后缀和枚举
b
以及a
。 - 空间复杂度:O(n²),用于存储哈希表
mp
中可能的所有(c, d)
对的最简分数。
总结
通过巧妙地将乘积关系转换为分数关系,并利用哈希表高效地统计和查找最简分数,我们成功地将原本可能的四重循环优化为两重循环,大大提升了算法的效率。这种方法不仅适用于本题,也为解决类似的组合类问题提供了有益的思路。