Programação Dinâmica em Árvores (Vertex Cover)
Metáfora e Motivação
A Programação Dinâmica brilha ao fatiar problemas em sub-problemas progressivamente menores. Em matrizes, a progressão era ir pela direita ou pra baixo. Mas como fazer transições numa Árvore/Grafo? O sub-problema de uma árvore é simplesmente olhar para uma ramificação inferior cortando o tronco acima!
O Problema do Reino (Vertex Cover)
Um reino possui \(N\) cidades conectadas entre si por \(N-1\) estradas perfeitamente pavimentadas (Grafo Conexo sem ciclos = Uma Árvore). A rainha precisa erguer postos militares em algumas cidades. Acordo estrito: para toda estrada, pelo menos uma das duas cidades interligadas deve ter um posto! Os postos custam caro. Qual o menor número de postos que podemos erguer e cobrir todas as estradas?
Na matemática de grafos, isso é conhecido como a Cobertura Mínima de Vértices (Vertex Cover). Sendo a geografia local formatada em árvore, podemos achar a raiz matematicamente de forma linear.
O Estado da Construção Dinâmica
Vamos batizar um vértice escolhido como Colorido (Instalamos o posto) ou Branco.
Na árvore, ao olharmos os galhos de cima para baixo começando na base 1, o que um nó da camada atual precisa saber do próprio passado? Ele precisa saber “Meu pai no tronco de cima foi colorido?” Por que? Porque se o pai dele não instalou um posto militar lá em cima, essa mesma cidade que estamos lendo Têm que, de forma divina e obrigatória, instalar o posto, senão a estrada entre ela e o pai ficaria sem proteção em ambos os lados!
- Estado da DP:
PD(CidadeAtual, PaiColorido_Boolean)- Possibilidade A (Colorir): Gastamos +1 instalação. Ganhamos carta branca pra todos os filhos (Eles herdarão
PaiColorido = True). - Possibilidade B (Não Colorir): Não gastamos grana (\(0\)). Mas sentenças duras passam aos nossos filhos (Herdarão
PaiColorido = False), obrigando-os a se colorir sem piedade. - Se a aba
PaiColorido_Boolean == False, o universo te obriga atuar a Possibilidade A. Senão, use a força da DP e explore e pegue omin(A, B).
- Possibilidade A (Colorir): Gastamos +1 instalação. Ganhamos carta branca pra todos os filhos (Eles herdarão
Algoritmo Vertex Cover Recursivo em DP
Este código injeta os conceitos acima no formato Top-Down, construindo uma tabela que varia de acordo com as respostas vizinhas.
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#include <cassert>
using namespace std;
const int MAXV = 1000;
int tabelaDP[MAXV][2];
vector<int> arvore[MAXV];
// X: Fator Posicional da Árvore
// paiColorido: 1 (Verdadeiro), 0 (Falso)
// paiN: Nó que realizou a chamada para evitar voltar pra cima infinitamente na recursao.
int vertexCover(int X, int paiColorido, int paiN) {
if(tabelaDP[X][paiColorido] != -1) {
return tabelaDP[X][paiColorido];
}
int custoColorir = 1; // +1 posto na cidade X
int custoNaoColorir = 0; // Economizamos
for(int filho : arvore[X]) {
if(filho == paiN) continue; // Ignora ciclo subindo
// Se X é colorido, o filho recebe (True) na chamada
custoColorir += vertexCover(filho, 1, X);
// Se X nao eh colorido, o filho recebe (False) na chamada obrigatoria futuramente
custoNaoColorir += vertexCover(filho, 0, X);
}
// Regulação da Decisão Exclusiva
if(paiColorido == 0) {
// Se meu pai era um desertor de bases, EU SOU OBRIGADO A ASSUMIR!
return tabelaDP[X][paiColorido] = custoColorir;
}
// Se o pai for colorido, eu tenho a margem de escolha livre.
return tabelaDP[X][paiColorido] = min(custoColorir, custoNaoColorir);
}
void executando_testes() {
// 5 cidades modelados (Indices 1 ao 5)
// Ligacoes: 1-2, 1-3, 2-4, 2-5
arvore[1] = {2, 3};
arvore[2] = {1, 4, 5};
arvore[3] = {1};
arvore[4] = {2};
arvore[5] = {2};
memset(tabelaDP, -1, sizeof(tabelaDP));
// Para resolver, entramos na Raiz (1) dizendo que seu "pai imaginário superior" ERA colorido (1).
// Isso dá passe livre para a base raiz decidir sozinha e sem restrições.
int menorTotal = vertexCover(1, 1, -1);
// O ideal seria colocar o Posto na Capital 1, e na Metropole 2! (Atrapalhando todas as vizinhancas)
// Assim gastaria 2 postos (Node 1, Node 2)
assert(menorTotal == 2);
cout << "[✅] Testes Dinâmicos do Vertex Cover Tree aprovados!\n";
}
int main() {
executando_testes();
return 0;
}O poder da Programação Dinâmica em árvores é que essa matriz de memorização só armazena linearmente de 0 a \(N\). Complexidade \(\mathcal{O}(2N)\) ou puramente temporal \(\mathcal{O}(N)\) de processamento na máquina iterativa.
Exercício Prático
- O caso explícito do Independent Set On Trees também navega nisso. É a versão onde o estado dita regras de não colisão vizinha (\(DP[X][0]\) vs \(DP[X][1]\)). Procure-o nas maratonas Beecrowd / UVa!