Algorithmie avancée

Notation

Cours

Tris

Exemple d’implantation de ces algorithmes ici

Complexité quadratique (\Theta(n^{2}))

Tri à bulle

f(n) = \Theta(n^{2}) si f\frac{n}{n^{2}} est bornée

Taille n :
\sum\limits_{k=1}^{n}k=\frac{n(n+1)}{2}=(n-1)+(n-2)+\dots+1

Tri par insertion

Complexité \Theta(nlog(n))

1+2+\dots+(n-1)=\frac{n(n+1)}{2}

Tri rapide et tri fusion

n base10 => binaire
_ _ _ _ _ _ \frac{n}{2}
_ _ _ _ _ 0 \frac{n}{4} (ou 1 à la place de 0)

n sur k bits
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

Méthode de récursivité lourde \approx \Theta(2^{n})

Soit C_{n} k nombre d’opérations pour calculer F(n)
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

\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 :
\left[\begin{array}{ll} F(n) = F(n-1) + F(n-2) \\\ F(n-1) = F(n-1) \end{array}\right.

Donnant :
\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

a^{n} puissance 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

a^{n} A^{n} puissance rapide

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

Triangle de Pascal (avec coefficient binomiaux) :
\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

De la droite discrète

jj.up8.site/droites

Attention, j’ai probablement fait des erreurs en recopiant, de toute façon le prof l’a noté en gros sur son site

Table des matières

Définition

En maths, une droite est un objet définie car il passe par 2 points et qu’il est infini dans les 2 sens.

En info, c’est un ensemble de pixels qui ont la qualité d’être proche de la droite réelle relativement proche de la droite réel (celle des maths), ces pixels forment un chemin biconnexe.

graph

void droite (int x0, int y0, int x1, int y1) {
  for (int x = x0; x <= x1; x++) {
     flat yf = (float) (x - x0) * (y1 - y0);
     yf /= (x1 - x0);
     yf += y0;
     int y = (int) (yf + .5);
     affiche(x,y);
  }
}

\frac{x-x_{p}}{x_{q} - x_{p}} = \frac{y - y_{p}}{y_{q} - y_{p}}
y = y_{p} + \frac{(x-x_{p})(y_{q} - y_{p})}{x_{q} - x_{p}}

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

bresenham
|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.

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 :
chemin

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

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]

Plages (Spans)

\Rightarrow Symétrie interne
symétrie

Récapitulation et algorithme rapide

Graphes : parcours et plus court chemin

Définition, notations

Un graphe est une structure de données non linéaires constituée de 2 types d’objets :

ex1
Un graphe est dit orienté si toutes les arêtes sont équipées d’une certaine direction, indiquée par une flèche. Dans un graphe orienté, chaque arête a un sommet source et un sommet but (target)
ex2
Un graphe est dit pondéré si toutes les arêtes sont étiquetées par un nombre, encadrant généralement la distance entre 2 sommets. Ce nombre est appelé poids.
ex3
Dans ce cas, nous allons nous intéresser principalement à des graphes orientés et pondérés, par des poids entiers positifs (la positivité est indispensable pour l’algorithme de Dijkstra)

Parcours

Il existe plusieurs manières de parcourir / d’explorer un graphe, nous allons en voir 2 principales

Parcours en profondeur

ex4

Remarque

Évidemment, on parle de voisins non-déjà-parcourus, pour savoir ça, par exemple, on stoque dans un tableau de booléens pour savoir si le sommet à déjà été parcourus ou non.

Parcours en largeur

ex4

Remarque

Algorithme de Dijkstra

C’est un algorithme permettant de trouver le plus court chemin entre 2 sommets dans un graphe pondéré
ex9
Paramètres de l’algorithme :

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 :

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

Sommets 0 1 2 3 4
Distance de 0 0 INF 18 INF 3
Sommets 0 1 2 3 4
Distance de 0 0 13 18 5 3
Sommets 0 1 2 3 4
Distance de 0 0 6 18 5 3
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.

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 */);
            }
        }
    }
}

Implémentation possibles :

  1. Un tableau

    • Enfiler : Rajouter à la fin du tableau
    • Défiler : Parcourir toutes les tâches jusqu’à trouver le minimum.
  2. 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
  3. (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)
    ex10

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}
ex13

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é :

Remarque: il y a 2x le nombre d’arêtes que de 1.

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…

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
ex16

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;
}

Remarques post-TP

La définition de graphe avec une matrice d’adjacence de taille MAX_NOEUD * MAX_NOEUD n’est pas optimale.

Une manière meilleure

typedef struct graph {
    int vertices; // nb sommets
    int **adj;
} graphe;

Dans new_graph, il faut allouer la taille qu’il faut pour la matrice d’adjacence :

grph->vertices = m;
grph->adj = malloc(n * sizeof(int *));
for (int i = 0; i < n - 1; i++) {
    grph->adj[i] = malloc(n * sizeof(int));
}

Il faut écrire une fonction en plus pour libérer la mémoire.

Parcours en largeur, dijkstra, correction TP

void accessibles(graphe *grph, int src) {
    int *vus = malloc(grph->vertices * sizeof(int));
    int k;
    for (k = 0; k < grph->vertices; k++)
        vus[k] = 0;
    fifo *aVisiter = new_fifo();
    vus[src] = 1;
    push(aVisiter, src);
    while (aVisiter->taille != 0) {
        int i = take_first(aVisiter);
        for (int j = 0; j < grph->vertices; j++) {
            if (grph->adj[i][j] != 0 && vus[j] == 0) {
                vus[j] = 1;
                push(aVisiter, j);
            }
        }
        defiler(aVisiter);
    }
    print_acc(vus, grph->vertices, src);
    free(aVisiter);
    free(vus);
}

void dijkstra(graphe *grph, int src) {
    int n = grph->vertices;
    int *dist = malloc(n * sizeof(int));
    int *access = malloc(n * sizeof(int));
    int i, j, count;
    for (i = 0; i < n; i++) {
        dist[i] = INF;
        access[i] = 0;
    }
    dist[src] = 0;
    for (int k = 0; k < n - 1; k++) {
        int u = minDistance(grph, dist, access);
        access[u] = 1;
        for (j = 0; j < n; j++) {
            if (access[j] == 0 && grph->adj[u][j] != 0 &&
                dist[u] + grph->adj[u][j] < dist[j]) {
                dist[j] = dist[u] + grph->adj[u][j];
            }
        }
    }
    print_dist(dist, n, src);
    free(dist);
    free(access);
}

Le coefficient grph->adj[u][j] représente le poids d’une arête qui va de u vers j dans grph. On peut donc adopter aisément cet algorithme pour d’autres implémentations de graphes pour peu que l’on sache testéer s’il y a une arête de i vers j dans un graphe et en récupérer son poids.

int is_edge_in_adjlist(graphe *grph, int i, int j) {
    int test = 0;
    noeudListeAdj *n = grph->array[i].head;
    while (n != NULL && test == 0) {
        if (n->but == j)
            test = n->poids;
        n = n->suivant;
    }
    return test;
}

Ainsi, dans les algos précédents, il suffit de remplacer grph->adj[u][j] par is_edge_in_adjlist(grph, u, j) pour définir Dijkstra sans file de priorité.

Représentation par triplet

Supposons que l’on ait un graphe G à 15 sommets et une seule arête 0\ \underrightarrow{10}\ 1
Si on utilise une matrice d’adjacence pour représenter G, on va stocker 15 \times 15 + 225 coefficients dont… 224 zéros !
Or, il suffirait de connaître :

On peut éviter de stocker tous les zéros de la matrice d’adjacence en stockant de cette manière toutes les arêtes dans un vecteur contenant des triplets de la forme
(i, j, w) \leftrightarrow arête de i vers j de poids w dans G.

typedef struct triplet {
    int i;
    int j;
    int poids;
} triplet;

typedef struct graphe {
    int nbs;         /* nombre de sommets */
    int nba;         /* nombre d aretes */
    triplet *aretes; /* vecteur d aretes*/
} graphe;

Exemple

ex17
Nombre de sommets : S
Vecteur d’arêtes: (0, 4, 3), (0, 2, 18), (1, 0, 8), (2, 1, 4), (3, 1, 1), (4, 1, 10), (4, 3, 2)

Remarque

Pour déterminer s’il y a une arête de i vers j dans G, il suffit de parcourir le vecteur d’arêtes. C’est une opération en \Theta(a)a est le nombre d’arêtes de G.

Représentation par brins

C’est une manière d’encoder un graphe introduite par Cori en 1973 ; très peu de références dans la littérature scientifique mais JJ aime bien alors voilà ¯\_(ツ)_/¯

Principe

On considère un graphe, dont on va numéroter les arêtes (si le graphe est pondéré, on peut garder les poids mais il faut des poids distincts)

Exemple

ex18
Une arête numérotée n va être séparée en deux parties :

On va ensuite associer à chaque sommet un brin (le premier brin), indiqué par le brin arrivant/sortant ou/du sommet avec la plus petite étiquette, et gardant son signe

Exemple

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.

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

ex20
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

ex21

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

ex22
\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

ex23

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)

Les raisons

Avec une sensibilité de 72ppp (points par pouce)

Avec ce modèle de couleur (Trichomie), on ne sais pas créer de Jaune, de Cyan ou de Magenta, ce qui veux dire que les lois du dessous sont fausse :)

Ca c’est les chiffres sur lesquels les lois précédentes se sont basés (on remet pas en question parce que yavait des français au CIE hihi ^^)

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

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.
ex2

Sauf que c’est moche, ducoup on lisse l’image en stockant 2 couleurs
ex1
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 :
ex3
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) :

Thèse d’Anaïs Atencia

Algorithmes par les Jeux

Résolution de jeux à un joueur

Le Sudoku

On a une manière brutale et naïve de résoudre un sudoku

Exemple

sudoku
C’est un algorithme de type “backtracking” (retour en arrière)
backtracking
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

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

lightup

Implémentation d’un algorithme de résolution par backtracking

typedef struct state {
   int nb_light;
   int light_pos;
}

Plus d’infos sur le TP ici et les fichiers ici.

\Rightarrow Sur un niveau difficile, la résolution par backtracking peut être couteuse

Comment améliorer cet algorithme ?

  1. Placer les lumières que lumières que l’on peut placer à coup sûr

    Exemple

    q1_exemple
    N_{b} = nombre de carrés noirs
    i \in \\\{1, ..., N_{b}\\\}, avec :

    • Need_{i} = nombre dans le carré (dont on a besoin)
    • Space_{i} = nombre de carrés blanc pouvant accueillir une ampoule autour de ?
    • Have_i = nombre d’ampoules que l’on a autour
      Spl_i = { carrés blanc admissibles autour }

    Si Space_i + Have_i = Need_i \rightarrow on met une ampoule dans tous les éléments de Spl_i

  2. Éliminer les cases qui ne pourront pas accueillir d’ampoule

Exemple

q2_exemple
Si Have_i = Need_i \rightarrow toutes les positions restantes de Spl_i peuvent être marquées d’une croix

Exemple : Si on applique ce pattern 1 - 2 deux foix :

q2_exemple2
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

Exemple

impl_ex
Il y a d’autres patterns possibles d’améliorations…

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 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

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.
\rightarrow jeu qui apparaît comme un problème aux “Computer Olympiads” en 2010

Jeux à deux joueurs et IA

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

arborescence_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}
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).

D = nombre de pions doublés (cf. image)
echec_pion_double
S = nombre de pions bloqués
I = nombre de pions isolés
M = mobilité du joueur

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

exemple_chess_board
\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…
exemple_chess_board 1-echec
Ceci est appelé un effet d’horizon.

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

tictactoe profondeur
Principe de résolution :

Pour obtenir une telle fonction, on utilise une estimation h : \mathcal{P} \rightarrow \R fonction d’évaluation
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

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.

Algorithme Minimax

Hypothèses :

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

  1. Complexité élevée \RA \O(c^{n}) avec c coups légaux en moyenne par chaque position et profondeur n.
  2. Plus problématique : effets d’horizon; pour minimax(n, p, J) il se peut que p_{n+1} soit perdante alors que p_{n} est favorable

Amélioration

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

Remarque

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}
alphabeta

(1) MAX va choisir la valeur maximale renvoyée par l’évaluation de tous les coups possibles de MIN, donc 12
(2) MIN va choisir la valeur minimale renvoyée par l’évaluation de tous les coups possibles de MAX, donc 12

Exemple :
alphabeta2

coupe_alpha

Alpha-Beta (\alpha-\beta)

On considère comme précédemment une fonction d’évaluation h : \mathcal{P} \rightarrow \R et deux joueurs : MIN et MAX.
Pour évaluer p à une profondeur n, pour le joueur J, on conserve :

[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
\RA L’ordre des coups est important car en fonction de l’ordre, on ne réalise pas le même élagage.

Exemple:
alphabeta_ordre

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

Schéma résumé :
alphabeta_resume

Remarque

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.
On peut donc toujours procéder à un élagage alpha-beta à partir d’un negamax

Améliorations de l’algorithme alpha-beta

  1. 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 :
    amelioration_alphabeta_parent
    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 :

    • soit le minimum sera obtenue par le frère du 20 à profondeur 2, auquel cas le 20 n’est pas selectionné.
    • soit le 20 remonte jusqu’à 1, mais dans ce cas c’est le 10 qui sera choisi
      \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+
  2. Alpha-beta itératif incrémental
    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.
    alphabeta_it_incr
    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 :

    • développer des heuristiques de parcours, en reprenant le parcours de l’arbre à partir de la position qui était jugée la meilleur au lancement à profondeur précédente
    • optimiser l’élagage en implémentant un tri rapide pour placer les meilleurs coups dans l’ordre
  3. Approfondissement sélectif quiescence

    • Pour pallier aux inconveniants des effets d’horizon, Deap Blue a utilisé une meta fonction d’évaluation qui n’évalue pas la position mais plutôt le type de la position
      \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 :
      1. Sélection : Depuis la racine de l’arbre, on sélectionne successivement des enfants jusqu’à atteindre une feuille.
      2. Expansion : Si cette feuille n’est pas une position terminante, créer un enfant (ou plusieurs) en appliquant un mouvement suivant les règles du jeu, puis ajouté cet enfant marqué (0/0)
      3. Simulation (“Playout” ou “Roll-out” en anglais) : Simuler une exécution d’une partie au hasard depuis cet enfant, jusqu’à atteindre une position de jeu terminale
      4. Back propagation : Utiliser le résultat de la partie au hasard et mettre à jour les informations sur la branche en partant de la feuille vers la racine
        Exemple : deepblue
  4. Approfondissement sélectif
    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

    • S’il analyse un coup qui perd 3 pions, plutôt que d’analyser à profondeur 10, il va réduire à seulement 5 coups à l’avance, en considérant que le coup a de bonnes chances d’être mauvais
    • S’il analyse un coup qui paraît très bon, il augmentera la profondeur à 12 coups à l’avance

Monte-Carlo Tree Search (MCTS)

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.
\ra une feuille de cet arbre est soit une position terminale du jeu, soit un noeud dont aucun enfant n’a encore été exploré.

Dans chaque noeud, on stocke deux nombres : le nombre de simulations gagnantes et le nombre totale de simuations.
mcts
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.

Avantages :

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)

Fonction d’évaluation Puissance 4 :

  1. Grille de jeu pondérée
    puissance4
    score(J1) = somme des poids des cases sur lesquelles J1 a un jeton

  2. Compter le nombre d’alignements
    Pour chaque joueur, on évalue son score de la façon suivante :

    • +1 par jeton
    • +5 par 2 jetons alignés
    • +50 par 3 jetons alignés
    • +1000 par 4 jetons alignés

    f(p) = score(J1) - score(J2)

    Inconvénient : Cette évaluation tient en compte des alignements “bloqués”, par exemple :
    exemple_puissance4 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