SOS DP
Prerequisiti
- Bitmasks
- Programmazione dinamica
Attenzione: Questo post è una quasi totale traduzione di un blog di Codeforces (ma neanche a dirlo) di usaxena95, visitatelo per trovare i problemi relativi all’argomento (e magari anche lasciare un upvote
). In ogni modo l’idea mi interessava e mi sembrava anche un buon seguito all’argomento precedente.
Introduzione
L’ottimizzazione presentata, Sum Over Subsets, permette di risolvere il seguente problema con complessità O(n\cdot 2^n):
Sia S un insieme di n elementi ed s il numero binario che lo rappresenta, per ogni suo sottoinsieme I sia i il numero binario che lo rappresenta, andiamo ad assegnare un valore A[i]. Vogliamo calcolare per ogni B sottoinsieme di S il valore F[b] = \sum\limits_{I\subseteq B} A[i].
Ovviamente assumeremo tutti gli A[i] già calcolati.
Soluzione subottimale
Si potrebbe semplicemente applicare la formula appena scritta sopra in modo banale, iterando su tutti i sottoinsiemi B di S, per poi calcolarsi F[b] iterando su tutti i sottoinsiemi I di B, grazie a quanto visto in Quarantraining 05 possiamo scrivere:
for(int b = 0; b <= s; b++){//s = 2^n - 1, tutti gli elementi sono scelti
for(int i = b; i >= 0; i = (i - 1) & b){
F[b] += A[i];
}
}
La complessità di questo algoritmo è O(3^n), infatti per ogni sottoinsieme B di cardinalità k (numero degli elementi) il programma fa 2^k operazioni, i sottoinsiemi di S di cardinalità k sono n\choose k, dunque il numero di operazioni totali sarà \sum\limits_{k=0}^{n} {n\choose k} \cdot 2^k=(1+2)^n (per il binomio di Newton).
Tuttavia come annunciato possiamo fare di meglio.
Approccio DP
Per evitare di scorrere tutti i sottoinsiemi di B nel calcolare F[b] dobbiamo cercare un modo intelligente per salvarci le somme di A[i].
Definiamo dp[b][j] la somma degli A[i] con b&i == i (I\subseteq B) ed i può differire da b solo nei primi j bit.
Pertanto dp[b][0] è proprio A[b], infatti l’unico i che può essere diverso da b nei primi 0 bit è b stesso, mentre dp[b][n] è la nostra F[b], infatti è la somma degli A[i] con b&i == i (dato che tutti i bit di i possono variare).
Ora cerchiamo di calcolare dp[b][j] in funzione delle altre dp[x][y]. Il bit che si trova in posizione j-1 è l’ultimo che posso variare:
- se esso vale
0non posso farlo (dato che valeb&i == i), in questo caso posso variare fino al bitj-2, dunquedp[b][j] = dp[b][j-1]. - se esso vale
1invece posso decidere se lasciarlo a1o metterlo a0: se lo lascio ad1è come se decidessi di non cambiarlo, quindi vale il caso di primadp[b][j] = dp[b][j-1], se invece lo metto a0vado a modificarebstesso (dato che cambio lo stato del bitj-1bdiventab^(1<<(j-1))) , quindidp[b][j] = dp[b^(1<<(j-1))][j] = dp[b^(1<<(j-1))][j-1](il bitj-1è0quindi l’uguaglianza vale). Ora i 2 casi sono banalmente disgiunti, dato che ho assegnato a un bit 2 valori diversi (e quel bit non può più cambiare daj-1in poi), quindi se il bitj-1è uguale a1abbiamodp[b][j] = dp[b][j-1] + dp[b^(1<<(j-1))][j-1].
Abbiamo quindi ben definito dp[b][j] in funzione di casi minori (lo XOR in b^(1<<(j-1)) manda il bit a 0, quindi è minore), possiamo quindi scrivere un algoritmo bottom up che risolva il problema:
for(int b = 0; b <= s; b++){
dp[b][0] = A[b];
for(int j = 1; j <= n; j++){
if(b & (1<<(j-1)))
dp[b][j] = dp[b][j-1] + dp[b^(1<<(j-1))][j-1];
else
dp[b][j] = dp[b][j-1];
}
F[b] = dp[b][n];
}
La complessità dell’algoritmo è chiaramente O(n\cdot 2^n).
Possiamo comunque ottimizzare l’algoritmo in modo da usare O(2^n) memoria anziché O(n\cdot 2^n): basta pensare che l’utilizzo di dp è inutile, in quanto dp[b][j] è definita solo in funzione delle dp con j-1 e non di quelle precedenti, basta quindi invertire i for e salvarci i risultati direttamente in F:
for(int b = 0; b <= s; b++)
F[b] = A[b];
for(int j = 0; j < n; j++){
for(int b = 0; b <= s; b++){
if(b & (1<<j))
F[b] += F[b^(1<<j)];
}
}
Il codice diventa anche più semplice.
Conclusioni
Nel post ho cercato di cambiare leggermente l’approccio alla spiegazione del problema (rispetto al blog di Codeforces), in modo da provare a dare un punto di vista in più, tuttavia nel poco tempo a disposizione potrei aver lasciato qualcosa indietro, scrivete pure se un punto risulta incomprensibile 
Problemi
I problemi potete trovarli alla fine del post originale.