2040-两个有序数组的第 K 小乘积

Raphael Liu Lv10

给你两个 从小到大排好序 且下标从 0 开始的整数数组 nums1nums2 以及一个整数 k ,请你返回第 _
_k (从 1 开始编号)小的 nums1[i] * nums2[j] _ _ 的乘积,其中 _ _0 <= i < nums1.length __ 且 __0 <= j < nums2.length

示例 1:

**输入:** nums1 = [2,5], nums2 = [3,4], k = 2
**输出:** 8
**解释:** 第 2 小的乘积计算如下:
- nums1[0] * nums2[0] = 2 * 3 = 6
- nums1[0] * nums2[1] = 2 * 4 = 8
第 2 小的乘积为 8 。

示例 2:

**输入:** nums1 = [-4,-2,0,3], nums2 = [2,4], k = 6
**输出:** 0
**解释:** 第 6 小的乘积计算如下:
- nums1[0] * nums2[1] = (-4) * 4 = -16
- nums1[0] * nums2[0] = (-4) * 2 = -8
- nums1[1] * nums2[1] = (-2) * 4 = -8
- nums1[1] * nums2[0] = (-2) * 2 = -4
- nums1[2] * nums2[0] = 0 * 2 = 0
- nums1[2] * nums2[1] = 0 * 4 = 0
第 6 小的乘积为 0 。

示例 3:

**输入:** nums1 = [-2,-1,0,1,2], nums2 = [-3,-1,2,4,5], k = 3
**输出:** -6
**解释:** 第 3 小的乘积计算如下:
- nums1[0] * nums2[4] = (-2) * 5 = -10
- nums1[0] * nums2[3] = (-2) * 4 = -8
- nums1[4] * nums2[0] = 2 * (-3) = -6
第 3 小的乘积为 -6 。

提示:

  • 1 <= nums1.length, nums2.length <= 5 * 104
  • -105 <= nums1[i], nums2[j] <= 105
  • 1 <= k <= nums1.length * nums2.length
  • nums1nums2 都是从小到大排好序的。

解法框架:二分查找

做过类似题目(668. 乘法表中第k小的数 或者 719. 找出第 k 小的距离对 )的不难发现这题的解法是二分查找。首先令 f(x) = 满足 nums_1[i] * nums_2[j] \le x 的数对个数,显然 f(x) 是关于 x 递增的,因此可以进行二分查找,找到第一个满足 f(x) \ge k 的 x 即可。

下面的给出三种求 f(x) 的解法。

解法一:双指针 — 分类讨论

在类似题目 668. 乘法表中第k小的数 719. 找出第 k 小的距离对 中,经典的解法是双指针,将单次 check 的时间复杂度降低到了 O(n)。那么这个题目呢?

首先,我们把 nums_1 分成 neg_1 和 pos_1,分别表示 nums_1 的 负数部分非负数部分
把 nums_2 分成 neg_2 和 pos_2,分别表示 nums_2 的 负数部分非负数部分

一图胜千言,下面用一幅图来解释双指针遍历的各种边界条件。

  • 情形一:nums_1[i] \ge 0, nums_2[j] \ge 0,分别对应 pos_1 和 pos_2;

  • 情形二:nums_1[i] < 0, nums_2[j] \ge 0,分别对应 neg_1 和 pos_2;

  • 情形三:nums_1[i] \ge 0, nums_2[j] < 0,分别对应 pos_1 和 neg_2;

  • 情形四:nums_1[i] < 0, nums_2[j] < 0,分别对应 neg_1 和 neg_2。

image.png

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
vector<int> neg1, pos1, neg2, pos2;
for(int v : nums1) (v < 0)? neg1.push_back(v) : pos1.push_back(v);
for(int v : nums2) (v < 0)? neg2.push_back(v) : pos2.push_back(v);

long long l = -1e10, r = 1e10;
while(l < r) {
long long m = (l + r) >> 1;
long long cur = 0;
for(int i = 0, j = (int)pos2.size() - 1; i < pos1.size(); ++i) {
while(j >= 0 && 1ll * pos1[i] * pos2[j] > m) --j;
cur += j + 1;
}
for(int i = 0, j = 0; i < neg1.size(); ++i) {
while(j < pos2.size() && 1ll * neg1[i] * pos2[j] > m) ++j;
cur += (int)pos2.size() - j;
}
for(int i = 0, j = 0; i < pos1.size(); ++i) {
while(j < neg2.size() && 1ll * pos1[i] * neg2[j] <= m) ++j;
cur += j;
}
for(int i = 0, j = (int)neg2.size() - 1; i < neg1.size(); ++i) {
while(j >= 0 && 1ll * neg1[i] * neg2[j] <= m) --j;
cur += (int)neg2.size() - 1 - j;
}
if(cur < k) l = m + 1;
else r = m;
}

return l;
}
};

如果不想思考那么多复杂的边界条件,那么下面这种双指针方法的思考量较小,可以一试。

解法一点五:双指针 — 等价转换

上面的分类讨论之所以麻烦,关键在于数字有正有负,需要分 4 种情况讨论。如果我们可以将 4 种情况都转化为一种情况呢?
答案是可以。

  • 如果 nums_1[i] < 0, nums_2[j] \ge 0,则 nums_1[i] \times nums_2[j] \le x
    等价于 (-nums_1[i]) \times nums_2[j] \ge x
  • 如果 nums_1[i] \ge 0, nums_2[j] < 0,则 nums_1[i] \times nums_2[j] \le x
    等价于 nums_1[i] \times (-nums_2[j]) \ge x
  • 如果 nums_1[i] \ge 0, nums_2[j] < 0,则 nums_1[i] \times nums_2[j] \le x
    等价于 (-nums_1[i]) \times (-nums_2[j]) \le x

这样我们就将有负数的情形二、三、四转化为全是非负整数的情形一了。注意,由于负数取反后,大小关系发生了逆转,所以需要将数组反转,以保持递增的顺序。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
long long calc(vector<int>& a, vector<int>& b, long long x, bool greater) {
long long res = 0;
if(!a.size() || !b.size()) return 0;
for(int i = 0, j = (int)b.size() - 1, k = (int)b.size() - 1; i < a.size(); ++i) {
while(j >= 0 && 1ll * a[i] * b[j] > x) --j;
while(k >= 0 && 1ll * a[i] * b[k] >= x) --k;
if(greater) res += (int)b.size() - 1 - k;
else res += j + 1;
}
return res;
}
long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
vector<int> neg1, pos1, neg2, pos2;
for(int v : nums1) (v < 0)? neg1.push_back(-v) : pos1.push_back(v);
for(int v : nums2) (v < 0)? neg2.push_back(-v) : pos2.push_back(v);
reverse(neg1.begin(), neg1.end());
reverse(neg2.begin(), neg2.end());

long long l = -1e10, r = 1e10;
while(l < r) {
long long m = (l + r) >> 1, cur = 0;
cur += calc(pos1, pos2, m, false);
cur += calc(neg1, pos2, -m, true);
cur += calc(pos1, neg2, -m, true);
cur += calc(neg1, neg2, m, false);
if(cur < k) l = m + 1;
else r = m;
}

return l;
}
};

解法二:解不等式 + 嵌套二分查找

我们可以首先在 nums_1 枚举数字 a,然后找出 nums_2 中满足 ab \le x 的数字的 b 的个数即可。

如果 a > 0,则不等式 ab \le x 成立的条件是 \displaystyle{b \le \left\lfloor x}{a} \right\rfloor(向下取整);

如果 a < 0,则不等式 ab \le x 成立的条件是 \displaystyle{b \ge \left\lceil x}{a} \right\rceil(向上取整);

a = 0 的情况比较特殊,此时若 x \ge 0,则 ab \le x 恒成立;否则 ab \le x 恒不成立。

由于 nums_2 已经排好序,故我们对于 nums_1 中的每个 a,只需要在 nums_2 中二分查找小于等于(或大于等于)x 的数量即可。

细节 + 小知识

如何实现向下取整呢?直接用整数除法 a/b 不行吗?不幸的是,实际上 C/C++ 的除法实现是 向 0 取整 的。例如 -1 / 2 的值是 0,而不是 -1。因此需要稍微改变一下思路。
思路一:采用浮点数 + floor 库函数实现向下取整;顺带用 ceil 库函数实现向上取整。

1
2
3
4
5
6
long long floorDiv(long long x, long long y) {
return floor(x / (double)y + 1e-7);
}
long long ceilDiv(long long x, long long y) {
return ceil(x / (double)y - 1e-7);
}

思路二:避免浮点数运算的实现:

1
2
3
4
5
6
7
8
9
10
long long floorDiv(long long x, long long y) {
if(y < 0) x = -x, y = -y;
if(x < 0) return (x - (y - 1)) / y;
return x / y;
}
long long ceilDiv(long long x, long long y) {
if(y < 0) x = -x, y = -y;
if(x < 0) return x / y;
return (x + (y - 1)) / y;
}

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Solution {
public:
long long floorDiv(long long x, long long y) {
if(y < 0) x = -x, y = -y;
if(x < 0) return (x - (y - 1)) / y;
return x / y;
}
long long ceilDiv(long long x, long long y) {
if(y < 0) x = -x, y = -y;
if(x < 0) return x / y;
return (x + (y - 1)) / y;
}
long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
long long l = -1e10, r = 1e10;
while(l < r) {
long long m = (l + r) >> 1;
long long cur = 0;
for(int v : nums1) {
if(v < 0) {
cur += nums2.end() - lower_bound(nums2.begin(), nums2.end(), ceilDiv(m, v));
} else if(v == 0) {
cur += (m >= 0)? nums2.size() : 0;
} else {
cur += upper_bound(nums2.begin(), nums2.end(), floorDiv(m, v)) - nums2.begin();
}
}
if(cur < k) l = m + 1;
else r = m;
}
return l;
}
};

解法三:前缀和

解法二中,我们也可以不用嵌套二分查找。注意到 -10^5 \le nums_1[i], nums_2[i] \le 10^5,我们完全可以开辟足够大的空间来统计 nums_2 中各个数字的数量,然后采用前缀和的方式来统计 nums_2 中 \le x 的数字数目,这样可以免去二分查找。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public:
long long sums[200005] = {0};
long long floorDiv(long long x, long long y) {
if(y < 0) x = -x, y = -y;
if(x < 0) return (x - (y - 1)) / y;
return x / y;
}
long long ceilDiv(long long x, long long y) {
if(y < 0) x = -x, y = -y;
if(x < 0) return x / y;
return (x + (y - 1)) / y;
}
long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
for(int v : nums2) sums[v + 100000]++;
for(int i = 1; i <= 200000; ++i) sums[i] += sums[i-1];

auto sum = [&](long long x) -> long long {
if(x < -100000) return 0;
if(x > 100000) return sums[200000];
return sums[x + 100000];
};

long long l = -1e10, r = 1e10;
while(l < r) {
long long m = (l + r) >> 1;

long long cnt = 0;
for(int v : nums1) {
if(v < 0) cnt += sum(100000) - sum(ceilDiv(m, v) - 1);
if(v == 0 && m >= 0) cnt += nums2.size();
if(v > 0) cnt += sum(floorDiv(m, v));
}

if(cnt < k) l = m + 1;
else r = m;
}

return l;
}
};
 Comments