Salta al contenuto
Note per Studenti Problemi computazionali - istanze, soluzioni, correttezza e complessità

Problemi computazionali - istanze, soluzioni, correttezza e complessità

In questa pagina 3

Il capitolo fissa il linguaggio con cui tutta la materia descrive un algoritmo: che cosa deve fare (il problema), che cosa fa (l'algoritmo), se lo fa bene (correttezza) e quanto costa (complessità). Sono richiami più formali di Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →, Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo → e Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica → di Dati e Algoritmi, che si danno per noti.

Algoritmo e problema computazionale

Definizione (algoritmo). Un algoritmoProcedura finita e deterministica: sequenza di passi elementari di un modello di calcolo. è una procedura computazionale finita e deterministica, specificata come sequenza di passi elementari (istruzioni) di un dato modello computazionale, che trasforma un ingresso in una e una sola uscita. Dal punto di vista insiemistico un algoritmo è una funzione A:I→OA : \mathcal{I} \to \mathcal{O} dall'insieme degli ingressi a quello delle uscite. La particolare funzione realizzata è descritta dalla sequenza finita di istruzioni.

Il modello computazionale è l'astrazione di un calcolatore e del suo insieme di istruzioni (nel corso: la RAM con pseudocodice, come in Dati e Algoritmi).

Definizione (problema computazionale). Un problema computazionale è una specifica astratta della relazione tra un insieme di istanze II e un insieme di soluzioni SS: Π⊆I×S,Π={(i1,s1),(i2,s2),… }.\Pi \subseteq I \times S, \qquad \Pi = \{(i_1, s_1), (i_2, s_2), \dots\}. Si scrive anche i Π si \,\Pi\, s per (i,s)∈Π(i, s) \in \Pi. La relazione deve essere:

  1. totale: ogni istanza ha almeno una soluzione, ∀i∈I, ∃s∈S:(i,s)∈Π\forall i \in I,\ \exists s \in S : (i,s) \in \Pi;
  2. non necessariamente univoca: possono esistere s1≠s2s_1 \neq s_2 con i Π s1i \,\Pi\, s_1 e i Π s2i \,\Pi\, s_2.

Il problema è un oggetto logico (una relazione), indipendente dal modo di risolverlo. L'algoritmo ne è una funzione: quando l'istanza ha più soluzioni, l'algoritmo ne sceglie una sola.

Definizione (algoritmo che risolve un problema). Un algoritmo AΠ:I→OA_\Pi : \mathcal{I} \to \mathcal{O} risolve Π\Pi se I=I\mathcal{I} = I, O=S\mathcal{O} = S e ∀i∈I:AΠ(i)=s ⟹ i Π s.\forall i \in I : \quad A_\Pi(i) = s \ \Longrightarrow\ i \,\Pi\, s.

Osservazione (codifica). Identificare ingressi e uscite dell'algoritmo con istanze e soluzioni astratte è una semplificazione: I\mathcal{I} e O\mathcal{O} sono in realtà codifiche di II e SS compatibili con il modello di calcolo (per I=NI = \mathbb{N} si può prendere l'insieme delle stringhe binarie). La distinzione si richiama solo quando serve, per esempio per la taglia di un intero (più sotto).

Esempi di problemi

Somma di due interi. I=Z×ZI = \mathbb{Z} \times \mathbb{Z}, S=ZS = \mathbb{Z}, Π={((a,b),c):c=a+b}\Pi = \{((a,b), c) : c = a + b\}. Qui la relazione è una funzione (ogni istanza ha una sola soluzione).

Raggiungibilità in un grafo diretto. Un grafo diretto è G=(V,E)G = (V, E) con V⊆NV \subseteq \mathbb{N} finito ed E⊆V×VE \subseteq V \times V. Un cammino è una sequenza π=⟨v1,…,vk⟩\pi = \langle v_1, \dots, v_k\rangle, k≥1k \ge 1, con (vj,vj+1)∈E(v_j, v_{j+1}) \in E per 1≤j<k1 \le j < k; esiste anche il cammino degenere ⟨v1⟩\langle v_1 \rangle, senza archi. Il problema chiede un cammino da uu a vv, se esiste:

  • I={⟨G,u,v⟩:u,v∈V}I = \{\langle G, u, v\rangle : u, v \in V\};
  • S={⟨v1,…,vk⟩:k≥1}∪{ϵ}S = \{\langle v_1, \dots, v_k\rangle : k \ge 1\} \cup \{\epsilon\}, dove ϵ\epsilon è la sequenza vuota e significa "non esiste";
  • Π\Pi contiene (⟨G,u,v⟩,π)(\langle G,u,v\rangle, \pi) se π\pi è un cammino da uu a vv, e (⟨G,u,v⟩,ϵ)(\langle G,u,v\rangle, \epsilon) se nessun cammino esiste.

Qui la relazione non è univoca: tra uu e vv ci possono essere più cammini, e tutti sono soluzioni valide.

Ordinamento di sequenze di interi. La stella di Kleene A⋆A^\star di un insieme AA è l'insieme di tutte le sequenze finite, vuota compresa, di elementi di AA; ε=⟨⟩\varepsilon = \langle\rangle ha lunghezza 00. Allora I=S=Z⋆I = S = \mathbb{Z}^\star e ⟨x1,…,xn⟩ Π ⟨y1,…,ym⟩  ⟺  {(1) n=m(2) y1≤y2≤⋯≤yn(3) ∃ φ:{1..n}→{1..n} biunivoca, yi=xφ(i).\langle x_1,\dots,x_n\rangle \,\Pi\, \langle y_1,\dots,y_m\rangle \iff \begin{cases} (1)\ n = m \\ (2)\ y_1 \le y_2 \le \dots \le y_n \\ (3)\ \exists\, \varphi : \{1..n\} \to \{1..n\}\ \text{biunivoca},\ y_i = x_{\varphi(i)}. \end{cases} I vincoli (1) e (2) dicono che la soluzione è una sequenza ordinata della stessa lunghezza, il (3) che è una permutazione degli elementi dell'istanza. Qui SS contiene anche sequenze non ordinate: non è richiesto che ogni elemento di SS sia in relazione con qualche istanza. Si potrebbe restringere SS alle sole sequenze ordinate, S′={⟨y1..yn⟩:y1≤⋯≤yn}S' = \{\langle y_1..y_n\rangle : y_1 \le \dots \le y_n\}, e il vincolo (2) diventerebbe implicito.

Radici quadrate di un numero complesso non nullo (domanda tipica di prima parte). L'istanza è un numero complesso z≠0z \ne 0: I=C−{0}I = \mathbb{C} - \{0\}. Ogni z≠0z \ne 0 ha due radici quadrate distinte, ww e −w-w, entrambe diverse da zero. Se la soluzione è la coppia delle due radici, S=(C−{0})×(C−{0})=(C−{0})2S = (\mathbb{C} - \{0\}) \times (\mathbb{C} - \{0\}) = (\mathbb{C} - \{0\})^2, e Π={(z,(w,−w)):w2=z}⊆I×S.\Pi = \{(z, (w, -w)) : w^2 = z\} \subseteq I \times S. Le soluzioni effettivamente usate stanno nel sottoinsieme S′={(w,−w):w≠0}⊂SS' = \{(w, -w) : w \ne 0\} \subset S. Anche qui Π\Pi non è univoca: per z=−1z = -1 sono soluzioni sia (i,−i)(i, -i) sia (−i,i)(-i, i), perché la coppia è ordinata. Le altre alternative del test non vanno: S=C−{0}S = \mathbb{C} - \{0\} conterrebbe una sola radice (è il problema "trova una radice"), I=(C−{0})2I = (\mathbb{C}-\{0\})^2 avrebbe coppie in ingresso, e C+\mathbb{C}^+ non descrive i numeri complessi non nulli.

Analisi di un algoritmo

Di ogni algoritmo si studiano correttezza e complessità (in tempo e a volte in spazio, cioè la memoria necessaria).

Definizione (correttezza). Provare che AΠA_\Pi è corretto per Π\Pi significa dimostrare ∀i∈I:AΠ(i)=s ⟹ i Π s.\forall i \in I : A_\Pi(i) = s \ \Longrightarrow\ i \,\Pi\, s. Informalmente: la funzione di ingresso e uscita realizzata dall'algoritmo è coerente con la specifica.

Lo strumento principale è l'induzione: sulla taglia per gli algoritmi ricorsivi (Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →), sugli invarianti per quelli iterativi (Dimostrazioni, induzione e invariantiTecniche di dimostrazione (esempio, controesempio, assurdo), induzione con casi base multipli, invarianti di ciclo (inizializzazione, conservazione, uso alla fine), schema generale per provare la correttezza; esempi svolti su arrayMax, sequenza di bit e numeri di Fibonacci.Dimostrazioni, induzione e invarianti →).

Definizione (modello di costo). Un modello di costo associa a ogni istruzione del modello computazionale un costo non negativo cistr∈R+∪{0}c_{\text{istr}} \in \mathbb{R}^+ \cup \{0\}. Il costo dell'esecuzione di AΠA_\Pi sull'istanza ii è tAΠ,i=∑istruzioni eseguite da AΠ(i)cistr.t_{A_\Pi, i} = \sum_{\text{istruzioni eseguite da } A_\Pi(i)} c_{\text{istr}}.

Nel corso si usano costi unitari o nulli: essendo interessati all'ordine di grandezza, si ignorano le costanti e si conta solo il tipo di istruzione che si ritiene dominante. La scelta va fatta con giudizio, perché da lei dipende se l'analisi è significativa:

  • aritmetica su interi in binario: le operazioni elementari tra bit;
  • aritmetica su matrici di numeri complessi: le operazioni aritmetiche tra numeri complessi;
  • ordinamento: il confronto tra due chiavi.

I conteggi esatti che si incontrano nel corso (per esempio n−1n - 1 confronti per il massimo di nn elementi) valgono nel modello in cui si è dichiarato quale istruzione costa 11 e quali costano 00.

Definizione (taglia). La taglia di un'istanza, ∣i∣∈N\lvert i\rvert \in \mathbb{N}, è una misura intera non negativa ragionevolmente rappresentativa della grandezza della codifica dell'istanza.

Non è una definizione rigorosa: la taglia si sceglie volta per volta.

  • Due interi z1,z2z_1, z_2: il numero totale di bit per rappresentarli.
  • Sequenza di nn interi da ordinare: nn.
  • Due matrici quadrate n×nn \times n: indifferentemente nn oppure n2n^2 (numero di elementi di una matrice), perché entrambe descrivono bene la grandezza.

Si pone In={i∈I:∣i∣=n}I_n = \{i \in I : \lvert i\rvert = n\}. Poiché tA,it_{A,i} varia da un'istanza all'altra, lo si riassume con una funzione TA:N→R+∪{0}T_A : \mathbb{N} \to \mathbb{R}^+ \cup \{0\} della sola taglia:

Definizione (tre complessità).

  • Caso peggiore: TA(n)=sup⁡i∈IntA,iT_A(n) = \sup_{i \in I_n} t_{A,i}.
  • Caso migliore: TA(n)=inf⁡i∈IntA,iT_A(n) = \inf_{i \in I_n} t_{A,i}.
  • Caso medio: TA(n)=E[tA,i]T_A(n) = \mathbb{E}[t_{A,i}], media rispetto a una distribuzione fissata su InI_n.

Il caso peggiore è conservativo (limita il costo di qualsiasi esecuzione di taglia nn) ed è quello usato di default nel corso. Il caso migliore di solito dice poco; il caso medio è molto difficile da stimare in teoria ma può essere prezioso in pratica.

Esempio (ricerca lineare). Cerco xx scandendo un array di n=10n = 10 elementi distinti, costo unitario per ogni confronto A[k]=xA[k] = x:

python
def lineare(A, x):
    confronti = 0
    for v in A:
        confronti += 1
        if v == x:
            break
    return confronti

A = list(range(10))
print(lineare(A, 0))                                  # caso migliore: 1
print(lineare(A, -1))                                 # caso peggiore (assente): 10
print(sum(lineare(A, x) for x in A) / len(A))         # medio, x presente e uniforme: 5.5

Caso migliore 11 (l'elemento è il primo), caso peggiore nn (l'elemento è l'ultimo o manca), caso medio (n+1)/2(n+1)/2 se xx è in posizione uniforme: 1n∑k=1nk=n+12=5,5\frac{1}{n}\sum_{k=1}^{n} k = \frac{n+1}{2} = 5{,}5.

Esempio (la taglia di un intero conta). Per decidere se un intero mm è primo, la divisione per tentativi esegue circa m\sqrt{m} divisioni (per m=1 000 003m = 1\,000\,003, che è primo, ne servono 999999). Se come taglia si prende mm stesso, il costo è m\sqrt{m}; ma la taglia della codifica binaria è b=⌈log⁡2m⌉b = \lceil\log_2 m\rceil bit, e in funzione di bb il costo è ≈2b/2\approx 2^{b/2}: esponenziale nella taglia. Un algoritmo che sembra radice quadrata è dunque esponenziale rispetto alla grandezza dell'ingresso. Lo stesso accade quando nn è il numero da calcolare e non la lunghezza di una sequenza: lo si ritrova nel conteggio del calcolo memoizzato di una ricorrenza in Esercizio - funzione g(n) definita per ricorrenza, calcolo memoizzato e formula chiusa (compito del 19 giugno 2024, es. 2).

Sommario: che cosa specificare

Per analizzare correttamente un algoritmo AΠA_\Pi bisogna sempre dire:

  1. quali sono le istruzioni a costo unitario;
  2. che cosa si intende per taglia ∣i∣\lvert i\rvert;
  3. quale complessità si studia (per default: tempo, caso peggiore).

Il passo seguente è ottenere T(n)T(n) per gli algoritmi ricorsivi, cioè scrivere e risolvere una ricorrenza: Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →.

Domande d'esame

  1. Sia Π\Pi il problema dell'estrazione delle radici quadrate di un numero complesso non nullo. Si definiscano II e SS. Risposta: I=C−{0}I = \mathbb{C} - \{0\}, S=(C−{0})2S = (\mathbb{C}-\{0\})^2 (la coppia delle due radici, ciascuna non nulla); Π⊆I×S′\Pi \subseteq I \times S' con S′={(w,−w)}S' = \{(w,-w)\}. Vedi Esercizio - domande di prima parte su problemi computazionali, ricorrenze e conteggi (simulazioni d'esame).
  2. Un problema computazionale è una funzione? Che relazione c'è con l'algoritmo? Risposta: no, è una relazione totale e non necessariamente univoca; l'algoritmo è una funzione che a ogni istanza associa una sola delle sue soluzioni.
  3. Che cosa si deve specificare prima di dichiarare "l'algoritmo costa T(n)T(n)"? Risposta: istruzioni a costo unitario, taglia dell'istanza e tipo di complessità (caso peggiore, migliore o medio).

Versione ripasso

Esercizi su questo argomento

Teoria collegata