Sume parțiale. Șmenul lui Mars
Problema inițială
Să presupunem că avem un șir de numere indexat de la 1, iar asupra șirului primim mai multe întrebări de forma: care este suma valorilor cuprinse între pozițiile și (inclusiv) în șir?
Răspunsul pentru această întrebare se poate calcula foarte ușor dacă realizăm parcurgerea efectivă a șirului de la poziția la poziția și ne-ar lua pași în cel mai rău caz ca să răspundem la o întrebare, complexitatea finală a programului ajungând la , ceea ce pentru valori mai mari de pentru și ar depăși limitele de timp la majoritatea problemelor de algoritmică. Așadar, este nevoie de o optimizare, care se numește „Sume parțiale”.
Prezentarea conceptului
Sumele parțiale reprezintă o optimizare pentru algoritmii care trebuie să afle o sumă pe un interval, iar pe acel interval nu se produc modificări.
Considerăm:
unde suma valorilor de pe prefixul .
Tabloul se calculează în felul următor:
for (int i = 1; i <= n; i++) {
sp[i] = sp[i - 1] + v[i];
}După calculare, putem începe să răspunem la întrebări. Răspunsul nostru pentru un interval , unde va fi:
Faptul că răspunsul nostru este dat de o formulă, va face ca timpul nostru efectuat pentru rezolvarea unei întrebări să fie constant , ceea ce va duce ca programul nostru să aibă o complexitate finală , pentru calcularea tabloului și pentru citirea și răspunderea la întrebări. Totuși, hai să vedem de ce formula menționată mai sus funcționează.
Pentru demonstrație, vom încerca o abordare grafică a formulei. Primul pas constă în adunarea sumei prefixului .
Apoi, va trebui să scădem prefixul .
În final, subsecvența va fi alcătuită din acele poziții care se află în segmentul mov, dar nu în segmentul roșiatic (). Așadar, în urma acestei delimitări o să obținem suma cerută pe intervalul nostru.
Extinderea sumelor parțiale pe matrice
De asemenea, sumele parțiale se pot extinde și pe tablouri bidimensionale. Să presupunem că lucrăm cu matricea care are linii și coloane. Vom defini matricea în felul următor: reprezintă suma valorilor aflate în submatricea care are colțul stânga-sus de coordonate și colțul dreapta-jos de coordonate .
Astfel, putem scrie:
unde reprezintă elementele matricei originale.
Față de cazul , aici vom începe cu demonstrația formulei de calcul a unei sume pe o submatrice, apoi vom și arăta cum se va calcula matricea . Vom analiza niște cazuri particulare de submatrice, apoi vom enunța o formulă finală.
Pentru început, datorită modului în care am definit matricea , primul caz particular pe care îl vom analiza va fi calcularea sumei de pe o submatrice care are colțul stânga-sus de coordonate și colțul dreapta-jos de coordonate . Răspunsul în acest caz va fi , deoarece fix acest lucru ne este calculat de către matricea .
Acum, să analizăm următorul caz: ni se cere să aflăm suma valorilor dintr-o submatrice care are colțul stânga-sus de coordonate și colțul dreapta-jos de coordonate . Formula menționată mai sus nu este corectă, dar este un punct de plecare. Noi vom conține o submatrice în plus în cea determinată de colțurile de coordonate și , anume cea determinată de colțurile și . Așadar, după adunarea sumei date de va fi nevoie să scădem .
Asemenea cazului precedent este si cazul în care noi dorim să aflăm suma unei submatrice care are colțurile și . Similar, nu este suficient, dar este un punct de plecare. Față de cazul precedent, submatricea în plus este cea determinată de colțurile și . În final, formula pentru acest caz va fi .
Acum, putem încerca să deducem o formulă pentru orice submatrice. Să considerăm submatricea determinată de colțurile stânga-sus de coordonate și dreapta-jos de coordonate . Dacă ar fi să adunăm formulele demonstrate în ultimele două cazuri , noi o să scădem de două ori suma din submatricea determinată de colțurile și , în timp ce noi o adunăm doar o dată. Așadar, la formulă se va aduna și suma din submatricea respectivă, pentru a compensa deficitul.
Cu un raționament asemănător celui pentru determinarea formulei pentru cazul general, vom determina și cum se calculează matricea . Să presupunem că vrem să aflăm . Mai întâi vom porni de la a scrie formula pentru a afla suma valorii de pe poziția în matrice (valoare pe care noi o și stim!):
Trecem toți termenii, cu excepția lui , în dreapta și obținem:
Deci, tabloul se poate calcula destul de ușor în timp . Atașăm, mai jos, o secvență de cod în care se calculează matricea .
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sp[i][j] = sp[i - 1][j] + sp[i][j - 1] - sp[i - 1][j - 1] + a[i][j];
}
}Șmenul lui Mars
Să considerăm următoarea problemă: se dă o axă și intervale de forma . Ni se dau după întrebări de forma: câte intervale conțin în interiorul lor punctul de pe axă?
O soluție foarte ineficientă ar fi pentru fiecare întrebare să luăm fiecare interval în parte și să verificăm dacă punctul nostru este inclus sau nu în interval. Soluția este ușor de intuit și de implementat, dar programul nostru ar avea complexitate . Șmenul lui Marius Andrei (Mars) ne poate rezolva această problemă în timp constant, chiar și dacă o extindem pe mai multe dimensiuni (două axe, 3 axe etc.).
Observație
Deși în algoritmica românească, această tehnică este cunoscută sub numele de Șmenul lui Mars, numele ei standard este difference arrays.
Șmenul lui Mars permite efectuarea operațiilor de adăugare a unei valori la toate elementele dintr-un interval (sau o submatrice, pentru cazul în care lucrăm cu o matrice), fără posibilitatea de a primi întrebări între operațiile de adăugare (pentru acest tip de problemă se vor utiliza arborii de intervale, o tehnică care va fi prezentată ulterior).
Când primim actualizările, noi vom efectua niște adunări și niște scăderi pentru a delimita bucata din șir / matrice pe care se efectuează operația. Apoi, valorile efective din structura noastră de date se vor calcula asemănător sumelor parțiale, fapt ce ne poate intui într-o modalitate cum vom efectua aceste operații.
Șmenul lui Mars 1D
Primul pas când aplicăm Șmenul lui Mars unui șir, va trebui să luăm fiecare interval în parte și să delimităm bucata din șir pe care efectuăm operația. Pentru intervalul de poziții , vom actualiza în șmen cu la poziția , ca să ilustrăm că a început un nou interval, și cu la poziția pentru a arăta faptul că intervalul nostru nu cuprinde și poziția . Astfel, vom avea:
mars[x]++;
mars[y + 1]--;unde vectorul reprezintă adunările/scăderile din șmen. Așa cum am zis și mai sus, noi calculăm valorile noastre din șir ca la sumele parțiale, deci, se poate afirma că:
Dupa efectuarea tuturor operațiilor de adăugare pe interval, noi vom calcula printr-o parcurgere simplă valorile din șirul :
for (int i = 1; i <= n; i++) {
mars[i] += mars[i - 1];
v[i] = mars[i];
}Atenție la faptul că suma pe prefixul va fi ținută în . Revenind la problema noastră inițială, răspunsul la fiecare întrebare va fi în , astfel obținând pe query. Evident, dacă vrem să adăugăm o valoare în loc de 1 pe interval, acest lucru se poate realiza foarte ușor:
mars[x] += z;
mars[y + 1] -= z;
/// cod din program
for (int i = 1; i <= n; i++) {
mars[i] += mars[i - 1];
v[i] += mars[i];
}Codul de mai sus poate să susțină și updateuri pe un șir inițial nevid. Dacă problema noastră nu are suficientă memorie pentru menținerea șirului, aceste plusuri și minusuri se pot reține ca evenimente, care se pot sorta după poziție pentru efectuarea lor. Nu o să intrăm în profunzime momentan cu această tehnică, dar o lăsăm ca temă pentru studiu cititorului.
Șmenul lui Mars 2D
Șmenul lui Mars aplicat pe o matrice presupune înțelegerea mai profundă a cum se propagă sumele parțiale pe matrice. Să presupunem că avem submatricea delimitată de colțurile și . Dacă o să facem doar o adunare și o scădere ca la cazul liniar din șmenul lui Mars, propagările noastre vor fi foarte eronate. Să privim în desen de ce:
O să avem, în plus, multe elemente care sunt actualizate. De aceea, se va proceda în felul următor: se va păstra adunarea de la și se va scădea la și . Încă nu este complet, fiindcă pe submatricea vor fi o adunare și două scăderi, deci acum scădem mai mult decât ar trebui, deci va trebui să adunăm și la .
În cod, ar arăta în felul următor:
mars[x][y] += k;
mars[z + 1][y] -= k;
mars[x][t + 1] -= k;
mars[z + 1][t + 1] += k;
/// cod din program
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
mars[i][j] += mars[i - 1][j] + mars[i][j - 1] - mars[i - 1][j - 1];
a[i][j] += mars[i][j];
}
}În final, în urma operațiilor de adăugare, dacă vrem să știm ce valoare se află pe poziția , răspunsul va fi dat de .
Șmenul lui Mars poate fi extins și pe 3 dimensiuni sau chiar mai multe, iar abordarea pe mai multe dimensiuni se va realiza identic, dar o să fie rar întâlnit în problemele de algoritmică cazuri în care să se ceară șmenul lui Mars pe mai mult de două dimensiuni.
Concluzii
Sumele parțiale sunt o optimizare cheie în algoritmică, ajutându-ne să transformăm lucruri precum aflarea unei sume pe un interval dintr-o întreagă parcurgere într-o simplă formulă, cu timp constant de răspuns.
Tehnici precum șmenul lui Mars reprezintă un pas înainte pentru procesarea diverselor probleme care implică lucrul cu queryuri de diverse feluri, fiind o metodă auxiliară utilă când vine vorba de alte tehnici.
Probleme de la olimpiade
- Prefix Sums 1D
- plaja ONI 2010
- gradina ONI 2013
- Breed Counting USACO Silver
- flip01 RoAlgo PreOJI 2024
- sirpal RoAlgo PreOJI 2024
- cdcq RoAlgo Contest #1
- infoarena mostenire2
- infoarena bmatrix
- tnia OJI 2018
- balon ONI 2023
- poseidon RoAlgo Contest #4
- investiție OJI 2023
- Probleme cu sume parțiale
- Probleme cu șmenul lui Mars
- Subsequences Summing to Sevens USACO Silver
- Haybale Stacking USACO Bronze
- ONI 2024 Eras
- Painting the Barn USACO Gold
Probleme de pe Codeforces
- Good Subarrays
- Robert Hood and Mrs Hood
- Running Miles
- Irreductible Anagrams
- Attribute Checks
- Tea Tasting
- Nusret Gokce
- Constant Palindrome Sum
- Two Pointers Step 3 - Codeforces EDU