BEKTREKING I GRUBA SILA- PRIMERI
Brute Force (gruba sila)
Brute force je najjednostavniji način rešavanja problema: pokušavamo sve moguće opcije ili kombinacije dok ne pronađemo rešenje. To je kao da koristimo silu da obavimo posao – ne pokušavamo nijedan specifičan pametni trik, nego testiramo svaki mogući put. Ovakav pristup je lak za razumevanje i programiranje, jer ne zahteva komplikovane algoritamske tehnike. Brute force koristimo kada imamo mali broj mogućih rešenja ili kada nam nije važna brzina programa. Na primer, ako imamo samo nekoliko mogućih kombinacija, možemo proveriti svaku od njih.
Međutim, kod većih problema brute force može postati veoma spor. Broj mogućih kombinacija brzo raste kako se veličina problema povećava, pa isprobavanje svake opcije postaje neefikasno. Brute force tada troši mnogo vremena na nepotrebne provere.
Karakteristike brute force metode:
- Proverava sve moguće kombinacije (ili rezultate) jednog problema.
- Jednostavna je za razumevanje i implementaciju.
- Dobro radi za mali broj kombinacija, ali postaje neučinkovita za veće probleme.
Primer brute force (C++)
Na primer, da pronađemo sve parove brojeva u nizu čiji zbir je jednak određenoj vrednosti (target), možemo koristiti brute force:
int arr[] = {1, 2, 3, 4};
int n = 4;
int target = 5;
// find pairs whose sum equals target
for (int i = 0; i < n; i++) {
for (int j = i+1; j < n; j++) {
if (arr[i] + arr[j] == target) {
cout << arr[i] << " + " << arr[j] << " = " << target << endl;
}
}
}
Backtracking
Backtracking je naprednija metoda pretrage rešenja koja gradi moguća rešenja korak po korak. Ideja je da u svakom koraku biramo jedan element i rekurzivno nastavimo dalje. Ako u nekom trenutku shvatimo da trenutna kombinacija ne može da dovede do rešenja (npr. neki uslov nije zadovoljen), pravimo korak unazad i menjamo izbor. Na taj način izbacujemo čitave grane pretrage za koje znamo da neće dati rešenje. Ovo znatno smanjuje broj provera u odnosu na običnu brute force pretragu.
Backtracking se često koristi kod kombinatornih problema ili slagalica sa dodatnim uslovima (npr. Sudoku, N-queens). Omogućava da brzo odbacimo nepotrebne puteve i da efikasno nađemo sva rešenja. Mana je što implementacija zahteva rekurziju i može biti složenija od prostog brute force pristupa, ali značajno ubrzava pretragu kod problema sa velikim brojem mogućnosti.
Karakteristike backtracking metode:
- Koristi rekurziju i vraćanje koraka (backtracking) kada neka kombinacija ne daje rešenje.
- Odbacuje nepotrebne puteve kako bi bila efikasnija.
- Efikasniji je od čistog brute force-a kod problema sa mnogo kombinacija, ali je kod složeniji za pisanje.
Primer backtracking (C++)
Jednostavan primer backtracking-a je generisanje svih podskupova skupa brojeva:
vector arr = {1, 2, 3};
vector current;
generateSubsets(0, arr, current); // start recursion
void generateSubsets(int idx, vector& arr, vector& current) {
if (idx == arr.size()) {
for (int x : current)
cout << x << " ";
cout << endl;
return;
}
// skip arr[idx]
generateSubsets(idx + 1, arr, current);
// take arr[idx]
current.push_back(arr[idx]);
generateSubsets(idx + 1, arr, current);
current.pop_back();
}
Brute Force vs Backtracking
I brute force i backtracking služe za pretragu svih mogućih kombinacija rešenja, ali to rade na različite načine. Brute force doslovno isprobava svaki mogući slučaj bez ikakve optimizacije. To ga čini najjednostavnijim za implementaciju, ali brzo gubi efikasnost kada se broj kombinacija poveća. Nasuprot tome, backtracking pametno prekida nepotrebne puteve čim shvati da ne vode ka rešenju. To ga čini efikasnijim za probleme sa dodatnim uslovima i velikim brojem kombinacija, mada zahteva rekurziju u implementaciji.
U praksi, brute force se koristi kada nam je važna jednostavnost rešenja ili kada je broj slučajeva stvarno mali. Backtracking koristimo kada želimo da ubrzamo pretragu velikog prostora rešenja i možemo lako da detektujemo neuspešne puteve. Na primer, kod rešavanja sudoku slagalice, backtracking će se vratiti na prethodni korak čim detektuje da neki broj ne može da se postavi, dok bi brute force nastavio da isprobava sve druge kombinacije bez prekida.
- Brute force: Jednostavna implementacija, isprobava sve mogućnosti, ali može biti veoma spora za veće probleme.
- Backtracking: Rekurzivna metoda koja odbacuje nepotrebne puteve, efikasnija za kombinatorne probleme sa puno uslova, ali kod je složeniji za pisanje.
1. Put kroz lavirint
Napisati program koji ispituje da li se u pravougaonom lavirintu može stići od gornjeg levog do donjeg desnog ugla.
Ulaz:
Sa standardnog ulaza se učitavaju dimenzije lavirinta m
i n
razdvojene razmakom,
a zatim m
redova po n
znakova. Polja kroz koja se može proći su označena
karakterom .
, a prepreke karakterom x
.
Izlaz:
Ispisati da
ako put postoji, odnosno ne
ako put ne postoji.
Primer 1:
Ulaz 8 8 .x.....x .x.x.x.x .x.x.x.x .x.x.x.x .x.x.x.x .x.x.x.x .x.x.x.x ...x.x.. Izlaz da
Primer 2:
Ulaz 8 8 .x..x..x .x.x.x.x .x.x.x.x .x.x.x.x .x...x.x .x.x.x.x .x.x.x.x ...x.x.. Izlaz ne
Uputstvo za rešavanje:
- Pročitaj
m
in
, pa magistruj matricugrid
dimenzijam×n
. - Napraviti pomoćnu matricu
visited[m][n]
inicijalno safalse
. - Implementirati rekurzivnu funkciju
bool dfs(int i, int j)
koja:- Proverava da li su
(i,j)
unutar granica i da jegrid[i][j]=='.'
i da nije već posećeno. - Ako je
i==m-1 && j==n-1
, vraćatrue
(stigli smo do cilja). - Obeležava
visited[i][j]=true
i rekurzivno poziva samu sebe za četiri susedna polja (gore, dole, levo, desno). - Ako bilo koji poziv vrati
true
, propagiratrue
nagore. - Inače vraća
false
.
- Proverava da li su
- U
main()
pozvatidfs(0,0)
i na osnovu rezultata ispisatida
iline
.
Rešenje 1: jednostavan DFS
#include <bits/stdc++.h>
using namespace std;
int m, n;
vector<string> grid;
vector<vector<bool>> visited;
// Pomoćna funkcija: proverava da li postoji put od (i,j) do (m-1,n-1)
bool dfs(int i, int j) {
if (i < 0 || i >= m || j < 0 || j >= n)
return false;
if (grid[i][j] == 'x' || visited[i][j])
return false;
if (i == m-1 && j == n-1)
return true;
visited[i][j] = true;
if (dfs(i+1, j)) return true;
if (dfs(i-1, j)) return true;
if (dfs(i, j+1)) return true;
if (dfs(i, j-1)) return true;
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
grid.resize(m);
for (int i = 0; i < m; i++)
cin >> grid[i];
visited.assign(m, vector<bool>(n, false));
bool postoji = dfs(0, 0);
cout << (postoji ? "da" : "ne") << endl;
return 0;
}
Rešenje 2: DFS sa backtracking-om
Ovaj primer vraća posećenost na false
nakon ispitivanja svih puteva iz ćelije, kako bi se videlo i alternativne puteve.
#include <bits/stdc++.h>
using namespace std;
// Dimenzije lavirinta
int m, n;
// Sama mapa lavirinta
vector<string> lavirint;
// Matrica posećenosti
vector<vector<bool>> poseceno;
// Pravci kretanja: enum za četiri smera (narandzasta)
int di[4] = {-1, 1, 0, 0};
int dj[4] = {0, 0, -1, 1};
bool dfs(int i, int j) {
if (i < 0 || i >= m || j < 0 || j >= n)
return false;
if (lavirint[i][j] == 'x' || poseceno[i][j])
return false;
if (i == m-1 && j == n-1)
return true;
poseceno[i][j] = true;
for (int dir = 0; dir < 4; dir++) {
int ni = i + di[dir];
int nj = j + dj[dir];
if (dfs(ni, nj))
return true;
}
poseceno[i][j] = false;
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
lavirint.resize(m);
for (int i = 0; i < m; i++)
cin >> lavirint[i];
poseceno.assign(m, vector<bool>(n, false));
bool postojiPut = dfs(0, 0);
cout << (postojiPut ? "da\n" : "ne\n");
return 0;
}
Objašnjenje
dfs(i,j) prvo proveri da li je (i,j) validna i prolazna ćelija. Ako jeste i ako je to cilj (m-1,n-1), vraća true.
Zatim obeleži tu ćeliju kao posećenu i rekurzivno pozove sebe za susedne ćelije. Ako neki od tih poziva vrati true, odmah propagira true nadole. Ako nijedan ne uspe, vrši backtracking (može da poništi oznaku posećeno) i vraća false.
Ovako implementirana DFS koristi princip backtrackinga da proba sve moguće puteve sve dok ne nađe uspešan, ili obidje ceo prostor i zaključi da puta nema.
2. Stepenasti nizovi
Napisati program koji ispisuje sve stepenaste nizove dužine n
u leksikografskom poretku.
Niz je stepenast ako počinje brojem 1
i svaki naredni element niza je ili jednak prethodnom ili za jedan veći od njega.
Ulaz:
Sa standardnog ulaza unosi se jedan ceo broj n
(1 ≤ n ≤ 18).
Izlaz:
Ispisati sve stepenaste nizove dužine n
, svaki u posebnom redu, elemente razdvojiti razmakom.
Primer:
Ulaz 3 Izlaz 1 1 1 1 1 2 1 2 2 1 2 3
3. Redosledi koraka robota
Robot je iz početnog položaja napravio a
koraka nadesno i b
koraka naviše (gledano na mapi),
ali ne znamo kojim redom. Napisati program koji ispisuje sve moguće redoslede koraka robota.
Ako postoji više od 1000 različitih redosleda, ispisati samo prvih 1000.
Ulaz:
U prvom i jedinom redu se nalaze dva cela nenegativna broja, razdvojena jednim razmakom:
a
(i broj koraka DESNO) i b
(i broj koraka GORE), oba ≤ 100.
Izlaz:
Ispisati tražene redoslede koraka, svaki u zasebnom redu.
Svaki redosled prikazati kao niz reči DESNO i GORE, razdvojenih razmakom,
u leksikografskom poretku (DESNO
< GORE
).
Primer 1:
Ulaz 3 2 Izlaz DESNO DESNO DESNO GORE GORE DESNO DESNO GORE DESNO GORE DESNO DESNO GORE GORE DESNO DESNO GORE DESNO DESNO GORE DESNO GORE DESNO GORE DESNO DESNO GORE GORE DESNO DESNO GORE DESNO DESNO DESNO GORE GORE DESNO DESNO GORE DESNO GORE DESNO GORE DESNO DESNO GORE GORE DESNO DESNO DESNO
Primer 2:
Ulaz 0 6 Izlaz GORE GORE GORE GORE GORE GORE
4. Binarni nizovi bez previše ponavljanja
Binarni niz je dosadan ako se u njemu pojavljuje više od k
istih cifara uzastopno.
Niz je interesantan ako nije dosadan.
Napisati program koji ispisuje sve interesantne binarne nizove dužine n
u leksikografskom poretku.
Ulaz:
Jedan red sa dva broja n
i k
(1 ≤ n ≤ 20, 1 ≤ k ≤ n).
Izlaz:
Sve interesantne binarne nizove dužine n
, svaki u posebnom redu, bez razmaka između cifara.
Primer:
Ulaz 3 2 Izlaz 001 010 011 100 101 110
5. Nizovi ispravno uparenih zagrada
Napisati program koji ispisuje sve nizove korektno uparenih zagrada, dužine n
i dubine najviše d
,
u leksikografskom rastućem poretku.
Ulaz:
U prvom redu standardnog ulaza je paran broj n
(1≤n≤30), a u drugom prirodan broj d
(1≤d≤7).
Zadata su tako da ukupno ima manje od 2000 rešenja.
Izlaz:
Ispisati sve tražene nizove zagrada u leksikografskom rastućem poretku, svaki u zasebnom redu.
Primer:
Ulaz 6 2 Izlaz (()()) (())() ()(()) ()()()