Mémoïsation
1. Mots de taille n
1.1. Étude théorique du problème
- C’est un problème de dénombrement.
On génère toutes les écritures binaires sur \(n\) bits (\(2^{n}\) écritures possibles) et pour chaque écriture on cherche s’il y a deux 0 consécutifs en \(\O(n)\). On incrémente un compteur à chaque fois qu’il n’y en a pas.
Voici un code possible.
def deuxzeros(ecriture) : # $\O(n)$ for i in range(len(ecriture)-1) : if ecriture[i] == 0 and ecriture[i+1] == 0 : return True return False def E(n) : if n == 1 : return [[]] E_prec = E(n-1) return [ x + [1] for x in E_prec ] + [ x + [0] for x in E_prec ] def P(n) : p = 0 for ecriture in E(n) : if not deuxzeros(ecriture) : p += 1 return p
La complexité de
Epourrait être améliorée mais dans tous les cas la complexité finale est donc en \(\mathrm O(n\,2^n)\).\(P(0) = 1\), \(P(1) = 2\), \(P(2) = 3\) et \(P(n) = P(n-1) + P(n-2)\), il s’agit de la suite de fibonacci décalée d’un terme. En effet on peut séparer les écritures sur \(n+2\) bits en 3 groupes :
Les écritures qui ont 1 comme bit de poids fort.
Dans ce groupe, les écritures ont deux 0 consécutifs, ssi l’écriture sur \(n-1\) bits obtenu en sortant le 1 ont deux 0 consécutifs. L’ensemble des entiers de \(E_{n}\) de ce groupe est exactement l’ensemble \(\{ 2^{n-1} + k, k \in E_{n-1}\}\).
Les écritures qui ont 01 comme bits de poids fort.
Dans ce groupe, les écritures ont deux 0 consécutifs, ssi l’écriture sur \(n-2\) bits obtenu en sortant le 01 ont deux 0 consécutifs. L’ensemble des entiers de \(E_{n}\) de ce groupe est exactement l’ensemble \(\{ 2^{n-2} + k, k \in E_{n-2}\}\).
Les écritures qui ont 00 comme bits de poids fort.
Dans ce groupe, les écritures ont toutes deux 0 consécutifs.
En conclusion, \(E_n = \{ 2^{n-1} + k, k \in E_{n-1}\} \uplus \{ 2^{n-2} + k, k \in E_{n-2}\}\) avec \(\uplus\) l’union disjointe. Ce qui signifie bien que \(P(n) = P(n-1) + P(n-2)\).
1.2. Implémentation de solution
def nb_c_memo(i, memo) : if i in memo : return memo[i] if i == 0 : memo[i] = 1 return memo[i] if i == 1 : memo[i] = 2 return memo[i] a = nb_c_memo(i-1, memo) b = nb_c_memo(i-2, memo) memo[i] = a + b return memo[i] def nb_c(n) : return nb_c_memo(n,{})
2. Somme de sous-ensembles
2.1. Étude théorique du problème
- Problème de décision.
On génère tous les sous-ensembles de
l, on calcule leur somme et on renvoieTruedès qu’on trouve un sous-ensemble dont la somme estk. Si on en trouve pas on renvoieFalse. On peut construire les sous-ensembles del[:i+1]à partir de ceux del[:i]en les dupliquant puis en ajoutantl[i]à l’un des deux seulement. Puis pour chaque sous-ensemble on peut calculer sa somme en$\O$(len(l))et faire une recherche linéaire d’un ensemble dont la somme vautkdans l’ensemble des sous-ensembles del.Voici un code possible.
def somme_egale(sousliste, k) : # $\O(n)$ s = 0 for x in sousliste : s += x return s == k def souslistes(l,i) : # Calcul les sous listes de l[:i] if i == 0 : return [[]] E_prec = souslistes(i-1) return [ e + [l[i-1]] for e in E_prec ] + [ e for e in E_prec ] def P(k,l) : for sousliste in souslistes(l,len(l)) : if somme_egale(sousliste, k) : return True return False
La génération des sous-listes peut-être rendue plus efficace, mais dans tous les cas la complexité est de \(\O(n\,2^{n})\).
P(0,l,i) = True, la liste vide marche. Pour \(k < 0\),P(k,l,i) = False, car les entiers delsont positifs. EnfinP(k,l,0) = Falsesi \(k > 0\) car le seul sous-ensemble del[:0]est la liste vide de somme nulle.P(k,l,i+1) = P(k,l,i) or P(k-l[i],l,i). La disjonction consiste aux deux choix possible pour la construction de chaque sous-ensembles del[:i+1], comme effectué dans la recherche exhaustive :l[i]est dans le sous ensemble, on se ramène àP(k-l[i],l,i+1),l[i]n’est pas dans le sous-ensemble, on se ramène àP(k,l,i).
2.2. Implémentation Descendante
def subset_sum_desc_rec(k,l,i) : if k == 0 : return True if i == 0 or k < 0 : return False return ( subset_sum_desc_rec(k,l,i-1) or subset_sum_desc_rec(k-l[i-1],l,i-1) ) def subset_sum_desc(k,l) : return subset_sum_desc_rec(k,l,len(l))
2.3. Implémentation Descendante avec Mémoïsation par Dictionnaire
On mémoïse uniquement les entrées (k,i), l n’est pas variable.
def subset_sum_dict_rec(k,l,i,memo) : if (k,i) in memo : return memo[(k,i)] if k == 0 : memo[(k,i)] = True return memo[(k,i)] if i == 0 or k < 0 : memo[(k,i)] = False return memo[(k,i)] memo[(k,i)] = ( subset_sum_dict_rec(k,l,i-1,memo) or subset_sum_dict_rec(k-l[i-1],l,i-1,memo) ) return memo[(k,i)] def subset_sum_dict(k,l) : return subset_sum_dict_rec(k,l,len(l),{})
2.4. Implémentation Montante
Dans les sous-problèmes utilisés, la valeur de k diminue tout le temps. Il est
difficile de savoir exactement les quels en fonction de l. Si on ne mémoïse
pas les valeurs de k négatives, ce qui n’est pas gênant vu que c’est un cas de
base qui se calcule en \(\O(1)\), on peut se contenter des sous-problèmes
P(s,l,i) avec 0 <= s <= k et 0 <= i <= len(l).
Une matrice (len(l)+1) $\times$ (k+1) suffira pour mémoïser toutes les valeurs.
import numpy as np def subset_sum_mont(k,l) : sp = np.full((len(l)+1,k+1), True) for i in range(1,len(l)+1) : for s in range(l[i-1]) : sp[i][s] = sp[i-1][s] for s in range(l[i-1],k+1) : sp[i][s] = sp[i-1][s] or sp[i-1][s-l[i-1]] return sp[len(l)][k]
2.5. Implémentation Descendante avec Mémoïsation par Matrices
En reprenant le raisonnement précédant, une matrice (len(l)+1) $\times$ (k+1)
suffira pour mémoïser toutes les valeurs.
Les matrices numpy doivent toujours contenirs le même type d’objets. On ne peut
pas avoir à la fois None, True et False. On peut faire une matrice par
liste de liste à la place.
Je présente ici une manière de s’en sortir avec trois valeurs entière. -1 pour
None, 0 pour True et 1 pour False. Le or se simule avec une
multiplication.
def subset_sum_mat_rec(k,l,i,memo) : if memo[k][i] < 0 : return memo[k][i] if k == 0 : memo[k][i] = 0 return memo[k][i] if i == 0 or k < 0 : memo[k][i] = 1 return memo[k][i] memo[k][i] = ( subset_sum_dict_rec(k,l,i-1,memo) * subset_sum_dict_rec(k-l[i-1],l,i-1,memo) ) return memo[k][i] def subset_sum_mat(k,l) : memo = np.full((k+1,len(l)+1),-1) return subset_sum_mat_rec(k,l,len(l),memo) == 0
3. Analyse des performances
3.1. Génération de jeux de test
import random def rand_subset_sum(n, subset_sum) : alea = [ random.randint(0,99) for i in range(n) ] return subset_sum(100 * n, alea)
3.2. Temps d’exécution moyen
import time def timing_subset_sum(n, subset_sum) : s = 0 for i in range(20) : t = time.process_time() rand_subset_sum(n, subset_sum) s += time.process_time() - t return s / 20
3.3. Tracé
import matplotlib.pyplot as plt x = [ i for i in range(100) ] y_dict = [ timing_subset_sum(i,subset_sum_dict) for i in x ]
y_mat = [ timing_subset_sum(i,subset_sum_mat) for i in x ]
y_mont = [ timing_subset_sum(i,subset_sum_mont) for i in x ]
y_desc = [ timing_subset_sum(i,subset_sum_desc) for i in x[:23] ]
plt.plot(x,y_dict, label="Memo Dict") plt.plot(x,y_mat, label="Memo Mat") plt.plot(x,y_mont, label="Montante") plt.plot(x[:23],y_desc, label="Descendante") plt.legend() plt.savefig("comparaison.png")
Figure 1 : Résultat de la comparaison