Table of Contents

Laborator 01: Divide et Impera

Obiective laborator

Precizări inițiale

Toate exemplele de cod se găsesc pe pagina pa-lab::demo/lab01.

Exemplele de cod apar încorporate și în textul laboratorului pentru a facilita parcurgerea cursivă a acestuia. ATENȚIE! Varianta actualizată a acestor exemple se găsește întotdeauna pe GitHub.

Importanţă – aplicaţii practice

Paradigma Divide et Impera stă la baza construirii de algoritmi eficienți pentru diverse probleme:

Un alt domeniu de utilizare a tehnicii divide et impera este programarea paralelă pe mai multe procesoare, sub-problemele fiind executate pe mașini diferite.

Prezentarea generală a problemei

O descriere a tehnicii D&I: “Divide and Conquer algorithms break the problem into several sub-problems that are similar to the original problem but smaller in size, solve the sub-problems recursively, and then combine these solutions to create a solution to the original problem.”

Deci un algoritm D&I împarte problema în mai multe subprobleme similare cu problema inițială şi de dimensiuni mai mici, rezolvă subproblemele recursiv şi apoi combină soluțiile obţinute pentru a obține soluția problemei inițiale.

Sunt trei pași pentru aplicarea algoritmului D&I:

Complexitatea algoritmilor D&I se calculează după formula:

$T(n) = D(n) + S(n) + C(n)$,

unde $D(n)$, $S(n)$ şi $C(n)$ reprezintă complexitățile celor 3 pași descriși mai sus: divide, stăpânește, respectiv combină.

Probleme clasice

MergeSort

Enunț

Sortarea prin interclasare (MergeSort [1]) este un algoritm de sortare de vectori ce folosește paradigma D&I:

Pseudocod

Mai jos găsiți algoritmul MergeSort scris în pseudocod.

Pseudocod

Pseudocod

MergeSort(v, start, end)		// v – vector, start – limită inferioră, end – limită superioară
	if (start == end) return;	// condiția de oprire
	mid = (start + end) / 2;	// etapa divide
	MergeSort(v, start, mid);	// etapa stăpânește
	MergeSort(v, mid + 1, end);
	Merge(v, start, end);		// etapa combină
 
Merge(v, start, end)			// interclasare sub-vectori
	// indecsi
        mid = (start + end) / 2;
	i = start;
	j = mid + 1;
	k = 1;
 
        tmp = buffer temporar in care incap (end - start + 1) numere
	while (i <= mid && j <= end) 
		if (v[i] <= v[j]) tmp[k++] = v[i++];
		             else tmp[k++] = v[j++];
 
	while (i <= mid) 
		tmp[k++] = v[i++];
 
	while (j <= end) 
		tmp[k++] = v[j++];
 
	copy(v[start..end], tmp[1..k-1]);

Implementare in C++

Implementare in C++

Mai jos puteti găsi o implementare în C++.

#include <bits/stdc++.h>
using namespace std;
 
vector<int> v;
 
void merge_halves(int left, int right) {
	int mid = (left + right) / 2;
	vector<int> aux;
	int i = left, j = mid + 1;
 
        while (i <= mid && j <= right) {
		if (v[i] <= v[j]) {
			aux.push_back(v[i]);
			i++;
		} else {
			aux.push_back(v[j]);
			j++;
		}
	}
 
        while (i <= mid) {
		aux.push_back(v[i]);
		i++;
	}
 
        while (j <= right) {
		aux.push_back(v[j]);
		j++;
	}
 
	for (int k = left; k <= right; k++) {
		v[k] = aux[k - left];
	}
}
 
 
void merge_sort(int left, int right) {
	if (left >= right) return ;
	int mid = (left + right) / 2;
 
        merge_sort(left, mid);
	merge_sort(mid + 1, right);
	merge_halves(left, right);
}
 
int main() {
	random_device rd;
	for (int i = 0; i < 10; i++) {
		v.push_back(rd() % 100);
	}
 
	cout << "Vectorul initial: ";
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << "\n";
 
	merge_sort(0, v.size() - 1);
 
	cout << "Vectorul sortat: ";
	for (int i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << "\n";
 
	return 0;
}
Complexitate

Complexitatea algoritmului este dată de formula: $T(n) = D(n) + S(n) + C(n)$, unde $D(n)=O(1)$, $S(n) = 2 * T(n / 2)$ și $C(n) = O(n)$, rezulta $T(n) = 2 * T(n / 2) + O(n)$.

Folosind teorema Master [8] găsim complexitatea algoritmului: $T(n) = O(n * log(n))$.

Rețineți cele două convenții despre complexități de mai sus. Le vom folosi pentru restul semestrului.

Enunț

Se dă un vector sortat crescător ($v[1]$, $v[2]$, …, $v[n]$) ce conține valori reale distincte și o valoare x.

Să se găsească la ce poziție apare x în vectorul dat.

Rezolvare

BinarySearch (Căutare Binară), se rezolva cu un algoritm D&I:

Pseudocod

BinarySearch(v, left, right, x) {             // functia returneaza pozitia x pe care se afla numarul x (oricare pozitie) sa 
       // vom cauta cat timp intervalul de cautare nu a fost inca epuizat (are cel putin un element)	
       while (left <= right) {
            mid = (left + right) / 2          // mijlocul intervalului de cautare
 
            if (x == v[mid]) return mid;      // elementul cautat este cel din mijloc
            if (x < v[mid])  right = mid - 1; // elementul cautat este mai mic decat cel din mijloc, ne mutam in prima jumatate
            if (x > v[mid])  left  = mid + 1; // elementul cautat este mai mare decat cel din mijloc, ne mutam in a doua jumatate
        }
 
        return -1;                            // in acest punct ajungem daca si numai daca x nu a fost gasit
}

Complexitate

Turnurile din Hanoi

Enunț

Se consideră 3 tije $S$ (sursa), $D$ (destinatie), $aux$ (auxiliar) şi n discuri de dimensiuni distincte ($1, 2, ..., n$ - ordinea crescătoare a dimensiunilor) situate inițial toate pe tija $S$ în ordinea $1,2,...,n$ (de la vârf către baza).

Singura operație care se poate efectua este de a selecta un disc ce se află în vârful unei tije şi plasarea lui în vârful altei tije astfel încât să fie așezat deasupra unui disc de dimensiune mai mare decât a sa.

Să se găsească un algoritm prin care se mută toate discurile de pe tija $S$ pe tija $D$.(problema turnurilor din Hanoi).

Soluție

Pentru rezolvarea problemei folosim următoarea strategie [9]:

Ideea din spate este că avem mereu o singura sursă si o singură destinație să atingem un scop. Întotdeauna a 3-a tija va fi considerată auxiliară și poate fi folosită pentru a atinge scopul propus.

Algoritm

// muta n discuri de pe tija S pe tija D folosind tija aux
Hanoi(n, S, D, aux)  {
    if (n >= 1) {
        Hanoi(n - 1, S, aux, D);   // mut n-1 discuri de pe sursa (S) pe auxiliar (aux)
                                   // in aceasta subproblema sursa este S, destinatia este aux, intermediarul este D
 
        Muta_disc(S, D);           // acum pot muta direct discul n de pe sursa (S) pe destinatie (D)
 
        Hanoi(n - 1, aux, D, S);   // mut n-1 discuri de pe sursa (aux, aici sunt ele momentan) pe destinatie (D - scop final)
                                   // in aceasta subproblema, S este auxiliar, intrucat este tija libera
    }
}

Complexitate

ZParcurgere

Enunț

Gigel are o tablă pătratică de dimensiuni $2^n * 2^n$. Ar vrea să scrie pe pătrățelele tablei numere naturale cuprinse între $1$ si $2^n * 2^n$ conform unei parcurgeri mai deosebite pe care o numește Z-parcurgere.

O Z-parcurgere vizitează recursiv cele patru cadrane ale tablei în ordinea: stânga-sus, dreapta-sus, stânga-jos, dreapta-jos.

La un moment dat, Gigel ar vrea să știe ce număr de ordine trebuie să scrie conform Z-parcurgerii pe anumite pătrațele date prin coordonatele lor $( x, y )$. Gigel incepe umplerea tablei întotdeauna din colțul din stânga-sus.

Exemplu 1

Exemplu 1

$n = 1$ si $(x, y) = (2, 2)$

Răspuns: $4$

Explicație: Matricea arată ca în exemplul următor.

12
34

Exemplu 2

Exemplu 2

$n = 2$ si $(x, y) = (3, 3)$

Răspuns: $13$

Explicație: Matricea arată ca în exemplul următor.

1256
3478
9101314
11121516

Soluție

Analizând modul în care se completează tabloul/matricea din enunț, observăm că la fiecare etapa împărțim matricea (problema) în 4 submatrici (4 subprobleme). De asemenea, șirul de numere pe care dorim să il punem în matrice se împarte în 4 secvente, fiecare corespunzând unei submatrici.

Observăm astfel că problema suportă descompunerea în subprobleme disjuncte și cu structura similara, ceea ce ne face să ne gândim la o soluție cu Divide et Impera.

Complexitate

Concluzii

Divide et impera este o tehnică folosită pentru a realiza soluții pentru o anumita clasă de probleme: acestea contin subprobleme disjuncte și cu structură similară. În cadrul acestei tehnici se disting trei etape: divide, stăpânește și combină.

Mai multe exemple de algoritmi care folosesc tehnica divide et impera puteți găsi la [11].

Exerciții

Scheletul de laborator se găsește pe pagina pa-lab::skel/lab01.

Count occurrences

Se dă un șir sortat v cu n elemente. Găsiți numărul de elemente egale cu x din șir.

Exemplu 1

Exemplu 1

$n = 6$ si $x = 10$

i123456
v124101020

Răspuns: $2$

Explicație: 10 apare de 2 ori in sir.

Task-uri:

Solutie C++

Solutie C++

Sursa main.cpp asociata cu task 1 este mai jos.

#include <bits/stdc++.h>
using namespace std;
 
class Task {
public:
    void solve() {
        read_input();
        print_output(get_result());
    }
 
private:
    int n, x;
    vector<int> v;
 
    void read_input() {
        ifstream fin("in");
        fin >> n;
        for (int i = 0, e; i < n; i++) {
            fin >> e;
            v.push_back(e);
        }
        fin >> x;
        fin.close();
    }
 
    int find_first() {
        int left = 0, right = n - 1, mid, res = -1;
        while (left <= right) {
            mid = (left + right) / 2;
            if (v[mid] >= x) {
                res = mid;
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return (res != -1 && v[res] == x) ? res : -1;
    }
 
    int find_last() {
        int left = 0, right = n - 1, mid, res = -1;
        while (left <= right) {
            mid = (left + right) / 2;
            if (v[mid] <= x) {
                res = mid;
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return (res != -1 && v[res] == x) ? res : -1;
    }
 
    int get_result() {
        int first = find_first();
        int last = find_last();
        if (first == -1 || last == -1) {
            return 0;
        }
        return last - first + 1;
    }
 
    void print_output(int result) {
        ofstream fout("out");
        fout << result;
        fout.close();
    }
};
 
// [ATENTIE] NU modifica functia main!
int main() {
    // * se aloca un obiect Task pe heap
    // (se presupune ca e prea mare pentru a fi alocat pe stiva)
    // * se apeleaza metoda solve()
    // (citire, rezolvare, printare)
    // * se distruge obiectul si se elibereaza memoria
    auto* task = new (std::nothrow) Task{}; // hint: cppreference/nothrow
    if (!task) {
        std::cerr << "new failed\n";
        return -1;
    }
    task->solve();
    delete task;
    return 0;
}

Solutie Java

Solutie Java

Sursa Main.java asociata cu task 1 este mai jos.

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Scanner;
 
public class Main {
	static class Task {
		public final static String INPUT_FILE = "in";
		public final static String OUTPUT_FILE = "out";
 
		int n;
		int[] v;
		int x;
 
		private void readInput() {
			try {
				Scanner sc = new Scanner(new File(INPUT_FILE));
				n = sc.nextInt();
				v = new int[n];
				for (int i = 0; i < n; i++) {
					v[i] = sc.nextInt();
				}
				x = sc.nextInt();
				sc.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
 
		private void writeOutput(int count) {
			try {
				PrintWriter pw = new PrintWriter(new File(OUTPUT_FILE));
				pw.printf("%d\n", count);
				pw.close();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
 
		private int findFirst() {
			int left = 0, right = n - 1, mid, res = -1;
			while (left <= right) {
				mid = (left + right) / 2;
				if (v[mid] >= x) {
					res = mid;
					right = mid - 1;
				} else {
					left = mid + 1;
				}
			}
			return (res != -1 && v[res] == x) ? res : -1;
		}
 
		private int findLast() {
			int left = 0, right = n - 1, mid, res = -1;
			while (left <= right) {
				mid = (left + right) / 2;
				if (v[mid] <= x) {
					res = mid;
					left = mid + 1;
				} else {
					right = mid - 1;
				}
			}
			return (res != -1 && v[res] == x) ? res : -1;
		}
 
		private int getAnswer() {
			int first = findFirst();
			int last = findLast();
			if (first == -1 || last == -1) {
				return 0;
			}
			return last - first + 1;
		}
 
		public void solve() {
			readInput();
			writeOutput(getAnswer());
		}
	}
 
	public static void main(String[] args) {
		new Task().solve();
	}
}

SQRT

Se dă un număr real n. Scrieți un algoritm de complexitate O(log n) care să calculeze $sqrt(n)$ cu o precizie de $0.001$.

Exemplu 1

Exemplu 1

$ n = 0.25 $

Răspuns: orice valoare intre $0.499$ si $0.501$ (inclusiv)

Pentru a putea trece testele, trebuie sa afișați rezultatul cu cel puțin 4 zecimale.

ZParcurgere

Rezolvați problema ZParcurgere folosind scheletul pus la dispoziție. Enunțul și explicațiile le găsiți în partea de seminar.

Exponențiere logaritmică

Se dau două numere naturale base și exponent. Scrieți un algoritm de complexitate $O(log (exponent))$ care să calculeze ${base} ^ {exponent} \ \% \ MOD $.

Întrucât expresia $ {base} ^ {exponent} $ este foarte mare, dorim să aflăm doar restul împărțirii lui la un număr MOD.

Proprietăți matematice necesare:

  • $(a + b) \ \% \ MOD = ((a \ \% \ MOD) + (b \ \% \ MOD)) \ \% \ MOD $
  • $(a \ * b) \ \% \ MOD = ((a \ \% \ MOD) \ * (b \ \% \ MOD)) \ \% \ MOD $

Atenție la înmulțire! Rezultatul temporar poate provoca un overflow.

Soluții:

  • C++: $a * b$ ⇒ $1LL * a * b$
  • Java: $a * b$ ⇒ $1L * a * b$

Exemplu 1

Exemplu 1

$ base = 2 $, $ exponent = 10$, $MOD = 5$

Răspuns: $4$

Explicatie: $2^{10} \ \% \ 5 = 4$

Bonus

Inversiuni

Inversiuni

Se da un sir $S$ de n numere intregi. Sa se detemine cate inversiuni sunt in sirul dat. Numim inversiune o pereche de indici $1 < = i < j < = n$ astfel incat $S[i] > S[j]$

Exemplu: in sirul {0 1 9 4 5 7 6 8 2} sunt 12 inversiuni.

Aceasta problema nu are schelet, dar poate fi testată pe infoarena, la problema Inv.

ClassicTask

ClassicTask

Testați soluția voastră de la Exponentiere logaritmica pe infoarena, la problema ClassicTask (trebuie să modificați numele fișierelor).

Identificați problema și modificați sursa astfel încât să luați punctaj maxim.

Extra

Statistici de ordine

Statistici de ordine

Se dă un vector de numere întregi neordonate. Scriind o funcție de partitionare, folosiți Divide et Impera pentru

  • a determina a k-lea element ca mărime din vector
  • a sorta vectorii prin QuickSort

Puteti testa problema partition aici. Problema QuickSort (chiar si MergeSort) poate fi testata aici.

Exemplu: pentru vectorul {0 1 2 4 5 7 6 8 9}, al 3-lea element ca ordine este 2, iar vectorul sortat este {0 1 2 4 5 6 7 8 9}

Missing number

Missing number

Se dau $n - 1$ numere naturale distincte intre $0$ si $n - 1$. Scriind o functie de partitionare, determinati numarul lipsa.

Exemplu: pentru $n = 9$ si vectorul ${0 1 9 4 5 7 6 8 2}$, numarul lipsa este $3$.

Fractal

Fractal

Puteți rezolva această problemă pe infoarena.

Hanoi

Hanoi

Puteți rezolva această problemă pe infoarena.

Secventa descrescatoare

Secventa descrescatoare

Dându-se N numere întregi sub forma unei secvenţe de numere strict crescătoare, care se continuă cu o secvenţă de întregi strict descrescătoare, se doreşte determinarea punctului din întregul şir înaintea căruia toate elementele sunt strict crescătoare, şi dupa care, toate elementele sunt strict descrescătoare. Considerăm evident faptul că acest punct nu există dacă cele N numere sunt dispuse într-un şir fie doar strict crescător, fie doar strict descrescător.

K closest points to origin

K closest points to origin

Puteți rezolva această problemă pe leetcode

Merge k Sorted Lists

Merge k Sorted Lists

Puteți rezolva această problemă pe leetcode

Referințe

[0] Chapter Divide-and-Conquer, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein