Search code examples
pythonalgorithmfunctionrecursiondynamic

Find the minimum and maximum possible sum, for a given number from an integer array


There is an ordered array of integer elements and a limit. It is necessary to find the minimum possible and maximum possible sum that can greater or equal the limit, for the given elements.

The minimum sum is the minimum sum of the sequence greater or equal than limit

The maximum sum, is the sum of the sequence that satisfies the conditions, and gives the largest result

Conditions:

  • The elements of the sequence can be repeated: for example you can take 101 + 101 + 101 + 101 + 101 + 201 + 201
  • When summing, you can not skip elements of the array, for example, you can not add 101 + 301 at once, must be 101 + 201 + 301
  • Stop summing as soon as you reach the limit

First Example:

arr = [100, 200, 300, 1000]

limit = 1000

The minimum here is 1000, because we can take 10 times 100 for example.

The maximum here is 1900, since we can take 100 + 200 + 300 + 300 + 1000.

Second Example:

arr = [3, 10, 15]

limit = 1000

The minimum here is 1000, because we can take 300 times 3 and 10 times 10.

The maximum here is 1014, since we can take 318 times 3 and 3 time 10 and 2 times 15

I wrote 1 algorithm based on recursion and one based on dynamic programming. Both the first and the second one produce incorrect results in certain situations

Example for Python:

from pprint import pprint
from typing import Tuple, List

import resource
import sys

resource.setrlimit(resource.RLIMIT_STACK, (0x10000000, resource.RLIM_INFINITY))
sys.setrecursionlimit(0x100000)


class CollectingRangeAnalyzer:
    def __init__(self):
        self.memo = {}

    def recursion_method(self, pools: List[int], target_cap: int) -> Tuple[float, float]:

        self._collect_helper(pools, target_cap, [], 0)

        if not self.memo:
            raise ValueError("No valid range found")

        max_cap = max(self.memo)
        min_cap = min(self.memo, key=lambda x: x if x >= target_cap else float("inf"))
        return max_cap, min_cap

    def _collect_helper(self, pools_, target_sum_, path, cur_sum):

        if cur_sum >= target_sum_:
            return

        if self.memo.get(cur_sum):
            return

        for i, v in enumerate(pools_):
            cur_sum += v
            path.append(v)

            self._collect_helper(pools_, target_sum_, path, cur_sum)

            self.memo[cur_sum] = True

            cur_sum -= v
            path.pop()

    @staticmethod
    def dynamic_method(arr, limit):
        table = []
        arr_size = len(arr)
        n_cols = limit // arr[0] + 1
        max_cap = float("-inf")
        min_cap = float("inf")

        for i in range(arr_size):
            table.append([])
            for j in range(n_cols):
                table[i].append(0)

        for i in range(arr_size):
            for j in range(n_cols):
                if i == 0 and arr[0] * (j + 1) <= limit + arr[i]:
                    table[i][j] = arr[i] * (j + 1)
                elif i > j or j < 0:
                    table[i][j] = table[i - 1][j]
                else:
                    diagonal_prev = table[i - 1][j - 1]
                    j_prev = table[i][j-1]

                    if diagonal_prev < limit:
                        table[i][j] = diagonal_prev + arr[i]
                    else:
                        table[i][j] = max(diagonal_prev, j_prev)

                max_cap = max(table[i][j], max_cap)
                min_cap = min(table[i][j], min_cap, key=lambda x: x if x >= limit else float("inf"))

        return max_cap, min_cap

# First Example
first_analysis_class = CollectingRangeAnalyzer()

first_array = [100, 200, 300, 1000]
first_limit = 1000

rec_first = first_analysis_class.recursion_method(first_array, first_limit)     # output: (1900, 1000) SUCCESS
dynamic_first = first_analysis_class.dynamic_method(first_array, first_limit)   # output: (1900, 1000) SUCCESS

# But if added the 10000 in first_array and run again I'll get the wrong result in the recursion function.
#
# first_array = [100, 200, 300, 1000, 10000]
#
#
# rec_first = first_analysis_class.recursion_method(first_array, first_limit)     # output: (10900, 1000) WRONG
# dynamic_first = first_analysis_class.dynamic_method(first_array, first_limit)   # output: (1900, 1000) SUCCESS


# Second Example
second_analysis_class = CollectingRangeAnalyzer()

second_array = [3, 10, 15]
second_limit = 1000

rec_second = second_analysis_class.recursion_method(second_array, second_limit)    # output: (1014, 1000) SUCCESS
dynamic_second = second_analysis_class.dynamic_method(second_array, second_limit)  # output: (1012, 1000) WRONG



Solution

  • I assume that you need to always start at the beginning of your sequence. Your examples were still not clear on that. If that is true then this should work:

    import heapq
    
    def min_max_limit_sum (arr, limit):
        # A path will be (idx_k, ...(idx_1, (idx_0, None))...)
    
        # By (value, index): path
        known = {}
    
        # heap of (value, counter, (next_index, prev_path))
        upcoming = [(0, 0, (0, None))]
        counter = 1
    
        best_min = limit + sum(arr)
        min_path = None
        best_max = limit - 1
        max_path = None
        # While there is stuff in the heap.
        while len(upcoming):
            value, _, next_path = heapq.heappop(upcoming)
            i = next_path[0]
            if (value, i) in known or len(arr) <= i:
                # Filter out alternate routes, and running off the end of the sequence.
                continue
            else:
                path = next_path[1]
                known[(value, i)] = path
                if value < limit:
                    value += arr[i]
                    heapq.heappush(upcoming, (value, counter, (i, next_path)))
                    heapq.heappush(upcoming, (value, counter, (i+1, next_path)))
                    counter += 1
                else:
                    if value < best_min:
                        best_min = value
                        min_path = path
                    if best_max < value:
                        best_max = value
                        max_path = path
        return (best_min, min_path, best_max, max_path)
    
    print(min_max_limit_sum([100, 200, 300, 1000], 1000))
    

    If the assumption is not true, just set upcoming to have an entry for every single index.