# Informatyka ## C++ ### Drzewa > Warto na początek zajrzeć [tutaj](https://hackmd.io/XoHx6BGQQIyw394yLKeX7w) i przypomnieć sobie czym są grafy oraz sposoby ich reprezentacji na potrzeby programowania. *Drzewo* to szczególny przypadek grafu, który spełnia następujące kryteria: 1. ma dokładnie $n - 1$ krawędzi 2. jest spójny 3. nie ma cykli (tą własność można wydedukować z dwóch poprzednich - zastanów się jak?) Drzewo możemy *ukorzenić*, czyli "powiesić" za wybrany wierzchołek. Ukorzenienie drzewa nie zmienia jego wewnętrznych "relacji", ale zmienia jego kształt. Ta obserwacja jest szczególnie istotna przy dużych drzewach, gdzie Często w treści zadania będzie sprecyzowane, który wierzchołek jest korzeniem. ```graphviz graph{ 1 -- 2 2 -- 3 1 -- 4 2 -- 5 2 -- 8 5 -- 6 4 -- 7 } ``` *Spójna składowa* to taki podzbiór wierzchołków, że jest możliwe przejście między dowolną parą z nich. Łatwo zauważyć, że *graf spójny* to taki, który ma tylko jedną spójną składową. Poniższy graf nie jest spójny, bo nie ma sposobu na wykonanie przejścia $1 \rightarrow 4$. ```graphviz graph{ 1 -- 2 2 -- 3 1 -- 6 2 -- 6 4 -- 5 } ``` #### Jak poruszać się po drzewie? Chcemy sprawnie przechodzić między wierzchołkami w drzewie, niech będzie ono tutaj reprezentowane w postaci list sąsiedztwa. Każde poddrzewo (mniejszy graf "zwisający" z jakiegoś wewnętrznego wierzchołka) również jest drzewem, więc można fajnie wykorzystać zależność rekurencyjną: ```c= void przejdz_po_drzewie(int v) { // zrób coś dla wierzchołka v for (int i = 0; i < sasiedzi[v].size(); i++) { int obecny_syn = sasiedzi[v][i]; // ewentualnie zrób coś dla syna przejdz_po_drzewie(obecny_syn); } } ``` **Ale uwaga!** Ze względu na nieskierowane krawędzie takie przeszukiwanie może się zapętlić (na przykład ten kod wywołany na drzewie z przykładu daje $1 \rightarrow 2 \rightarrow 1 \rightarrow 2 \rightarrow \dots$), potrzebny jest jakiś sposób na pamiętanie odwiedzonych wierzchołków. Dobrze sprawdzi się tablica `bool odwiedzony[N]`. Wtedy: ```c= void przejdz_po_drzewie(int v) { odwiedzony[v] = true // zrób coś dla wierzchołka v for (int i = 0; i < sasiedzi[v].size(); i++) { int obecny_syn = sasiedzi[v][i]; if (!odwiedzony[obecny_syn]) { // ewentualnie zrób coś dla syna przejdz_po_drzewie(obecny_syn); } } } ``` Zwróć uwagę, że w przytoczonych wyżej fragmentach programu jest linijka do uzupełnienia `// zrób coś dla wierzchołka v`. To co będzie w tym miejscu zależy od zadania, ale z reguły wykonamy tutaj jakąś operację arytmetyczną, uzupełnianie odpowiedniego pola w tablicy itp. Koszt trawersowania drzewa w ten sposób ma złożoność $O(n + m)$, ponieważ do każdego wierzchołka wchodzimy co najwyżej raz (dba o to struktura `odwiedzony[]`). #### Numeracja *preorder* i *postorder* Czasem będziemy chcieli "przenumerować" wierzchołki podane na wejściu, żeby przeglądać je w bardziej systematyczny sposób (o dalszych zastosowaniach porozmawiamy później). Dwoma popularnymi metodami są: * *preorder* - w jakiej kolejności wchodzimy do wierzchołków * *postorder* - w jakiej kolejności wychodzimy z wierzchołków tj. po przejrzeniu wszystkich synów Na przykład dla ilustracji z początku notatki nowa numeracja wygląda następująco: | Numer wierzchołka | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | ----------------- | --- | --- | --- | --- | --- | --- | --- | --- | | **Preorder** | 1 | 2 | 3 | 7 | 4 | 5 | 8 | 6 | | **Postorder** | 8 | 5 | 1 | 7 | 3 | 2 | 6 | 4 | ### Grafy **Grafy** to abstrakcyjna struktura danych, która świetnie nadaje się do modelowania zależności z prawdziwego życia. Weźmy na przykład: * osoby (wierzchołki) i ich znajomości (krawędzie) * miasta i łączące je drogi * posty w mediach społecznościowych i tagi Grafy można reprezentować na różne sposoby, a odpowiednią reprezentację dobieramy do rozwiązywanego zadania. Na przykład dla poniższego przykładu ```graphviz digraph{ rankdir = LR 1 -> 2 1 -> 3 2 -> 3 3 -> 2 4 -> 2 1 -> 5 4 -> 5 3 -> 4 5 -> 3 } ``` *reprezentacja macierzowa* wygląda tak: | | 1 | 2 | 3 | 4 | 5 | | ----- | - | - | - | - | - | | **1** | 0 | 1 | 1 | 0 | 1 | | **2** | 0 | 0 | 1 | 0 | 0 | | **3** | 0 | 1 | 0 | 1 | 0 | | **4** | 0 | 1 | 0 | 0 | 1 | | **5** | 0 | 0 | 1 | 0 | 0 | a z wykorzystaniem *list sąsiedztwa* tak: | | Sąsiedzi | | ----- | ---------- | | **1** | 2, 3, 5 | | **2** | 3 | | **3** | 2, 4 | | **4** | 2, 5 | | **5** | 3 | ==Uwaga!== Graf może być *skierowany* lub *nieskierowany*, czyli krawędzie mogą być jednokierunkowe lub obukierunkowe (powyżej mamy przykład grafu skierowanego). W przypadku grafów nieskierowanych dla uproszczenia nie rysujemy strzałek mimo, że teoretycznie tam są (w obie strony). To jaką reprezentację chcemy wykorzystać w danym zadaniu zależy od wymagań (jakich operacji będziemy wykonywać najwięcej, czy mamy mało czy dużo pamięci itp.). Przeanalizujmy złożoność niektórych operacji grafowych dla obu znanych nam reprezentacji: | Zadanie | Rep. macierzowa | Rep. listowa | | -------------------------------- | --------------- | -------------------- | | Obliczanie stopnia wierzchołka | $O(n)$ | $O(deg(v))$ | | Przejrzenie wszystkich krawędzi | $O(n^2)$ | $O(n + m)$ | | Sprawdzenie czy krawędź istnieje | $O(1)$ | $O(deg(v))$ | | Dodanie nowej krawędzi | $O(1)$ | $O(deg(v))$ | | Usunięcie krawędzi | $O(1)$ | $O(deg(u) + deg(v))$ | *Stopniem wejściowym (wyjściowym)* wierzchołka $v$ nazywamy liczbę wchodzących do niego (lub wychodzących z niego). Dla grafów nieskierowanych jest to ta sama wartość i dla uproszczenia używamy po prostu pojęcia stopnia. ### STL Kod z zajęć ```c++ #include <iostream> #include <vector> #include <queue> #include <map> #include <algorithm> using namespace std; vector<int> myVec; pair<int, int> para; priority_queue<int, vector<int>, greater<int>> kolejka; map<int,char> mapa; //set<int> zbior; //stack<int> stosik; //queue<int> kolejka_zwykla; //deque<int> kolejka_podwojna; bool cmp(int a, int b){ if(a%2==0){ if(b%2==0){ return a < b; }else{ return true; } }else if (b%2==0){ return false; }else{ return a < b; } } //sort vector<pair<int, int>> pary; int tab[10]; int main(){ /*para = {1, 2}; pary.push_back(para); pary.push_back({3, 1}); pary.push_back({2,7}); pary.push_back({1,1}); pary.push_back({0,0}); for(int i = 0; i< pary.size(); i++){ cout << pary[i].first << " " << pary[i].second << endl; } //sort(tab+0, tab+10); sort(pary.begin(), pary.end()); cout << endl; for(int i = 0; i< pary.size(); i++){ cout << pary[i].first << " " << pary[i].second << endl; } kolejka.emplace(7); //7 cout << kolejka.top() << endl; kolejka.emplace(3); //3 7 cout << kolejka.top() << endl; kolejka.emplace(5); //3 5 7 cout << kolejka.top() << endl; kolejka.pop(); //5 7 cout << kolejka.top() << endl;*/ vector<int> vecc = {1,2,5,8,0, 4,13, 23, 10}; sort(vecc.begin(), vecc.end(), cmp); for(int i = 0; i<vecc.size();i++){ cout << vecc[i] << " "; } return 0; } ``` ### Binary search Rozważmy następujący problem: Zostało wybrane n=10 liczb, które zostały zapisane w porządku niemalejącym. Te liczby są zakryte. Naszym celem jest dowiedzieć się czy liczba x znajduje się wśród wybranych liczb. Do dyspozycji mamy tylko pytania o konkretną pozycję i jednocześnie chcemy użyć jak najmniej pytać. Niech liczby będą takie 2 14 36 44 48 60 79 80 84 95 Szukamy 79 `_ _ _ _ _ _ _ _ _ _` Optymalną strategią jest pytanie zawsze w połowie Pytanie o 5 pozycję `_ _ _ _ 48 _ _ _ _ _` 79 jest większe od 48, więc musimy szukać na prawo Pytamy o 8 pozycję `_ _ _ _ 48 _ _ 80 _ _` Pytamy o 6 `_ _ _ _ 48 60 _ 80 _ _` Pytamy o 7 `_ _ _ _ 48 60 79 80 _ _` Znaleźliśmy szukaną liczbę w 4 ruchach. Przeszukując naiwnie od lewej strony potrzebowalibyśmy ich aż 7 Dzięki dzieleniu przeszukiwanego przedziału na pół co zapytanie osiągnęliśmy złożoność logarytmiczną Postarajmy się sformalizować to intuicyjne podejście, by ostatecznie wprodukować działający kod Niech l i r oznaczają końce tablicy (l=0, r=n) Pytanie o środek realizujemy przez zapytanie o mid=(l+r)/2 Poniższy kod znajduje liczbę q w tablicy tab ```cpp int r = n; int l = 0; while(l < r) { int mid = (l + r) / 2; cout << mid << " " << tab[mid] << endl; if(tab[mid] >= q) { r = mid; } else { l = mid +1; } } ``` ### Vectory Korzystając z tablic tablic spotkaliśmy się z problemem zużycia pamięci: Mamy 10 kategorii w każdej maksymalnie dziesięć produktów, a sumarycznie produktów może być 20 Wówczas musimy przygotować się na najgorsze opcje i musimy zrobić tablicę 10x10 łącznie 100 komórek, czyli 5 razy więcej niż potrzeba Poznamy rozszerzalne tablice, które pozwolą nam zaoszczędzić pamięć. Są to vectory ```cpp #include <vector> vector<typ> nazwa; vector<int> myVec; ``` Do vectora można dodawać elementy na koniec ```cpp myVec.push_back(7); myVec.push_back(10); ``` Elementy można odczytywać jak z tablicy ```cpp myVec[0];//zwraca 7 ``` Można robić vector vectorów ```cpp vector<vector<int>> myVec2d; ``` Można zmieniać rozmiar vectora, szczególnie przydatne, gdy chcemy rozpocząć pracę z kilkoma wektorami pustymi ```cpp myVec2d.resize(n); //teraz myVec2d składa się z n pustych wektorów ``` Po użyciu funckji resize można korzystać z myVec2d prawie jak z tablicy 2d (pamiętać o push_back!) Porównanie tablica 2x3 ```cpp int tab[2][3] ``` Zawiera od razu 6 komórek pamięci [[0,0,0],[0,0,0]] vector intów ```cpp vector<int> myVec; ``` zawiera pusty pojemnik na inty [] Po wykonaniu `myVec.push_back(7)` wygląda tak [7], a po `myVec.push_back(10)` tak [7, 10] vector vectorów intów ```cpp vector<vector<int>> myVec2d; ``` zawiera pusty pojemnik na pojemniki na inty [] vector vectorów intów + resize ```cpp vector<vector<int>> myVec2d; myVec2d.resize(2); ``` zawiera 2 pojemniki na inty [[],[]] Po wykonaniu `myVEc2d[1].push_back(2)` wygląda tak [[],[2]], a po wykonaniu `myVec2d[0].push_back(6)` tak [[6],[2]] Żeby wykonywać pętle po wektorach przydaje się funkcja size, która zwara rozmiar wektora ```cpp myVec.size(); ``` Przykład Wczytywanie i wypisywanie listy sąsiedztwa ```cpp= #include <bits/stdc++.h> using namespace std; vector <vector <int>> V; int main () { int n, d; //n - liczba wierzchołków, d liczba krawędzi cin >> n >> d; V.resize(n);//wierzchołki numeruję od 0 do n-1 for (int i = 0; i < d; i++) { int P1, P2; cin >> P1 >> P2; V[P1].push_back(P2); //jak jest krawędź to zapisuję V[P2].push_back(P1); } for (int i = 0; i < n; i++) {//pętla dla każdego wierzchołka cout << i << ": "; for (int j = 0; j < V[i].size(); j++) {//dla każdego sąsiada cout << V[i][j] << " "; } cout << "\n"; } return 0; } ``` ### Sito Schemat rozwiązania ```c++= bool czyWykr[50]; czyWykr[0] = ?; czyWykr[1] = ?; for(?){//przejdz po wszystkich liczbach if(?){//jeśli jest pierwsza for(?){//to wykreśl wielokrotności czyWykr[?]=?; } } } ``` Pętle z wypisywaniem liczb najlepiej zrobić osobno (dla prędkości programu). Przypominam o magicznych linijkach ### Sumy prefiksowe Sumy prefiksowe jest to technika pozwalająca na szybkie odpowiadanie na pytanie "Ile razy wystąpiło zjawisko w przedziale a b?". Nazwa bierze się od sposobu zebrania danych (agregacji) w formie np. sumy dla przedziałów postacii <1, i> dla każdego i tj. dla kolejnych początków tablicy Rozważmy taki przykład: Bajtazar notował kiedy w ciągu roku padał deszcz. Wielu jego znajomych pyta go o to, ile razy w pewnych przedziałów czasowych padał deszcz. Pomóż Bajtazarowi przygotować się na serię pytań. Pomińmy wczytywanie danych ```c++= //dane int deszcz[366]; // tablica wypełniona 1-padało i 0-nie padało int t; liczba pytań int pref[366]; //pierwszego dnia padało tyle samo razy co w przedziale dni <1, 1> pref[1] = deszcz[1]; //dla kolejnych przedziałów padało tyle samo razy co wczoraj + 1 jeśli dziś też padało for(int i = 2; i < 366; i++){ pref[i] = pref[i-1] + deszcz[i]; } //teraz możemy odpowiadać na pytania dla dni od a do b while(t--){ cin >> a >> b; cout << pref[b] - pref[a-1]; } ``` Czemu takie odejmowanie? Jeżeli pytamy od dnia 3 do 5 to możemy odpowiedzieć, że tyle samo co w dniach 1-5, ale pominąć dni 1-2 ### Szybkie potęgowanie Idea: wykorzystamy zamianę liczby na system binarny, żeby zredukować złożoność potęgowania z liniowej na logarytmiczną Chcemy obliczyć $3^{19}$. Klasycznie pomnożylibyśmy dziewiętnaście trójek. Spójrzmy na 19 w systemie binarnym: 10011 Przyda nam się zależność, że $3^{19}=3^{16}\cdot3^{2}\cdot3^1$ 16, 2, 1 pojawiają się, bo sumują się do 19 Potrafimy w czasie logarytmicznym obliczyć $3, 3^2, 3^4, 3^8, 3^{16}$ itd. (Patrz zadania z poprzednich list) W czasie ich wyliczania do akumulatora wystarczy domnażać liczby odpowiadające jedynkom w zapisie binarnym wykładnika ### Funkcje rekurencyjne Funkcje rekurencyjne to takie, które w swojej definicji korzystają same z siebie. Pozwalają one na zwięzłe wyrażanie wielu problemów Policzmy sumę liczb od 0 do n: ```cpp= int f(int x){ if(x==0){ return 0; } return x+f(x-1); } ``` Ta funkcja zwraca 0 dla 0 - czyli dobry wynik Dla większych liczb obliczenia będą wyglądały tak f(3) = 3 + f(2) = 3 + 2 + f(1) = 3 + 2 + 1 + f(0) = 3 + 2 + 1 + 0 = 6 Można liczyć znacznie więcej rzeczy jak liczby Fibonacciego https://pl.wikipedia.org/wiki/Ci%C4%85g_Fibonacciego ```cpp= int fib(int x){ if(x <=1 ){ return 1; //zerowa i pierwsza liczba ciągu to 1 } return f(x-1)+f(x-2); } ``` Schemat z liczenia sumy pozwala nam zastąpić pętlę for od n do 0 Po drobnej modyfikacji otrzymamy pętlę for w formie rekurencyjnej od 0 do n Zróbmy zadanie o treści "Funkcja g liczy magiczne wartości. Policz sumę liczb g(0) + g(1) + ... + g(n-1)" ```cpp= int g(int x){ //jakieś obliczenia return magiczna_wartosc } int forrec(int i, int n){ if(i>=n){ return 0; } return g(i) + forrec(i+1, n); } ``` dla każdego i od 0 do n zawołamy funkcję g(i) i dodamy ją do reszty wyników ### Sztuczka dla programów z wieloma testami Jeśli w zadaniu mamy treść typu "Podana jest liczba t - liczba testów oraz t linii z danymi dla problemu" to można napisać schludny kod w następujący sposób: ```cpp= int main(){ int t; cin >> t; while(t--){ //tutaj kod rozwiązujący pojedynczy przypadek } } ``` Pętla while przerywa się, gdy warunek obliczy się do false lub liczby zero. W tym podejściu korzystamy z tego, że po każdym rozpatrzonym przypadku t zmiejsza się o 1 aż do 0 ### Funkcje Przpomnijmy sobie pewne wzory z geometrii: wzór na pole kwadratu $P = a\cdot a$ oraz na pole prostokąta $P = a \cdot b$ Oba te wzory możemy zapisać jako funkcje $P(a)=a\cdot a$ oraz $P(a,b) = a\cdot b$ Ten zapis oznacza, że żeby policzyć wartość P należy podać wartości argumentów (tj. zmiennych w nawiasach) i wstawić je do wzoru Bardzo podobnie jest w programowaniu ```cpp= int pole_kwadratu(int a){ return a*a; } int pole_prostokata(int a, int b){ return a*b; } ``` Ponieważ programujemy to musimy powiedzieć komputerowi jakimi rodzajami wartości są a i b - są całkowitoliczbowe czyli int Wartość którą otrzymamy z tych funkcji również oznaczyliśmy jako int w pierwszym słowie każdej z tych definicji Żeby skorzystać z naszej funkcji piszemy ```cpp= int main(){ int moje_pole; moje_pole = pole_prostokata(3, 4); cout << moje_pole; } ``` Na razie skorzystaliśmy tylko z funkcji z intami, a co z long longami i charami? Oczywiście też można zrobić takie funkcje ```cpp= long long duza_funkcja(long long a, long long b){ long long tmp = a*b; return tmp - a - b; } ``` Mamy też specjalny typ void. Mówi on o tym, że funkcja nic nie zwraca/nie produkuje żadnej wartości. Po co nam taka funkcja? ```cpp= void wypisz_ladnie(int a){ if(a%2==0){ cout << "Ooo, to bardzo ładna liczba\n"; }else{ cout << "Nie podoba mi się\n"; } //można dać return; ale nie trzeba } ``` Dzięki takiej funkcji możemy wielokrotnie wypisywać ładnie liczby, ale nie oczekujemy wartości. Istotne jest, że środku funckji modyfikujemy kopie argumentów. Spójrzmy na przykład ```cpp= void start(int a){ a = 5; } int main(){ int a = 7; start(a); cout<<a; //wypisze się 7, bo do funkcji start trafia kopia zmiennej a } ``` Do funkcji można przekazywać też tablice, ale zachowują się one inaczej niż zwykłe zmienne - funckja pracuje na oryginale tablicy, a nie kopii ```cpp= void start(int t[], int n){ for(int i = 0; i < n; i++){ t[i] = i; } } int tab[100]; int main(){ start(tab, 100); cout << tab[57]; //wypisze 57 } ``` ### Lista 7 - podpowiedzi Najpierw przeczytaj notatkę poniżej. #### System binarny Patrz niżej #### Szybkie potęgowanie Złe rozwiązanie działa w czasie liniowym, szybkie w czasie logarytmicznym (w związku z reprezentacją binarną wykładnika) Jeśli wychodzą Ci dziwne liczby to pewnie musisz zrobić operację modulo po każdym mnożeniu np. zamiast a=a*x zrób a=(a*x)%1000009 Robienie takiego modulo pozwala na zachowanie dodatnich liczb oraz na sprawdzenie poprawności kodu (więcej na lekcji) #### Czy jest pierwsza Złe rozwiązanie działa w czasie liniowym i sprawdza dzielniki od 2 do n, dobre w czasie pierwiastkowym tj. czas działania szacujemy funkcją $\sqrt n$ Po więcej wskazówek patrz niżej #### Suma kolejnych Złe rozwiązanie działa w czasie liniowym i liczy na piechotę sumę liczb w przedziale Dobre działa w czasie stałym i korzysta ze wzoru na sumę https://pl.wikipedia.org/wiki/Szereg_1_%2B_2_%2B_3_%2B_4_%2B_%E2%80%A6 ### Złożoność obliczeniowa Złożoność programu mówi nam jak wiele operacji zostanie wykonane w zależności od rozmiaru danych wejściowych Intuicyjnie myślimy o oszacowaniu "mniej więcej", bo nie interesują nas konkretne parametry #### Czas stały Np. wtedy gdy zawsze wykonujemy nie więcej niż 10 operacji Przykład: dana jest liczba n, sprawdź czy jest parzysta #### Czas liniowy Gdy na wejściu mamy n liczb i dla wszystkich/większości musimy wykonać jakąś operację Przykład: dane jest n liczb, policz ich sumę oraz iloczyn ##### Funkcja liniowa O funkcji matematycznej myślimy jak o wzorze, do którego podstawiamy liczbę. Np. gdy piszemy $f(x) = 5\cdot x+7$, czytamy ef od iks równa się $\dots$ Możemy policzyć wartość dla dowolnej liczby. Policzmy dla 3. Piszemy wtedy $f(3)= 5\cdot 3 + 7=22$ Funkcje mogą mieć dowolne wzory po prawej stronie równości, funkcje liniowe to te, które są postaci $c\cdot n + d$ Jeśli możemy oszacować czas działania programu funkcją liniową to mówimy, że działa ona w czasie liniowym #### Czas logarytmiczny ##### Logarytm Logarytm to funkcja powiązana ściśle z potęgowaniem. Zapis $log_2 x = 3$ jest prawdą, tylko wtedy gdy prawdą jest $2^3=x$. Można sprawdzić, że $x = 8$ Niektóre programy, które piszemy mają złożoność logartymiczną. Np. zamiana na system binarny. Dlaczego? Każda liczba w systemie binarnym ma tylko logarytmicznie wiele cyfr ```cpp= int main(){ int n; cin >> n; int s = 1; while(s<=n){ s*=2; } s/=2; while(s>=1){ if(s <= n){ cout << 1; n = n - s; }else{ cout << 0; } s /= 2; } } ``` Zamiana na system unarny będzie złym przykładem, bo zajmuje czas liniowy Dla przypomnienia w systemie unarnym mamy $4 = (11110)_1$ oraz $5=(111110)_1$ ```cpp= int main(){ int n; cin >> n; while(n>0){ cout << 1; } cout << 0; } ``` #### Czas kwadratowy Czas kwadratowy mamy wtedy, gdy czas działania programu szacujemy funkcją kwadratową tj. postaci $an^2+bn+c$ Przykładem jest nieefektywne policzenie sum prefiksowych (nieefektywne, bo można to zrobić w czasie liniowym) ```cpp= int tab[100]; int main(){ int n; cin >> n; //wczytywanie for(int i = 0; i < n; i++){ cin >> tab[i]; } //rozwiązanie kwadratowe for(int i = 0; i < n; i++){ int suma = 0; for(int j = 0; j <= i){ suma += tab[j]; } cout << suma << " "; } cout << endl; //rozwiązanie liniowe int suma = 0 for(int i = 0; i < n; i++){ suma += tab[i]; cout << suma << " "; } } ``` Często, ale nie zawsze (!) czas liniowy można rozróżnić od czasu kwadratowego liczbą pętli w pętli. ### Pętle while Pętle while (dopóki) pozwalają powtarzać operacje dopóki sprawdzany warunek jest prawdziwy ```cpp= int main(){ int n; cin >> n; //ten program wczytuje liczbę n, a potem wypisuje jej dzielniki int i = 1; while(i*i<=n){ if(n%i==0){ cout << i << " "; if(n/i!=i){ cout << n/i << " " } } i++; } return 0; } ``` ### Pętle for Pętle umożliwiają wykonywanie tych samych operacji wielokrotnie ```cpp= #include <iostream> using namespace std; int main(){ int n; cin >> n; //ten program wczytuje liczbę n, a potem wczytuje n liczb i wypisuje ich kwadraty for (int i = 0; i < n; i++){ int a; cin >> a; cout << a*a << endl; } return 0; } ``` ```cpp= for(int i = 0; i < n; i++) int i = 0; <- zmienna, którą będziemy odliczać. Możemy zaczynać od innej liczby niż 0 i < n; <- jeśli prawda, to wykonaj instrukcje w klamerkach. Może być dowolny warunek typu <=, != itd. i++ <- po każdym wykonaniu zmień liczbę. i++, i+=1, i=i+1 zwiększają o 1; i--,i-=1,i=i-1 zmiejszają o 1, można podawać inne wartości np. i+=2 lub inne operatory np. i *= 2 ``` ### Struktura programu ```cpp= //dwoma slashami oznaczamy komentarze, które nie zmieniają programu #include <iostream> //to jest dołączenie bibliotek, które zawierają użyteczne i złożone instrukcje using namespace std; //nie musimy pisać std::cout tylko cout itd. int main(){ //w mainie piszemy nasz program, jego zakres wyznaczają nawiasy wąsate/klamerki return 0; //koniec programu podobny do halt w Maszynie RAM } ``` Konstrukcje języka, które poznaliśmy ```cpp int main(){ int liczba1, liczba2; // zmienna całkowitoliczbowa, podobna do komórek w RAM cin >> liczba1 >> liczba2; //wczytywanie cout << "Tekst do wypisania" << liczba1; //wypisywanie if(liczba1 < liczba2){ //konstrukcja jeżeli }else if (liczba1 > liczba2){//w przeciwnym przypadku, jeżeli }else{//w przeciwnym przypadku } //znamy porównania <, <=, >, >=, ==, != //mniejsze, mniejsze równe, większe, większe równe, równe, nierówne //instrukcja przypisania = liczba1 = liczba + 2; //instrukcje arytmetyczne +, -, *, /, % //ostatnie to reszta z dzielenia //sprawdzanie dwóch warunków //oraz, koniunkcja, dwa warunki na raz liczba1 < liczba2 && liczba3 < liczba4 //lub, alternatywa, wystarczy jeden z liczba1 > 0 || liczba 2 > 0 } ``` Program kompilujemy (tłumaczymy na język komputera) zębatką, a uruchamiamy przyciskiem play Typowe błędy: * kompilator nie wie co to iostream - sprawdź czy plik ma rozszerzenie .cpp a nie .c * przyciski na górze są wyszarzone - sprawdź czy program dalej działa zminimalizowany * cout (lub inna instrukcja) does not name a type - sprawdź czy instrukcja jest w mainie lub czy main ma poprawne klamerki * uwaga na count i cout * inne błędy - często brak klamerek i średników * program dziwnie działa, ale się uruchamia? - sprawdź czy nie pomyliłeś = z == * kierunek strzałek w cin i cout ### Edytory kodu Wybieramy jeden z edytorów według własnego upodobania. Jestem w stanie bardziej pomóc przy opcjach 1 i 3, bo je używałem samemu, ale 2 nie powinna odbiegać za bardzo. 1. Code::Blocks link do pobrania: http://www.codeblocks.org/downloads/binaries/ Wybieramy wersję mingw setup Mingw to kompilator, czyli narzędzie, które przekłada kod w C++ do zrozumiałego dla maszyny. W wersji bez mingw dostaniecie po prostu skomplikowany edytor tekstu bez możliwości uruchomienia programu. Tak wygląda edytor ![](https://i.imgur.com/Ofbgjt3.png) Należy stworzyć nowy plik, wkleić poniższy kod, a następnie wybrać przycisk zębatki i przycisk play (lub łączony). Jeśli program się uruchomił to wszytko ok. Na zajęciach wyjaśnimy sobie strukturę programu ```cpp #include <iostream> using namespace std; int main(){ cout << "Witaj świecie!\n"; return 0; } ``` 2. Dev C++ Link: https://www.bloodshed.net/ Wybieramy: original Dev-C++5 (NIE wersję IDE only) ![](https://i.imgur.com/wxlJHIT.png) Testujemy podobnie jak Code::Blocks z tą różnicą, że przycisk uruchomienia to jeden z tych kolorowych kwadracików u góry. Interesuje nas Compile & Run lub inna podobna opcja. 3. Dowolny inny edytor tekstu/kodu z kompilacją z poziomu linii poleceń Jest to rozwiązanie dla osób które miały już kontakt z programowaniem i chcą spróbować bardziej współczesnych narzędzi. Jeżeli ktoś jest zainteresowany wyborem np. VS Code, a nie wie jak to zrobić, to proszę o kontakt. Wtedy napiszę co należy zrobić. ## Maszyna RAM Elementy interfejsu: * taśma wejściowa * na górze ekranu, tutaj wpisujemy liczby wchodzące **po kolei** * taśma wyjściowa * na dole ekranu, tutaj wypisywane są liczby **po kolei** * kod programu * główna część, w której podajemy listę instrukcji * pamięć * numerowana lista po lewej stronie, każda komórka może przechowywać 1 wartość * procesor * w lewym górnym rogu, służy do pokazywania obecnie przetwarzanej instrukcji * akumulator * komórka 0, służy do wykonywania operacji matematycznych z dwoma argumentami Omówione instrukcje: * read i write * wczytuje wartość z taśmy wejściowej do komórki o numerze danym w argumencie * wypisuje wartość na taśmę wyjściową z komórki o numerze danym w argumencie * load i store * load z numerem komórki wczytuje komórkę do akumulatora * load ze stałą (=liczba) wpisuje liczbę do akumulatora * store zapisuje akumulator do komórki * add, mult, sub, div * wykonują operacje dodawania, mnożenia, odejmowania, dzielenia całkowitego * w dwóch wariantach: * z liczbą w argumencie $\rightarrow$ operacja jest wykonywana na akumulatorze i drugiej komórce * ze stałą $\rightarrow$ operacja wykonywana jest na akumulatorze z pomocą zadanej liczby * jump * program skacze do etykiety podanej w argumencie * jzero (jump if zero) * program skacze do etykiety, jeśli w akumulatorze jest 0, w przeciwnym przypadku wykonuje kolejną instrukcję z listy * jgtz (jump if greater than zero) * program skacze do etykiety, jeśli w akumaltorze, jest wartość >0 Lista 1 Zadanie Bardzo proste wyrażenie Ważnym krokiem, w tym zadaniu, jest wczytanie wartości a i b do komórek, tak by nie stacić ich wartości. Można to wykonać przez instrukcję read 1, read 2, a potem skorzystać z instrukcji load, w celu przyniesienia wartości a lub b do akumulatora. Lista 2 Zadanie Suma ciągu Załączam schemat blokowy podobny do zadania długość ciągu ![](https://i.imgur.com/hRBUCE6.jpg) Lista 3 Zadanie Suma cyfr Bardzo podobne do poprzednich zadań. Najważniejsza obserwacja to, że gdy podzielimy całkowicie liczbę dodatnią przez większą od niej otrzymamy 0. Dla przykładu 6 div 10 = 0 r. 6 => to zero pozwala stwierdzić czy należy zatrzymać program Zamiana liczby na system binarny Sposób 1: Dzielimy liczbę całkowicie przez 2, dopóki nie dostaniemy 0. Reszty z dzielenia dają reprezentację binarną 19 div 2 = 9 r. 1 9 div 2 = 4 r. 1 4 div 2 = 2 r. 0 2 div 2 = 1 r. 0 1 div 2 = 0 r. 1 19 w zapisie binarnym to 10011 Sposób 2 Znajdujemy najmniejszą potęgę dwójki, która nie mieści się w naszej liczbie. Dopóki potęga nie spadnie do 0 to wykonujemy sprawdzenie czy potęga mieści się w analizowanej liczbie. Jeśli tak to odejmujemy ją od analizowanej liczby i otrzymujemy 1 w zapisie, jeśli nie to 0 w zapisie. Potęgę dzielimy na 2 Liczba 19 Potęga 32 Potęga 16 Liczba 19 - 16 = 3, do zapisu trafia 1, mamy 1 Potęga 8 Liczba 3, 3 < 8, więc do zapisu trafia 0, mamy 10 Potęga 4 Liczba 3, zapis 100 Potęga 2 Liczba 3 - 2 = 1, zapis 1001 Potęga 1 Liczba 1 - 1 = 0, zapis 10011 Potęga 0 -> Stop