6

I have one list and one target number.

  • I need to print the number of ways to reach target
l = [1,2,3]

target = 5

Number of ways is below

  • 1+ 1 + 1 + 1 + 1 = 5
  • 1 + 1 + 1+ 2 =5
  • 1 + 2 + 2 = 5
  • 1 +1 +3 =5
  • 2 + 3 = 5

Output 5 ways

def countways(l, target ):

    if (target  == 0):
        return 0
    else:
        pass
if __name__ == "__main__":
     l = [1,2,3], 
     target = 5
     countways(l, target )

Can we do it using native python or itertools?

Red
  • 26,798
  • 7
  • 36
  • 58
sim
  • 524
  • 3
  • 14
  • `1+1+1+2` also works btw – 12944qwerty Jun 04 '21 at 13:01
  • 6
    This problem is usually known as "subset sum". This version, with replacement, has been discussed [here](https://stackoverflow.com/questions/11929938/subset-sum-algorithm-with-repetition-of-numbers-in-the-set) and the basic version of the problem without replacement has been discussed [here](https://stackoverflow.com/questions/4632322/finding-all-possible-combinations-of-numbers-to-reach-a-given-sum) – He3lixxx Jun 07 '21 at 21:14

5 Answers5

6

I will assume all numbers are positive.

You can use itertools to check all combinations_with_replacement, as proposed by Ann, but it will get unnecessarily slow for large inputs as there are exponentially many combinations.

Naive recursive approach

This version uses the recursion approach Nevetha described, which allows to early-return out of branches where no matches will ever be found, but should do it with replacement.

As with the other results: It is fairly easy to extend to print the actual summands. We would simply add an optional, third parameter that gives the summands so far, and print it in the target == 0 case.

def countWays(elements, target):
    if target < 0:
        return 0

    if target == 0:
        return 1

    total = 0
    for index, element in enumerate(elements):
       total += countWays(elements[index:], target - element)
 
    return total
 
 
if __name__ == '__main__':
    print(countWays([1, 2, 3], 5))
    print(countWays([5, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], 30))
    print(countWays([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37], 40))
    print(countWays([1, 2, 3, 4, 5], 200))

DP algorithm

As you can see, for the target of 200, this already takes quite some time to execute. This is because at the end of the recursion, we always only add one to our result. This can be improved by using dynamic programming -- either by simply adding caching (example code, of course the global variable shouldn't be used in any real program):

cache = {}
def countWays(elements, target):
    global cache

    if target < 0:
        return 0

    if target == 0:
        return 1

    cache_key = (tuple(elements), target)
    if cache_key in cache:
        return cache[cache_key]

    total = 0
    for index, element in enumerate(elements):
       total += countWays(elements[index:], target - element)

    cache[cache_key] = total
    return total

or by directly building the dp array as already discussed here:

def countWays(elements, target):   
    dp = [1] + [0] * target
    for element in elements:
        for i in range(0, target - element + 1):
            dp[i + element] += dp[i]
    return dp[target]
He3lixxx
  • 3,263
  • 1
  • 12
  • 31
  • what is `dp = [1] + [0] * target` – sim Jun 08 '21 at 05:18
  • `[0] * 3` results in `[0, 0, 0]`, so `[0] * target` is a list of "target-many" zeros. `+` between the lists is list concatenation, so this gives you a list `[1, 0, 0, 0, 0, ...]` with length `target + 1`. The variable itself is just the dynamic programming array: At position x, we store how many different ways we have to achieve the sum x when using only the elements we iterated over so far. So, initially, before we included any summands, we have exactly one way to reach the sum 0. After the loop, `dp[target]` thus contains how many ways we have to sum up to `target` using all elements. – He3lixxx Jun 08 '21 at 09:05
  • Can you tell me? how you derive this algorithm. Suggest something will I also can write this kind of algorithm one day – sim Jun 09 '21 at 07:50
  • Hmm, for this algorithm, you'd probably need some experience in dynamic programming. For that, its probably easiest if you look for resources about this topic, maybe in your native language. If I google "dynamic programming tutorial" or similar, I get tons of results, but I can't tell you which of them are good. – He3lixxx Jun 09 '21 at 13:31
  • Why the third approach is saying ```If the target is reasonably small```, second approach and third is same ? right? because both are saving in cache – sim Aug 06 '21 at 08:25
  • 1
    If you only have the algorithmic complexity in mind, the second and third approaches are identical. For a real machine, executing the third approach will probably be a bit faster (almost linear memory access vs hashmap lookups). The linked answer says "If the target is reasonably small" because this problem is [NP-hard](https://en.wikipedia.org/wiki/NP-hardness). So far, we can't solve it in polynomial time depending on the number of input elements. However, we can solve it in polynomial time depending on the value of the target ("Pseudo-polynomial runtime") (approach 2 and 3 do this). – He3lixxx Aug 08 '21 at 11:06
  • There is no way to give 2 upvote for you, otherwise I would have done that. Thank you so much man for your time to explain – sim Aug 09 '21 at 12:05
2

You can use the itertools.combinations_with_replacement() method:

from itertools import combinations_with_replacement as cwr

def countways(l, target):
    return len([1 for i in range(target) for j in cwr(l, i + 1) if sum(j) == target])

print(countways([1, 2, 3], 5))

Output:

5

Explanation

The docstring for the method is as follows:

Return r length subsequences of elements from the input iterable allowing individual elements to be repeated more than once.

So it's like the itertools.combinations() method, expect that the itertools.combinations_with_replacement() method allows elements to be repeated.

If you want to visualize the different solutions:

from itertools import combinations_with_replacement as cwr

def countways(l, target):
    for i in range(target):
        for j in cwr(l, i + 1):
            if sum(j) == target:
                print(j)

countways([1, 2, 3], 5)

Output:

(2, 3)
(1, 1, 3)
(1, 2, 2)
(1, 1, 1, 2)
(1, 1, 1, 1, 1)

Note: This can be very slow for large inputs, as pointed out by @He3lixxx (+1). You can improve the efficiency by filtering out numbers in l that are greater than target, and by dividing the target in range(target) by max(l) and min(l), like so:

def countways(l, target):
    l = [i for i in l if i <= target]
    return len([1 for i in range(target // max(l) - 1,
                                 target // min(l)) for j in cwr(l, i + 1) if sum(j) == target])
Red
  • 26,798
  • 7
  • 36
  • 58
  • 1
    This works, but it goes through all combinations and thus will have the exponential runtime, even if it is clear that most of the combinations will not meet the requirement -- e.g. try calling `countways([5, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40], 30)` and wait for it to finish. – He3lixxx Jun 07 '21 at 13:40
  • While filtering for min and max solves the performance for the specific case I posted, the actual core of the problem still remains: This approach checks an exponential space, and it does not have an early way to leave branches where no results will ever be found. This still remains true, for example for this call: `countways([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37], 40)`. Just for the last length `i`, it will check more than 12C20 combinations, which is approx. 8 * 10^7. Thus, it takes a long time to finish. – He3lixxx Jun 07 '21 at 18:31
  • @He3lixxx Sure, I know. But the OP's numbers might not be that large. – Red Jun 07 '21 at 22:52
1

A dynamic programming approach will work. This code outputs all the possible combinations possible. Just print the length of the list to get the total counts. Also, all the possible combinations are unique and don't repeat.

def ways(l, target):
    dp =[ [] for i in range(target+1) ]
    l.sort()
    n=len(l)
    for i in range(n):
        for j in range(l[i], target+1):    
            if j==l[i]:
                dp[j].append([l[i]])
            else:
                if dp[j-l[i]]:   
                    for u in dp[j-l[i]]:       
                        dp[j].append(u+[l[i]])       
    return dp[-1]

if __name__ == "__main__":
     l = [1,2,3] 
     target = 5
     print(len(ways(l, target)))
     l = [5, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
     target = 30
     print(len(ways(l, target)))
Saankhya Mondal
  • 151
  • 1
  • 2
  • 11
  • This surely works, but there is no need to make it this complex. By storing all the intermediate results, you add a huge memory overhead. If I run this with `ways([1, 2, 3, 4, 5], 200)`, it consumes more than 15GB of memory on my machine, forcing it into swapping and getting really slow (windows 10, python 3.9). – He3lixxx Jun 07 '21 at 19:00
  • @He3lixxx, we have to store the intermediate results in order to check if anything's repeating or not. – Saankhya Mondal Jun 07 '21 at 19:12
1

You can resolve using itertools like the example below:

import itertools 

def countways(l, target): 
    data = []  
    for length in range(1, target+1):  
        data.extend([x for x in itertools.combinations_with_replacement(l, length) if sum(x) == target]) 
    return len(data)

You need to create combinations with all the sizes between 1 and target so the for is necessary. In each iteration you save the combinations with the sum values equal target. In the end you just need to count the saved lists.

daniboy000
  • 1,069
  • 2
  • 16
  • 26
-3

The below one works

Count ways to calculate a target from elements of a specified list

def countWays(A, n, target):
 
    # base case: if a target is found
    if target == 0:
        return 1
 
    # base case: no elements are left
    if n < 0:
        return 0
 
    # 1. ignore the current element
    exclude = countWays(A, n - 1, target)
 
    # 2. Consider the current element
    #    2.1. Subtract the current element from the target
    #    2.2. Add the current element to the target
    include = countWays(A, n - 1, target - A[n]) + countWays(A, n - 1, target + A[n])
 
    # Return total count
    return exclude + include
 
 
if __name__ == '__main__':
 
    # input list and target number
    A = [5, 3, -6, 2]
    target = 6
 
    print(countWays(A, len(A) - 1, target), "ways")
12944qwerty
  • 2,001
  • 1
  • 10
  • 30
Nevetha
  • 11
  • 5