TD17. Le problème de partition
Le sujet de ce TD est disponible au format pdf ici.
Pour le dernier TD BPI, nous allons nous intéresser à un problème connu comme étant "le plus simple des problèmes difficiles" (un peu de pub : nous en reparlerons dans le cours optionnel d’algorithmique avancée de seconde année).
Exercice 1 : le problème de partition
On dispose en entrée d'un multi-ensemble E
contenant n
entiers strictement positifs stockés dans un tableau.
Pour rappel, dans un multi-ensemble une même valeur peut apparaître plusieurs fois.
On souhaite partitionner E
en deux sous-multi-ensembles E1
et E2
tel que la somme des éléments de E1
soit égale à la somme des éléments de E2
.
Question 1
Trouvez une solution pour le multi-ensemble E = {19, 4, 7, 19, 16, 12, 7, 12, 8, 8}
.
Correction question 1
Cliquez ici pour révéler la correction.
Il n'y a que deux solutions pour ce multi-ensemble : E1 = {19, 19, 7, 7, 4}
/ E2 = {16, 12, 12, 8, 8}
et la partition "inverse" E2 = {19, 19, 7, 7, 4}
/ E1 = {16, 12, 12, 8, 8}
.
Question 2
Proposez une fonction récursive renvoyant le nombre de partitions valides différentes en pensant à bien définir ce que "différentes" signifie.
Correction question 2
Cliquez ici pour révéler la correction.
La notion de partitions différentes que nous utilisons se base sur la position des éléments dans le multi-ensemble e
et non sur la valeur.
Voici une première solution :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 | def compte_partitions_valides_rec_v1(entiers):
"""Renvoie le nombre de partitions valides différentes.
Valide signifie que la somme des éléments du premier
sous-multi-ensemble est égale à la somme des éléments
du deuxième sous-multi-ensemble.
La notion de partitions différentes se base ici sur la
position des éléments dans le multi-ensemble `entiers`
et non sur la valeur.
L'idée de l'implémentation est d'essayer toutes les possibilités
en plaçant chaque élément soit dans e1 soit dans e2.
"""
def _compte_partitions_valides_rec(entiers, cible):
"""Ici nous utilisons une fonction interne.
L'objectif de cette fonction interne est de cacher
les paramètres propres à l'implémentation à l'utilisateur
de la fonction `compte_partitions_valides_rec`.
`entiers` contient les éléments qu'il reste à placer.
`cible` est l'objectif de somme à atteindre pour e2.
`entiers` est modifié au cours des appels récursifs (sa taille
diminue), mais reste inchangé en sortie de cette fonction.
"""
# Si on a placé tout le monde
if not entiers:
return int(cible == 0)
# On peut s'arréter si on a pas les reins
# assez solides pour aller au bout
if sum(entiers) < cible:
return 0
# On peut aussi s'arréter si on est allé trop loin
if cible < 0:
return 0
# Sinon on va essayer de placer le dernier
# élément de entiers soit dans e1 soit dans e2.
# On prend le dernier car pop() est gratuit
# contrairement à pop(0) (si on prenait le
# premier).
dernier = entiers.pop()
# On met `dernier` dans e1 et on fait un appel récursif.
# Comme cible est la cible pour e2, on ne la change pas
# ici.
res = _compte_partitions_valides_rec(entiers, cible)
# On met `dernier` dans e2 et on fait un appel récursif
res += _compte_partitions_valides_rec(entiers, cible - dernier)
# NE PAS OUBLIER de remettre dernier dans entiers.
# Là encore, comme on rajoute à la fin, c'est
# gratuit.
entiers.append(dernier)
return res
# Si la somme est impaire, on peut s'arréter
# tout de suite.
total = sum(entiers)
if total % 2 != 0:
return 0
# Sinon, on y va !
return _compte_partitions_valides_rec(entiers, total // 2)
|
Et voici une deuxième solution :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 | def compte_partitions_valides_rec_v2(entiers):
"""Renvoie le nombre de partitions valides différentes.
Valide signifie que la somme des éléments du premier
sous-multi-ensemble est égale à la somme des éléments
du deuxième sous-multi-ensemble.
La notion de partitions différentes se base ici sur la
position des éléments dans le multi-ensemble `entiers`
et non sur la valeur.
L'idée de l'implémentation est la même que dans la version 1,
à savoir essayer toutes les possibilités en plaçant chaque
élément soit dans e1 soit dans e2.
On rajoute nénamoins une optimisation qui ne change pas la
complexité mais divise par 2 le nombre d'appels récursifs
en ne testant pas une partition et "son symétrique" mais seulement
l'une des deux. Si entiers = {1, 2, 3} alors e1 = {1}, e2 = {2, 3}
est le symétrique de e1 = {2, 3}, e2 = {1}
"""
def _compte_partitions_valides_rec(indice, cible):
"""Ici nous utilisons une fonction interne.
L'objectif de cette fonction interne est de cacher
les paramètres propres à l'implémentation à l'utilisateur
de la fonction `compte_partitions_valides_rec`.
`entiers` n'est plus passé en paramètre car on peut y accéder
directement dans cette fonction puisqu'elle est incluse
dans la fonction ayant `entiers` comme paramètre.
`indice` est l'indice dans `entiers` du prochain élément à placer.
`cible` est l'objectif de somme à atteindre pour e2.
"""
# Si on a placé tout le monde
if indice == len(entiers):
return int(cible == 0)
# On peut s'arréter si on a pas les reins
# assez solides pour aller au bout
if sum(entiers[indice:]) < cible:
return 0
# On peut aussi s'arréter si on est allé trop loin
if cible < 0:
return 0
# Sinon on va essayer de placer `e[indice]`
# soit dans e1 soit dans e2.
# On met `e[indice]` dans e1 et on fait un appel récursif.
# Comme cible est la cible pour e2, on ne la change pas
# ici.
res = _compte_partitions_valides_rec(indice + 1, cible)
# On met `e[indice]` dans e2 et on fait un appel récursif
res += _compte_partitions_valides_rec(indice + 1, cible - entiers[indice])
return res
# Si la somme est impaire, on peut s'arréter
# tout de suite.
total = sum(entiers)
if total % 2 != 0:
return 0
# Sinon, on y va !
# L'ensemble vide doit être traité séparément car
# dans ce qui suit on commence au premier élément de e
if not entiers:
return 1
# Pour ne pas tester les partitions symétriques, on
# met e[0] dans e1 et on compte deux fois chaque
# solution.
return 2 * _compte_partitions_valides_rec(1, total // 2)
|
Question 3
Proposez une fonction itérative renvoyant le nombre de partitions valides différentes.
Correction question 3
Cliquez ici pour révéler la correction.
Voici une solution itérative utilisant itertools.combinations
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 | def compte_partitions_valides_iter(entiers, affiche=False):
"""Renvoie le nombre de partitions valides différentes.
Valide signifie que la somme des éléments du premier
sous-multi-ensemble est égale à la somme des éléments
du deuxième sous-multi-ensemble.
La notion de partitions différentes se base ici sur la
position des éléments dans le multi-ensemble `entiers`
et non sur la valeur.
L'idée ici est d'utiliser le module itertools pour
récupérer toutes les combinaisons possibles de toutes
les tailles possibles pour `e2` et de vérifier si leur somme
est égale à la moitié de la somme des éléments de `entiers`.
ATTENTION dans cette solution il n'y a pas de "coupe" contrairement
aux solutions récursives car `e2` n'est pas construit progressivement.
Cette solution risque donc d'être moins intéressante quand des coupes
sont possibles.
"""
# L'ensemble vide est un cas particulier
if not entiers:
return 1
# Si la somme est impaire, on peut s'arréter
# tout de suite.
total = sum(entiers)
if total % 2 != 0:
return 0
count = 0
cible = total // 2
# Pour ne pas tester les symétriques on enlève
# le dernier et on fera x 2 au final
dernier = entiers.pop()
for multiset_2_size in range(1, len(entiers)):
combinations = itertools.combinations(entiers, multiset_2_size)
for multiset_2 in combinations:
if sum(multiset_2) == cible:
if affiche:
print(" ", multiset_2)
count += 1
entiers.append(dernier)
return 2 * count
|
Exercice 2 : n-reines (pour aller plus loin)
On considère un échiquier de taille n x n
.
On rappelle qu’une reine aux échecs menace toutes les cases situées sur les mêmes lignes, colonnes ou diagonales qu'elle.
On souhaite placer n
reines sur l'échiquier de taille n x n
sans qu'elles se menacent entre elles.
Question 1
Proposez une fonction récursive calculant le nombre de placement différents des n
reines répondant à la contrainte ci-dessus.
Correction question 1
Cliquez ici pour révéler la correction.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 | #!/usr/bin/env python3
"""Le problème des n reines."""
import time
def conflict(queen1, queen2):
"""Return if the two given queens are in conflict."""
if queen1[0] == queen2[0]: # on the same line (should never happen in our case)
assert False, "two queens on the same line !"
# return True
elif queen1[1] == queen2[1]: # on the same column
return True
elif abs(queen1[0] - queen2[0]) == abs(
queen1[1] - queen2[1]
): # on the same diagonal
return True
return False
def has_conflict(queens):
"""Return if the last queen of queens is in conflict with at least one other queen."""
last_queen = queens[-1]
for queen in queens[:-1]:
if conflict(last_queen, queen):
return True
return False
def nb_valid_configs_params(size):
"""Return the number of valid configurations with 'size' queens for a boardgame sizexsize."""
def nb_valid_configs_params_rec(queens, size):
"""Internal recursive function to compute the number of valid configurations
- queens is a list of queens already on the boardgame.
A queen is a tuple (line_no, col_no).
queens contains a single queen per line, since it's useless to try to put more
and because we add queens line by line, queens[i][0] == i is *always* True
- size is the size of one side of the boardgame
"""
# Stop the recursion if we sucessfully added a queen on every line
if len(queens) == size:
return 1
# Else we make a recursive call for each possible column on the current line
count = 0
current_line = len(queens)
for column in range(size):
queens.append((current_line, column))
# Stop the recursion if at least two queens are in conflict
# and return 0 meaning this branch of the recursion tree has 0
# valid configurations
if not has_conflict(queens):
count += nb_valid_configs_params_rec(queens, size)
queens.pop()
return count
queens = [] # We start with an empty boardgame
return nb_valid_configs_params_rec(queens, size)
def test_functions():
"""Test all functions above"""
sizes = range(1, 12)
for size in sizes:
start = time.process_time_ns()
nb_valid_configs = nb_valid_configs_params(size)
end = time.process_time_ns()
size_s = f"{size}x{size}"
print(
f"size {size_s:5} --> {nb_valid_configs:4d} "
f"configs in {(end - start) * 1E-9:.3f} seconds"
)
if __name__ == "__main__":
test_functions()
|