Techniques algorithmiques : sommes前缀es, tableaux de différences et méthode des deux pointeurs

  1. Sommes前缀es =============

1.1 Principe fondamental

La somme前缀e constitue une technique permettant de mémoriser le cumul des éléments précédents dans une structure de données. Cette approche offre une complexité temporelle constante O(1) pour récupérer la somme de n'importe quel intervalle donné.

Tableau unidimensionnel

Pour calculer la somme des éléments de l'indice 1 jusqu'à li'ndice i, on procède de manière itérative : lorsque l'algorithme atteint la position i, les positions 1 à i-1 ont déjà été traitées. La formule devient donc : table[i] = table[i] + table[i-1].

Pour obtenir la somme d'un intervalle [l, r], on utilise simplement : table[r] - table[l-1].

Tableau bidimensionnel

Pour une matrice, la somme前缀e permet de calculer rapidement la somme de n'importe quel sous-rectangle. En trattant la position (i, j), les lignes précédentes i-1 et les colonnes précédentes j-1 sont déjà calculées. La formule de construction est : table[i][j] = table[i][j] + table[i-1][j] + table[i][j-1] - table[i-1][j-1].

Pour un rectangle défini par les coins supérieur gauche (x1, y1) et inférieur droit (x2, y2), la somme s'obtient par : table[x2][y2] - table[x2][y1-1] - table[x1-1][y2] + table[x1-1][y1-1].

1.2 Applications pratiques

Problème 795 : Somme d'un intervalle

Implémentation de référence :

void calculerSomme(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> prefix[i];
        prefix[i] += prefix[i-1];
    }
    while(m--) {
        int gauche, droite;
        cin >> gauche >> droite;
        cout << prefix[droite] - prefix[gauche - 1] << endl;
    }
}

Problème 796 : Somme d'une sous-matrice

Implémentation de référence :

void calculerMatrice(){
    int n, m, requetes;
    cin >> n >> m >> requetes;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= m; j++) {
            cin >> matrice[i][j];
            matrice[i][j] += matrice[i-1][j] + matrice[i][j-1] - matrice[i-1][j-1];
        }
    }
    while(requetes--) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << matrice[x2][y2] - matrice[x2][y1-1] - matrice[x1-1][y2] + matrice[x1-1][y1-1] << endl;
    }
}

  1. Tableaux de différences ==========================

2.1 Principe fondamental

Le tableau de différences stocke l'écart entre chaque élément et son prédécesseur. Cette structure permet d'effectuer des opérations d'addition ou de soustraction sur des intervalles complets avec une complexité O(1). Une propriété fondamentale réside dans le fait que la somme前缀e du tableau de différences redonne le tableau original.

Différence unidimensionnelle

Pour construire le tableau de différences, on calcule : diff[i] = original[i] - original[i-1].

Pour ajouter une valeur c à tous les éléments de l'intervalle [l, r], il suffit de modifier : diff[l] += c et diff[r+1] -= c.

Différence bidimensionnelle

Pour un rectangle défini par les coins (x1, y1) et (x2, y2), l'ajout de la valeur c s'effectue en quatre opérations :

  • diff[x1][y1] += c
  • diff[x1][y2+1] -= c
  • diff[x2+1][y1] -= c
  • diff[x2+1][y2+1] += c

2.2 Applications pratiques

Problème 797 : Différences

Implémentation de référence :

void resoudreProbleme(){
    for(int i = 1; i <= n; i++) {
        cin >> valeurs[i];
    }
    for(int i = 1; i <= n; i++) {
        diff[i] = valeurs[i] - valeurs[i-1];
    }
    while(operations--) {
        int debut, fin, valeur;
        cin >> debut >> fin >> valeur;
        diff[debut] += valeur;
        diff[fin + 1] -= valeur;
    }
    for(int i = 1, accumulateur = 0; i <= n; i++) {
        accumulateur += diff[i];
        cout << accumulateur << ' ';
    }
}

Problème 798 : Matrice de différences

Implémentation de référence :

void ajouterRectangle(int x1, int y1, int x2, int y2, int valeur) {
    diff[x1][y1] += valeur;
    diff[x1][y2 + 1] -= valeur;
    diff[x2 + 1][y1] -= valeur;
    diff[x2 + 1][y2 + 1] += valeur;
}

void resoudreProbleme(){
    for(int i = 1; i <= lignes; i++) {
        for(int j = 1; j <= colonnes; j++) {
            cin >>donnees[i][j];
            int x1 = i, x2 = i, y1 = j, y2 = j;
            int val = donnees[i][j];
            ajouterRectangle(x1, y1, x2, y2, val);
        }
    }
    while(requetes--) {
        int x1, y1, x2, y2, val;
        cin >> x1 >> y1 >> x2 >> y2 >> val;
        ajouterRectangle(x1, y1, x2, y2, val);
    }
    for(int i = 1; i <= lignes; i++) {
        for(int j = 1; j <= colonnes; j++) {
            donnees[i][j] = diff[i][j] + donnees[i-1][j] + donnees[i][j-1] - donnees[i-1][j-1];
            cout << donnees[i][j] << ' ';
        }
        cout << endl;
    }
}

  1. Méthode des deux pointeurs =============================

3.1 Principe fondamental

La technique des deux pointeurs exploite les propriétés intrinsèques des séquences ordonnées pour optimiser les performances algorithmiques. Un pointeur secondaire permet de délimiter la plage de recherche valide, réduisant ainsi significativement la complexité temporelle.

Cette approche se divise en deux catégories principales :

  1. Sur une seule séquence : Deux pointeurs délimitent une fenêtre glissante.
  2. Sur deux séquences : Maintien d'un ordre spécifique entre les éléments.

Modèle générique (inspiré d'AcWing) :

void resoudre(){
    for(int i = 0, j = 0; i < taille; i++){
        while(j < i && conditionVerification(i, j)) {
            j++;
        }
        // Logique spécifique au problème
    }
}

3.2 Applications pratiques

Problème 799 : Sous-séquence continue sans doublons

Approche : Utiliser un tableau de comptage pour suivre les occurrences dans la fenêtre [j, i]. Lorsque l'élément à la position i existe déjà dans la fenêtre, déplacer le pointeur j vers l'avant jusqu'à élimination du doublon.

Implémentation de référence :

void trouverPlusLongue(){
    int resultat = 0;
    for(int i = 1, j = 1; i <= nombre; i++){
        if(presence[elements[i]] > 0){
            while(presence[elements[i]] > 0) {
                presence[elements[j]]--;
                j++;
            }
        }
        presence[elements[i]]++;
        resultat = max(i - j + 1, resultat);
    }
    cout << resultat << endl;
}

Problème 800 : Cible numérique dans un tableau

Approche : Puisque les tableaux sont triés par ordre croissant, si a[i] + b[j] > cible, alors toute valeur avec un indice plus grand dans le second tableau dépassera également la cible. Le pointeur j peut donc reculer de manière monotone.

Implémentation de référence :

void rechercherCible(){
    for(int i = 0, j = taille2 - 1; i < taille1 && j >= 0; i++){
        while(i < taille1 && j >= 0 && tableau1[i] + tableau2[j] > valeurCible) {
            j--;
        }
        if(tableau1[i] + tableau2[j] == valeurCible){
            cout << i << ' ' << j << endl;
            return;
        }
    }
}

Problème 799 : Vérification de sous-séquence

Approche : Vérifier si une chaîne secondaire apparaît dans une chaîne principale dans le même ordre. Parcourir simultanément les deux chaînes en recherchant chaque caractère.

Implémentation de référence :

void verifierSequence(){
    int i = 0, j = 0;
    while(i < longueurPrincipale && j < longueurSecondaire){
        while(j < longueurSecondaire && principale[i] != secondaire[j]) {
            j++;
        }
        if(j == longueurSecondaire) break;
        i++;
        j++;
    }
    if(i == longueurPrincipale) {
        cout << "Oui" << endl;
    } else {
        cout << "Non" << endl;
    }
}

Étiquettes: algorithmique structures de données tableaux sommes préfixes differences

Publié le 2 août à 13h28