La nuit dernière, j'ai essayé de résoudre défi #15 du Projet Euler :
En commençant dans le coin supérieur gauche d'une 2×2, il y a 6 routes (sans retour en arrière) retour en arrière) jusqu'au coin inférieur droit droite.
(source : <a href="http://projecteuler.net/project/images/p_015.gif" rel="nofollow noreferrer">projecteuler.net </a>)Combien de routes y a-t-il à travers une grille 20×20 ?
Je me suis dit que ça ne devait pas être si difficile, alors j'ai écrit une fonction récursive de base :
const int gridSize = 20;
// call with progress(0, 0)
static int progress(int x, int y)
{
int i = 0;
if (x < gridSize)
i += progress(x + 1, y);
if (y < gridSize)
i += progress(x, y + 1);
if (x == gridSize && y == gridSize)
return 1;
return i;
}
J'ai vérifié qu'il fonctionnait pour des grilles plus petites comme 2×2 ou 3×3, puis je l'ai configuré pour qu'il fonctionne sur une grille 20×20. Imaginez ma surprise lorsque, 5 heures plus tard, le programme était toujours en train de calculer les chiffres, et n'avait terminé qu'à 80 % (d'après l'examen de sa position/route actuelle dans la grille).
Il est clair que je m'y prends de la mauvaise façon. Comment résoudriez-vous ce problème ? Je pense qu'il devrait être résolu en utilisant une équation plutôt qu'une méthode comme la mienne, mais ce n'est malheureusement pas mon point fort.
Mise à jour :
J'ai maintenant une version qui fonctionne. En gros, elle met en cache les résultats obtenus auparavant lorsqu'il reste un bloc n×m à parcourir. Voici le code ainsi que quelques commentaires :
// the size of our grid
static int gridSize = 20;
// the amount of paths available for a "NxM" block, e.g. "2x2" => 4
static Dictionary<string, long> pathsByBlock = new Dictionary<string, long>();
// calculate the surface of the block to the finish line
static long calcsurface(long x, long y)
{
return (gridSize - x) * (gridSize - y);
}
// call using progress (0, 0)
static long progress(long x, long y)
{
// first calculate the surface of the block remaining
long surface = calcsurface(x, y);
long i = 0;
// zero surface means only 1 path remains
// (we either go only right, or only down)
if (surface == 0)
return 1;
// create a textual representation of the remaining
// block, for use in the dictionary
string block = (gridSize - x) + "x" + (gridSize - y);
// if a same block has not been processed before
if (!pathsByBlock.ContainsKey(block))
{
// calculate it in the right direction
if (x < gridSize)
i += progress(x + 1, y);
// and in the down direction
if (y < gridSize)
i += progress(x, y + 1);
// and cache the result!
pathsByBlock[block] = i;
}
// self-explanatory :)
return pathsByBlock[block];
}
En l'appelant 20 fois, pour des grilles de taille 1×1 à 20×20, on obtient le résultat suivant :
There are 2 paths in a 1 sized grid
0,0110006 seconds
There are 6 paths in a 2 sized grid
0,0030002 seconds
There are 20 paths in a 3 sized grid
0 seconds
There are 70 paths in a 4 sized grid
0 seconds
There are 252 paths in a 5 sized grid
0 seconds
There are 924 paths in a 6 sized grid
0 seconds
There are 3432 paths in a 7 sized grid
0 seconds
There are 12870 paths in a 8 sized grid
0,001 seconds
There are 48620 paths in a 9 sized grid
0,0010001 seconds
There are 184756 paths in a 10 sized grid
0,001 seconds
There are 705432 paths in a 11 sized grid
0 seconds
There are 2704156 paths in a 12 sized grid
0 seconds
There are 10400600 paths in a 13 sized grid
0,001 seconds
There are 40116600 paths in a 14 sized grid
0 seconds
There are 155117520 paths in a 15 sized grid
0 seconds
There are 601080390 paths in a 16 sized grid
0,0010001 seconds
There are 2333606220 paths in a 17 sized grid
0,001 seconds
There are 9075135300 paths in a 18 sized grid
0,001 seconds
There are 35345263800 paths in a 19 sized grid
0,001 seconds
There are 137846528820 paths in a 20 sized grid
0,0010001 seconds
0,0390022 seconds in total
J'accepte la réponse de danben, car c'est lui qui m'a le plus aidé à trouver cette solution. Mais upvotes aussi à Tim Goodman et Agos :)
Mise à jour du bonus :
Après avoir lu la réponse d'Eric Lippert, j'y ai jeté un nouveau coup d'œil et l'ai quelque peu réécrite. L'idée de base est toujours la même, mais la partie "cache" a été supprimée et placée dans une fonction distincte, comme dans l'exemple d'Eric. Le résultat est un code beaucoup plus élégant.
// the size of our grid
const int gridSize = 20;
// magic.
static Func<A1, A2, R> Memoize<A1, A2, R>(this Func<A1, A2, R> f)
{
// Return a function which is f with caching.
var dictionary = new Dictionary<string, R>();
return (A1 a1, A2 a2) =>
{
R r;
string key = a1 + "x" + a2;
if (!dictionary.TryGetValue(key, out r))
{
// not in cache yet
r = f(a1, a2);
dictionary.Add(key, r);
}
return r;
};
}
// calculate the surface of the block to the finish line
static long calcsurface(long x, long y)
{
return (gridSize - x) * (gridSize - y);
}
// call using progress (0, 0)
static Func<long, long, long> progress = ((Func<long, long, long>)((long x, long y) =>
{
// first calculate the surface of the block remaining
long surface = calcsurface(x, y);
long i = 0;
// zero surface means only 1 path remains
// (we either go only right, or only down)
if (surface == 0)
return 1;
// calculate it in the right direction
if (x < gridSize)
i += progress(x + 1, y);
// and in the down direction
if (y < gridSize)
i += progress(x, y + 1);
// self-explanatory :)
return i;
})).Memoize();
Au fait, je n'ai pas trouvé de meilleure façon d'utiliser les deux arguments comme clé pour le dictionnaire. J'ai fait quelques recherches sur Internet, et il semble que ce soit une solution courante. Mais bon.
12 votes
Vous pouvez le résoudre avec les mathématiques discrètes plutôt qu'avec la programmation si vous le souhaitez. Spoilers à venir. Sur le carré 2x2, il faut 4 pas pour atteindre la fin. Donc sur le carré 20x20, il faut 40 pas. Vous pouvez utiliser la formule n choisi r pour le résoudre : n!/r !(n-r) ! r = le nombre de rangées qui est 20, et n est 2r qui est 40. On peut la simplifier en 40!/20!^2. Si quelqu'un est intéressé, jetez un coup d'œil ici : fr.wikipedia.org/wiki/Binomial_coefficient
0 votes
@RBarryYoung : D'après l'exemple donné, il semble que "revenir en arrière" signifie toute soustraction de la coordonnée x ou y. Sans l'exemple, j'aurais pensé que cela signifiait seulement aller directement sur votre chemin. C'est-à-dire que j'aurais supposé
x++, y++, x--, y++, x++, x++
était une solution valable, mais le fait qu'ils n'incluent pas de solutions comme celle-ci dit le contraire.0 votes
@Tim Goodman : Donc je suppose aussi, mais je déteste faire des suppositions comme ça.
1 votes
Il existe plusieurs façons d'utiliser deux arguments pour un dictionnaire. Le problème avec votre approche est que tous les types ne font pas un "aller-retour" vers la chaîne de caractères. En fait, la plupart des types renvoient simplement leur nom de type comme représentation de la chaîne de caractères. Deux bonnes techniques : (1) vous pouvez utiliser un "tuple" pour retenir deux éléments, puis l'utiliser comme clé du dictionnaire. (2) faire un Dictionnaire<K1, Dictionnaire<K2, R>>. C'est-à-dire que le dictionnaire externe a pour clé la première clé. Il renvoie un dictionnaire juste pour cette première clé, qui a pour clé la deuxième clé. La deuxième technique a tendance à être plus lente.
0 votes
Bonne idée de mettre en cache la longueur des chemins.
0 votes
Un grand bravo pour le fait que tu aies écrit toutes les réponses. J'obtenais des nombres corrects jusqu'à 15, puis mes réponses étaient incorrectes.
int
et ça débordait. On pourrait penser qu'à ce jour, j'aurais réalisé qu'il fallait juste utiliserlong
dans tous ces problèmes...