题目
类型:数组
解题思路
二分
将所有「数对和」按照升序排序,两端的值分别为 l = nums1[0] + nums2[0] 和 r = nums1[n - 1] + nums2[m - 1]
因此可以在值域 [l, r] 上进行二分,找到第一个满足「点对和小于等于 x 的,且数量超过 k 的值 x」。
之所以能够二分,是因为 x 所在的点对和数轴上具有二段性:
- 点对和小于 x 的点对数量少于 k 个;
- 点对和大于等于 x 的点对数量大于等于 k 个。
判定小于等于 x 的点对数量是否大于等于 k 个这一步可直接使用循环来做,由于二分是从中间值开始,这一步不会出现跑满两层循环的情况。
当二分出第 k 小的值为 x 后,由于存在不同点对的点对和值相等,我们需要先将所有点对和小于等于 x 的值加入答案,然后酌情把值等于 x 的点对加入答案,知道满足答案数量为 k。
找值为 x 的所有点对这一步,可以通过枚举 nums1[i] ,然后在 nums2 上二分目标值 x - nums1[i] 的左右端点来做。
最后,在所有处理过程中,都可以利用答案数组的大小与 k 的关系做剪枝。
代码
class Solution {
int[] nums1, nums2;
int n, m;
public List<List<Integer>> kSmallestPairs(int[] n1, int[] n2, int k) {
nums1 = n1; nums2 = n2;
n = nums1.length; m = nums2.length;
List<List<Integer>> ans = new ArrayList<>();
int l = nums1[0] + nums2[0], r = nums1[n - 1] + nums2[m - 1];
while (l < r) {
int mid = l + r >> 1;
if (check(mid, k)) r = mid;
else l = mid + 1;
}
int x = r;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (nums1[i] + nums2[j] < x) {
List<Integer> temp = new ArrayList<>();
temp.add(nums1[i]); temp.add(nums2[j]);
ans.add(temp);
} else break;
}
}
for (int i = 0; i < n && ans.size() < k; i++) {
int a = nums1[i], b = x - a;
int c = -1, d = -1;
l = 0; r = m - 1;
while (l < r) {
int mid = l + r >> 1;
if (nums2[mid] >= b) r = mid;
else l = mid + 1;
}
if (nums2[r] != b) continue;
c = r;
l = 0; r = m - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums2[mid] <= b) l = mid;
else r = mid - 1;
}
d = r;
for (int p = c; p <= d && ans.size() < k; p++) {
List<Integer> temp = new ArrayList<>();
temp.add(a); temp.add(b);
ans.add(temp);
}
}
return ans;
}
boolean check(int x, int k) {
int ans = 0;
for (int i = 0; i < n && ans < k; i++) {
for (int j = 0; j < m && ans < k; j++) {
if (nums1[i] + nums2[j] <= x) ans++;
else break;
}
}
return ans >= k;
}
}