Anagrammes

En passant par une page de documentation, je me suis attardé sur std::next_permutation. Ça m’a tout de suite donné des idées d’anagrammes. En effet, de pouvoir aussi facilement boucler irréfragablement sur toutes les permutations des lettres d’un mot m’a  suscité l’irrépressible envie de créer un petit détecteur d’anagrammes.

Évidemment, pour détecter un anagramme, il faut d’abord et avant tout avoir une base de données de mots valides pour une langue donnée. Il se trouve que j’ai une telle base de données.  En effet, il y a déjà une vingtaine d’années, j’avais réalisé une petite page web d’aide aux mots-croisés. Pour ce faire j’avais contacté, via un forum, un cruciverbiste-internaute qui avait eu la grande gentillesse de me faire parvenir ce fichier par courriel. C’était un gros fichier à l’époque: presque 3 Mo. En fait, à l’origine c’était même 26 fichiers allant de A.txt à Z.txt. Cruciverbiste-internaute des années ’90 qui se reconnait dans cette histoire, je te remercie encore une fois!

Le corpus en tant que tel date un peu: le mot courriel, par exemple, n’y apparaît même pas. Quoi qu’il en soit, avec ses 280000 entrées composées de noms propres, de substantifs et d’adjectif au singulier comme au  pluriel, de verbes conjugués à toutes les formes, c’est suffisant pour tenter quelques expériences significatives.

En tout cas, c’est amplement suffisant pour tester une première version d’un détecteur d’anagramme. Une version pas très robuste mais qui a le mérite de tester l’utilisation de std::next_permutation.

La première version lit la base de données en insérant dans un std::vector> tous les mots rencontrées. Le vecteur est triée pour permettre une recherche rapide avec std::binary_search

Une fonction ressemblant à ça pourrait par exemple faire l’affaire (j’assume un compilateur C++11 ou plus qui garantit que les objets comme la base de données ne sont pas retournés par copie sur la pile mais bien en utilisant le mécanisme de Copy Elision).

typedef vector<std::string> AnagramDB ;

AnagramDB GetAnagramDB(const std::string& filename) 
{
	std::vector<std::string> words;
	std::ifstream datafile(filename);

    std::string str;
    while (datafile >> str )
	{
		words.push_back(str);
    }

    std::sort(words.begin(), words.end());
	return words;
}
Lecture de la base de données et insertion dans un vecteur trié

Avec un vecteur ainsi trié, on peut rechercher un mot avec grosso-modo la même efficacité que dans un arbre binaire. En imaginant qu’on boucle sur les différentes permutations des lettres d’un mot, on voudra tester pour chaque combinaison si le mot est présent  ou pas dans la base de données. Quelque chose comme ça.

bool isInDatabase(const AnagramDB& database, const std::string& str)
{
	return std::binary_search(database.cbegin(), database.cend(), str);
}
Utilisation de binary_search pour trouver un mot dans une liste triée

Une fois qu’on a une base de données chargée et un mécanisme pour tester si un mot (une combinaison de lettres) est valide (donc présent dans le base de données), il ne reste plus qu’à obtenir un mot à tester, boucler sur toutes ses permutations et vérifier pour chaque anagramme si il correspond à un mot valide dans la langue française (ici représentée par notre fichier de base de données).

On pourrait par exemple lire en boucle sur stdin pour obtenir des mots, qu’on testerait un à la fois. Pour chacun des mots, la liste des anagrammes valides serait envoyée sur stdout, et ainsi de suite. On remarque que pour initier les permutations, le mot est tout d’abord trié en ordre lexicographique, de manière à commencer par le « premier » anagramme.

Ça ressemblerait donc à ça.

int main(int argc, char** argv)
{
	if(argc<=1) 
		return 0;
	std::string str = argv[1];
    auto db = GetAnagramDB(str);
	std::cout << "Word to look for: "; 
	while(std::cin >> str ) 
	{
		std::sort(str.begin(), str.end());
		int num = 0;
		while(std::next_permutation(str.begin(), str.end()) )
		{
			if( isInDatabase(str) ) 
			{
				std::cout << str << std::endl;; 
				num++;
			}
		}
			
		std::cout << "Found : " << num << std::endl; 
		std::cout << "Word to look for: "; 
	 }

	return 0;
}
Boucle naïve pour la recherche d'anagrammes valides basé sur std::next_permutation

Cette boucle et ce programme fonctionne parfaitement comme il se doit. Par contre il souffre de quelques faiblesses:

  • Le nombre de permutations est N! ; pour un mot de 10 lettres, on a déjà plus de 3,5 millions de combinaisons à tester (3628800)
  • Les casses minuscule et majuscules sont considérées comme étant des lettres différentes
  • Les accents et autres diacritiques génèrent des lettres différentes

Dans le prochain épisode sur les anagrammes, on améliorera tout ça en repensant la base de données et en évitant les multiples permutations. On s’affranchira aussi des problèmes de casse et de diacritiques.

Comment enlever les accents des caractères en C++

Souvent lorsqu’on manipule du texte, on cherche à retrouver la forme de base des caractères, c’est-à-dire sans les signes diacritiques. Ce peut être utile pour générer une liste en ordre alphabétique de mots par exemple (on notera que dans l’ordre lexicographique des dictionnaires français, le E, et le É ont la même valeur).

Cet exercice peut être assez complexe car fortement dépendant de l’encodage des caractères (UTF-8, ASCII étendu, etc.) et de la définition donnée à « forme de base« . En effet, d’une langue à l’autre, cette notion peut varier et les diacritiques dans certaines langues donne une lettre à part entière. Par exemple en espagnol, la lettre ñ est une lettre à part qui est classée après le n.

Le remplacement des accents dans une application multi-langues est une tâche complexe et pour se faire, le mieux est d’être aidé par des bibliothèques comme ICU.

En revanche, pour tous les cas relativement fréquents où les caractères sont simplement codés sur un octet en utilisant l’ASCII étendu avec l’encodage ISO-Latin-1, on pourrait utiliser le petit bout de code suivant (inspiré d’une réponse sur Stack Overflow).

char RemoveAccent(char ch) 
{
	static const std::map<char, char> tr =
	{
		{ 'À', 'A' },
		{ 'Á', 'A' },
		{ 'Â', 'A' },
		{ 'Ã', 'A' },
		{ 'Ä', 'A' },
		{ 'Å', 'A' },
		
		{ 'Æ', 'E' },

		{ 'Ç', 'C' },

		{ 'È', 'E' },
		{ 'É', 'E' },
		{ 'Ê', 'E' },
		{ 'Ë', 'E' },

		{ 'Ì', 'I' },
		{ 'Í', 'I' },
		{ 'Î', 'I' },
		{ 'Ï', 'I' },

		{ 'Ð', 'D' },

		{ 'Ñ', 'N' },

		{ 'Ò', 'O' },
		{ 'Ó', 'O' },
		{ 'Ô', 'O' },
		{ 'Õ', 'O' },
		{ 'Ö', 'O' },

		{ '×', 'x' },

		{ 'Ø', '0' },

		{ 'Ù', 'U' },
		{ 'Ú', 'U' },
		{ 'Û', 'U' },
		{ 'Ü', 'U' },

		{ 'Ý', 'Y' },
		{ 'Þ', 'P' },
		{ 'ß', 'S' },

		{ 'à', 'a' },
		{ 'á', 'a' },
		{ 'â', 'a' },
		{ 'ã', 'a' },
		{ 'ä', 'a' },
		{ 'å', 'a' },

		{ 'æ', 'e' },

		{ 'ç', 'c' },

		{ 'è', 'e' },
		{ 'é', 'e' },
		{ 'ê', 'e' },
		{ 'ë', 'e' },

		{ 'ì', 'i' },
		{ 'í', 'i' },
		{ 'î', 'i' },
		{ 'ï', 'i' },

		{ 'ð', 'd' },

		{ 'ñ', 'n' },

		{ 'ò', 'o' },
		{ 'ó', 'o' },
		{ 'ô', 'o' },
		{ 'õ', 'o' },
		{ 'ö', 'o' },

		{ '÷', '/' },

		{ 'ø', '0' },

		{ 'ù', 'u' },
		{ 'ú', 'u' },
		{ 'û', 'u' },
		{ 'ü', 'u' },

		{ 'ý', 'y' },
		{ 'þ', 'p' },
		{ 'ß', 's' }
	};
	
	auto it = tr.find(ch);
	if (it != tr.end())
		return it->second;
	return ch;
}
RemoveAccent()