06f7d601e9345e1275800935d902c603bd0d4256
resources/2018_Territoriali_soluzioni.md
... | ... | @@ -0,0 +1,216 @@ |
1 | +## Soluzioni — Selezione Territoriali 2018 |
|
2 | +Di seguito spiegheremo brevemente come risolvere i quattro task delle selezioni territoriali 2018. |
|
3 | + |
|
4 | +### Festa canina (<code>party</code>) — <span style="color: #1dd01d;">molto facile</span></h3> |
|
5 | + |
|
6 | +Mettendo da parte i problemi di amicizia e inimicizia del famosissimo Mojito, il succo del problema è questo: ci viene dato un array di numeri interi e vogliamo ottenere un risultato (anch'esso un numero intero). Una volta letto e compreso il testo del problema, possiamo facilmente convincerci del fatto che la soluzione sia calcolabile come la **somma** di tutti i numeri **positivi** presenti nell'array fornito. Infatti, se un numero è negativo, vuol dire che Mojito non gradisce la presenza di quello specifico cane, quindi ci basta non invitarlo (ovvero: non includerlo nella somma). |
|
7 | + |
|
8 | +Una semplice implementazione in C++ è la seguente: |
|
9 | +```[cpp] |
|
10 | +int main() { |
|
11 | + int T; |
|
12 | + cin >> T; |
|
13 | + for (int t = 1; t <= T; t++) { |
|
14 | + int N; cin >> N; |
|
15 | + int sum = 0; |
|
16 | + for (int i = 0; i < N; i++) { |
|
17 | + int x; |
|
18 | + cin >> x; |
|
19 | + if (x > 0) |
|
20 | + sum += x; |
|
21 | + } |
|
22 | + cout << "Case #" << t << ": " << sum << endl; |
|
23 | + } |
|
24 | +} |
|
25 | +``` |
|
26 | + |
|
27 | +### Antivirus (<code>antivirus</code>) — <span style="color: green;">facile</span> |
|
28 | + |
|
29 | +Anche qui, una volta estratto il problema dalla "storiella", ciò che ci viene richiesto è esprimibile in termini primitivi (variabili e array). In particolare, ci vengono fornite delle variabili (numeri interi) e 4 stringhe (array di caratteri). Vogliamo calcolare 4 indici (numeri interi) che rappresentano il "punto di partenza" del virus in ciascuna stringa. |
|
30 | + |
|
31 | +Una possibile soluzione è scrivere 5 cicli for nidificati (ovvero: uno dentro l'altro, come una [matrioska](https://it.wikipedia.org/wiki/Matrioska). I 4 cicli più esterni faranno riferimento all'indice delle 4 stringhe (non è importante l'ordine). Per esempio, possiamo chiamare gli indici <code>i</code>, <code>j</code>, <code>k</code>, <code>l</code>. Il quinto ciclo, quello più interno, servirà invece a verificare che il virus sia effettivamente presente nelle posizioni indicate dai quattro indici. Per esempio, potremmo chiamare <code>m</code> l'indice di questo ciclo.</p> |
|
32 | + |
|
33 | +Il codice che segue implementa la soluzione appena descritta (con un <code>+ M</code> nella condizione dei cicli for, per evitare che i vari indici "fuoriescano" dalle stringhe). |
|
34 | + |
|
35 | +```[cpp] |
|
36 | +void solve(int t) { |
|
37 | + int N1, N2, N3, N4; |
|
38 | + cin >> N1 >> N2 >> N3 >> N4; |
|
39 | + |
|
40 | + int M; |
|
41 | + cin >> M; |
|
42 | + |
|
43 | + string F1, F2, F3, F4; |
|
44 | + cin >> F1 >> F2 >> F3 >> F4; |
|
45 | + |
|
46 | + for (int i = 0; i + M <= N1; i++) |
|
47 | + for (int j = 0; j + M <= N2; j++) |
|
48 | + for (int k = 0; k + M <= N3; k++) |
|
49 | + for (int l = 0; l + M <= N4; l++) { |
|
50 | + bool match = true; |
|
51 | + |
|
52 | + for (int m = 0; m < M; m++) |
|
53 | + if (F1[i + m] != F2[j + m] || F2[j + m] != F3[k + m] || F3[k + m] != F4[l + m]) |
|
54 | + match = false; |
|
55 | + |
|
56 | + if (match) { |
|
57 | + cout << "Case #" << t << ": " << i << " " << j << " " << k << " " << l << endl; |
|
58 | + return; |
|
59 | + } |
|
60 | + } |
|
61 | +} |
|
62 | + |
|
63 | +int main() { |
|
64 | + int T; |
|
65 | + cin >> T; |
|
66 | + |
|
67 | + for (int t = 1; t <= T; t++) { |
|
68 | + solve(t); |
|
69 | + } |
|
70 | +} |
|
71 | +``` |
|
72 | + |
|
73 | +**ATTENZIONE!** Questa soluzione, sebbene sia sufficiente a prendere tutti i punti, non è quella asintoticamente ottimale. Infatti, la (complessità computazionale)[https://it.wikipedia.org/wiki/Teoria_della_complessit%C3%A0_computazionale] di questa soluzione, quando la esprimiamo come funzione della lunghezza massima <code>N</code> delle quattro stringhe e della lunghezza massima <code>M</code> del virus, diventa pari a: $O(N^4M)$. |
|
74 | + |
|
75 | +Dato che i valori massimi di <code>N</code> e <code>M</code> sono piuttosto bassi, il nostro programma terminerà la sua esecuzione in un tempo ragionevole. Tuttavia, se questo stesso problema fosse stato proposto alla *finale nazionale*, i valori sarebbero sicuramente stati molto più elevati. Esistono infatti soluzioni asintoticamente molto più efficienti di quella appena descritta! |
|
76 | + |
|
77 | +### Radioanalisi fossile (<code>xray</code>) — <span style="color: blue;">medio</span> |
|
78 | + |
|
79 | +Un modo per risolvere questo problema è guardando ai "picchi". Possiamo convincerci che un algoritmo ottimale, in termini di numero di azionamenti della macchina, è il seguente: |
|
80 | + |
|
81 | +- cerchiamo il massimo nell'array, ad esempio otterremo <code>8</code> se l'array fosse <code>5 8 6</code>; |
|
82 | +- lo riduciamo fino a che non diventa uguale al più grande dei suoi vicini, nel nostro caso <code>8</code> si riduce a <code>6</code> mediante 2 azionamenti; |
|
83 | +- torniamo al punto 1 |
|
84 | + |
|
85 | +Naturalmente dobbiamo fare attenzione al caso in cui, per esempio, l'array fosse <code>5 8 8 6</code>. In questo caso infatti il massimo non è un solo elemento ma un intervallo di due elementi: dobbiamo quindi considerarli tutti come fossero un unico elemento (ed applicare le radiazioni all'intero intervallo). Dobbiamo inoltre fare attenzione a terminare l'algoritmo non appena tutti gli elementi diventano pari a zero. |
|
86 | + |
|
87 | +Un'implementazione alternativa di questa soluzione (più facile da codificare) considera tutti i numeri, da sinistra a destra, riducendoli a zero sequenzialmente (uno dopo l'altro) cercando di "includere" quanti più vicini possibile (ovvero: i vicini a destra fino a che non si incontra uno zero o la fine dell'array). È ragionevole convincersi che questa soluzione è equivalente a quella descritta sopra. |
|
88 | + |
|
89 | +```[cpp] |
|
90 | +const int MAXN = 1000; |
|
91 | + |
|
92 | +int compute(int N, int vec[MAXN + 2]) { |
|
93 | + int risp = 0; |
|
94 | + for (int i = 1; i <= N; i++) { |
|
95 | + while (vec[i]) { |
|
96 | + risp++; |
|
97 | + for (int j = i; j <= N; j++) { |
|
98 | + if (!vec[j]) break; |
|
99 | + vec[j]--; |
|
100 | + } |
|
101 | + } |
|
102 | + } |
|
103 | + |
|
104 | + return risp; |
|
105 | +} |
|
106 | + |
|
107 | +void solve(int t) { |
|
108 | + int N; |
|
109 | + cin >> N; |
|
110 | + int vec[MAXN + 2]; |
|
111 | + |
|
112 | + vec[0] = vec[N + 1] = 0; |
|
113 | + for (int i = 1; i <= N; i++) { |
|
114 | + cin >> vec[i]; |
|
115 | + } |
|
116 | + |
|
117 | + cout << "Case #" << t << ": " << compute(N, vec) << endl; |
|
118 | +} |
|
119 | + |
|
120 | +int main() { |
|
121 | + int T; |
|
122 | + cin >> T; |
|
123 | + |
|
124 | + for (int t = 1; t <= T; t++) { |
|
125 | + solve(t); |
|
126 | + } |
|
127 | +} |
|
128 | +``` |
|
129 | + |
|
130 | +### Escursione (<code>escursione</code>) — <span style="color: red;">difficile</span> |
|
131 | + |
|
132 | +Per risolvere questo problema è necessario avere dimestichezza con la ricorsione (per risolvere il problema in modo subottimale) o con i grafi (per la soluzione ottimale). |
|
133 | + |
|
134 | +#### Esplorazione completa |
|
135 | + |
|
136 | +Una possibile soluzione subottimale è utilizzare una funzione ricorsiva che "esplora" la matrice. La funzione, dato un punto di partenza, visita ricorsivamente tutte le direzioni possibili (sinistra, sopra, destra, sotto) a patto che non si esca oltre i bordi. La funzione dovrà quindi avere tra i suoi parametri: la posizione (data da <code>i</code> e <code>j</code>) e la differenza di altitudine più alta trovata finora. |
|
137 | + |
|
138 | +Affinché la funzione non vada in ciclo infinito, è necessario mantenere globalmente una matrice booleana che indica se abbiamo visitato oppure no ciascuna cella. Dovremo quindi fare attenzione a impostare a <code>true</code> la posizione in cui stiamo entrando ricorsivamente, e soprattutto di reimpostare a <code>false</code> la posizione dalla quale stiamo uscendo ricorsivamente. Questa tecnica prende il nome di (backtracking)[https://en.wikipedia.org/wiki/Backtracking]. |
|
139 | + |
|
140 | +Nel corpo della funzione dobbiamo controllare se la posizione attuale corrisponde con quella di arrivo e, se è così, dobbiamo memorizzare in una variabile globale il risultato migliore trovato finora (utilizzando il parametro della funzione che indica la differenza di altitudine più alta del percorso). |
|
141 | + |
|
142 | +Questa soluzione è corretta perché stiamo percorrendo tutti i possibili percorsi che partono dalla posizione di partenza ed arrivano a quella di arrivo senza passare due volte per lo stesso punto. Tuttavia, impiega *troppo* tempo! In una griglia 100x100 non basterebbero tutte le ore di gara a calcolare la soluzione. In realtà, non basterebbe nemmeno un milione di anni! Un'illustrazione di questo fenomeno (che prende il nome di *crescita esponenziale*) si può trovare [in questo simpatico video](https://www.youtube.com/watch?v=Q4gTV4r0zRs). |
|
143 | + |
|
144 | +#### Versione ottimizzata |
|
145 | + |
|
146 | +Un modo per ottimizzare questa soluzione è memorizzare per ciascuna posizione (oltre allo stato "visitato" / "non visitato") anche un'altra informazione: la migliore soluzione trovata per arrivare dal punto di partenza a quella posizione. Infatti, mantenendo questa informazione, possiamo **terminare anticipatamente** un intero ramo ricorsivo se notiamo che la soluzione "candidata" (quella che ci stiamo portando dietro nel parametro della funzione ricorsiva) è maggiore di quella che abbiamo salvato globalmente nella posizione corrente. Un accorgimento a cui dobbiamo far caso è che all'inizio del programma dobbiamo inizializzare la miglior soluzione trovata per ciascuna posizione ad un valore molto alto (è prassi definire una costante <code>INF</code> nel codice e impostarla a un valore sicuramente più alto della soluzione cercata). |
|
147 | + |
|
148 | +Questa soluzione dovrebbe essere molto più veloce ed è probabilmente sufficiente per prendere tutti i punti. |
|
149 | + |
|
150 | +#### Soluzione ottima |
|
151 | + |
|
152 | +La soluzione ottimale a questo problema consiste nell'usare una variazione dell'[Algoritmo di Dijkstra](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm). |
|
153 | + |
|
154 | +```[cpp] |
|
155 | +#define INFTY 1000000000 |
|
156 | + |
|
157 | +const int MAXH = 100; |
|
158 | +const int MAXW = 100; |
|
159 | + |
|
160 | +int A[MAXH + 2][MAXW + 2]; |
|
161 | +int D[MAXH + 2][MAXW + 2]; |
|
162 | +int H, W; |
|
163 | + |
|
164 | +int compute() { |
|
165 | + priority_queue<pair<int, pair<int, int>>> q; |
|
166 | + |
|
167 | + q.push(make_pair(0, make_pair(1, 1))); |
|
168 | + |
|
169 | + while (!q.empty()) { |
|
170 | + auto top = q.top(); |
|
171 | + q.pop(); |
|
172 | + |
|
173 | + if (D[top.second.first][top.second.second] != INFTY) |
|
174 | + continue; |
|
175 | + |
|
176 | + D[top.second.first][top.second.second] = -top.first; |
|
177 | + |
|
178 | + for (int i = -1; i <= 1; i++) { |
|
179 | + for (int j = -1; j <= 1; j++) { |
|
180 | + if (i * j == 0) { |
|
181 | + q.push(make_pair( |
|
182 | + -max(-top.first, |
|
183 | + abs(A[top.second.first][top.second.second] - |
|
184 | + A[top.second.first + i][top.second.second + j])), |
|
185 | + make_pair(top.second.first + i, top.second.second + j))); |
|
186 | + } |
|
187 | + } |
|
188 | + } |
|
189 | + } |
|
190 | + return D[H][W]; |
|
191 | +} |
|
192 | + |
|
193 | +void solve(int t) { |
|
194 | + cin >> H >> W; |
|
195 | + |
|
196 | + memset(D, 0, sizeof(D)); |
|
197 | + |
|
198 | + for (int i = 1; i <= H; i++) { |
|
199 | + for (int j = 1; j <= W; j++) { |
|
200 | + cin >> A[i][j]; |
|
201 | + D[i][j] = INFTY; |
|
202 | + } |
|
203 | + } |
|
204 | + |
|
205 | + cout << "Case #" << t << ": " << compute() << endl; |
|
206 | +} |
|
207 | + |
|
208 | +int main() { |
|
209 | + int T; |
|
210 | + cin >> T; |
|
211 | + |
|
212 | + for (int t = 1; t <= T; t++) { |
|
213 | + solve(t); |
|
214 | + } |
|
215 | +} |
|
216 | +``` |
|
... | ... | \ No newline at end of file |