咒语和药水的成功对数
问题描述
给你两个正整数数组 spells
和 potions
,长度分别为 n
和 m
,其中 spells[i]
表示第 i
个咒语的能量强度,potions[j]
表示第 j
瓶药水的能量强度。同时给你一个整数 success
。一个咒语和药水的能量强度相乘如果大于等于 success
,则视为一对成功的组合。
请返回一个长度为 n
的整数数组 pairs
,其中 pairs[i]
是能跟第 i
个咒语成功组合的药水数目。
示例:
- 输入:
spells = [5,1,3], potions = [1,2,3,4,5], success = 7
,输出:[4,0,3]
- 第0个咒语(5)与药水[2,3,4,5]组合成功(5×2=10≥7),共4对
- 第1个咒语(1)与所有药水组合都不满足(1×5=5<7),共0对
- 第2个咒语(3)与药水[3,4,5]组合成功(3×3=9≥7),共3对
- 输入:
spells = [3,1,2], potions = [8,5,8], success = 16
,输出:[2,0,2]
约束条件:
n == spells.length
,m == potions.length
1 <= n, m <= 10^5
1 <= spells[i], potions[i] <= 10^5
1 <= success <= 10^10
解题思路
核心分析
对于每个咒语 spells[i]
,需要找到满足 spells[i] * potions[j] >= success
的药水数量。直接遍历所有药水(时间复杂度 O(n*m)
)会因数据规模过大(105×105=10^10 操作)超时,因此需要更高效的方法。
最优解法:排序 + 二分查找
- 排序药水数组:对
potions
排序,为二分查找做准备(时间O(m log m)
)。 - 二分查找每个咒语的临界值:
- 对于咒语
s
,满足s * p >= success
的药水p
需满足p >= ceil(success / s)
(向上取整)。 - 在排序后的
potions
中,用二分查找找到第一个大于等于临界值的位置,该位置右侧的所有元素均满足条件。 - 成功对数 = 药水总数 - 临界位置索引(时间
O(n log m)
)。
- 对于咒语
关键公式
临界值计算:target = (success + s - 1) // s
(用整数运算实现向上取整,避免浮点数精度问题)。
代码实现
C++ 实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;class Solution {
public:vector<int> successfulPairs(vector<int>& spells, vector<int>& potions, long long success) {int n = spells.size();int m = potions.size();vector<int> pairs(n);// 排序药水数组sort(potions.begin(), potions.end());for (int i = 0; i < n; ++i) {long long s = spells[i];// 计算临界药水值(向上取整)long long target = (success + s - 1) / s;// 找到第一个 >= target 的位置auto it = lower_bound(potions.begin(), potions.end(), target);// 成功对数 = 总数 - 位置索引pairs[i] = potions.end() - it;}return pairs;}
};int main() {Solution solution;vector<int> spells1 = {5,1,3};vector<int> potions1 = {1,2,3,4,5};vector<int> res1 = solution.successfulPairs(spells1, potions1, 7);for (int num : res1) cout << num << " "; // 输出: 4 0 3 cout << endl;vector<int> spells2 = {3,1,2};vector<int> potions2 = {8,5,8};vector<int> res2 = solution.successfulPairs(spells2, potions2, 16);for (int num : res2) cout << num << " "; // 输出: 2 0 2cout << endl;return 0;
}
Python 实现
import bisect
from typing import Listclass Solution:def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:m = len(potions)pairs = []# 排序药水数组potions.sort()for s in spells:# 计算临界药水值(向上取整)target = (success + s - 1) // s# 找到第一个 >= target 的位置pos = bisect.bisect_left(potions, target)# 成功对数 = 总数 - 位置索引pairs.append(m - pos)return pairs# 测试
solution = Solution()
print(solution.successfulPairs([5,1,3], [1,2,3,4,5], 7)) # 输出: [4, 0, 3]
print(solution.successfulPairs([3,1,2], [8,5,8], 16)) # 输出: [2, 0, 2]
Java 实现
import java.util.Arrays;public class SuccessfulPairs {public int[] successfulPairs(int[] spells, int[] potions, long success) {int n = spells.length;int m = potions.length;int[] pairs = new int[n];// 排序药水数组Arrays.sort(potions);for (int i = 0; i < n; i++) {long s = spells[i];// 计算临界药水值(向上取整)long target = (success + s - 1) / s;// 找到第一个 >= target 的位置int pos = lowerBound(potions, target);// 成功对数 = 总数 - 位置索引pairs[i] = m - pos;}return pairs;}// 自定义二分查找,返回第一个 >= target 的位置private int lowerBound(int[] arr, long target) {int left = 0, right = arr.length;while (left < right) {int mid = left + (right - left) / 2;if (arr[mid] < target) {left = mid + 1;} else {right = mid;}}return left;}public static void main(String[] args) {SuccessfulPairs solution = new SuccessfulPairs();int[] spells1 = {5, 1, 3};int[] potions1 = {1, 2, 3, 4, 5};int[] res1 = solution.successfulPairs(spells1, potions1, 7);for (int num : res1) System.out.print(num + " "); // 输出: 4 0 3 System.out.println();int[] spells2 = {3, 1, 2};int[] potions2 = {8, 5, 8};int[] res2 = solution.successfulPairs(spells2, potions2, 16);for (int num : res2) System.out.print(num + " "); // 输出: 2 0 2System.out.println();}
}
复杂度分析
操作 | 时间复杂度 | 空间复杂度 |
---|---|---|
排序药水数组 | O(m log m) | O(log m) |
处理所有咒语 | O(n log m) | O(1) |
整体 | O(m log m + n log m) | O(n + log m) |
总结
本题的最优解法通过排序+二分查找将时间复杂度从暴力法的 O(n*m)
优化至 O((n+m) log m)
,完美适配 10^5
级别的数据规模。核心思路是:
- 排序药水数组,为高效查询奠定基础;
- 对每个咒语计算临界药水值,利用二分查找快速定位符合条件的药水范围;
- 通过整数运算
(success + s - 1) // s
精准实现向上取整,避免浮点数精度问题。
这种方法兼顾了时间效率和实现简洁性,是处理此类“范围查询”问题的典型范式。