Técnicas Avançadas de Otimização em Programação Dinâmica

Aceleração por Matrizes

A otimização via matrizes é fundamental para resolver problemas de recorrência linear em tempo logarítmico. Abaixo, uma implementação base para multiplicação e exponenciação de matrizes quadradas.

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

const int DIM = 3;

struct MatrixContainer {
    long long mat[DIM][DIM];

    MatrixContainer() {
        std::memset(mat, 0, sizeof(mat));
    }

    MatrixContainer operator*(const MatrixContainer& other) const {
        MatrixContainer result;
        for (int i = 0; i < DIM; ++i) {
            for (int k = 0; k < DIM; ++k) {
                if (mat[i][k] == 0) continue;
                for (int j = 0; j < DIM; ++j) {
                    result.mat[i][j] += mat[i][k] * other.mat[k][j];
                }
            }
        }
        return result;
    }
};

void execute_demo() {
    MatrixContainer m1, m2;
    m1.mat[0][0] = 1; 
    m2.mat[0][0] = 1;
    MatrixContainer res = m1 * m2;
    std::cout << res.mat[0][0] << std::endl;
}

Otimização com Estruturas de Dados

Muitas vezes, a transição de um estado de DP depende de consultas de intervalo ou busca por valores específicos. O uso de Árvores de Fenwick (Binary Indexed Trees) pode reduzir a complexidade de uma dimensão inteira.

Exemplo: Subsequências de Tamanho K (CF597C)

O objetivo é contar subsequências de comprimento fixo. A árvore de Fenwick elimina a necessidade de iterar sobre todos os elementos anteriores para verificar a condição de ordem.

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

using namespace std;

const int MAX_VAL = 100005;
const int MAX_LEN = 15;

long long bit_table[MAX_LEN][MAX_VAL];
int limit_n;

void update_bit(int len, int idx, long long val) {
    for (; idx <= limit_n; idx += idx & -idx)
        bit_table[len][idx] += val;
}

long long query_bit(int len, int idx) {
    long long sum = 0;
    for (; idx > 0; idx -= idx & -idx)
        sum += bit_table[len][idx];
    return sum;
}

int main() {
    int n, k;
    if (!(cin >> n >> k)) return 0;
    limit_n = n;
    k++; 

    for (int i = 0; i < n; ++i) {
        int x;
        cin >> x;
        update_bit(1, x, 1);
        for (int j = 2; j <= k; ++j) {
            long long prev_count = query_bit(j - 1, x - 1);
            update_bit(j, x, prev_count);
        }
    }

    cout << query_bit(k, limit_n) << endl;
    return 0;
}

Otimização de Inclinação (Slope Optimization)

Esta técnica é aplicada quando a transição da DP possui a forma $dp[i] = \min \{ dp[j] + f(i, j) \}$, onde $f(i, j)$ pode ser linearizada. O problema é transformado na manutenção de um invólucro convexo (convex hull).

Exemplo: Toy Packing (HNOI2008)

A equação de custo pode ser reescrita na forma $y = mx + c$. Mantemos um conjunto de pontos e utilizaoms uma fila monótona para encontrar o ponto que minimiza o intercepto.

#include <iostream>
#include <vector>

using namespace std;

typedef long long ll;

const int MAXN = 50005;
ll prefix_sum[MAXN], dp_val[MAXN], coord_a[MAXN], coord_b[MAXN];
int deque_idx[MAXN];

double calculate_slope(int i, int j) {
    double dy = (double)(dp_val[i] + coord_b[i] * coord_b[i]) - (dp_val[j] + coord_b[j] * coord_b[j]);
    double dx = (double)coord_b[i] - coord_b[j];
    return dy / dx;
}

int main() {
    int n;
    ll L;
    cin >> n >> L;

    for (int i = 1; i <= n; ++i) {
        ll weight;
        cin >> weight;
        prefix_sum[i] = prefix_sum[i - 1] + weight;
        coord_a[i] = prefix_sum[i] + i;
        coord_b[i] = prefix_sum[i] + i + 1 + L;
    }

    coord_b[0] = 1 + L;
    int head = 0, tail = 0;
    deque_idx[0] = 0;

    for (int i = 1; i <= n; ++i) {
        while (head < tail && calculate_slope(deque_idx[head + 1], deque_idx[head]) < 2.0 * coord_a[i]) 
            head++;
        
        int best_j = deque_idx[head];
        ll diff = coord_a[i] - coord_b[best_j];
        dp_val[i] = dp_val[best_j] + diff * diff;

        while (head < tail && calculate_slope(i, deque_idx[tail]) < calculate_slope(deque_idx[tail], deque_idx[tail - 1])) 
            tail--;
        deque_idx[++tail] = i;
    }

    cout << dp_val[n] << endl;
    return 0;
}

Lidando com Coeficientes não Monótonos

Quando a inclinação ou a coordenada X não são monótonas, a fila monótona tradicional falha. Nesses casos, utilizamos busca binária sobre o invólucro convexo ou estruturas mais complexas como a Li Chao Tree.

Exemplo: Agendamento de Tarefas com Custos Variáveis

Se o tempo de processamento puder ser negativo, o coeficiente angular deixa de ser monótono. A busca binária é necessária para encontrar o segmento correto do invólucro.

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long ll;
const int MAXN = 300005;

ll T[MAXN], C[MAXN], dp[MAXN];
int q[MAXN];

double get_slope(int i, int j) {
    if (C[i] == C[j]) return 1e18;
    return (double)(dp[i] - dp[j]) / (C[i] - C[j]);
}

int binary_search_hull(int l, int r, ll target_slope) {
    while (l < r) {
        int mid = (l + r) / 2;
        if (get_slope(q[mid + 1], q[mid]) <= target_slope) l = mid + 1;
        else r = mid;
    }
    return q[l];
}

int main() {
    int n;
    ll s;
    cin >> n >> s;
    for (int i = 1; i <= n; ++i) {
        ll ti, ci;
        cin >> ti >> ci;
        T[i] = T[i - 1] + ti;
        C[i] = C[i - 1] + ci;
    }

    int head = 0, tail = 0;
    q[0] = 0;

    for (int i = 1; i <= n; ++i) {
        int best_j = binary_search_hull(head, tail, s + T[i]);
        dp[i] = dp[best_j] + T[i] * (C[i] - C[best_j]) + s * (C[n] - C[best_j]);

        while (head < tail && get_slope(i, q[tail]) <= get_slope(q[tail], q[tail - 1])) 
            tail--;
        q[++tail] = i;
    }

    cout << dp[n] << endl;
    return 0;
}

Tags: dynamic-programming competitive-programming algorithms C++ data-structures

Publicado em 7-3 09:38