每日算法 - 250527
2364. 统计坏数对的数目
题目

思路与解题过程
核心思想:转换问题 + 哈希表计数
题目定义"坏数对" (i, j) 为满足 i < j 且 j - i != nums[j] - nums[i] 的数对。
直接统计"坏数对"可能比较复杂,我们可以考虑其补集------"好数对"。
"好数对" (i, j) 满足 i < j 且 j - i == nums[j] - nums[i].
对"好数对"的条件进行变形:
j - i == nums[j] - nums[i]
nums[i] - i == nums[j] - j
令 d[x] = nums[x] - x。那么,"好数对"的条件就变成了 i < j 且 d[i] == d[j].
解法:直接统计坏数对
虽然可以通过"总数对 - 好数对"来计算,但我们也可以直接统计"坏数对"。
遍历数组 nums,对于每个元素 nums[i](及其下标 i):
- 计算
key = nums[i] - i. - 我们想知道,在
nums[i]之前(即下标p < i),有多少个元素nums[p]满足nums[p] - p == key。这些(p, i)会构成"好数对"。 - 设
map存储了之前遇到的nums[x] - x的值及其出现的次数。val = map.getOrDefault(key, 0)就表示在nums[i]之前,有多少个下标p使得nums[p] - p == key. - 对于当前的
nums[i], 总共有i个可能的配对下标p(从0到i-1)。 - 其中,有
val个下标p与当前i构成了"好数对"。 - 因此,与当前
i构成的"坏数对"的数目就是i - val。我们将这个数目累加到总结果ret中。 - 然后,更新
map:将key(即nums[i] - i) 的计数加 1,map.put(key, val + 1). 这为后续的元素nums[j](其中j > i) 提供了信息。
这种方法在遍历到 nums[i] 时,计算的是所有以 i 为较大下标 j 的坏数对 (p, i) 的数量。
复杂度
- 时间复杂度 : O ( N ) O(N) O(N), 其中 N N N 是数组
nums的长度。我们只需要遍历数组一次。哈希表的插入和查找操作平均时间复杂度为 O ( 1 ) O(1) O(1)。 - 空间复杂度 : O ( N ) O(N) O(N), 最坏情况下,哈希表可能需要存储 N N N 个不同的
nums[i] - i的值。
Code
java
class Solution {
public long countBadPairs(int[] nums) {
long ret = 0;
Map<Integer, Integer> map = new HashMap<>(nums.length);
for (int i = 0; i < nums.length; i++) {
int key = nums[i] - i;
int val = map.getOrDefault(key, 0);
ret += (long)i - val;
map.put(key, val + 1);
}
return ret;
}
}
2874. 有序三元组中的最大值 II
题目

思路与解题过程
核心思想:枚举中间元素 + 前缀最大值 + 后缀最大值
我们要找的是三元组 (i, j, k) 使得 i < j < k,并且 (nums[i] - nums[j]) * nums[k] 的值最大。
题目约束 1 <= nums[x] <= 10^6,所以 nums[k] 始终是正数。
为了使乘积最大,我们需要:
nums[k]尽可能大。nums[i] - nums[j]尽可能大(且为正)。这意味着nums[i]要大,nums[j]要小。
解法步骤:
-
枚举中间元素
j:我们可以遍历所有可能的
j(从1到n-2,其中n是数组长度)。 -
优化
nums[i]和nums[k]的选择:nums[i]必须从nums[0...j-1]中选取。为了使nums[i] - nums[j]最大,nums[i]应为nums[0...j-1]中的最大值。nums[k]必须从nums[j+1...n-1]中选取。为了使整个表达式最大 (因为nums[k]是正数),nums[k]应为nums[j+1...n-1]中的最大值。
-
预计算/动态计算:
- 后缀最大值
suffixMax: 我们可以预先计算一个数组suffixMax,其中suffixMax[p]表示nums[p...n-1]中的最大值。这样,对于给定的j,nums[k]的最佳选择就是suffixMax[j+1]。 - 前缀最大值
prevMax: 当我们遍历j从左到右时,可以动态维护nums[0...j-1]的最大值。
令prevMax为max(nums[0], nums[1], ..., nums[j-1]).
- 后缀最大值
-
遍历与更新:
- 初始化
ret = 0。 - 初始化
prevMax = nums[0]. - 遍历
j从1到n-2:- 当前的
prevMax就是max(nums[0...j-1]). nums[k]的最佳选择是suffixMax[j+1].- 如果
prevMax > nums[j](确保nums[i] - nums[j]为正,从而得到正的乘积),则计算current_value = (long)(prevMax - nums[j]) * suffixMax[j+1]. - 更新
ret = Math.max(ret, current_value). - 更新
prevMax = Math.max(prevMax, nums[j]),为下一次迭代(当j变成j+1时)做准备。此时,当前的nums[j]将成为前缀的一部分。
- 当前的
- 初始化
注意 : 乘积可能超过 int 的范围,所以计算时需要使用 long 类型。
复杂度
- 时间复杂度 : O ( N ) O(N) O(N).
- 计算
suffixMax数组需要 O ( N ) O(N) O(N). - 主循环遍历
j需要 O ( N ) O(N) O(N).
- 计算
- 空间复杂度 : O ( N ) O(N) O(N), 主要用于存储
suffixMax数组。
Code
java
class Solution {
public long maximumTripletValue(int[] nums) {
long ret = 0;
int n = nums.length;
int[] suffixMax = new int[n];
suffixMax[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; i--) {
suffixMax[i] = Math.max(nums[i], suffixMax[i + 1]);
}
int prevMax = nums[0];
for (int j = 1; j < n - 1; j++) {
if (prevMax > nums[j]) {
long currentVal = (long)(prevMax - nums[j]) * suffixMax[j + 1];
ret = Math.max(ret, currentVal);
}
prevMax = Math.max(prevMax, nums[j]);
}
return ret;
}
}