Algorithmie avancée
- Sites : bdupont.up8.site et jj.up8.site
- Mails : benjamin.dupont02(at)univ-paris8.fr et jj(at)up8.edu
- Mercredi 9/11 -> Choix des projets (monomes, pas 2 personnes qui font le même projet)
Notation
- Note final basé sur un projet individuel
- Projet en lui-même
- Rapport du projet
- Soutenance sur la semaine 10
- TPs pas noté
- Peut-être le premier, mais pas sûr
- Rendre le premier TP par mail, les autres c’est pas sûr qu’ils seront à rendre
- Rendre le TP sur les droites discrètes pour le 19/10
Cours
Exemple d’implantation de ces algorithmes ici f(n) = \Theta(n^{2}) si f\frac{n}{n^{2}} est bornée Taille n : 1+2+\dots+(n-1)=\frac{n(n+1)}{2} n base10 => binaire n sur k bits Méthode de récursivité lourde \approx \Theta(2^{n}) Soit C_{n} k nombre d’opérations pour calculer F(n) \begin{pmatrix} 1 & 1 \\\ 1 & 1 \end{pmatrix} avec \begin{pmatrix} 1 & 2 \\\ 3 & 4 \end{pmatrix} donne : \begin{pmatrix} 3 & 3 \\\ 7 & 7 \end{pmatrix} La matrice est cachée dans ce système : Donnant : a^{n} puissance rapide a^{n} A^{n} puissance rapide Triangle de Pascal (avec coefficient binomiaux) :
Tris
\Theta(n^{2}))
Complexité quadratique (
Tri à bulle
\sum\limits_{k=1}^{n}k=\frac{n(n+1)}{2}=(n-1)+(n-2)+\dots+1
Tri par insertion
\Theta(nlog(n))
Complexité
Tri rapide et tri fusion
_ _ _ _ _ _ \frac{n}{2}
_ _ _ _ _ 0 \frac{n}{4} (ou 1 à la place de 0)
2^{k-1} \leq n \leq 2^{k}
\begin{aligned}a^{b} =& e^{ln(a)b} \\\ &e^{(h-1)ln(2)} \leq n \leq e^{ln(2)k} \\\ &(k-1)ln(2) \leq ln(n) \leq ln(2)k \\\ &(k-1) < \frac{ln(n)}{ln(2)} \leq k \end{aligned}
Suite de Fibonacci
C_{n} = (1 +) C_{n-1} + C_{n-2}, la complexité c’est grosso-modo Fibonacci + 1, donc
C_{n} \simeq F(n)
Donc, F(n) = \frac{1}{\sqrt{5}}((\frac{1+\sqrt{5}}{2})^{n} + (\frac{1-\sqrt{5}}{2})^{n}) : ça fonctionne jusqu’à une certaines valeurs et après ça ne fonctionne plus.
Calcul matricielle
\left[\begin{array}{ll} F(n) = F(n-1) + F(n-2) \\\ F(n-1) = F(n-1) \end{array}\right.
\begin{array}{ll}
\quad\quad F(n) = \\\
F(n-1) =
\end{array}
\begin{pmatrix} 1 & 1 \\\ 1 & 0 \end{pmatrix}
\text{ avec }
\begin{pmatrix} F(n-1) \\\ F(n-2) \end{pmatrix}
\text{ donne }
\begin{pmatrix} F(n-1) & F(n-2) \\\ F(n-1) \end{pmatrix}
Exponentiation rapide
si (n == 1)
retourne a
sinon
si n % 2 == 0
x = puissance_rapide(a, n / 2)
retourne x * x
sinon
x = puissance_rapide(a, n / 2)
retourne a * x * x
si (n == 1)
retourne A
sinon
si n % 2 == 0
x = puissance_rapide(A, n / 2)
retourne multiplication(x, x)
sinon
x = puissance_rapide(a, n / 2)
retourne a * x * x
\begin{pmatrix} n \\\ k \end{pmatrix} = \begin{pmatrix} n - 1 \\\ k - 1 \end{pmatrix} + \begin{pmatrix} n - 1 \\\ k \end{pmatrix}
n^{k} | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 1 | ||||
2 | 1 | 1 | |||
3 | 1 | 2 | 1 | ||
4 | 1 | 3 | 3 | 1 | |
5 | 1 | 4 | 6 | 4 | 1 |
x_p | 10 |
---|---|
y_{p} | 12 |
x_{q} | 12 |
y_{q} | 36 |
x | 10 11 |
y | 12 34 |
De l’évidence à la référence
|y-y_{n}| > |y+1 - y_{2}| \Rightarrow y++
y_{2} = \frac{x\times dy}{dx}
\underbrace{\left| y - \frac{x\times dy}{dx} \right|}_A > \underbrace{\left| y+1 - \frac{x\times dy}{dx} \right|}_B \Rightarrow y++
On a donc A > B \Rightarrow 0 > A+B
Il y ça aussi :
A + B < 0 \Rightarrow y++
2y + 1 - \frac{2x\times dy}{dx} < 0 \Rightarrow y++
\underbrace{2y\times dx + dx - 2x \times dx}_{\text{delta(x, y)}} < 0 \Rightarrow y++
Horizontal | Oblique |
---|---|
x++, y |
x++, y++ |
delta(x+1, y) = delta(x, y) - 2dy
delta(x+1, y+1) = delta(x, y) - 2dy - 2dx
En C :
void droitebr(int dx, int dy) {
int delta, incD, incH;
incH = dy << 1;
delta = incH - dx;
incD = delta - dx;
for (int x = 0, y = 0; x <= dx; ++x) {
affiche(x, y);
if (delta > 0) {
y++;
delta += incD;
} else {
delta = incH;
}
}
}
Améliorations
Optimisation de l’algorithme en coupant en 2 le problème, et donc en faisant un pas de 2 dans la boucle-for. Pareil avec un pas de 3 et un pas de 4.
- Rappel pour la droitebr :
\begin{cases} \text{delta}(x, y) = 2xdy - 2ydx - dx \\\\ \text{si delta} >= 0 \text{ alors } y++ \end{cases}
Alors pour le pas de 2 :
\text{delta}(x+2, y) = 2xdy + 2ydx + 4dy - dx
delta(x+1, y) = delta(x + 2, y) - 2dy
if (delta >= 0) {
if (delta - 2dy >= 0) {
Pour le pas de 3, c’est toujours la même équation, on adapte juste l’équation avec un x+3, par contre il peut pas avoir 2 partie oblique consécutives (conséquence de propriété, cf. plus bas dans le cours).
On fait *4 pour y parce que on a un pas de 2 pour x, donc x croit 2 fois plus vite, donc pour y il faut *2 aussi, donc *4
Il y a ensuite le pas adaptaif, le prof explique ça comme quand on monte une pente, on fait des plus grands pas quand on monte, bah là c’est pareil mais à l’envers. Dans notre cas de la droite, on regarde si la pente est importante, dans ce cas on fait des petits pas, et si la pente est petite genre plate, on fait des grands pas.
PGCD
Angel et Morrison disent qu’ils peuvent tout simplifier et aller plus vite.
Avec y = \frac{x \times dy}{dx}, ils se demandent si ils pourrait pas simplifier en calculant le PGCD, par exemple si y = \frac{22}{6} = \frac{11}{3}.
Malheureusement 60% du temps on a un PGCD qui vaut 1, mais 40% du temps le PGCD c’est 10.
Chercher ailleurs et autrement
“Sous les pavés”
Citation de Wu
Sur 8 mouvements possible, seulement 2 sont utilisés
There are at most two basic directions and these ones can differ only by unity, modulo eight.
Sur ces 2 mouvements, l’un apparaît toujours de façon solitaire, l’autre apparaît de façon groupé (le prof appelle ça une plage)
One of these values always occurs “singly”.
Le pas solitaire est aussi bien répartie que possible, c’est à dire qu’ils peuvent pas être tous regroupés au même endroit, ils doivent être dispersés (autrement dit : les plages ont toutes la même longueur à 1 près)
Successive occurrences of the principal direction occurring singly are as uniformly spaced as possible.
Euclide et applications
Dulucq et Bourdin
Droite | Valeure |
---|---|
Horizontale | 0 |
Oblique | 1 |
Donc par exemple :
-
|w| = dx
-
|w|_{1} = dy
-
|w|_{0} = dx-dy
-
Nombre de plages = dy
-
Longue plages = \begin{cases} \frac{dx-dy}{dy} \\\\ \frac{dx-dy}{dy} + 1 \end{cases}
-
Seulement des plages courtes : dyx \frac{dx-dy}{dy} \Rightarrow 0 utilisés
Il en reste dx-dy - dyx \frac{dx-dy}{dy}, qui est le nombre de plages longues -
\frac{dx-dy}{dy} \texttt{-=} \frac{dx}{dy}, le nombre de plages courtes est \frac{dx-dy}{dy}
-
-
\varphi_{n}(0) = O^{n+1}1
-
\varphi_{n}(1) = O^{n}1
-
w(dxdy)=\varphi(w(p-q))
-
n = \frac{dx-dy}{dy} ; p = dy ; q = dy - \frac{dx}{dy}
-
\varphi_{m}(O) = 0\ 1^{m}
-
\varphi_{m}(1) = 0\ 1^{m+1}
-
w(x+dy) = \Psi_{n}(w(p, q))
-
m = \frac{dy}{dx+dy} ; p = dx-dy ; q = dy-dy \mod (dx-dy)
Green et Pitteway
droite (dx, dy) {
dx -= dy;
s = "0";
t = "1";
while (dx != dy) {
if (dx > dy) {
dx -= dy;
t = s . t;
}
else {
dy -= dx;
s = s . t;
}
}
return ( s . t ) ^ dx;
}
Berstel
W = 00100010001
W' = 01000100010
W'' = 10010001000
C’est conjugés
- \exists f ; w'f = fw
- \exists g ; w'g = gw'
- |f| = l
- dy \times l + \frac{dx}{2} \equiv 0 (dx)
- 3\Delta l + \frac{11}{2} mod 11 = 0
- (3l + 5) mod 11 = 0
- l peut être égale à 2, -9, etc… le plus intéressant étant le plus petit, soit 2
Fraction continue
\frac{3}{11}= [0, 3, 1, 2], ça peut s’écrire aussi comme ça : a+\frac{1}{b+\frac{1}{c+\frac{1}{d+\frac{1}{e+\frac{1}{f+\frac{1}{g+\frac{1}{h+\frac{1}{i+\frac{1}{j+\frac{1}{k+\frac{1}{\dots}}}}}}}}}}},
Soit [a, b, c, d, e, f, g, h, i, j, k]
-
\frac{dx}{dy}\Rightarrow 0
-
\frac{v}{u} = \frac{1}{\frac{u}{v}} = \frac{1}{\frac{u}{v} + \frac{\frac{u}{v}}{v}}
-
w_{0} = 0
-
w_{1} = O^{u_{i-1}}1
-
w_{2} = w^{u}\_{i-1} w_{i-2}, i impair
-
w_{3} = w_{i-2} \times w^{u}_{i-1}, i pair
-
w_{0} = 0
-
w_{1} = 001
-
w_{2} = 0001
-
w_{3} = 00010001001
Plages (Spans)
- w_{0} = 0
- w_{dx} = 1
- w_{i} = w_{dx}\dots
\Rightarrow Symétrie interne
- w_{i}(u, v) = w_{v}(u, u-v) sauf pour w_{0} et w_{n} (le premier et le dernier)
2dy > dx
Récapitulation et algorithme rapide
- PGCD
- Pas de 2
- Symétrie interne
- Plage
- Partie basse de l’octant
\approx 20x \text{ bresenham}
Sommets | s_{1} | s_{2} | \dots | s_{n} |
---|---|---|---|---|
Distance de src |
d_1 | d_2 | \dots | d_{n} |
Avec d_{i} la distance de src
au sommet s_{i} la plus petite
Initialisation
On crée un tableau de taille n qu’on remplit avec :
- INF/9999 pour les sommets différents de
src
- 0 pour
src
Traitement
Commençons par traiter le sommet src
et tous ses arcs sortants
On va regarder toutes les arêtes sortantes de a, dans un ordre quelconque
- Arc de 0 vers 2, longueur 18, qui est mieux que la valeur précédente INF dans dist[2]
\Rightarrow On change la valeur dist[2] en 18. - Arc de 0 vers 4, longueur 3
\Rightarrow On change la valeur dist[4] en 3.
Sommets | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Distance de 0 | 0 | INF | 18 | INF | 3 |
- On va ensuite choisir un autre sommet
- Peut on prendre n’importe lequel des deux rencontrés ? NON!!
- \Rightarrow L’algorithme de Dijkstra impose de choisir celui qui est situé à la plus petite distance \rightarrow ici 4 !
- On traite donc 4 :
- L’arc 4 \rightarrow 3 de longueur 2 permet de mettre 5 = dist[4] + 2 dans dist[3]
- L’arc 4 \rightarrow 1 de longueur 10 permet de mettre 13 = dist[4] + 10 dans dist[1]
À ce stade :
Sommets | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Distance de 0 | 0 | 13 | 18 | 5 | 3 |
- On recomence avec le sommet non traité qui a la plus petite distance : 3
- L’arc 3 \rightarrow 1 de longueur 1 permet de découvrir un chemin de longueur 6 = \textbf{dist[3] + 1} de 0 vers 1, qui est meilleur que le 13 précédent \Rightarrow on change donc dist[1] en 6
Sommets | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Distance de 0 | 0 | 6 | 18 | 5 | 3 |
- On traite ensuite 1 :
- L’arc 1 \rightarrow 0 permet de découvrir un chemin de longueur 14 de a vers a
\Rightarrow Pas meilleur que le 0 précédent, donc on ne change rien - L’arc 1 \rightarrow 2 permet de découvrir un chemin de longueur 10 de a vers c, mieux que le 18 précédent
- L’arc 1 \rightarrow 0 permet de découvrir un chemin de longueur 14 de a vers a
- On traite ensuite 2 :
- Pas d’arc sortant \Rightarrow aucune modification
- On a alors traité tous les sommets, l’algorithme s’arrête et notre résultat est :
Sommets | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Distance de 0 | 0 | 6 | 10 | 5 | 3 |
Commentaires
Pour savoir quels sommets ont étés parcourus ou non, on va initialiser un tableau de booléens (comme dit précédemment) contenant des 0, et on change la valeur à 1 lorsque le sommet est traité. Avant de traiter un nouveau sommet, il faut donc bien vérifier qu’il est toujours à 0 dans ce tableau.
- Comment sélectionner le sommet à la plus petite distance pour le traiter juste après ? Il y a plusieurs possibilités :
- On peut écrire une fonction
minDistance
qui parcourt les sommets adjacents et sélectionne celui de plus petit poids
- On peut écrire une fonction
Exemple
#include <stdlib.h>
#define INF 9999
// S'il manque des trucs dans le code, c'est dans les exemples plus bas.
/* Fonction permettant de trouver le sommet à plus petite distance
parmis les sommets pas encore parcourus, sans utiliser de pile de priorité */
int minDistance(int dist[], int access[]) {
int min = INF, min_index;
int i;
for (int i = 0; i < MAX_NOEUD; i++)
if (access[i] == 0 && dist[i] <= min)
min = dist[i], min_index = i;
return min_index;
}
// TODO: Faire une fonction poour récupérer le poids? idk
/* Algorithme de Dijkstra */
void dijkstra(graphe *g, int src) {
int n = g->vertices;
int *dist = malloc(n * sizeof(int));
int *acces = malloc(n * sizeof(int));
int i, j, count;
for (i = 0; i < n; i++) {
dist[i] = INF;
acces[i] = 0;
}
dist[src] = 0;
for (count = 0; count < n - 1; count++) {
int u = minDistance(dist, acces);
acces[u] = 1;
for (j = 0; j < n; j++) {
if ((acces[j] == 0) && poids(/* u->j */) != 0 && dist[u] != INF &&
dist[u] + poids(/* u->j */) < dist[j]) {
dist[j] = dist[u] + poids(/* u->j */);
}
}
}
}
- Sinon, il faut stocker les sommets adjacents dans une structure adaptée appellée file de priorité.
C’est une file dans laquelleles tâches de plus petites valeur enfiler -> 16 15 12 5 -> défiler
Implémentation possibles :
-
Un tableau
- Enfiler : Rajouter à la fin du tableau
- Défiler : Parcourir toutes les tâches jusqu’à trouver le minimum.
-
Un tableau trié
- Défiler : Retirer l’élément au début du tableau qui est le minimum
- Enfiler : Il faut trouver où insérer l’élément à la bonne place
-
(Mieux) Un tas binaire
C’est un arbre binaire presque complet (chaque niveau est rempli complètement sauf éventuellement le dernier) dans lequel chaque tâche est plus prioritaire que ses enfants
-> La tâche la plus prioritaire (le minimum est la racine)
- Enfiler : On insère où il y a une place de libre, en veillant à garder la propriété de tas linéaire
- si la tâche est moins prioritaire, on laisse
- sinon, on échange avec le parent et on répète tant qu’il le faut
- Défiler :
- On enlève l’évènement la plus prioritaire qui est à la racine
- On place le dernier élément en bas à droite à la racine et on compose : \begin{cases} \text{racine} \\\\ \text{racine}(\text{fils}-\text{g}) \\\\ \text{racine}(\text{fils}-\text{d})\end{cases}
Et on met le plus petit en haut
Ces 2 opérations sont en \Theta(log(n)) : elles se font sur la hauteur d’une branche
Matrices d’adjacences
On peut encoder tout un graphe (orienté ou non, pondéré ou non), par une matrice de taille n\times n appelée matrice d’adjacence.
M[i][j]
:= \begin{cases} 1 \text{ s'il y a une arête de i vers j} \\\\ 0 \text{ sinon}\end{cases}
Remarque: il y a autant de
1
que de nombre d’arêtes dans le graphe.
On peut aussi définir une matrice d’adjacence pour un graphe non-orienté :
- Dans ce cas, s’il y a une arête de
i
versj
, il y en a aussi une dej
versi
\RightarrowM[i][j] = M[j][i]
(la matrice d’ordonnance est symétrique)
Remarque: il y a 2x le nombre d’arêtes que de
1
.
- Idem pour un graphe pondéré :
\RightarrowM[i][j]
= \begin{cases} \text{poids(i}\rightarrow\text{j) s'il y a une arête de i vers j} \\\\ 0 \text{ sinon}\end{cases}
Remarque: nombre de coefficients non nuls vaut le nombre d’arêtes
#define MAX_NOEUD 15
typedef struct graphe {
int vertices;
int adj[MAX_NOEUD][MAX_NOEUD];
} graphe;
/* Fonction qui crée un graphe vide (sans arêtes) à n sommets. */
graphe *newgraph(int n) {
graphe *grph;
grph = malloc(sizeof(*grph));
if (grph == NULL) {
fprintf(stderr, "Erreur : Probleme creation Graphe.\n");
exit(EXIT_FAILURE);
}
grph->vertices = n;
int i, j, w;
float v;
srand(time(NULL));
for (i = 0; i < MAX_NOEUD; i++) {
for (j = 0; j < MAX_NOEUD; j++) {
grph->adj[i][j] = 0;
}
}
return grph;
}
/* Affiche le nombre de sommets et la matrice d'adjacence */
void display(graphe *grph) {
printf("Nombre de sommets : %d. \n", grph->vertices);
printf("Matrice d'adjacence : \n");
int i, j;
for (i = 0; i < MAX_NOEUD; i++) {
for (j = 0; j < MAX_NOEUD; j++) {
printf("%d", grph->adj[i][j]);
printf(j < MAX_NOEUD - 1 ? "\t" : "\n");
}
}
}
/* Ajoute une arête d'un sommet i vers un sommet j (avec poids) */
void add_edge_weight(graphe *grph, int i, int j, int weight) {
grph->adj[i][j] += weight;
}
/* Ajoute une arête d'un sommet i vers un sommet j (sans poids) */
void add_edge(graphe *grph, int i, int j) { grph->adj[i][j] += 1; }
/* Supprime une arête (avec ou sans poids) de i vers j */
void remove_edge(graphe *grph, int i, int j) {
if (grph->adj[i][j] == 0) {
printf("Il n'y a pas d'arête à supprimer. ");
} else {
grph->adj[i][j] = 0;
printf("Arête de %d vers %d supprimée. \n", i, j);
}
}
Propriété (matrice adjacence)
Si G est un graphe (non) orienté de matrice d’adjacence A, le nombre de chemins de longueur n d’un sommet i vers un sommet j est égal au coefficient (i, j) de la matrice A^{n}
Ceci fournit un autre algo de parcours/plus court chemin…
- On calcule toutes les puissances successives A^{k} (k \geq 1) de A par exponentiation rapide, et on arrête si on trouve un entier n tel que \begin{cases} A^{n}(i, j) \neq 0 \\\\ A^{n}(i, j) = 0 \quad \forall \ k < n \end{cases}
Listes d’adjacences
Un graphe à n sommets peut aussi être représenté par un tableau de n listes contenant chacune les successeurs en question.
En C, on va représenter ces listes par des listes chaînées
Sachant qu’on veut aussi accéder au poids des arêtes, on va encoder un noeud d’une liste d’adjacence par :
typedef struct nodeAdjList {
int target;
int weight;
struct nodeAdjList *next;
} nodeAdjList;
typedef struct AdjList {
struct nodeAdjList *head;
} AdjList;
On peut ensuite représenter un graphe comme suit :
typedef struct graphe {
int vertices;
struct AdjList *array;
} graphe;
\Rightarrow Encodant le nombre de sommets et un tableau de listes d’adjacences
/* Fonction qui crée un graphe vide (sans arêtes) à n sommets. */
graphe *newgraph(int n) {
graphe *grph = malloc(sizeof(*grph));
grph->vertices = n;
grph->array = (AdjList *)malloc(n * sizeof(struct AdjList));
for (int i = 0; i < n; ++i)
grph->array[i].head = NULL;
return grph;
}
/* Ajoute une arête avec un poids */
void add_edge(graphe *grph, int src, int target, int weight) {
if (weight == 0)
return;
nodeAdjList *Node = new_nodeAdjList(target, weight);
Node->next = grph->array[src].head;
grph->array[src].head = Node;
}
/* Fonction qui affiche une liste d'adjacence */
void print_adjlist(struct graphe *grph) {
int v;
for (v = 0; v < grph->vertices; v++) {
nodeAdjList *temp = grph->array[v].head;
printf("\n Liste d'adjacence de %d : \n head ", v);
while (temp != NULL) {
printf("-> (%d, %d)", temp->target, temp->weight);
temp = temp->next;
}
printf("\n");
}
}
/* Génération d'un graphe aléatoire, en stockant les poids des arêtes qu'on
* ajoute dans un tableau n*n */
graphe *remplirgraphe(int n) {
graphe *grph = newgraph(n);
int i, j, w, num, max;
float v, taux;
max = n * n;
taux = 25.0;
num = n / 10;
while (num > 1) {
num /= 5;
taux /= 3.0;
}
taux /= 100.0;
srand(time(NULL));
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
v = (float)rand() / (float)RAND_MAX;
add_edge(grph, i, j, v < taux ? (int)(v * 100.) : 0);
}
}
return grph;
}
Sommet | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
Brin | -1 | -2 | +2 | +1 | -3 |
Ensuite, chaque brin est associé à un brin suivant en fonction du sommet qui lui est associé, ce brin suivant étant le premier que l’on rencontre en tournant autour du sommet dans le sens \circlearrowleft.
On aurait pu choisir l’autre convention, mais il faut être cohérent et garder le même.
Exemple
Brin | -5 | -4 | -3 | -2 | -1 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|---|---|---|---|
Sommet | 0 | 4 | 4 | 1 | 0 | 3 | 2 | 1 | 0 | 1 |
Brin suivant | +4 | -3 | -4 | +5 | +5 | +1 | +2 | -2 | -1 | +3 |
Question : Comment reconstruire un graphe de manière unique à partir de cette donnée ?
Exemple
Brin | -4 | -3 | -2 | -1 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|---|---|---|
Sommet | 2 | 0 | 1 | 0 | 1 | 2 | 2 | 0 |
Brin suivant | +2 | -1 | +1 | +4 | -2 | +3 | -4 | 0 |
On va construire un graphe à 3 sommets et 4 arêtes.
- le fait que le brin
-4
soit associé au sommet2
va indiquer que l’arête étiquetée 4 va partir du sommet 2 - le fait que le sommet
0
admette dans la ligne brin suivant le+4
indique que ce brin va arriver vers0
. On a donc une arête 2\ \underrightarrow{4}\ 0 et on peut ainsi reconstruire le graphe :
typedef struct strand {
short int made;
short next;
} strand;
typedef struct strandgraph {
short int nbs;
short int nba;
short *made; // brim
strand *nxt; // sommet et brim suivant
} strandgraphe;
Autres algos plus court chemin
Algo puissances de matrices d’adjacence (cf. Cours3)
Le nombre de chemin de longueur n d’un sommet i vers un sommet j est le coefficient (i, j) de la matrice M^{n}
(O -> 3)
k = 1
tant que M^{k}[0][3] == 0
k+=1
Algo de Floyd-Warshall
Distance d’un plus court chemin \delta(u, v) entre deux sommets u
et v
pour toutes les paires (u, v)
G = (S, A) = S = \{s_{1}, \dots, s_{n}\}
On utilise une matrice M := M[i][j] = \begin{cases} 0\ p_{i} := J \\\\ w(s_{i}, s_{J})\ s_{i} j_{i}\ \overrightarrow{EA}\ s_{j} \\\\ \infty \text{ sinon} \end{cases}
Et on veut construire une matrice S(i, j) telle que \delta(S_{i}, J_{j})
Remarque
On peut avoir des poids négatifs
Exemple
Idée de l’algo :
P : v_{0} \underbrace{\rightarrow v_{1} \rightarrow v_{2} \dots \rightarrow v_{k-1}}\_\text{Intérieur de P} \rightarrow v_{k}
Un chemin d’intérieur vide est un chemin de la forme v_{k} ou v_{k} \rightarrow v_{k+1}
\delta(s_{i}, s_{j}) = un chemin dont l’intérieur est potentiellement contenu dans tout S
L’idée est que l’on va déchirer des chemins avec des intérieurs petits, et les autoriser à être de plus en plus grands
Soit I \leq S, soit \delta^{I}(u, v) la distance d’un plus court chemin d’intérieur inclus dans I.
Exemple
Propriété
\delta^{Iu\{x\}}(u, v) = \text{min}(\underbrace{\delta^{I}(u, v)}_\text{passer par x augmente la longueur du chemin}, \underbrace{\delta^{I}(u, x) + \delta^{I}(x, v)}_\text{passer par x améliore le ???})
void floyd_warshall(graphe *g) {
int n = g->vertices;
int dist[n][n];
// Ici faut construire la matrice initiale
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
// C'est du O(n³) donc ça pue un peu la merde
}
Algo de Belmann-Ford
C’est un algorithme de plus court chemin qui autorise d’avoir des arêtes avec des poids négatifs ; mais pas de cycles de poids négatifs
Exemple
\Rightarrow Le cycle 1\rightarrow 2\rightarrow 3\rightarrow 4 amène un chemin de longueur -2 de 1 vers lui-même. Ça n’a donc pas de sens de parler de plus court chemin de 0 vers 1.
Une partie importante de l’algo consiste à vérifier qu’il n’y a pas de cycle négatif, on s’en passera ici car on garde notre hypothèse de poids positifs !
Idée
On va calculer les longeurs des chemins les plus courts de longueur k, pour k++.
Observation
Les chemins les plus courts auront au plus n-1 arêtes ! (où n = nombre de sommets)
En effet
S’il y a plus de n arêtes, on passe par un sommet au moins 2 fois donc il y a forcément un cycle. Si ce cycle est de poids \geq 0, il est mauvais pour le plus court chemin donc on l’oublie. Il ne peut pas être de poids \leq 0.
De plus, un chemin de longueur du plus court chemin de src
= u vers v avec au plus k arêtes.
On a alors :
\text{dist}^{k}[v] = \text{min}(\text{dist}^{k-1}[v], \text{dist}^{k-1}[w] + \text{poids}(w\rightarrow v))\ \text{pour tout voisin de }w\text{ et }v
Formule de réccurence !
Initialisation
\text{dist°}[u] = 0 \text{dist°}[w] = \text{INF} pour tout autre sommet
Exemple
Longueur \ Sommet | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
0 | 0 | INF | INF | INF | INF | INF | INF |
1 | 0 | 6 | 5 | 5 | INF | INF | INF |
2 | 0 | 3 | 3 | 5 | 5 | 4 | INF |
3 | 0 | 1 | 3 | 5 | 2 | 4 | 7 |
4 | 0 | 1 | 3 | 5 | 0 | 4 | 5 |
5 | 0 | 1 | 3 | 5 | 0 | 4 | 3 |
6 | 0 | 1 | 3 | 5 | 0 | 4 | 3 |
Pour la représentation triplets :
void bellmanford(graphe *grph, int src) {
int n = grph->nbs;
int a = grph->nba;
int u, v, poids;
int *dist = malloc(n * sizeof(int));
for (int k = 0; k < n; k++) {
dist[k] = INF;
}
dist[src] = 0;
for (int k = 0; k < n; k++) {
for (int l = 0; l < a; l++) {
u = grph->aretes[l].i;
v = grph->aretes[l].j;
poids = grph->aretes[l].poids;
if (poids != 0 && dist[u] + poids < dist[v]) {
dist[v] = dist[u] + poids;
}
}
}
print_dist(dist, n, src);
free(dist);
}
Complexités
Algo | Complexité |
---|---|
Floyd-Warshall | \Theta(n^{3}) |
Bellman-Frod | \begin{cases} \Theta(n^{3})\ \text{avec matrice d'adjacence} \\\\ \Theta(n\times a)\ \text{avec liste d'adjacence} \\\\ \Theta(n\times a)\ \text{avec triplets} \end{cases} |
Matrice adjacence | Liste adjacence | Triplets | |
---|---|---|---|
Dijkstra | \Theta(2n^{2}) | \Theta(n^2 + na) | \Theta(n^{2} + na) |
Dijkstra avec tos binaire | \Theta(n log(n) + n^{2}) | \Theta((n+a)log(n)) | \Theta(n log(n) + an) |
Source | Intensité (en lux) |
---|---|
Soleil (>50°) | 10^{5} lux |
Éclairage intense | 10^{4} lux |
Éclairage de bureau | 500 lux |
Éclairage à Paris 8 | 50 lux |
Balade dehors aisée | 0.1 à 1 lux |
Limite | 10^{-2} à 10^{-3} |
Compression sans perte
- Shannon-Fano
- Huffman
- LZW
- RLE (Run length encoding) qui ne fonctionne que quand il y a plusieurs répétitions dans l’image.
Les taux de compressions de ces algos sont d’environs 30% (1Mo \rightarrow 700Ko)
Compression avec perte
Réduction de la fonction
On peut prendre une certaine surface de l’image et prendre la couleur dominante.
Sauf que c’est moche, ducoup on lisse l’image en stockant 2 couleurs
Pour le lissage / moyenne, 4 est un exemple, on peut prendre la valeur que l’on veut. Plus le paquet est grand plus la compression est grande, donc plus il y a de pertes.
On peut améliorer la compression en conservant non pas 1 couleur mais plusieurs, exemple de répartition :
Table d’indirection de couleurs
0 | 1 | … |
---|---|---|
0 | 255 | … |
0 | 255 | … |
0 | 255 | … |
1 l \Rightarrow 128ko pour une image de 1Mo de pixel, mais là c’est du noir et blanc
0 | 1 | 2 | 3 | 4 | 5 | 6 | … |
---|---|---|---|---|---|---|---|
0 | 255 | 255 | 0 | 0 | 255 | 255 | 0 |
0 | 255 | 0 | 255 | 0 | 255 | 255 | 0 |
0 | 255 | 0 | 0 | 255 | 0 | 255 | 255 |
nombres d’entrées \Rightarrow log_{2}n chiffres binaires.
Le prof raconte qu’il a connu ce genre de PC en parlant de la taille d’indirection de couleurs des cartes graphiques de l’époque.
Nb de couleurs | Taille |
---|---|
256 | 1 octet |
1024 | 10 cb |
4096 | 12 cb |
10^{6} | 16 cb |
Principe de base d’une table d’indirection de couleur (lookup table - CLUT) :
- considérer la CLUT comme un “nuage de points”, c’est à dire RGB ou HSV ou \dots
\hookrightarrow Jusqu’à compactage à peu de couleurs - simplification/détermination, “choisir” n couleurs | les “n milieu” à chaque étape
\rightarrow en parcourant l’image faut faire le choix le plus justifié possible, ce qui est le plus compliqué et le moins systématisé. Le choix est très important pour éviter de n’utiliser que très peu de couleur dans la palette de couleurs choisis (par exemple n’utiliser que 10 couleurs sur 64 disponibles)
Thèse d’Anaïs Atencia
On a une manière brutale et naïve de résoudre un sudoku Exemple Light up est un puzzle consistant à illuminer une grille formée de carrés noirs et blancs en ajoutant des ampules selon les règles suivantes : Exemple Plus d’infos sur le TP ici et les fichiers ici. \Rightarrow Sur un niveau difficile, la résolution par backtracking peut être couteuse Placer les lumières que lumières que l’on peut placer à coup sûr Exemple Si Space_i + Have_i = Need_i \rightarrow on met une ampoule dans tous les éléments de Spl_i Éliminer les cases qui ne pourront pas accueillir d’ampoule Exemple Exemple : Si on applique ce pattern 1 - 2 deux foix : Exemple Chiu - Chou - Yang - Yen ont publié en 2010 un article “A simple and rapid Lights-Up solver” dans lequel ils présentent un algorithme efficace du puzzle. Ils utilisent notamment 1, 2 et 3 ainsi que d’autres patterns ; ainsi qu’une méthode de parcours arborescente pour résoudre les zones blanches non encore éclairées. Le fichier En fait, la résolution rapide de gros puzzles de Light Up est un problème qui intéresse encore des chercheurs à l’heure actuelle. On étudie un jeu à deux joueurs à somme nulle (somme des gains et pertes égale à 0) où : Exemple TicTacToe, Dames, Jeu de Go, Échecs, Puissance 4… Difficultés : Idée (naïve) : Construire l’arborescence des coups réalisables Exemple avec un TicTacToe Exemple : Pour les Échecs, \begin{aligned}F(p) = &200(N_{k} - N_{k'}) + 9(N_{Q} - N_{Q'}) + S(N_{R} - N_{R'}) \\\ &+ 3(N_{B} + N_{N} - N_{B'} - N_{N'}) + N_{p}- N_{p'} \\\ &-(D-D'+S-S'+I-I')\end{aligned} D = nombre de pions doublés (cf. image) Cette fonction renvoie un nombre flottant : plus il est grand (positif), plus le joueur blanc a l’avantage; plus il est petit (négatif); plus le joueur noir a l’avantage Exemple Problème majeur : Complexité spatiale beaucoup trop grande aka arbre de jeu immense Exemples Pour éviter cette explosion combinatoire, on peut se limiter à parcourir l’arbre jusqu’à une certaine profondeur Exemple : TicTacToe avec profondeur 2 Pour obtenir une telle fonction, on utilise une estimation h : \mathcal{P} \rightarrow \R fonction d’évaluation Plus la profondeur est élevée, plus la fonction d’évaluation a des chances d’être efficace en s’approchant des positions terminales; mais plus la taille en mémoire sera importante. Hypothèses : Première idée : on va éviter de parcourir les positions superflus de l’arbre de jeu \Rightarrow Élagage de l’arbre de recherche Cet algorithme s’appelle alpha-beta (\alpha-\beta) puisque selon le joueur, on va réaliser deux types d’élagage dans l’arbre Cela n’enlève pas l’effet d’horizon, mais permet d’augmenter la profondeur en réduisant le nombre de positions à explorer. On donne maintenant deux exemples d’élage de positions superflues : \begin{cases} \text{une coupure } \alpha \\\\ \text{une coupure } \beta \end{cases} (1) MAX va choisir la valeur maximale renvoyée par l’évaluation de tous les coups possibles de MIN, donc 12 Exemple : On considère comme précédemment une fonction d’évaluation h : \mathcal{P} \rightarrow \R et deux joueurs : MIN et MAX. [Knuth ‘7s]: Lorsque l’algorithme minimax trouve un coup en parcourant n noeuds, alpha-beta trouve ce même coup en parcourant \approx 1+2\sqrt{n} coups si les coups sont bien ordonnées Exemple: Schéma résumé : Il existe une variante de l’algorithme minimax, appelée negamax, plus utilisée actuellement. Plutôt que de tester si on est à une profondeur paire ou impaire pour savoir si on cherche à maximiser ou minimiser, on peut inverser le signe des évaluations et toujours chercher à maximiser. Autoriser des coupures plus longues : jusqu’à présent, on a réalisé des coupures alpha ou beta en comparant la valeur d’un noeud avec les valeurs des frères de son parent. On peut aussi regarder les parents des parents, exemple : Alpha-beta itératif incrémental Approfondissement sélectif quiescence Approfondissement sélectif L’algorithme MCTS est un autre algorithme d’IA pour les jeux basé sur une recherche arborescente MCTS conserve en mémoire un arbre qui correspond aux noeuds déjà explorés de l’arbre de jeu. Dans chaque noeud, on stocke deux nombres : le nombre de simulations gagnantes et le nombre totale de simuations. Pour la phase de sélection, il faut choisir un bon compromis entre l’exploitation des choix qui ont l’air prometteurs et l’exploration des noeuds à partir desquels peu de simulations ont été réalisées La partie \frac{w}{n} correspond à l’exploitation : la fraction est grande pour les successeurs qui ont eu beaucoup de succès jusque là La partie \sqrt{\frac{\ln{N_{i}}}{n_{i}}} correspond à l’exploration : elle est grande pour des successeurs impliquées dans peu de simulations Avantages : Fonction d’évaluation Puissance 4 : Grille de jeu pondérée Compter le nombre d’alignements f(p) = score(J1) - score(J2) Inconvénient : Cette évaluation tient en compte des alignements “bloqués”, par exemple :
Algorithmes par les Jeux
Résolution de jeux à un joueur
Le Sudoku
C’est un algorithme de type “backtracking” (retour en arrière)
On parcourt ainsi toutes les possibilités de remplir la grille \rightarrow c’est en fait un parcours en profondeur d’un arbre, dans lequel on cherche la branche qui mène à une solution
Light up
Ce nombre d’ampoules doit être exact, pas intérieur ni supérieur. Si un carré noir n’est pas numéroté le nombre d’ampoules l’entourant peut être quelconque.
Implémentation d’un algorithme de résolution par backtracking
typedef struct state {
int nb_light;
int light_pos;
}
print_board(board b)
\rightarrow affichage du jeuadd_light
\rightarrow ajoute une lumière supplémentaire dans le jeu et le vecteur détatsfill_board(board b, state s)
\rightarrow éclaire le tableau en fonction des lampes présentescheck(board b, state s)
\rightarrow vérifie si l’état actuel des lampes respecte les contraintes du jeu (0 si non, 1 si oui)is_solved(board b, state s)
\rightarrow détermine si le jeu est résolu (les contraintes sont respectés + tout est éclairé)solve(board b)
\rightarrow résout le jeu par backtracking naïfwhile (is_solved(b, s) == 0) {
if (check(b, s) == 0) {
return 0;
}
for(int k = 0; k < size; k++) {
if (b.game[k] == ' ') {
b <- add_light(b, s, k);
b <- fill_board(b, s);
if (solve(b) == 0) {
s.nb_light--;
b = fill_board(b, s);
solve(b);
}
}
print_board(b);
}
}
Comment améliorer cet algorithme ?
N_{b} = nombre de carrés noirs
i \in \\\{1, ..., N_{b}\\\}, avec :
Spl_i = { carrés blanc admissibles autour }
Si Have_i = Need_i \rightarrow toutes les positions restantes de Spl_i peuvent être marquées d’une croix
Le jeu est presque résolu en 2 étapes !
3. S’il y a une case non allumée isolée, on peut y placer une ampoule
N_w = nombre de carrés blancs
J \in \\\{1, ..., N_w\\\}
Si Source_J = 1, on peut placer une ampoule dans la seule position valable; Source_J correspond au nombre de carrés non éclairés autour de J_i incluent J
Il y a d’autres patterns possibles d’améliorations…lightup.c
contient une implémentation de cet algorithme \rightarrow vous pouvez ainsi tester et comparer l’efficacité de votre algorithme de backtracking par rapport à celui-ci.
Remarque
\rightarrow jeu qui apparaît comme un problème aux “Computer Olympiads” en 2010
Jeux à deux joueurs et IA
\rightarrow fonction qui nous permet d’évaluer et de mesurer quel joueur à l’avantage à une position de jeu donnée. Il faut donc un moyen de définir une fonction
où N\cdot désigne le nombre de \cdot chez le joueur blanc (respectivement N\cdot' chez le joueur noir) et K King, Q Queen, R Rook (Tour), B Bishop (Fou), N kNight (Cavalier) et P Pawn (Pion).
S = nombre de pions bloqués
I = nombre de pions isolés
M = mobilité du joueur
\begin{aligned}f(p) = &200(1-1)+9(1-1)+5(0-1)+3(2+0-1-0) \\\ &+3-3-\frac{0-2+0-1+1-1}{2}+\frac{37-46}{10}\\\ &\simeq-1.4\end{aligned}
On peut donc supposer qu’à cette position, le joueur noir a légèrement l’avantage. Cependant, si blanc bouge son fou en H7 (pour mettre en échec le roi), il prend l’avantage en capturant la dame au coup suivant…
Ceci est appelé un effet d’horizon.
Principe de résolution :
Idéalement : fonction h^{*} : \mathcal{P} \rightarrow \\\{-\infty, 0, +\infty\\\} (avec -\infty : le joueur 2 gagne, 0 match nul et +\infty le joueur 1 gagne) qui détermine quel joueur gagne.
Cette fonction devra être d’autant plus fiable que p est proche d’une position terminale
Mais h est seulement une heuristique, et on ne peut pas prévoir les effets d’horizon.
\Rightarrow Il faut donc trouver un bon compromis entre la profondeur de jeu choisie et la fonction d’évaluation
Algorithme Minimax
Joueur 1 = MAX
Joueur 2 = MIN
minimax (profondeur n, position p, joueur J)
- si p est terminale
return h*(p)
- si n = 0 # On a atteint la profondeur maximale
return h(p)
- sinon, soit p1, ..., pm les m positions accessibles depuis p
- si J = MAX
return max minimax(n - 1, pi, MIN) # 1 <= i <= m
- si J = MIN
return min minimax(n - 1, pi, MAX) # 1 <= 1 <= m
Inconvénients
Amélioration
Remarque
(2) MIN va choisir la valeur minimale renvoyée par l’évaluation de tous les coups possibles de MAX, donc 12
MIN va donc ensuite choisir la valeur minimale entre 12 et \geq 14 \ra ce sera 12 !
… peu importe ce que donne h(p_{1}) : on a donc pas besoin d’explorer la position p_{1} et on peut couper la tranchée qui relie p_{1} à son parent \RA c’est une coupe \beta
Ainsi, MAX préfèrera le coup qui mène à 12 plutôt que une position \leq 6 : c’est une coupe \alpha\alpha-\beta)
Alpha-Beta (
Pour évaluer p à une profondeur n, pour le joueur J, on conserve :
\RA L’ordre des coups est important car en fonction de l’ordre, on ne réalise pas le même élagage.
alphabeta (profondeur n, position p, joueur J, alpha, beta)
- si p est terminale, alpha = -INF ; beta = +INF
return h*(p)
- si n = 0
return h(p)
- sinon
- soient p1, ..., pm les m positions accessibles depuis p
- si J = MIN
- e <- alphabeta(n-1, p1, MAX, alpha, beta)
- si e <= alpha
return alpha
- sinon beta <- min(beta, e)
recalculer e pour les autres positions p2, ..., pm
- si J = MAX
- e <- alphabeta(n-1, p1, MIN, alpha, beta)
- si e >= beta
return beta
- sinon alpha <- max(alpha, e)
recalculer e pour les autres positions p2, ..., pm
Remarque
On peut donc toujours procéder à un élagage alpha-beta à partir d’un negamax
Améliorations de l’algorithme alpha-beta
Le 20 entouré en rouge ne peut a priori pas donner lieu à un élagage classique.
Mais on peut constater que le frère du parent de son parent a pour valeur 10, et que valeur \geq 20 ne sera jamais choisie :
\ra le 20 ne sera donc jamais choisi, et donc son frère non plus (soit il est \leq 20 et ne remonte pas, soit il est \geq 20 et il sera encore moins choisi pour la même raison) : on peut donc réaliser une coupure “grande-\betaeta / \beta+”
L’idée est de commencer par réaliser une recherche à profondeur 1, puis recommencer avec une recherche complète à profondeur 2, puis profondeur 3, etc, jusqu’à ce qu’une solution soit trouvée
Avantage : On trouve toujours la solution la plus courte, puisqu’un noeud n’est jamais engendré tant que tous les noeuds à profondeur inférieur n’ont pas été explorés.
Complexité: \O(d) si l’algo se termine à profondeur d
A priori, on perd beaucoup de temps dans les itérations précédent la solution. Mais ce travail supplémentaire est en général beaucoup plus petit que la dernière itération
Exemple : Si n coups en moyenne par position, à profondeur d, on a n^{d} noeuds.
Le nombre de noeuds à profondeur d-1 est de n^{d-1} mais chaque noeud est engendré 2 fois (une fois par le lancement à profondeur d-1 et une fois pour le lancement à profondeur d)
Au final : nombre de noeuds engendrés : n^{d} + 2n^{d-1} + 3n^{d-2} + \dots + dn = \O(n^{d})
Si n est grand, le premier terme est dominant, et c’est la dernière itération qui prend le plus de temps.
De plus : alpha-beta incrémental permet de :
\ra elle évalue si une position est stable (essentiellement, si il reste des pièces en prise) qui donneront grosso-modo la même évaluation à profondeur d ou d+1.
Une position stable a donc une évaluation en laquelle on peut avoir confiance alors qu’une instable non.
Deepblue utilise un mécanisme de quiescence pour continuer de développer les positions basses de l’ordre jusqu’à des positions stables. Aux échecs, la quiescence est couramment utilisée pour des coups de capture ou des coups de promotion (sauf quand le roi est en échec)
Chaque étape de l’algorithme est constituée de 4 phases :
Exemple :
Si un coup semble intéressant, on continue à cherche à une profondeur plus grande que la profondeur habituelle.
Si un coup semble mauvais, on arrête de chercher à une profondeur plus petite que la profondeur habituelle.
Exemple : Chinook, IA pour les dames
Monte-Carlo Tree Search (MCTS)
\ra une feuille de cet arbre est soit une position terminale du jeu, soit un noeud dont aucun enfant n’a encore été exploré.
Si la partie est perdante pour J1 : on incrémente le nombre de simulation total uniquement pour les noeuds J1 et on incrémente à la fois le nombre de simulations totales et gagnantes pour J2… et inversement si la partie est gagnante pour J1.
\ra formule UCT (Upper Confidence Bound) introduite par Kocsis et Szepesvári donne un bon compromis
On choisit en chaque noeud de l’arbre le successeur i qui maximise l’expression \frac{w_{i}}{m_{i}} + c \sqrt{\frac{\ln{N_{i}}}{n_{i}}} où :
selection (pos p)
Si p est terminale
return p
N = nombre de visites du père p (fonction getN)
Si N == 0
return p
M <- { Mouvements possibles depuis p }
{ max, best } <- { -1, ∅ }
pour chaque m ∈ M faire
ni = nombre de fois où p à été visité
Si ni == 0
return p
new_eval <- UCT(p, m)
Si new_eval > max
{ max, best } <- { new_eval, m }
p' <- apply_move(p, best)
return selection(p')
expansion (pos p)
Si p est terminale
return p
N <- getN(p)
Si N == 0
make_moves(s)
M <- { Mouvements possibles depuis p }
pour chaque m ∈ M
Ni <- getNi(p, m)
Si Ni == 0
p' = apply_move(p, m)
return p'
playout (pos p)
while not(p position termionale)
M <- next_moves(p)
m <- random(M)
p <- apply_move(p, m)
return score(p)
back_propagate (pos p, score)
if parent(p) == ∅
return
add_score(p, score)
return back_propagate(parent(p), score)
score(J1) = somme des poids des cases sur lesquelles J1 a un jeton
Pour chaque joueur, on évalue son score de la façon suivante :
donnera un score positif pour J1 avec 2 alignements de 3, alors qu’un seul alignement de 4 n’est encore possible.
Mieux : Ne compter dans ce calcul que les alignements encore envisageables