Analyse de Problèmes de Programmation Compétitive : Combinatoire, Arbres et Segments

Problème 1 : Sommes de Sous-ensembles (Collection)

L'objectif est de calculer le produit de toutes les sommes possibles de sous-ensembles, chacune élevée à la puissance de sa fréquence d'apparition. Le cœur du problème repose sur un sac à dos (knapsack) pour compter ces occurrences.

Soit $f[j]$ le nombre de façons d'obtenir une somme $j$. En raison du Petit Théorème de Fermat, lors du calcul du produit modulaire $ans = \prod j^{f[j]} \pmod M$, l'exposant $f[j]$ doit être calculé modulo $(M-1)$.

#include <iostream>
#include <vector>

using namespace std;

const int MODULO = 998244353;
const int PHI_MOD = MODULO - 1;

long long rapide_exp(long long base, int exp) {
    long long res = 1;
    base %= MODULO;
    while (exp > 0) {
        if (exp % 2 == 1) res = (res * base) % MODULO;
        base = (base * base) % MODULO;
        exp /= 2;
    }
    return res;
}

int main() {
    int n;
    if (!(cin >> n)) return 0;

    int max_sum = n * (n + 1) / 2;
    vector<int> dp_frequence(max_sum + 1, 0);
    dp_frequence[0] = 1;

    for (int i = 1; i <= n; ++i) {
        for (int j = (i * (i + 1) / 2); j >= i; --j) {
            dp_frequence[j] = (dp_frequence[j] + dp_frequence[j - i]) % PHI_MOD;
        }
    }

    long long resultat_final = 1;
    for (int j = 1; j <= max_sum; ++j) {
        if (dp_frequence[j] > 0) {
            resultat_final = (resultat_final * rapide_exp(j, dp_frequence[j])) % MODULO;
        }
    }

    cout << resultat_final << endl;
    return 0;
}

Problème 2 : Gestion de Placement et Segment Tree (Lantern)

Le problème revient à vérifier si une configuration de placement respecte certaines contraintes de densité. Mathématiquement, la condition de validité est équivalente à vérifier que la somme maximale d'un suffixe (basée sur l'écart entre les demandes et la capacité $k$) ne dépasse pas un seuil $d \cdot k$.

On utilise un arbre de segments pour maintenir la "somme maximale de sous-segment" de manière dynamique, permettant des mises à jour ponctuellse et des requêtes globales en $O(\log n)$.

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

struct SegmentNode {
    long long total_sum;
    long long max_prefix;
    long long max_suffix;
    long long max_sub;
};

int N_size, M_queries, D_dist;
long long K_cap;
vector<SegmentNode> tree;

void update_node(int p) {
    tree[p].total_sum = tree[2 * p].total_sum + tree[2 * p + 1].total_sum;
    tree[p].max_prefix = max(tree[2 * p].max_prefix, tree[2 * p].total_sum + tree[2 * p + 1].max_prefix);
    tree[p].max_suffix = max(tree[2 * p + 1].max_suffix, tree[2 * p + 1].total_sum + tree[2 * p].max_suffix);
    tree[p].max_sub = max({tree[2 * p].max_sub, tree[2 * p + 1].max_sub, tree[2 * p].max_suffix + tree[2 * p + 1].max_prefix});
}

void build_tree(int l, int r, int p) {
    if (l == r) {
        tree[p] = {-K_cap, 0, 0, 0};
        return;
    }
    int mid = (l + r) / 2;
    build_tree(l, mid, 2 * p);
    build_tree(mid + 1, r, 2 * p + 1);
    update_node(p);
}

void modify(int l, int r, int p, int idx, int val) {
    if (l == r) {
        tree[p].total_sum += val;
        long long v = max(0LL, tree[p].total_sum);
        tree[p].max_prefix = tree[p].max_suffix = tree[p].max_sub = v;
        return;
    }
    int mid = (l + r) / 2;
    if (idx <= mid) modify(l, mid, 2 * p, idx, val);
    else modify(mid + 1, r, 2 * p + 1, idx, val);
    update_node(p);
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> N_size >> M_queries >> K_cap >> D_dist;
    tree.resize(4 * N_size + 1);
    build_tree(1, N_size, 1);

    while (M_queries--) {
        int pos, val;
        cin >> pos >> val;
        modify(1, N_size, 1, pos, val);
        cout << (tree[1].max_sub <= (long long)D_dist * K_cap ? "YES" : "NO") << "\n";
    }
    return 0;
}

Problème 3 : DP sur Arbre et Contraintes (Connection)

On cherche à trouver la composante connexe de valeur maximale dans un arbre, sous réserve de certaines contraintes d'exclusion entre nœuds. Soit $f[i][j]$ la valeur maximale d'une composante enracinée en $i$ dont le dernier nœud dans l'ordre DFS est $j$.

L'optimisation consiste à remarquer que le nombre de nœuds impliqués dans des contraintes explicites est faible ($2m$). On peut donc remapper ces nœuds pour réduire la complexité de la DP à $O(nm)$, où $m$ est le nombre de contraintes.

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

using namespace std;

const long long INF = 1e16;
int n, m, mapped_id[100005], id_counter;
long long weights[100005], dp[100005][45], global_max;
bool conflict[45][45];
vector<int> adj[100005];

void solve_tree(int u, int p) {
    dp[u][mapped_id[u]] = weights[u];
    for (int v : adj[u]) {
        if (v == p) continue;
        solve_tree(v, u);
        for (int j = 0; j <= id_counter; ++j) {
            long long best_prev = -INF;
            for (int k = 0; k <= id_counter; ++k) {
                if (!conflict[k][mapped_id[v]]) best_prev = max(best_prev, dp[u][k]);
            }
            dp[u][j] = max(dp[u][j], best_prev + dp[v][j]);
        }
    }
    for (int j = 0; j <= id_counter; ++j) global_max = max(global_max, dp[u][j]);
}

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> weights[i];
    for (int i = 1; i <= n; ++i) {
        int count; cin >> count;
        while (count--) { int neighbor; cin >> neighbor; adj[i].push_back(neighbor); }
    }
    for (int i = 0; i < m; ++i) {
        int u, v; cin >> u >> v;
        if (!mapped_id[u]) mapped_id[u] = ++id_counter;
        if (!mapped_id[v]) mapped_id[v] = ++id_counter;
        conflict[mapped_id[u]][mapped_id[v]] = conflict[mapped_id[v]][mapped_id[u]] = true;
    }
    for (int i = 0; i <= n; ++i) for (int j = 0; j < 45; ++j) dp[i][j] = -INF;
    solve_tree(1, 0);
    cout << global_max << endl;
    return 0;
}

Problème 4 : Combinatoire et État de Jeu (Checkers)

Le nombre de configurations valides pour une séquence de pions peut être modélisé par une formule combinatoire $\binom{n-a-b}{a}$, où $a$ est le nombre de paires de pions consécutifs et $b$ le nombre de pions isolés.

Pour gérer les points d'interrogation ('?'), on utilise une DP pour compter le nombre de façons d'obtenir chaque paire $(a, b)$. L'état de la DP inclut la position actuelle, les compteurs $a$ et $b$, et l'état du dernier pion placé (0: vide, 1: pion isolé, 2: partie d'une paire).

#include <iostream>
#include <vector>
#include <string>

using namespace std;

const int MOD = 1e9 + 7;
long long memo[2][255][255][3];
long long fact[505], invFact[505];

long long power(long long base, long long exp) {
    long long res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp % 2 == 1) res = (res * base) % MOD;
        base = (base * base) % MOD;
        exp /= 2;
    }
    return res;
}

long long nCr(int n, int r) {
    if (r < 0 || r > n) return 0;
    return fact[n] * invFact[r] % MOD * invFact[n - r] % MOD;
}

int main() {
    int n; cin >> n;
    string s; cin >> s;
    s = " " + s;

    fact[0] = 1;
    for (int i = 1; i <= n; i++) fact[i] = (fact[i - 1] * i) % MOD;
    invFact[n] = power(fact[n], MOD - 2);
    for (int i = n - 1; i >= 0; i--) invFact[i] = (invFact[i + 1] * (i + 1)) % MOD;

    memo[0][0][0][0] = 1;
    for (int i = 1; i <= n; i++) {
        int cur = i & 1, prev = (i - 1) & 1;
        for (int x = 0; x <= n / 2; x++) {
            for (int y = 0; y <= n; y++) {
                for (int k = 0; k < 3; k++) memo[cur][x][y][k] = 0;
                
                if (s[i] != '1') {
                    memo[cur][x][y][0] = (memo[prev][x][y][0] + memo[prev][x][y][1] + memo[prev][x][y][2]) % MOD;
                }
                if (s[i] != '0') {
                    if (y > 0) memo[cur][x][y][1] = (memo[prev][x][y - 1][0] + memo[prev][x][y - 1][2]) % MOD;
                    if (x > 0 && y < n) memo[cur][x][y][2] = memo[prev][x - 1][y + 1][1];
                }
            }
        }
    }

    long long total_ans = 0;
    for (int x = 0; x <= n / 2; x++) {
        for (int y = 0; y <= n; y++) {
            long long ways = (memo[n & 1][x][y][0] + memo[n & 1][x][y][1] + memo[n & 1][x][y][2]) % MOD;
            if (ways > 0) {
                total_ans = (total_ans + nCr(n - x - y, x) * ways) % MOD;
            }
        }
    }
    cout << total_ans << endl;
    return 0;
}

Étiquettes: Programmation-Dynamique segment-tree combinatoire algorithmes-de-graphes C++

Publié le 19 juillet à 11h03