This presentation is an adaptation of material from Antoni Lozano and Conrado Martinez
Before we start: Questions on previous material?
In many computational problems we must exhaustively explore a solution space.
Many times it is the only way we know to solve the problem at hand.
For instance,
Sometimes, the only way to solve a problem is to try all possible combinations. This trivial algorithm is called brute force or exhaustive search:
it can be practical for small input sizes
it can benefit from other techniques such as divide-and-conquer
usually is very slow (exponential, or worse). Still better than not having no algorithm at all. Do you know some computational problem for which there is no algorithm at all?
BFS and DFS are also exhaustive searches, they run in polynomial time because the graph to explore is given explicitly as part of the input.
Input:
a natural number n
Output:
all sequences of 0s and 1s of length exactly n
What is the size of the output?
#include<iostream>
#include<vector>
using namespace std;
void binary(vector<int>& A, int k) {
if (k == A.size()){ // base case
for(int j = 0; j < k; ++j) cout << A[j];
cout << endl;
return;
}
// inductive case
A[k] = 0;
binary(A,k+1);
A[k] = 1;
binary(A,k+1);
}
int main(){
int n;
cin >> n;
vector<int> A(n);
binary(A,0);
}
void backtracking(int k, ...other parameters...){
if (k == base_case){// base case of the recursion
...
return;
}
// inductive case
...
}
true
or false
and it is not done explicitly with a for loop)
Input:
two natural numbers n and \ell with \ell\leq n
Output:
all sequences of 0s and 1s of length n with exactly \ell ones
What is the size of the ouput?
#include<iostream>
#include<vector>
using namespace std;
void strings(vector<int>& A, int k, int l) {
if (k == A.size()) {
int c = 0;
for (int x : A) c += x; // Counts 1s
if (c == l){
for(int j = 0; j < k; ++j) cout << A[j];
cout << endl;
}
return;
}
A[k] = 0; strings(A, k+1, l);
A[k] = 1; strings(A, k+1, l);
}
int main(){
int n, l;
cin >> n >> l;
vector<int> A(n);
strings(A,0,l);
}
A
: partial string (size n)k
: first non-filled cell in Al
: total number of 1s we want
Do we really need to count from scratch the number of 1’s in A
every time?
We could keep, at every moment, the number of 1s of the partial string.
This forces us to use one more parameter in the procedure.
#include<iostream>
#include<vector>
using namespace std;
void strings2(vector<int>& A, int k, int u, int l) {
if (k == A.size()) {
if (u == l){
for(int j = 0; j < k; ++j) cout << A[j];
cout << endl;
}
return;
}
A[k] = 0; strings2(A, k+1, u, l);
A[k] = 1; strings2(A, k+1, u+1, l);
}
int main(){
int n, l;
cin >> n >> l;
vector<int> A(n);
strings2(A,0,0,l);
}
A
: partial string (size n)k
: first non-filled cell in Au
: number of 1s in A[0...k-1]
(already filled)l
: total number of 1s we want
Can we detect situations in which a partial string cannot be extended to a total string with exactly \ell 1s?
If we can do that we can avoid to visit branches in the search tree, i.e. we prune the search space.
#include<iostream>
#include<vector>
using namespace std;
void strings3(vector<int>& A, int k, int z, int u, int l) {
if (k == A.size()){
for(int j = 0; j < k; ++j) cout << A[j];
cout << endl;
return;
}
if (z < A.size() - l) {
A[k] = 0; strings3(A, k+1, z+1, u, l);
}
if (u < l) {
A[k] = 1; strings3(A, k+1, z, u+1, l);
}
}
int main(){
int n, l;
cin >> n >> l;
vector<int> A(n);
strings3(A,0,0,0,l);
}
A
: partial string (size n)k
: first non-filled cell in Au
: number of 1s in A[0...k-1]
(already placed)z
: number of 0s in A[0...k-1]
(already placed)l
: total number of 1s we want
Input:
a natural number n
Output:
all possible permutations of n
#include<iostream>
#include<vector>
using namespace std;
void write_perm(int n, vector<int>& A, int k, vector<bool>& used) {
if (k == A.size()){
for(int j = 0; j < k; ++j) cout << A[j];
cout << endl;
return;
}
for (int i = 1; i <= n; ++i) {
if (not used[i]) {
A[k] = i;
used[i] = true;
write_perm(n,A,k+1,used);
used[i] = false;
}
}
}
int main() {
int n;
cin >> n;
vector<int> A(n);
vector<bool> used(n+1,false);
write_perm(n,A,0,used);
}
The solution space is organized as a configuration tree. Each node or tree configuration is represented with a vector A = (a_1, a_2, . . . , a_k ) that contains the choices already made (the partial solution).
Vector A grows in the forward phase by choosing a_{k +1} from a set S_{k +1} of candidates (exploration in depth).
A is reduced in the backtracking phase (backtrack).
void backtracking(int k, ...other parameters...){
if (k == base_case){// base case of the recursion
...
return;
}
// inductive case
...for loop exploring all possible ways of extending...
}
Inside the for
loop exploring all the possible extensions, check whether the way you are trying to extend is allowed (this really depends on the problem at hand).
If it is allowed:
backtracking
with the the auxiliary structures upadted, i.e. call backtracking(k+1, ...other parameters updated...);
Trying to do more than one check in the if
condition of the base case. Or forgetting to have a return;
at the end. This will likely result in some unwanted execution, for instance entering in the inductive case when k > base case
and perhaps going out the range of a vector.
Assuming that the base case is k=0. This is not true!
The base case of the recursion is when we have a full solution. k=0 is signaling the opposite, the partial solution is empty, we have not made yet any progress towards a full solution.
In the inductive case, trying to update k
with ++k
or updating other quantities in the same way instead of calling backtracking(k+1, ...)
.
In the inductive case, not assuming you already have a partial solution up to k-1
, and therefore trying to compute it again, typically with some for loop.
A common error is forgetting to undo the marking after the call to backtracking(k+1, ...)
. This will result in exploring only one possible branch (one way of extending partial solutions). This might lead or not to a full solution.
Input:
a positive integer n
Output:
write all possible positions of n queens in an n\times n chessboard such that no queen attack another one (i.e. each row, column or diagonal contains at most one queen).
Worst-cast cost in time: \Theta(n^n)
Use vector<int> t;
to store the positions of the queens:
The queen in row i
is in column t[i]
.
When two queens can attack each other?
The i
-th queen and the j
-th queen can attack each other if
t[i] == t[j]
(same column)t[i]-t[j] == i-j
(same diagonal)t[i]-t[j] == j-i
(same anti-diagonal)bool attacked(int i) {
for (int k = 0; k < i; ++k)
if (t[k] == t[i] or t[k] - k == t[i] - i or t[k] + k == t[i] + i)
return true;
return false;
}
To write the output we use write()
Q.......
....Q...
.......Q
.....Q..
..Q.....
......Q.
.Q......
...Q....
Using the functions write
and attacked
then we can output all the solutions as follow:
Instead of computing whether a position is attacked every time we can use a marking strategy remembering which rows/columns/diagonals are not allowed since they already have a queen in it.
vector<bool>
of size n)vector<bool>
of size 2n-2.vector<bool>
of size 2n-2.#include <iostream>
#include <vector>
using namespace std;
int n;
vector<int> t;
// mc[j] == queen at column j
// md1[k] == queen at diagonal i+j = k, etc.
vector<int> mc, md1, md2;
void queens(int i);
int main() {
cin >> n;
t = vector<int>(n);
mc = vector<int>(n, false);
md1 = md2 = vector<int>(2*n-1, false);
queens(0);
}
queens
functionbool queens(int i) {
if (i == n){
write();
return true;
}
for (int j = 0; j < n; ++j) // j is column for queen of row i
if (not mc[j] and not md1[i+j] and not md2[i-j + n-1]) {
t[i] = j;
mc[j] = md1[i+j] = md2[i-j + n-1] = true;
if (queens(i+1)) return true;
mc[j] = md1[i+j] = md2[i-j + n-1] = false;
}
return false;
}
int queens(int i) {
if (i == n){
return 1;
}
int res = 0;
for (int j = 0; j < n; ++j) // j is column for queen of row i
if (not mc[j] and not md1[i+j] and not md2[i-j + n-1]) {
t[i] = j;
mc[j] = md1[i+j] = md2[i-j + n-1] = true;
res += queens(i+1);
mc[j] = md1[i+j] = md2[i-j + n-1] = false;
}
return res;
}
Input:
a positive integer n
Output:
all Latin squares of size n, i.e. all n\times n grids filled with n distinct symbols and each symbol appear once in every row and column.
#include <iostream>
#include <vector>
using namespace std;
int n;
vector<vector<int>> q;
vector<vector<bool>> r;
vector<vector<bool>> c;
void latin_square(int i, int j);
int main () {
cin >> n;
q = vector<vector<int>>(n, vector<int>(n));
r = c = vector<vector<bool>>(n, vector<bool>(n, false));
latin_square(0, 0);
}
q[i][j]
equals value at row i, column j
r[i][v]
equals whether row i already uses value v
c[j][v]
equals whether column j already uses value v
Cost: \mathcal O(n^{n^2} )
latin_square
function// Find all Latin squares completing from (i, j)
void latin_square(int i, int j) {
if (i == n)
return write();
if (j == n)
return latin_square(i+1, 0);
for (int v = 0; v < n; ++v) {
if (not r[i][v] and not c[j][v]) {
r[i][v] = c[j][v] = true;
q[i][j] = v;
latin_square(i, j+1);
r[i][v] = c[j][v] = false;
}
}
}
Input:
a positive integer n and i,j\leq n
Output:
a sequence of positions of the n\times n board s.t.
#include <iostream>
#include <vector>
using namespace std;
int n;
vector<vector<int>> t;
bool possible(int i, int j, int s);
int main() {
int i, j;
cin >> n >> i >> j;
t = vector<vector<int>>(n, vector<int>(n, -1));
cout << possible(i, j, 0) << endl;
}
t[i][j] == k
if in the k-th jump we arrive at (i,j)vector<int> di = {1, 1, -1, -1, 2, 2, -2, -2};
vector<int> dj = {2, -2, 2, -2, 1, -1, 1, -1};
bool possible(int i, int j, int s) {
if (i >= 0 and i < n and j >= 0 and j < n and t[i][j] == -1){
t[i][j] = s;
if (s == n*n-1) return true;
for (int k = 0; k < 8; ++k)
if (possible(i + di[k], j + dj[k], s+1)) return true;
t[i][j] = -1;
}
return false;
}
Input:
a graph G
Output:
True/False according whether G has a Hamiltonian cycle, i.e. a cycle that visits each vertex of G exactly once
vector<vector<int>> G;
int n; //number of vertices
vector<int> s;
bool found;
vector<int> sol;
void hamilton(int v, int t);
int main(){
...read the input graph G...
n = G.size();
s = vector<int>(n, -1);
found = false;
hamilton(0,1);
cout << found << endl;
}
s
is the partial solution we build. s[i]
is the vertex after i
in the partial solution (−1 if not used yet)
v
= last vertex in the patht
= number of vertices in the path
Input:
a positive integer C and n pairs of numbers (v_1,w_1),\dots,(v_n,w_n)
Output:
a subset S of \{1,\dots,n\} such that
#include <iostream>
#include <vector>
using namespace std;
int c; // Capacity
int n; // Number of objects
vector<int> w; // Weights
vector<int> v; // Values
vector<int> s; // (Partial) Solution we are building
// s[i] == 1 iff i-th object is chosen
int bv = -1; // Best value so far
vector<int> bs; // Best solution so far
void opt(int k, int swp, int svp);
int main() {
cin >> c >> n;
w = v = s = vector<int>(n);
for (int& x : p) cin >> x;
for (int& x : v) cin >> x;
opt(0, 0, 0);
cout << bv << endl;
}
void opt(int k, int swp, int svp) { // swp: sum weights partial
if (swp > c) return; // Exceed capacity: do not continue
if (k == n) {
if (svp > bv) { // Improve best solution so far
bs = s;
bv = svp;
}
return;
}
// Discard obj. k
s[k] = 0;
opt(k+1, swp, svp );
// Choose obj. k
s[k] = 1;
opt(k+1, swp + w[k], svp + v[k]);
}
Prune also when we cannot improve the best cost found so far
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
...same global parameters as before...
// svr: sum values remaining
void opt(int k, int swp, int svp, int svr);
int main() {
cin >> c >> n;
p = v = s = vector<int>(n);
for (int& x : p) cin >> x;
for (int& x : v) cin >> x;
sum = accumulate(v.begin(), v.end(), 0);
opt(0, 0, 0, sum);
cout << bv << endl;
}
The Travelling Salesman Problem (TSP) consists in, given a network of cities, finding the order in which we should visit the cities such that:
class TSP {
int n; matrix<int> M; vector<int> s; vector<int> best_sol; double best_cost;
void recursive (int v, int t, double c) {
if (t == n) {
c += M[v][0];
if (c < best_cost) {
best_cost = c;
best_sol = s;
best_sol[v] = 0;
}
return;
}
for(int u = 0; u < n; ++u)
if (u != v and s[u] == -1) {
if (c + M[v][u] < best_cost) {
s[v] = u;
recursive(u, t+1, c + M[v][u]);
s[v] = -1;
}
}
}
public:
TSP (matrix& M) {
this->M = M;
n = M.rows();
s = vector<int>(n, -1);
best_sol = vector<int>(n);
best_cost = infinite;
recursive(0, 1, 0);
}
vector<int> solution ( ) { return best_sol; }
int next (int x) { return best_sol[x]; }
double cost ( ) { return best_cost; }
};
n
: number of citiesM
: matrix of distancess
is the partial solution we build. s[i]
is next city after city i
(−1 if not yet used)best_sol
: best solution found so farbest_cost
: cost of best solution found so far
v
: last vertex in partial solution st
: size of the path given by sc
: cost of s
Current best solution pruning (CBSP) is a technique that can be applied whenever we want to solve optimization problems (maximization or minimization). Say, our goal is to find a solution of minimum cost.
Let x be the current solution, and best_cost
the cost of the best solution found so far. We compute an estimation of the cost of the best solution that we can find from x (estimated_cost
). It must be a lower bound the cost of the best solution that we could find from x (real_best_cost
).
Then if estimated_cost
> best_cost
we can prune at x and avoid exploring its descendants in the recursion tree: no solution found there will be better than the one we already have.
Questions?