Lab contests and the defence
What to say
- There are 12 lab contests, one a week from week 3. Each is worth its own number of points, listed in the syllabus.
- A contest is submitted on ejudge, but it counts only after the defence: two weeks after the lab opens, a separate contest with 2 problems from the same list, solved on the spot.
- Say plainly why the defence multiplies instead of adding: an accepted submission you cannot reproduce is worth nothing. Solving 5 problems honestly beats copying 10.
- The two defence problems come from the whole list, not from what someone happened to solve — so leaving half the lab untouched is a gamble on both counts.
- Let them play with the calculator for a minute — it answers "what do I get if…" faster than you can.
Lab contests
There are 12 lab contests across the semester, one per week starting from week 3, plus a TSIS assignment each week. Every contest carries its own point value — the syllabus lists them next to the week.
| Contest | Week | Points |
|---|---|---|
| LabContest 1 | 3 | 3 |
| LabContest 2 | 4 | 3 |
| LabContest 3 | 5 | 3 |
| LabContest 4 | 6 | 3 |
| LabContest 5 | 7 | 3 |
| LabContest 6 | 8 | 2 |
| LabContest 7 … 12 — see the syllabus | ||
The rule
LabContest 1 contains 10 problems, A to J, and is worth 3 points. Solve as many as you can on ejudge.
Two weeks after the lab opens a separate defence contest runs. It holds 2 problems taken from this same list of 10 — the same two for everyone — and you solve them there, on the spot, from scratch.
Your result on the defence contest, as a percentage, is multiplied by the percentage of the lab you solved.
Formula
| Symbol | Meaning | LabContest 1 |
|---|---|---|
| L | points this lab contest is worth (from the syllabus) | 3 |
| Rlab | share of the lab you solved — solved ÷ total | S / 10 |
| Rdef | your result on the 2-problem defence contest, as a share | 0 … 100 % |
Rounded to two decimals. The same formula applies to every lab contest — only L and the number of problems change.
3 × 1.00 × 0 = 0. And it cuts the other way too: acing the defence without having solved the lab gives 3 × 0 × 1.00 = 0. Both halves are required.Demo · what will I get?
Full marks — the whole lab solved and the defence contest passed in full.
| Lab | Defence | Points | What it means |
|---|---|---|---|
| 10 / 10 | 100 % | 3.00 | everything solved, defence clean |
| 10 / 10 | 70 % | 2.10 | whole lab solved, defence partly |
| 8 / 10 | 70 % | 1.68 | 80 % of the lab × 70 % of the defence |
| 10 / 10 | 50 % | 1.50 | one defence problem out of two |
| 5 / 10 | 100 % | 1.50 | half the lab, defence clean |
| 5 / 10 | 70 % | 1.05 | |
| 10 / 10 | 0 % | 0.00 | defence failed — the lab does not count |
| 0 / 10 | 100 % | 0.00 | lab not done — the defence does not count |
Course Overview
What the course consists of
- Syllabus, covered topics, laboratory works
- Midterm, endterm, final examinations
- Ejudge.kz system, GitHub, defence rules
- Labs — practice lessons to help solving labs
Course Topics by Lecture
What to say
- Walk the 13 lectures: the course builds from simple structures up to graphs, and what we do today comes back at the end.
Thirteen lectures
| Lecture | Title | Topics |
|---|---|---|
| 1 | Complexity and Memory | GCD, Binary Exponentiation, Sieve of Eratosthenes, Prime factorization, Stack, Queue, Deque |
| 2 | Data Structures | Stack, Queue, Deque, Linked lists, Doubly-Linked lists |
| 3 | Search Algorithms | Binary search |
| 4 | Tree Structures | Binary search tree |
| 5 | Priority Structures | Priority queues, Heap |
| 6 | Sorting Algorithms I | Heap sort, Quick Sort |
| 7 | Sorting Algorithms II | Merge sort, Midterm |
| 8 | Hash Tables | Hash tables, Rabin-Karp algorithm based on hash calculation |
| 9 | String Algorithms | Knuth-Morris-Pratt algorithm |
| 10 | Graph Representation and BFS | Adjacency list and matrix, Edge list, BFS |
| 11 | Graph Traversal and Sorting | DFS, Topological Sort |
| 12 | Spanning Tree Algorithms | Kruskal algorithm, Prim algorithm |
| 13 | Shortest Path Algorithms | Dijkstra, Floyd-Warshall, Bellman-Ford |
Greatest Common Divisor (GCD)
What to say
- Start with the simple solution and show why we need something better: check every number from min(a, b) down to 1.
- For gcd(12, 18) it is fine — it stops at 6 after 7 checks. For gcd(1000000007, 1000000009) it needs about a billion checks. Run the demo and read the number off the screen.
- Then the Euclidean algorithm: gcd(a, b) = gcd(b, a mod b), and gcd(a, 0) = a. Do a few steps by hand on the board.
- Three implementations: the naive loop, the recursive Euclid, and an iterative one that keeps taking the remainder of the larger by the smaller.
The problem and the naive approach
Problem: find the largest positive integer that divides both numbers a and b.
Naive approach
- Check all numbers from min(a, b) down to 1
- Time complexity: O(min(a, b))
- Inefficient for large numbers
Example
- For small numbers like GCD(12, 18) it works fine: at most 12 checks — it actually stops at 6 after 7
- For large primes like GCD(1000000007, 1000000009) it needs about 1 billion checks
- With large numbers this approach takes an extremely long time
The Euclidean algorithm
GCD(a, 0) = a
On the board:
gcd(12, 18) = gcd(18, 12) = gcd(12, 6) = gcd(6, 0) = 6 gcd(48, 180) = gcd(180, 48) = gcd(48, 36) = gcd(36, 12) = gcd(12, 0) = 12
Implementation
int gcd(int a, int b) { for (int i = min(a, b); i >= 1; --i) { if (a % i == 0 && b % i == 0) { return i; } } return 1; }
int gcd2(int a, int b) { if (b == 0) { return a; } return gcd2(b, a % b); }
// Iterative version int gcd1(int a, int b) { while (a > 0 && b > 0) { if (a > b) a = a % b; else b = b % a; } // if (a > 0) // return b; // return a; return a + b; }
GCD — the full program41 lines
#include <iostream> using namespace std; int gcd(int a, int b) { for (int i = min(a, b); i >= 1; --i) { if (a % i == 0 && b % i == 0) { return i; } } return 1; } int gcd2(int a, int b) { if (b == 0) { return a; } return gcd2(b, a % b); } // Iterative version int gcd1(int a, int b) { while (a > 0 && b > 0) { if (a > b) a = a % b; else b = b % a; } // if (a > 0) // return b; // return a; return a + b; } int main() { int a, b; cin >> a >> b; cout << gcd2(a, b) << endl; return 0; }
12 18
6
Demo · how many steps each one takes
| step | a | b | a mod b |
|---|
Another version · with long long
The same three functions on long long. That is what problem A needs: a and b go up to 10¹⁸, which does not fit into int.
the full program — version 238 lines
#include <iostream> #define ll long long using namespace std; ll gcd1(ll a, ll b) { for (ll i = min(a, b); i >= 1; --i) { if (a % i == 0 && b % i == 0) { return i; } } return 1; } ll gcd2(ll a, ll b) { if (b == 0) { return a; } return gcd2(b, a % b); } ll gcd3(ll a, ll b) { while (a > 0 && b > 0) { if (a > b) { a = a % b; } else { b = b % a; } } return a + b; } int main() { ll a, b; cin >> a >> b; cout << gcd1(a, b) << endl; return 0; }
12 18
6
Binary Exponentiation (Binpow)
What to say
- Naively an costs n multiplications. For n = 10¹⁸ that is more than thirty years.
- If n is even, an = (an/2)²; if n is odd, an = a · an−1. Every two steps halve the exponent — O(log n).
- Do 210 by hand: 4 real multiplications instead of 10.
- Then the version with a modulus — that is problem B — and its two traps.
The problem and the idea
Problem: calculate an efficiently.
Naive approach
- Multiply a by itself n times
- Time complexity: O(n)
- Inefficient for large exponents
Binary exponentiation
- Based on the property an = (an/2)2 if n is even
- If n is odd: an = a × an−1
- Time complexity: O(log n)
Algorithm
- If n = 0, return 1
- If n is even: result = (an/2)2
- If n is odd: result = a × an−1
- Use recursion or iteration
Example · 210
210 = (25)2 = (2 × 24)2 = (2 × (22)2)2 = (2 × 42)2 = (2 × 16)2 = 322 = 1024
What the recursive code below actually multiplies, in order:
2^1 = 2 × 1 = 2 2^2 = 2 × 2 = 4 2^4 = 4 × 4 = 16 2^5 = 2 × 16 = 32 2^10 = 32 × 32 = 1024
4 real multiplications instead of 10 — the first line, 2 × 1, is the base case and costs nothing.
Implementation
// Recursive version long long binpow(long long a, long long n) { if (n == 0) return 1; if (n % 2 == 0) { long long half = binpow(a, n / 2); return half * half; } else { return a * binpow(a, n - 1); } }
// Iterative version long long binpow_iter(long long a, long long n) { long long result = 1; while (n > 0) { if (n % 2 == 1) { result *= a; } a *= a; n /= 2; } return result; }
// Example usage int main() { long long base, exp; cin >> base >> exp; cout << "Result: " << binpow(base, exp) << endl; cout << "Result (iterative): " << binpow_iter(base, exp) << endl; return 0; }
2 10
Result: 1024 Result (iterative): 1024
Demo · step by step
| n | bit | result | a |
|---|
Another version · modulo m, and the naive loop
For problem B the answer is an mod m. The naive loop for comparison:
ll pow_a_b(ll a, ll b) {
if (b == 0)
return 1;
ll result = 1;
for (ll i = 0; i < b; ++i)
result *= a;
return result;
}and binary exponentiation modulo m:
ll bin_pow(ll a, ll b, ll m) {
if (b == 0)
return 1 % m; // fixed: for m = 1 the answer is 0, not 1
if (b % 2 == 0) {
ll half = bin_pow(a, b / 2, m);
return (half * half) % m;
} else {
return (a % m * bin_pow(a, b - 1, m)) % m; // fixed: a up to 10^18 overflows without % m
}
}n = 0, m = 1 the answer is 0, so the base case returns 1 % m, not 1. And a goes up to 10¹⁸: multiplying it by a residue overflows long long, so it is reduced with a % m first.the full program — version 230 lines
#include <iostream> #define ll long long using namespace std; ll bin_pow(ll a, ll b, ll m) { if (b == 0) return 1 % m; // fixed: for m = 1 the answer is 0, not 1 if (b % 2 == 0) { ll half = bin_pow(a, b / 2, m); return (half * half) % m; } else { return (a % m * bin_pow(a, b - 1, m)) % m; // fixed: a up to 10^18 overflows without % m } } ll pow_a_b(ll a, ll b) { if (b == 0) return 1; ll result = 1; for (ll i = 0; i < b; ++i) result *= a; return result; } int main() { ll a, b, m; cin >> a >> b >> m; cout << bin_pow(a, b, m) << endl; return 0; }
2 10 1000000000
1024
Prime Numbers — Sieve of Eratosthenes
What to say
- The task is to find all primes up to n. Testing each number separately is O(n√n); the sieve does it in O(n log log n).
- Start the animation and narrate it: the first number that is not crossed out is prime — cross out all its multiples.
- Stop on why crossing starts at i²: every smaller multiple was already crossed by a smaller prime.
The problem and the naive approach
Problem: find all prime numbers up to n efficiently.
Naive approach
- Check each number for primality individually
- Time complexity: O(n√n)
Example · primes up to 30
Start with [2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30] Cross out multiples of 2: [2,3,5,7,9,11,13,15,17,19,21,23,25,27,29] Cross out multiples of 3: [2,3,5,7,11,13,17,19,23,25,29] Cross out multiples of 5: [2,3,5,7,11,13,17,19,23,29] 6² = 36 > 30 — stop, everything left is prime
Result: [2,3,5,7,11,13,17,19,23,29]
Complexity and the algorithm
- Time complexity: O(n log log n) — much better than the naive approach
- Space complexity: O(n)
- Create a boolean array
is_prime[0..n], initialize all as true - Set
is_prime[0] = is_prime[1] = false - For each number i from 2 to √n: if
is_prime[i]is true, mark all multiples of i, starting from i², as false - The remaining true values are primes
Demo · the sieve running
■ current prime p · ■ being crossed out · ■ prime · ■ composite
Another version · trial division and the sieve side by side
Testing one number by dividing up to √n:
bool is_prime(int n) { if (n <= 1) return false; for (int i = 2; i * i <= n; ++i) { if (n % i == 0) return false; } return true; }
and the sieve — here 0 means "prime", 1 means "crossed out":
vector<int> primes(int n) { vector<int> is_prime(n + 1, 0); for (int i = 2; i * i <= n; i++) { if (is_prime[i] == 0) { for (int j = i * i; j <= n; j += i) { is_prime[j] = 1; } } } return is_prime; }
the full program — version 242 lines
#include <iostream> #include <vector> using namespace std; bool is_prime(int n) { if (n <= 1) return false; for (int i = 2; i * i <= n; ++i) { if (n % i == 0) return false; } return true; } vector<int> primes(int n) { vector<int> is_prime(n + 1, 0); for (int i = 2; i * i <= n; i++) { if (is_prime[i] == 0) { for (int j = i * i; j <= n; j += i) { is_prime[j] = 1; } } } return is_prime; } int main() { int n; cin >> n; // for (int i = 1; i <= n; i++) { // if (is_prime(i)) { // cout << i << " "; // } // } vector<int> p = primes(n); for (int i = 2; i <= n; i++) { if (p[i] == 0) { cout << i << " "; } } cout << endl; return 0; }
30
2 3 5 7 11 13 17 19 23 29
Another version · the sieve returns the list of primes
the full program — version 331 lines
#include <iostream> #include <vector> using namespace std; vector<int> prime_numbers(int n) { int a[n + 1]; for (int i = 0; i <= n; i++) a[i] = 0; for (int i = 2; i * i <= n; i++) { if (a[i] == 0) { for (int j = i * i; j <= n; j+=i) a[j] = 1; } } vector<int> result; for (int i = 2; i <=n; i++) if (a[i] == 0) result.push_back(i); return result; } int main() { int n; cin >> n; vector<int> primes = prime_numbers(n); for (int i = 0; i < primes.size(); i++) cout << primes[i] << " "; return 0; }
30
2 3 5 7 11 13 17 19 23 29
Prime Factorization
What to say
- Prime factorization breaks a number into a product of primes.
- Dividing up to √n is enough. After the loop the leftover may be greater than 1 — and that leftover is a prime factor too. Show it in the demo on
999999999989. - The faster version divides only by primes, precomputed with the sieve.
Examples
60 = 2 × 2 × 3 × 5
60 ÷ 2 = 30 → ÷ 2 = 15 → ÷ 3 = 5 → 5 is prime
210 = 2 × 3 × 5 × 7
210 ÷ 2 = 105 → ÷ 3 = 35 → ÷ 5 = 7 → 7 is prime
Basic implementation
#include <iostream> using namespace std; void primeFactorization(int n) { for (int i = 2; i * i <= n; i++) { while (n % i == 0) { cout << i << " "; n /= i; } } if (n > 1) cout << n; // remaining prime cout << endl; } int main() { int num; cout << "Enter a number: "; cin >> num; cout << "Prime factors: "; primeFactorization(num); return 0; }
60
Enter a number: Prime factors: 2 2 3 5
Worst case: O(√n) — trial division.
Demo · dividing step by step
| divisor | n left | factors so far |
|---|
Faster · sieve + precomputed primes
// Generate primes up to limit using Sieve of Eratosthenes vector<int> sieve(int limit) { vector<bool> isPrime(limit + 1, true); vector<int> primes; isPrime[0] = isPrime[1] = false; for (int i = 2; i * i <= limit; i++) { if (isPrime[i]) { for (int j = i * i; j <= limit; j += i) { isPrime[j] = false; } } } for (int i = 2; i <= limit; i++) { if (isPrime[i]) primes.push_back(i); } return primes; }
// Prime factorization using precomputed primes void primeFactorization(int n, const vector<int>& primes) { int temp = n; for (int p : primes) { if (p * p > n) break; // stop when p > sqrt(n) while (n % p == 0) { cout << p << " "; n /= p; } } if (n > 1) cout << n; // remaining prime factor cout << endl; }
the full program54 lines
#include <iostream> #include <vector> #include <cmath> using namespace std; // Generate primes up to limit using Sieve of Eratosthenes vector<int> sieve(int limit) { vector<bool> isPrime(limit + 1, true); vector<int> primes; isPrime[0] = isPrime[1] = false; for (int i = 2; i * i <= limit; i++) { if (isPrime[i]) { for (int j = i * i; j <= limit; j += i) { isPrime[j] = false; } } } for (int i = 2; i <= limit; i++) { if (isPrime[i]) primes.push_back(i); } return primes; } // Prime factorization using precomputed primes void primeFactorization(int n, const vector<int>& primes) { int temp = n; for (int p : primes) { if (p * p > n) break; // stop when p > sqrt(n) while (n % p == 0) { cout << p << " "; n /= p; } } if (n > 1) cout << n; // remaining prime factor cout << endl; } int main() { int num; cout << "Enter a number: "; cin >> num; // Precompute primes up to sqrt(num) int limit = (int)(sqrt(num)) + 1; vector<int> primes = sieve(limit); cout << "Prime factors of " << num << ": "; primeFactorization(num, primes); return 0; }
210
Enter a number: Prime factors of 210: 2 3 5 7
Efficiency
- Sieve: O(n log log n), once
- Factorization: O(π(√n)), where π(x) is the number of primes ≤ x — much fewer than √n divisions
Time Complexity and Memory
What to say
- Sorting 10 numbers finishes instantly with any algorithm. Sorting 10 million takes hours with a slow one and seconds with a fast one.
- So we do not measure seconds — we measure how fast the cost grows as n grows. Drag the slider and let the room watch n² fall out of the time limit.
- Two things we drop: constant factors and less significant terms.
- A judge does roughly 10⁸–10⁹ simple operations per second — from the n in the statement you can pick the complexity you are allowed.
Why complexity matters
- Sorting 10 numbers → both good and bad algorithms finish instantly.
- Sorting 10 million numbers → slow algorithms take hours, fast ones finish in seconds.
- We use complexity analysis to predict how runtime and memory grow as n increases.
Big-O describes the upper bound on the growth rate — how fast runtime or memory usage can grow in the worst case.
Key principles
- Ignore constant factors — 2n or 5n, it is still O(n)
- Ignore less significant terms — n² + n → O(n²), because n² dominates for large n
Demo · how long an algorithm runs for a given n
| Complexity | Example | Operations | Time at 10⁹ ops/s |
|---|
Green fits inside one second, red does not. This is exactly the estimate to make before choosing an approach.
Common time complexities
| Complexity | Name | Example |
|---|---|---|
| O(1) | Constant | Access an array element |
| O(log n) | Logarithmic | Binary search |
| O(n) | Linear | Loop over an array |
| O(n log n) | Linearithmic | Merge sort, quicksort on average |
| O(n²) | Quadratic | Nested loops |
| O(2ⁿ) | Exponential | Naive Fibonacci recursion |
| O(n!) | Factorial | Traveling Salesman brute force |
Example of O(2ⁿ) — very inefficient
int fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); // Very slow for large n }
Space (Memory) Complexity
Extra memory beyond the input
- O(1) → constant extra space (variables, counters)
- O(n) → memory proportional to n (arrays, hash maps)
- O(n²) → large 2D matrices, graph adjacency matrices
Practical Takeaways
Time, space, and the other bounds
- Time complexity tells you how fast runtime grows.
- Space complexity tells you how much extra memory grows.
- Use Big-O for worst-case analysis, but know there is also Big-Θ (Theta) — a tight bound, the exact growth rate — and Big-Ω (Omega) — the best-case bound.
For competitive programming and large-scale apps, aim for
- Sorting: O(n log n)
- Searching: O(log n) or better
- Graphs: BFS/DFS O(V + E)
- Precomputation (like a sieve) can trade memory for speed
Stack — LIFO (Last In, First Out)
What to say
- Think of a stack of plates: you can only add to or remove from the top. Every operation is O(1).
- Use the playground to show the stack, then switch it to queue and deque — the same idea with other ends open.
pop()andtop()on an empty stack are undefined behaviour, not an exception. Checkempty()first.- Then the same stack written by hand on linked nodes — the preview of lecture 2.
Core operations
push(item)— add an item to the top — O(1)pop()— remove the top item — O(1)top()/peek()— view the top item without removing it — O(1)empty()— check if the stack is empty — O(1)size()— get the number of elements — O(1)
C++ implementation using the STL
// C++ built-in stack #include <iostream> #include <stack> using namespace std; int main() { stack<int> st; st.push(10); st.push(20); st.push(15); st.push(1); cout << st.top() << endl; // 1 st.pop(); st.pop(); cout << st.top() << endl; // 20 cout << st.size() << endl; cout << st.empty(); return 0; }
1 20 2 0
Demo · playground
| Structure | Rule | Insert at | Remove from | STL |
|---|---|---|---|---|
| Stack | LIFO | top | top | std::stack |
| Queue | FIFO | back | front | std::queue |
| Deque | both ends | front and back | front and back | std::deque |
Another version · Stack and Queue on linked nodes
No STL: each element is a Node with a pointer to the next one. The stack pushes and pops at the head; the queue keeps both front and rear.
the full program — version 2134 lines
#include <iostream> #include <stack> using namespace std; class Node { public: int data; Node* next; Node(int val) { data = val; next = nullptr; } }; class Stack { public: Node* top; int sz; Stack() { top = NULL; sz = 0; } void push(int val) { Node* newNode = new Node(val); newNode->next = top; top = newNode; sz++; } void pop() { if (top == NULL) return; Node* temp = top; top = top->next; delete temp; sz--; } int peek() { if (top == NULL) return -1; // or throw an exception return top->data; } int size() { return sz; } int empty() { return sz == 0; } }; class Queue { public: Node* front; Node* rear; Queue() { front = NULL; rear = NULL; } void push(int val) { Node* newNode = new Node(val); if (rear == NULL) { front = rear = newNode; return; } rear->next = newNode; rear = newNode; } void pop() { if (front == NULL) return; Node* temp = front; front = front->next; if (front == NULL) rear = NULL; delete temp; } int peek() { if (front == NULL) return -1; // or throw an exception return front->data; } int empty() { return front == NULL; } int size() { int count = 0; Node* current = front; while (current != NULL) { count++; current = current->next; } return count; } }; int main() { // stack<int> st; // st.push(10); // st.push(20); // st.push(30); // st.push(40); // cout << st.top() << endl; // st.pop(); // cout << st.top() << endl; // cout << st.size() << endl; // cout << st.empty() << endl; // Node* node1 = new Node(10); // Node* node2 = new Node(20); // Node* node3 = new Node(30); // node1->next = node2; // node2->next = node3; // cout << node1->data << " " << node2->data << " " << node3->data << endl; // cout << node1->next->next->data << endl; Stack *st = new Stack(); st->push(10); st->push(20); st->push(30); st->push(40); cout << st->peek() << endl; st->pop(); cout << st->peek() << endl; cout << st->size() << endl; cout << st->empty() << endl; return 0; }
40 30 3 0
Lab 1 — the problems
What to say
- The problems follow the order of the lecture: A–B are GCD and binpow, C–E are primes, F–H the stack, I–J the queue and the deque.
- H is the hard one — a monotonic stack. Say plainly that it is an application of the stack.
- Limits: 1 second and 256 MB almost everywhere.