L'objectif de ce problème est de partitionner un tableau d'entiers donné en le nombre maximal de sous-tableaux contigus. Pour chaque sous-tableau résultant, nous calculons sa valeur MEX (Minimum Excluded value), qui est le plus petit entier non négatif absent de cet ensemble. Le but est de construire un nouveau tableau contenant ces valeurs MEX, et de maximiser sa longueur.
La stratégie principale consiste à identifier le MEX du tableau entier initial. Ce MEX (appelons-le mex\_cible\_global) sera la valeur que nous essayerons d'atteindre pour le premier sous-tableau. Nous parcourons le tableau, accumulant des éléments dans un sous-tableau temporaire. Au fur et à mesure que nous ajoutons des éléments, nous mettons à jour le MEX du sous-tableau courant (mex\_prefixe\_actuel) et surveillons également un candidat pour le prochain mex\_cible\_global.
Le candidat pour le prochain mex\_cible\_global (prochain\_mex\_possible) est mis à jour chaque fois que nous traitons un élément arr\[i\]. Si arr\[i\] est inférieur à l'actuel mex\_cible\_global et qu'il s'agit de sa dernière occurrence dans la partie restante du tableau, alors arr\[i\] devient un candidat potentiel pour le mex\_cible\_global du prochain segment. Cela est dû au fait que la consommation de arr\[i\] rendra ce nombre absent du reste du tableau, faisant potentiellement de lui le plus petit entier manquant.
Lorsqu'un sous-tableau temporaire atteint un mex\_prefixe\_actuel égal au mex\_cible\_global, nous avons trouvé un segment valide. Nous ajoutons alors ce mex\_cible\_global à notre liste de résultats, réinitialisons les compteurs pour le sous-tableau temporaire, et mettons à jour le mex\_cible\_global pour le segment suivant en utilisant notre prochain\_mex\_possible accumulé. Le prochain\_mex\_possible est ensuite réinitialisé à cette nouvelle valeur de mex\_cible\_global.
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
const int VALEUR_MAXIMALE_POSSIBLE = 300005; // Maximum value for elements + 1
void resoudreProblemeMEX() {
int n;
std::cin >> n;
std::vector<int> tableau_entree(n);
std::vector<int> compteurs_valeurs_restantes(VALEUR_MAXIMALE_POSSIBLE, 0); // Compteurs pour les valeurs dans la partie restante du tableau
for (int i = 0; i < n; ++i) {
std::cin >> tableau_entree[i];
compteurs_valeurs_restantes[tableau_entree[i]]++;
}
// Déterminer le MEX initial global de l'ensemble du tableau
int mex_cible_global = 0;
while (mex_cible_global < VALEUR_MAXIMALE_POSSIBLE && compteurs_valeurs_restantes[mex_cible_global] > 0) {
mex_cible_global++;
}
int mex_prefixe_actuel = 0; // MEX du segment actuellement accumulé
int prochain_mex_possible_cible = mex_cible_global; // Candidat pour le MEX cible du *prochain* segment
std::vector<int> compteurs_segment_actuel(VALEUR_MAXIMALE_POSSIBLE, 0); // Compteurs pour les valeurs dans le segment actuel
std::vector<int> liste_resultats_mex;
for (int i = 0; i < n; ++i) {
compteurs_segment_actuel[tableau_entree[i]]++;
compteurs_valeurs_restantes[tableau_entree[i]]--; // Cet élément fait maintenant partie du segment actuel
// Mettre à jour le mex_prefixe_actuel basé sur les compteurs_segment_actuel
while (mex_prefixe_actuel < VALEUR_MAXIMALE_POSSIBLE && compteurs_segment_actuel[mex_prefixe_actuel] > 0) {
mex_prefixe_actuel++;
}
// Si tableau_entree[i] est un élément critique pour le mex_cible_global (c'est-à-dire sa dernière occurrence)
// et il est plus petit que le mex_cible_global actuel, il devient un candidat pour le prochain MEX cible.
if (tableau_entree[i] < mex_cible_global && compteurs_valeurs_restantes[tableau_entree[i]] == 0) {
prochain_mex_possible_cible = std::min(prochain_mex_possible_cible, tableau_entree[i]);
}
// Si le MEX du segment actuel correspond au mex_cible_global, nous avons trouvé un segment valide
if (mex_prefixe_actuel == mex_cible_global) {
liste_resultats_mex.push_back(mex_cible_global);
// Réinitialiser les compteurs pour le segment suivant
std::fill(compteurs_segment_actuel.begin(), compteurs_segment_actuel.end(), 0); // Effacer efficacement
mex_prefixe_actuel = 0; // Réinitialiser le MEX du préfixe
// Le MEX cible pour le *prochain* segment est déterminé
mex_cible_global = prochain_mex_possible_cible;
// Et réinitialiser le candidat pour le segment d'après
prochain_mex_possible_cible = mex_cible_global;
}
}
std::cout << liste_resultats_mex.size() << std::endl;
for (size_t i = 0; i < liste_resultats_mex.size(); ++i) {
std::cout << liste_resultats_mex[i] << (i == liste_resultats_mex.size() - 1 ? "" : " ");
}
std::cout << std::endl;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
resoudreProblemeMEX();
}
return 0;
}
Minimisation des Exposants pour Séquences Croissantes
Ce problème nous demande de trouver une séquence d'exposants non négatifs, k\_1, k\_2, ..., k\_n, pour un tableau a donné, de sorte que la nouvelle séquence b\_i = a\_i \* 2^{k\_i} soit non décroissante (c'est-à-dire b\_1 ≤ b\_2 ≤ ... ≤ b\_n). La condition initiale est k\_1 = 0. Notre objectif est de minimiser la somme totale des exposants, ∑ k\_i.
Pour chaque élément a\_i (à partir de i=2), nous devons choisir k\_i de manière à ce que a\_i \* 2^{k\_i} ≥ a\_{i-1} \* 2^{k\_{i-1}}. Nous voulons minimiser k\_i tout en satisfaisant cette condition. La variable exposant\_courant\_necessaire maintient la valeur de k\_{i-1} (l'exposant choisi pour l'élément précédent).
- Si
a\_{i-1} > a\_i: L'élément précédent (brut) est plus grand que l'élément actuel (brut). Pour maintenir la conditino non décroissante, nous devons augmenterk\_i. Nous calculons combien de foisa\_idoit être doublé pour être au moins aussi grand quea\_{i-1}. Ce nombre de doublages est ajouté àexposant\_courant\_necessaire. La nouvelle valeur deexposant\_courant\_necessaireserak\_i. - Si
a\_{i-1} ≤ a\_i: L'élément actuel (brut) est déjà plus grand ou égal à l'élément précédent (brut). Cela signifie que nous pouvons potentiellement réduirek\_i. Nous calculons combien de foisa\_{i-1}peut être doublé et rester inférieur ou égal àa\_i. Ce nombre de doublages est soustrait deexposant\_courant\_necessaire. Nous nous assurons queexposant\_courant\_necessairene devienne jamais négatif, car les exposants doivent être non négatifs. La nouvelle valeur deexposant\_courant\_necessaireserak\_i.
La somme totale des exposants est mise à jour avec la valeur finale de exposant\_courant\_necessaire (qui représente k\_i pour l'élément a\_i) à chaque itération.
#include <iostream>
#include <vector>
#include <algorithm>
void resoudreOptimisationSequence() {
int n;
std::cin >> n;
std::vector<long long> valeurs(n);
for (int i = 0; i < n; ++i) {
std::cin >> valeurs[i];
}
long long somme_exposants_totale = 0;
int exposant_courant_necessaire = 0; // Représente k_i pour valeurs[i]
// Le problème exige k_1 = 0. Nous commençons donc le traitement à partir du deuxième élément (index 1).
// exposant_courant_necessaire contient conceptuellement k_{i-1} lorsque l'on considère valeurs[i].
for (int i = 1; i < n; ++i) {
long long valeur_precedente_brute = valeurs[i-1];
long long valeur_courante_brute = valeurs[i];
if (valeur_precedente_brute > valeur_courante_brute) {
// Si la valeur précédente (non modifiée) est supérieure à la valeur actuelle (non modifiée),
// nous devons augmenter exposant_courant_necessaire pour satisfaire la condition non décroissante :
// valeur_courante_brute * 2^k_i >= valeur_precedente_brute * 2^k_{i-1}
// Avec exposant_courant_necessaire actuel comme k_{i-1}.
// Nous avons besoin de valeur_courante_brute * 2^delta_k >= valeur_precedente_brute.
int doublages_requis = 0;
long long temp_valeur_courante = valeur_courante_brute;
while (temp_valeur_courante < valeur_precedente_brute) {
temp_valeur_courante *= 2;
doublages_requis++;
}
exposant_courant_necessaire += doublages_requis;
} else { // valeur_precedente_brute <= valeur_courante_brute
// Si la valeur actuelle est déjà supérieure ou égale, nous pouvons essayer de diminuer exposant_courant_necessaire.
// Nous voulons valeur_courante_brute * 2^k_i >= valeur_precedente_brute * 2^k_{i-1}
// Et minimiser k_i.
int reductions_possibles = 0;
long long temp_valeur_precedente = valeur_precedente_brute;
// Tant que la valeur précédente (potentiellment modifiée par 2) est <= la valeur courante
// cela signifie que nous avons une marge pour réduire l'exposant actuel.
while (temp_valeur_precedente * 2 <= valeur_courante_brute) {
temp_valeur_precedente *= 2;
reductions_possibles++;
}
exposant_courant_necessaire -= reductions_possibles;
exposant_courant_necessaire = std::max(0, exposant_courant_necessaire); // k_i doit être non négatif
}
somme_exposants_totale += exposant_courant_necessaire;
}
std::cout << somme_exposants_totale << std::endl;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
resoudreOptimisationSequence();
}
return 0;
}
Découverte des Nœuds Maximaux Atteignables dans un Graphe
Pour un graphe dirigé donné, ce problème demande de trouver, pour chaque nœud u, le nœud v de plus grand identifiant qui peut être atteint depuis u via un chemin dirigé. Un algorithme de parcours en profondeur (DFS) sur le graphe inversé, combiné à un balayage des nœuds par identifiant décroissant, permet de résoudre ce problème efficacement.
Nous construisons un graphe inversé où chaque arête u -> v du graphe original est représentée par une arête v -> u. Cela signifie que la liste d'adjacence inversée adj\_inverse\[v\] contiendra tous les nœuds u pour lesquels il existe une arête u -> v dans le graphe original.
L'algorithme procède comme suit :
- Initialisez un tableau
max\_atteignable\[p\]à 0 pour tous les nœudsp, qui stockera le plus grand identifiant de nœud atteignable depuisp. - Parcourez les nœuds de
N(le plus grand identifiant) jusqu'à1(le plus petit identifiant). Soitile nœud courant de ce parcours. - Si
max\_atteignable\[i\]est encore 0, cela signifie que le nœudin'a pas encore été marqué comme atteignable par un nœud d'identifiant supérieur. Dans ce cas, nous lançons un DFS depuisi, avecicomme candidat pour le plus grand identifiant atteignable. - La fonction
dfs(noeud\_actuel, candidat\_max\_id)fonctionne ainsi :- Si
max\_atteignable\[noeud\_actuel\]n'est pas 0 (déjà visité et marqué par un ID plus grand ou égal), la fonction se termine. - Sinon,
max\_atteignable\[noeud\_actuel\] = candidat\_max\_id. Cela signifie que lenoeud\_actuelpeut atteindrecandidat\_max\_id(carcandidat\_max\_idest à l'origine du DFS ou un nœud sur le chemin). - Pour chaque prédécesseur
pdenoeud\_actuel(c'est-à-dire chaque voisin denoeud\_actueldans le graphe inversé), appelez récursivementdfs(p, candidat\_max\_id). L'idée est que sinoeud\_actuelpeut atteindrecandidat\_max\_id, et queppeut atteindrenoeud\_actueldans le graphe original, alorsppeut aussi atteinrdecandidat\_max\_id.
- Si
Grâce au parcours des nœuds de N à 1, la première fois qu'un nœud est visité, il est marqué avec le plus grand identifiant de nœud qu'il peut atteindre. Les visites ultérieures par des identifiants plus petits seront ignorées.
#include <iostream>
#include <vector>
#include <algorithm>
const int NOMBRE_MAX_NOEUDS = 200005;
// Liste d'adjacence inversée : adj_inverse[v] contient tous les u tels que u -> v dans le graphe original.
std::vector<std::vector<int>> liste_adj_inverse(NOMBRE_MAX_NOEUDS);
// Stocke le plus grand identifiant de nœud atteignable depuis chaque nœud.
std::vector<int> valeur_max_atteignable_noeud(NOMBRE_MAX_NOEUDS, 0);
void explorerNoeudsAtteignablesDFS(int noeud_actuel, int candidat_max_id) {
if (valeur_max_atteignable_noeud[noeud_actuel] != 0) { // Si déjà visité et marqué par un ID supérieur ou égal
return;
}
valeur_max_atteignable_noeud[noeud_actuel] = candidat_max_id; // Marquer noeud_actuel comme atteignable par candidat_max_id
// Parcourir les prédécesseurs de noeud_actuel dans le graphe original
// (qui sont les voisins de noeud_actuel dans le graphe inversé)
for (int predecesseur : liste_adj_inverse[noeud_actuel]) {
explorerNoeudsAtteignablesDFS(predecesseur, candidat_max_id);
}
}
void resoudreAtteignabiliteGraphe() {
int nb_noeuds, nb_arretes;
std::cin >> nb_noeuds >> nb_arretes;
// Réinitialisation des structures pour un cas de test unique (si plusieurs, cela serait dans la boucle principale)
for(int i = 1; i <= nb_noeuds; ++i) {
liste_adj_inverse[i].clear();
valeur_max_atteignable_noeud[i] = 0;
}
for (int i = 0; i < nb_arretes; ++i) {
int u, v;
std::cin >> u >> v;
// Arête de u vers v. Dans le graphe inversé, cela signifie que v a une connexion depuis u.
liste_adj_inverse[v].push_back(u);
}
// Parcourir les nœuds de nb_noeuds jusqu'à 1.
// Cela garantit que lorsqu'un nœud est visité pour la première fois,
// il est marqué avec l'identifiant de nœud atteignable le plus grand possible.
for (int i = nb_noeuds; i >= 1; --i) {
// Si valeur_max_atteignable_noeud[i] est 0, cela signifie que 'i' n'a pas encore été marqué
// par un nœud plus grand. Donc, 'i' lui-même est le nœud le plus grand atteignable pour
// certains chemins qui se terminent à 'i'.
if (valeur_max_atteignable_noeud[i] == 0) {
explorerNoeudsAtteignablesDFS(i, i);
}
}
for (int i = 1; i <= nb_noeuds; ++i) {
std::cout << valeur_max_atteignable_noeud[i] << (i == nb_noeuds ? "" : " ");
}
std::cout << std::endl;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// Le problème sur Luogu (P3916) implique un seul cas de test.
resoudreAtteignabiliteGraphe();
return 0;
}