How to find the biggest possible combination of two numbers under a certain threshold from a list?
Question:
So let’s say if the list is [2,5,14,18,44], what I want to find out is what the biggest sum of two numbers under 60 would be. So the correct answer should be (14,44)
Right now I am calculating every possible combination before getting the result. It is horribly inefficient
Edit:
Here is my code. It serves to solve the problem but it is slow because the list is really long
import itertools
#build_list is a long list of all constructions with the construction cost
#build_power1 is the available amount of funds
#in the first line I exclude all the constructions beyond total funds
build_list = [ i for i in self.list_of_constructions if i['cost']<build_power1]
iter_all_build_combos = itertools.combinations(build_list,2)
list_all_build_combos_with_cost = []
for combo in iter_all_build_combos:
sum1 = combo[0]['cost'] + combo[1]['cost']
list_all_build_combos_with_cost.append((sum1, combo))
#here I have a list with all combonations alongside with the sum of the costs
list_all_build_combos_with_cost.sort()
Answers:
Here’s one possible approach based on iterating from both ends of the (sorted) list:
def best_pair(nums: list[int], threshold: int) -> tuple[int, int]:
"""Return the pair of nums whose sum is closest to threshold
without meeting or exceeding it."""
nums = sorted(nums)
best = nums[0], nums[1]
if sum(best) >= threshold:
raise ValueError(f'No pair exists under {threshold}.')
j = len(nums) - 1
for i in range(len(nums) - 1):
while nums[i] + nums[j] >= threshold:
j -= 1
if j <= i:
break
if nums[i] + nums[j] > sum(best):
best = nums[i], nums[j]
return best
assert best_pair([2, 4, 14, 18, 44], 60) == (14, 44)
For any given number in the list, there is a particular other number that forms the best possible pair. If you iterate through the list in order from smallest to largest (i
), the best pairing for those numbers (j
) will always decrease. As i
increases, all you need to do is incrementally decrease j
enough to keep it from "busting" the pair it forms with that i
. Once j <= i
you can’t form any more valid pairs, and you’re done.
This drastically cuts down on the number of possibilities that you need to consider; since each iteration only goes in one direction through the list, the whole thing ends up being O(n).
Note that calling sorted()
is O(n log n) only if the list isn’t already sorted; for an already-sorted list it’s O(n). If you know for certain that the list will always be sorted, you can save some time by skipping it, but it doesn’t change the big-O runtime.
Assuming the list is sorted as given in the example provided you can use a two-pointer approach from either side of the list for a more efficient O(n)
solution than the O(n^2)
you currently have:
from typing import Optional
def find_biggest_combination(numbers: list[int],
threshold: int) -> Optional[tuple[int, int]]:
"""
Finds the biggest combination of two numbers under a certain threshold
from a sorted list of numbers.
Args:
numbers: A sorted list of integers to search for a combination.
threshold: An integer representing the upper limit for the sum of
the combination.
Returns:
A tuple of two integers representing the biggest combination found
whose sum is less than the threshold, or None if no such combination
exists.
"""
left, right = 0, len(numbers) - 1
biggest_combination: Optional[tuple[int, int]] = None
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum >= threshold:
right -= 1
else:
if not biggest_combination or current_sum > sum(biggest_combination):
biggest_combination = (numbers[left], numbers[right])
left += 1
return biggest_combination
print(f'{find_biggest_combination([2, 5, 14, 18, 44], 60) = }')
Output:
find_biggest_combination([2, 5, 14, 18, 44], 60) = (14, 44)
So let’s say if the list is [2,5,14,18,44], what I want to find out is what the biggest sum of two numbers under 60 would be. So the correct answer should be (14,44)
Right now I am calculating every possible combination before getting the result. It is horribly inefficient
Edit:
Here is my code. It serves to solve the problem but it is slow because the list is really long
import itertools
#build_list is a long list of all constructions with the construction cost
#build_power1 is the available amount of funds
#in the first line I exclude all the constructions beyond total funds
build_list = [ i for i in self.list_of_constructions if i['cost']<build_power1]
iter_all_build_combos = itertools.combinations(build_list,2)
list_all_build_combos_with_cost = []
for combo in iter_all_build_combos:
sum1 = combo[0]['cost'] + combo[1]['cost']
list_all_build_combos_with_cost.append((sum1, combo))
#here I have a list with all combonations alongside with the sum of the costs
list_all_build_combos_with_cost.sort()
Here’s one possible approach based on iterating from both ends of the (sorted) list:
def best_pair(nums: list[int], threshold: int) -> tuple[int, int]:
"""Return the pair of nums whose sum is closest to threshold
without meeting or exceeding it."""
nums = sorted(nums)
best = nums[0], nums[1]
if sum(best) >= threshold:
raise ValueError(f'No pair exists under {threshold}.')
j = len(nums) - 1
for i in range(len(nums) - 1):
while nums[i] + nums[j] >= threshold:
j -= 1
if j <= i:
break
if nums[i] + nums[j] > sum(best):
best = nums[i], nums[j]
return best
assert best_pair([2, 4, 14, 18, 44], 60) == (14, 44)
For any given number in the list, there is a particular other number that forms the best possible pair. If you iterate through the list in order from smallest to largest (i
), the best pairing for those numbers (j
) will always decrease. As i
increases, all you need to do is incrementally decrease j
enough to keep it from "busting" the pair it forms with that i
. Once j <= i
you can’t form any more valid pairs, and you’re done.
This drastically cuts down on the number of possibilities that you need to consider; since each iteration only goes in one direction through the list, the whole thing ends up being O(n).
Note that calling sorted()
is O(n log n) only if the list isn’t already sorted; for an already-sorted list it’s O(n). If you know for certain that the list will always be sorted, you can save some time by skipping it, but it doesn’t change the big-O runtime.
Assuming the list is sorted as given in the example provided you can use a two-pointer approach from either side of the list for a more efficient O(n)
solution than the O(n^2)
you currently have:
from typing import Optional
def find_biggest_combination(numbers: list[int],
threshold: int) -> Optional[tuple[int, int]]:
"""
Finds the biggest combination of two numbers under a certain threshold
from a sorted list of numbers.
Args:
numbers: A sorted list of integers to search for a combination.
threshold: An integer representing the upper limit for the sum of
the combination.
Returns:
A tuple of two integers representing the biggest combination found
whose sum is less than the threshold, or None if no such combination
exists.
"""
left, right = 0, len(numbers) - 1
biggest_combination: Optional[tuple[int, int]] = None
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum >= threshold:
right -= 1
else:
if not biggest_combination or current_sum > sum(biggest_combination):
biggest_combination = (numbers[left], numbers[right])
left += 1
return biggest_combination
print(f'{find_biggest_combination([2, 5, 14, 18, 44], 60) = }')
Output:
find_biggest_combination([2, 5, 14, 18, 44], 60) = (14, 44)