Geri qayıt

Dərs 2: Acgöz alqoritmlər və metodlar

Dərsin videosu

Qeydlər

Bu dərsin qeydləri hələ tam deyil. Əlavə olunana qədər dərsin videosuna baxmağınız tövsiyə olunur. Sonda əlavə resurslar bölməsinə baxmağı da unutmayın.

İkinci dərsə xoş gəldiniz! Ümid edirəm, ilk dərsdə hamı C++ haqqında maraqlı bir şey öyrənə bildi. Piazza forumunda da çox əla söhbətlər oldu. Söz verdiyim kimi, onları da paylaşıram.

Ən maraqlı Piazza paylaşımları
@7, @14, @15, @16, @20, @21, @23, @24, @27, @28, @34, @35, @47

Piazza-da aktiv olanlar və başqalarına yardım edənlər
Bu adların hamısına təşəkkürlər! Əgər kimisə unutdumsa və ya gözümdən yayındısa, mənə Piazza-da bildirə bilər.
Royal Hasanov, Ali Qurbanli, al95ireyiz, Nizami Tağızadə, Aydin, Nizami Tağızadə, Muhammedali Ahmadov, Nasir Bashirov, Rasul Gasimli, Fatimə Əlizadə , Shahin Alekberli , Mete Namazov , Ali Aliyev , Shahin Alekberli, Mehman Karimov, Onur Rahimli , Akbar Habibullayev, Aylin Masiyeva

Sizin yazdıqlarınız
Etməli: Burada kampçıların qeydlərini paylaş.

Bu gün çox maraqlı mövzulara baxacağıq. İki hədəfim var: dərsin sonunda siz həm olimpiada üçün lazım olan C++ dilinin 95%-ni öyrənəcəksiniz (qalan 5%-i isə yalnız beynəlxalq olimpiadalara hazırlaşdıqda öyrənmək lazımdır, çünki onlara aid məsələlər olduqda, çox çətin olurlar), həm də olimpiadalarda düşən ən məşhur məsələ tipini həll etmək üçün çoxlu və ən önəmli metodları öyrənəcəksiniz. Söhbət "acgöz" adlandırdığımız alqoritmlərdən gedir. Əslində, bu tipli məsələlərin konkret mənasını tapmaq çətindir, amma məntiq budur ki, hansısa intuitiv yollarla cavaba çatırıq. Hətta, ən asan məsələləri belə acgöz tipində ifadə etmək olar. Tutaq ki, sizin qabağınızda n sayda tort var və hərəsi üçün obyektiv dad balları professional dad testi edənlər tərəfindən müəyyən olunmuşdur. Siz bu balları bilirsiniz və ən dadlı tortu yemək istəyirsiniz. Hansı tortu yeyəcəksiniz? Təbii ki, bu, verilmiş ədədlər arasında ən böyük ədədi tapmaq məsələsi ilə eynidir. Amma həm məsələnin şərti fərqli verilmişdir, həm də istifadə etdiyimiz seçim acgöz seçimdir, yəni, acgöz davranıb dad balı ən çox olan tortu yeyirik. Bəzən asan məsələlərin şərtləri bu tipdə verilir. Yalnızca çoxlu məsələ işləməklə şərtləri daha sürətli oxuya və daha tez başa düşə bilərsiniz.

Keçən dəfə dərsdə tiplərdən çox danışdıq. İndi isə struct mürəkkəb tip qurucusu haqqında danışaq. Bəzən məsələlərdə verilənlər arasında əlaqələr olur, məsələn, girişə müstəvi üzərində kordinatlar verilir. Bu nöqtələri daxil etmək üçün, məsələn, iki massiv və ya vector işlədə bilərdik, amma bu, onların arasında əlaqə yaratmır. Məsələn, bu nöqtələri x kordinatına görə sıralamalı olsaydıq, bunu necə edərdik? Bir həll olaraq, pair tipi işlədə bilərdik. Bəs kordinatlar 3-ölçülü olsaydı necə? Onda pair<int, pair<int, int>> tipindən istifadə edə bilərdik, amma bu həllərin arasında ən sadə və rahat olanı, məncə, struct istifadə etməkdir. İstifadəsi çox rahatdır:

#include<bits/stdc++.h>
using namespace std;

struct point { // point yaratdığımız tipin adı olacaq
  int x, y;
}; // burada nöqtə-vergülü unutmayın

int main() {
  int n;
  cin >> n;
  vector<point> v(n);
  for (int i = 0; i < n; i++) {
    cin >> v[i].x >> v[i].y;
  }
  // kodun davamı...
}

Bu tip bizə həm işləyəndə rahat olur (məsələn, x və y kimi dəyişən adları daha rahat başa düşülür, nəinki first and second ifadələri), həm də koddakı qarışıqlığı aradan qaldırır. İndi isə sual yaranır: bəs biz bu nöqtələri necə sıralayırıq? sort(v.begin(), v.end()) yazsaq nə baş verir? C++ iki point tipini müqayisə edə bilmir, ona görə də, kodu icra edə bilmir. Bu sıralamaları etmək üçün isə iki önəmli metod göstərəcəm və ikisini də bilmək lazımdır. Bu yollardan biri müqayisə əməlini təyin etməkdir, bu əməl üçün isə yalnız < operatorunu nəzərdə tuturuq. Bunu xüsusi operator funksiyası yazmaqla edirik:

#include<bits/stdc++.h>
using namespace std;

struct point { // point yaratdığımız tipin adı olacaq
  int x, y;
}; // burada nöqtə-vergülü unutmayın

bool operator<(point &p1, point &p2) {
  if (p1.x == p2.x) {
      return p1.y < p2.y; // əgər x kordinatları bərabərdirsə, y-ə görə sıralayaq
  }
  return p1.x < p2.x;
}

int main() {
  int n;
  cin >> n;
  vector<point> v(n);
  for (int i = 0; i < n; i++) {
    cin >> v[i].x >> v[i].y;
  }
  sort(v.begin(), v.end());
  for (auto &p: v) {
    cout << p.x << " " << p.y << endl;
  }
}

Funksiyalar haqqında danışmamışıq, amma onlar haqqında bilmirsinizsə, bu keçiddən oxumağı və öyrənməyi tövsiyə edirəm. Nəzərə alın ki, bu funksiyanın tipi bool olmalıdır, çünki keçən dəfə dediyimiz kimi, müqayisə ifadələri məntiqi ifadələr sayılır. Daha sonrasında isə operator < yazmaqla, həmin əməli təyin etdiyimizi bildirmiş oluruq. Daha sonra isə funksiya iki point tipi alır. Nəzərə alın ki, dəyişənlərin adının qabağında & işarəsi var. Bu, o deməkdir ki, bu funksiyaya dəyişən daxil etdikdə, o, kopyalanmır, birbaşa ötürülür. Bunu etməyimizin səbəbi isə zamana qənaət etməkdir. Adi funksiyalarda və primitiv tiplərdə kopyalama çox zaman almır, amma daha mürəkkəb tiplərdə bu referans adlandırdığımız işarəni qoymaqla proqramın zaman itirməsinin qarşısını alırıq. İçəridə yazdığımız dəyəri isə p1p2 dəyişənlərinin aldığı sıra kimi başa düşmək olar. Bu hissəni daha yaxşı anlamaq üçün dərsin videosuna baxmağı çox tövsiyə edirəm, orada daha geniş izah var.

Sən də yaz
Funksiyalar haqqında Piazza-da yazın. void tipi haqqında yazmağı da unutmayın. Dəyişənlərin önünə & işarəsi yazmağın nə mənaya gəldiyini və onun başqa istifadələrini də göstərin (məsələn, funksiyaya vector daxil etdikdə).

Sıralamağın ikinci yolu isə sort funksiyasını komparator ilə təmin etməkdir. Biz bu funksiyaya hazırda iki dəyər veririk (v.begin()v.end()), amma məcburi olmayan 3-cü dəyər də verə bilərik. Həmin dəyər funksiya olmalıdır. Mən bu dəyəri lambda funksiyası kimi yazmağı daha çox sevirəm.

#include<bits/stdc++.h>
using namespace std;

struct point { // point yaratdığımız tipin adı olacaq
  int x, y;
}; // burada nöqtə-vergülü unutmayın

int main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);
  int n;
  cin >> n;
  vector<point> v(n);
  for (int i = 0; i < n; i++) {
    cin >> v[i].x >> v[i].y;
  }
  sort(v.begin(), v.end(), [&](auto &p1, auto &p2) {
    if (p1.x == p2.x) {
      return p1.y < p2.y; // əgər x kordinatları bərabərdirsə, y-ə görə sıralayaq
    }
    return p1.x < p2.x;
  });
  for (auto &p: v) {
    cout << p.x << " " << p.y << endl;
  }
}

Bu, çətin görünə bilər, amma bayaqkı funksiyanın eysini yazırıq. Bayaqkı funksiyada dəyişənləri point olaraq qeyd etməli idik, amma burada auto da işlədə bilərik, çünki C++ başa düşür ki, bu v-nin elementləri arasında müqayisədir və həmin elementlər point tipindədir. Lambda funksiyanın əvvəlində isə [&] var. Oradakı referans işarəsini də yazmağı unutmayın (bu halda, fərq etmir, amma başqa hallarda kodu sürətləndirə bilər). Bu haqda da daha çox dərsin videosunda var, amma Piazza-da sual yaza bilərsiniz.

Sən də yaz
Lambda funksiyaları haqqında daha geniş öyrənin və Piazza-da yazın. Onları nə vaxt istifadə edə bilərik? Adi funksiyalar ilə sürət fərqi varmı?

İndi isə C++-da ən güclü tiplər haqqında öyrənəcəyik. priority_queue tipi ilə başlayaq. Ona biz elementlər əlavə etdikdə, onları avtomatik olaraq sıralayır, amma vector-da edə bildiyimiz kimi istənilən indeksdəki elementi oxuya bilmirik. Bu, bizə ən böyük və ya kiçik elementlərdə dinamik şəkildə işləməyə icazə verir.

priority_queue<int> pq;

pq.push(x); // x elementini artır

pq.top(); // başdakı elementi oxu

pq.pop(); // başdakı elementi sil

pq.size(); // elementlərin sayı

Başdakı element ən böyük element olur, amma bunu dəyişə bilərik. Ən kiçik element olması üçün belə təyin edirik:

priority_queue<int, vector<int>, greater<int>> pq;

Bu tip bizə ən böyük və ya ən kiçik elementləri tapmağa kömək edir, amma biz bunu vector tipi ilə də edə bilərdik. Bəs niyə priority_queue işlədək? Bunun səbəbi alqoritmin işləmə sürətidir. vector tipində biz ən böyük və ya ən kiçik elementi tapmaq üçün xətti sayda əməl edirik. Xətti dedikdə, vectorun ölçüsünə nəzərən deyilir. Məsələn, ölçü n-dirsə, onda vectorda axtarış n-dən xətti asılı olacaq. Buna $O(n)$ zaman çətinliyi deyirik. Məsələn, n dəfə ən böyük ədədi axtarsaq, onda zaman çətinliyi $O(n\cdot n) = O(n^2)$ olar. Kompüterlər isə hazırda təxminən $10^9$ əməliyyatı bir saniyəyə görürlər. Yəni, $n=10^5$ halında ümumi əməliyyatlar üçün sərf olunan zaman çox təxminən 10 saniyə çəkəcək. Bu təxminlər çox kobuddurlar, çünki bəzən kompilyatorlar bizim üçün optimizasiyalar edirlər. Bəzən isə bir əməliyyat xətti olsa da, onun əmsalı böyük olur (məsələn, $10n$ əməliyyat edir, amma bu da $O(n)$ sayılır). priority_queue-də isə element əlavə etmək və silmək loqarifmik çətinlikdə (loqarifmik dedikdə, 2 əsasda nəzərdə tuturuq), ən başdakı elementi tapmaq üçün isə konstant zaman lazımdır. Konstant zaman adətən çox sürətli deməkdir (məsələn, 5 əməliyyat). Loqarifmik də çox sürətlidir. Nəzərə alın ki, milyon ədədinin 2 əsasda loqarifması təxminən 20 edir. Yəni, milyon əvəzinə 20 əməliyyat edirik. Dediyim kimi, bu hesablamarın hamısı çox təxminidirlər, amma məsələ üçün alqoritmik çətinlik düşünəndə kömək olurlar.

Önəmli tövsiyə (+sən də yaz)
Zaman çətinlikləri haqqında bu linkdəki kitabın ikinci fəsilindən (Chapter 2) oxuyun. Mövzunun qısa izahını Piazza-da qeyd kimi paylaşa bilərsiniz.

İndi isə priority_queue-dən daha güclü bir tipə baxaq. set tipi elə bir strukturdur ki, ora element əlavə edib, istənilən elementi silə bilərik, həm də eyni zamanda həm ən böyük, həm də ən kiçik elementi oxuya bilərik. Bundan əlavə, iki çox maraqlı lower_boundupper_bound metodları da var, amma onlar haqqında daha sonra danışacağıq. Bu tiplə çalışmaq daha çətindir, amma praktika ilə daha rahat olacaq. Nəzərə alın ki, set tipi yalnız fərqli ədədləri özündə saxlayır. Əgər əlavə etdiyimiz ədəd set-də var idisə, heç nə dəyişmir.

set<int> s;

s.insert(x); // x elementini əlavə et

s.erase(x); // x elementini sil

s.find(x); // x elementinə uyğun gələn iteratoru tap (yoxdursa, s.end() olur)

s.begin(); // başlanğıc iterator (ən kiçiyə uyğun gəlir)

s.end(); // son iterator (burada heç bir element uyğun gəlmir)

s.size(); // set-in ölçüsü

s.count(x); // x elementinin set-də sayı (setdə 0 və ya 1 olur)

(burada iteratorları və onlardan elementin özünü almaqla bağlı danış)

Özünü yoxla
Müxtəlif-müxtəlif məsələsini həll et.

Özünü yoxla
Qiymət nişanları məsələsini həll et.

set tipindəki demək olar ki, bütün əməliyyatlar loqarifmik zamana işləyirlər. Bəs niyə bu tip ola-ola priority_queue işlədirik, axı bu daha güclüdür? Səbəbi isə zaman çətinlikləri eyni olsa da, set tipində əməliyyatların konstant əmsalı daha çoxdur, yəni praktikada daha çox zaman aparır. Dediyimiz kimi, set ancaq fərqli ədədləri özündə saxlayır. Bəs bir ədəddən çoxlu olmağını istəsək necə? Onda bütün əməlləri set ilə eyni olan multiset tipi istifadə etmək olar.

map tipi haqqında danış.

#include<bits/stdc++.h>
using namespace std;

int main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);
  map<int, int> mp;
  mp[1] = 5;
  mp[4] = 10;
  // C++11 və yuxarı
  for (auto &p: mp) {
    cout << p.first << " " << p.second << endl;
  }
  cout << endl;
  // C++14 və yuxarı
  for (auto &[k, v]: mp) {
    cout << k << " " << v << endl;
  }
  // ...
}

Özünü yoxla
Girişə n ədədi və n ədəd söz verilir. Ən çox rast gəlinən sözü necə taparıq?

#include<bits/stdc++.h>
using namespace std;

int main() {
  ios_base::sync_with_stdio(0);
  cin.tie(0);
  map<int, int> mp;
  mp[1] = 5;
  mp[4] = 10;
  cout << mp.count(1) << endl;
  cout << mp.count(3) << endl;
  if (!mp[3]) {
    cout << "3 yoxdur" << endl;
  }
  cout << mp.count(3) << endl; // ???
  return 0;
}

Bu tiplərdə struct istifadə edəndə nə etməli haqqında danış.

İki indeks (two pointers) metodu haqqında danış.

Özünü yoxla
Apartments məsələsini həll et.

Özünü yoxla
Playlist məsələsini həll et.

Binary search (ikili axtarış) haqqında danış. lower_bound, upper_bound metodları və funksiyaları haqqında danış.

Özünü yoxla
İki dəfə böyük məsələsini həll et.

Özünü yoxla
Towers məsələsini həll et.

Özünü yoxla
Sal məsələsini həll et.

Özünü yoxla
Subarray Sums I məsələsini həll et.

Prefix sum haqqında danış.

Özünü yoxla
Subarray Sums II məsələsini həll et.

Özünü yoxla
Forest Queries məsələsini həll et.

Əlavə faydalı resurslar

Ev tapşırığı

Bu dərs üçün xüsusi ev tapşırığı cses.fi saytındakı Sorting and Searching bölməsi altındakı bütün məsələlərdir. Bu məsələlərin hamısı çox önəmlidirlər.

Ev tapşırıqları üçün eolymp saytında yaratdığım qrup var. Əgər qrupa dəvət almamısınızsa, Piazza-da "E-olymp qrupu üçün" başlıqlı paylaşımın altına istifadəçi adınızı qeyd edin. Dərsdə dediyim kimi, ev tapşırığının məqsədi sizin üçün praktikadır, gələn dərsə qədər bitirməyə ehtiyac yoxdur. Bəzi məsələlərin həllini bilirsinizsə və onlar sizin üçün asandırsa, etməyə də bilərsiniz.