Minimum amount of spaces inserted in a given string, such that it gets composed only of elements from a given list
Firstly, great job on creating a solution that works. That is often the first step in creating an optimal solution. 

But as you may have noticed, making combinations and permutations based on the number of elements in small_strings will start to scale incredibly as you process longer and longer small_strings arrays. 

Mathematically, your solution is concise and straight-forward-- kudos again to that. The approach I will suggest is based on Dynamic Programming and preprocessing. This is to speed up scenarios where you have a very large (>100) number of small_strings. 



This approach will use the fact that we can solve the whole problem in the same manner of solving a subset of the problem. Specifically, if we can construct the big_string piece by piece, then we can simply add the result of the part we have constructed and add/recurse it on the rest of the string. 

The first idea to consider is building a trie for the strings in small_strings. Also known as a prefix tree, a trie will help us figure out which strings we can use to 'construct' the big_string. I highly recommend understanding the concept of a trie for further insight into this problem.

Secondly, we want to implement a function that explores the problem space in a way that is efficient and breaks on paths that yield no results. This is typical of many DP (Dynamic Programming) problems and often looks like recursion in practice. Knowing when and how to implement this comes with practice. I also highly recommend practicing some classical DP problems if you want to become a skilled developer.

With those points in mind, here's a solution:

def buildTrie(words):
    trie = {}
    for word in words:
        current = trie
        for c in word:
            current = current.setdefault(c, {})
        current['isWord'] = word

    return trie


def findPrefixes(trie, word):
    results = {}
    current = trie

    for length, l in enumerate(word, start=1):
        if l in current:
            current = current[l]
        else:
            break
        if current.get('isWord'):
            results[current['isWord']] = length

    return results


def find_num_spaces(big_string, small_strings):
    trie = buildTrie(small_strings)
    N = len(big_string)

    def helper(index=0):
        nonlocal trie, N

        if index == N:
            return 0

        prefixes = findPrefixes(trie, big_string[index:])

        if prefixes:
            return min(1 + helper(index + length) for length in prefixes.values())
        else:
            return float('inf')

    return helper() - 1


big_string = '3141592653589793238462643383279'
small_strings = ['3', '314', '49', '9001', '15926535897', '14', '9323', '8462643383279', '4', '793', '3141592653589793238462643383278', '314159265358979', '3238462643383279']

assert find_num_spaces(big_string, small_strings) == 1

big_string = '3141592653589793238462643383279'
small_strings = ['3', '314', '49', '9001', '15926535897', '14', '9323', '8462643383279', '4', '793', '3141592653589793238462643383278', '3141592', '65358979323']

assert find_num_spaces(big_string, small_strings) == 2


Since we do our preprocessing in the form of a trie, we know how to efficiently reconstruct our big_string. The general idea here is that we are trading space for time complexity. Storing our strings allows for efficient exploration of the problem space. 

Note: I assumed you only needed the number of spaces required-- requiring the specific strings involved would tweak this solution slightly. There would be an additional variable that would track the working solution through the recursion.