28 votes

Trouver le mot le plus long à partir d'une collection

Il s'agit d'une question de l'entretien google et je trouve la plupart des réponses en ligne en utilisant HashMap ou une structure de données similaire. J'essaie de trouver une solution en utilisant Trie si possible. Quelqu'un pourrait-il me donner quelques conseils ?

Voici la question : On vous donne un dictionnaire, sous la forme d'un fichier qui contient un mot par ligne. Par exemple,

abacus 
deltoid 
gaff 
giraffe 
microphone 
reef 
qar 

On vous donne également une collection de lettres. Par exemple,

{a, e, f, f, g, i, r, q}. 

La tâche consiste à trouver le mot le plus long du dictionnaire qui peut être épelé avec la collection de lettres. Par exemple, la réponse correcte pour l'exemple de valeurs ci-dessus est "girafe". (Notez que "récif" n'est pas une réponse possible, car la collection de lettres ne contient qu'un seul "e").

Une mise en œuvre de Java serait préférable.

12voto

Stephen C Points 255558

Pas de code Java. Vous pouvez le découvrir par vous-même.

En supposant que nous ayons besoin de faire cela de nombreuses fois, voici ce que je ferais :

  • Je commencerais par créer des "signatures" pour chaque mot du dictionnaire, composées de 26 bits, où bit[lettre] est activé si le mot contient une (ou plusieurs) instances de la lettre. Ces signatures peuvent être encodées sous la forme d'un code Java int .

  • Créez ensuite un mappage qui associe les signatures à des listes de mots comportant cette signature.

Pour effectuer une recherche en utilisant la carte précalculée :

  • Créez la signature pour l'ensemble des lettres dont vous voulez trouver les mots.

  • Ensuite, il faut itérer sur les clés du mappage en recherchant les clés où (key & (~signature) == 0) . Cela vous donne une courte liste de "possibilités" qui ne contiennent aucune lettre ne faisant pas partie du jeu de lettres requis.

  • Iterer sur la liste courte en cherchant les mots avec le mot bon nombre de chacune des lettres requises, en enregistrant le résultat le plus long.


Notes :

  1. Alors que la recherche primaire est en gros O(N) sur le nombre de mots du dictionnaire, le test est extrêmement bon marché.

  2. Cette approche a l'avantage de nécessiter une structure de données en mémoire relativement petite, qui a (très probablement) une bonne localité. Cela est susceptible de favoriser des recherches plus rapides.


Voici une idée pour accélérer la O(N) étape de recherche ci-dessus.

En commençant par la carte de signature ci-dessus, créez (précalculez) des cartes dérivées pour tous les mots qui faire contiennent des paires de lettres spécifiques ; par exemple, une pour les mots contenant AB, pour AC, BC, ... et pour YZ. Ensuite, si vous recherchez des mots contenant (disons) P et Q, vous pouvez simplement scanner la carte dérivée PQ. Cela réduira O(N) étape par étape 26^2 ... au prix d'une augmentation de la mémoire pour les cartes supplémentaires.

Cela peut être étendu à 3 lettres ou plus, mais l'inconvénient est l'explosion de l'utilisation de la mémoire.

Une autre modification possible consiste à orienter (d'une manière ou d'une autre) la sélection de la paire de lettres initiale vers les lettres/paires qui apparaissent peu fréquemment. Mais cela ajoute une charge initiale qui pourrait être plus importante que l'économie (moyenne) réalisée en recherchant une liste plus courte.

3voto

Frerich Raabe Points 23711

Je pense qu'une implémentation basée sur les Trie ne serait pas très efficace en termes d'espace, mais elle serait très bien parallélisée, car vous pourriez descendre dans toutes les branches de l'arbre en parallèle et collecter les nœuds les plus profonds que vous pouvez atteindre depuis chaque branche supérieure avec le jeu de lettres donné. Au final, il suffit de collecter tous les nœuds les plus profonds et de sélectionner le plus long.

Je commencerais par cet algorithme (désolé, ce n'est qu'un pseudo-code), qui ne tente pas de paralléliser mais utilise simplement la bonne vieille récursion (et le retour en arrière) pour trouver la correspondance la plus longue :

TrieNode visitNode( TrieNode n, LetterCollection c )
{
    TreeNode deepestNode = n;
    for each Letter l in c:
        TrieNode childNode = n.getChildFor( l );

        if childNode:
            TreeNode deepestSubNode = visitNode( childNode, c.without( l ) );
            if deepestSubNode.stringLength > deepestNode.stringLength:
                deepestNode = deepestSubNode;
   return deepestNode;
}

C'est-à-dire que cette fonction est censée commencer au nœud racine du tableau, avec toute la collection de lettres donnée. Pour chaque lettre de la collection, on essaie de trouver un noeud enfant. S'il en existe un, on procède à une récursion et on supprime la lettre de la collection. À un moment donné, votre collection de lettres sera vide (dans le meilleur des cas, toutes les lettres sont consommées - vous pouvez vous en sortir immédiatement sans continuer à parcourir le tableau) ou il n'y aura plus d'enfant avec aucune des lettres restantes - dans ce cas, vous supprimez le nœud lui-même, car c'est votre "correspondance la plus longue".

Cela pourrait se paralléliser assez bien si l'on modifiait l'étape de récursion de manière à visiter tous les enfants en parallèle, à collecter les résultats, à sélectionner le résultat le plus long et à le renvoyer.

3voto

Thomas Jungblut Points 11072

Tout d'abord, bonne question. L'examinateur veut voir comment vous abordez le problème. Dans ce genre de problèmes, vous devez analyser le problème et choisir soigneusement une structure de données.

Dans ce cas, deux structures de données me viennent à l'esprit : HashMaps et Tries . HashMaps ne conviennent pas, car vous n'avez pas de clé complète à rechercher (vous pouvez utiliser un index inversé basé sur des cartes, mais vous avez dit avoir déjà trouvé ces solutions). Vous n'avez que les parties - c'est là que la solution Trie est le meilleur ajustement.

Ainsi, l'idée avec les essais est que vous pouvez ignorer les branches de caractères qui ne sont pas dans votre dictionnaire tout en parcourant l'arbre.

Dans votre cas, l'arbre ressemble à ceci (j'ai laissé de côté les ramifications pour les chemins non ramifiés) :

\*
   a
     bacus
   d 
     deltoid
   g
     a
       gaff
     i
       giraffe
   m 
     microphone
   r 
     reef
   q 
     qar

Ainsi, à chaque niveau de ce tri, nous examinons les enfants du nœud actuel et vérifions si le caractère de l'enfant figure dans notre dictionnaire.

Si oui, nous allons plus loin dans cet arbre et supprimons le caractère de l'enfant de notre dictionnaire.

Cela continue jusqu'à ce que vous atteigniez une feuille (plus d'enfants), ici vous savez que ce mot contient tous les caractères de ce dictionnaire. C'est un candidat possible. Maintenant, nous voulons retourner dans l'arbre jusqu'à ce que nous trouvions une autre correspondance que nous pouvons comparer. Si la dernière correspondance trouvée est plus petite, jetez-la, si elle est plus longue, c'est notre meilleur candidat possible.

Un jour, la récusion se terminera et vous obtiendrez le résultat souhaité.

Notez que cela fonctionne s'il n'y a qu'un seul mot le plus long, sinon vous devriez renvoyer une liste de candidats (c'est la partie inconnue de l'entretien où vous devez demander ce que l'interviewer veut voir comme solution).

Vous avez donc eu besoin du code Java, le voici avec un simplissime Trie et la version du mot le plus long :

public class LongestWord {

  class TrieNode {
    char value;
    List<TrieNode> children = new ArrayList<>();
    String word;

    public TrieNode() {
    }

    public TrieNode(char val) {
      this.value = val;
    }

    public void add(char[] array) {
      add(array, 0);
    }

    public void add(char[] array, int offset) {
      for (TrieNode child : children) {
        if (child.value == array[offset]) {
          child.add(array, offset + 1);
          return;
        }
      }
      TrieNode trieNode = new TrieNode(array[offset]);
      children.add(trieNode);
      if (offset < array.length - 1) {
        trieNode.add(array, offset + 1);
      } else {
        trieNode.word = new String(array);
      }
    }    
  }

  private TrieNode root = new TrieNode();

  public LongestWord() {
    List<String> asList = Arrays.asList("abacus", "deltoid", "gaff", "giraffe",
        "microphone", "reef", "qar");
    for (String word : asList) {
      root.add(word.toCharArray());
    }
  }

  public String search(char[] cs) {
    return visit(root, cs);
  }

  public String visit(TrieNode n, char[] allowedCharacters) {
    String bestMatch = null;
    if (n.children.isEmpty()) {
      // base case, leaf of the trie, use as a candidate
      bestMatch = n.word;
    }

    for (TrieNode child : n.children) {
      if (contains(allowedCharacters, child.value)) {
        // remove this child's value and descent into the trie
        String result = visit(child, remove(allowedCharacters, child.value));
        // if the result wasn't null, check length and set
        if (bestMatch == null || result != null
            && bestMatch.length() < result.length()) {
          bestMatch = result;
        }
      }
    }
    // always return the best known match thus far
    return bestMatch;
  }

  private char[] remove(char[] allowedCharacters, char value) {
    char[] newDict = new char[allowedCharacters.length - 1];
    int index = 0;
    for (char x : allowedCharacters) {
      if (x != value) {
        newDict[index++] = x;
      } else {
        // we removed the first hit, now copy the rest
        break;
      }
    }
    System.arraycopy(allowedCharacters, index + 1, newDict, index,
        allowedCharacters.length - (index + 1));

    return newDict;
  }

  private boolean contains(char[] allowedCharacters, char value) {
    for (char x : allowedCharacters) {
      if (value == x) {
        return true;
      }
    }
    return false;
  }

  public static void main(String[] args) {
    LongestWord lw = new LongestWord();
    String longestWord = lw.search(new char[] { 'a', 'e', 'f', 'f', 'g', 'i',
        'r', 'q' });
    // yields giraffe
    System.out.println(longestWord);
  }

}

Je ne peux également que suggérer la lecture du livre Cracking the Coding Interview : 150 questions et solutions sur la programmation Il vous guide dans la prise de décision et la construction de ces algorithmes spécialisés sur les questions d'entretien.

-1voto

Evgeniy Dorofeev Points 52031

Je ferais quelque chose comme ça

public static void main(String[] args) throws Exception {
    Collection<String> words = ...
    char[] chars = { 'a', 'e', 'f', 'f', 'g', 'i', 'r', 'q' };
    String longestWord = "";
    for (String w : words) {
        if (canSpell(w, chars)) {
            if (w.length() > longestWord.length()) {
                longestWord = w;
            }
        }
    }
    System.out.println(longestWord);
}

static boolean canSpell(String w, char[] chars) {
    StringBuilder sb = new StringBuilder(w);
    for (int i = 0; i < chars.length && sb.length() > 0; i++) {
        char c = chars[i];
        for (int j = 0; j < sb.length(); j++) {
            if (sb.charAt(j) == c) {
                sb.deleteCharAt(j);
                break;
            }
        }
    }
    return sb.length() == 0;
}

-1voto

The111 Points 2103

Avertissement : il ne s'agit pas d'une solution trie, mais je pense que c'est une idée qui mérite d'être explorée.

Créez une sorte de fonction de hachage qui ne tient compte que des lettres d'un mot et non de leur ordre (aucune collision ne devrait être possible, sauf dans le cas de permutations). Par exemple, ABCD et DCBA génèrent tous deux le même hachage (mais ABCDD ne le fait pas). Générer une telle table de hachage contenant chaque mot du dictionnaire, en utilisant le chaînage pour lier les collisions (d'un autre côté, à moins que vous n'ayez une exigence stricte de trouver "tous" les mots les plus longs et pas seulement un, vous pouvez simplement laisser tomber les collisions, qui sont juste des permutations, et renoncer à tout le chaînage).

Maintenant, si votre ensemble de recherche est composé de 4 caractères, par exemple A, B, C, D puis, en tant que recherche näive, vous vérifiez les hachages suivants pour voir s'ils sont déjà contenus dans le dictionnaire :

hash(A), hash(B), hash(C), hash(D) // 1-combinations
hash(AB), hash(AC), hash(AD), hash(BC), hash(BD), hash(CD) // 2-combinations
hash(ABC), hash(ABD), hash(ACD), hash(BCD) // 3-combinations
hash(ABCD) // 4-combinations

Si vous recherchez les hachages dans cet ordre, la dernière correspondance trouvée sera la plus longue.

Au final, le temps d'exécution dépend de la longueur de l'ensemble de recherche plutôt que de la longueur du dictionnaire. Si M est le nombre de caractères dans l'ensemble de recherche, alors le nombre de consultations de hachage est la somme M choose 1 + M choose 2 + M choose 3 + ... + M choose M qui est aussi la taille du powerset de l'ensemble de recherche, donc c'est O(2^M) . À première vue, cela semble vraiment mauvais puisque c'est exponentiel, mais pour mettre les choses en perspective, si votre ensemble de recherche est de taille 10, il n'y aura qu'environ 1000 consultations, ce qui est probablement beaucoup plus petit que la taille de votre dictionnaire dans un scénario pratique du monde réel. Avec M = 15, nous obtenons 32 000 consultations, et en réalité, combien de mots anglais ont plus de 15 caractères ?

Il y a cependant deux façons (alternatives) de l'optimiser :

1) Recherchez d'abord les correspondances les plus longues, par exemple les M-combinaisons, puis les (M-1)-combinaisons, etc. Dès que vous trouvez une correspondance, vous pouvez vous arrêter ! Il y a de fortes chances que vous ne couvriez qu'une petite fraction de votre espace de recherche, probablement au pire la moitié.

2) Recherchez d'abord les matchs les plus courts (1-combos, 2-combos, etc.). Supposons que vous rencontriez un échec au niveau 2 (par exemple, aucune chaîne de caractères de votre dictionnaire n'est composée uniquement de A et B ). Utilisez une structure de données auxiliaire (un bitmap peut-être) qui vous permet de vérifier si un mot dans le dictionnaire est même partiellement composé de A et B (contrairement à votre table de hachage primaire qui vérifie que complet composition). Si vous obtenez également un échec sur le bitmap secondaire, vous savez alors que vous pouvez sauter toutes les combinaisons de niveau supérieur, y compris les suivantes A et B (c'est-à-dire que vous pouvez sauter hash(ABC) , hash(ABD) et hash(ABCD) parce qu'aucun mot ne contient les deux A et B ). Cela permet de tirer parti de la Apriori et réduirait considérablement l'espace de recherche au fur et à mesure que M augmente et que les échecs deviennent plus fréquents. EDIT : Je me rends compte que les détails que je laisse de côté concernant la "structure de données auxiliaire" sont importants. En réfléchissant davantage à cette idée, je réalise qu'elle tend vers un balayage complet du dictionnaire en tant que sous-procédure, ce qui va à l'encontre de l'objectif de cette approche. Néanmoins, il semble qu'il devrait y avoir un moyen d'utiliser la fonction Apriori principe ici.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X