Dans les concours de programmation, le modèle suivant se produit dans de nombreuses tâches:
Étant donné les nombres A et B qui sont énormes (peut-être 20 chiffres décimaux ou plus), déterminez le nombre d'entiers X avec A ≤ X ≤ B qui ont une certaine propriété P
SPOJ a beaucoup de tâches comme celles-ci pour la pratique.
Voici des exemples de propriétés intéressantes:
Je sais que si nous définissons f (Y) comme le nombre de tels entiers X ≤ Y, alors la réponse à notre question est f (B) - f (A - 1) . Le problème réduit est de savoir comment calculer la fonction f efficacement. Dans certains cas, nous pouvons utiliser certaines propriétés mathématiques pour arriver à une formule, mais souvent les propriétés sont plus compliquées et nous n'avons pas assez de temps pour cela dans un concours.
Existe-t-il une approche plus générale qui fonctionne dans de nombreux cas? Et peut-il également être utilisé pour énumérer les nombres avec la propriété donnée ou calculer une agrégation sur eux?
Une variante de ceci consiste à trouver le nombre k-ème avec une propriété donnée, qui peut bien sûr être résolu en utilisant la recherche binaire avec une fonction de comptage.
En effet, il existe une approche de ce schéma qui s'avère fonctionner assez souvent. Il peut également être utilisé pour énumérer tous les X avec la propriété donnée, à condition que leur nombre soit raisonnablement petit. Vous pouvez même l'utiliser pour agréger un opérateur associatif sur tout le X avec la propriété donnée, par exemple pour trouver leur somme.
Pour comprendre l'idée générale, essayons de formuler la condition X ≤ Y en termes de représentations décimales de X et Y.
Disons que nous avons X = x1 X2 ... Xn - 1 Xn et Y = y1 y2 ... yn - 1 yn, où xje et yje sont les chiffres décimaux de X et Y. Si les nombres ont une longueur différente, nous pouvons toujours ajouter zéro chiffre au début du plus court.
Définissons leftmost_lo
Comme le plus petit i avec xje <yje. Nous définissons leftmost_lo
Comme n + 1 s'il n'y en a pas i. De façon analogue, nous définissons leftmost_hi
Comme le plus petit i avec xje > yje, ou n + 1 sinon.
Maintenant, X ≤ Y est vrai si et exactement si leftmost_lo <= leftmost_hi
. Avec cette observation, il devient possible d'appliquer une approche programmation dynamique au problème, qui "fixe" les chiffres de X les uns après les autres. Je vais le démontrer avec vos exemples de problèmes:
Calculez le nombre f(Y) d'entiers X avec la propriété X ≤ Y et X a la somme des chiffres 60
Soit n
le nombre de chiffres de Y et y[i]
Le i - ème chiffre décimal de Y selon la définition ci-dessus. L'algorithme récursif suivant résout le problème:
count(i, sum_so_far, leftmost_lo, leftmost_hi):
if i == n + 1:
# base case of the recursion, we have recursed beyond the last digit
# now we check whether the number X we built is a valid solution
if sum_so_far == 60 and leftmost_lo <= leftmost_hi:
return 1
else:
return 0
result = 0
# we need to decide which digit to use for x[i]
for d := 0 to 9
leftmost_lo' = leftmost_lo
leftmost_hi' = leftmost_hi
if d < y[i] and i < leftmost_lo': leftmost_lo' = i
if d > y[i] and i < leftmost_hi': leftmost_hi' = i
result += count(i + 1, sum_so_far + d, leftmost_lo', leftmost_hi')
return result
Nous avons maintenant f(Y) = count(1, 0, n + 1, n + 1)
et nous avons résolu le problème. Nous pouvons ajouter mémorisation à la fonction pour la rendre rapide. Le runtime est O (n4) pour cette implémentation particulière. En fait, nous pouvons intelligemment optimiser l'idée pour en faire O (n). Ceci est laissé comme exercice au lecteur (Astuce: vous pouvez compresser les informations stockées dans leftmost_lo
Et leftmost_hi
En un seul bit et vous pouvez tailler si sum_so_far > 60
). La solution se trouve à la fin de ce post.
Si vous regardez attentivement, sum_so_far
Voici juste un exemple de fonction arbitraire calculant une valeur à partir de la séquence de chiffres de X. Il pourrait s'agir de la fonction any qui peut être calculée chiffre par chiffre et produit un résultat suffisamment petit. Cela pourrait être le produit de chiffres, un masque de bits de l'ensemble de chiffres qui remplissent une certaine propriété ou beaucoup d'autres choses.
Il pourrait également s'agir simplement d'une fonction qui renvoie 1 ou 0, selon que le nombre est uniquement composé des chiffres 4 et 7, ce qui résout trivialement le deuxième exemple. Nous devons être un peu prudents ici parce que nous sommes autorisés à avoir des zéros non significatifs au début, nous devons donc effectuer un bit supplémentaire à travers les appels de fonction récursifs nous indiquant si nous sommes toujours autorisés à utilisez zéro comme chiffre.
Calculez le nombre f(Y) d'entiers X avec la propriété X ≤ Y et X est palindromique
Celui-ci est légèrement plus dur. Nous devons être prudents avec les zéros non significatifs: le point miroir d'un nombre palindromique dépend du nombre de zéros non significatifs que nous avons, nous devons donc garder une trace du nombre de zéros non significatifs.
Il existe cependant une astuce pour le simplifier un peu: si nous pouvons compter le f (Y) avec la restriction supplémentaire que tous les nombres X doivent avoir le même nombre de chiffres que Y, alors nous pouvons résoudre le problème d'origine également, en itérant sur tous les nombres de chiffres possibles et en additionnant les résultats.
Nous pouvons donc simplement supposer que nous n'avons pas de zéros non significatifs:
count(i, leftmost_lo, leftmost_hi):
if i == ceil(n/2) + 1: # we stop after we have placed one half of the number
if leftmost_lo <= leftmost_hi:
return 1
else:
return 0
result = 0
start = (i == 1) ? 1 : 0 # no leading zero, remember?
for d := start to 9
leftmost_lo' = leftmost_lo
leftmost_hi' = leftmost_hi
# digit n - i + 1 is the mirrored place of index i, so we place both at
# the same time here
if d < y[i] and i < leftmost_lo': leftmost_lo' = i
if d < y[n-i+1] and n-i+1 < leftmost_lo': leftmost_lo' = n-i+1
if d > y[i] and i < leftmost_hi': leftmost_hi' = i
if d > y[n-i+1] and n-i+1 < leftmost_hi': leftmost_hi' = n-i+1
result += count(i + 1, leftmost_lo', leftmost_hi')
return result
Le résultat sera à nouveau f(Y) = count(1, n + 1, n + 1)
.
MISE À JOUR: Si nous ne voulons pas seulement compter les nombres, mais peut-être les énumérer ou calculer à partir d'eux une fonction d'agrégation qui n'expose pas la structure du groupe, nous devons également appliquer la borne inférieure sur X pendant la récursivité. Cela ajoute quelques paramètres supplémentaires.
MISE À JOUR 2: O(n) Solution pour l'exemple "chiffre somme 60":
Dans cette application, nous plaçons les chiffres de gauche à droite. Puisque nous voulons seulement savoir si leftmost_lo < leftmost_hi
Est vrai, ajoutons un nouveau paramètre lo
. lo
est vrai si leftmost_lo < i
et faux sinon. Si lo
est vrai, nous pouvons utiliser n'importe quel chiffre pour la position i
. S'il est faux, nous ne pouvons utiliser que les chiffres 0 à Y [i], car tout chiffre plus grand entraînerait leftmost_hi = i < leftmost_lo
Et ne peut donc pas conduire à une solution. Code:
def f(i, sum_so_far, lo):
if i == n + 1: return sum_so_far == 60
if sum_so_far > 60: return 0
res = 0
for d := 0 to (lo ? 9 : y[i]):
res += f(i + 1, sum + d, lo || d < y[i])
return res
On peut dire que cette façon de voir les choses est un peu plus simple, mais aussi un peu moins explicite que l'approche leftmost_lo
/leftmost_hi
. Il ne fonctionne pas non plus immédiatement pour des scénarios un peu plus compliqués comme le problème du palindrome (bien qu'il puisse également y être utilisé).