每日算法-250527
每日算法 - 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
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
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;}
}