Salut
Récemment, j'ai résolu des problèmes dans les archives de
Timus Online Judge et
suis tombé sur une section appelée
tâches de programmation dynamique . Ce type de tâche m'intéresse particulièrement, car souvent cette approche assure la rapidité et l'élégance de la solution. Qu'est-ce que la programmation dynamique?
La programmation dynamique est une approche de résolution de problèmes dans laquelle il existe une division en sous-tâches plus «simples» par rapport à l'original. Le mot "dynamique" a un sens proche de "inductif": on suppose que la réponse est connue pour un certain sens
, et je veux trouver la réponse pour
. En mathématiques, cela s'appelle une transition inductive, et c'est l'idée principale de la programmation dynamique.
Exemples simples
La tâche la plus frappante et la plus indicative est la tâche de calculer
numéro de la séquence de Fibonacci.
Il est connu que la séquence a les propriétés suivantes:
Cela implique immédiatement la formule de récurrence:
int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); }
Si la récursivité recherche un nombre "à partir de la fin", la méthode suivante calcule séquentiellement tous les nombres situés entre
et
:
int dpFibonacci(int n) { int prev1 = 1; int prev2 = 1; int curr = 0; for(int j = 2; j < n; j++) { curr = prev1 + prev2; prev1 = prev2; prev2 = curr; } return curr; }
Il est clair que pour des
cet algorithme fonctionne beaucoup plus rapidement: il ne calcule pas plusieurs fois les valeurs intermédiaires. Prenons un exemple un peu plus complexe.
Exemple 1. Vous marchez sur un escalier à péage. Pour marcher
étape que vous devez payer
pièces de monnaie. Vous pouvez passer à l'étape suivante ou en sauter une. Tâche: passer
étapes et dépenser le moins de pièces possible.
Il est clair qu'en franchissant chaque étape, nous minimisons le nombre de «paiements», mais nous pouvons entrer dans une étape très coûteuse, que nous voudrions éviter. Créer un tableau de valeurs
dans lequel
-le lieu sera le nombre (minimum) de pièces qui doivent être dépensées pour
e étape. Il est immédiatement clair que
. Et puis nous prendrons un minimum des deux étapes précédentes et ajouterons le coût de l'étape elle-même:
Nous modifions un peu les conditions du problème: supposons qu'à certaines étapes vous pouvez obtenir des pièces (cela signifie que
) Que faut-il changer dans l'algorithme pour qu'il donne le bon résultat?
SolutionIl suffit de changer le «début» de notre dynamique. Si le premier escalier ne nous apporte pas de pièces, il est conseillé de sauter par-dessus, cependant, si , il vaut mieux marcher et récupérer vos pièces. Alors .
Prenons un autre exemple qui utilise une dynamique «bidimensionnelle».
Exemple 2. Dans le labyrinthe, il y a
chambres, dont chacune contient de l'or (dans une cage
mensonges
or). La tâche consiste à déterminer quelle quantité maximale d'or peut être collectée avec un itinéraire optimal à partir d'un point
au point
si vous pouvez descendre ou à droite.
Donc, nous voulons connaître le meilleur itinéraire vers la cellule
. Nous pouvons arriver ici à partir de deux cellules -
et
. Étant donné que les routes optimales pour ces deux cellules sont connues (elles sont stockées dans un tableau
), puis la réponse pour la cellule
obtenu comme suit:
Il s'agit d'une autre tâche de programmation dynamique classique, dont les modifications sont assez courantes dans les tâches de programmation sportive. Une tâche similaire est expliquée plus en détail
ici .
Tâches plus difficiles
Si vous le souhaitez, une approche dynamique peut être vissée où vous le souhaitez. Considérez une
tâche des archives du juge en ligne Timus.
La formulation mathématique du problème est la suivante: il faut trouver le nombre minimum de termes nécessaires pour décomposer un nombre donné en carrés entiers.
Comme précédemment, supposons que nous connaissions les réponses pour tous les nombres
qui sont stockés dans un tableau
et nous aimerions trouver
.
Prenez ce numéro
et analyser quelles situations peuvent être:
- est un carré plein. Dans ce cas .
- Peut-être le numéro précédent était un carré complet. Alors .
En général, l'option d'ajouter une unité à la précédente ne semble pas si mauvaise.
On procède comme suit: on cherche une décomposition
tel que
Depuis
- plein carré alors
et
c'est-à-dire que nous avons trouvé une partition qui est simplement meilleure que
, et la réponse dans ce cas sera
Exemple de code Java qui implémente cet algorithme: for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1;
Considérez le
problème suivant. L'objectif est de construire un escalier à partir de
cubes selon les règles:
- l'escalier a au moins deux marches;
- un escalier ne peut pas avoir deux marches identiques;
- les marches de l'escalier vont dans l'ordre croissant (c'est-à-dire que le suivant est plus grand que le précédent).
Cette fois, nous allons construire une dynamique bidimensionnelle. Créer une table
dans lequel la position
le nombre d'escaliers composé de
cubes dont la hauteur ne dépasse pas
. Si cela fonctionne, alors la réponse à notre problème sera la somme
Donc, nous allons résoudre le problème de trouver le nombre d'escaliers composé de
cubes qui sont grands
. La photo montre les escaliers qui tombent dans
:

Puisque nous connaissons tous les escaliers, qui se composent de moins de cubes, nous allons «diviser» les escaliers
colonne de droite. Le résultat est un escalier c
cubes. Exemple pour
:

Mais pour de tels escaliers, le résultat est déjà connu, nous allons donc trier tous ces escaliers avec un cycle
et additionnez tous les résultats. De cette façon
Maintenant, nous allons trier les hauteurs des escaliers:
Enfin, changer
de
avant
nous obtenons la réponse.
Important : dans le processus de construction de notre matrice, il est nécessaire de prendre en compte
, car sinon, certains types d'escaliers seront "perdus" (lorsqu'ils seront "séparés"), mais il va sans dire qu'un tel escalier ne satisfait pas aux conditions du problème, la réponse sera donc le nombre
.
Exemple de code Java qui implémente cet algorithme: dp = new long[n + 1][n+1]; d[1][1] = 1; d[2][1] = 0; d[2][2] = 1; for(int i = 3; i < n + 1; i++) { for(int j = 2; j <i; j++) { long cnt = 0; for(int k = 1; k < j; k++) { cnt += d[i - j][k]; } d[i][j] = cnt; } d[i][i] = 1;
La
tâche suivante est résolue à l'aide d'un tableau unidimensionnel.
Alors ce que nous avons. Le premier ent connaît 2 mots. Chaque ent enseigne tous les mots qu'il connaît lui-même deux ents: jeunes et vieux. À leur tour, les jeunes ont appris autant de mots qu'il le sait déjà, et les vieux n'ont appris qu'un seul mot. Vous devez savoir combien d'Ents savent exactement
mots (il faut déduire le nombre de ces ents modulo
)
La solution est assez simple. Créer un tableau
dans lequel
-th place, nous allons stocker le nombre d'ents (modulo
) qui sait
des mots. Tout commence par le premier ent, qui connaît deux mots, donc
. Et puis tout est simple:
- Tous les élèves qui connaissent un nombre impair de mots sont anciens et ne peuvent apprendre que des précédents. Par conséquent, pour impair
- Quant aux ents qui connaissent un nombre pair de mots, ce sont tous ceux qui ont reçu le même nombre de mots des elfes (jeunes) ceux qui ont appris de la précédente (ancienne); c'est-à-dire, même nous avons .
Reste à traiter du calcul modulo. Afin de ne pas stocker de nombres énormes, nous retiendrons immédiatement toutes les valeurs modulo.
Exemple de code Java qui implémente cet algorithme: int[] d = new int[K + 1]; if(K >= 2) d[2] = 1; if(P != 1) { for(int i = 3; i <= K; i++) { if(i % 2 != 0) { d[i] = d[i - 1]; } else { d[i] = ((d[i/2] % P) + d[i - 1] % P) % P; } } } else d[K] = 0;
Ressources utilisées:
- Juge en ligne Timus;
- Un peu sur la programmation dynamique;
- Propriétés de comparaison modulo.