Dołącz do konkursu i wysyłaj zadania: https://szkopul.edu.pl/c/oki-olimpiada-od-podstaw
Sumy prefiksowe służą do szybkiego liczenia sumy dowolnego przedziału tablicy.
Aby policzyć sumę na przedziale $[l, r]$, możemy policzyć sumę na przedziale $[1,r]$ i odjąć od niej sumę na przedziale $[1,l-1]$. Wszystkie sumy postaci $[1,i]$ możemy policzyć na początku programu, przez co zapytanie będzie działać w czasie $\mathcal{O}(1)$.
Tablica
Tekstowe streszczenie sum prefiksowych
pref[i] = a[1]+...+a[i] liczona raz w O(n) pozwala odpowiadać na pytanie o sumę dowolnego przedziału [l,r] jedną operacją: pref[r] - pref[l-1]. Zamiast miliona operacji na zapytanie, jedna.
Zadania na rozgrzewkę. Pozwolą zapoznać Ci się konceptem i przygotują cię do kolejnych zadań.
poziom OIJ 1
poziom OIJ 1
poziom OIJ 1
Dodatkowe: Rewolucja Ciapongowa 1
Wyślij
Zadania wymagające, czasami wręcz ostentacyjnie nietrywialnej, ilości pomyślunku, której skala pozostaje niewspółmierna do liczby linijek kodu prowadzących do rozwiązania.
poziom OIJ 1
poziom OIJ 1
Zadania z materiału do tego tematu włącznie. Niekoniecznie muszą zawierać materiały z tego tematu, ale zalecamy zrobienie wszystkich dla powtórzenia i treningu!
poziom OIJ 1
Binary search (wyszukiwanie binarne) służy do szybkiego znajdowania elementu lub odpowiedzi w posortowanej tablicy.
W każdym kroku sprawdzamy środkowy element przedziału. Jeśli szukana wartość jest mniejsza, przechodzimy do lewej połowy, a jeśli większa - do prawej. Dzięki temu w każdej iteracji odrzucamy połowę możliwych odpowiedzi, więc algorytm działa w czasie $\mathcal{O}(log n)$.
Porównanie wyszukiwania binarnego z liniowym
Sprawdź w środku, odrzuć połowę!
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
int N, Q;
cin >> N >> Q;
int a[N];
for (int i = 0; i < N; i++) cin >> a[i];
while (Q--) {
int K;
cin >> K;
if (a[N - 1] < K) {
cout << "-1";
continue;
}
int l = 0;
int r = N - 1;
while (l < r) {
int s = (l + r) / 2;
if (a[s] < K) l = s + 1;
else r = s;
}
cout << a[l] << "";
}
}
#include <iostream>
#include <vector>
using namespace std;
int main() {
// Posortowana tablica; pamiętaj żeby po wczytaniu danych zrobić sort(a.begin(), a.end()); lub inaczej posortować
vector<int> a = {2, 5, 7, 10, 14, 18, 21, 30};
int x = 14; // Szukana wartość
int l = 0;
int r = a.size() - 1;
while (l <= r) {
// Obliczamy środek przedziału
int mid = l + (r - l) / 2;
if (a[mid] == x) {
cout << "Znalazlem x na pozycji: " << mid;
return 0;
}
// Szukana wartość jest po prawej stronie
if (a[mid] < x) {
l = mid + 1;
}
// Szukana wartość jest po lewej stronie
else {
r = mid - 1;
}
}
// Jeśli pętla się zakończyła, element nie istnieje
cout << "Nie znalazlem x :<";
return 0;
}
Zadania na rozgrzewkę. Pozwolą zapoznać Ci się konceptem i przygotują cię do kolejnych zadań.
poziom OIJ 1
Zadania z materiału do tego tematu włącznie. Niekoniecznie muszą zawierać materiały z tego tematu, ale zalecamy zrobienie wszystkich dla powtórzenia i treningu!
poziom OIJ 3
Gąsienica (lub po angielsku two pointers) służy do przeglądania przedziałów tablicy w czasie liniowym.
Utrzymujemy dwa wskaźniki oznaczające początek i koniec aktualnego przedziału. Rozszerzamy przedział, gdy spełnia warunek, a w razie potrzeby przesuwamy jego początek. Dzięki temu każdy element jest odwiedzany najwyżej kilka razy, więc algorytm działa w czasie $\mathcal{O}(n)$.
Two pointers na usaco.guide
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
int n;
long long S;
cin >> n >> S;
vector<long long> a(n);
for (int i = 0; i < n; ++i) cin >> x;
long long suma = 0;
int l = 0, best = 0;
for (int r = 0; r < n; ++r) {
suma += a[r]; // <------------------- rozszerzamy okno w prawo
while (suma > S) { // <-------------- dociagamy lewy koniec, az warunek wroci
suma -= a[l];
++l;
}
best = max(best, r - l + 1); // <---- teraz [l, r] jest poprawne
}
cout << best << "
";
return 0;
}
Zadania na rozgrzewkę. Pozwolą zapoznać Ci się konceptem i przygotują cię do kolejnych zadań.
poziom OIJ 1
poziom OIJ 1
Zadania wymagające, czasami wręcz ostentacyjnie nietrywialnej, ilości pomyślunku, której skala pozostaje niewspółmierna do liczby linijek kodu prowadzących do rozwiązania.
Zadania z materiału do tego tematu włącznie. Niekoniecznie muszą zawierać materiały z tego tematu, ale zalecamy zrobienie wszystkich dla powtórzenia i treningu!
Sito Eratostenesa służy do szybkiego znalezienia wszystkich liczb pierwszych z przedziału $[2, n]$.
sido era-to-ste-nesa
Zamiast sprawdzać każdą liczbę osobno,
idziemy od $2$ w górę i wykreślamy wielokrotności
każdej napotkanej liczby pierwszej.
To, co nie zostało wykreślone, jest pierwsze (bo nie jest wielokrotnością).
Wykreślanie dla liczby $i$ możemy zacząć od
$i \cdot i$, bo mniejsze wielokrotności wykreśliły
już mniejsze liczby pierwsze.
Całość działa w czasie $\mathcal{O}(n \log \log n)$, czyli niemal liniowo.
#include <bits/stdc++.h>
using namespace std;
int main(){
int n = 1000000;
vector<bool> pierwsza(n + 1, true); // false = przekreśliliśmy, nie pierwsza, true = jest ok
pierwsza[0] = pierwsza[1] = false;
for(long long i = 2; i * i <= n; i++){
if(pierwsza[i]){
// wykreślamy od i*i — mniejsze wielokrotności już wykreślone
for(long long j = i * i; j <= n; j += i)
pierwsza[j] = false;
}
}
// teraz pierwsza[x] mówi w O(1), czy x jest pierwsze
cout << (pierwsza[997] ? "997 jest pierwsze" : "997 jest złożone");
return 0;
}
Zadania na rozgrzewkę. Pozwolą zapoznać Ci się konceptem i przygotują cię do kolejnych zadań.
Programowanie 3
Zadania z materiału do tego tematu włącznie. Niekoniecznie muszą zawierać materiały z tego tematu, ale zalecamy zrobienie wszystkich dla powtórzenia i treningu!
poziom OIJ 1
W wielu zadaniach możemy przyjąć strategię "divide and conquer", czyli podzielić problem na mniejsze problemy, które łatwiej rozwiązać.Zamiast sprawdzać wykładniczo wiele kombinacji, wypełniamy tablicę $dp$, w której $dp[i]$ oznacza najlepszą odpowiedź dla "mniejszej wersji" zadania. Każdy stan liczymy raz, więc typowa złożoność to $\mathcal{O}(n)$ lub $\mathcal{O}(n \cdot w)$. W odróżnieniu od zachłana, dynamik rozważa wszystkie możliwości w uporządkowany sposób.
Programowanie dynamiczne (dynamiki) polega na rozbiciu problemu na stany i liczeniu wyniku dla każdego stanu z wyników wcześniejszych, mniejszych stanów.
Binary Search po wyniku stosujemy, gdy nie umiemy policzyć odpowiedzi wprost, ale umiemy szybko odpowiedzieć na pytanie: „czy da się osiągnąć wynik $x$?”.
Jeśli odpowiedzi układają się monotonicznie (TAK, TAK, ..., TAK, NIE, ..., NIE),
to szukamy binary searchem ostatniego TAK, czyli najlepszego osiągalnego wyniku.
Zamiast sprawdzać wszystkie wartości, wykonujemy $\mathcal{O}(\log(\text{zakres}))$ sprawdzeń.
To ulepszenie bruta: z milionów operacji robi się kilkadziesiąt.
Zadania na rozgrzewkę. Pozwolą zapoznać Ci się konceptem i przygotują cię do kolejnych zadań.
poziom OIJ 1
Zadania z materiału do tego tematu włącznie. Niekoniecznie muszą zawierać materiały z tego tematu, ale zalecamy zrobienie wszystkich dla powtórzenia i treningu!
poziom OIJ 3