← Algorytmy na maturę
0 / 0
Zanim zaczniesz

Algorytmy na maturę z informatyki

Kurs obejmuje algorytmy z podstawy programowej dla zakresu rozszerzonego. Przy każdym jest pomysł, kod, ślad działania na przykładzie i zadania do sprawdzenia się.

Jak wygląda egzamin

Matura z informatyki (od 2023 r.) to jeden arkusz na 210 minut. Część zadań rozwiązujesz na papierze: śledzisz algorytm, uzupełniasz tabelę, piszesz własny algorytm w pseudokodzie albo w wybranym języku. Większość punktów zdobywasz jednak przy komputerze: dostajesz pliki z danymi, piszesz program i zapisujesz odpowiedzi do pliku wynikowego.

Programy możesz pisać w Pythonie, C++ albo Javie. Kurs pokazuje kod w Pythonie (domyślnie), w C++ i w pseudokodzie, który przydaje się w zadaniach na papierze. Wersję przełączasz u góry strony.

Jak korzystać

  • Lekcja zaczyna się od pomysłu. Najpierw go zrozum, potem czytaj kod. Na egzaminie pomysł odtworzysz, a kodu na pamięć nie musisz znać.
  • Przepisz kod u siebie i uruchom go na kilku przykładach. Przepisywanie uczy więcej niż kopiowanie.
  • Zadania „Policz sam” sprawdzają, czy umiesz wykonać algorytm ręcznie. To dokładnie ten typ zadań, który jest w części papierowej.
  • Postęp zapisuje się tylko w tej przeglądarce.
GrupaAlgorytmy w kursie
Liczbycyfry i dzielniki, budowanie liczb z cyfr, liczby pierwsze, sito Eratostenesa, NWD i rozszerzony Euklides, systemy liczbowe, Horner, zapis binarny i operacje na bitach, szybkie potęgowanie
Rekurencjasilnia, Fibonacci, drzewo wywołań, generowanie ciągów, podzbiorów i permutacji
Wyszukiwanieliniowe, min i max, przesunięcie cykliczne, sumy prefiksowe, binarne i szukanie po wyniku
Sortowaniebąbelkowe, przez wybór, przez wstawianie, przez scalanie, szybkie, przez zliczanie, kubełkowe
Tekstypalindromy, anagramy, wyszukiwanie wzorca, szyfr Cezara, Vigenère’a, przestawieniowy
Strukturyplansze 2D i BFS, drzewo BST w tablicy, kopiec, hierarchia z tablicą przełożonych
Numerycznemiejsce zerowe metodą połowienia, pierwiastek metodą Newtona, pole pod wykresem, Monte Carlo, geometria na liczbach całkowitych
Strategiedziel i zwyciężaj, algorytm zachłanny (reszta, przedziały), programowanie dynamiczne
Arkusz i bazyformuły, daty, tabela przestawna, wykres, symulacja dzień po dniu; SQL: grupowanie, łączenie tabel, LEFT JOIN, podzapytania

Tematy dobrane są według arkuszy rozszerzonych z lat 2015–2026. Przy lekcjach są notatki „Na maturze”, a zestawienie arkuszy pokazuje, które zadanie ćwiczy dana lekcja.

Lekcja 1

Wczytywanie danych i zapis wyników

Każde zadanie przy komputerze zaczyna się tak samo: wczytaj plik, policz, zapisz odpowiedź. Ten szkielet warto umieć napisać bez zastanowienia.

Pliki z danymi leżą w folderze podanym w arkuszu. Program uruchamiaj w tym samym folderze, wtedy wystarczy sama nazwa pliku. Odpowiedzi zapisujesz do pliku wynikowego, którego nazwę podaje treść zadania. Każdą odpowiedź poprzedź numerem zadania.

python
# wszystkie liczby z pliku, niezależnie od podziału na wiersze
with open("dane.txt") as f:
    liczby = [int(x) for x in f.read().split()]

# plik czytany wiersz po wierszu, w każdym wierszu dwie liczby
pary = []
with open("pary.txt") as f:
    for wiersz in f:
        a, b = map(int, wiersz.split())
        pary.append((a, b))

parzyste = sum(1 for x in liczby if x % 2 == 0)

with open("wyniki.txt", "w") as out:
    print("1.1.", parzyste, file=out)
c++
#include <fstream>
#include <vector>
using namespace std;

int main() {
    ifstream we("dane.txt");
    vector<int> liczby;
    int x;
    while (we >> x)            // czyta, dopóki są liczby
        liczby.push_back(x);

    int parzyste = 0;
    for (int v : liczby)
        if (v % 2 == 0) parzyste++;

    ofstream wy("wyniki.txt");
    wy << "1.1. " << parzyste << "\n";
}
pseudokod
wczytaj wszystkie liczby z pliku dane.txt do tablicy T[0..n-1]
parzyste ← 0
dla i = 0, 1, …, n-1 wykonuj
    jeżeli T[i] mod 2 = 0
        parzyste ← parzyste + 1
zapisz do pliku wyniki.txt: "1.1.", parzyste

Przykładowy plik dane.txt i wynik programu:

dane.txt
12 7 30
5 8
41
wyniki.txt
1.1. 3
Najczęstszy błąd: program czyta plik z innego folderu niż ten, w którym leżą dane. Gdy pojawia się błąd „nie znaleziono pliku”, sprawdź folder roboczy w swoim edytorze albo podaj pełną ścieżkę.

Wiersze z tekstem

Gdy w wierszu jest słowo, pamiętaj o strip(), bo wiersz kończy się znakiem nowej linii. Metoda split() bez argumentu sama pomija spacje i znaki nowej linii.

Operator >> sam pomija spacje i znaki nowej linii, więc we >> slowo wczyta kolejne słowo. Cały wiersz ze spacjami wczytasz funkcją getline(we, wiersz).

W Pythonie wczytujesz słowa pętlą for w in f: i sprawdzasz w == w[::-1]. Żadne słowo nie wychodzi palindromem. Dlaczego?

  1. Python źle odwraca napisy
  2. Każde w kończy się znakiem \n, który po odwróceniu jest na początku
  3. Plik trzeba otworzyć w trybie "rb"
Lekcja 2

Złożoność obliczeniowa

Złożoność mówi, jak rośnie liczba operacji, gdy rosną dane. Na maturze pytają o nią wprost i pośrednio: program, który działa minutę zamiast sekundy, nie zdąży z dużym plikiem.

Liczymy operacje dominujące, np. porównania albo mnożenia, i patrzymy tylko na najszybciej rosnący składnik. Algorytm, który wykonuje 3n² + 5n + 2 porównań, ma złożoność O(n²).

ZłożonośćNazwaPrzykładn = 1 000 000
O(1)staładostęp do t[i]1
O(log n)logarytmicznawyszukiwanie binarne, szybkie potęgowanie≈ 20
O(√n)pierwiastkowatest pierwszości przez dzielenie1 000
O(n)liniowaminimum, wyszukiwanie liniowe1 000 000
O(n log n)liniowo-logarytmicznasortowanie przez scalanie≈ 20 000 000
O(n²)kwadratowasortowanie bąbelkowe10¹²
O(2ⁿ)wykładniczaFibonacci rekurencyjnie bez pamięcinie do policzenia

Zgrubna miara: zwykły komputer wykonuje około 10⁸ prostych operacji na sekundę w C++ i kilka razy mniej w Pythonie. Pliki maturalne mają zwykle od kilkuset do kilkudziesięciu tysięcy wierszy, więc O(n²) jeszcze zwykle przejdzie, ale O(2ⁿ) już nie.

Pętle w pętlach

Dwie zagnieżdżone pętle po n elementach dają O(n²). Pętla, w której zmienna jest co krok dzielona przez 2, wykona się około log₂ n razy.

Jaka jest złożoność pętli i = n; while i > 1: i = i // 2?

  1. O(n)
  2. O(log n)
  3. O(n / 2)
  4. O(√n)

Algorytm wykonuje n²/2 + 100n porównań. Jaka jest jego złożoność?

  1. O(n)
  2. O(n²)
  3. O(100n)
  4. O(n³)

Ile razy (w przybliżeniu, jako liczbę całkowitą) wykona się pętla połowiąca dla n = 1024?

Lekcja 3

Cyfry liczby i jej dzielniki

Dwie operacje wystarczą do rozebrania liczby na cyfry: reszta z dzielenia przez 10 daje ostatnią cyfrę, a dzielenie całkowite przez 10 ją obcina.

python
def suma_cyfr(n):
    s = 0
    while n > 0:
        s += n % 10       # ostatnia cyfra
        n //= 10          # obcinamy ostatnią cyfrę
    return s

def odwroc(n):
    w = 0
    while n > 0:
        w = w * 10 + n % 10
        n //= 10
    return w
c++
int suma_cyfr(long long n) {
    int s = 0;
    while (n > 0) {
        s += n % 10;      // ostatnia cyfra
        n /= 10;          // obcinamy ostatnią cyfrę
    }
    return s;
}

long long odwroc(long long n) {
    long long w = 0;
    while (n > 0) {
        w = w * 10 + n % 10;
        n /= 10;
    }
    return w;
}
pseudokod
funkcja suma_cyfr(n)
    s ← 0
    dopóki n > 0 wykonuj
        s ← s + (n mod 10)        // ostatnia cyfra
        n ← n div 10              // obcinamy ostatnią cyfrę
    zwróć s

funkcja odwroc(n)
    w ← 0
    dopóki n > 0 wykonuj
        w ← w · 10 + (n mod 10)
        n ← n div 10
    zwróć w
nn % 10s po kroku
202444
20226
2006
228
0—koniec

Dzielniki do pierwiastka

Dzielniki występują w parach: jeśli d dzieli n, to n / d też. Mniejszy z pary jest zawsze ≤ √n. Wystarczy więc sprawdzać d od 1 do √n i dopisywać oba dzielniki z pary. To O(√n) zamiast O(n).

python
def dzielniki(n):
    wynik = []
    d = 1
    while d * d <= n:
        if n % d == 0:
            wynik.append(d)
            if d != n // d:          # para d i n/d, bez dublowania pierwiastka
                wynik.append(n // d)
        d += 1
    return sorted(wynik)

def doskonala(n):
    return sum(dzielniki(n)) - n == n
c++
#include <vector>
#include <algorithm>
using namespace std;

vector<long long> dzielniki(long long n) {
    vector<long long> wynik;
    for (long long d = 1; d * d <= n; d++) {
        if (n % d == 0) {
            wynik.push_back(d);
            if (d != n / d)          // para d i n/d, bez dublowania pierwiastka
                wynik.push_back(n / d);
        }
    }
    sort(wynik.begin(), wynik.end());
    return wynik;
}

bool doskonala(long long n) {
    long long s = 0;
    for (long long d : dzielniki(n)) s += d;
    return s - n == n;
}
pseudokod
funkcja dzielniki(n)
    D ← pusta lista
    d ← 1
    dopóki d · d ≤ n wykonuj
        jeżeli n mod d = 0
            dopisz d do D
            jeżeli d ≠ n div d
                dopisz n div d do D   // para d i n/d
        d ← d + 1
    zwróć D posortowaną rosnąco

funkcja doskonala(n)
    zwróć (suma elementów dzielniki(n)) − n = n
Warunek d * d <= n zamiast d <= sqrt(n) chroni przed błędem zaokrąglenia liczb zmiennoprzecinkowych.

Liczba doskonała to taka, która jest równa sumie swoich dzielników mniejszych od niej, np. 6 = 1 + 2 + 3 i 28 = 1 + 2 + 4 + 7 + 14.

Jaka jest suma cyfr liczby 2024?

Ile dzielników ma liczba 36?

Ile razy wykona się pętla w funkcji dzielniki dla n = 10 000?

  1. 10 000
  2. 100
  3. 5 000
  4. 50
Lekcja 4

Budowanie nowej liczby z cyfr

Częste zadanie maturalne: z cyfr jednej liczby zbudować drugą, np. bez cyfr parzystych albo z każdą cyfrą zamienioną na inną. Wystarczą dzielenie przez 10 i zmienna, która pamięta pozycję następnej cyfry.

Cyfry odczytujemy od końca (n % 10). Żeby nowa liczba miała je w tej samej kolejności, każdą kolejną cyfrę mnożymy przez 1, 10, 100… Ten mnożnik rośnie tylko wtedy, gdy cyfra faktycznie trafia do wyniku.

python
def tylko_nieparzyste(n):        # 52318 -> 531: zostawia cyfry nieparzyste
    wynik = 0
    mnoznik = 1                  # pozycja, na którą trafi następna cyfra
    while n > 0:
        c = n % 10
        if c % 2 == 1:
            wynik += c * mnoznik
            mnoznik *= 10
        n //= 10
    return wynik

def dopelnienie(n):              # każda cyfra c zamienia się na 9 - c
    wynik = 0
    mnoznik = 1
    while True:
        wynik += (9 - n % 10) * mnoznik
        mnoznik *= 10
        n //= 10
        if n == 0:
            return wynik

def przeniesienia(a, b):         # ile razy przy dodawaniu pisemnym jest przeniesienie
    ile = 0
    p = 0
    while a > 0 or b > 0:
        p = (a % 10 + b % 10 + p) // 10   # 1, gdy suma cyfr z przeniesieniem ≥ 10
        ile += p
        a //= 10
        b //= 10
    return ile
c++
long long tylko_nieparzyste(long long n) {   // 52318 -> 531: zostawia cyfry nieparzyste
    long long wynik = 0, mnoznik = 1;        // mnoznik: pozycja następnej cyfry
    while (n > 0) {
        int c = n % 10;
        if (c % 2 == 1) {
            wynik += c * mnoznik;
            mnoznik *= 10;
        }
        n /= 10;
    }
    return wynik;
}

long long dopelnienie(long long n) {         // każda cyfra c zamienia się na 9 - c
    long long wynik = 0, mnoznik = 1;
    do {
        wynik += (9 - n % 10) * mnoznik;
        mnoznik *= 10;
        n /= 10;
    } while (n > 0);
    return wynik;
}

int przeniesienia(long long a, long long b) {   // przeniesienia przy dodawaniu pisemnym
    int ile = 0, p = 0;
    while (a > 0 || b > 0) {
        p = (a % 10 + b % 10 + p) / 10;      // 1, gdy suma cyfr z przeniesieniem >= 10
        ile += p;
        a /= 10;
        b /= 10;
    }
    return ile;
}
pseudokod
funkcja tylko_nieparzyste(n)          // 52318 -> 531
    w ← 0;  mnoznik ← 1               // mnoznik: pozycja następnej cyfry
    dopóki n > 0 wykonuj
        c ← n mod 10
        jeżeli c mod 2 = 1
            w ← w + c · mnoznik
            mnoznik ← mnoznik · 10
        n ← n div 10
    zwróć w

funkcja dopelnienie(n)                // każda cyfra c → 9 − c
    w ← 0;  mnoznik ← 1
    powtarzaj
        w ← w + (9 − n mod 10) · mnoznik
        mnoznik ← mnoznik · 10
        n ← n div 10
    aż n = 0
    zwróć w

funkcja przeniesienia(a, b)
    ile ← 0;  p ← 0
    dopóki a > 0 lub b > 0 wykonuj
        p ← (a mod 10 + b mod 10 + p) div 10
        ile ← ile + p
        a ← a div 10;  b ← b div 10
    zwróć ile
ncyfranieparzysta?wynikmnoznik po kroku
523188nie01
52311tak110
5233tak31100
522nie31100
55tak5311000

Dodawanie pisemne działa tak samo: idziemy od ostatnich cyfr, a przeniesienie to (cyfra_a + cyfra_b + poprzednie) div 10. Pętla kręci się, dopóki którakolwiek liczba ma jeszcze cyfry.

Warunek „zmienne tylko całkowite”. Arkusz czasem zabrania zamiany liczby na napis. Wtedy rozwiązanie musi używać tylko div i mod, tak jak powyżej. Gdy takiego zakazu nie ma, w programie możesz pracować na napisie str(n).
Na maturze: usuwanie cyfr parzystych (2024, zad. 3), algorytm budujący liczbę z połówek cyfr (2024, zad. 2), dopełnienie do 9 (2021, zad. 1), zamiana miejscami par cyfr (2025, zad. 1) i przeniesienia przy dodawaniu pisemnym (2026, zad. 2).

Co zwróci tylko_nieparzyste(70315)?

Jakie jest dopełnienie liczby 3051?

Ile przeniesień jest przy dodawaniu pisemnym 4567 + 5433?

Lekcja 5

Liczby pierwsze i rozkład na czynniki

Liczba pierwsza ma dokładnie dwa dzielniki: 1 i samą siebie. Wystarczy sprawdzić, czy nie dzieli jej żadna liczba od 2 do √n.

python
def czy_pierwsza(n):
    if n < 2:
        return False
    d = 2
    while d * d <= n:
        if n % d == 0:
            return False
        d += 1
    return True

def czynniki(n):
    wynik = []
    d = 2
    while d * d <= n:
        while n % d == 0:
            wynik.append(d)
            n //= d
        d += 1
    if n > 1:              # została jedna duża liczba pierwsza
        wynik.append(n)
    return wynik
c++
#include <vector>
using namespace std;

bool czy_pierwsza(long long n) {
    if (n < 2) return false;
    for (long long d = 2; d * d <= n; d++)
        if (n % d == 0) return false;
    return true;
}

vector<long long> czynniki(long long n) {
    vector<long long> wynik;
    for (long long d = 2; d * d <= n; d++)
        while (n % d == 0) {
            wynik.push_back(d);
            n /= d;
        }
    if (n > 1)             // została jedna duża liczba pierwsza
        wynik.push_back(n);
    return wynik;
}
pseudokod
funkcja czy_pierwsza(n)
    jeżeli n < 2
        zwróć FAŁSZ
    d ← 2
    dopóki d · d ≤ n wykonuj
        jeżeli n mod d = 0
            zwróć FAŁSZ
        d ← d + 1
    zwróć PRAWDA

funkcja czynniki(n)
    C ← pusta lista
    d ← 2
    dopóki d · d ≤ n wykonuj
        dopóki n mod d = 0 wykonuj
            dopisz d do C
            n ← n div d
        d ← d + 1
    jeżeli n > 1
        dopisz n do C              // została jedna duża liczba pierwsza
    zwróć C

W rozkładzie na czynniki dzielimy przez d, dopóki się da, dopiero potem zwiększamy d. Dzięki temu do listy trafiają tylko liczby pierwsze: zanim dojdziemy do 4, wszystkie dwójki już zostały wyciągnięte.

dn przeddopisane czynnikin po
23602, 2, 245
3453, 35
45— (4·4 > 5, koniec pętli)5
n > 1, więc 5
Pułapka: liczby 0 i 1 nie są pierwsze. Bez warunku n < 2 funkcja uzna je za pierwsze, bo pętla nie wykona się ani razu.

Podaj rozkład liczby 360 na czynniki pierwsze, od najmniejszego, oddzielając spacjami.

Dlaczego w teście pierwszości wystarczy sprawdzać dzielniki do √n?

  1. Bo liczby pierwsze są mniejsze od √n
  2. Bo jeśli n = a·b i oba czynniki są większe od √n, to a·b > n; jeden z nich musi być ≤ √n
  3. Bo tak jest szybciej, choć czasem daje błędny wynik

Czy 91 jest liczbą pierwszą?

  1. Tak
  2. Nie, 91 = 7 · 13
Lekcja 6

Sito Eratostenesa

Gdy potrzebujesz wszystkich liczb pierwszych do n albo musisz sprawdzić tysiące liczb, sito jest dużo szybsze niż osobny test dla każdej z nich.

Zaczynamy od tablicy „wszystko pierwsze”. Bierzemy najmniejszą niewykreśloną liczbę i wykreślamy wszystkie jej wielokrotności. Powtarzamy aż do √n. Co zostało, jest pierwsze.

python
def sito(n):
    pierwsza = [True] * (n + 1)
    pierwsza[0] = pierwsza[1] = False
    i = 2
    while i * i <= n:
        if pierwsza[i]:
            for j in range(i * i, n + 1, i):   # wielokrotności i
                pierwsza[j] = False
        i += 1
    return [k for k in range(n + 1) if pierwsza[k]]
c++
#include <vector>
using namespace std;

vector<int> sito(int n) {
    vector<bool> pierwsza(n + 1, true);
    pierwsza[0] = pierwsza[1] = false;
    for (int i = 2; i * i <= n; i++)
        if (pierwsza[i])
            for (int j = i * i; j <= n; j += i)   // wielokrotności i
                pierwsza[j] = false;
    vector<int> wynik;
    for (int k = 2; k <= n; k++)
        if (pierwsza[k]) wynik.push_back(k);
    return wynik;
}
pseudokod
funkcja sito(n)
    P[0..n] ← PRAWDA
    P[0] ← FAŁSZ;  P[1] ← FAŁSZ
    i ← 2
    dopóki i · i ≤ n wykonuj
        jeżeli P[i]
            j ← i · i
            dopóki j ≤ n wykonuj      // wielokrotności i
                P[j] ← FAŁSZ
                j ← j + i
        i ← i + 1
    zwróć wszystkie k, dla których P[k] = PRAWDA

Wykreślanie zaczynamy od i·i, bo mniejsze wielokrotności, np. 2·i albo 3·i, wykreśliły już wcześniejsze liczby. Złożoność to O(n log log n), w praktyce prawie liniowa.

sito(30)
2 3 5 7 11 13 17 19 23 29
Na egzaminie: jeśli w pliku są liczby do miliona, a trzeba sprawdzić pierwszość każdej z nich, zbuduj raz sito do największej liczby i odczytuj pierwsza[x].

Ile jest liczb pierwszych mniejszych lub równych 30?

Od której liczby zaczyna się wykreślanie wielokrotności 7?

  1. 14
  2. 21
  3. 49
  4. 7
Lekcja 7

Algorytm Euklidesa: NWD i NWW

Największy wspólny dzielnik dwóch liczb nie zmienia się, gdy od większej odejmiemy mniejszą. Na tej obserwacji opiera się jeden z najstarszych algorytmów.

Wersja z odejmowaniem jest prosta, ale wolna dla liczb bardzo różnej wielkości, np. NWD(1000000, 1) wymaga miliona kroków. Wersja z resztą z dzielenia robi wiele odejmowań naraz i wykonuje O(log n) kroków.

python
def nwd_odejmowanie(a, b):
    while a != b:
        if a > b:
            a -= b
        else:
            b -= a
    return a

def nwd(a, b):
    while b != 0:
        a, b = b, a % b
    return a

def nwd_rek(a, b):
    if b == 0:
        return a
    return nwd_rek(b, a % b)

def nww(a, b):
    return a // nwd(a, b) * b    # najpierw dzielenie: mniejsze liczby po drodze
c++
long long nwd_odejmowanie(long long a, long long b) {
    while (a != b) {
        if (a > b) a -= b;
        else b -= a;
    }
    return a;
}

long long nwd(long long a, long long b) {
    while (b != 0) {
        long long r = a % b;
        a = b;
        b = r;
    }
    return a;
}

long long nwd_rek(long long a, long long b) {
    if (b == 0) return a;
    return nwd_rek(b, a % b);
}

long long nww(long long a, long long b) {
    return a / nwd(a, b) * b;    // najpierw dzielenie: chroni przed przepełnieniem
}
pseudokod
funkcja nwd_odejmowanie(a, b)
    dopóki a ≠ b wykonuj
        jeżeli a > b
            a ← a − b
        w przeciwnym razie
            b ← b − a
    zwróć a

funkcja nwd(a, b)
    dopóki b ≠ 0 wykonuj
        r ← a mod b
        a ← b
        b ← r
    zwróć a

funkcja nwd_rek(a, b)
    jeżeli b = 0
        zwróć a
    zwróć nwd_rek(b, a mod b)

funkcja nww(a, b)
    zwróć (a div nwd(a, b)) · b
aba % b
481812
18126
1260
60koniec: NWD = 6

NWW liczymy ze wzoru NWW(a, b) = a · b / NWD(a, b).

Python liczy na dowolnie dużych liczbach całkowitych, ale a // nwd(a, b) * b i tak jest lepsze, bo liczby po drodze są mniejsze. Używaj //, a nie /, bo / daje liczbę zmiennoprzecinkową.

Kolejność działań ma znaczenie: a / nwd(a, b) * b nie przekroczy zakresu long long tam, gdzie a * b już by go przekroczyło.

NWD wielu liczb liczysz po kolei: NWD(a, b, c) = NWD(NWD(a, b), c). Tak samo NWW.

Ile wynosi NWD(48, 18)?

Ile wynosi NWW(12, 18)?

Rozszerzony algorytm Euklidesa

Wersja rozszerzona oprócz d = NWD(a, b) znajduje liczby całkowite x i y, dla których a·x + b·y = d. Wywołanie dla (b, a mod b) zwraca x′ i y′, a z nich: x = y′ oraz y = x′ − (a div b)·y′.

python
def nwd_rozszerzony(a, b):       # zwraca (d, x, y), gdzie d = NWD(a, b) = a·x + b·y
    if b == 0:
        return a, 1, 0
    d, x, y = nwd_rozszerzony(b, a % b)
    return d, y, x - (a // b) * y
c++
// zwraca d = NWD(a, b) i ustawia x, y tak, że d = a·x + b·y
long long nwd_rozszerzony(long long a, long long b, long long& x, long long& y) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    long long x1, y1;
    long long d = nwd_rozszerzony(b, a % b, x1, y1);
    x = y1;
    y = x1 - (a / b) * y1;
    return d;
}
pseudokod
// zwraca (d, x, y), gdzie d = NWD(a, b) = a·x + b·y
funkcja nwd_rozszerzony(a, b)
    jeżeli b = 0
        zwróć (a, 1, 0)
    (d, x, y) ← nwd_rozszerzony(b, a mod b)
    zwróć (d, y, x − (a div b) · y)
Na maturze: w 2015 r. (zad. 3) trzeba było uzupełnić i przeanalizować rozszerzony algorytm Euklidesa.

Rozszerzony Euklides dla a = 240, b = 46 daje d = 2 i x = −9. Ile wynosi y?

Ile kroków pętli (ile reszt z dzielenia) wykona funkcja nwd(21, 13)?

Lekcja 8

Systemy liczbowe i schemat Hornera

Zamiana na system o podstawie p to powtarzane dzielenie przez p. Reszty, czytane od ostatniej, to kolejne cyfry.

python
CYFRY = "0123456789ABCDEF"

def na_system(n, p):
    if n == 0:
        return "0"
    wynik = ""
    while n > 0:
        wynik = CYFRY[n % p] + wynik    # reszty czytamy od końca
        n //= p
    return wynik

def z_systemu(s, p):                    # schemat Hornera
    wynik = 0
    for znak in s:
        wynik = wynik * p + CYFRY.index(znak)
    return wynik
c++
#include <string>
using namespace std;

const string CYFRY = "0123456789ABCDEF";

string na_system(long long n, int p) {
    if (n == 0) return "0";
    string wynik = "";
    while (n > 0) {
        wynik = CYFRY[n % p] + wynik;   // reszty czytamy od końca
        n /= p;
    }
    return wynik;
}

long long z_systemu(const string& s, int p) {   // schemat Hornera
    long long wynik = 0;
    for (char znak : s)
        wynik = wynik * p + (long long)CYFRY.find(znak);
    return wynik;
}
pseudokod
CYFRY ← "0123456789ABCDEF"

funkcja na_system(n, p)
    jeżeli n = 0
        zwróć "0"
    w ← ""
    dopóki n > 0 wykonuj
        w ← CYFRY[n mod p] + w        // reszty czytamy od końca
        n ← n div p
    zwróć w

funkcja z_systemu(s, p)               // schemat Hornera
    w ← 0
    dla każdego znaku z w s, od lewej, wykonuj
        w ← w · p + (wartość cyfry z)
    zwróć w
nn % 2n // 2
45122
22011
1115
512
201
110

Reszty od dołu: 101101. W drugą stronę działa schemat Hornera: zaczynamy od zera i dla każdej cyfry od lewej mnożymy wynik przez p i dodajemy cyfrę. 1F₁₆ = 1·16 + 15 = 31.

Skrót w Pythonie: int("1F", 16) zamienia na liczbę dziesiętną, a bin(45), oct(45), hex(45) w drugą stronę (z przedrostkiem 0b, 0o, 0x). Na egzaminie możesz z nich korzystać, ale zadanie na papierze wymaga znajomości algorytmu.
Skrót w C++: stoll("1F", nullptr, 16) zamienia napis w systemie o podstawie 16 na liczbę. W drugą stronę nie ma gotowej funkcji dla dowolnej podstawy, więc na_system warto umieć napisać.

Horner dla wielomianu

Ten sam schemat liczy wartość wielomianu z n mnożeniami zamiast z n² mnożeniami: 2x³ − 3x² + x − 5 = ((2x − 3)x + 1)x − 5.

python
def horner(wsp, x):
    # wsp: współczynniki od najwyższej potęgi, np. 2x³ - 3x² + x - 5 -> [2, -3, 1, -5]
    w = 0
    for a in wsp:
        w = w * x + a
    return w
c++
#include <vector>
using namespace std;

// wsp: współczynniki od najwyższej potęgi, np. 2x³ - 3x² + x - 5 -> {2, -3, 1, -5}
long long horner(const vector<long long>& wsp, long long x) {
    long long w = 0;
    for (long long a : wsp)
        w = w * x + a;
    return w;
}
pseudokod
// a[0..n] – współczynniki od najwyższej potęgi
funkcja horner(a, x)
    w ← 0
    dla i = 0, 1, …, n wykonuj
        w ← w · x + a[i]
    zwróć w

Zapisz liczbę 45 w systemie dwójkowym.

Ile wynosi liczba 1F zapisana w systemie szesnastkowym, w zapisie dziesiętnym?

Oblicz schematem Hornera wartość 2x³ − 3x² + x − 5 dla x = 2.

Cyfry zapisane symbolami

Zadanie może podać liczbę w nietypowym zapisie, np. symbolami o, +, * zamiast cyfr 0, 1, 2 systemu trójkowego. Wtedy wystarczy podmienić symbole na cyfry i zastosować schemat Hornera: CYFRY = "o+*" w funkcji z_systemu.

W Pythonie podmianę zrobisz jednym wywołaniem: int(s.translate(str.maketrans("o+*", "012")), 3).

W C++ wystarczy CYFRY.find(znak) z napisem "o+*".

Na maturze: w 2025 r. (zad. 2) napisy z symboli o, +, * były liczbami w systemie trójkowym. Trzeba było znaleźć największą i policzyć sumę.

Symbole o, +, * oznaczają cyfry 0, 1, 2 systemu trójkowego. Ile dziesiętnie wynosi +*o?

Ile cyfr ma liczba 255 w systemie dwójkowym?

  1. 7
  2. 8
  3. 9
Lekcja 9

Zapis binarny i operacje na bitach

Zapis dwójkowy liczby czytamy tak jak dziesiętny, tylko dzielimy przez 2 zamiast przez 10. Do tego dochodzą operatory działające na wszystkich bitach naraz.

OperatorPython i C++Przykład (12 = 1100₂, 10 = 1010₂)
AND (oba bity 1)a & b12 & 10 = 8 (1000₂)
OR (co najmniej jeden 1)a | b12 | 10 = 14 (1110₂)
XOR (dokładnie jeden 1)a ^ b12 ^ 10 = 6 (0110₂)
przesunięcie w prawoa >> k12 >> 1 = 6, to samo co 12 div 2
przesunięcie w lewoa << k1 << 4 = 16, czyli 2⁴

Blok to najdłuższy ciąg takich samych cyfr obok siebie, np. 110011100 ma bloki 11, 00, 111, 00. Przeglądając bity od końca, liczymy, ile razy cyfra różni się od poprzedniej.

python
def bloki(n):                    # bloki takich samych cyfr w zapisie binarnym n > 0
    ile = 1
    ostatni = n % 2
    n //= 2
    while n > 0:
        if n % 2 != ostatni:     # zmiana cyfry: zaczyna się nowy blok
            ile += 1
            ostatni = n % 2
        n //= 2
    return ile

def jedynki(n):                  # liczba jedynek w zapisie binarnym
    ile = 0
    while n > 0:
        ile += n & 1             # ostatni bit
        n >>= 1                  # przesunięcie w prawo, czyli n // 2
    return ile

def potega_dwojki(n):            # n > 0 ma dokładnie jedną jedynkę
    return n & (n - 1) == 0

def gray(n):                     # kod Graya: sąsiednie liczby różnią się jednym bitem
    return n ^ (n >> 1)
c++
int bloki(long long n) {          // bloki takich samych cyfr w zapisie binarnym n > 0
    int ile = 1, ostatni = n % 2;
    n /= 2;
    while (n > 0) {
        if (n % 2 != ostatni) {   // zmiana cyfry: zaczyna się nowy blok
            ile++;
            ostatni = n % 2;
        }
        n /= 2;
    }
    return ile;
}

int jedynki(long long n) {        // liczba jedynek w zapisie binarnym
    int ile = 0;
    while (n > 0) {
        ile += n & 1;             // ostatni bit
        n >>= 1;                  // przesunięcie w prawo, czyli n / 2
    }
    return ile;
}

bool potega_dwojki(long long n) { // n > 0 ma dokładnie jedną jedynkę
    return (n & (n - 1)) == 0;    // nawiasy obowiązkowe: == wiąże mocniej niż &
}

long long gray(long long n) {     // kod Graya: sąsiednie liczby różnią się jednym bitem
    return n ^ (n >> 1);
}
pseudokod
funkcja bloki(n)                      // n > 0
    ile ← 1
    ostatni ← n mod 2
    n ← n div 2
    dopóki n > 0 wykonuj
        jeżeli n mod 2 ≠ ostatni      // nowy blok
            ile ← ile + 1
            ostatni ← n mod 2
        n ← n div 2
    zwróć ile

funkcja jedynki(n)
    ile ← 0
    dopóki n > 0 wykonuj
        ile ← ile + (n mod 2)
        n ← n div 2
    zwróć ile

funkcja potega_dwojki(n)              // n > 0
    zwróć (n AND (n − 1)) = 0

funkcja gray(n)
    zwróć n XOR (n div 2)

n XOR (n div 2) to kod Graya: kolejne liczby różnią się w nim dokładnie jednym bitem. Z kolei n AND (n − 1) usuwa najmłodszą jedynkę, więc daje 0 tylko dla potęg dwójki.

Liczbę zapisaną binarnie w pliku zamienisz poleceniem int(s, 2), a z powrotem poleceniem bin(n)[2:] albo format(n, "b").

Liczbę zapisaną binarnie w pliku zamienisz poleceniem stoll(s, nullptr, 2), a z powrotem funkcją na_system(n, 2) z lekcji o systemach. Uwaga: w C++ == wiąże mocniej niż &, więc pisz (n & 1) == 0.

Na maturze: w 2023 r. (zad. 2) liczono bloki, porównywano długie liczby binarne i obliczano p XOR (p div 2). W 2017 r. (zad. 2) funkcja rekurencyjna przechodziła po bitach liczby, a w 2020 r. w teście (zad. 3.1) pojawił się zegar binarny.

Ile bloków ma zapis binarny 110011100?

Ile wynosi 12 XOR 10 (dziesiętnie)?

Ile wynosi gray(11), czyli 1011₂ XOR 0101₂? Podaj wynik binarnie.

Jak sprawdzić, czy liczba n > 0 jest potęgą dwójki?

  1. n % 2 == 0
  2. n & (n − 1) == 0
  3. n ^ 1 == 0
Lekcja 10

Szybkie potęgowanie

Zamiast mnożyć a przez siebie n razy, wykorzystujemy aⁿ = (aⁿᐟ²)². Liczba mnożeń spada z n do około 2·log₂ n.

Dla parzystego n liczymy aⁿᐟ² raz i podnosimy do kwadratu. Dla nieparzystego zapisujemy aⁿ = a · aⁿ⁻¹. To przykład metody dziel i zwyciężaj.

python
def potega(a, n):
    if n == 0:
        return 1
    if n % 2 == 1:
        return a * potega(a, n - 1)
    p = potega(a, n // 2)
    return p * p                 # jedno wywołanie, wynik użyty dwa razy

def potega_mod(a, n, m):
    wynik = 1
    a %= m
    while n > 0:
        if n % 2 == 1:           # bieżący bit wykładnika to 1
            wynik = wynik * a % m
        a = a * a % m
        n //= 2
    return wynik
c++
long long potega(long long a, int n) {
    if (n == 0) return 1;
    if (n % 2 == 1) return a * potega(a, n - 1);
    long long p = potega(a, n / 2);
    return p * p;                // jedno wywołanie, wynik użyty dwa razy
}

long long potega_mod(long long a, long long n, long long m) {
    long long wynik = 1;
    a %= m;
    while (n > 0) {
        if (n % 2 == 1)          // bieżący bit wykładnika to 1
            wynik = wynik * a % m;
        a = a * a % m;
        n /= 2;
    }
    return wynik;
}
pseudokod
funkcja potega(a, n)
    jeżeli n = 0
        zwróć 1
    jeżeli n mod 2 = 1
        zwróć a · potega(a, n − 1)
    p ← potega(a, n div 2)
    zwróć p · p                       // jedno wywołanie, wynik użyty dwa razy

funkcja potega_mod(a, n, m)
    w ← 1
    a ← a mod m
    dopóki n > 0 wykonuj
        jeżeli n mod 2 = 1            // bieżący bit wykładnika to 1
            w ← (w · a) mod m
        a ← (a · a) mod m
        n ← n div 2
    zwróć w

Wersja iteracyjna przegląda bity wykładnika od najmłodszego: 13 = 1101₂, więc a¹³ = a⁸ · a⁴ · a¹. Liczenie reszty z dzielenia po każdym mnożeniu trzyma liczby w małym zakresie, więc obliczenia są szybkie nawet dla wykładnika rzędu 10¹⁸.

Python ma tę funkcję wbudowaną: pow(a, n, m). Liczby całkowite w Pythonie nie mają limitu, ale bez reszty po drodze stają się ogromne i obliczenia zwalniają.

W C++ to konieczność: long long mieści tylko około 9·10¹⁸, więc dla m do 10⁹ iloczyn dwóch reszt jeszcze się mieści.

Błąd, który psuje złożoność: return potega(a, n // 2) * potega(a, n // 2) wywołuje funkcję dwa razy i znowu daje O(n). Zapisz wynik w zmiennej.

Ile wynosi 3¹³ mod 7?

Ile wynosi 2¹⁰?

Jaka jest złożoność szybkiego potęgowania względem wykładnika n?

  1. O(n)
  2. O(log n)
  3. O(√n)
  4. O(n²)
Lekcja 11

Rekurencja i ciąg Fibonacciego

Funkcja rekurencyjna wywołuje samą siebie dla mniejszego problemu. Potrzebuje warunku zakończenia, inaczej wywołania nigdy się nie skończą.

python
def silnia(n):
    if n <= 1:                   # warunek zakończenia
        return 1
    return n * silnia(n - 1)

def fib_rek(n):                  # proste, ale wykładniczo wolne
    if n < 2:
        return n
    return fib_rek(n - 1) + fib_rek(n - 2)

def fib(n):                      # iteracyjnie: O(n)
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a
c++
long long silnia(int n) {
    if (n <= 1) return 1;        // warunek zakończenia
    return n * silnia(n - 1);
}

long long fib_rek(int n) {       // proste, ale wykładniczo wolne
    if (n < 2) return n;
    return fib_rek(n - 1) + fib_rek(n - 2);
}

long long fib(int n) {           // iteracyjnie: O(n)
    long long a = 0, b = 1;
    for (int i = 0; i < n; i++) {
        long long c = a + b;
        a = b;
        b = c;
    }
    return a;
}
pseudokod
funkcja silnia(n)
    jeżeli n ≤ 1
        zwróć 1                       // warunek zakończenia
    zwróć n · silnia(n − 1)

funkcja fib_rek(n)                    // wykładniczo wolne
    jeżeli n < 2
        zwróć n
    zwróć fib_rek(n − 1) + fib_rek(n − 2)

funkcja fib(n)                        // iteracyjnie: O(n)
    a ← 0;  b ← 1
    powtórz n razy
        c ← a + b
        a ← b
        b ← c
    zwróć a

Rekurencyjny Fibonacci liczy te same wartości wiele razy: fib_rek(5) woła fib_rek(3) dwa razy, a fib_rek(2) trzy razy. Liczba wywołań rośnie wykładniczo. Wersja iteracyjna pamięta tylko dwie ostatnie wartości i ma O(n).

Rekurencję można przyspieszyć, zapamiętując obliczone wartości w tablicy (spamiętywanie). To pierwszy krok do programowania dynamicznego.

Na papierze: w zadaniach z rekurencją rysuj drzewo wywołań. Liczba węzłów to liczba wywołań, a głębokość drzewa to największe zagłębienie.

Ile wynosi fib(10), jeśli fib(0) = 0 i fib(1) = 1?

Ile razy łącznie zostanie wywołana funkcja fib_rek przy obliczaniu fib_rek(5), licząc pierwsze wywołanie?

  1. 5
  2. 8
  3. 15
  4. 32

Ile wynosi silnia(6)?

Lekcja 12

Generowanie wszystkich ciągów, podzbiorów i permutacji

Gdy trzeba sprawdzić wszystkie możliwości, rekurencja dokłada po jednym elemencie i schodzi głębiej. Wywołania tworzą drzewo, a liście drzewa to gotowe rozwiązania.

python
def ciagi(s, n, k, wynik):       # wszystkie napisy długości n z cyfr 0..k-1
    if len(s) == n:
        wynik.append(s)
    else:
        for c in range(k):
            ciagi(s + str(c), n, k, wynik)

def podzbiory(t):                # każdy podzbiór to jedna maska bitowa 0..2ⁿ-1
    n = len(t)
    wynik = []
    for maska in range(2 ** n):
        wynik.append([t[i] for i in range(n) if maska >> i & 1])
    return wynik

def permutacje(t, k, wynik):     # na pozycję k wstawiamy kolejno każdy pozostały element
    if k == len(t):
        wynik.append(t[:])
    for i in range(k, len(t)):
        t[k], t[i] = t[i], t[k]
        permutacje(t, k + 1, wynik)
        t[k], t[i] = t[i], t[k]  # cofamy zamianę
c++
#include <string>
#include <vector>
#include <utility>
using namespace std;

void ciagi(string s, int n, int k, vector<string>& wynik) {   // napisy długości n z cyfr 0..k-1
    if ((int)s.size() == n) {
        wynik.push_back(s);
        return;
    }
    for (int c = 0; c < k; c++)
        ciagi(s + to_string(c), n, k, wynik);
}

vector<vector<int>> podzbiory(const vector<int>& t) {   // każdy podzbiór to maska 0..2ⁿ-1
    int n = t.size();
    vector<vector<int>> wynik;
    for (int maska = 0; maska < (1 << n); maska++) {
        vector<int> p;
        for (int i = 0; i < n; i++)
            if (maska >> i & 1) p.push_back(t[i]);
        wynik.push_back(p);
    }
    return wynik;
}

void permutacje(vector<int>& t, int k, vector<vector<int>>& wynik) {
    if (k == (int)t.size()) wynik.push_back(t);
    for (int i = k; i < (int)t.size(); i++) {   // na pozycję k każdy pozostały element
        swap(t[k], t[i]);
        permutacje(t, k + 1, wynik);
        swap(t[k], t[i]);                       // cofamy zamianę
    }
}
pseudokod
procedura ciagi(s, n, k)              // napisy długości n z cyfr 0..k−1
    jeżeli długość(s) = n
        wypisz s
    w przeciwnym razie
        dla c = 0, 1, …, k−1 wykonuj
            ciagi(s + napis(c), n, k)

procedura podzbiory(T[0..n-1])
    dla maska = 0, 1, …, 2ⁿ − 1 wykonuj
        wypisz te T[i], dla których i-ty bit liczby maska to 1

procedura permutacje(T[0..n-1], k)
    jeżeli k = n
        wypisz T
    dla i = k, …, n−1 wykonuj
        zamień T[k] i T[i]
        permutacje(T, k + 1)
        zamień T[k] i T[i]            // cofamy zamianę

Dla napisów długości n z k cyfr wynik ma kⁿ elementów. Drzewo wywołań ma 1 + k + k² + … + kⁿ węzłów, bo każde wywołanie poniżej ostatniego poziomu wywołuje k następnych. Podzbiorów zbioru n-elementowego jest 2ⁿ, a permutacji n!.

drzewo wywołań
ciagi("", 2, 2)
├── ciagi("0")  →  "00", "01"
└── ciagi("1")  →  "10", "11"
Na papierze rysuj drzewo wywołań poziom po poziomie i podpisuj węzły argumentami. Liczba węzłów to liczba wywołań, a liście to wypisane wyniki.
Na maturze: w 2019 r. (zad. 2) rekurencyjna funkcja wypisywała wszystkie napisy długości n z cyfr 0..k−1. Trzeba było uzupełnić drzewo wywołań i policzyć wywołania. Rekurencję śledzi się też w latach 2017, 2020, 2021, 2025 i 2026 (zad. 1 lub 2).

Ile napisów wypisze ciagi("", 3, 2)?

Ile łącznie wywołań funkcji ciagi (z pierwszym) będzie dla n = 2 i k = 3?

Ile jest podzbiorów zbioru 10-elementowego (z pustym)?

  1. 100
  2. 1024
  3. 3 628 800
Lekcja 13

Przeglądanie ciągu: wyszukiwanie, minimum, ciągi

Wiele zadań maturalnych to jeden przebieg po danych z kilkoma zmiennymi pomocniczymi. Najważniejsze jest wybranie, co trzeba pamiętać z poprzednich elementów.

python
def indeks(t, x):
    for i in range(len(t)):
        if t[i] == x:
            return i
    return -1                    # nie ma takiego elementu

def minimum(t):
    m = t[0]
    for x in t:
        if x < m:
            m = x
    return m

def najdluzszy_rosnacy(t):
    # najdłuższy fragment kolejnych elementów, w którym każdy jest większy od poprzedniego
    najl = dl = 1
    for i in range(1, len(t)):
        if t[i] > t[i - 1]:
            dl += 1
        else:
            dl = 1
        if dl > najl:
            najl = dl
    return najl
c++
#include <vector>
using namespace std;

int indeks(const vector<int>& t, int x) {
    for (int i = 0; i < (int)t.size(); i++)
        if (t[i] == x) return i;
    return -1;                   // nie ma takiego elementu
}

int minimum(const vector<int>& t) {
    int m = t[0];
    for (int x : t)
        if (x < m) m = x;
    return m;
}

int najdluzszy_rosnacy(const vector<int>& t) {
    // najdłuższy fragment kolejnych elementów, w którym każdy jest większy od poprzedniego
    int najl = 1, dl = 1;
    for (int i = 1; i < (int)t.size(); i++) {
        if (t[i] > t[i - 1]) dl++;
        else dl = 1;
        if (dl > najl) najl = dl;
    }
    return najl;
}
pseudokod
funkcja indeks(T[0..n-1], x)
    dla i = 0, 1, …, n-1 wykonuj
        jeżeli T[i] = x
            zwróć i
    zwróć −1                          // nie ma takiego elementu

funkcja minimum(T[0..n-1])
    m ← T[0]
    dla i = 1, 2, …, n-1 wykonuj
        jeżeli T[i] < m
            m ← T[i]
    zwróć m

funkcja najdluzszy_rosnacy(T[0..n-1])
    najl ← 1;  dl ← 1
    dla i = 1, 2, …, n-1 wykonuj
        jeżeli T[i] > T[i − 1]
            dl ← dl + 1
        w przeciwnym razie
            dl ← 1
        jeżeli dl > najl
            najl ← dl
    zwróć najl
Pułapka: zaczynanie minimum od 0. Jeśli wszystkie liczby są dodatnie, wynik będzie błędny. Zaczynaj od pierwszego elementu.

Minimum i maksimum naraz

Osobne szukanie minimum i maksimum to 2(n − 1) porównań. Gdy bierzemy elementy parami, porównujemy je ze sobą, a potem mniejszy tylko z minimum i większy tylko z maksimum. Na dwa elementy wypadają 3 porównania, łącznie około 3n/2.

python
def min_max(t):
    n = len(t)
    if n % 2 == 1:
        mn = mx = t[0]
        i = 1
    else:
        if t[0] < t[1]:
            mn, mx = t[0], t[1]
        else:
            mn, mx = t[1], t[0]
        i = 2
    while i < n:
        a, b = t[i], t[i + 1]
        if a > b:                # 1 porównanie w parze
            a, b = b, a
        if a < mn:               # mniejszy z pary walczy tylko o minimum
            mn = a
        if b > mx:               # większy tylko o maksimum
            mx = b
        i += 2
    return mn, mx
c++
#include <vector>
#include <utility>
using namespace std;

pair<int, int> min_max(const vector<int>& t) {
    int n = t.size(), mn, mx, i;
    if (n % 2 == 1) {
        mn = mx = t[0];
        i = 1;
    } else {
        if (t[0] < t[1]) { mn = t[0]; mx = t[1]; }
        else { mn = t[1]; mx = t[0]; }
        i = 2;
    }
    for (; i < n; i += 2) {
        int a = t[i], b = t[i + 1];
        if (a > b) swap(a, b);   // 1 porównanie w parze
        if (a < mn) mn = a;      // mniejszy z pary walczy tylko o minimum
        if (b > mx) mx = b;      // większy tylko o maksimum
    }
    return {mn, mx};
}
pseudokod
funkcja min_max(T[0..n-1])
    jeżeli n jest nieparzyste
        mn ← T[0];  mx ← T[0];  i ← 1
    w przeciwnym razie
        jeżeli T[0] < T[1]
            mn ← T[0];  mx ← T[1]
        w przeciwnym razie
            mn ← T[1];  mx ← T[0]
        i ← 2
    dopóki i < n wykonuj
        a ← T[i];  b ← T[i + 1]
        jeżeli a > b                  // 1 porównanie w parze
            zamień a i b
        jeżeli a < mn                 // mniejszy walczy tylko o minimum
            mn ← a
        jeżeli b > mx                 // większy tylko o maksimum
            mx ← b
        i ← i + 2
    zwróć mn, mx

Jaka jest długość najdłuższego fragmentu rosnącego w ciągu 3 1 4 5 9 2 6 7?

Ile porównań wykona min_max dla 100 elementów?

  1. 198
  2. 150
  3. 148
  4. 100

Przesunięcie cykliczne

Przesunięcie tablicy o k miejsc w lewo, z przeniesieniem początku na koniec, to t[k:] + t[:k]. Tablica b jest przesunięciem a, gdy b == a[k:] + a[:k] dla jakiegoś k od 0 do n − 1. Sprawdzenie wszystkich k to O(n²). Szybciej: b musi wystąpić jako fragment tablicy a + a.

Przesunięcie tablicy o k miejsc w lewo: element t[(i + k) % n] trafia na pozycję i. Tablica b jest przesunięciem a, gdy dla jakiegoś k od 0 do n − 1 zachodzi b[i] == a[(i + k) % n] dla każdego i. Sprawdzenie wszystkich k to O(n²).

Na maturze: w 2020 r. (zad. 1) tablice były „podobne”, gdy jedna była przesunięciem cyklicznym drugiej, a trzeba było znaleźć to przesunięcie. W 2017 r. (zad. 1) szukano największego iloczynu dwóch różnych liczb ze zbioru z dodatkowym warunkiem, czyli przeglądano wszystkie pary albo sortowano.

Tablicę 1 2 3 4 5 przesuwamy cyklicznie o 2 w lewo. Jaki jest wynik?

Lekcja 14

Sumy prefiksowe i fragmenty ciągu

Gdy trzeba wiele razy liczyć sumę fragmentu ciągu, raz liczymy sumy od początku. Potem każdą sumę fragmentu dostajemy jednym odejmowaniem.

python
def sumy_prefiksowe(t):
    P = [0] * (len(t) + 1)       # P[i] = t[0] + t[1] + ... + t[i-1]
    for i in range(len(t)):
        P[i + 1] = P[i] + t[i]
    return P
# suma fragmentu t[a..b] (włącznie) to P[b + 1] - P[a]

def najlepsza_srednia(t, k):     # fragment o co najmniej k elementach, O(n²)
    P = sumy_prefiksowe(t)
    suma, start, dl = P[k], 0, k
    for a in range(len(t)):
        for b in range(a + k, len(t) + 1):     # fragment t[a..b-1]
            s = P[b] - P[a]
            if s * dl > suma * (b - a):         # s/(b-a) > suma/dl bez dzielenia
                suma, start, dl = s, a, b - a
    return suma / dl, start, dl

def najwieksza_suma(t):          # algorytm Kadane: najlepsza suma fragmentu, O(n)
    naj = biezaca = t[0]
    for x in t[1:]:
        biezaca = max(x, biezaca + x)   # przedłużamy fragment albo zaczynamy nowy
        naj = max(naj, biezaca)
    return naj
c++
#include <vector>
#include <algorithm>
using namespace std;

vector<long long> sumy_prefiksowe(const vector<int>& t) {
    vector<long long> P(t.size() + 1, 0);       // P[i] = t[0] + ... + t[i-1]
    for (int i = 0; i < (int)t.size(); i++)
        P[i + 1] = P[i] + t[i];
    return P;
}
// suma fragmentu t[a..b] (włącznie) to P[b + 1] - P[a]

// fragment o co najmniej k elementach z największą średnią; ustawia start i dl
double najlepsza_srednia(const vector<int>& t, int k, int& start, int& dl) {
    vector<long long> P = sumy_prefiksowe(t);
    int n = t.size();
    long long suma = P[k];
    start = 0;
    dl = k;
    for (int a = 0; a < n; a++)
        for (int b = a + k; b <= n; b++) {      // fragment t[a..b-1]
            long long s = P[b] - P[a];
            if (s * dl > suma * (b - a)) {      // s/(b-a) > suma/dl bez dzielenia
                suma = s;
                start = a;
                dl = b - a;
            }
        }
    return (double)suma / dl;
}

long long najwieksza_suma(const vector<int>& t) {   // algorytm Kadane, O(n)
    long long naj = t[0], biezaca = t[0];
    for (int i = 1; i < (int)t.size(); i++) {
        biezaca = max((long long)t[i], biezaca + t[i]);   // przedłużamy albo od nowa
        naj = max(naj, biezaca);
    }
    return naj;
}
pseudokod
// P[i] = T[0] + … + T[i−1];  suma T[a..b] = P[b+1] − P[a]
funkcja sumy_prefiksowe(T[0..n-1])
    P[0] ← 0
    dla i = 0, 1, …, n−1 wykonuj
        P[i + 1] ← P[i] + T[i]
    zwróć P

funkcja najlepsza_srednia(T[0..n-1], k)   // fragment ≥ k elementów
    P ← sumy_prefiksowe(T)
    suma ← P[k];  start ← 0;  dl ← k
    dla a = 0, 1, …, n−1 wykonuj
        dla b = a+k, …, n wykonuj         // fragment T[a..b−1]
            s ← P[b] − P[a]
            jeżeli s · dl > suma · (b − a)
                suma ← s;  start ← a;  dl ← b − a
    zwróć suma / dl, start, dl

funkcja najwieksza_suma(T[0..n-1])        // algorytm Kadane
    naj ← T[0];  biezaca ← T[0]
    dla i = 1, …, n−1 wykonuj
        biezaca ← max(T[i], biezaca + T[i])
        naj ← max(naj, biezaca)
    zwróć naj
i012345678
t[i]3−141−592−6
P[i]03267211137

Suma t[2..5] to P[6] − P[2] = 11 − 2 = 9. Bez tablicy P sprawdzenie wszystkich fragmentów kosztowałoby O(n³), a z nią O(n²).

Średnie porównujemy bez dzielenia: s₁/d₁ > s₂/d₂ to to samo co s₁·d₂ > s₂·d₁. Wynik jest dokładny i przy remisie zostaje pierwszy znaleziony fragment, czego zwykle wymaga treść zadania.

Liczniki z przodu i z tyłu

Ta sama idea działa dla liczników: A[i] to liczba liter „a” wśród pierwszych i znaków, a B[i] to liczba liter „b” od i-tego znaku do końca. Wtedy dla każdego miejsca podziału słowa od razu wiadomo, ile jest „a” przed nim i „b” po nim.

Na maturze: w 2024 r. (zad. 4) szukano fragmentu co najmniej 50 liczb o największej średniej. W 2022 r. (zad. 2) analizowano liczniki z przodu i z tyłu słowa. W 2023 r. (zad. 3) zliczano fragmenty dwóch kolejnych cyfr rozwinięcia π w tablicy 100 liczników.

Dla tablicy P powyżej: ile wynosi suma t[3..7]?

Jaka jest największa suma spójnego fragmentu ciągu 2, −3, 4, −1, 2, −5, 3?

Ile jest fragmentów (spójnych) w ciągu n-elementowym?

  1. n
  2. n(n+1)/2
  3. 2ⁿ
Lekcja 15

Wyszukiwanie binarne

W posortowanej tablicy każde porównanie ze środkiem odrzuca połowę kandydatów. Milion elementów przeszukasz w 20 krokach.

python
def szukaj_binarnie(t, x):       # t musi być posortowana rosnąco
    lewy, prawy = 0, len(t) - 1
    while lewy <= prawy:
        srodek = (lewy + prawy) // 2
        if t[srodek] == x:
            return srodek
        if t[srodek] < x:
            lewy = srodek + 1    # x może być tylko na prawo
        else:
            prawy = srodek - 1   # x może być tylko na lewo
    return -1

def pierwszy_niemniejszy(t, x):
    # indeks pierwszego elementu >= x; len(t), gdy takiego nie ma
    lewy, prawy = 0, len(t)
    while lewy < prawy:
        s = (lewy + prawy) // 2
        if t[s] < x:
            lewy = s + 1
        else:
            prawy = s
    return lewy
c++
#include <vector>
using namespace std;

int szukaj_binarnie(const vector<int>& t, int x) {   // t musi być posortowana rosnąco
    int lewy = 0, prawy = (int)t.size() - 1;
    while (lewy <= prawy) {
        int srodek = (lewy + prawy) / 2;
        if (t[srodek] == x) return srodek;
        if (t[srodek] < x) lewy = srodek + 1;   // x może być tylko na prawo
        else prawy = srodek - 1;                // x może być tylko na lewo
    }
    return -1;
}

int pierwszy_niemniejszy(const vector<int>& t, int x) {
    // indeks pierwszego elementu >= x; t.size(), gdy takiego nie ma
    int lewy = 0, prawy = t.size();
    while (lewy < prawy) {
        int s = (lewy + prawy) / 2;
        if (t[s] < x) lewy = s + 1;
        else prawy = s;
    }
    return lewy;
}
pseudokod
// T[0..n-1] posortowana rosnąco
funkcja szukaj_binarnie(T, x)
    lewy ← 0;  prawy ← n − 1
    dopóki lewy ≤ prawy wykonuj
        s ← (lewy + prawy) div 2
        jeżeli T[s] = x
            zwróć s
        jeżeli T[s] < x
            lewy ← s + 1              // x może być tylko na prawo
        w przeciwnym razie
            prawy ← s − 1             // x może być tylko na lewo
    zwróć −1

funkcja pierwszy_niemniejszy(T, x)
    lewy ← 0;  prawy ← n
    dopóki lewy < prawy wykonuj
        s ← (lewy + prawy) div 2
        jeżeli T[s] < x
            lewy ← s + 1
        w przeciwnym razie
            prawy ← s
    zwróć lewy
kroklewyprawysrodekt[srodek]
10949
259715
356511 — znaleziono

Ślad dla t = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] i szukanego x = 11.

Wariant „pierwszy element ≥ x” jest często bardziej przydatny: mówi, gdzie wstawić x albo ile elementów jest mniejszych od x. Ta sama idea działa dla dowolnej funkcji monotonicznej, np. przy szukaniu miejsca zerowego.

Szukanie po wyniku

Wyszukiwać binarnie można nie tylko w tablicy, ale też wśród możliwych wyników, jeśli warunek „wynik jest za mały” zmienia się tylko raz: najpierw jest prawdziwy, potem fałszywy. Tak liczy się np. całkowity pierwiastek bez liczb zmiennoprzecinkowych.

python
def pierwiastek3(n):             # najmniejsze p ≥ 1, dla którego p³ ≥ n
    lewy, prawy = 1, n
    while lewy < prawy:
        s = (lewy + prawy) // 2
        if s * s * s < n:
            lewy = s + 1         # s za małe
        else:
            prawy = s            # s może być wynikiem
    return lewy
c++
long long pierwiastek3(long long n) {   // najmniejsze p >= 1, dla którego p³ >= n
    long long lewy = 1, prawy = n;
    while (lewy < prawy) {
        long long s = (lewy + prawy) / 2;
        if (s * s * s < n) lewy = s + 1;  // s za małe
        else prawy = s;                   // s może być wynikiem
    }
    return lewy;
}
pseudokod
funkcja pierwiastek3(n)               // najmniejsze p, dla którego p³ ≥ n
    lewy ← 1;  prawy ← n
    dopóki lewy < prawy wykonuj
        s ← (lewy + prawy) div 2
        jeżeli s · s · s < n
            lewy ← s + 1
        w przeciwnym razie
            prawy ← s
    zwróć lewy
Na maturze: w 2018 r. (zad. 1) trzeba było przeanalizować dokładnie taki algorytm. W 2019 r. (zad. 1) ciąg miał najpierw liczby nieparzyste, potem parzyste, a maksimum punktów dawał algorytm o złożoności lepszej niż liniowa, czyli binarne szukanie granicy.

Jaki wynik da pierwiastek3(100)?

Ciąg ma najpierw same liczby nieparzyste, a potem same parzyste. Jak najszybciej znaleźć pierwszą parzystą?

  1. Przejrzeć wszystkie liczby od początku
  2. Wyszukiwaniem binarnym: warunek „t[s] jest nieparzyste” zmienia się tylko raz
  3. Posortować ciąg
Warunek konieczny: tablica musi być posortowana. Dla nieposortowanych danych wyszukiwanie binarne daje przypadkowe wyniki.

Ile co najwyżej porównań ze środkiem wykona wyszukiwanie binarne w tablicy 1000 elementów?

Jaki indeks zwróci pierwszy_niemniejszy([2, 4, 4, 8, 10], 5)?

Lekcja 16

Sortowanie bąbelkowe, przez wybór i przez wstawianie

Trzy algorytmy O(n²), które trzeba umieć prześledzić na papierze. Różnią się tym, co dzieje się w jednym przebiegu.

AlgorytmPomysłNajlepszy przypadek
bąbelkowezamieniaj sąsiadów w złej kolejności; największy „wypływa” na koniecO(n), gdy dane są posortowane (z flagą zamiany)
przez wybórznajdź minimum reszty i postaw je na początkuzawsze O(n²), ale tylko n − 1 zamian
przez wstawianiewstaw kolejny element w posortowany początekO(n), gdy dane są prawie posortowane
python
def babelkowe(t):
    n = len(t)
    for i in range(n - 1):
        zamiana = False
        for j in range(n - 1 - i):       # ostatnie i elementów już stoi na miejscu
            if t[j] > t[j + 1]:
                t[j], t[j + 1] = t[j + 1], t[j]
                zamiana = True
        if not zamiana:                  # przebieg bez zamian: koniec
            break

def przez_wybor(t):
    n = len(t)
    for i in range(n - 1):
        m = i
        for j in range(i + 1, n):        # szukamy minimum w części nieposortowanej
            if t[j] < t[m]:
                m = j
        t[i], t[m] = t[m], t[i]

def przez_wstawianie(t):
    for i in range(1, len(t)):
        x = t[i]
        j = i - 1
        while j >= 0 and t[j] > x:       # przesuwamy większe w prawo
            t[j + 1] = t[j]
            j -= 1
        t[j + 1] = x
c++
#include <vector>
#include <utility>
using namespace std;

void babelkowe(vector<int>& t) {
    int n = t.size();
    for (int i = 0; i < n - 1; i++) {
        bool zamiana = false;
        for (int j = 0; j < n - 1 - i; j++)   // ostatnie i elementów już stoi na miejscu
            if (t[j] > t[j + 1]) {
                swap(t[j], t[j + 1]);
                zamiana = true;
            }
        if (!zamiana) break;                  // przebieg bez zamian: koniec
    }
}

void przez_wybor(vector<int>& t) {
    int n = t.size();
    for (int i = 0; i < n - 1; i++) {
        int m = i;
        for (int j = i + 1; j < n; j++)       // szukamy minimum w części nieposortowanej
            if (t[j] < t[m]) m = j;
        swap(t[i], t[m]);
    }
}

void przez_wstawianie(vector<int>& t) {
    for (int i = 1; i < (int)t.size(); i++) {
        int x = t[i], j = i - 1;
        while (j >= 0 && t[j] > x) {          // przesuwamy większe w prawo
            t[j + 1] = t[j];
            j--;
        }
        t[j + 1] = x;
    }
}
pseudokod
procedura babelkowe(T[0..n-1])
    dla i = 0, 1, …, n-2 wykonuj
        zamiana ← FAŁSZ
        dla j = 0, 1, …, n-2-i wykonuj
            jeżeli T[j] > T[j + 1]
                zamień T[j] i T[j + 1]
                zamiana ← PRAWDA
        jeżeli nie zamiana
            zakończ                   // przebieg bez zamian

procedura przez_wybor(T[0..n-1])
    dla i = 0, 1, …, n-2 wykonuj
        m ← i
        dla j = i+1, …, n-1 wykonuj   // minimum części nieposortowanej
            jeżeli T[j] < T[m]
                m ← j
        zamień T[i] i T[m]

procedura przez_wstawianie(T[0..n-1])
    dla i = 1, 2, …, n-1 wykonuj
        x ← T[i]
        j ← i − 1
        dopóki j ≥ 0 oraz T[j] > x wykonuj
            T[j + 1] ← T[j]           // przesuwamy większe w prawo
            j ← j − 1
        T[j + 1] ← x
bąbelkowe
start:        5 1 4 2 8
po przebiegu: 1 4 2 5 8
po przebiegu: 1 2 4 5 8
po przebiegu: 1 2 4 5 8  (bez zamian, koniec)

Które sortowanie jest najszybsze dla danych prawie posortowanych?

  1. przez wybór
  2. przez wstawianie
  3. wszystkie tak samo

Jak wygląda tablica 7 3 5 1 po dwóch pierwszych krokach sortowania przez wybór? Wpisz liczby oddzielone spacjami.

Ile zamian sąsiadów wykona sortowanie bąbelkowe dla tablicy 4 3 2 1?

Lekcja 17

Sortowanie przez scalanie

Dziel i zwyciężaj: podziel tablicę na połowy, posortuj każdą, a potem scal dwie posortowane listy w jedną.

Scalanie przegląda obie listy po razie, zawsze zabierając mniejszy z dwóch czołowych elementów, więc kosztuje O(n). Poziomów podziału jest log₂ n. Razem daje to O(n log n) w każdym przypadku.

python
def scal(a, b):                  # a i b są już posortowane
    wynik = []
    i = j = 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            wynik.append(a[i])
            i += 1
        else:
            wynik.append(b[j])
            j += 1
    wynik.extend(a[i:])          # resztka jednej z list
    wynik.extend(b[j:])
    return wynik

def sortuj_scalanie(t):
    if len(t) <= 1:
        return t
    s = len(t) // 2
    return scal(sortuj_scalanie(t[:s]), sortuj_scalanie(t[s:]))
c++
#include <vector>
using namespace std;

// sortuje t[lewy..prawy] włącznie
void sortuj_scalanie(vector<int>& t, int lewy, int prawy) {
    if (lewy >= prawy) return;
    int s = (lewy + prawy) / 2;
    sortuj_scalanie(t, lewy, s);
    sortuj_scalanie(t, s + 1, prawy);

    vector<int> pom;             // scalanie dwóch posortowanych połówek
    int i = lewy, j = s + 1;
    while (i <= s && j <= prawy) {
        if (t[i] <= t[j]) pom.push_back(t[i++]);
        else pom.push_back(t[j++]);
    }
    while (i <= s) pom.push_back(t[i++]);
    while (j <= prawy) pom.push_back(t[j++]);
    for (int k = 0; k < (int)pom.size(); k++)
        t[lewy + k] = pom[k];
}
pseudokod
funkcja scal(A, B)                    // A i B posortowane
    W ← pusta lista;  i ← 0;  j ← 0
    dopóki i < |A| oraz j < |B| wykonuj
        jeżeli A[i] ≤ B[j]
            dopisz A[i] do W;  i ← i + 1
        w przeciwnym razie
            dopisz B[j] do W;  j ← j + 1
    dopisz do W pozostałe elementy A i B
    zwróć W

funkcja sortuj_scalanie(T)
    jeżeli |T| ≤ 1
        zwróć T
    s ← |T| div 2
    zwróć scal(sortuj_scalanie(T[0..s-1]), sortuj_scalanie(T[s..|T|-1]))
podział i scalanie
[5, 2, 4, 7, 1, 3, 2, 6]
[5, 2, 4, 7]     [1, 3, 2, 6]
[5, 2] [4, 7]    [1, 3] [2, 6]
[2, 5] [4, 7]    [1, 3] [2, 6]
[2, 4, 5, 7]     [1, 2, 3, 6]
[1, 2, 2, 3, 4, 5, 6, 7]
Stabilność: warunek <= przy scalaniu zachowuje kolejność równych elementów. Ma to znaczenie, gdy sortujesz np. uczniów po ocenie i chcesz zachować kolejność alfabetyczną.

Wynikiem scalenia list [1, 4, 9] i [2, 3, 10] jest lista… Wpisz liczby oddzielone spacjami.

Ile co najwyżej porównań wymaga scalenie dwóch posortowanych list po 5 elementów?

  1. 5
  2. 9
  3. 10
  4. 25
Lekcja 18

Sortowanie szybkie (quicksort)

Wybierz element dzielący (pivot), ustaw mniejsze przed nim, a większe za nim, i posortuj obie części tak samo.

python
def quicksort(t, lewy, prawy):
    if lewy >= prawy:
        return
    pivot = t[prawy]             # element dzielący: ostatni
    i = lewy
    for j in range(lewy, prawy):
        if t[j] < pivot:         # mniejsze od pivota idą na początek
            t[i], t[j] = t[j], t[i]
            i += 1
    t[i], t[prawy] = t[prawy], t[i]   # pivot na swoje ostateczne miejsce
    quicksort(t, lewy, i - 1)
    quicksort(t, i + 1, prawy)

# wywołanie: quicksort(t, 0, len(t) - 1)
c++
#include <vector>
#include <utility>
using namespace std;

void quicksort(vector<int>& t, int lewy, int prawy) {
    if (lewy >= prawy) return;
    int pivot = t[prawy];        // element dzielący: ostatni
    int i = lewy;
    for (int j = lewy; j < prawy; j++)
        if (t[j] < pivot)        // mniejsze od pivota idą na początek
            swap(t[i++], t[j]);
    swap(t[i], t[prawy]);        // pivot na swoje ostateczne miejsce
    quicksort(t, lewy, i - 1);
    quicksort(t, i + 1, prawy);
}

// wywołanie: quicksort(t, 0, t.size() - 1);
pseudokod
procedura quicksort(T, lewy, prawy)
    jeżeli lewy ≥ prawy
        zakończ
    pivot ← T[prawy]                  // element dzielący: ostatni
    i ← lewy
    dla j = lewy, …, prawy−1 wykonuj
        jeżeli T[j] < pivot           // mniejsze idą na początek
            zamień T[i] i T[j]
            i ← i + 1
    zamień T[i] i T[prawy]            // pivot na swoje miejsce
    quicksort(T, lewy, i − 1)
    quicksort(T, i + 1, prawy)

Średnio quicksort działa w O(n log n) i w praktyce jest bardzo szybki. Przy złym wyborze pivota jedna część jest pusta, a druga ma n − 1 elementów. Tak dzieje się np. dla posortowanej tablicy i pivota równego ostatniemu elementowi. Wtedy złożoność spada do O(n²).

jeden podział
t = [3, 8, 2, 5, 1, 4], pivot = 4
po podziale: [3, 2, 1, 4, 8, 5]
             mniejsze ^ pivot ^ większe
Na egzaminie przy komputerze możesz po prostu użyć sorted(t) albo t.sort(). Własne sortowanie pisz tylko wtedy, gdy wymaga tego treść zadania.
Na egzaminie przy komputerze możesz po prostu użyć sort(t.begin(), t.end()) z biblioteki <algorithm>. Własne sortowanie pisz tylko wtedy, gdy wymaga tego treść zadania.
W pseudokodzie na papierze zapisz algorytm sortowania w całości, chyba że treść pozwala napisać „posortuj tablicę”.
Na maturze: w 2016 r. (zad. 2) analizowano funkcję, która przestawia tablicę tak, że na lewo są elementy mniejsze od pierwszego elementu (klucza), a na prawo pozostałe. To ten sam podział co w quicksorcie, tylko z kluczem na początku.

Kiedy quicksort z pivotem równym ostatniemu elementowi działa najwolniej?

  1. dla danych losowych
  2. dla danych już posortowanych
  3. dla tablicy z dwiema wartościami

Na której pozycji (od 0) stanie pivot 4 po podziale tablicy [3, 8, 2, 5, 1, 4]?

Lekcja 19

Sortowanie przez zliczanie i kubełkowe

Gdy wartości są małymi liczbami całkowitymi, nie trzeba ich porównywać. Wystarczy policzyć, ile razy występuje każda z nich.

python
def przez_zliczanie(t, maks):    # wszystkie liczby z przedziału 0..maks
    ile = [0] * (maks + 1)
    for x in t:
        ile[x] += 1
    wynik = []
    for v in range(maks + 1):
        wynik.extend([v] * ile[v])
    return wynik

def kubelkowe(t, k=10):          # liczby rzeczywiste z przedziału [0, 1)
    kubelki = [[] for _ in range(k)]
    for x in t:
        kubelki[int(x * k)].append(x)
    wynik = []
    for kub in kubelki:
        kub.sort()               # kubełki są małe, wystarczy dowolne proste sortowanie
        wynik.extend(kub)
    return wynik
c++
#include <vector>
#include <algorithm>
using namespace std;

vector<int> przez_zliczanie(const vector<int>& t, int maks) {   // liczby 0..maks
    vector<int> ile(maks + 1, 0);
    for (int x : t) ile[x]++;
    vector<int> wynik;
    for (int v = 0; v <= maks; v++)
        for (int k = 0; k < ile[v]; k++)
            wynik.push_back(v);
    return wynik;
}

vector<double> kubelkowe(const vector<double>& t, int k = 10) {  // liczby z [0, 1)
    vector<vector<double>> kubelki(k);
    for (double x : t) kubelki[(int)(x * k)].push_back(x);
    vector<double> wynik;
    for (auto& kub : kubelki) {
        sort(kub.begin(), kub.end());   // kubełki są małe
        for (double x : kub) wynik.push_back(x);
    }
    return wynik;
}
pseudokod
// wszystkie liczby z przedziału 0..maks
funkcja przez_zliczanie(T, maks)
    ILE[0..maks] ← 0
    dla każdego x w T wykonuj
        ILE[x] ← ILE[x] + 1
    W ← pusta lista
    dla v = 0, 1, …, maks wykonuj
        dopisz v do W  ILE[v] razy
    zwróć W

// liczby rzeczywiste z przedziału [0, 1), k kubełków
funkcja kubelkowe(T, k)
    K[0..k-1] ← puste listy
    dla każdego x w T wykonuj
        dopisz x do K[⌊x · k⌋]
    posortuj każdy kubełek (np. przez wstawianie)
    zwróć połączone K[0], K[1], …, K[k-1]

Sortowanie przez zliczanie ma złożoność O(n + k), gdzie k to zakres wartości. Świetnie sprawdza się dla ocen 1–6, wieku albo liczb do kilku tysięcy. Przy wartościach do miliarda tablica byłaby za duża.

Sortowanie kubełkowe dzieli zakres na przedziały (kubełki), sortuje każdy osobno i łączy wyniki. Dla równomiernie rozłożonych danych kubełki są małe, a całość działa średnio w O(n).

Zliczanie przydaje się nie tylko do sortowania. Tablica ile od razu odpowiada na pytania „która wartość występuje najczęściej” i „ile jest różnych wartości”. To częste zadania maturalne.

Tablica ocen to 5 3 5 6 3 5. Ile wynosi ile[5]?

Kiedy sortowanie przez zliczanie się nie opłaca?

  1. gdy liczb jest dużo
  2. gdy zakres wartości jest ogromny w porównaniu z liczbą elementów
  3. gdy wartości się powtarzają
Lekcja 20

Napisy: palindromy, anagramy, wzorce

Napis to tablica znaków, a każdy znak ma kod liczbowy. Większość algorytmów tekstowych to pętla po indeksach i porównywanie znaków.

ZnakKod ASCIIUwagi
0…948…57cyfra = kod − 48
A…Z65…90wielkie litery są „mniejsze” od małych
a…z97…122mała = wielka + 32
python
def palindrom(s):
    i, j = 0, len(s) - 1
    while i < j:
        if s[i] != s[j]:
            return False
        i += 1
        j -= 1
    return True

def anagramy(a, b):              # małe litery a-z
    if len(a) != len(b):
        return False
    ile = [0] * 26
    for z in a:
        ile[ord(z) - ord("a")] += 1
    for z in b:
        ile[ord(z) - ord("a")] -= 1
    for x in ile:
        if x != 0:
            return False
    return True

def wystapienia(tekst, wzorzec): # wyszukiwanie naiwne
    wynik = []
    n, m = len(tekst), len(wzorzec)
    for i in range(n - m + 1):
        j = 0
        while j < m and tekst[i + j] == wzorzec[j]:
            j += 1
        if j == m:
            wynik.append(i)
    return wynik
c++
#include <string>
#include <vector>
using namespace std;

bool palindrom(const string& s) {
    int i = 0, j = (int)s.size() - 1;
    while (i < j) {
        if (s[i] != s[j]) return false;
        i++;
        j--;
    }
    return true;
}

bool anagramy(const string& a, const string& b) {   // małe litery a-z
    if (a.size() != b.size()) return false;
    int ile[26] = {0};
    for (char z : a) ile[z - 'a']++;
    for (char z : b) ile[z - 'a']--;
    for (int x : ile)
        if (x != 0) return false;
    return true;
}

vector<int> wystapienia(const string& tekst, const string& wzorzec) {  // naiwnie
    vector<int> wynik;
    int n = tekst.size(), m = wzorzec.size();
    for (int i = 0; i + m <= n; i++) {
        int j = 0;
        while (j < m && tekst[i + j] == wzorzec[j]) j++;
        if (j == m) wynik.push_back(i);
    }
    return wynik;
}
pseudokod
funkcja palindrom(s[0..n-1])
    i ← 0;  j ← n − 1
    dopóki i < j wykonuj
        jeżeli s[i] ≠ s[j]
            zwróć FAŁSZ
        i ← i + 1;  j ← j − 1
    zwróć PRAWDA

funkcja anagramy(a, b)                // małe litery a–z
    jeżeli |a| ≠ |b|
        zwróć FAŁSZ
    ILE[0..25] ← 0
    dla każdej litery z w a:  ILE[kod(z) − kod('a')] ← ILE[…] + 1
    dla każdej litery z w b:  ILE[kod(z) − kod('a')] ← ILE[…] − 1
    zwróć PRAWDA, gdy wszystkie ILE[i] = 0

funkcja wystapienia(tekst[0..n-1], wzorzec[0..m-1])
    W ← pusta lista
    dla i = 0, 1, …, n−m wykonuj
        j ← 0
        dopóki j < m oraz tekst[i + j] = wzorzec[j] wykonuj
            j ← j + 1
        jeżeli j = m
            dopisz i do W
    zwróć W

Anagramy to słowa z tych samych liter w innej kolejności, np. kot i tok. Zliczanie liter działa w O(n). Porównanie posortowanych słów (sorted(a) == sorted(b)) też jest poprawne, ale wolniejsze: O(n log n).

Porządek leksykograficzny to porządek słownikowy według kodów znaków. W Pythonie i C++ porównujesz napisy zwykłym <, ale "Zebra" < "ala" jest prawdą, bo Z ma kod 90, a a 97.

Pod jakimi indeksami (od 0) wzorzec aba występuje w tekście abababa? Wpisz liczby oddzielone spacjami.

Które porównanie jest prawdziwe?

  1. "ala" < "Ala"
  2. "Ala" < "ala"
  3. Są równe

Początek i koniec słowa

Czy słowo a zaczyna się słowem p: a[:len(p)] == p albo a.startswith(p). Czy kończy się: a.endswith(p). Najdłuższy fragment, który jest początkiem a i końcem b, znajdziesz, sprawdzając długości d od największej: a[:d] == b[len(b) - d:].

Czy słowo a zaczyna się słowem p: a.compare(0, p.size(), p) == 0 albo a.substr(0, p.size()) == p. Najdłuższy fragment, który jest początkiem a i końcem b, znajdziesz, sprawdzając długości d od największej: a.substr(0, d) == b.substr(b.size() - d).

Na maturze: w 2026 r. (zad. 3) liczono sumy kodów ASCII słów, wspólne litery dwóch słów (zliczanie w tablicy 26 liczników, jak przy anagramach) i najdłuższy fragment będący początkiem jednego i końcem drugiego słowa. W 2025 r. (zad. 2) szukano palindromów.

Jaka jest długość najdłuższego fragmentu, który jest początkiem słowa abcab i końcem słowa xxab?

Czy kajak i kajaki są anagramami?

  1. Tak
  2. Nie
Lekcja 21

Szyfry: Cezar, Vigenère, przestawieniowy

Szyfry podstawieniowe zamieniają każdą literę na inną. Szyfry przestawieniowe zostawiają litery, ale zmieniają ich kolejność.

python
def cezar(tekst, k):             # wielkie litery A-Z, reszta bez zmian
    wynik = ""
    for z in tekst:
        if "A" <= z <= "Z":
            wynik += chr((ord(z) - 65 + k) % 26 + 65)
        else:
            wynik += z
    return wynik
# odszyfrowanie: cezar(szyfrogram, -k)

def vigenere(tekst, klucz, kierunek=1):   # kierunek -1 odszyfrowuje
    wynik = ""
    for i in range(len(tekst)):
        k = ord(klucz[i % len(klucz)]) - 65
        wynik += chr((ord(tekst[i]) - 65 + kierunek * k) % 26 + 65)
    return wynik

def przestaw_pary(tekst):        # szyfr przestawieniowy: zamiana sąsiednich znaków
    t = list(tekst)
    for i in range(0, len(t) - 1, 2):
        t[i], t[i + 1] = t[i + 1], t[i]
    return "".join(t)
c++
#include <string>
#include <utility>
using namespace std;

string cezar(const string& tekst, int k) {   // wielkie litery A-Z, reszta bez zmian
    string wynik = tekst;
    k = (k % 26 + 26) % 26;      // w C++ reszta z liczby ujemnej jest ujemna
    for (char& z : wynik)
        if (z >= 'A' && z <= 'Z')
            z = (z - 'A' + k) % 26 + 'A';
    return wynik;
}
// odszyfrowanie: cezar(szyfrogram, -k)

string vigenere(const string& tekst, const string& klucz, int kierunek = 1) {
    string wynik = tekst;
    for (int i = 0; i < (int)tekst.size(); i++) {
        int k = klucz[i % klucz.size()] - 'A';
        wynik[i] = ((tekst[i] - 'A' + kierunek * k) % 26 + 26) % 26 + 'A';
    }
    return wynik;
}

string przestaw_pary(string t) { // szyfr przestawieniowy: zamiana sąsiednich znaków
    for (int i = 0; i + 1 < (int)t.size(); i += 2)
        swap(t[i], t[i + 1]);
    return t;
}
pseudokod
funkcja cezar(tekst, k)               // wielkie litery A–Z
    dla każdego znaku z w tekst wykonuj
        jeżeli z jest literą A–Z
            z ← znak o kodzie ((kod(z) − 65 + k) mod 26) + 65
    zwróć tekst
// odszyfrowanie: cezar(szyfrogram, −k), mod daje wynik 0..25

funkcja vigenere(tekst[0..n-1], klucz[0..d-1], kierunek)
    dla i = 0, 1, …, n-1 wykonuj
        k ← kod(klucz[i mod d]) − 65
        tekst[i] ← znak o kodzie ((kod(tekst[i]) − 65 + kierunek · k) mod 26) + 65
    zwróć tekst

funkcja przestaw_pary(t[0..n-1])      // zamiana sąsiednich znaków
    i ← 0
    dopóki i + 1 < n wykonuj
        zamień t[i] i t[i + 1]
        i ← i + 2
    zwróć t

W szyfrze Cezara każdą literę przesuwamy o k pozycji w alfabecie, a po Z wracamy do A dzięki reszcie z dzielenia przez 26. Szyfr Vigenère’a używa innego przesunięcia dla każdej litery, wziętego z kolejnej litery klucza (A = 0, B = 1, …).

Reszta z liczby ujemnej: w Pythonie -3 % 26 == 23, więc odszyfrowanie przez cezar(t, -k) działa od razu.
Reszta z liczby ujemnej: w C++ -3 % 26 == -3. Przy odszyfrowaniu zawsze używaj (x % 26 + 26) % 26.
Reszta z liczby ujemnej: w pseudokodzie przyjmuje się, że mod daje wynik od 0 do 25. W programie sprawdź, jak działa reszta w Twoim języku.

Zaszyfruj szyfrem Cezara z k = 3 tekst ABC XYZ.

Zaszyfruj szyfrem Vigenère’a tekst MATURA kluczem AB.

Co da przestaw_pary("INFORMATYKA")?

Lekcja 22

Tablice dwuwymiarowe: plansze i obrazy

Plik z wierszami jednakowej długości to plansza: T[i][j] to znak w wierszu i, kolumnie j. Sąsiadów pola liczymy przesunięciami wiersza i kolumny.

Wczytanie: T = [w.strip() for w in open("plansza.txt")]. Liczba wierszy to len(T), kolumn len(T[0]).

Wczytanie: vector<string> T; string w; while (we >> w) T.push_back(w);. Liczba wierszy to T.size(), kolumn T[0].size().

python
from collections import deque

def kwadraty(T):                 # środki kwadratów 3×3 z jednakowych znaków
    wynik = []
    for i in range(1, len(T) - 1):
        for j in range(1, len(T[0]) - 1):
            dobry = True
            for di in (-1, 0, 1):
                for dj in (-1, 0, 1):
                    if T[i + di][j + dj] != T[i][j]:
                        dobry = False
            if dobry:
                wynik.append((i, j))
    return wynik

def dojscie(A):                  # czy z lewego górnego rogu dojdziemy do prawego dolnego,
    n, m = len(A), len(A[0])     # idąc w prawo lub w dół tylko po polach 1
    P = [[False] * m for _ in range(n)]
    for i in range(n):
        for j in range(m):
            if A[i][j] == 1:
                if i == 0 and j == 0:
                    P[i][j] = True
                else:
                    P[i][j] = (i > 0 and P[i - 1][j]) or (j > 0 and P[i][j - 1])
    return P[n - 1][m - 1]

def obszar(T, i, j):             # BFS: wielkość obszaru takich samych znaków wokół (i, j)
    znak = T[i][j]
    odwiedzone = {(i, j)}
    kolejka = deque([(i, j)])
    while kolejka:
        y, x = kolejka.popleft()
        for dy, dx in ((-1, 0), (1, 0), (0, -1), (0, 1)):
            ny, nx = y + dy, x + dx
            if 0 <= ny < len(T) and 0 <= nx < len(T[0]) \
                    and (ny, nx) not in odwiedzone and T[ny][nx] == znak:
                odwiedzone.add((ny, nx))
                kolejka.append((ny, nx))
    return len(odwiedzone)
c++
#include <string>
#include <vector>
#include <queue>
#include <utility>
using namespace std;

vector<pair<int, int>> kwadraty(const vector<string>& T) {   // środki kwadratów 3×3
    vector<pair<int, int>> wynik;
    for (int i = 1; i + 1 < (int)T.size(); i++)
        for (int j = 1; j + 1 < (int)T[0].size(); j++) {
            bool dobry = true;
            for (int di = -1; di <= 1; di++)
                for (int dj = -1; dj <= 1; dj++)
                    if (T[i + di][j + dj] != T[i][j]) dobry = false;
            if (dobry) wynik.push_back({i, j});
        }
    return wynik;
}

bool dojscie(const vector<vector<int>>& A) {   // w prawo lub w dół, tylko po polach 1
    int n = A.size(), m = A[0].size();
    vector<vector<bool>> P(n, vector<bool>(m, false));
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++)
            if (A[i][j] == 1) {
                if (i == 0 && j == 0) P[i][j] = true;
                else P[i][j] = (i > 0 && P[i - 1][j]) || (j > 0 && P[i][j - 1]);
            }
    return P[n - 1][m - 1];
}

int obszar(const vector<string>& T, int i, int j) {   // BFS: wielkość obszaru wokół (i, j)
    int n = T.size(), m = T[0].size(), ile = 1;
    vector<vector<bool>> odw(n, vector<bool>(m, false));
    queue<pair<int, int>> kolejka;
    odw[i][j] = true;
    kolejka.push({i, j});
    int dy[] = {-1, 1, 0, 0}, dx[] = {0, 0, -1, 1};
    while (!kolejka.empty()) {
        auto [y, x] = kolejka.front();
        kolejka.pop();
        for (int k = 0; k < 4; k++) {
            int ny = y + dy[k], nx = x + dx[k];
            if (ny >= 0 && ny < n && nx >= 0 && nx < m && !odw[ny][nx] && T[ny][nx] == T[i][j]) {
                odw[ny][nx] = true;
                ile++;
                kolejka.push({ny, nx});
            }
        }
    }
    return ile;
}
pseudokod
funkcja kwadraty(T[0..n-1][0..m-1])   // środki kwadratów 3×3
    dla i = 1, …, n−2 wykonuj
        dla j = 1, …, m−2 wykonuj
            jeżeli wszystkie T[i+di][j+dj] = T[i][j] dla di, dj ∈ {−1, 0, 1}
                dopisz (i, j) do wyniku

funkcja dojscie(A[0..n-1][0..m-1])    // w prawo lub w dół po polach 1
    dla i = 0, …, n−1 wykonuj
        dla j = 0, …, m−1 wykonuj
            jeżeli A[i][j] = 0
                P[i][j] ← FAŁSZ
            w przeciwnym razie jeżeli i = 0 oraz j = 0
                P[i][j] ← PRAWDA
            w przeciwnym razie
                P[i][j] ← (i > 0 oraz P[i−1][j]) lub (j > 0 oraz P[i][j−1])
    zwróć P[n−1][m−1]

funkcja obszar(T, i, j)               // przeszukiwanie wszerz (BFS)
    oznacz (i, j) jako odwiedzone;  wstaw (i, j) do kolejki
    dopóki kolejka niepusta wykonuj
        (y, x) ← wyjmij z kolejki
        dla każdego sąsiada (y', x') z góry, dołu, lewej i prawej wykonuj
            jeżeli (y', x') na planszy, nieodwiedzone i T[y'][x'] = T[i][j]
                oznacz jako odwiedzone;  wstaw do kolejki
    zwróć liczbę odwiedzonych pól

Kwadrat 3×3 sprawdzamy dla każdego pola, które ma sąsiadów ze wszystkich stron, czyli bez pierwszego i ostatniego wiersza i kolumny. To częsta pułapka: wyjście poza tablicę.

W dojscie pole jest osiągalne, jeśli jest białe i osiągalne jest pole nad nim albo na lewo od niego. Wypełniamy tablicę wierszami, więc obie wartości są już policzone. To programowanie dynamiczne na planszy.

Gdy ruchy są dowolne (góra, dół, lewo, prawo), używamy przeszukiwania wszerz (BFS): kolejka pól do odwiedzenia i zbiór już odwiedzonych. Tak liczy się wielkość jednolitego obszaru albo najkrótszą drogę.

Na maturze: w 2024 r. (zad. 1) analizowano algorytm osiągalności na planszy z ruchami w prawo i w dół. W 2025 r. (zad. 2) szukano w pliku kwadratów 3×3 z identycznych symboli.

Ile kwadratów 3×3 z jednakowych znaków jest na planszy 4×4 wypełnionej samymi x?

Plansza (1 = białe): 110 / 010 / 011. Czy da się przejść z lewego górnego do prawego dolnego rogu, idąc tylko w prawo i w dół po białych polach?

  1. Tak
  2. Nie
Lekcja 23

Drzewa: BST, kopiec i hierarchia

Drzewo binarne mieści się w zwykłej tablicy: korzeń ma indeks 1, a dzieci węzła i mają indeksy 2i oraz 2i + 1. Rodzic węzła s to s div 2.

W drzewie BST mniejsze liczby idą do lewego poddrzewa, a większe do prawego. Przechodząc drzewo w kolejności lewe poddrzewo, węzeł, prawe (inorder), wypisujemy liczby rosnąco. W kopcu każdy rodzic jest większy od dzieci, więc największy element jest w korzeniu. Nowy element wstawiamy na koniec i zamieniamy z rodzicem, dopóki jest od niego większy.

python
def wstaw_bst(D, x):             # drzewo w tablicy: korzeń D[1], dzieci węzła i to 2i i 2i+1
    i = 1
    while i < len(D) and D[i] is not None:
        if x < D[i]:
            i = 2 * i            # mniejsze w lewo
        else:
            i = 2 * i + 1        # większe w prawo
    while len(D) <= i:
        D.append(None)
    D[i] = x

def inorder(D, i, wynik):        # lewe poddrzewo, węzeł, prawe: daje liczby rosnąco
    if i < len(D) and D[i] is not None:
        inorder(D, 2 * i, wynik)
        wynik.append(D[i])
        inorder(D, 2 * i + 1, wynik)

def wstaw_kopiec(T, x):          # kopiec w T[1..n]: rodzic węzła s to s // 2
    T.append(x)
    s = len(T) - 1
    while s > 1 and T[s] > T[s // 2]:   # większy od rodzica wędruje w górę
        T[s], T[s // 2] = T[s // 2], T[s]
        s //= 2

def hierarchia(szef):            # szef[x] < x to bezpośredni przełożony x; szef[1] = 0
    n = len(szef) - 1
    dzieci = [0] * (n + 1)       # bezpośredni podwładni
    wszyscy = [0] * (n + 1)      # wszyscy podwładni
    glebokosc = [0] * (n + 1)    # liczba przełożonych
    for x in range(2, n + 1):
        dzieci[szef[x]] += 1
        glebokosc[x] = glebokosc[szef[x]] + 1   # szef ma mniejszy numer: już policzony
    for x in range(n, 1, -1):    # od końca: podwładni x są już zliczeni
        wszyscy[szef[x]] += wszyscy[x] + 1
    return dzieci, wszyscy, glebokosc
c++
#include <vector>
#include <utility>
using namespace std;

const int PUSTE = -1;   // drzewo w tablicy: korzeń D[1], dzieci węzła i to 2i i 2i+1

void wstaw_bst(vector<int>& D, int x) {
    int i = 1;
    while (i < (int)D.size() && D[i] != PUSTE)
        i = x < D[i] ? 2 * i : 2 * i + 1;   // mniejsze w lewo, większe w prawo
    if ((int)D.size() <= i) D.resize(i + 1, PUSTE);
    D[i] = x;
}

void inorder(const vector<int>& D, int i, vector<int>& wynik) {   // daje liczby rosnąco
    if (i >= (int)D.size() || D[i] == PUSTE) return;
    inorder(D, 2 * i, wynik);
    wynik.push_back(D[i]);
    inorder(D, 2 * i + 1, wynik);
}

void wstaw_kopiec(vector<int>& T, int x) {   // kopiec w T[1..n]: rodzic s to s / 2
    T.push_back(x);
    int s = T.size() - 1;
    while (s > 1 && T[s] > T[s / 2]) {       // większy od rodzica wędruje w górę
        swap(T[s], T[s / 2]);
        s /= 2;
    }
}

// szef[x] < x to bezpośredni przełożony x; szef[1] = 0
void hierarchia(const vector<int>& szef, vector<int>& dzieci, vector<int>& wszyscy, vector<int>& glebokosc) {
    int n = szef.size() - 1;
    dzieci.assign(n + 1, 0);
    wszyscy.assign(n + 1, 0);
    glebokosc.assign(n + 1, 0);
    for (int x = 2; x <= n; x++) {
        dzieci[szef[x]]++;
        glebokosc[x] = glebokosc[szef[x]] + 1;   // szef ma mniejszy numer: już policzony
    }
    for (int x = n; x >= 2; x--)                 // od końca: podwładni x są już zliczeni
        wszyscy[szef[x]] += wszyscy[x] + 1;
}
pseudokod
// drzewo w tablicy: korzeń D[1], dzieci węzła i to 2i oraz 2i+1
procedura wstaw_bst(D, x)
    i ← 1
    dopóki D[i] niepuste wykonuj
        jeżeli x < D[i]
            i ← 2 · i
        w przeciwnym razie
            i ← 2 · i + 1
    D[i] ← x

procedura inorder(D, i)               // wypisuje liczby rosnąco
    jeżeli D[i] niepuste
        inorder(D, 2 · i)
        wypisz D[i]
        inorder(D, 2 · i + 1)

procedura wstaw_kopiec(T[1..n], x)    // rodzic węzła s to s div 2
    n ← n + 1;  T[n] ← x;  s ← n
    dopóki s > 1 oraz T[s] > T[s div 2] wykonuj
        zamień T[s] i T[s div 2]
        s ← s div 2

procedura hierarchia(szef[1..n])      // szef[x] < x
    dla x = 2, …, n wykonuj
        dzieci[szef[x]] ← dzieci[szef[x]] + 1
        glebokosc[x] ← glebokosc[szef[x]] + 1
    dla x = n, n−1, …, 2 wykonuj
        wszyscy[szef[x]] ← wszyscy[szef[x]] + wszyscy[x] + 1
BST w tablicy
wstawiane: 10, 4, 15, 12, 2, 7, 20

            10            D[1]
         4      15        D[2], D[3]
       2   7  12  20      D[4] … D[7]

Wstawianie liczb już posortowanych tworzy w BST jedną długą gałąź: 1, 2, 3 trafiają pod indeksy 1, 3 i 7. Tablica rośnie wtedy wykładniczo, a drzewo ma wysokość n.

Hierarchia zapisana tablicą przełożonych

Gdy dla każdego węzła znamy tylko rodzica i rodzic ma zawsze mniejszy numer, nie trzeba budować drzewa. Przechodząc numery rosnąco, liczymy głębokość (rodzic jest już policzony). Przechodząc malejąco, sumujemy podwładnych (dzieci są już policzone). Obie pętle mają O(n).

Na maturze: w 2023 r. (zad. 1) książki ustawiano na półkach jak w drzewie BST: przegródka j na półce i miała pod sobą przegródki 2j − 1 i 2j. W 2021 r. (zad. 2) analizowano wstawianie do kopca. W 2026 r. (zad. 4) plik zawierał numery przełożonych 50 000 pracowników, a trzeba było policzyć podwładnych i liczbę przełożonych.

Pod jakim indeksem tablicy (korzeń = 1) stanie liczba 7 po wstawieniu do pustego BST kolejno 10, 4, 15, 12, 2, 7?

Do pustego kopca wstawiamy 5, 3, 8, 1, 9. Jaka liczba jest w T[2]?

W jakiej kolejności inorder wypisuje BST?

  1. tak, jak wstawiano
  2. rosnąco
  3. poziomami od korzenia
Lekcja 24

Miejsce zerowe, pierwiastek, pole i Monte Carlo

Niektórych wartości nie da się policzyć dokładnie. Wtedy przybliżamy je krok po kroku, aż błąd będzie mniejszy od zadanej dokładności.

Połowienie przedziału (bisekcja)

Jeśli funkcja ciągła ma na końcach przedziału [a, b] różne znaki, to gdzieś w środku ma miejsce zerowe. Sprawdzamy znak w środku i zostawiamy tę połowę, w której zmienia się znak. Każdy krok połowi przedział.

python
def f(x):
    return x**3 - x - 2

def bisekcja(a, b, eps):         # f(a) i f(b) mają różne znaki
    while b - a > eps:
        s = (a + b) / 2
        if f(a) * f(s) <= 0:     # zmiana znaku w lewej połowie
            b = s
        else:
            a = s
    return (a + b) / 2

def pierwiastek(p, eps):         # metoda Herona (Newtona) dla p > 0
    x = p
    while abs(x * x - p) > eps:
        x = (x + p / x) / 2
    return x
c++
#include <cmath>
using namespace std;

double f(double x) {
    return x * x * x - x - 2;
}

double bisekcja(double a, double b, double eps) {   // f(a) i f(b) mają różne znaki
    while (b - a > eps) {
        double s = (a + b) / 2;
        if (f(a) * f(s) <= 0) b = s;   // zmiana znaku w lewej połowie
        else a = s;
    }
    return (a + b) / 2;
}

double pierwiastek(double p, double eps) {   // metoda Herona (Newtona) dla p > 0
    double x = p;
    while (fabs(x * x - p) > eps)
        x = (x + p / x) / 2;
    return x;
}
pseudokod
f(x) = x³ − x − 2

// f(a) i f(b) mają różne znaki
funkcja bisekcja(a, b, ε)
    dopóki b − a > ε wykonuj
        s ← (a + b) / 2
        jeżeli f(a) · f(s) ≤ 0        // zmiana znaku w lewej połowie
            b ← s
        w przeciwnym razie
            a ← s
    zwróć (a + b) / 2

funkcja pierwiastek(p, ε)             // metoda Herona, p > 0
    x ← p
    dopóki |x · x − p| > ε wykonuj
        x ← (x + p / x) / 2
    zwróć x

Metoda Herona (Newtona) uśrednia x i p/x: jeśli x jest za duże, to p/x jest za małe, więc średnia leży bliżej √p. Zbiega bardzo szybko, zwykle w kilku krokach.

Pole pod wykresem

Dzielimy [a, b] na n pasków o szerokości h i sumujemy pola prostokątów albo trapezów. Im większe n, tym lepsze przybliżenie.

python
def g(x):
    return x * x

def pole_prostokaty(a, b, n):    # wysokość brana w środku każdego paska
    h = (b - a) / n
    s = 0
    for i in range(n):
        s += g(a + i * h + h / 2)
    return s * h

def pole_trapezy(a, b, n):
    h = (b - a) / n
    s = (g(a) + g(b)) / 2
    for i in range(1, n):
        s += g(a + i * h)
    return s * h
c++
double g(double x) {
    return x * x;
}

double pole_prostokaty(double a, double b, int n) {   // wysokość w środku paska
    double h = (b - a) / n, s = 0;
    for (int i = 0; i < n; i++)
        s += g(a + i * h + h / 2);
    return s * h;
}

double pole_trapezy(double a, double b, int n) {
    double h = (b - a) / n, s = (g(a) + g(b)) / 2;
    for (int i = 1; i < n; i++)
        s += g(a + i * h);
    return s * h;
}
pseudokod
g(x) = x²

funkcja pole_prostokaty(a, b, n)      // wysokość w środku paska
    h ← (b − a) / n;  s ← 0
    dla i = 0, 1, …, n-1 wykonuj
        s ← s + g(a + i · h + h / 2)
    zwróć s · h

funkcja pole_trapezy(a, b, n)
    h ← (b − a) / n
    s ← (g(a) + g(b)) / 2
    dla i = 1, 2, …, n-1 wykonuj
        s ← s + g(a + i · h)
    zwróć s · h

Metoda Monte Carlo

Losujemy punkty w kwadracie i liczymy, jaka część trafiła do figury. Stosunek pól jest w przybliżeniu równy stosunkowi trafień. Dla ćwiartki koła o promieniu 1 to π/4.

python
import random

def pi_monte_carlo(n):
    trafione = 0
    for _ in range(n):
        x, y = random.random(), random.random()   # punkt w kwadracie 1×1
        if x * x + y * y <= 1:                    # wewnątrz ćwiartki koła
            trafione += 1
    return 4 * trafione / n
c++
#include <random>
using namespace std;

double pi_monte_carlo(int n) {
    mt19937 gen(2026);                            // generator z ustalonym ziarnem
    uniform_real_distribution<double> los(0.0, 1.0);
    int trafione = 0;
    for (int i = 0; i < n; i++) {
        double x = los(gen), y = los(gen);        // punkt w kwadracie 1×1
        if (x * x + y * y <= 1) trafione++;       // wewnątrz ćwiartki koła
    }
    return 4.0 * trafione / n;
}
pseudokod
funkcja pi_monte_carlo(n)
    trafione ← 0
    powtórz n razy
        x ← losowa liczba z [0, 1)
        y ← losowa liczba z [0, 1)    // punkt w kwadracie 1×1
        jeżeli x² + y² ≤ 1            // wewnątrz ćwiartki koła
            trafione ← trafione + 1
    zwróć 4 · trafione / n

Ile kroków bisekcji potrzeba, żeby przedział [0, 1] skurczył się do długości nie większej niż 0,001?

Wykonaj jeden krok metody Herona dla p = 10, zaczynając od x = 10. Jakie jest nowe x?

Jaki będzie wynik metody trapezów dla funkcji liniowej?

  1. Zawsze przybliżony
  2. Dokładny, bo trapezy idealnie pokrywają pole pod prostą
  3. Zależy od n
Lekcja 25

Geometria na liczbach całkowitych

Punkty o współrzędnych całkowitych pozwalają liczyć dokładnie, bez błędów zaokrągleń. Wystarczą dodawanie, mnożenie i porównanie.

python
def iloczyn(ax, ay, bx, by, cx, cy):     # iloczyn wektorowy AB × AC
    return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
# > 0: C na lewo od prostej AB, < 0: na prawo, = 0: A, B, C leżą na jednej prostej

def pole_trojkata(ax, ay, bx, by, cx, cy):
    return abs(iloczyn(ax, ay, bx, by, cx, cy)) / 2

def mniejszy_ulamek(a, b, c, d):         # czy a/b < c/d, gdy b > 0 i d > 0
    return a * d < c * b                 # bez dzielenia: wynik dokładny

def pozycje(ruchy):                      # kolejne punkty po przesunięciach (dx, dy)
    x = y = 0
    wynik = []
    for dx, dy in ruchy:
        x += dx
        y += dy
        wynik.append((x, y))
    return wynik
c++
#include <cstdlib>
#include <vector>
#include <utility>
using namespace std;

long long iloczyn(long long ax, long long ay, long long bx, long long by, long long cx, long long cy) {
    return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);   // iloczyn wektorowy AB × AC
}
// > 0: C na lewo od prostej AB, < 0: na prawo, = 0: A, B, C leżą na jednej prostej

double pole_trojkata(long long ax, long long ay, long long bx, long long by, long long cx, long long cy) {
    return llabs(iloczyn(ax, ay, bx, by, cx, cy)) / 2.0;
}

bool mniejszy_ulamek(long long a, long long b, long long c, long long d) {   // a/b < c/d, b, d > 0
    return a * d < c * b;                // bez dzielenia: wynik dokładny
}

vector<pair<long long, long long>> pozycje(const vector<pair<long long, long long>>& ruchy) {
    long long x = 0, y = 0;
    vector<pair<long long, long long>> wynik;
    for (auto [dx, dy] : ruchy) {
        x += dx;
        y += dy;
        wynik.push_back({x, y});
    }
    return wynik;
}
pseudokod
funkcja iloczyn(A, B, C)              // iloczyn wektorowy AB × AC
    zwróć (xB − xA)·(yC − yA) − (yB − yA)·(xC − xA)
// > 0: C na lewo od AB;  < 0: na prawo;  = 0: współliniowe

funkcja pole_trojkata(A, B, C)
    zwróć |iloczyn(A, B, C)| / 2

funkcja mniejszy_ulamek(a, b, c, d)   // a/b < c/d przy b, d > 0
    zwróć a · d < c · b

funkcja pozycje(ruchy)
    x ← 0;  y ← 0
    dla każdego (dx, dy) w ruchy wykonuj
        x ← x + dx;  y ← y + dy
        dopisz (x, y) do wyniku

Iloczyn wektorowy mówi, po której stronie prostej AB leży punkt C. Gdy jest równy 0, punkty leżą na jednej prostej. Jego wartość bezwzględna to podwojone pole trójkąta ABC.

Kąty, nachylenia i ułamki porównuj mnożeniem na krzyż zamiast dzielenia: a/b < c/d ⇔ a·d < c·b (przy dodatnich b i d). Odległości porównuj bez pierwiastka, przez kwadrat: dx² + dy².

Punkt (x, y) leży w prostokącie o rogach (x1, y1) i (x2, y2), gdy x1 ≤ x ≤ x2 i y1 ≤ y ≤ y2. Zwróć uwagę, czy brzeg się liczy: „wewnątrz” zwykle oznacza nierówności ostre.

Na maturze: w 2018 r. (zad. 2) szczyty gór porządkowano według ilorazu x/y, czyli przez porównywanie ułamków. W 2025 r. (zad. 3) dron wykonywał przesunięcia: trzeba było policzyć jego kolejne pozycje, sprawdzić punkty w kwadracie i NWD przesunięć.

Ile wynosi pole trójkąta o wierzchołkach (0, 0), (4, 0), (0, 3)?

Czy punkty (1, 1), (3, 2), (7, 4) leżą na jednej prostej?

  1. Tak
  2. Nie

Dron startuje z (0, 0) i wykonuje przesunięcia (2, 3), (4, −1), (1, −2). Podaj jego końcową pozycję jako dwie liczby.

Lekcja 26

Algorytm zachłanny i programowanie dynamiczne

Algorytm zachłanny w każdym kroku wybiera to, co w tej chwili wygląda najlepiej. Bywa szybki i poprawny, ale nie zawsze.

Przy wydawaniu reszty zachłannie bierzemy jak najwięcej największych monet. Dla polskich nominałów (1, 2, 5, 10, 20, 50 gr…) daje to zawsze najmniej monet. Dla nominałów 1, 3, 4 i kwoty 6 algorytm weźmie 4 + 1 + 1, czyli 3 monety, choć wystarczą 3 + 3.

Programowanie dynamiczne rozwiązuje problem dla wszystkich mniejszych kwot i zapisuje wyniki w tablicy. dp[k] to najmniejsza liczba monet na kwotę k: sprawdzamy każdą monetę m i bierzemy dp[k − m] + 1.

python
def reszta_zachlanna(kwota, nominaly):   # nominały od największego
    ile = 0
    for m in nominaly:
        ile += kwota // m
        kwota %= m
    return ile

def reszta_optymalna(kwota, nominaly):
    NIESKONCZONOSC = kwota + 1
    dp = [0] + [NIESKONCZONOSC] * kwota  # dp[k]: najmniej monet na kwotę k
    for k in range(1, kwota + 1):
        for m in nominaly:
            if m <= k and dp[k - m] + 1 < dp[k]:
                dp[k] = dp[k - m] + 1
    return dp[kwota]
c++
#include <vector>
using namespace std;

int reszta_zachlanna(int kwota, const vector<int>& nominaly) {   // od największego
    int ile = 0;
    for (int m : nominaly) {
        ile += kwota / m;
        kwota %= m;
    }
    return ile;
}

int reszta_optymalna(int kwota, const vector<int>& nominaly) {
    vector<int> dp(kwota + 1, kwota + 1);   // dp[k]: najmniej monet na kwotę k
    dp[0] = 0;
    for (int k = 1; k <= kwota; k++)
        for (int m : nominaly)
            if (m <= k && dp[k - m] + 1 < dp[k])
                dp[k] = dp[k - m] + 1;
    return dp[kwota];
}
pseudokod
// N[0..k-1] – nominały od największego
funkcja reszta_zachlanna(kwota, N)
    ile ← 0
    dla każdego m w N wykonuj
        ile ← ile + kwota div m
        kwota ← kwota mod m
    zwróć ile

funkcja reszta_optymalna(kwota, N)
    dp[0] ← 0
    dp[1..kwota] ← ∞                  // dp[k]: najmniej monet na kwotę k
    dla k = 1, 2, …, kwota wykonuj
        dla każdego m w N wykonuj
            jeżeli m ≤ k oraz dp[k − m] + 1 < dp[k]
                dp[k] ← dp[k − m] + 1
    zwróć dp[kwota]
k0123456
dp[k] dla 1, 3, 40121122
StrategiaKiedyPrzykłady
dziel i zwyciężajproblem dzieli się na niezależne mniejsze częściscalanie, quicksort, szybkie potęgowanie, wyszukiwanie binarne
zachłannalokalnie najlepszy wybór prowadzi do optimumreszta w polskich monetach, plan zajęć według najwcześniejszego końca
programowanie dynamicznepodproblemy się powtarzająreszta dla dowolnych nominałów, Fibonacci z tablicą, plecak

Wybór przedziałów

Z listy przedziałów czasu (np. filmów, zajęć) wybieramy jak najwięcej takich, które na siebie nie nachodzą. Strategia zachłanna, która zawsze daje optimum: sortujemy po końcu i bierzemy każdy przedział, który zaczyna się nie wcześniej niż koniec ostatnio wybranego.

python
def najwiecej_przedzialow(P):    # P: lista par (początek, koniec)
    ile = 0
    koniec = None
    for p, k in sorted(P, key=lambda x: x[1]):   # od najwcześniejszego końca
        if koniec is None or p >= koniec:
            ile += 1
            koniec = k
    return ile
c++
#include <vector>
#include <algorithm>
#include <utility>
using namespace std;

int najwiecej_przedzialow(vector<pair<int, int>> P) {   // pary (początek, koniec)
    sort(P.begin(), P.end(), [](auto a, auto b) { return a.second < b.second; });
    int ile = 0;
    bool pierwszy = true;
    int koniec = 0;
    for (auto [p, k] : P)                // od najwcześniejszego końca
        if (pierwszy || p >= koniec) {
            ile++;
            koniec = k;
            pierwszy = false;
        }
    return ile;
}
pseudokod
funkcja najwiecej_przedzialow(P)      // pary (początek, koniec)
    posortuj P rosnąco według końca
    ile ← 0;  koniec ← −∞
    dla każdej pary (p, k) w P wykonuj
        jeżeli p ≥ koniec
            ile ← ile + 1
            koniec ← k
    zwróć ile
Na maturze: w 2015 r. (zad. 1) widz chciał obejrzeć jak najwięcej filmów w całości. To dokładnie ten problem.

Po czym sortować przedziały, żeby zachłanny wybór był optymalny?

  1. po początku
  2. po długości
  3. po końcu

Przedziały: 9–12, 11–16, 12–14, 15–17, 13–15. Ile najwięcej z nich można wybrać bez nakładania się (koniec jednego może być początkiem następnego)?

Ile monet wyda algorytm zachłanny dla kwoty 6 i nominałów 4, 3, 1?

Ile monet to optimum dla kwoty 6 i nominałów 4, 3, 1?

Kwota 30, nominały 25, 10, 1. Czy algorytm zachłanny da optimum?

  1. Tak
  2. Nie: zachłannie 25 + 5·1 = 6 monet, a 10 + 10 + 10 to 3 monety
Lekcja 27

Arkusz kalkulacyjny: formuły i adresowanie

W każdym arkuszu od 2023 r. jedno zadanie rozwiązuje się w arkuszu kalkulacyjnym: wczytujesz plik z danymi, dopisujesz kolumny z formułami, robisz zestawienie i wykres. Możesz użyć Excela albo LibreOffice Calc.

Wczytanie pliku

Adresy względne i bezwzględne

Formuła kopiowana w dół zmienia adresy: =C2*D2 w wierszu 3 staje się =C3*D3. Znak $ blokuje zmianę: $H$1 zostaje zawsze tą samą komórką, $A2 blokuje kolumnę, A$2 blokuje wiersz. Klawisz F4 przełącza te warianty.

arkusz
=C2*D2                         wartość sprzedaży w wierszu
=C2*$H$1                       cena z jednej komórki, wspólnej dla wszystkich wierszy
=SUMA(E2:E366)                 suma kolumny
=ŚREDNIA(C2:C366)
=MAX(C2:C366)    =MIN(C2:C366)

Warunki

arkusz
=JEŻELI(C2>=10;"duża";"mała")
=JEŻELI(ORAZ(C2>=5;D2<3);1;0)            oba warunki
=JEŻELI(LUB(B2="bułka";B2="rogal");1;0)  co najmniej jeden
=JEŻELI(C2<20;"mało";JEŻELI(C2<50;"średnio";"dużo"))   zagnieżdżone

Liczenie i sumowanie z warunkiem

arkusz
=LICZ.JEŻELI(B2:B500;"bułka")               ile wierszy z bułką
=LICZ.JEŻELI(C2:C500;">=10")                ile wierszy z ilością co najmniej 10
=SUMA.JEŻELI(B2:B500;"bułka";E2:E500)       suma wartości dla bułek
=SUMA.WARUNKÓW(E2:E500;B2:B500;"bułka";F2:F500;3)   bułki w marcu (F: miesiąc)
=LICZ.WARUNKI(B2:B500;"bułka";C2:C500;">=10")

Tekst i wyszukiwanie

arkusz
=LEWY(B2;3)   =PRAWY(B2;2)   =FRAGMENT.TEKSTU(B2;2;4)   =DŁ(B2)
=B2&" "&C2                              sklejenie tekstów
=WYSZUKAJ.PIONOWO(B2;$K$2:$L$20;2;FAŁSZ)    cena produktu z tabeli K:L
=ZAOKR(E2;2)                            zaokrąglenie do 2 miejsc
Polski ExcelAngielskiCo robi
JEŻELI, ORAZ, LUBIF, AND, ORwarunki
LICZ.JEŻELI, LICZ.WARUNKICOUNTIF, COUNTIFSliczenie z warunkiem
SUMA.JEŻELI, SUMA.WARUNKÓWSUMIF, SUMIFSsumowanie z warunkiem
ŚREDNIA.JEŻELI, MAKS.WARUNKÓWAVERAGEIF, MAXIFSśrednia, maksimum z warunkiem
WYSZUKAJ.PIONOWOVLOOKUPwartość z tabeli według klucza
PODAJ.POZYCJĘ, INDEKSMATCH, INDEXpozycja wartości, wartość z pozycji
LEWY, PRAWY, FRAGMENT.TEKSTU, DŁLEFT, RIGHT, MID, LENtekst
ZAOKRROUNDzaokrąglenie
Średnik, nie przecinek. W polskich wersjach Excela i LibreOffice argumenty oddziela się średnikiem, bo przecinek jest separatorem dziesiętnym: 0,5. W LibreOffice nazwy funkcji są w większości takie same jak w Excelu.

Formułę =A1*$B$1 z komórki C1 skopiowano do D3. Jak wygląda formuła w D3?

W A1:A5 są liczby 1, 4, 3, 7, 5. Co zwróci =LICZ.JEŻELI(A1:A5;">3")?

Co zwróci =FRAGMENT.TEKSTU("informatyka";3;5)?

Co zwróci =JEŻELI(ORAZ(5>3;2>4);"tak";"nie")?

  1. tak
  2. nie
  3. błąd
Lekcja 28

Arkusz: daty, zestawienia i wykresy

Większość zadań z arkusza to zestawienia: suma, średnia albo maksimum dla każdego miesiąca, klienta czy rodzaju. Najszybciej robi się je tabelą przestawną, a do niej często trzeba dorobić wykres.

Daty

Arkusz przechowuje datę jako liczbę dni, a jej wygląd to tylko format komórki. Dlatego daty można odejmować i porównywać. Gdy zamiast daty widzisz liczbę, np. 45658 (to 1 stycznia 2025), zmień format komórki na datę.

arkusz
=ROK(A2)   =MIESIĄC(A2)   =DZIEŃ(A2)
=DZIEŃ.TYG(A2;2)             1 = poniedziałek, …, 7 = niedziela
=TEKST(A2;"mmmm")            nazwa miesiąca
=A3-A2                       liczba dni między datami
=DATA(2025;3;1)              data z roku, miesiąca i dnia
=DATA(PRAWY(A2;4);FRAGMENT.TEKSTU(A2;4;2);LEWY(A2;2))   tekst "dd.mm.rrrr" na datę

Tabela przestawna

Najczęstsza wartość w każdej grupie, np. najlepiej sprzedająca się odmiana w każdym miesiącu: w tabeli przestawnej ustaw miesiące w wierszach, odmiany w kolumnach i sumę w wartościach. Potem dla każdego wiersza znajdź maksimum i nazwę jego kolumny:

arkusz
=INDEKS($B$4:$K$4;PODAJ.POZYCJĘ(MAX(B5:K5);B5:K5;0))

Wykres

Zaznacz zestawienie razem z nagłówkami i wybierz Wstawianie → Wykres kolumnowy (albo typ podany w treści). Punkty za wykres zależą od opisu, więc odhacz każdą rzecz:

Na maturze: zestawienia miesięczne z wykresem kolumnowym były w 2023 r. (zad. 6), 2024 r. (zad. 7) i 2026 r. (zad. 7). W 2024 r. trzeba było podać najpopularniejszą odmianę jabłek w każdym miesiącu.

Ile dni minęło od 1 marca 2025 do 15 kwietnia 2025 (wynik =A3-A2)?

Co zwróci =DZIEŃ.TYG(A2;2), gdy w A2 jest 4 maja 2026 (poniedziałek)?

W kolumnie z datami widać liczby typu 45658. Co zrobić?

  1. Zaimportować plik jeszcze raz
  2. Zmienić format komórek na datę
  3. Podzielić przez 365
Lekcja 29

Symulacja dzień po dniu

Typowe zadanie: stan czegoś (wody, towaru, minerału) zmienia się każdego dnia według reguł z treści. W arkuszu każdy wiersz to jeden dzień, a stan na początku dnia to stan z końca poprzedniego wiersza.

Przykład: zbiornik na deszczówkę ma pojemność zapisaną w komórce M1. Na początku jest w nim 100 l. Każdy milimetr deszczu dolewa 10 l, a nadmiar ponad pojemność się przelewa. Potem, jeśli jest co najmniej 30 l, ogród podlewa się 30 litrami, a jeśli nie, tego dnia brakuje wody.

KolumnaZnaczenieWiersz 2 (pierwszy dzień)Wiersz 3 i dalej
Adataz plikuz pliku
Bopady (mm)z plikuz pliku
Cstan rano100=F2
Dpo deszczu=MIN($M$1;C2+10*B2)kopiuj w dół
Epodlewanie=JEŻELI(D2>=30;30;0)kopiuj w dół
Fstan wieczorem=D2-E2kopiuj w dół
Gbrak wody (1/0)=JEŻELI(D2<30;1;0)kopiuj w dół
Hpełny (1/0)=JEŻELI(D2=$M$1;1;0)kopiuj w dół
arkusz
=SUMA(G2:G366)                         ile dni zabrakło wody
=PODAJ.POZYCJĘ(1;H2:H366;0)            numer dnia, w którym zbiornik pierwszy raz był pełny
=INDEKS(A2:A366;PODAJ.POZYCJĘ(1;H2:H366;0))   data tego dnia

Pytanie typu „jaka najmniejsza pojemność wystarczy, żeby ani razu nie zabrakło wody” rozwiązujesz, zmieniając liczbę w M1 i patrząc na sumę braków. Gdy wartości jest dużo, szybciej jest napisać program:

python
def symuluj(opady, pojemnosc, start=100):   # opady w mm, dzień po dniu
    stan = start                             # litry w zbiorniku
    braki = 0
    pierwszy_pelny = None
    for dzien, mm in enumerate(opady, start=1):
        stan = min(pojemnosc, stan + 10 * mm)    # deszcz; nadmiar się przelewa
        if stan == pojemnosc and pierwszy_pelny is None:
            pierwszy_pelny = dzien
        if stan >= 30:
            stan -= 30                           # podlewanie
        else:
            braki += 1                           # za mało wody tego dnia
    return braki, pierwszy_pelny

def najmniejsza_pojemnosc(opady):            # pierwsza pojemność bez braków wody
    for pojemnosc in range(100, 10001, 10):
        if symuluj(opady, pojemnosc)[0] == 0:
            return pojemnosc
    return None
c++
#include <vector>
#include <algorithm>
using namespace std;

// opady w mm, dzień po dniu; ustawia pierwszy_pelny (0, gdy zbiornik nigdy nie był pełny)
int symuluj(const vector<int>& opady, int pojemnosc, int& pierwszy_pelny, int start = 100) {
    int stan = start, braki = 0;              // litry w zbiorniku
    pierwszy_pelny = 0;
    for (int d = 0; d < (int)opady.size(); d++) {
        stan = min(pojemnosc, stan + 10 * opady[d]);   // deszcz; nadmiar się przelewa
        if (stan == pojemnosc && pierwszy_pelny == 0)
            pierwszy_pelny = d + 1;
        if (stan >= 30) stan -= 30;           // podlewanie
        else braki++;                         // za mało wody tego dnia
    }
    return braki;
}

int najmniejsza_pojemnosc(const vector<int>& opady) {   // pierwsza bez braków wody
    int p;
    for (int pojemnosc = 100; pojemnosc <= 10000; pojemnosc += 10)
        if (symuluj(opady, pojemnosc, p) == 0) return pojemnosc;
    return -1;
}
pseudokod
funkcja symuluj(opady[1..n], pojemnosc)
    stan ← 100;  braki ← 0;  pierwszy_pelny ← brak
    dla d = 1, 2, …, n wykonuj
        stan ← min(pojemnosc, stan + 10 · opady[d])   // nadmiar się przelewa
        jeżeli stan = pojemnosc oraz pierwszy_pelny = brak
            pierwszy_pelny ← d
        jeżeli stan ≥ 30
            stan ← stan − 30                          // podlewanie
        w przeciwnym razie
            braki ← braki + 1
    zwróć braki, pierwszy_pelny

funkcja najmniejsza_pojemnosc(opady)
    dla pojemnosc = 100, 110, 120, …, 10000 wykonuj
        jeżeli symuluj(opady, pojemnosc) daje braki = 0
            zwróć pojemnosc
    zwróć brak

Suma narastająco

Gdy reguła zależy od tego, ile było dotąd (np. rabat od łącznych zakupów klienta), potrzebna jest suma od początku do bieżącego wiersza. Pierwszy adres jest zablokowany, drugi przesuwa się przy kopiowaniu:

arkusz
=SUMA($C$2:C2)                         narastająco dla wszystkich wierszy
=SUMA.JEŻELI($B$2:B2;B2;$C$2:C2)       narastająco tylko dla klienta z B2
Na maturze: symulacja dzień po dniu była w 2023 r. (zad. 6: produkcja konfitur), 2025 r. (zad. 6: wydobycie i wysyłka martianeum po przekroczeniu progu) i 2026 r. (zad. 7: rzęsa w stawie, z pytaniem o najmniejszą liczbę ryb). W 2024 r. (zad. 7) rabat zależał od sumy narastającej zakupów klienta.

Pojemność 150 l, opady przez 5 dni: 0, 0, 0, 4, 0 mm. Ile litrów jest w zbiorniku wieczorem 3. dnia?

Dla tych samych danych: ile dni zabrakło wody?

Opady przez 7 dni: 20, 0, 0, 0, 0, 0, 0 mm. Jaka jest najmniejsza pojemność (sprawdzana co 10 l od 100 l), przy której ani razu nie zabraknie wody?

Lekcja 30

SQL: zapytania do jednej tabeli

Zadanie z bazą danych daje kilka plików z tabelami. Importujesz je do Accessa, LibreOffice Base albo MySQL i odpowiadasz na pytania zapytaniami. Jedno podzadanie zwykle wymaga napisania zapytania SQL na papierze.

W tej i następnej lekcji używamy małej bazy szkolnej biblioteki:

uczniowie

id_uczniaimienazwiskoklasa
1OlaNowak3A
2JanKowalski3A
3EwaWiśniewska3B
4PiotrZając3B
5KasiaLis2A

ksiazki

id_ksiazkitytulautorgatunekrok_wydania
1WiedźminSapkowskifantastyka1993
2SolarisLemfantastyka1961
3LalkaPruspowieść1890
4Quo vadisSienkiewiczpowieść1896
5HobbitTolkienfantastyka1937
6Pan TadeuszMickiewiczpoezja1834

wypozyczenia

idid_uczniaid_ksiazkidata_wypdata_zwrotu
1112025-09-022025-09-16
2122025-09-202025-10-01
3232025-09-052025-09-30
4312025-10-032025-10-20
5352025-10-21
6152025-10-022025-10-15
7462025-11-042025-11-18
8212025-11-10
9142025-11-122025-11-30
10322025-11-202025-12-01

Wybieranie i warunki

sql
SELECT tytul, rok_wydania
FROM ksiazki
WHERE gatunek = 'fantastyka'
ORDER BY rok_wydania;
tytulrok_wydania
Hobbit1937
Solaris1961
Wiedźmin1993
sql
SELECT tytul
FROM ksiazki
WHERE autor LIKE 'S%' AND rok_wydania BETWEEN 1850 AND 2000;
tytul
Wiedźmin
Quo vadis

LIKE 'S%' to tekst zaczynający się od S (% to dowolny ciąg znaków, _ to jeden znak). BETWEEN obejmuje oba końce. Brak wartości sprawdza się przez IS NULL, nigdy przez = NULL.

sql
SELECT COUNT(*)
FROM wypozyczenia
WHERE data_zwrotu IS NULL;
COUNT(*)
2

Grupowanie

GROUP BY zbiera wiersze o tej samej wartości w jedną grupę. Funkcje COUNT, SUM, AVG, MIN i MAX liczą wtedy wynik dla każdej grupy osobno. WHERE filtruje wiersze przed grupowaniem, a HAVING filtruje gotowe grupy.

sql
SELECT gatunek, COUNT(*) AS ile
FROM ksiazki
GROUP BY gatunek
ORDER BY ile DESC;
gatunekile
fantastyka3
powieść2
poezja1
sql
SELECT gatunek, MIN(rok_wydania) AS najstarsza
FROM ksiazki
GROUP BY gatunek
HAVING COUNT(*) >= 2;
gatuneknajstarsza
fantastyka1937
powieść1890
sql
SELECT id_ksiazki, COUNT(*) AS ile
FROM wypozyczenia
GROUP BY id_ksiazki
ORDER BY ile DESC
LIMIT 1;
id_ksiazkiile
13
Access ma kilka różnic. Zamiast LIMIT 1 na końcu pisze się SELECT TOP 1 na początku. W LIKE zamiast % i _ są * i ?. Daty zapisuje się jako #2025-09-01#. Na papierze wystarczy standardowy SQL.

Ile wierszy zwróci SELECT * FROM ksiazki WHERE rok_wydania < 1900;?

Jaki wynik da SELECT AVG(rok_wydania) FROM ksiazki WHERE gatunek = 'powieść';?

Chcesz wypisać tylko te gatunki, które mają co najmniej 2 książki. Gdzie wpisać warunek COUNT(*) >= 2?

  1. w WHERE
  2. w HAVING
  3. w ORDER BY
Lekcja 31

SQL: łączenie tabel, import i podzapytania

Dane z jednego pytania prawie zawsze leżą w kilku tabelach. Łączy się je przez klucze: klucz główny jednoznacznie wskazuje wiersz, a klucz obcy w innej tabeli wskazuje na ten wiersz.

W naszej bazie wypozyczenia.id_ucznia to klucz obcy wskazujący uczniowie.id_ucznia. Jeden uczeń ma wiele wypożyczeń, więc to relacja jeden do wielu.

Import plików

JOIN

sql
SELECT u.imie, u.nazwisko, COUNT(*) AS ile
FROM uczniowie AS u
JOIN wypozyczenia AS w ON w.id_ucznia = u.id_ucznia
GROUP BY u.id_ucznia, u.imie, u.nazwisko
ORDER BY ile DESC;
imienazwiskoile
OlaNowak4
EwaWiśniewska3
JanKowalski2
PiotrZając1

W GROUP BY podajemy identyfikator, a nie tylko imię i nazwisko, bo dwie osoby mogą się tak samo nazywać. Kolejne tabele dołącza się kolejnymi JOIN:

sql
SELECT DISTINCT k.tytul
FROM ksiazki AS k
JOIN wypozyczenia AS w ON w.id_ksiazki = k.id_ksiazki
JOIN uczniowie AS u ON u.id_ucznia = w.id_ucznia
WHERE u.klasa = '3B'
ORDER BY k.tytul;
tytul
Hobbit
Pan Tadeusz
Solaris
Wiedźmin

Kto nie ma żadnego wiersza

LEFT JOIN zostawia wszystkie wiersze z lewej tabeli. Gdy nie ma pary, kolumny z prawej mają NULL. To sposób na pytania „którzy klienci nic nie kupili”:

sql
SELECT u.imie, u.nazwisko
FROM uczniowie AS u
LEFT JOIN wypozyczenia AS w ON w.id_ucznia = u.id_ucznia
WHERE w.id IS NULL;
imienazwisko
KasiaLis

To samo można zapisać podzapytaniem z NOT IN:

sql
SELECT tytul
FROM ksiazki
WHERE id_ksiazki NOT IN (SELECT id_ksiazki
                         FROM wypozyczenia
                         WHERE data_zwrotu IS NULL)
ORDER BY tytul;
tytul
Lalka
Pan Tadeusz
Quo vadis
Solaris

Daty i liczenie różnych wartości

sql
SELECT MONTH(data_wyp) AS miesiac, COUNT(*) AS ile
FROM wypozyczenia
GROUP BY MONTH(data_wyp)
ORDER BY miesiac;
miesiacile
93
103
114

MONTH i YEAR działają w MySQL. W Accessie piszemy Month(data_wyp), a w LibreOffice Base MONTH("data_wyp"). Liczba różnych wartości to COUNT(DISTINCT …):

sql
SELECT COUNT(DISTINCT w.id_ucznia)
FROM wypozyczenia AS w
JOIN ksiazki AS k ON k.id_ksiazki = w.id_ksiazki
WHERE k.gatunek = 'fantastyka';
COUNT(DISTINCT w.id_ucznia)
3

Access nie ma COUNT(DISTINCT). Robi się to podzapytaniem, które najpierw wybiera różne wartości, a potem je zlicza:

sql
SELECT COUNT(*)
FROM (SELECT DISTINCT w.id_ucznia
      FROM wypozyczenia AS w
      JOIN ksiazki AS k ON k.id_ksiazki = w.id_ksiazki
      WHERE k.gatunek = 'fantastyka') AS t;

Nowa tabela na papierze

Ostatnie podzadanie zwykle dokłada do bazy nową tabelę i prosi o zapytanie, które z niej korzysta. Czasem trzeba też podać jej definicję:

sql
CREATE TABLE oceny (
    id_oceny INTEGER PRIMARY KEY,
    id_ucznia INTEGER REFERENCES uczniowie(id_ucznia),
    id_ksiazki INTEGER REFERENCES ksiazki(id_ksiazki),
    ocena INTEGER
);
Na maturze: zadanie z bazą danych było w każdym arkuszu od 2023 r.: gry planszowe (2023, zad. 7), rejestr wykroczeń (2024, zad. 8), poszukiwanie wody na Marsie (2025, zad. 7) i sieć sklepów (2026, zad. 8). Zawsze było łączenie tabel, liczenie w grupach i pytanie o rekordy bez pary, np. kierowców bez wykroczeń albo klientów bez zakupów.

Ile wierszy zwróci SELECT * FROM uczniowie AS u JOIN wypozyczenia AS w ON w.id_ucznia = u.id_ucznia;?

A ile wierszy, jeśli zamiast JOIN użyjesz LEFT JOIN?

Ile łącznie wypożyczeń mają uczniowie klasy 3A?

Arkusze CKE

Co było na maturze i gdzie to jest w kursie

Przegląd zadań z algorytmów, programowania, arkusza kalkulacyjnego i baz danych z arkuszy rozszerzonych z lat 2015–2026. Testy prawda/fałsz z wiedzy ogólnej (sieci, grafika, systemy) nie są tu opisane.

RokZadanieCo trzeba było umiećLekcja
20151. Problem telewidzazachłanny wybór nienachodzących przedziałówStrategie
20153. Rozszerzony algorytm EuklidesaNWD jako a·x + b·yNWD i NWW
20161. Liczby skojarzonesuma dzielników do √nCyfry i dzielniki
20162. Przestawienia w tablicypodział tablicy względem kluczaSortowanie szybkie
20171. Prostokątnajwiększy iloczyn z warunkiem podzielnościWyszukiwanie liniowe
20172. Rekurencjafunkcja rekurencyjna po bitach liczbyZapis binarny
20181. Analiza algorytmuwyszukiwanie binarne pierwiastkaWyszukiwanie binarne
20182. Krajobrazporównywanie ułamków, porządekGeometria
20191. Ulubione liczbybinarne szukanie granicy, złożonośćWyszukiwanie binarne
20192. Analiza algorytmudrzewo wywołań, generowanie napisówGenerowanie ciągów
20201. Podobne tabliceprzesunięcie cykliczne tablicyWyszukiwanie liniowe
20202. Symetryczny ciągśledzenie rekurencji, długość wynikuRekurencja
20211. Cyfrowe dopełnienieprzekształcanie cyfr liczbyBudowanie liczb z cyfr
20212. Analiza algorytmuwstawianie do kopcaDrzewa
20221. n-permutacjazliczanie wystąpień w tablicy licznikówZliczanie
20222. ab-słowoliczniki z przodu i z tyłu (prefiksy)Sumy prefiksowe
20231. Biblioteczka Adamadrzewo BST w tablicy, przejście inorderDrzewa
20232. Liczby binarnebloki bitów, XOR, porównywanie binarnychZapis binarny
20233. Liczba Pizliczanie fragmentów, najdłuższy fragment o zadanej własnościSumy prefiksowe, Wyszukiwanie liniowe
20236. Konfitury owocowezestawienie miesięczne, wykres, produkcja dzień po dniuArkusz: daty i wykresy, Arkusz: symulacja
20237. Gry planszoweliczenie i średnie w grupach, łączenie tabel, kategorie wiekuSQL: jedna tabela, SQL: łączenie tabel
20241. Planszaosiągalność na planszy (DP)Plansze 2D
20242. Cyfry; 3. Nieparzysty skrótbudowanie liczby z cyfr, NWDBudowanie liczb z cyfr
20244. Liczbypodzielność, rozkład na czynniki, fragment o największej średniejLiczby pierwsze, Sumy prefiksowe
20247. Hurtowniasumy dla klientów, najpopularniejsza odmiana w miesiącu, rabat od sumy narastającejArkusz: formuły, Arkusz: daty i wykresy, Arkusz: symulacja
20248. Rejestr wykroczeńsumy z łączeniem tabel, miesiące, kierowcy bez wykroczeńSQL: łączenie tabel
20251. Funkcja rekurencyjnarekurencja po parach cyfr, wersja iteracyjnaBudowanie liczb z cyfr
20252. Zapis symbolicznypalindromy, kwadraty 3×3, system trójkowyNapisy, Plansze 2D, Systemy
20253. DronNWD, kolejne pozycje, punkty w kwadracieGeometria
20256. Martianeumsymulacja dzień po dniu z progamiArkusz: symulacja
20257. Woda na Marsiełączenie tabel, daty, obszary bez pomiarówSQL: łączenie tabel
20261. Rekurencjaliczba wywołań, rekurencja jak w szybkim potęgowaniuSzybkie potęgowanie
20262. Dodawanieprzeniesienia przy dodawaniu pisemnymBudowanie liczb z cyfr
20263. Pary słówkody ASCII, wspólne litery, początek i koniec słowaNapisy
20264. Korporacjahierarchia z tablicą przełożonychDrzewa
20267. Stawśrednie miesięczne, wykres, symulacja wzrostu, najmniejszy parametrArkusz: daty i wykresy, Arkusz: symulacja
20268. Sieć sklepówliczba transakcji, klienci bez zakupów, różne sklepy w miesiącuSQL: łączenie tabel

Od 2023 r. w każdym arkuszu jest zadanie z arkusza kalkulacyjnego (zestawienia, wykresy, symulacja dzień po dniu) i zadanie z bazy danych (zapytania SQL, jedno pisane na papierze).

Arkusze z kluczem odpowiedzi i plikami z danymi znajdziesz na stronie CKE i na arkusze.pl.

Na koniec

Ściąga i plan na egzamin

Zestawienie wszystkich algorytmów z kursu i lista rzeczy, które warto mieć opanowane przed egzaminem.

AlgorytmZłożonośćKluczowy pomysł
suma cyfr, odwracanieO(liczba cyfr)% 10 i // 10
dzielniki, test pierwszościO(√n)pary dzielników, d·d ≤ n
sito EratostenesaO(n log log n)wykreślanie od i·i
NWD (Euklides)O(log n)(a, b) → (b, a % b)
zamiana systemówO(liczba cyfr)dzielenie przez p / schemat Hornera
szybkie potęgowanieO(log n)aⁿ = (aⁿᐟ²)²
Fibonacci iteracyjnieO(n)dwie ostatnie wartości
min i max naraz≈ 3n/2 porównańporównania parami
wyszukiwanie binarneO(log n)połowienie, dane posortowane
bąbelkowe, wybór, wstawianieO(n²)zamiany sąsiadów / minimum / wstawianie
scalanieO(n log n)podziel, posortuj, scal
quicksortśrednio O(n log n), najgorzej O(n²)podział względem pivota
zliczanieO(n + k)tablica liczników
wzorzec naiwnieO(n·m)porównanie od każdej pozycji
bisekcjaO(log((b − a)/ε))zmiana znaku
wydawanie reszty (DP)O(kwota · liczba nominałów)dp[k] = min(dp[k − m] + 1)
budowanie liczby z cyfrO(liczba cyfr)mnożnik 1, 10, 100… rośnie tylko przy dopisaniu
rozszerzony EuklidesO(log n)x = y′, y = x′ − (a div b)·y′
bloki, jedynki w zapisie binarnymO(log n)n % 2, n // 2, &, ^, >>
wszystkie napisy / podzbiory / permutacjekⁿ / 2ⁿ / n!rekurencja, maska bitowa, zamiany
sumy prefiksoweO(n) budowa, O(1) zapytaniesuma t[a..b] = P[b+1] − P[a]
największa suma fragmentu (Kadane)O(n)przedłuż albo zacznij od nowa
szukanie po wyniku (pierwiastek)O(log n)warunek zmienia się raz
plansza: osiągalność / BFSO(n·m)P[i][j] z góry lub z lewej / kolejka
BST i kopiec w tablicyO(wysokość)dzieci 2i i 2i+1, rodzic i div 2
hierarchia z tablicą przełożonychO(n)rosnąco głębokość, malejąco podwładni
iloczyn wektorowyO(1)strona prostej, współliniowość, pole
wybór przedziałówO(n log n)sortuj po końcu

Dzień egzaminu

Najlepszy trening to arkusze z poprzednich lat ze strony CKE. Rozwiązuj je przy komputerze i z zegarem.