09/10/09 Esercizio preliminare (Programmazione 1) Università di Torino – Facoltà di Scienze MFN Corso di Studi in Informatica Curriculum SR (Sistemi e Reti) Algoritmi e Laboratorio a.a. 2009-10 Lezioni prof. Elio Giovannetti Lezione 6– Segmento di somma massima versione 09/10/09 Dato un array di interi rappresentante una sequenza di temperature giornaliere, trovare la lunghezza del più lungo periodo di gelo, cioè della più lunga sequenza consecutiva di temperature negative o zero. INVARIANTE: • la variabile max contiene la lunghezza massima di gelo nel periodo a[0 .. i-1] • la variabile lun contiene la lunghezza del periodo corrente di gelo, cioè del periodo di gelo che arriva fino ad a[i – 1]. i Quest' opera è pubblicato sotto una Licenza Creative Commons Attribution-NonCommercial-ShareAlike 2.5. a: AlgELab-09-10 - Lez. 6 Definizioni preliminari Origine storica del problema • segmento non vuoto di una sequenza non vuota a0, a1, ..., an-1 è una qualunque sottosequenza ak, ak+1 ..., ah di elementi consecutivi, con 0 ≤ k ≤ h ≤ n-1; Il problema nacque in una procedura di pattern-matching bidimensionale progettata da Ulf Grenander della Brown University. In essa, la sotto-matrice di somma massima forniva una stima della massima probabilità di in certo tipo di pattern in un'immagine digitalizzata. Poiché l'algoritmo ingenuo cubico era troppo lento, Grenander semplificò il problema considerando una sola dimensione, e scoprì un algoritmo g quadratico. Nel 1977 descrisse il problema a M. Shamos, che in una notte trovò un algoritmo di complessità n log n. Poco tempo dopo, Shamos presentò il problema in un seminario alla Carnegie-Mellon University cui assisteva il matematico Jay Kadane, il quale nel tempo di un minuto trovò l'algoritmo lineare. L'algoritmo non fu poi utilizzato direttamente, ma la tecnica su cui si basa ha avuto un forte impatto sui metodi di progettazione degli algoritmi. • in particolare, per una sequenza realizzata come array: segmento non vuoto di un array non vuoto a è una qualunque sottosequenza a[k], a[k+1] ..., a[h] di elementi consecutivi, con 0 ≤ k ≤ h < a.length; • useremo la notazione abbreviata a[k .. h] per indicare il segmento a[k], a[k+1] ..., a[h] (cioè il segmento da k ad h, estremi compresi); potremo scrivere a[k] invece di a[k .. k]; • useremo talvolta la notazione abbreviata somma(a[k .. h]) per indicare la somma degli elementi del segmento, cioè: a[k] + a[k+1] + ... + a[h] (Nota Bene: tali notazioni NON sono del linguaggio Java). AlgELab-09-10 - Lez. 6 Problema: ricerca del segmento di somma massima. Si definisca una procedura la quale, preso un array (non vuoto) di interi, restituisca la somma degli elementi del segmento (non vuoto) di somma massima. Esempi array {-7, 4, -8, 3, 4, -2, 6, -10, 1, 3, -9, 9}: il segmento di somma massima è quello sottolineato, e la sua somma è 11; AlgELab-09-10 - Lez. 6 Il segmento di somma massima: soluzione ingenua • Si calcolano le somme di tutti i possibili segmenti, tenendo memoria della massima. array {4, 3, 5, 0, 8, 1} il segmento di somma massima è l’intero array, e la sua somma è 21; array {-4, -3, -5, -8} il segmento di somma massima è quello costituito dal solo elemento sottolineato, e la sua somma è -3. AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 1 09/10/09 Il segmento di somma massima Soluzione naturale: public static int sss(int[] a) { int n = a.length; int max = a[0]; for(int i = 0; i < n; i++) { calcola la somma di a[i .. j] per ogni j compreso fra i ed n-1 e confrontala con max, eventualmente aggiornando max; } } Complessità dell'algoritmo (numero di passi) n iterazioni del ciclo esterno: prima iterazione: n iterazioni del ciclo interno; seconda iterazione: n-1 iterazioni del ciclo interno; ... i-esima iterazione: n-i iterazioni del ciclo interno; Complessità della soluzione naturale n + (n-1) + (n-2) + ... + 2 + 1 = n(n+1)/2 = Θ(n2) C'è un algoritmo più efficiente e più semplice ! Per trovarlo, occorre scrivere un invariante un po' sofisticato. AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 Soluzione ottima: l’idea di base La soluzione ottima: definizioni ausiliarie. • Si percorre l'array sommando gli elementi successivi in una variabile som che contiene quindi la somma corrente, tenendo in una variabile maxSom la massima somma trovata fino a quell'istante, e confrontando ad ogni passo som con maxSom (e aggiornando, se è il caso, maxSom). • Nota: si sommano anche gl’interi negativi, perché possono essere seguiti da interi positivi che permettono di raggiungere una somma più grande. Esempio: 3, 4, -2, 1, 3 somma 9. • Se però diventa negativa la somma corrente, al passo successivo bisogna cominciare una nuova somma corrente, perché ogni somma che contenga il segmento “negativo” sarà minore della corrispondente somma che non lo contiene. Esempio: 3, -2, 4, 6, -15, 7, 3, … con -15 si ottiene la somma -4; allora si riparte da 7 Per semplificare il discorso successivo, introduciamo alcune definizioni e notazioni ausiliarie. Sia a l'array considerato. Allora definiamo: STj = l'insieme delle somme dei segmenti terminanti in j, cioè l'insieme dei valori somma(a[k .. j]), per 0 ≤ k ≤ j Esempio: 6 0 1 2 3 4 5 -3 8 -2 6 0 1 2 3 ... 5 -2 -6 si ha ovviamente ST4 = {8, 12, 6, 4, 9} som (4) = 12 8 -2 6 9 -3 8 -2 6 14 5 -3 8 -2 6 Che relazione c'è fra som(j –1) (la massima somma terminante in j-1) e som(j) (la massima somma terminante in j) ? Le somme (di segmenti) terminanti nella posizione j sono: • il valore a[j]; • le somme terminanti in j-1 incrementate di a[j] STj-1 STj j-1 a[j] j-1 a[j] a[j] ST2 = {-6, -8, -3} som (2) = -3 a[j] a[j] s ∈ STj ⇒ s ≤ som(j) AlgELab-09-10 - Lez. 6 12 AlgELab-09-10 - Lez. 6 Definizioni ausiliarie (continua) 0 1 2 3 4 5 -2 -6 4 8 -2 6 ST4 = {6, 4, 12, 9, 14} AlgELab-09-10 - Lez. 6 som(j) = massimo dell'insieme STj, cioè maxst(j) è la più grande delle somme dei segmenti terminanti iiiiiiiiiiiiiiiibbnella posizione j (inclusa), o massima somma terminante in j. Esempi: 0 1 2 3 4 ST4 = {6, 4, 12, 9, 14} 5 -3 8 -2 6 som (4) = 14 6 4 AlgELab-09-10 - Lez. 6 2 09/10/09 Qual è la massima somma terminante in j ? a[j] ? j-1 ≤ a[j] ≤ ≤ a[j] ≤ som(j-1) ≤ a[j] ≤ Qual è la massima somma terminante in j ? Quale sia il maggiore fra som(j-1) + a[j] e a[j] dipende dal segno di som(j-1). som(j-1) a[j] Infatti: se B > 0, si ha B + A > A se B ≤ 0, si ha B + A ≤ A a[j] ≤ ≤ • som(j-1) + a[j] è la massima somma terminante in j composta di almeno due elementi, perché, quali che siano i segni di A, idi B e di C, si ha: A < B ⇒ A + C < B + C; Analogamente: se som(j-1) > 0, allora a[j] + som(j-1) > a[j] se som(j-1) ≤ 0, allora a[j] + som(j-1) ≤ a[j], • a[j] è l’unica altra somma terminante in j. La max. somma terminante in j è quindi la maggiore delle due. Quale delle due è la maggiore ? AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 Caso som(j –1) > 0: esempi. In conclusione: a[j] ≤ j-1 som(j-1) + a[j] se som(j-1) > 0 ≤ a[j] ≤ ≤ a[j] ≤ som(j-1) ≤ som(j) = a[j] ≤ a[j] ≤ ≤ a[j] se som(j-1) ≤ 0 0 1 2 3 4 5 som(5) = som(4) +a[5] 5 -2 -6 4 8 ST4 = {8, 12, 6, 4, 9} som(4) = 12 5 -2 -6 4 8 10 ST5 = {10, 18, 22, 16, 14, 19} som(5) = 12 + 10 = 22 5 -2 -6 4 8 -5 ST5 = {-5, 3, 7, 1, -1, 4} som(5) = 12 + (-5) = 7 AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 Caso som(j –1) <= 0: rappresentazione pittografica Caso som(j-1) <= 0: esempio j-1 ≤ ≤ ≤ 0 1 2 3 ... 5 -2 -6 10 ≤0 som(j-1) 0 1 2 3 ... 5 -2 2 -6 6 -4 4 a[j] [j] ? a[j] ≤ a[j] ≤ ≤0 som(j-1) a[j] ST2 = {-6, -8, -3} som(2) = -3 ST3 = {10, 4, 2, 7} som(3) = 10 ≤ a[j] ≤ som(j) som(j-1) a[j] ≤ a[j] ≤ a[j] ST2 = {{-6 6, -8 8, -3} 3} som(2) = -3 ST3 = {-4, -10, -12, -7} som(3) = -4 Attenzione: come si è visto anche negli esempi, il segno di a[j] è irrilevante per stabilire il valore di som(j); decisivo è solo il segno di som(j-1). AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 3 09/10/09 La soluzione ottima. L'invariante L’insieme di tutti i segmenti possibili è ovviamente dato da: • il segmento costituito dal solo primo elemento a[0]; • i (due) segmenti terminanti all’indice 1: a[0 .. 1] e a[1]. – som(1) è la massima delle somme di tali segmenti; • i (tre) segmenti terminanti all’indice 2: a[0 .. 2], a[1 .. 2], a[1]; – som(2) è la massima delle somme di tali segmenti; • i (quattro) segmenti terminanti all’indice all indice 3; •… La somma massima è quindi il massimo di som(0), som(1), ..., som(n-1), cioè è il massimo dei massimi ! Ma, come abbiamo visto, conoscendo som(j-1) è possibile ricavare som(j) senza calcolare tutte le somme dei segmenti terminanti all'indice j. Nel ciclo si dovranno tenere due variabili contenenti rispettiv.: • la massima somma contenuta in a[0 .. j-1]; • la massima somma terminante all' indice j-1. L'invariante è quindi: maxsom = max{ somma(a[h..k]) | 0 ≤ h ≤ k < j } ⋀ som = max{ somma(a[i..j-1]) | 0 ≤ i < j } oppure, con formulazione alternativa equivalente: som = som(j-1) ⋀ maxSom = max{som(k) | 0 ≤ k < j } j 9 31 5 7 6 -2 10 3 -1 52 maxSom 16 som AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 Il corpo del ciclo, il test, l'inizializzazione Una lieve variante del problema: segmenti vuoti. INVARIANTE: som = som(j-1) ⋀ maxSom = max somma in a[0..j-1]. CORPO DEL CICLO: • aggiorno som ricavando som(j) a partire da som(j-1): if(som > 0) som += a[j]; else som = a[j]; • confronto som con il massimo maxSom, aggiornando maxSom se necessario: if(som > maxSom) maxSom = som; • infine incremento j: j++; TEST: j < a.length; INIZIALIZZAZIONE: som = maxSom = a[0]; Definizioni preliminari • segmento (eventualmente vuoto) di un array (eventualmente vuoto) a è la sequenza vuota, oppure una qualunque sottosequenza non vuota a[k], a[k+1] ..., a[h] di elementi consecutivi, con 0 ≤ k ≤ h < a.length; • indichiamo con a[] il segmento vuoto di a; • indichiamo con a[k .. h] il segmento a[k], a[k+1] ..., a[h] (cioè il segmento da k ad h, estremi compresi); stabiliamo inoltre che per k > h la notazione a[k .. h] sia sinonima di a[] (seg. vuoto) • esempio: i segmenti di un array a di tre elementi sono: a[], a[0], a[1], a[2], a[0..1], a[1..2], a[0..2]. j = 1; Nota: Invece di un ciclo while, si può utilizzare un ciclo for. AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 Una lieve variante del problema. Nota Si definisca una procedura la quale, preso un array di interi, restituisca la somma degli elementi del segmento (eventualmente vuoto) di somma massima. La somma degli elementi di un segmento vuoto sia per definizione uguale a 0. Esempi array {-7, 4, -8, 3, 4, -2, 6, -10, 1, 3, -9, 9}: il segmento di somma massima è quello sottolineato, e la sua somma è 11; Il problema si risolve con un algoritmo strettamente analogo a quello per il problema originale, percorrendo l'array una volta sola, e quindi senza testare prima se si è nel caso di elementi tutti negativi. array {4, 3, 5, 0, 8, 1} il segmento di somma massima è l’intero array, e la sua somma è 21; array {-4, -3, -5, -8} il segmento di somma massima è il segmento vuoto, la cui somma è 0 per definizione. AlgELab-09-10 - Lez. 6 AlgELab-09-10 - Lez. 6 4