Design patterns în probleme - clasa IX

Teorema împărțirii cu rest

Fie două numere \(a, b \in \mathbb{Z}, b \neq 0\). Există și sunt unice numerele întregi câtul q și restul r astfel încât să fie satisfăcute simultan condițiile: \(a = b \cdot q + r, r < b\)

În C/C++ se exprimă altfel cu operatori:

    int a, b, q, r;

    q = a / b; /* în pseudocod era a div b. q e garantat întreg dacă a și b sunt int */
    r = a % b; /* în pseudocod era a mod b. valoarea maximă a lui r e b-1 */

Criterii de divizibiliate

Fie două numere \(a, b \in \mathbb{Z}\). Spunem că a este divizibil cu b dacă și numai dacă a % b == 0. Condiția este universal valabilă și nu este nevoie de criteriile de divizibilitate de la matematică. Restul 0 la împărțire ne spune că numerele se împart exact.

Exemplu de problemă: Se citesc n numere de la tastatură. Afișați numerele divizibile cu 5

#include <iostream>

using namespace std;

int main()
{
    int n, i, nr;

    for (i = 0; i < n; i++) {
        cin >> nr;
        if (nr % 5 == 0)
            cout << nr;
    }

    return 0;
}

Observație: este evident că a NU este divizibil cu b dacă a % b != 0, adică restul împărțirii lui a la b este nenul.

Ce înseamnă că numărul natural n este par? Răspuns: n % 2 == 0. Dar impar? n % 2 != 0

Divizorii unui număr

O problemă frecvent întâlnită este determinarea divizorilor unui număr dat. În practică se pot cere diverse operații cu aceștia: afișarea, însumarea, numărarea și diverse altele.

Implementare naivă

Toți divizorii lui n sunt între 1 și n, inclusiv. Putem parcurge numerele din acest interval și verifica dacă restul împărțirii este 0.

    for(d = 1; d <= n; d++)
        if(n % d == 0)
            cout << d << " ";

Optimizare n/2

O primă optimizare pe care o putem observa este că pentru orice n, de la n/2 la n nu mai sunt divizori. În plus excludedm pe și pe 1. Putem astfel să înjumătățim interval în care căutăm divizorii:

    for(d = 2; d <= n/2; d++ )
        if(n % d == 0)
            cout << d << " ";

Optimizare sqrt(n)

Optimizare eficientă din punct de vedere a timpului: Observăm că divizorii oricărui număr n sunt în pereche: dacă d este divizor al lui n, atunci și n/d este divizor al lui n. Atenție dacă n este pătrat perfect. Asta deoarece vom parcurge numerele doar de la 1 la sqrt(n) și aici trebuie să nu afișăm de două ori sqrt(n) care este divizor în acest caz. Programul nostru final devine (și acesta trebuie să îl folosiți mereu în probleme!)

#include <iostream>
#include <cmath>

using namespace std;

int main()
{
    int n, d;
    cin >> n;

    for(d = 1; d <= sqrt(n); d++)
        if(n % d == 0) {
            cout << d << " ";
            if (d < sqrt(n))
                cout << n / d << " ";
        }
    
    return 0;
}

Detalii și exemple pe https://www.pbinfo.ro/articole/72/divizorii-unui-numar. De asemenea revedeți și lecțiile de matematică: Divizibiliatea numerelor naturale

Verificarea că un număr este prim

Un număr n este prim dacă are exact 2 divizori: pe 1 și pe el însuși. Pentru a stabili dacă un număr p este prim avem 3 posibilități:

  • numărăm divizorii săi. Dacă sunt 2 divizori, p este prim.
  • determinăm suma divizorilor. Dacă suma este p + 1, numărul este prim.
  • căutăm divizori ai săi diferiți de 1 și de el însuși. Dacă nu găsim, numărul este prim.

Atenție! Ne folosim de optimizarea discutată mai sus ca să putem face un algoritm eficient. Ca să ne creem algoritmul pentru verificarea dacă un număr n este prim îl vom presupune de la început prim, eliminăm cazurile particulare n = 0, n = 1 și apoi parcurgem intervalul [2, sqrt(n)] și aplicăm unul din cele 3 bullet-uri de mai sus. Pentru a nu cicla mereu până la finalul buclei facem o optimizare folosind instrucțiunea break

#include <iostream>
using namespace std;

int main()
{
    int n;
    cin >> n;
    
    int prim = 1; // presupunem ca n este prim, prim=1 adevarat, prim=0 fals
    int d;

    if(n < 2)
        prim = 0; // 0 si 1 nu sunt prime

    for(d = 2 ; d <= sqrt(n); d++)
        if(n % d == 0) {
            prim = 0;
            break; //iesim din bucla, am gasit deja un divizor clar nu e prim
        }
    
    if (prim)
        cout << n << " este prim";
    else
        cout << n << " nu este prim";

    return 0;
}

CMMDC, CMMMC și Algoritmul lui Euclid

Material foarte bun pe https://www.pbinfo.ro/articole/73/cmmdc-si-cmmmc-algoritmul-lui-euclid

Descompunerea în factori primi

Citirea șirurilor de numere de la tastatură

Se citesc numere pana la 0

Într-o problemă va apărea exprimarea: “Se citesc numere de la tastatură până la apariția lui zero.”

Soluția este pe pași:

  • Citesc primul număr într-o variabilă n
  • Cât timp n este diferit de 0 continui citirea. Pentru că am făcut citirea la pasul anterior variabila n este inițializată
#include <iostream>

using namespace std;

int main()
{
    int n;

    cin >> n;
    /* procesez n conform cerinței */

    while (n != 0) {
        cin >> n;
        /* procesez n conform cerinței */
    }

    return 0;
}

Alternativ: inițializez n cu orice valoare (să zicem 1) și fac citirea în buclă:

#include <iostream>

using namespace std;

int main()
{
    int n = 1;

    while (n != 0) {
        cin >> n;
        /* procesez n conform cerinței */
    }

    return 0;
}

Se citesc n numere

În alte probleme pur și simplu se specifică clar: “Se citesc n numere de la tastatură”. Aici soluția elegantă este să se folosească un for

#include <iostream>

using namespace std;

int main()
{
    int n, i, nr;

    cin >> n;

    for (i = 0; i < n; i++) {
        cin >> nr;
        /* procesez nr conform cerinței */
    }

    return 0;
}

Dar se poate și cu while:

#include <iostream>

using namespace std;

int main()
{
    int n, i = 0;

    cin >> n;

    while (i < n) {
        cin >> nr;
        /* procesez nr conform cerinței */
        i++;
    }
    
    return 0;
}

Se citesc numere pana cand doua consecutive

Se citesc numere de la tastatură până când se introduc două numere consecutive egale. Programul citește de la tastatură numere întregi. Citirea se încheie când se introduc două numere egale.

Vom folosi două variabile prev și crt. Le citim întâi pe amândouă.

Apoi vom citi în mod repetat pe crt (până când e egal cu prev). Procesăm datele prev și crt conform cerinței din problemă, apoi copiem pe crt în prev și citim altă valoare pentru crt

Copierea lui crt în prev este necesară pentru a avea tot timpul acces la două valori citite consecutiv: valoarea curenta crt și valoarea anterioară prev.

#include <iostream>

using namespace std;

int main()
{
    int prev, crt, s = 0;
    
    /* Fac o citire inițială a primelor două numere */
    cin >> prev;
    cin >> crt;   
    
    while (prev != crt) {
        prev = crt;  /* in prev stochez valoarea anterioara */
        cin >> crt; /* citesc valoare noua */
       
       /* procesez prev si crt conform cerintei ulterioare */
    }

    return 0;
}

Atenție: programul este scris cu presupunerea că datele introduse chiar sunt în perechi și că vi se vor citi minim 2 numere. Lucrurile se pot complica dacă nu vi se precizează ce date de intrare veți avea, dar nu sunt obiectul acestui exemplu.

Alt exemplu: Se citesc numere de la tastatură până la apariția lui zero. Să se afișeze câte perechi de elemente citite consecutiv sunt egale:

#include <iostream>

using namespace std;

int main()
{
    int prev, crt = -1, counter = 0;
    
    /* Citesc primul numar */
    cin >> prev;

    while (crt != 0) {
        /* citesc repetetiv */
        cin >> crt;
        
        /* determin dacă sunt o pereche de egale */
        if (crt == prev)
            counter++;
        
        /* imi fac o copie ca sa am acces mereu la doua valori citite consecutiv:
         * valoarea curenta e mereu crt valoarea anterioara e prev
         */
        prev = crt;
    }
    
    cout << counter;

    return 0;
}

Determinare maxim și minim

În multe probleme de informatică suntem în situația ca pentru un șir de numere să determinăm valoarea maximă respectiv cea minimă. Pentru exemplificare datele de intrare sunt un șir de n numere citit de la tastatură cu una din metodele menționate mai sus.

Vă recomand să urmăriți următorul videoclip scurt: Data visualization - Maximum

Există și solutii mai elegante/eficiente, dar pentru clasa a IX-a este general acceptată această metodă:

  • Se declară două variabile max, min. În general este bine max să fie inițializat la cea mai mică valoare posibilă în funcție de contextul problemei iar min să fie inițializat la cea mai mare valoare posibilă. Exemplu: dacă dorim să determinăm cifra minimă și maximă atunci inițializările sunt max = 0, min = 9.
  • Se parcurge șirul de numere cu metoda de citire descrisă mai sus. Fiecare număr citit de la tastatură se compară cu max, min
  • Dacă x < min înseamnă că am găsit o valoare x mai mică decât minimul curent, deci minimul curent devine x: min = x. Similar pentru maxim, doar că invers facem comparția.
#include <iostream>

using namespace std;

int main()
{
    int n, max = -65535, min = 65535;

    cin >> n;

    for (i = 0; i < n; i++) {
        cin >> nr;

        if (nr < min)
            min = nr;

        if (nr > max)
            max = nr;
    }

    cout << "Maximul este: " << max << " si minimul " << min;

    return 0;
}

Vă recomand și articolul similar https://www.pbinfo.ro/articole/79/maximul-minimul-pentru-un-numar-oarecare-de-valori

Calcul de sume și produs

Ne dorim să calculăm: \(\displaystyle S = \sum_{i=1}^{n} i, \ P = \prod_{i=1}^{n} i\). P se mai scrie și P = n! (factorial)

Ce este important la sume/produse: trebuie să ne inițializăm variabila în care stocăm rezultatul final (cu 0 la sume, cu 1 la produse).

#include <iostream>

using namespace std;

int main()
{
    int n, i, s = 0, p = 1;

    cin >> n;

    for (i = 1; i <= n; i++) {
        s = s + i;
        p = p * i;
    }

    cout << "Suma este: " << s << " si produsul " << p;

    return 0;
}

Întotdeauna este important când vi se dă o sumă de calculat (sau un produs) să scrieți forma prescurtată. Pe baza ei veți putea scrie algoritmul. De exemplu: dacă vi se cere să calculați suma:

\[S = \frac{1}{1 \cdot 3} + \frac{1}{2 \cdot 4} + \ldots + \frac{1}{n(n+2)}\]

Îi scriem forma prescurtată: \(\displaystyle S = \sum_{i=1}^{n} \frac{1}{k(k+2)}\) și atunci algoritmul nostru devine:

#include <iostream>

using namespace std;

int main()
{
    int n, k;
    double s;

    cin >> n;

    for (k = 1; k <= n; k++) {
        s = s + 1.0 / (k * (k + 2));
    }

    cout << "Suma este: " << s << " si produsul " << p;

    return 0;
}

Descompunerea unui număr în cifre

Teorie

Un număr natural de trei cifre se scrie sub forma \(\overline{abc}\). Descompus, acest număr se poate scrie \(\overline{abc} = 100 \cdot a + 10 \cdot b + c\). Observăm că c < 10, b < 10, a < 10 deci ne putem folosi de operatorul modulo pentru a obține cifrele.

Întotdeauna ultima cifră a unui număr natural n este n % 10. Cum ajung la următoarea cifră? Mă folosesc de descompunerea menționată mai sus și cer câtul împărțirii lui n la 10 pentru a elimina ultima cifră. Practic pe relația \(n = \overline{abc} = 100 \cdot a + 10 \cdot b + c\) avem:

  • c = n % 10
  • b = (n / 10) % 10
  • a = ((n / 10) / 10) % 10 = (n / 100) % 10

Când ne oprim? Atunci când câtul împărțirii la 10 este 0. Practic am ajuns la ultima cifră (cunoscută și sub numele de cea mai semnificativă cifră).

Exemplu

Exemplu de problemă: Se citește numărul n. Afișați toate cifrele lui

#include <iostream>

using namespace std;

int main()
{
    int n, c;

    cin >> n;

    while (n != 0) {
        c = n % 10;
        cout << c << ", ";
        n = n / 10;
    }

    return 0;
}

Palindrom

Observație importantă: folosind algoritmul de mai sus pentru un număr \(n = \overline{abc}\) noi vom afișa cifrele lui în ordine inversă \(b, c, a\). De ce este importantă? Există multe probleme care cer să verificați dacă un număr natural n este palindrom. Palindromul este un număr care e același indiferent că citim de la stânga la dreapta sau dreapta la stânga. Exemplu: 123 nu este palindrom, dar 12321 este palindrom. Cum scriem algoritmul să verificăm dacă un număr e palindrom? Ne folosim din nou de \(n = \overline{abc}\) și încercăm să compunem \(s = \overline{cba}\) și la final comparăm cele două numere: dacă sunt egale e palindrom.

Încă o observație importantă: pe programul de mai sus valoarea lui n la ieșirea din buclă este 0 deci noi pierdem valoarea citită. Trebuie să facem o copie pentru aceasta.

#include <iostream>

using namespace std;

int main()
{
    int n, c, s = 0, cn;

    cin >> n;

    /* Fac o copie pentru n */
    cn = n;

    while (n != 0) {
        c = n % 10;
        s = (s * 10) + c;
        n = n / 10;
    }

    /* Aici n e 0. Dar am copia numărului citit în cn. Compar cn cu s */
    if (cn == s)
        cout << "este palindrom";
    else
        cout << "nu este palindrom";

    return 0;
}

Vă recomand și articolul https://www.pbinfo.ro/articole/65/cifrele-unui-numar

Șirul lui Fibonacci

Fie șirul \((f_n)_{n \ge 3}\) pentru care se definește relația de recurență \(f_n = f_{n-1} + f_{n-2}\) având valorile inițiale \(f_1 = 1, f_2 = 1\). Numerele Fibonacci sunt numere naturale care fac parte din următorul șir, în care fiecare număr este egal cu suma celor două de dinainte: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

Algoritmul pentru a determina primii n termeni din șirul lui Fibonacci este: folosim trei variabile a,b,c. Două dintre ele reprezintă termenii anteriori \(f_{n-1}, f_{n-2}\) și a treia reprezintă termenul curent \(f_n\):

#include <iostream>

int main()
{
    int a = 1, b = 1, c, i;
    cout << a << ", " << b;

    for (i = 3; i <= n; i++) {
        c = a + b;
        cout << c << ", ";
        a = b;
        b = c;
    }
}

Baze de numerație