Il y a beaucoup de solutions jusqu'à présent, mais toutes sont de la forme générer puis filtrer. Ce qui signifie qu'ils passent potentiellement beaucoup de temps à travailler sur des chemins récursifs qui ne mènent pas à une solution.
Voici une solution qui est O(size_of_array * (number_of_sums + number_of_solutions))
. En d'autres termes, il utilise la programmation dynamique pour éviter d'énumérer des solutions possibles qui ne correspondront jamais.
Pour le plaisir de rire, je l'ai fait fonctionner avec des nombres à la fois positifs et négatifs, et j'en ai fait un itérateur. Cela fonctionnera pour Python 2.3+.
def subset_sum_iter(array, target):
sign = 1
array = sorted(array)
if target < 0:
array = reversed(array)
sign = -1
# Checkpoint A
last_index = {0: [-1]}
for i in range(len(array)):
for s in list(last_index.keys()):
new_s = s + array[i]
if 0 < (new_s - target) * sign:
pass # Cannot lead to target
elif new_s in last_index:
last_index[new_s].append(i)
else:
last_index[new_s] = [i]
# Checkpoint B
# Now yield up the answers.
def recur(new_target, max_i):
for i in last_index[new_target]:
if i == -1:
yield [] # Empty sum.
elif max_i <= i:
break # Not our solution.
else:
for answer in recur(new_target - array[i], i):
answer.append(array[i])
yield answer
for answer in recur(target, len(array)):
yield answer
Et voici un exemple de son utilisation avec un tableau et une cible où l'approche de filtrage utilisée dans d'autres solutions n'aboutirait jamais.
def is_prime(n):
for i in range(2, n):
if 0 == n % i:
return False
elif n < i * i:
return True
if n == 2:
return True
else:
return False
def primes(limit):
n = 2
while True:
if is_prime(n):
yield(n)
n = n + 1
if limit < n:
break
for answer in subset_sum_iter(primes(1000), 76000):
print(answer)
Cela imprime les 522 réponses en moins de 2 secondes. Les approches précédentes auraient de la chance de trouver des réponses dans la durée de vie actuelle de l'univers. (L'espace complet a 2^168 = 3.74144419156711e+50
combinaisons possibles à parcourir. Cela... prend un certain temps.)
Explication On m'a demandé d'expliquer le code, mais expliquer les structures de données est généralement plus révélateur. Je vais donc expliquer les structures de données.
Considérons subset_sum_iter([-2, 2, -3, 3, -5, 5, -7, 7, -11, 11], 10)
.
Au point de contrôle A, nous avons réalisé que notre cible est positive, donc sign = 1
. Et nous avons trié notre entrée de sorte que array = [-11, -7, -5, -3, -2, 2, 3, 5, 7, 11]
. Comme nous y accédons souvent par index, voici la correspondance entre les index et les valeurs :
0: -11
1: -7
2: -5
3: -3
4: -2
5: 2
6: 3
7: 5
8: 7
9: 11
Au point de contrôle B, nous avons utilisé Programmation dynamique pour générer notre last_index
structure des données. Que contient-elle ?
last_index = {
-28: [4],
-26: [3, 5],
-25: [4, 6],
-24: [5],
-23: [2, 4, 5, 6, 7],
-22: [6],
-21: [3, 4, 5, 6, 7, 8],
-20: [4, 6, 7],
-19: [3, 5, 7, 8],
-18: [1, 4, 5, 6, 7, 8],
-17: [4, 5, 6, 7, 8, 9],
-16: [2, 4, 5, 6, 7, 8],
-15: [3, 5, 6, 7, 8, 9],
-14: [3, 4, 5, 6, 7, 8, 9],
-13: [4, 5, 6, 7, 8, 9],
-12: [2, 4, 5, 6, 7, 8, 9],
-11: [0, 5, 6, 7, 8, 9],
-10: [3, 4, 5, 6, 7, 8, 9],
-9: [4, 5, 6, 7, 8, 9],
-8: [3, 5, 6, 7, 8, 9],
-7: [1, 4, 5, 6, 7, 8, 9],
-6: [5, 6, 7, 8, 9],
-5: [2, 4, 5, 6, 7, 8, 9],
-4: [6, 7, 8, 9],
-3: [3, 5, 6, 7, 8, 9],
-2: [4, 6, 7, 8, 9],
-1: [5, 7, 8, 9],
0: [-1, 5, 6, 7, 8, 9],
1: [6, 7, 8, 9],
2: [5, 6, 7, 8, 9],
3: [6, 7, 8, 9],
4: [7, 8, 9],
5: [6, 7, 8, 9],
6: [7, 8, 9],
7: [7, 8, 9],
8: [7, 8, 9],
9: [8, 9],
10: [7, 8, 9]
}
(Note secondaire, ce n'est pas symétrique car la condition if 0 < (new_s - target) * sign
nous empêche d'enregistrer quoi que ce soit dans le passé target
qui, dans notre cas, était de 10).
Qu'est-ce que cela signifie ? Eh bien, prenez l'entrée, 10: [7, 8, 9]
. Cela signifie que nous pouvons aboutir à une somme finale de 1,5 million d'euros. 10
le dernier numéro choisi étant aux indices 7, 8 ou 9. En d'autres termes, le dernier numéro choisi peut être 5, 7 ou 11.
Examinons de plus près ce qui se passe si nous choisissons l'indice 7. Cela signifie que nous terminons sur un 5. Donc, avant d'arriver à l'indice 7, nous devions arriver à 10-5 = 5. Et l'entrée pour 5 se lit comme suit , 5: [6, 7, 8, 9]
. Nous aurions donc pu choisir l'indice 6, qui est 3. Bien que nous arrivions à 5 aux indices 7, 8 et 9, nous n'y sommes pas arrivés avant l'indice 7. Donc notre avant-dernier choix doit être le 3 à l'indice 6.
Et maintenant nous devons arriver à 5-3 = 2 avant l'indice 6. L'entrée 2 se lit : 2: [5, 6, 7, 8, 9]
. Encore une fois, nous ne nous intéressons qu'à la réponse à l'indice 5
parce que les autres sont arrivés trop tard. Donc le troisième et dernier choix doit être le 2 à l'indice 5.
Et enfin, nous devons arriver à 2-2 = 0 avant l'indice 5. L'entrée 0 se lit : 0: [-1, 5, 6, 7, 8, 9]
. Encore une fois, nous ne nous intéressons qu'au -1
. Mais -1
n'est pas un indice - en fait, je l'utilise pour signaler que nous avons fini de choisir.
Nous venons donc de trouver la solution 2+3+5 = 10
. Ce qui est la toute première solution que nous imprimons.
Et maintenant nous arrivons à la recur
sous-fonction. Comme elle est définie à l'intérieur de notre fonction principale, elle peut voir last_index
.
La première chose à noter est qu'il appelle yield
pas return
. Cela en fait un générateur . Lorsque vous l'appelez, vous renvoyez une sorte spéciale de itérateur . Lorsque vous bouclez sur cet itérateur, vous obtenez une liste de toutes les choses qu'il peut produire. Mais vous les obtenez au fur et à mesure qu'il les génère. Si c'est une longue liste, vous ne la mettez pas en mémoire. (C'est assez important car nous pourrions obtenir une longue liste).
Quoi recur(new_target, max_i)
rendront sont toutes les manières que vous auriez pu résumer à new_target
en utilisant uniquement des éléments de array
avec un indice maximal max_i
. C'est ça les réponses : "Nous devons nous rendre à new_target
avant l'indice max_i+1
." C'est, bien sûr, récursif.
Par conséquent, recur(target, len(array))
c'est toutes les solutions qui atteignent la cible en utilisant n'importe quel indice. Ce qui est ce que nous voulons.
9 votes
L'article de wikipedia ( fr.wikipedia.org/wiki/Sous-ensemble_somme_problème ) mentionne même que ce problème est une bonne introduction à la classe des problèmes NP-complets.
6 votes
Peut-on utiliser plus d'une fois le même élément de l'ensemble original ? Par exemple, si l'entrée est {1,2,3,5} et la cible 10, est-ce que 5 + 5 = 10 est une solution acceptable ?
0 votes
Juste une fois. Si un nombre entier doit être répété, il apparaît comme un nouvel élément.
1 votes
stackoverflow.com/a/64380474/585411 montre comment utiliser la programmation dynamique pour éviter tout travail inutile dans la production des réponses.