Editorial (non ufficiale) OII 2020
In questo post proverò a descrivervi le mie soluzioni ai problemi che ho risolto partecipando al mirror delle OII 2020.
Prerequisiti
- Sapere cos’è una deque (libreria in c++ che la implementa:
<deque>
): è una coda (a doppia estremità) che permette di guardare/aggiungere/eliminare un elemento davanti alla coda (operazionifront()
,push_front()
epop_front()
) e guardare/aggiungere/eliminare un elemento in fondo alla coda (operazioniback()
,push_back()
epop_back()
), ognuna di queste operazioni ha complessità \mathcal O(1). Ci servirà per il problema Candele; - conoscenze base sui grafi: sapere come memorizzare un grafo tramite liste di adiacenza e saper implementare l’Algoritmo di Dijkstra per cercare il cammino minimo. Ci servirà per il problema Candele;
- Sapere cos’è una mappa (libreria in c++ che la implementa:
<map>
): le mappe sono delle strutture associative che memorizzano elementi formati da una combinazione di un valore chiave e un valore mappato. Ci servirà per il problema Ristorante e Compleanno;
//mappa in cui gli indici sono dei long long e i valori mappati sono degli interi
map<long long,int> f;
/*crea un elemento di valore 42 che ha indice 3485342382
(nota: viene usata memoria solo per allocare questo nuovo elemento,
quindi non viene allocato spazio per gli elementi con indice compreso
tra 0 e 3485342381) */
f[3485342382]=42;
- conoscenze di programmazione dinamica: è una tecnica di risoluzione che consiste nella suddivisione del problema in sottoproblemi più piccoli (e più semplici) da risolvere. Ci servirà per il problema Compleanno.
Candele
Si consideri il seguente grafo: ciascun nodo rappresenta una candela e vi sia un arco diretto x\rightarrow y di peso t per ogni coppia di candele (x,y) tali che, dopo un eventuale accensione di x verrebbe accesa anche y dopo t secondi (la miccia di y è raggiungibile e ha distanza t dalla miccia di x). Ora si può notare come il problema chieda semplicemente la distanza minima di ogni candela dalla candela 0, che si può calcolare applicando dijkstra sul grafo che abbiamo costruito partendo dal nodo 0.
Però sorge una complicazione: il numero di archi da costruire può essere molto elevato (dell’ordine di N^2, dove N è il numero di candele) e quindi non è possibile costruirli tutti. Per risolverla è necessario notare che non sono davvero necessari tutti gli archi, ma per ogni candela x è necessario tenere solo i due archi che corrispondono alle due candele (che chiamiamo a e b) più vicine a destra e a sinistra che la accendono.
Dimostrazione
se la candela x (diversa da 0) viene accesa, allora almeno una candela tra a (la candela a destra più vicina che accende x) e b (la candela a sinistra più vicina che accende x) devono essere state accese in precedenza (e quindi sono sufficienti questi due archi per considerare l’accensione di x).
\square
Quindi è sufficiente tenersi questi \mathcal O(N) archi e applicare dijkstra su questo grafo.
Qualche spunto di implementazione (in pseudo codice)
ordino le candele rispetto alla posizione della miccia
q è una deque (di candele) inzialmente vuota
for(int i=0;i<n;i++){
while(candela[i] accende la candela che si trova in deque.back() ){
creo un arco da candela[i] alla candela deque.back()
q.pop_back();
}
q.push_back(candela[i]);
}
In questo modo in \mathcal O(N) vado a creare tutti gli archi necessari che vanno da destra verso sinistra (per ogni candela ho messo l’arco relativo alla candela più vicina a destra che la accende). Per creare gli archi verso destra basta ripetere il procedimento invertendo l’array che contiene le candele.
Dopodichè applicare dijkstra partendo dal nodo 0 permette di calcolare il cammino minimo (che è il valore che viene richiesto dal problema) per ogni candela.
Ristorante
Chiamiamo gli array di antipasti, pizze e dessert rispettivamente array 1,2,3. Si consideri la seguente strategia:
Scegliamo X elementi dall’array a, poi finchè posso (ovvero finchè non ho già preso un piatto con lo stesso prezzo) prendo degli elementi dall’array b, poi finchè posso prendo gli elementi dall’array c. ((a,b,c) è una permutazione di (1,2,3))
Se si itera questa strategia su tutte le 3! permutazioni di (1,2,3) e su tutti i possibili valori di X, si troverà la soluzione al problema.
Dimostrazione
Diciamo che l’array v blocca l’array w se, incrementando di uno gli elementi presi nell’array w, andiamo a prendere un elemento che è già preso nell’array v (oppure, caso particolare, non posso incrementare di 1 gli elementi presi da w perché li ho già presi tutti)
Nella soluzione ottimale ovviamente ogni array è bloccato da un altro array, altrimenti potremmo aumentare gli elementi presi.
Quindi nella soluzione ottimale esiste una tripla (a,b,c) che è una permutazione di (1,2,3) tale che: c è bloccato da b, b è bloccato da a. (a sarà bloccato da qualcuno ma non ci interessa)
Questa soluzione con tripla (a,b,c) viene considerata se faccio la strategia:
Scegliamo X elementi dall’array a, poi finchè posso prendo degli elementi dall’array b, poi finchè posso prendo gli elementi dall’array c.
(Provando tutti gli X possibili)
\square
Quindi ora abbiamo ristretto il campo a 3!\cdot N casi da analizzare (3! sono le permutazioni e N i possibili valori di X). Ora si può notare come, data una permutazione (a,b,c), è possibile calcolare i piatti presi per tutti gli X possibili in \mathcal O(N\log N).
Infatti si supponga di calcolare inizialmente il numero di piatti presi seguendo la strategia con il massimo X possibile: basta scorrere l’array a e in modo greedy prendere il piatto se non è già stato preso un altro piatto con lo stesso prezzo (per fare questo controllo in \mathcal O(\log N) è necessario usare una mappa o un set), altrimenti passare all’array b e continuare a prendere i piatti finchè possibile e poi fare lo stesso con c. In questo modo viene calcolato X, il numero Y di piatti presi nel secondo array e il numero Z di piatti presi nel terzo array in \mathcal O(N\log N).
map<int,int> f; //f[i] mi dirà quante volte ho preso un piatto con prezzo i
siano a,b,c i 3 array
int x=0,y=0,z=0;
while(x<N && f[a[x]]==0){ //finchè posso prendo i piatti dal primo array
x++;
f[a[x]]++;
}
while(y<N && f[b[y]]==0){ //poi passo al secondo
y++;
f[b[y]]++;
}
while(z<N && f[c[z]]==0){ //poi passo al terzo
z++;
f[c[z]]++;
}
Si può notare inoltre che se X diminuisce di uno, i valori di Y e Z possono solo aumentare, quindi non è necessario ricalcolarli da capo, ma semplicemente si può prendere il valore di Y ottenuto precedentemente e provare a vedere se può aumentare (andando a controllare solo il (Y+1)-esimo piatto) e lo stesso vale per Z.
x--; //ora calcoliamo il valore per un x più piccolo di uno
f[a[x]]--;
while(y<N && f[b[y]]==0){ //provo ad aumentare y
y++;
f[b[y]]++;
}
if(z<N && f[c[z]]==0){ //provo ad aumentare z
z++;
f[c[z]]++;
}
Il valore di X diminuisce sempre, quelli di Y e Z aumentano sempre quindi la complessità totale è \mathcal(N\log N) (il fattore \log N è dovuto alla complessità della mappa per controllare se esiste un elemento con un certo prezzo)
Ricapitolando la complessità della nostra soluzione è \mathcal O(k!N\log N), dove k è il numero di array (nel nostro problema si ha sempre k=3) e N il numero di elementi di ciascun array.
Compleanno
Facciamo qualche osservazione:
- Possiamo assumere che nessuna operazione
incolla
avvenga subito dopo un’operazioneaggiungi
(perchè in tal caso scambiando l’ordine delle due operazioni il risultato rimarrebbe invariato); - Quindi possiamo assumere che ogni operazione
copia
sia seguita un certo certo numero x\geq 1 di operazioniincolla
(altrimenti sarebbe inutile) e nessuna operazioneincolla
sia preceduta da un’operazioneaggiungi
; - Un’operazione
copia
seguita da X operazioniincolla
equivale a moltiplicare il numero di emoji per X+1; - Un’operazione
copia
seguita da X operazioniincolla
impiega complessivamente X+2 click.
Quindi il problema è equivalente a dire che, partendo da 0 emoji, dobbiamo ottenere N emoji (minimizzando il costo) usando le seguenti operazioni:
- aggiungere un’emoji (questa operazione ha costo 1);
- moltiplicare per un numero X\geq 2 il il numero di emoji attualmente presenti (questa operazione ha costo X+1)
Inoltre si può notare come non sia mai ottimale moltiplicare il numero di emoji presenti per un numero X>5 che non è primo.
Dimostrazione
Se X>5 non è primo, allora \exists y,z>1 tali che X=yz . Inoltre si ha che y+z<X:
yz>y+z \iff yz-y-z>0 \iff yz-y-z+1≥2 \iff (y-1)(z-1)≥2 e quest’ultima è sempre vera dato che x,y>1 e non posso essere entrambi 2 (visto che il loro prodotto è maggiore di 5).
Ma allora ne consegue che conviene sempre moltiplicare il numero di emoji prima per y (costo y+1) e poi per z (costo z+1), anzichè direttamente per X, dato che:
y+z<X
y+1+z+1\leq X+1 (quindi sicuramente si ottiene un costo uguale o minore)
\square
Inoltre si può dimostrare che non è mai necessario moltiplicare il numero di emoji per un certo X primo maggiore di 19. (La dimostrazione è lasciata al lettore)
Quindi ora è possibile fare una dp di questo tipo:
f(x)=costo minimo per ottenere x emojis
sapendo che le uniche operazioni sono: moltiplico per 2,3,4,5,7,11,13,17 o 19 il numero di emoji attuali (con rispettivo costo 3,4,5,6,8,12,14,18,20), oppure aggiungo una emoji (costo 1).
Quindi le transizioni della nostra dp saranno:
f(x)= min_{0\leq i<9}(f(\lfloor {x/val[i]} \rfloor)+val[i]+1+(x\%a[i]))
dove val[9]=\{2,3,4,5,7,11,13,17,19\}
faccio notare che ci sono dei corner case per gli x<5, in cui il costo minimo è semplicemente x. (se si usasse la transizione descritta sopra si otterrebbe x+1 come costo minimo) Questo è dovuto al fatto che nella strategia ottimale per ottenere x<5 emoji sono necessarie solo operazioni del tipo “aggiungi un’emoji”, senza moltiplicare il numero di emoji in nessuno momento.
Chiamando f(N) si otterrà il costo minimo.
Nota: nel problema viene chiesto esplicitamente anche la sequenza di operazioni, quindi oltre a salvare il numero minimo di operazioni è necessario tenere traccia anche dell’operazione da eseguire che consente di ottenere tale minimo, in modo da poter fare backtracking e ottenere la sequenza finale. Per salvare queste informazioni non è possibile usare un array (visto che i numeri arrivano fino a 10^{18}), ma si possono tenere in modo sparso usando una mappa.
Richiesta
@taulant (che ha partecipato alla gara ufficiale) è interessato a sapere un po di più sulla classifica prima di martedì, e mi ha chiesto di fare questo annuncio: se qualcuno ha fatto più di 100 in gara scrivetegli in privata.
Grazie a tutti e avvisatemi di eventuali typo o errori.