Aritmetički trougao - rešenje
Pokušajte najpre da samostalno rešite zadatak. Čak i ako ne dođete do kompletnog rešenja, pokušaj razvijanja ideje je veoma važan za razvoj algoritamskog razmišljanja.
Uvod
Ovaj primer pokazuje razliku između edukativnog (simulacionog) pristupa i efikasne matematičke formule. Cilj je da razumete kako se redovi u trouglu grade, kako se iz toga izvode prvi i poslednji broj reda i, na kraju, zbir reda. Prvo je prikazano Rešenje 1, koje „gradi” trougao red po red, a zatim Rešenje 2, koje do rezultata dolazi direktno, formulom.
Tekst zadatka
Koliki je zbir brojeva u datom redu sledećeg trougla?
1
2 3 4
5 6 7 8 9
10 11 12 13 14 15 16
...
Ulaz
Sa standardnog ulaza se učitava broj n redova za koje je potrebno izračunati zbir
(1 ≤ n ≤ 50 000). Nakon toga se učitava n rednih brojeva redova k
(1 ≤ k ≤ 5·104) čiji zbir treba izračunati. Brojanje redova počinje od 1.
Izlaz
Za svaki upit, u posebnom redu, ispisati zbir brojeva u zadatom redu trougla.
Primer
Ulaz:
3
1
2
3
Izlaz:
1
9
35
Prvi red sadrži samo broj 1, drugi red 2 + 3 + 4 = 9, a treći red 5 + 6 + 7 + 8 + 9 = 35.
Rešenje 1 — simulacioni (edukativni) pristup
Ovo rešenje simulira gradnju trougla red po red. Za traženi red k iterativno se povećava
broj elemenata po redovima (1, 3, 5, ...) dok se ne dođe do traženog reda.
- Broj elemenata u redu svaki put raste za 2, pa red
jima2·j − 1elemenata. - Prvi broj reda je za 1 veći od poslednjeg broja prethodnog reda.
- Poslednji broj reda dobija se kada se na prvi broj doda
broj_elemenata − 1. - Zbir reda računa se formulom za zbir aritmetičke progresije:
(prvi + poslednji) · broj_elemenata / 2.
Prednost ovog pristupa je jednostavnost i dobra preglednost, ali je vremenska složenost O(k) po upitu.
Prikaži / sakrij rešenje 1
#include <iostream>
#include <vector>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
// Učitavanje broja upita
if (!(cin >> n))
return 0;
vector<int> redovi(n);
// Učitavanje traženih redova
for (int i = 0; i < n; ++i)
cin >> redovi[i];
// Obrada svakog upita posebno
for (int idx = 0; idx < n; ++idx) {
// Red za koji tražimo zbir
int k = redovi[idx];
// Broj elemenata u trenutnom redu
long long broj_elemenata = 0;
// Prvi broj u redu
long long a_prvi = 0;
// Poslednji broj u redu
long long a_poslednji = 0;
// Zbir traženog reda
long long zbir_reda = 0;
// Iterativno gradimo trougao do k-tog reda
for (int j = 1; j <= k; ++j) {
// Svaki red ima 2 elementa više od prethodnog
if (j == 1)
broj_elemenata = 1;
else
broj_elemenata += 2;
// Prvi element reda je za 1 veći
// od poslednjeg elementa prethodnog reda
a_prvi = a_poslednji + 1;
// Poslednji element reda
a_poslednji = a_prvi + (broj_elemenata - 1);
// Kada dođemo do traženog reda
if (j == k) {
// Formula za zbir aritmetičke progresije
zbir_reda = (a_prvi + a_poslednji) * broj_elemenata / 2;
cout << zbir_reda << '\n';
}
}
}
return 0;
}
Nedostaci rešenja 1 i šta treba popraviti
- Rešenje daje tačne rezultate, ali za svaki upit ponovo gradi trougao od prvog reda.
- U najgorem slučaju ima 50 000 upita za red 50 000, odnosno oko 2,5 milijarde koraka. Izmereno, takav ulaz se izvršava oko 2 sekunde, pa rešenje neće proći najveće testove ako je vremensko ograničenje 1 sekunda.
- Ako želite da zadržite ideju simulacije, trougao je dovoljno izgraditi jednom, do najvećeg traženog reda, i zbirove sačuvati u nizu. Tada svaki upit traje O(1), a ukupna složenost je O(max k + n).
- Dobro je što se koristi
long long: zbir reda 50 000 iznosi oko 2,5·1014, što ne staje uint.
Rešenje 2 — efikasno matematičko rešenje
Za razliku od simulacije, ovde tražimo matematički obrazac, da bismo direktno izračunali broj elemenata u redu, prvi broj, poslednji broj i zbir reda, bez gradnje trougla. Tako dobijamo rešenje sa vremenskom složenošću O(1) po upitu.
Neka je r red za koji želimo zbir (u kodu je to nr[i]).
1. Broj elemenata u r-tom redu
Redovi sadrže neparan broj elemenata, pa je:
k = 2r − 1
Na primer, za r = 3 dobijamo k = 5.
2. Ukupan broj elemenata u prethodnih r − 1 redova
To je zbir prvih r − 1 neparnih brojeva:
T = 1 + 3 + 5 + ... + (2(r − 1) − 1)
Brojevi 1, 3, 5, ... čine aritmetičku progresiju sa prvim članom a1 = 1, razlikom
d = 2 i r − 1 članova. Poslednji član je 1 + (r − 2)·2 = 2r − 3,
pa je zbir:
T = (1 + (2r − 3)) · (r − 1) / 2 = (2r − 2) · (r − 1) / 2 = (r − 1)²
Za r = 3 dobijamo T = 4: pre trećeg reda nalaze se četiri broja (1, 2, 3, 4).
3. Prvi broj u r-tom redu
a1 = T + 1 = (r − 1)² + 1
Za r = 3 dobijamo a1 = 5.
4. Poslednji broj u r-tom redu
Brojevi u redu rastu za 1, pa je an = a1 + (k − 1). Jednostavnije, poslednji broj
r-tog reda jednak je ukupnom broju elemenata u prvih r redova:
an = r²
Za r = 3 dobijamo an = 9.
5. Zbir u r-tom redu
S = (a1 + an) · k / 2 = ((r − 1)² + 1 + r²) · (2r − 1) / 2
Pošto je (r − 1)² + 1 + r² = 2r² − 2r + 2, izraz se skraćuje na:
S = (r² − r + 1) · (2r − 1)
Provera na primeru (r = 3)
a1 = 5, an = 9, k = 5, pa je S = (5 + 9) · 5 / 2 = 35. Iz skraćene formule: (9 − 3 + 1) · 5 = 7 · 5 = 35.
Prikaži / sakrij rešenje 2
#include <iostream>
#include <vector>
using namespace std;
/*
Efikasno matematičko rešenje
Promenljive:
- r -> red koji tražimo
- k -> broj elemenata u redu
- T -> broj elemenata u prethodnim redovima
- a1 -> prvi broj reda
- an -> poslednji broj reda
- S -> zbir reda
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
// Broj upita
if (!(cin >> n))
return 0;
vector<int> nr(n);
// Učitavanje redova
for (int i = 0; i < n; ++i) {
cin >> nr[i];
}
long long a1 = 0;
long long an = 0;
long long S = 0;
long long d = 1; // razlika susednih brojeva u redu
for (int i = 0; i < n; ++i) {
long long r = nr[i];
// Broj elemenata u redu
long long k = 2 * r - 1;
// Broj elemenata do prethodnog reda
long long T = (r - 1) * (r - 1);
// Prvi broj reda
a1 = T + 1;
// Poslednji broj reda
an = a1 + (k - 1) * d;
// Zbir reda
S = (a1 + an) * k / 2;
cout << S << '\n';
}
return 0;
}
Objašnjenje važnih delova programa
ios::sync_with_stdio(false); cin.tie(nullptr);ubrzavaju učitavanje, što je korisno kod 50 000 upita.- Sve vrednosti su tipa
long long, jer zbir reda 50 000 iznosi 249 992 500 149 999, što je mnogo više od najveće vrednosti tipaint(oko 2,1·109). - Deljenje sa 2 u izrazu
(a1 + an) * k / 2je uvek tačno, jer jea1 + an = 2r² − 2r + 2paran broj. - Isti rezultat daje i skraćena formula
(r * r - r + 1) * (2 * r - 1), bez pomoćnih promenljivih.
Složenost rešenja
- Rešenje 1: O(k) po upitu, ukupno O(n · max k). Na najvećem ulazu oko 2 sekunde.
- Rešenje 2: O(1) po upitu, ukupno O(n). Na najvećem ulazu nekoliko desetina milisekundi.
- Oba rešenja proverena su na 1000 nasumičnih upita poređenjem sa direktnim sabiranjem i daju iste rezultate.
Zaključak
Zadatak pokazuje kako se iz posmatranja nekoliko prvih redova dolazi do opšte formule. Ključni koraci su zbir prvih neparnih brojeva, (r − 1)², i formula za zbir aritmetičke progresije. Kada se problem rešava za veliki broj upita, matematičko rešenje u O(1) je ono koje prolazi sve testove.
