TD7 - Pale machine

 Login :  Mot de passe :

Pour commencer :

Remarque : les exercices 1 et 2 sont indépendants et peuvent se traiter dans n’importe quel ordre. La question 3.1 nécessite la solution de la question 2.1. La question 3.2 nécessite la question 1.1 ou 1.3

La documentation de Java est accessible en local via le lien suivant : Documentation Java

1 Range Minimum Queries

Dans cet exercice, nous allons implémenter une manière efficace de répondre au problème appelé Range Minimum Query sur un tableau T de taille n. Étant donnés deux indices 0 ≤ l ≤ r < n, le résultat renvoyé par rmq(T, l, r) sera un élément de valeur minimale parmi ceux stockés dans le sous-tableau T[l..r] (de l inclus à r inclus).

Les données manipulées sont des paires (String s, int val)val est la valeur et s une étiquette. Elles sont codées par la classe Data (fournie).

public class Data {
    String s;  // étiquette de la donnée
    int val;   // valeur de la donnée

    Data(String s, int val) {
        this.s   = s;
        this.val = val;
    }

    public int compareTo(Data d) {
        if(this.val>d.val) return 1;
        else if(this.val<d.val) return -1;
        return 0;
    }

    /* Renvoie l'élément ayant valeur minimale entre d1 et d2 */
    static Data min(Data d1, Data d2) {
        if (d1.compareTo(d2) <= 0) return d1;
        else return d2;
    }

    /* Affiche les éléments d'un tableau de type Data[] */
    static void printArray(Data[] T) {...}
}

La comparaison entre deux données d1 et d2 peut se faire à l’aide de la méthode int compareTo(...). On fournit aussi la fonction Data min(Data d1, Data d2) qui renvoie la donnée ayant la valeur minimale entre d1 et d2.

Les données sont stockées dans un tableau Data[] T qui constitue l’entrée du programme.

Dans l’exemple ci-dessous, le tableau T contient les données démographiques de 16 villes dans le monde. Le résultat de rmq(T, 5, 11) est (Los Angeles, 3287), car c’est la donnée de plus petite valeur entre l’indice 5 (New York) et l’indice 11 (Buenos Aires).

Exemple : densité de 16 villes au monde

1.1 Méthode naïve

Implémenter la méthode naïve, de complexité linéaire, qui consiste à comparer tous les éléments qui se trouvent dans le sous-tableau T[l..r].

Dans la classe BruteForceRMQ, compléter la méthode getMin(int l, int r) qui renvoie un élément d dont la valeur d.val est minimale dans T[l..r].

Suggestion : pour calculer le min entre deux données, vous pouvez vous servir de la fonction static Data min(Data d1, Data d2) de la classe Data.

Tester votre code avec la classe Test11.

Déposer BruteForceRMQ.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

1.2 Décomposition en blocs

Une solution plus efficace consiste à faire une étape de pré-traitement en décomposant le tableau T en blocs de taille b. On obtient ainsi l = ⌈n/b blocs, tous de taille b sauf éventuellement le dernier bloc, dont la taille est au plus b. Remarque: on pourra supposer que b ≤ n.

public class EfficientRMQ implements RMQ {
    Data[] table; // tableau en entrée
    int b;         // taille des blocs
    Data[] B;     // tableau auxiliaire

    /* Initialise le tableau auxiliaire */
    EfficientRMQ(Data[] table, int b) {...}

    /* Renvoie un élément de valeur minimale */
    public Data getMin(int l, int r) {...}
}

On utilise un tableau auxiliaire B (ayant l entrées) dont les éléments sont ceux ayant la valeur minimale dans chaque bloc de T. La recherche de la valeur minimale dans T[l..r] se fait alors en explorant dans T le bloc contenant T[l] et celui contenant T[r], et en effectuant une recherche dans B pour tous les blocs entièrement contenus dans l’intervalle considéré. Le tout nécessite un temps au plus O(b + n/b). En choisissant bien la valeur de b, on obtient une complexité sous-linéaire.

L’exemple ci-dessous illustre le cas d’un tableau T de taille 16 décomposé en blocs de taille 3 (le tableau auxiliaire B a ainsi 6 entrées).

A titre d’exemple, pour répondre à la requête rmq(T, 5, 11), on explore et on compare d’abord les entrées dans les sous-tableaux T[5..5] et T[9..11] qui correspondent aux deux blocs b1 et b3 (contenant les indices 5 et 11). Ensuite on compare le résultat avec la valeur contenue dans B[2] (dans cet exemple b2 est le seul bloc dans l’intervalle entre b1 et b3). Le résultat de rmq(T, 5, 11) sera (L. A., 3287).

Exemple :

Dans la classe EfficientRMQ, compléter le constructeur EfficientRMQ(Data[] table, int b) qui initialise les champs de la classe EfficientRMQ. Notamment, le constructeur doit initialiser et remplir le tableau auxiliaire B.

Suggestion : lors de l’initialisation du tableau auxiliaire B, vous pouvez réutiliser la classe BruteForceRMQ et sa méthode getMin(int l, int r).

Tester votre code avec la classe Test12.

Déposer EfficientRMQ.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

1.3 Méthode plus efficace (de complexité sous-linéaire)

Il s’agit maintenant d’utiliser la décomposition en blocs du tableau T et le tableau auxiliaire B pour fournir une implémentation efficace de la méthode getMin.

Dans la classe EfficientRMQ, compléter la méthode Data getMin(int l, int r) qui renvoie l’élément de valeur minimale dans T[l..r].

Tester votre code avec la classe Test13.

Déposer EfficientRMQ.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

2 Arbres et calcul du LCA

Dans cet exercice, nous manipulons des arbres généraux dont les nœuds ont une arité (nombre de fils) arbitraire. Les nœuds sont représentés à l’aide de la classe Tree (fournie).

/* Classe qui représente un (noeud d'un) arbre général */
public class Tree {
    /* étiquette stockée dans le nœud */
    String element;

    /* vecteur stockant les fils du nœud courant */
    Vector<Tree> children;

    /* Construit un nœud de l'arbre ayant 'element' pour étiquette. */
    Tree(String element) {
        this.element = element;
        this.children = new Vector<Tree>();
    }

    // Pour aider le debugging
    /* affiche les noeuds de l'arbre selon un parcours BFS (par niveaux) */
    static void print(Tree node) {...}
}

Un nœud contient un champ String element (la donnée stockée dans le nœud) et un Vector (champ children) contenant les sous-arbres. Un nœud est une feuille si le vecteur est vide.

On souhaite implémenter une fonction qui, étant données les deux étiquettes el1 et el2 de deux nœuds dans un même arbre, calcule leur plus proche ancêtre commun (en anglais Lowest Common Ancestor ou LCA), c’est-à-dire le nœud le plus éloigné de la racine qui soit à la fois un ancêtre du nœud el1 et du nœud el2.

Remarque (importante) : dans la suite, on supposera que l’arbre considéré contient des étiquettes toutes distinctes et que les étiquettes el1 et el2 de la requête sont distnicteapparaissent bien dans l’arbre. Ainsi, on pourra aussi supposer que l’arbre en entrée contient au moins deux éléments.

Les images ci-dessous illustrent la définition du LCA :

Exemple : le LCA de

Suggestion (aide pour débogage) La classe Tree est munie d’une méthode void print(Tree node) qui affiche les nœuds de l’arbre par niveaux. L’arbre illustré dans l’exemple sera ainsi affiché :

A
B C D E
F G H I J K
L

2.1 Calcul du LCA en temps linéaire

Une solution pour le calcul du lca(t, el1, el2) consiste à définir plus généralement une fonction F comme suit :

Dans la classe Tree, compléter la fonction static Tree lca(Tree node, String el1, String el2) qui renvoie le nœud correspondant au LCA des deux nœuds dont les étiquettes sont el1 et el2.

Remarque : on peut parcourir les entrées d’un Vector<T> v à l’aide de la syntaxe suivante :

for(T element: v) {... }

Tester votre code avec la classe Test21.

Déposer Tree.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

2.2 Taille d’un arbre

La fonction suivante sera utilisée lors de la question 3.1

Dans la classe Tree, compléter la fonction static int size(Tree node) qui renvoie le nombre de nœuds de l’arbre node.

Tester votre code avec la classe Test22.

Déposer Tree.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

3 LCA (plus efficace)

Il est possible de calculer le lca(el1, el2) en traduisant un problème de requête sur un arbre en un problème de range minimum query sur un tableau. Cela peut s’avérer efficace lorsque le nombre de requêtes de type LCA est important par rapport à la taille de l’arbre.

Remarque : comme fait auparavant, on pourra supposer que l’arbre en entrée a une taille au moins 2, et contient notamment les étiquettes el1 et el2 qui sont censées être distinctes.

Dans une première phase, nous allons remplir un tableau Data[] dist de longueur 2n − 1 (où n est la taille de l’arbre) de la manière suivante. En partant de la racine, on effectue un parcours récursif de l’arbre comme ceci : chaque fois qu’on rencontre un nœud,

Par ailleurs, on remplit une table de hachage indices avec, pour chaque étiquette el de l’arbre, l’indice de sa première occurrence dans le tableau dist.

Voici une illustration du résultat sur un arbre contenant n = 12 nœuds :

Exemple : calcul du LCA

Pour répondre à une requête de type lca(s1, s2) sur l’arbre, on peut localiser (en temps constant) les deux indices i1 et i2 correspondants aux premières occurrences de s1 et s2 dans le tableau dist. Puis trouver la paire de profondeur minimale dans dist[i1..i2] en utilisant une implémentation de Range Minimum Query.

On se donne une classe EfficientLCA pour les questions suivantes.

public class EfficientLCA {

    /* Implémentation de RMQ: naive (ex 1.1) ou efficace (1.3) */
    RMQ rmq;

    /* l'arbre considéré */
    Tree node;

    /* pour chaque nœud, on stocke sa distance à la racine */
    Data[] dist;

    /* pour chaque nœud, stoque son indice dans dist[]
    (utile pour exo 3.1) */
    HashMap<String,Integer> indices=new HashMap<String,Integer>();

    EfficientLCA(Tree node, int b) {
        this.node = node;
        int n=Tree.size(node);
        this.dist=new Data[2*n-1];
        ...
    }

}

3.1 Initialisation du tableau dist

Dans la classe EfficientLCA, compléter la méthode setDistances() qui

  • initialise et remplit le tableau Data[] dist ;
  • initialise la table de hachage indices.

Suggestion : pour remplir le tableau dist[], on pourra se servir d’une fonction auxiliaire int setDistancesAux(Tree node, int depth, int count) qui reçoit en argument la prochaine position libre dans dist (le paramètre count) et renvoie de même la prochaine position libre dans dist à l’issue de son travail (c’est-à-dire le nombre d’entrées stockées dans le tableau dist).

Tester votre code avec la classe Test31.

Déposer EfficientLCA.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer

3.2 Calcul du LCA

Il ne reste qu’à initialiser la structure de données pour le calcul des RMQ (champ rmq) et implémenter la méthode du calcul du LCA illustrée ci-dessus.

Dans la classe EfficientLCA,
  • compléter le constructeur EfficientLCA(...) qui doit initialiser le champ rmq: à vous de choisir l’implémentation plus appropriée (celle fournie à l’exo 1.1 ou celle de l’exo 1.3) ;
  • compléter la méthode String computeLCA(String el1, String el2) qui renvoie l’étiquette du LCA des nœuds étiquetés par el1 et el2.

Tester votre code avec la classe Test32.

Déposer EfficientLCA.java.

Le nom du fichier à déposer
Il faut se connecter avant de pouvoir déposer