Principes fondamentaux de la Digit DP
La technique dite « Digit DP » repose essentiellement sur une recherche en profondeur avec mémoïsation. Elle permet de résoudre des problèmes combinatoires ou arithmétiques sur un intervalle de nombres entiers en traitant chaque chiffre individuellement, généralement du plus significatif au moins significatif.
Gestion des contraintes d'état
Pour construire la solution récursive, il est crucial de définir correctement l'espace d'état. Deux paramètres supplémentaires sont souvent nécessaires pour gérer les bornes supérieures et les zéros non significatifs :
- Contrainte de borne (
is_limit) : Un booléen indiquant si le préfixe construit jusqu'à présent est exactement égal à la borne supérieure cible. Siis_limitest vrai, le chiffre actuel ne peut pas dépasser le chiffre correspondant dans la borne supérieure ; sinon, il peut prendre toute valeur entre 0 et 9. - Zéros non significatifs (
has_leading_zero) : Un indicateur pour savoir si nous sommes encore dans la partie initiale remplie de zéros. Cela est important lorsque la définition du problème dépend de la présence effective de chiffres (par exemple, compter les occurrences d'un chiffre spécifique).
Transitions d'état
Lors de la transition vers le niveau suivant de récursion :
- La nouvelle contrainte de borne devient vraie uniquement si elle était vraie auparavant ET que le chiffre choisi est égal à la limite actuelle.
- L'indicateur de zéros non significatifs devient faux dès qu'un chiffre non nul est sélectionné.
Cas d'aplication 1 : Comptage d'occurrences de chiffres
Problème type (ex: P2602 ZJOI2010) : Étant donné deux entiers $a$ et $b$, déterminer combien de fois chaque chiffre de 0 à 9 apparaît dans tous les nombres entiers compris dans l'intervalle $[a, b]$.
Stratégie : Nous définissons une fonction auxiliaire qui calcule le nombre d'occurrences d'un chiffre $d$ pour tous les nombres inférieurs ou égaux à une borne $x$. Le résultat final pour l'intervalle $[a, b]$ est obtenu par la soustraction : $Count(b) - Count(a-1)$.
Dans la fonction de recherche profonde (dfs), nous parcourons les positions de gauche à droite. L'état mémorisé inclut la position actuelle, le nombre d'occurrences trouvées jusqu'ici, ainsi que les flags de contrainte et de zéro initial.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// Tableau pour stocker les chiffres de la borne supérieure (stockés inversés ou selon l'implémentation)
ll digits[20];
int len_digits;
// Tableau de mémoïsation : dp[pos][count_so_far][limit_flag][leading_zero_flag]
// Note : La dimension 'count_so_far' dépend du problème. Ici, on passe la somme accumulée comme paramètre.
ll memo[20][20][2][2];
/**
* @param pos Position actuelle (index de la position restante)
* @param is_limit Vrai si le préfixe est contraint par la borne supérieure
* @param has_zero Vrai si aucun chiffre non nul n'a été placé depuis le début (zéros non significatifs actifs)
* @param current_count Nombre d'occurrences du chiffre cible trouvé jusqu'ici
* @param target_digit Le chiffre (0-9) que nous cherchons à compter
*/
ll solve_pos(int pos, bool is_limit, bool has_zero, ll current_count, int target_digit) {
// Cas de base : toutes les positions ont été traitées
if (pos == 0) {
return current_count;
}
// Vérification de la mémoïsation
// Attention : On ne mémoïse que si ce n'est pas une situation "libre" (limit=0)
// ou si la structure le permet. Ici, on utilise l'index complet.
if (memo[pos][current_count][is_limit][has_zero] != -1) {
return memo[pos][current_count][is_limit][has_zero];
}
ll result = 0;
// Déterminer la borne maximale pour le chiffre actuel
int upper_bound = is_limit ? digits[pos] : 9;
for (int digit = 0; digit <= upper_bound; ++digit) {
// Mise à jour des états pour la prochaine récursion
// Nouvelle contrainte de borne : reste vraie seulement si on était limité
// et qu'on choisit exactement la valeur limite
bool next_is_limit = is_limit && (digit == upper_bound);
// Mise à jour du flag de zéro non significatif :
// Il devient faux (0) si on place un chiffre non nul OU si on avait déjà placé un chiffre non nul avant
bool next_has_zero = has_zero || (digit != 0);
// Calcul de l'increment du compteur d'occurrences
// On compte le chiffre s'il est égal au target ET si ce n'est pas un zéro non significatif
// (Sauf si le target est 0 lui-même, mais la logique standard ignore les zeros de tête pour le count)
ll new_count = current_count;
if (!has_zero && digit == target_digit) {
new_count += 1;
}
// Cas particulier pour le chiffre 0 : si has_zero est vrai, on ne compte pas le 0 comme occurrence valide
// sauf si le nombre entier est 0 (géré souvent par des cas spécifiques hors boucle ou ajustement).
// Dans cette implémentation simplifiée, on suppose que les zéros de tête ne comptent pas.
result += solve_pos(pos - 1, next_is_limit, next_has_zero, new_count, target_digit);
}
// Sauvegarde du résultat
memo[pos][current_count][is_limit][has_zero] = result;
return result;
}
// Fonction principale pour calculer le nombre d'occurrences de 'target' dans [0, x]
ll count_occurrences(ll x, int target) {
if (x < 0) return 0;
// Extraction des chiffres de x
len_digits = 0;
while (x > 0) {
digits[++len_digits] = x % 10;
x /= 10;
}
// Initialisation de la table de mémoïsation
memset(memo, -1, sizeof(memo));
// Démarrage de la DFS depuis la position la plus haute
// is_limit commence à vrai, has_zero commence à vrai (on est au début)
return solve_pos(len_digits, true, true, 0, target);
}
int main() {
ll a, b;
cin >> a >> b;
for (int d = 0; d <= 9; ++d) {
// Résultat pour l'intervalle [a, b] = Count(b) - Count(a-1)
cout << count_occurrences(b, d) - count_occurrences(a - 1, d) << " ";
}
cout << endl;
return 0;
}
Cas d'application 2 : Somme des bits set pour les multiples
Problème type : Étant donnés $k$ et $b$, trouver la somme totale du nombre de bits à 1 dans la représentation binaire de tous les entiers dans l'intervalle $[0, 2^b - 1]$ qui sont divisibles par $k$. Le résultat doit être modulo $10^9 + 9$.
Stratégie : Au lieu de travailler en base 10, nous travaillons en base 2. Les positions correspondent aux bits. L'état doit maintenant suivre le reste de la division euclidienne par $k$ pour vérifier la divisibilité à la fin.
Les dimensions de l'état sont :
bit_index: La position actuelle du bit (du plus significatif au moins significatif).ones_count: Le nombre de bits à 1 accumulés jusqu'à présent (pour calculer la somme finale).remainder: La valeur actuelle du nombre préfixé modulo $k$.
Note importante : Comme nous voulons la somme des nombres de bits, la fonction de retour ne doit pas simplement renvoyer un booléen (divisible ou non), mais accumuler les contributions. Une approche courante consiste à renvoyer une paire ou deux valeurs : le nombre de solutions valides et la somme des bits dans ces solutions. Cependant, pour simplifier ici, nous pouvons intégrer le comptage des 1 directement dans la propagation si nous modifions légèrement la logique, ou plus communément, utiliser une DP où l'état contient le nombre de 1s vus et le reste, et à la base, si le reste est 0, on ajoute le nombre de 1s.
Dans l'exemple ci-dessous, nous utilisons une approche où num_ones fait partie de l'état pour permettre la mémoïsation correcte de la contribution future.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 9;
int k_val, max_bits;
// memo[bit_pos][current_ones][current_remainder]
// Valeur retournée : Somme des nombres de bits à 1 pour toutes les complétions possibles
// à partir de cet état qui résultent en un multiple de k.
// Pour que cela fonctionne correctement avec la somme, il faut souvent retourner
// {count_valid_numbers, sum_of_ones_in_valid_numbers}.
// Simplification pour l'exemple : On suppose que la question demande la somme totale.
// Une implémentation rigoureuse pour "Somme des popcounts" nécessite souvent de propager
// le poids des bits restants.
// Alternative plus simple pour ce type de problème Codeforces/Gym :
// On itère sur les bits. Si on met un 1, le reste change.
// À la fin, si reste==0, on ajoute le nombre total de 1s du chemin.
ll dp[130][130][1010];
// Cette fonction renvoie la SOMME DES POPCOUNTS de tous les suffixes valides.
// Il est complexe de faire ceci avec une seule valeur sans struct.
// Approche alternative courante : Renvoyer le nombre de chemins valides et la somme séparément.
// Ici, pour rester fidèle à la structure "un seul retour", nous allons calculer
// la contribution incrémentale.
// NOTE : L'exemple original fourni dans la prompt utilisait une logique spécifique.
// Réécriture pour clarté et correction algorithmique typique pour ce problème :
struct Result {
ll count; // Nombre de nombres valides formés
ll sum_ones; // Somme des popcounts de ces nombres valides
};
Result dfs_bit(int pos, int ones_so_far, int rem_so_far) {
if (pos == 0) {
if (rem_so_far == 0) {
return {1, ones_so_far};
} else {
return {0, 0};
}
}
// Si la mémoïsation est utilisée pour le pair (count, sum), on a besoin d'une structure.
// Pour simplifier le code présenté, nous allons utiliser une approche où l'on
// reconstruit la somme à partir des sous-problèmes.
// État : pos bits restants à traiter.
// ones_so_far : nombre de 1 déjà placés dans le préfixe.
// rem_so_far : valeur du préfixe modulo k.
Result res = {0, 0};
// Option 1 : Placer un 0 à la position actuelle (bit le plus significatif restant)
// Le nouveau reste est (rem_so_far * 2 + 0) % k
// Le nombre de 1 ne change pas pour le préfixe, mais affectera la somme finale via le suffixe.
// Option 2 : Placer un 1 à la position actuelle
// Le nouveau reste est (rem_so_far * 2 + 1) % k
// Le nombre de 1 augmente de 1.
// Appeler récursivement pour le bit suivant (pos - 1)
// Attention : La gestion de la somme des popcounts totaux est délicate.
// Si un suffixe donne C nombres valides avec une somme de bits internes S_internes,
// alors le nombre total de bits pour ces C nombres est S_internes + C * (ones_so_far + bit_actuel).
// Calcul pour bit = 0
int new_rem_0 = (rem_so_far * 2 + 0) % k_val;
Result sub_0 = dfs_bit(pos - 1, ones_so_far, new_rem_0);
// Calcul pour bit = 1
int new_rem_1 = (rem_so_far * 2 + 1) % k_val;
Result sub_1 = dfs_bit(pos - 1, ones_so_far + 1, new_rem_1);
// Agrégation des résultats
// Pour sub_0 : Les nombres valides ont 'ones_so_far' 1s dans le préfixe.
// Leur contribution totale au popcount global est : sub_0.sum_ones + sub_0.count * ones_so_far
// Pour sub_1 : Les nombres valides ont 'ones_so_far + 1' 1s dans le préfixe.
// Leur contribution totale au popcount global est : sub_1.sum_ones + sub_1.count * (ones_so_far + 1)
res.count = (sub_0.count + sub_1.count) % MOD;
res.sum_ones = (sub_0.sum_ones + sub_0.count * ones_so_far +
sub_1.sum_ones + sub_1.count * (ones_so_far + 1)) % MOD;
return res;
}
// Version simplifiée utilisant tableau statique comme dans la prompt originale,
// mais adaptée pour éviter la confusion sur le retour unique.
// La prompt originale semblait mélanger concepts. Voici une implémentation robuste.
ll n_k, n_b;
ll f[130][130][1010]; // Non utilisé dans la version Struct, mais conservé pour compatibilité si besoin
// Utilisons une mémoïsation sur le résultat structuré est plus propre en C++ moderne.
// Réimplémentation directe pour correspondre à la logique "Somme des 1s"
// Si l'on veut strictement coller au style de la requête (tableau ll),
// il faut souvent séparer la DP en deux tableaux : dp_count et dp_sum.
ll dp_count[130][130][1010];
ll dp_sum[130][130][1010];
bool vis[130][130][1010];
void init_memo() {
memset(vis, 0, sizeof(vis));
}
pair<ll> solve_dp(int pos, int ones, int rem) {
if (pos == 0) {
if (rem == 0) return {1, ones};
return {0, 0};
}
if (vis[pos][ones][rem]) {
return {dp_count[pos][ones][rem], dp_sum[pos][ones][rem]};
}
vis[pos][ones][rem] = true;
ll cnt_total = 0;
ll sum_total = 0;
// Bit 0
int rem_next_0 = (rem * 2) % k_val;
auto res_0 = solve_dp(pos - 1, ones, rem_next_0);
cnt_total += res_0.first;
// Contribution des 1s existants dans le préfixe à tous les nombres valides du suffixe
sum_total += res_0.second + (res_0.first * ones) % MOD;
// Bit 1
int rem_next_1 = (rem * 2 + 1) % k_val;
auto res_1 = solve_dp(pos - 1, ones + 1, rem_next_1);
cnt_total += res_1.first;
sum_total += res_1.second + (res_1.first * (ones + 1)) % MOD;
dp_count[pos][ones][rem] = cnt_total % MOD;
dp_sum[pos][ones][rem] = sum_total % MOD;
return {dp_count[pos][ones][rem], dp_sum[pos][ones][rem]};
}
int main() {
cin >> k_val >> n_b;
init_memo();
// On traite n_b bits.
// Début : pos = n_b, ones = 0, rem = 0
auto final_result = solve_dp(n_b, 0, 0);
cout << final_result.second << endl;
return 0;
}
</ll>