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
0
non 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
1
invece posso decidere se lasciarlo a1
o 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 a0
vado a modificareb
stesso (dato che cambio lo stato del bitj-1
b
diventab^(1<<(j-1))
) , quindidp[b][j] = dp[b^(1<<(j-1))][j] = dp[b^(1<<(j-1))][j-1]
(il bitj-1
è0
quindi 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-1
in poi), quindi se il bitj-1
è uguale a1
abbiamodp[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.