Salta al contenuto
Note per Studenti Prodotto di matrici - algoritmo iterativo e ricorsivo a blocchi

Prodotto di matrici - algoritmo iterativo e ricorsivo a blocchi

In questa pagina 5

Si lavora con matrici quadrate n×nn \times n a elementi complessi. Il caso rettangolare si tratta allo stesso modo; il caso quadrato non è una vera limitazione. Per le dimostrazioni sulla struttura a blocchi si assume n=2kn = 2^k con k≥0k \ge 0, così ogni matrice si divide in quattro quadranti n2×n2\frac n2 \times \frac n2 fino alle matrici 1×11 \times 1.

Modello di costo. Costo unitario per ogni operazione aritmetica tra due scalari (somma, sottrazione, prodotto, divisione). Gli assegnamenti, le copie e il calcolo degli indici costano 00. La complessità si esprime in funzione di nn (dimensione della matrice) oppure di N=n2N = n^2 (numero di elementi): sono la stessa quantità descritta in due modi, quindi va sempre dichiarata la metrica di taglia. Un algoritmo con T(n)=Θ(n2)T(n) = \Theta(n^2) è lineare nella taglia NN.

Notazione: le matrici sono lettere maiuscole, i loro elementi la lettera minuscola con due indici, A=[aij]1≤i,j≤nA = [a_{ij}]_{1 \le i,j \le n}. Le precondizioni sull'input (qui: le matrici hanno la stessa dimensione) si chiamano vincoli statici. Gli algoritmi "basati sulla definizione" calcolano ogni elemento del risultato con la formula che lo definisce. Per la definizione di prodotto righe per colonne vedi Operazioni tra matriciLe matrici m×n formano uno spazio vettoriale (somma e prodotto per scalare elemento per elemento); il prodotto righe per colonne corrisponde alla composizione di funzioni lineari, è associativo ma non commutativo; la trasposta scambia righe e colonne e (AB)^T = B^T A^T.Operazioni tra matrici →.

Somma e sottrazione

C=A+BC = A + B è definita da cij=aij+bijc_{ij} = a_{ij} + b_{ij}.

SUM(A, B)
  n <- rows(A)
  for i <- 1 to n do
    for j <- 1 to n do
      c[i][j] <- a[i][j] + b[i][j]
  return C

Ogni for si traduce in una sommatoria con la stessa variabile, il cui termine generico è il lavoro di una iterazione. for annidati diventano sommatorie annidate:

TSUM(n)=∑i=1n∑j=1n1=∑i=1nn=n2=Θ(n2).T_{SUM}(n) = \sum_{i=1}^{n} \sum_{j=1}^{n} 1 = \sum_{i=1}^{n} n = n^2 = \Theta(n^2).

Qualunque algoritmo di somma deve eseguire, al caso peggiore, n2n^2 somme distinte (ogni cijc_{ij} dipende da una coppia diversa di input, e non c'è modo di ottenerlo da altre somme): quindi n2n^2 non è migliorabile e l'algoritmo basato sulla definizione è ottimo. Lo stesso vale per la sottrazione SUB.

Prodotto righe per colonne, versione iterativa

C=A×BC = A \times B è definita da

cij=∑k=1naikbkj,1≤i,j≤n.c_{ij} = \sum_{k=1}^{n} a_{ik} b_{kj}, \qquad 1 \le i, j \le n.

MUL(A, B)
  n <- rows(A)
  for i <- 1 to n do
    for j <- 1 to n do
      c[i][j] <- a[i][1] * b[1][j]
      for k <- 2 to n do
        c[i][j] <- c[i][j] + a[i][k] * b[k][j]
  return C

Si inizializza con il primo prodotto (e non con 00) perché sommare a 00 sarebbe un'operazione in più. Conto le operazioni sostituendo i for con sommatorie. Per ogni coppia (i,j)(i,j) il lavoro è: 11 prodotto iniziale, poi per ciascun k=2,…,nk = 2, \dots, n un prodotto e una somma, cioè 22 operazioni:

T(n)=∑i=1n(∑j=1n(1+∑k=2n2))=∑i=1n∑j=1n(1+2(n−1))=n2(2n−1)=2n3−n2.T(n) = \sum_{i=1}^{n} \Bigl( \sum_{j=1}^{n} \bigl( 1 + \sum_{k=2}^{n} 2 \bigr) \Bigr) = \sum_{i=1}^{n} \sum_{j=1}^{n} \bigl(1 + 2(n-1)\bigr) = n^2 (2n - 1) = 2n^3 - n^2.

La sommatoria interna ha n−1n - 1 termini (da k=2k=2 a k=nk=n), da cui il fattore 2(n−1)2(n-1). In totale i prodotti sono n3n^3 e le somme n3−n2n^3 - n^2 (un prodotto per ogni terna (i,j,k)(i,j,k), e per ogni coppia (i,j)(i,j) una somma in meno dei prodotti). Quindi TMUL(n)=Θ(n3)=Θ(NN)T_{MUL}(n) = \Theta(n^3) = \Theta(N \sqrt N): non è lineare nel numero di elementi.

Esempio (n=2n = 2).

(1234)(5678)=(1⋅5+2⋅71⋅6+2⋅83⋅5+4⋅73⋅6+4⋅8)=(19224350).\begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \begin{pmatrix} 5 & 6 \\ 7 & 8 \end{pmatrix} = \begin{pmatrix} 1\cdot 5 + 2 \cdot 7 & 1 \cdot 6 + 2 \cdot 8 \\ 3 \cdot 5 + 4 \cdot 7 & 3 \cdot 6 + 4 \cdot 8 \end{pmatrix} = \begin{pmatrix} 19 & 22 \\ 43 & 50 \end{pmatrix}.

Le operazioni sono 88 prodotti e 44 somme, cioè 12=2⋅23−2212 = 2 \cdot 2^3 - 2^2, come prevede la formula.

Prodotto ricorsivo a blocchi

Si cerca un algoritmo divide & conquer (vedi 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 →). Per n=2kn = 2^k si scrivono AA e BB a quadranti:

A=(A11A12A21A22),B=(B11B12B21B22),Ahk,Bhk∈Cn2×n2.A = \begin{pmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{pmatrix}, \qquad B = \begin{pmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{pmatrix}, \qquad A_{hk}, B_{hk} \in \mathbb C^{\frac n2 \times \frac n2}.

Proprietà di sottostruttura (proprietà frattale)

Teorema (proprietà frattale del prodotto). I quadranti di C=A×BC = A \times B si ottengono con la stessa formula del prodotto righe per colonne di matrici 2×22 \times 2, dove al posto degli scalari ci sono i quadranti: per 1≤i,j≤21 \le i, j \le 2

Cij=∑k=12AikBkj=Ai1B1j+Ai2B2j.C_{ij} = \sum_{k=1}^{2} A_{ik} B_{kj} = A_{i1} B_{1j} + A_{i2} B_{2j}.

Dimostrazione per C11C_{11} (gli altri tre quadranti sono identici). Con (M)ij(M)_{ij} si indica l'elemento di posto (i,j)(i,j) della matrice MM; per 1≤i,j≤n21 \le i, j \le \frac n2 vale (C11)ij=cij(C_{11})_{ij} = c_{ij} perché C11C_{11} occupa le prime n2\frac n2 righe e colonne.

  1. Si parte dalla definizione e si spezza la somma su kk in due metà, la prima per k≤n2k \le \frac n2 e la seconda per k>n2k > \frac n2: (C11)ij=∑k=1naikbkj=∑k=1n/2aikbkj+∑k=n/2+1naikbkj.(C_{11})_{ij} = \sum_{k=1}^{n} a_{ik} b_{kj} = \sum_{k=1}^{n/2} a_{ik} b_{kj} + \sum_{k=n/2+1}^{n} a_{ik} b_{kj}.
  2. Nella seconda somma si pone k=h+n2k = h + \frac n2 con 1≤h≤n21 \le h \le \frac n2 (traslazione dell'indice, non cambia i termini): =∑k=1n/2aikbkj+∑h=1n/2ai,h+n/2 bh+n/2, j.= \sum_{k=1}^{n/2} a_{ik} b_{kj} + \sum_{h=1}^{n/2} a_{i,h+n/2}\, b_{h+n/2,\,j}.
  3. Si riconoscono i quadranti. Nella prima somma i,k≤n2i, k \le \frac n2 e k,j≤n2k, j \le \frac n2: aik=(A11)ika_{ik} = (A_{11})_{ik} e bkj=(B11)kjb_{kj} = (B_{11})_{kj}. Nella seconda i≤n2i \le \frac n2 e h+n2>n2h + \frac n2 > \frac n2: ai,h+n/2=(A12)iha_{i,h+n/2} = (A_{12})_{ih} e bh+n/2, j=(B21)hjb_{h+n/2,\,j} = (B_{21})_{hj} (riga hh del blocco in basso, colonna jj del blocco a sinistra): =∑k=1n/2(A11)ik(B11)kj+∑h=1n/2(A12)ih(B21)hj=(A11B11)ij+(A12B21)ij.= \sum_{k=1}^{n/2} (A_{11})_{ik} (B_{11})_{kj} + \sum_{h=1}^{n/2} (A_{12})_{ih} (B_{21})_{hj} = (A_{11} B_{11})_{ij} + (A_{12} B_{21})_{ij}.

Quindi C11=A11B11+A12B21C_{11} = A_{11}B_{11} + A_{12}B_{21}. ■\blacksquare

La proprietà dice che un prodotto n×nn \times n si riduce a 8 prodotti n2×n2\frac n2 \times \frac n2 e a 4 somme di matrici n2×n2\frac n2 \times \frac n2.

Algoritmo RMUL

RMUL(A, B)
  n <- rows(A)
  if n = 1 then return [a11 * b11]       { matrice 1x1 }
  * A = [A11 A12; A21 A22], B = [B11 B12; B21 B22] *
  for i <- 1 to 2 do
    for j <- 1 to 2 do
      Cij <- SUM(RMUL(Ai1, B1j), RMUL(Ai2, B2j))
  return C

La riga fra asterischi è un metacomando: non è un'istruzione. In un linguaggio reale la divisione in quadranti si realizza passando alla funzione ricorsiva i parametri che indicano la sottomatrice (riga e colonna di partenza e dimensione), senza copiare niente. Costi: nel caso base c'è un prodotto; nel caso ricorsivo, 8 chiamate su taglia n2\frac n2 più 4 somme di matrici n2×n2\frac n2 \times \frac n2, ciascuna di costo (n2)2(\frac n2)^2 per TSUMT_{SUM}, cioè 4⋅n24=n24 \cdot \frac{n^2}{4} = n^2:

T(n)={1n=1,8 T ⁣(n2)+n2n>1.T(n) = \begin{cases} 1 & n = 1, \\ 8\,T\!\left(\frac n2\right) + n^2 & n > 1. \end{cases}

Analisi con il Master Theorem

Qui a=8a = 8, b=2b = 2, w(n)=n2w(n) = n^2 e la funzione di soglia è nlog⁡ba=nlog⁡28=n3n^{\log_b a} = n^{\log_2 8} = n^3. Poiché w(n)=n2=O(n3−ε)w(n) = n^2 = O(n^{3 - \varepsilon}) con ε=1\varepsilon = 1, siamo nel caso 1 del Master Theorem - enunciato, casi e applicazioniMaster Theorem per T(n) = aT(n/b) + w(n): si confronta w(n) con la funzione soglia n^(log_b a). Caso 1 (w minore di una potenza polinomiale): Theta(n^(log_b a)); caso 2 (w della stessa taglia): Theta(n^(log_b a) log n); caso 3 (w maggiore e regolare): Theta(w(n)). Prova completa dai livelli dell'albero, versione estesa (parti intere, n0 qualsiasi), esempi, e l'uso scorretto quando nessun caso si applica (w = n log n).Master Theorem - enunciato, casi e applicazioni →: T(n)=Θ(n3)T(n) = \Theta(n^3). A dominare è il lavoro alle foglie (n3n^3 foglie, una per terna (i,j,k)(i,j,k)): l'arietà 88 dell'albero fa crescere troppo in fretta il numero di foglie, mentre il lavoro nei nodi interni è piccolo. Non c'è nessun miglioramento rispetto all'iterativo.

Soluzione esatta: stesso conteggio dell'iterativo

Proposizione. T(n)=2n3−n2T(n) = 2n^3 - n^2 per ogni n=2kn = 2^k: RMUL esegue esattamente le stesse operazioni di MUL, solo in ordine diverso.

Dimostrazione per induzione su kk (vedi 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 →).

  • Base (n=1n = 1): T(1)=1T(1) = 1 e 2⋅13−12=12 \cdot 1^3 - 1^2 = 1.
  • Ipotesi: T(n2)=2(n2)3−(n2)2T\bigl(\frac n2\bigr) = 2\bigl(\frac n2\bigr)^3 - \bigl(\frac n2\bigr)^2.
  • Passo: T(n)=8 T ⁣(n2)+n2=8(2n38−n24)+n2=2n3−2n2+n2=2n3−n2.■T(n) = 8\,T\!\left(\tfrac n2\right) + n^2 = 8\left( 2\tfrac{n^3}{8} - \tfrac{n^2}{4} \right) + n^2 = 2n^3 - 2n^2 + n^2 = 2n^3 - n^2. \qquad \blacksquare

La stessa formula si ottiene srotolando l'albero (formula generale: vedi Albero della ricorrenza e formula generale per le ricorrenzeL'albero della ricorrenza ha la forma dell'albero delle chiamate dell'istanza peggiore e T(n) è la somma dei lavori dei nodi. Per la famiglia T(n) = s(n)T(f(n)) + w(n) con base T0 per n <= n0, contando i nodi per livello con le iterate di f e il livello f*(n,n0) si ottiene la formula chiusa T(n) = somma_l [prodotto_j s(f^(j)(n))] w(f^(l)(n)) + [prodotto s] T0, verificata per induzione e applicata a esempi (merge sort, Hanoi, ricorrenze con radice).Albero della ricorrenza e formula generale per le ricorrenze →). Al livello ℓ\ell ci sono 8ℓ8^\ell nodi di taglia n/2ℓn/2^\ell, ciascuno con lavoro (n/2ℓ)2\bigl(n/2^\ell\bigr)^2; i nodi interni sono i livelli ℓ=0,…,k−1\ell = 0, \dots, k-1 e le 8k=n38^k = n^3 foglie valgono 11 ciascuna:

T(n)=∑ℓ=0k−18ℓn24ℓ+8k=n2∑ℓ=0k−12ℓ+n3=n2(2k−1)+n3=n3−n2+n3=2n3−n2.T(n) = \sum_{\ell=0}^{k-1} 8^\ell \frac{n^2}{4^\ell} + 8^k = n^2 \sum_{\ell=0}^{k-1} 2^\ell + n^3 = n^2 (2^k - 1) + n^3 = n^3 - n^2 + n^3 = 2n^3 - n^2.

La somma geometrica ∑ℓ=0k−12ℓ=2k−1=n−1\sum_{\ell=0}^{k-1} 2^\ell = 2^k - 1 = n - 1.

Esempio (n=4n = 4). Il livello 00 ha una chiamata con 44 somme 2×22 \times 2 (cioè 4⋅4=164 \cdot 4 = 16 operazioni); il livello 11 ha 88 chiamate di taglia 22, ognuna con 44 somme di matrici 1×11 \times 1 (44 operazioni), cioè 3232; il livello 22 ha 6464 foglie, 6464 prodotti. Totale 16+32+64=112=2⋅43−4216 + 32 + 64 = 112 = 2 \cdot 4^3 - 4^2.

Verifica con un programma

python
def mul(A, B):                      # iterativo, conta le operazioni
    n, ops = len(A), 0
    C = [[0]*n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            c = A[i][0]*B[0][j]; ops += 1
            for k in range(1, n):
                c += A[i][k]*B[k][j]; ops += 2
            C[i][j] = c
    return C, ops

# n = 1, 2, 4, ..., 64: ops == 2*n**3 - n**2

Per n=8n = 8 il conteggio dà 2⋅512−64=9602 \cdot 512 - 64 = 960 operazioni, e la versione ricorsiva dà lo stesso valore.

Il Master Theorem non basta a battere il cubo: serve ridurre il numero di chiamate ricorsive da 88 a meno, accettando di aumentare il lavoro dei nodi interni. È l'idea di Algoritmo di Strassen e ibridazioneStrassen calcola il prodotto n x n con 7 chiamate ricorsive invece di 8, usando 10 somme/sottrazioni nel divide e 8 nel conquer: T(n) = 7T(n/2) + (9/2)n^2, Theta(n^(log2 7)) con soluzione esatta 7n^(log2 7) - 6n^2; la costante 7 è più che tripla di quella dell'iterativo (2) e Strassen batte l'iterativo solo da n = 655 (per le potenze di 2, da 1024). L'ibridazione cambia il caso base (iterativo per n <= n0): costo n^(log2 7)(2n0+5)/n0^(log2 7 - 2) - 6n^2, minimo per n0 = 8 con costante ~3.92.Algoritmo di Strassen e ibridazione →.

Esercizi collegati

Domande d'esame

  1. Quante operazioni tra scalari esegue il prodotto righe per colonne ricorsivo basato sulla definizione per n=8n = 8? Risposta: 2n3−n2=2⋅512−64=9602n^3 - n^2 = 2 \cdot 512 - 64 = 960 (n3=512n^3 = 512 prodotti e n3−n2=448n^3 - n^2 = 448 somme, perché ogni prodotto si somma nel cijc_{ij} tranne i 6464 primi). Traccia: l'algoritmo ricorsivo fa le stesse operazioni dell'iterativo.
  2. Perché il prodotto ricorsivo a blocchi con 88 chiamate non migliora l'iterativo? Traccia: a=8a = 8, soglia n3n^3, lavoro n2n^2 polinomialmente minore, caso 1 del Master Theorem: dominano le foglie, Θ(n3)\Theta(n^3); per di più il conteggio esatto è identico, 2n3−n22n^3 - n^2.
  3. Perché la somma di matrici è ottima? Traccia: ogni cijc_{ij} richiede la propria somma, quindi servono n2n^2 operazioni e Θ(n2)\Theta(n^2) è anche un limite inferiore.

Versione ripasso

Esercizi su questo argomento

Teoria collegata