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()
Asked By: Aaron Grace

||

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.

Answered By: Samwise

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)
Answered By: Sash Sinha