Matematický operátor sumácie (), aritmetická postupnosť, určovanie počtu vykonaných operácií na základe kódu v určitom programovacom jazyku.
Rýchlokurz matematickej časti
Matematický operátor sumácie ( – Sigma)
Keďže pre spracovanie určitého objemu údajov na vstupe (zväčša uložených v množine, poli, zozname, a podobne) sa používajú v programovacích jazykoch zväčša cykly, počet operácií sa dá vyjadriť pomocou matematického operátora sumácie – .
Napríklad: znamená, že začína na čísle 1 a pokračuje po 8 (vrátane). S každou iteráciou sa sčítava časť za operátorom (v tomto prípade sa teda číslo 1 sčíta presne 8 krát: , výsledok: ).
Toto by sa dalo vyjadriť v kóde takto:
vysledok = 0
for i in range(8): # vykoná sa 8 krát
vysledok += 1
print(vysledok) # 8
Sumácia môže mať rôzny názov a definíciu riadiacej premennej, ako aj hornú hranicu. Všetky nasledujúce zápisy sú platné:
Matematický zápis | Zápis v Python kóde |
suma = 0
for i in range(5, 10):
suma += 1
| |
suma = 0
for i in range(n+1):
suma += 1
| |
suma = 0
i = ... # napr.: môže pochádzať z vonkajšieho cyklu
for j in range(1, n+2):
suma += (i + 1)
|
Avšak, nemôžeme definovať negatívny krok. Napríklad, tento zápis nie je matematicky správny: ( by nikdy nenabudlo hodnotu 0).
Aritmetická postupnosť
Príklady vyššie sa dajú vypočítať manuálne. Ale čo ak by sme mali napríklad postupnosť 100 prvkov, napríklad: .
V tomto prípade by sa postupnosť rozbalila ako . Ručne, číslo po čísle, by sme to sčítavali asi iba dlho...
Avšak, múdri matematici si všimli, že medzi číslami platí určitý vzťah. Ak zoberieme v (rozbalenej) postupnosti vždy dva protiľahlé prvky a sčítame ich, výsledkom bude vždy rovnaké číslo. To znamená, že pre výpočet súčtu všetkých členov aritmetickej postupnosti vyššie nám stačí sčítať dva protiľahlé prvky (spravidla prvý a posledný prvok), vynásobiť ich počtom prvkov a vydeliť dvomi:

Výsledok: . Samozrejme, je dôležité že číselný odstup od každého prvku (respektíve, súčet dvoch protiľahlých párov) je rovnaký. Napríklad, postupnosť čísiel nie je aritmetickou postupnosťou a teda by sme nemohli aplikovať metódu ktorá je znázornená vyššie.
Všeobecný vzorec pre výpočet súčtu prvkov aritmetickej postupnosti je:
Horná a dolná hranica postupnosti
Každá aritmetická postupnosť má hornú a dolnú hranicu, ktorou vyjadrujeme maximálny možný rozsah hodnôt ktoré môže postupnosť obsahovať. Spravidla:
- dolnou hranicou je prvý prvok aritmetickej postupnosti;
- hornou hranicou je posledný prvok aritmetickej postupnosti;
Počet prvkov aritmetickej postupnosti () definovanej pomocou sumácie () môžeme vypočítať ako:
(plus 1 – pretože berieme do úvahy aj prvý prvok postupnosti)
Určovanie počtu vykonaných operácií v kóde
Poďme ale začať pekne od začiatku a krok po kroku. Začneme jednoduchými príkladmi a postupne budeme zvyšovať náročnosť.
Majme napríklad tento kód pre zistenie názvu dňa podľa jeho poradia v týždni (čiže od 1 pre pondelok až po 7 pre nedeľu):
DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
# 0 1 2 3 4 5 6
poradie_v_tyzdni = 3 # tretí deň v týždni
den = DNI[poradie_v_tyzdni-1] # -1, pretože indexujeme od 0
print(den)
# "Streda" – tretím dňom v týždni je streda
Tuto sa ešte nenachádza žiadna premenná , keďže vstup sa pre kód nikdy nemení – vždy máme pevne danú množinu v ktorej je 7 prvkov (dní) a vyberáme práve jeden, v kóde nie sú žiadne cykly ani iné vetvy).
Nezáleží na tom, koľkokrát kód vyššie spustíme, môžeme tvrdiť že vždy sa vykonajú práve 4 operácie:
- vytvorenie premennej
DNI
; - vytvorenie premennej
poradie_v_tyzdni
; - vytvorenie premennej
den
; - výpis tejto premennej na terminál;
Teda, .
Čo sa vlastne myslí pod pojmom "operácia"? Napríklad, kód vysledok = (1 + 2) * (2 ** 5)
má hneď niekoľko operácií – je potrebné ich ďalej deliť?
Operácia je iba abstrakciou nad nejakou časťou kódu ktorá niečo robí. Môže ísť napríklad o vykonanie nejakej funkcie, priradenie premennej, ale aj o porovnanie a výmenu prvkov v množine (napríklad pri algoritme Bubble sort).
Operácia vyššie síce vykonáva viacero aritmetických operácií (sčítanie, umocnenie, vynásobenie), ale rátame ju iba ako jednu operáciu. Je to z toho dôvodu, že nikdy nemôžeme presne určiť časovú zložitosť algoritmu (rôzne procesory a architektúry majú rôznu rýchlosť, a tak ďalej), vnímame to na vyššej abstraktnej úrovni. Väčšinou sa jedna operácia berie ako jeden riadok kódu ktorý niečo robí.
Určovanie počtu operácií v kóde pre rôzne veľkosti vstupu
Aby sme dokázali vyjadriť časovú zložitosť algoritmu, musíme všeobecne vyjadriť funkciu pre zistenie počtu operácií pre dané (veľkosť vstupu) – algoritmus ktorý má menší počet operácií je efektívnejší. Funkcia, ktorá udáva počet operácií v závislosti od veľkosti vstupu je definovaná matematicky ako . Jej výsledná hodnota pre dané predstavuje najväčší počet operácií ktorý vykoná daný kód (pri vstupe o veľkosti ).
Pri týchto typoch úloh máme zadaný konkrétny kód v nejakom programovacom jazyku (napríklad v Pythone) a musíme určiť funkciu . Kľúčom je prepísať si kód na matematický zápis ktorý vyjadruje počet operácií a potom to čo najviac zjednodušiť a zovšeobecniť (aby sme tam mali nanajvýš iba premennú , bez žiadnych riadiacich premenných ako , , a podobne).
Majme kód v Pythone. Chceme vyjadriť funkciu pre počet výpisov:
for i in range(n):
print("*", end="")
Už vieme, že print
má konštantnú časovú zložitosť (tvárime sa, že je to jedna operácia v kóde, teda akoby číslo 1). Matematicky môžeme teda cyklus vyššie vyjadriť ako: .
Využívame pritom vlastnosti funkcie range
– začíname od čísla 0 a ideme až pokým nedosiahneme číslo (ktoré ale už v cykle nie je zahrnuté, pretože range
vynecháva posledný prvok: range(3) => [0, 1, 2] # spolu tri prvky, ale bez 3
). Dostali sme matematický zápis aritmetickej postupnosti, ktorú už vieme vyjadriť:
Prečo: vieme, že horná hranica je a dolná hranica je . Počet prvkov (jednotiek) teda získame ako: . Potom iba spočítame, koľkokrát sa tam nachádza číslo jedna (toľkokrát, koľko je tam prvkov, keďže všetky prvky sú 1). Teda, .
Po zjednodušení je konečný výsledok: . Hmm, kto by povedal že takýto kód bude volať print
presne n
-krát 😱? Ale aspoň sme dokázali, že naše matematické metódy fungujú...
Teraz skúsime niečo iné:
for i in range(1, 2 * n):
print("*")
print(".")
Úlohou je (opäť) zistiť počet volaní na funkciu print
(vyjadriť to všeobecne ako matematickú funkciu časovej zložitosti – ). Máme jeden cyklus s dvomi volaniami na print
. Tento cyklus generuje čísla od 1
po 2 * n
. Keďže jazyk Python nezahŕňa hornú hranicu do čísiel ktoré generuje, cyklus sa zastaví na čísle (2 * n) - 1
. No a dva print
y v cykle iba znamenajú, že sčítavame dve operácie (dve jednotky) – sčítavanie robíme vždy ak máme za sebou sekvenciu operácií na rovnakej úrovni odsadenia. Matematicky vieme cyklus vyššie zapísať ako:
Určíme počet prvkov: .
To znamená, že počet v cykle je . Výsledok je teda:
Skúsme ešte niečo ťažšie. Určme počet výpisov (volaní na print
) pre nasledovný kód:
for i in range(1, n):
print(i + n)
for j in range(n-2):
for k in range(n-2):
print((i * n + j) * n + k)
print(j * 2)
Na prvý pohľad sa to môže zdať zložité, pretože používame v print
och viacero premenných. Avšak, zaujíma nás iba počet volaní na print
, nie konkrétne hodnoty ktoré sa vypíšu. Teda, nemusíme vôbec brať do úvahy čo sa v print
och nachádza.
Máme 3 cykly ktoré sú vnorené, pritom každý z nich realizuje jeden print
. Keďže sekvenčné operácie sa sčítavajú a sčítavanie je kumulatívna operácia (nezáleží v akom poradí sčítavame, výsledok je rovnaký), môžeme si kód predstaviť zjednodušene. Ak k tomu ešte pridáme fakt že nás nezaujíma obsah print
ov, tak z hľadiska analýzy počtu operácií je tento kód identický s kódom vyššie:
for i in range(1, n): # 1. cyklus
print(1)
# +
for j in range(n-2): # 2. cyklus
print(1)
# +
for k in range(n-2): # 3. cyklus
print(1)
To už vyzerá trochu jednoduchšie. Matematicky to môžeme zapísať ako:
V hornej hranici nesmieme zabudnúť pridať -1, pretože Python nezahŕňa túto hodnotu v range
(ako už vieme, cyklus skončí pri predposlednej hodnote). Ak máme viacero vnorených cyklov, začíname vždy od cyklu ktorý je najviac vnorený (pamätajme si, že počet prvkov sa vypočíta ako ):
Hodnotu z cyklu na tretej úrovni dosadíme do druhého cyklu:
Nakoniec iba dopočítame vonkajší cyklus:
Teda, počet operácií pre kód vyššie je daný funkciou (kde je veľkosť vstupu – číslo ≥ 1).
Riešenie môžeme overiť pre niekoľko vstupov, ak do kódu doplníme premennú ktorú budeme navyšovať pri každom volaní na print
:
vstupy = range(2, 7)
for n in vstupy:
pocet_printov = 0
# --------------------------- #
for i in range(1, n):
# print(1)
pocet_printov += 1
for j in range(n - 2):
# print(1)
pocet_printov += 1
for k in range(n - 2):
# print(1)
pocet_printov += 1
# --------------------------- #
# naša funkcia `T(n)`:
ocakavany_pocet = (n**3) - 4*(n**2) + (6*n) - 3
print("pre n =", n)
print("očakávaný počet printov:", ocakavany_pocet)
print("skutočný počet printov:", pocet_printov)
print()
Po spustení vidíme, že očakávaný počet operácií sa zhoduje so skutočným počtom, riešenie je teda správne (pre veľkosti vstupov n
od 2 po 6):
pre n = 2
očakávaný počet printov: 1
skutočný počet printov: 1
pre n = 3
očakávaný počet printov: 6
skutočný počet printov: 6
pre n = 4
očakávaný počet printov: 21
skutočný počet printov: 21
pre n = 5
očakávaný počet printov: 52
skutočný počet printov: 52
pre n = 6
očakávaný počet printov: 105
skutočný počet printov: 105
Určovanie počtu operácií v kóde pre rôzne veľkosti vstupu (s riadiacimi premennými)
Častokrát sa stane, že počas cyklu používame aj riadiacu premennú (napríklad ) ktorá mení svoju hodnotu s každou iteráciou. Určenie funkcie môže byť pre takéto cykly zložité, pretože nemôžme jednoznačne určiť akú hodnotu bude mať (na rozdiel od ktoré sa nemení).
Majme napríklad cyklus . Všeobecne je to jednoducho , avšak my odtiaľ potrebujeme dať preč premennú (pretože sa mení s každou iteráciou, a napokon, funkcia ani neobsahuje miesto pre ďalší parameter kam by sa zmestilo).
Keď nevieme, ako sa pohnúť ďalej tak je dobré si cyklus rozpísať s konkrétnymi číslami namiesto (objaví sa nám vzor, podľa ktorého dokážeme identifikovať vzťahy medzi jednotlivými iteráciami a odvodiť všeobecný vzorec). Teda, cyklus vyššie je v podstate:
Prvým prvkom je vždy číslo 1 (pretože tak je to definované pod sumou: ) a posledným prvkom je (čo vidíme hore). Počet prvkov vieme zistiť pomocou vzorca: (je to očividné, avšak tento vzorec pomôže v prípade ak cyklus začína od čísla iného ako 1, aby sme sa zbytočne nepomýlili).
Ak máme tieto informácie, vieme ich vložiť do vzorca pre výpočet súčtu prvkov aritmetickej postupnosti a získame funkciu :
Výsledok: počet operácií pre ľubovoľnú veľkosť vstupu môžeme určiť ako .
Pri takomto type úloh sa treba zamyslieť, že keď čísla rozpisujeme, tak namiesto troch bodiek tam môže byť akákoľvek veľká postupnosť čísiel – preto rozpíšeme iba zopár, aby sme lepšie videli vzory ktoré sa opakujú a vedeli pre ne odvodiť všeobecnú funkciu. Zameriavame sa na to, o aký typ postupnosti sa jedná (aritmetická/geometrická – o tej bude reč v ďalšej téme), ktorý prvok je prvý a ktorý posledný a koľko je prvkov spolu.
Presuňme sa na analýzu konkrétnych zdrojových kódov. Majme napríklad:
n = int(input("n: "))
op = 0
for i in range(n):
for j in range(i):
op += 1
print(op)
Cieľom je určiť, koľkokrát sa zrealizuje súčet (respektíve, koľkokrát sa vykoná riadok číslo 6).
Prepíšeme cyklus (riadok 4 až 6) na matematický zápis (nesmieme zabudnúť od hornej hranici odpočítať 1!):
.