DS 1 MP* Corrigé (X-ENS 2023)
Contents
DS 1 MP* Corrigé (X-ENS 2023)#
Partie I : Différentiels par positions fixes#
Question 1#
La complexité est en O(\(n\)) où \(n\) est la taille de texte1
(égal à la taille de texte2
).
def textes_égaux(texte1, texte2):
if len(texte1) != len(texte2):
return False
for i in range(len(texte1)):
if texte1[i] != texte2[i]:
return False
return True
Question 2#
La complexité est clairement O(\(n\)).
def distance(texte1, texte2):
d = 0
for i in range(len(texte1)):
if texte1[i] != texte2[i]:
d += 1
return d
Question 3#
On note \(n_1\) et \(n_2\) les tailles de texte1
et texte2
. La complexité est en O(\(n_1+n_2\)) d’après les annotations ci-dessous.
def aucun_caractère_commun(texte1, texte2):
d = {}
for c in texte1: # O(n1)
d[c] = True
for c in texte2: # O(n2)
if c in d: # O(1)
return False
return True
def tranche(arg_début, arg_avant, arg_après):
return {'début': arg_début, 'avant': arg_avant, 'après': arg_après}
def début(tr):
return tr['début']
def après(tr):
return tr['après']
def avant(tr):
return tr['avant']
def fin(tr):
return début(tr) + len(après(tr))
Question 4#
La complexité totale de la fonction ci-dessous correspond à la somme des longueurs des tranches, car texte1[i:j]
est en O(\(j-i\)). La complexité est donc bien O(\(n\)).
def différentiel(texte1, texte2):
L, i = [], 0
while True:
d = {}
while i < len(texte1) and texte1[i] == texte2[i]:
i += 1
if i == len(texte1):
return L
j = i + 1
while j < len(texte1) and texte1[j] != texte2[j]:
j += 1
L.append(tranche(i, texte1[i:j], texte2[i:j]))
i = j
return L
texte1 = list("Le grand château fort")
texte2 = list("Le petit chien a soif")
d = différentiel(texte1, texte2)
d
[{'début': 3,
'avant': ['g', 'r', 'a', 'n', 'd'],
'après': ['p', 'e', 't', 'i', 't']},
{'début': 11,
'avant': ['â', 't', 'e', 'a', 'u'],
'après': ['i', 'e', 'n', ' ', 'a']},
{'début': 17, 'avant': ['f'], 'après': ['s']},
{'début': 19, 'avant': ['r', 't'], 'après': ['i', 'f']}]
Question 5#
La concaténation ci-dessous est en O(\(n\)) (car il faut recopier la chaîne de caractères en totalité, de taille \(n\)).
Si diff
est de taille \(k\), la complexité totale est donc O(\(nk\)).
def applique(texte1, diff):
texte2 = texte1.copy()
for d in diff:
texte2[début(d):début(d) + len(avant(d))] = après(d)
return texte2
applique(texte1, d)
['L',
'e',
' ',
'p',
'e',
't',
'i',
't',
' ',
'c',
'h',
'i',
'e',
'n',
' ',
'a',
' ',
's',
'o',
'i',
'f']
Question 6#
La fonction suivante est clairement en O(\(k\)).
def inverse(diff):
diff2 = []
for d in diff:
diff2.append(tranche(début(d), après(d), avant(d)))
return diff2
inverse(d)
[{'début': 3,
'avant': ['p', 'e', 't', 'i', 't'],
'après': ['g', 'r', 'a', 'n', 'd']},
{'début': 11,
'avant': ['i', 'e', 'n', ' ', 'a'],
'après': ['â', 't', 'e', 'a', 'u']},
{'début': 17, 'avant': ['s'], 'après': ['f']},
{'début': 19, 'avant': ['i', 'f'], 'après': ['r', 't']}]
def versionne(texte):
return {'courant' : texte, 'historique' : [] }
def courant(texte_versionné):
return texte_versionné['courant']
def remplace_courant(texte_versionné, texte):
texte_versionné['courant'] = texte
def historique(texte_versionné):
return texte_versionné['historique']
Question 7#
modifie(texte_versionné, texte)
utilisedifférentiel
en O(\(n\)) et des opérations en O(\(1\)) donc est en O(\(n\)).annule(texte_versionné)
utiliseinverse
en O(\(k\)), applique en O(\(nk\)), modifie en O(\(n\)) et des opérations en O(\(1\)). Donc la complexité deannule(texte_versionné)
est en O(\(nk\)).
def modifie(texte_versionné, texte):
historique(texte_versionné).append(différentiel(courant(texte_versionné), texte))
remplace_courant(texte_versionné, texte)
def annule(texte_versionné):
if len(historique(texte_versionné)) == 0:
return
texte = applique(courant(texte_versionné), inverse(historique(texte_versionné).pop()))
remplace_courant(texte_versionné, texte)
return texte
texte_versionné = versionne(['a', 'v', 'i', 's'])
modifie(texte_versionné, ['v', 'i', 's', 'a'])
modifie(texte_versionné, ['v', 'i', 't', 'a'])
modifie(texte_versionné, ['l', 'i', 's', 'a'])
assert courant(texte_versionné) == ['l', 'i', 's', 'a']
assert historique(texte_versionné) == [
différentiel(['a', 'v', 'i', 's'], ['v', 'i', 's', 'a']),
différentiel(['v', 'i', 's', 'a'], ['v', 'i', 't', 'a']),
différentiel(['v', 'i', 't', 'a'], ['l', 'i', 's', 'a'])
]
print(annule(texte_versionné))
print(annule(texte_versionné))
print(annule(texte_versionné))
print(texte_versionné)
['v', 'i', 't', 'a']
['v', 'i', 's', 'a']
['a', 'v', 'i', 's']
{'courant': ['a', 'v', 'i', 's'], 'historique': []}
Partie II : Différentiels sur des positions variables#
def tranche(arg_début_avant, arg_avant, arg_début_après, arg_après):
return {'début_avant': arg_début_avant,
'avant': arg_avant,
'début_après': arg_début_après,
'après': arg_après}
def début_avant(tr):
return tr['début_avant']
def début_après(tr):
return tr['début_après']
def après(tr):
return tr['après']
def avant(tr):
return tr['avant']
def fin_avant(tr):
return début_avant(tr) + len(avant(tr))
def fin_après(tr):
return début_après(tr) + len(après(tr))
Question 8#
La complexité est clairement O(\(n\)) avec \(n\) le nombre de tranches.
def poids(diff):
s = 0
for d in diff:
s += len(avant(d)) + len(après(d))
return s
Question 9#
Si \(texte1[i] = texte2[j]\), il suffit de faire correspondre les \(i\) premiers caractères de texte1
avec les \(j\) premiers caractères de texte2
.
Sinon, il y a deux possibilités :
faire correspondre les \(i\) premiers caractères de
texte1
avec les \(j + 1\) premiers caractères detexte2
et ajouter \(texte2[j]\) à la fin \(texte1\) ;ou faire correspondre les \(i + 1\) premiers caractères de
texte1
avec les \(j\) premiers caractères detexte2
et supprimer \(texte1[i]\).
Question 10#
On utilise le cas de base \(M[0][j] = j\) et \(M[i][0] = i\).
def levenshtein(texte1, texte2):
n1, n2 = len(texte1), len(texte2)
M = [[0]*(n2 + 1) for _ in range(n1 + 1)]
for i in range(n1 + 1):
M[i][0] = i
for j in range(n2 + 1):
M[0][j] = j
for i in range(n1):
for j in range(n2):
if texte1[i] == texte2[j]:
M[i + 1][j + 1] = M[i][j]
else:
M[i + 1][j + 1] = 1 + min(M[i][j + 1], M[i + 1][j])
return M
levenshtein(['A','B','C','D','C','E','F'], ['U','A','B','C','C','X','Y','Z'])
[[0, 1, 2, 3, 4, 5, 6, 7, 8],
[1, 2, 1, 2, 3, 4, 5, 6, 7],
[2, 3, 2, 1, 2, 3, 4, 5, 6],
[3, 4, 3, 2, 1, 2, 3, 4, 5],
[4, 5, 4, 3, 2, 3, 4, 5, 6],
[5, 6, 5, 4, 3, 2, 3, 4, 5],
[6, 7, 6, 5, 4, 3, 4, 5, 6],
[7, 8, 7, 6, 5, 4, 5, 6, 7]]
Question 11#
On parcourt la matrice comme sur la figure 3, en commencant par la case en bas à droite. À chaque fois que l’on va en haut, une lettre est supprimée de \(texte1\) donc il faut la rajouter à avant
. De même à chaque fois que l’on va à gauche, une lettre est insérée dans \(texte2\) donc il faut la rajouter à après
. Quand deux lettres sont égales, on ajoute la tranche trouvée et on calcule la tranche suivante.
def différentiel(texte1, texte2, M):
L, i, j = [], len(texte1), len(texte2)
avant, après = [], []
while i > 0 or j > 0:
if i > 0 and j > 0 and texte1[i - 1] == texte2[j - 1]:
if len(avant) > 0 or len(après) > 0:
L.append(tranche(i, avant, j, après))
avant, après = [], []
i, j = i - 1, j - 1
else:
if i > 0 and (j == 0 or M[i][j - 1] > M[i - 1][j]):
avant = [texte1[i - 1]] + avant
i -= 1
else:
après = [texte2[j - 1]] + après
j -= 1
if len(avant) > 0 or len(après) > 0:
L.append(tranche(i, avant, j, après))
return L[::-1]
Question 12#
On parcourt diff1
et diff2
en passant à chaque itération la tranche qui se termine en premier. On parcourt donc les tranches par ordre croissant de leur indice de fin.
def conflit(diff1, diff2):
i, j = 0, 0
while i < len(diff1) and j < len(diff2):
if fin_avant(diff1[i]) <= début_avant(diff2[j]):
i += 1
elif fin_avant(diff2[j]) <= début_avant(diff1[i]):
j += 1
else:
return True
return False
texte = ['l', 'e', ' ', 'c', 'h', 'a', 't', ' ', 'a', ' ', 's', 'o', 'i', 'f']
texte1 = ['l', 'e', ' ', 'c', 'h', 'a', 't', ' ', 'a', ' ', 't', 'r', 'è', 's',
' ', 's', 'o', 'i', 'f']
texte2 = ['l', 'e', ' ', 'c', 'h', 'i', 'e', 'n', ' ', 'a', ' ', 's', 'o', 'i', 'f']
diff1 = différentiel(texte, texte1, levenshtein(texte, texte1))
print(diff1)
diff2 = différentiel(texte, texte2, levenshtein(texte, texte2))
print(diff2)
assert diff2 == [tranche(5, ['a', 't'], 5, ['i', 'e', 'n'])]
assert not conflit(diff1, diff2)
[{'début_avant': 9, 'avant': [], 'début_après': 9, 'après': [' ', 't', 'r', 'è', 's']}]
[{'début_avant': 5, 'avant': ['a', 't'], 'début_après': 5, 'après': ['i', 'e', 'n']}]
Question 13#
def fusionne(diff1, diff2):
diff = []
i, j = 0, 0
while i < len(diff1) and j < len(diff2):
if fin_avant(diff1[i]) <= début_avant(diff2[j]):
diff.append(diff1[i])
i += 1
else:
diff.append(diff2[j])
j += 1
for k in range(i, len(diff1)):
diff.append(diff1[k])
for k in range(j, len(diff2)):
diff.append(diff2[k])
return diff
def applique(texte1, diff):
texte2 = texte1
d = 0
for t in diff:
texte2 = texte2[:début_avant(t) + d] + après(t) + texte2[fin_avant(t) + d:]
d += len(après(t)) - len(avant(t))
return texte2
print(applique(texte, fusionne(diff1, diff2)))
['l', 'e', ' ', 'c', 'h', 'i', 'e', 'n', ' ', 'a', ' ', 't', 'r', 'è', 's', ' ', 's', 'o', 'i', 'f']
Question 14#
def successeurs(texte1, texte2, sommet):
x, y = sommet
succ = [(x + 1, y, 1), (x, y + 1, 1)]
if texte[sommet[0]] == texte2[sommet[1]]:
succ.append((x + 1, y + 1, 0))
return succ
Question 15#
Propriété de l’algorithme de Dijkstra : à chaque fois qu’un sommet v
est extrait de la file, dist[v]
contient la distance entre entrée
et v
dans le graphe.
dist_final
contient la distance de entrée
à chaque sommet du graphe (dist_final[v]
contient la distance de entrée
à v
). L’ensemble des clés de dist_final
est l’ensemble des sommets visités du graphe.
Question 16#
Soient \(n_1\) et \(n_2\) les tailles de texte1
et texte2
.
Chaque sommet est extrait au plus une fois de la file, donc il y a \(n_1 n_2\) itérations du while
.
Chacune de ces itérations est en O(\(\log(n_1 n_2)\)) car il y a au plus \(3n_1 n_2\) arcs donc au plus \(3n_1 n_2\) éléments dans la file et que les fonctions extraire_min
et ajoute
sont en complexité logarithmique.
Donc la complexité de l’algorithme de Dijkstra est O(\(n_1 n_2 \log(n_1 n_2)\)).
La complexité est donc moins intéressante que celle de l’algorithme de programmation dynamique (en O(\(n_1 n_2\))). Cependant, si le sommet sortie
peut être visité avant la fin de l’algorithme de Dijkstra, ce qui peut diminuer sa complexité.
Question 17#
On peut utiliser \(h((i, j)) = |i - j|\) comme heuristique. \(h\) est admissible car s’il y a une différence de taille entre les deux textes alors il faut au moins \(|i - j|\) opérations pour les rendre identiques.
levenshtein(['A', 'B', 'C'], ['B', 'X'])
[[0, 1, 2], [1, 2, 3], [2, 1, 2], [3, 2, 3]]