Skip to content

Commit

Permalink
ct0371-2: Add notes
Browse files Browse the repository at this point in the history
  • Loading branch information
alek3y committed Mar 2, 2024
1 parent fbbbb16 commit ab6f789
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 2 deletions.
3 changes: 3 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
- [Tabelle hash](./ct0371-2/06/README.md)
- [Concatenamento](./ct0371-2/06/01/README.md)
- [Indirizzamento aperto](./ct0371-2/06/02/README.md)
- [Confronto](./ct0371-2/06/03/README.md)
- [Programmazione dinamica](./ct0371-2/07/README.md)
- [Taglio delle aste](./ct0371-2/07/01/README.md)

- [Algoritmi e strutture dati (M. 1)](./ct0371-1/README.md)
- [Complessità asintotica](./ct0371-1/01/README.md)
Expand Down
2 changes: 1 addition & 1 deletion src/ct0371-2/06/01/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ h(k) = \lfloor (k \cdot A \bmod 1) \cdot m \rfloor
$$
con $U \subseteq \mathbb{N}$, fissando $A \in (0, 1)$ e ottenendo da $k \cdot A \bmod 1$ un valore in $[0, 1)$ da trasformare in $[0, m)$.

In questo modo $m$ non è più critico e la funzione è ottimale per la maggior parte degli $A$, tra cui $A = \frac{\sqrt{5} - 1}{2}$.
In questo modo $m$ non è più critico e la funzione è **ottimale** per la maggior parte degli $A$, tra cui $A = \frac{\sqrt{5} - 1}{2}$.

Se $k$ è una _word_ lunga $w$, la funzione si può **semplificare** scegliendo una $q$ tra le $2^w$ _word_ e ponendo:
$$
Expand Down
62 changes: 61 additions & 1 deletion src/ct0371-2/06/02/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Indirizzamento aperto

Un modo alternativo per risolvere le _collisioni_ consiste nel memorizzare gli elementi con lo stesso _hash_ nella tabella insieme agli altri, per poi cercarli attraverso l'**ispezione** di `T[h(k)]`:
Un modo alternativo per risolvere le _collisioni_ consiste nel memorizzare gli elementi con lo stesso _hash_ nella tabella **insieme agli altri**, per poi cercarli attraverso l'**ispezione** di `T[h(k)]`:
- Se la cella equivale a `k` la ricerca ha **successo**
- Se equivale a `NIL` la ricerca termina con **insuccesso**
- Se è diversa da `k` si trova il prossimo indice dall'**ordine di ispezione**, cioè il numero di _ispezioni_ fatte
Expand Down Expand Up @@ -61,3 +61,63 @@ In queste versioni delle operazioni, per semplicità, gli elementi di `T` sono s
Il motivo per cui si usa `DELETED` invece che `NIL` è che quest'ultimo serve ad indicare la **fine della catena** di ricerca, e quindi porterebbe alla perdita dei valori sulle $i$ successive.

Lo svantaggio di questo metodo è che il tempo di ricerca **non dipende più** da $\alpha = \frac{n}{m}$.

## Metodi di ispezione

Come per il [concatenamento](../01/README.md#tempo-di-ricerca), una funzione $h$ ideale rispetterebbe le proprietà dell'**hashing uniforme** così che, dato un $k$, ogni $h(k, i)$ è distribuito uniformemente sulle $m$ celle.

Dato che è difficile rispettarle, vengono adottate delle approssimazioni.

### Ispezione lineare

$$
h(k, i) = (h'(k) + i) \bmod m
$$
dove $h'\colon U \to \{0, 1, ..., m-1\}$ è detta funzione **hash ausiliaria**.

Questo metodo è **semplice**, ma genera la **stessa sequenza** di ispezioni per le $k$ diverse con lo stesso $h'(k)$.

### Ispezione quadratica

$$
h(k, i) = (h'(k) + c_1 \cdot i + c_2 \cdot i^2) \bmod m
$$
dove $h'$ è l'_hash ausiliaria_ e $c_1, c_2 \in \{1, ..., m-1\}$ sono costanti, con buoni valori $c_1 = c_2 = \frac{1}{2}$ e $m = 2^p$.

Anche in questo caso genera la **stessa sequenza** di ispezioni per $k$ diverse con $h'(k)$ uguali.

### Doppio hashing

$$
h(k, i) = (h_1(k) + i \cdot h_2(k)) \bmod m
$$
dove $h_{1,2}$ sono _hash ausiliarie_, di cui $h_1$ marca la cella di partenza mentre $h_2$ definisce lo step delle ispezioni.

Questo metodo genera **sequenze diverse** di ispezione, dato che dipendono da $h_2 \neq h_1$.

Per generare sequenze su tutti i valori della tabella, $h_2(k)$ dev'essere [relativamente primo](../../../ct0434/08/README.md#massimo-comun-divisore) con $m$:
- Si usa $m = 2^p$ **pari** e si definisce $h_2(k)$ come sempre **dispari**, e.g. $h_2(k) = 2 \cdot h'(k) + 1$
- Si usa $m$ **primo** e $h_2(k)$ **minore** di $m$, e.g. $h_1(k) = k \bmod m$, $h_2(k) = 1 + (k \bmod m')$ per $m' < m$

## Tempo di ricerca

Al contrario del [concatenamento](../01/README.md#tempo-di-ricerca), l'indice di prestazione $\alpha = \frac{n}{m} \in [0, 1]$ perchè $n = |K| \leq m$.

Se $\alpha < 1$ esiste almeno una cella vuota su cui la **ricerca senza successo** si può fermare, quindi:
- $P(i = 0) = 1$ perchè la prima ispezione è sempre effettuata
- $P(i = 1) = \frac{n}{m}$ ovvero la probabilità che la cella su $i = 0$ sia occupata
- $P(i = 2) = \frac{n}{m} \cdot \frac{n - 1}{m - 1}$ perchè anche la cella su $i = 1$ sia occupata
- $P(i = 3) = \frac{n}{m} \cdot \frac{n - 1}{m - 1} \cdot \frac{n - 2}{m - 2}$ perchè anche la cella su $i = 2$ sia occupata

Di conseguenza il [valore atteso](../../../ct0111/03/README.md#valore-atteso) di $i$, ovvero il **numero medio di ispezioni**, è al massimo[^1]:
$$
\begin{split}
E(i) &= 0 \cdot P(i = 0) + 1 \cdot P(i = 1) + ... \\
&\leq \sum_{k = 0}^\infty \alpha^k = \frac{1}{1 - \alpha}
\end{split}
$$
e lo stesso vale per l'**inserimento**, dato che cerca una cella vuota.

Nella **ricerca con successo** invece, il _numero medio di ispezioni_ è $\frac{1}{\alpha}\log\frac{1}{1 - \alpha}$.

[^1]: CLRS, Introduction to Algorithms (4th ed.), pp. 298-299
7 changes: 7 additions & 0 deletions src/ct0371-2/06/03/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Confronto

<p align="center"><img src="assets/01.png" alt="Confronto dei tempi del concatenamento e dell'indirizzamento aperto"></p>

Dal grafico si nota che `T` è bene sia **ristrutturata** raddoppiando la dimensione, dopo un certo valore:
- $\alpha \geq 2$ per il _concatenamento_, e richiederà $O(m + n)$ dato che vanno visitate le liste nelle celle
- $\alpha \geq \frac{1}{2}$ per l'_indirizzamento aperto_, e richiederà $O(m)$
Binary file added src/ct0371-2/06/03/assets/01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions src/ct0371-2/07/01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Taglio delle aste

Nel problema del **taglio delle aste** si vuole cercare il **massimo guadagno** $r_n$ ottenibile dalla vendita di pezzi ricavati dal taglio di un'asta lunga $n$, i cui prezzi $p_i$ dipendono dalla lunghezza $i$ venduta.

Per esempio se $n = 7$ e
$$
\Set{(i, p_i) | i \leq n} = \{(1, 1), (2, 5), (3, 8), (4, 9), (5, 10), (6, 17), (7, 17)\}
$$
conviene tagliare l'asta in pezzi da $2, 2, 3$ o $6, 1$ invece che $5, 2$ perchè si guadagna $18$ invece che $15$.

## Ricorsione

Un'asta lunga $n$ può essere **tagliata o meno** in ogni posizione $1 \leq i \leq n-1$, totalizzando
$$
\underbrace{2 \cdot 2 \cdots 2}_{n-1} = 2^{n-1}
$$
tagli e quindi portando la **complessità** a $\Theta(2^n)$.

Il **ricavo** per l'asta $r_n$ sarà quindi definito ricorsivamente come:
$$
r_n = \max(p_n, r_1 + r_{n-1}, r_2 + r_{n-2}, ..., r_{n-1} + r_1)
$$
dove $p_n$ è il ricavo dell'**asta senza tagli** e $r_i + r_{n-i}$ è il ricavo dato dal **taglio su $i$**.

Per il problema si dice che valga la proprietà di **sottostruttura ottima** perchè la **soluzione ottima** cercata, in questo caso $r_n$, è esprimibile da combinazioni di _soluzione ottime_ di sottoproblemi.
101 changes: 101 additions & 0 deletions src/ct0371-2/07/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Programmazione dinamica

La **programmazione dinamica** è una tecnica di **progettazione** di algoritmi che:
- Sono **riducibili** a molteplici sottoproblemi annidati
- Possiedono sottoproblemi detti **sovrapponibili**, ovvero che sono riutilizzati tra i vari sottoproblemi

La tecnica si basa quindi sul **salvare** il risultato dei sottoproblemi così da avere la soluzione a disposizione.

Per esempio, dall'albero ricorsivo della funzione di Fibonacci per $f(5)$
```dot process
digraph {
node [shape=circle]
edge [dir=none]
5 [color="#6a4c93"]
4 [color="#ff595e"]
{
node [color="#8ac926"]
31 [label="3"]
32 [label="3"]
}
{
node [color="#1982c4"]
21 [label="2"]
22 [label="2"]
23 [label="2"]
}
{
node [shape=none width=0 height=0]
11 [label="1"]
12 [label="1"]
13 [label="1"]
14 [label="1"]
15 [label="1"]
01 [label="0"]
02 [label="0"]
03 [label="0"]
}
5 -> 4, 31
4 -> 32, 21
31 -> 22, 11
32 -> 23, 12
21 -> 13, 01
22 -> 14, 02
23 -> 15, 03
{
node [shape=point width=0]
_0
_1
_2
_3
_4
_5
}
{
edge [style=invis]
{
rank=same
4 -> _0 -> 31
}
{
rank=same
32 -> _1 -> 21
21 -> 22
22 -> _2 -> 11
}
{
rank=same
23 -> _3 -> 12
12 -> 13
13 -> _4 -> 01
01 -> 14
14 -> _5 -> 02
}
{
rank=same
15 -> 03
}
{
edge [weight=100]
5 -> _0
4 -> _1
31 -> _2
32 -> _3
21 -> _4
22 -> _5
}
}
}
```
si nota che conviene _salvare_ il risultato di $f(2)$ e $f(3)$ per calcolarlo **una sola volta**.

Durante l'implementazione si può scegliere tra due **tecniche di costruzione** di algoritmi:
- **Top-down**, attraverso la **memoization**: salva le soluzioni in una tabella durante la ricorsione
- **Bottom-up**: ordina i sottoproblemi in base alla dimensione e li risolve dal più piccolo, salvandoli

0 comments on commit ab6f789

Please sign in to comment.