Module preview:
In this module, we explore brute-force algorithms, iterative generation techniques, and combinatorial search methods — including subset generation, permutation generation, Gray codes, and the Steinhaus-Johnson-Trotter algorithm. Students will learn how exhaustive search can solve both feasibility and optimization problems, and how studying brute-force solutions often reveals insights that lead to more efficient algorithms.
Module summary:
Brute-force algorithms are fundamental to both game development and artificial intelligence because they provide a systematic way to explore large spaces of possible decisions, actions, or configurations. Many AI systems in games — such as pathfinding, move evaluation, puzzle solving, procedural generation, and strategic planning — begin with exhaustive search before introducing optimizations and heuristics. Understanding subsets, permutations, and combinatorial generation is especially important in areas like state-space exploration, turn-based strategy games, inventory optimization, and simulation systems.
The convex hull and geometric search examples connect directly to collision detection, visibility testing, navigation meshes, and spatial reasoning in game engines and robotics. Minimal-change generation methods such as Gray codes and the Steinhaus-Johnson-Trotter algorithm demonstrate how carefully ordering computations can dramatically improve performance by reusing previous results — a concept heavily used in real-time systems and AI optimization.
More broadly, brute-force thinking teaches developers how to model problems, define candidate solutions, and recognize patterns that lead to smarter algorithms. Even when exhaustive search is impractical for large inputs, it often serves as the conceptual foundation for advanced AI techniques such as pruning, dynamic programming, heuristic search, and machine learning-based optimization.
**Brute force algorithms**
# Introduction and Motivation
The term *brute-force algorithm* often carries a negative connotation — slow, inefficient, unsophisticated. However, this is not always a fair characterization.
A brute-force algorithm can be a perfectly adequate solution in many situations:
- the problem size is small
- the problem only needs to be solved once, and runtime is not a concern
- no more efficient solution is known
Definition: a *brute-force algorithm* is an algorithm that enumerates all possible solutions (candidate solutions) to a problem and checks each one for correctness.
# Types of problems
Brute-force algorithms typically fall into one of two forms:
- find a solution that satisfies a given set of constraints
- find a solution that maximizes (or minimizes) a given function
We refer to the first type as *constraint satisfaction* or *feasibility problems*, and to the second type as *optimization problems*.
Examples of feasibility problems:
```cpp
// The possible solutions are valid indices 0, 1, ..., n-1 and the
// special index -1, which means "not found".
int linear_search(int arr[], int n, int x) {
for (int i = 0; i < n; ++i) { // traverse all possible solutions
if (arr[i] == x) {
return i;
}
}
return -1; // no solution was found
}
```
and
```cpp
// To sort an array, we need to find a permutation of 0, 1, ..., n-1
// that places the elements in sorted order.
void deterministic_bogosort(int *arr, int n) {
for each permutation p of 0, 1, ..., n-1 {
temp = permute arr according to p;
if (is_sorted(temp)) {
arr = temp; // record the solution
return;
}
}
// will we ever reach here?
}
```
Notice that both algorithms exit early — they stop as soon as a valid solution is found. This is a characteristic feature of feasibility problems.
There is one notable difference between them, though. The first algorithm has a **not found** case: if no solution exists, execution reaches the end of the loop. The second algorithm has no such case — we are guaranteed that some permutation will sort the array, so we will never exhaust all possibilities without finding a solution.
Examples of optimization problems:
```cpp
// To find the maximum element in an array, we must examine all indices.
int find_max(int arr[], int n) {
int max_index = 0;
for (int i = 1; i < n; ++i) {
if (arr[i] > arr[max_index]) {
max_index = i;
}
}
return max_index;
}
```
and
```cpp
// Given a set of points, find the pair with the smallest distance between them.
// Since distance is symmetric, we only need to check pairs with i < j.
Pair find_closest_pair(Point * points, int n) {
Pair closest_pair = {0, 1};
double closest_distance = distance(points[0], points[1]);
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
double d = distance(points[i], points[j]);
if (d < closest_distance) {
closest_distance = d;
closest_pair = {i, j};
}
}
}
return closest_pair;
}
```
Note the common pattern: we maintain the best solution found so far, and update it whenever we encounter a better one as we traverse all candidates.
# Convex Hull problem
Brute-force algorithms are often useful even when they are too slow for
practical use, because they help us understand the structure of a
problem more deeply. While implementing a brute-force solution, we may notice patterns,
repeated computations, constraints, or mathematical properties that allow
us to eliminate unnecessary work and design a faster algorithm.
The *convex hull problem* is a good illustration of this.
Given a set of points in a plane, we want to find a subset of points
that form a convex polygon containing all the other points inside it.
For simplicity, we assume that:
- no three points are collinear, so we do not need to handle degenerate cases
- there are at least 3 points
Under these assumptions, a convex hull is guaranteed to exist and to be unique.
Example of a convex hull:

Examples of non-convex hulls (using the same set of points as above):
- not all points lie inside the polygon

- the polygon is not convex

A brute-force solution is to check every possible subset of points and test whether it forms a convex polygon that contains all remaining points:
```cpp
// Brute-force solution to the convex hull problem.
// Assumes at least three points and no three points are collinear.
subset convex_hull_brute_force(vector points) {
for each subset S of points {
if (is_convex_polygon(S) and contains_all_points(S, points)) {
return S;
}
}
// We should never reach here, since the convex hull is guaranteed to exist.
}
```
where `Point` is defined as:
```cpp
struct Point {
double x, y;
};
```
The helper functions rely on a simple method for determining which side of a line a point lies on. The logic works as follows: given a line defined by two points A and B, and a point P, we want to determine whether P is on the left side, the right side, or on the line itself. We can do this by:
- computing the direction vector of the line: $\vec{t} = B - A$
- computing the normal vector of the line: $\vec{n} = (-t_y, t_x)$, where $t_x$ and $t_y$ are the components of $\vec{t}$
- computing the vector from A to P: $\vec{v} = P - A$
- computing the dot product of $\vec{n}$ and $\vec{v}$:
- if the dot product is positive, P is on the left side of the line
- if the dot product is negative, P is on the right side of the line
- if the dot product is zero, P lies on the line
Here is an illustration of the method:

The vector $\vec{v}_1$ from point $P_1$ forms an acute angle with the normal vector $\vec{n}$, so the dot product is positive and $P_1$ is on the left side. The vector $\vec{v}_2$ from point $P_2$ forms an obtuse angle, making the dot product negative.
The corresponding C++ implementation:
```cpp
// Returns 1 if the point is on the left side of line AB,
// -1 if on the right side, and 0 if on the line.
int side_of_line(Point A, Point B, Point P) {
double t_x = B.x - A.x;
double t_y = B.y - A.y;
double n_x = -t_y;
double n_y = t_x;
double v_x = P.x - A.x;
double v_y = P.y - A.y;
double dot_product = n_x * v_x + n_y * v_y;
if (dot_product > 0) {
return 1; // left side
} else if (dot_product < 0) {
return -1; // right side
} else {
return 0; // on the line
}
}
```
Using this helper function, we can implement `is_convex_polygon` with the following idea: for each triple of consecutive points in the subset, check that the third point lies on the same side of the line formed by the first two. This is equivalent to requiring that a walk around the polygon always turns in the same direction — either always left or always right. If at any point the turn goes the other way, the polygon is not convex. For a subset $P_0, P_1, ..., P_{k-1}$, we check that for all triples $(P_i,\ P_{(i+1) \bmod k},\ P_{(i+2) \bmod k})$, the point $P_{(i+2) \bmod k}$ lies on the same side of the line through $P_i$ and $P_{(i+1) \bmod k}$:
```cpp
// Check if a subset of points forms a convex polygon.
bool is_convex_polygon(vector subset) {
int initial_side = side_of_line(subset[0], subset[1], subset[2]); // determine the turning direction using the first three points
for each triple of points (P_i, P_{(i+1) mod k}, P_{(i+2) mod k}) { // k is the size of the subset
int side = side_of_line(P_i, P_{(i+1) mod k}, P_{(i+2) mod k});
if (side != initial_side) {
return false; // the turn direction changed — polygon is not convex
}
}
return true; // all turns go the same way — polygon is convex
}
```
Note: the implementation above has a subtle issue — it checks points only in the order they appear in `subset`, but the points may not be listed in the order they appear around the polygon. The example below shows the same four points in two different orderings: one yields a convex polygon, the other does not.


To address this, we can either 1) try all possible orderings (permutations) of the points in the subset, or more efficiently 2) sort the points by their polar angle with respect to the leftmost point (or any other fixed reference), and then apply the same convexity check. This ensures the points are processed in the order they appear on the polygon. This is left as an exercise for the reader.
For `contains_all_points`, we verify that for every edge of the polygon, all points not in the subset lie on the same side of the line through that edge. If any edge has points on both sides, the polygon does not contain all points. For a polygon $P_0, P_1, ..., P_{k-1}$ and original point set $Q_0, Q_1, ..., Q_{n-1}$, we check all edges $(P_i,\ P_{(i+1) \bmod k})$ against all points $Q_j$ not in the polygon:
```cpp
// Check if a convex polygon contains all points.
bool contains_all_points(vector polygon, vector points) {
for each edge (P_i, P_{(i+1) mod k}) of the polygon { // k is the size of the polygon
for each point Q_j in points that is not in the polygon {
check that all points are on the same side of the line formed by P_i and P_{(i+1) mod k};
if not - return false;
}
}
return true;
}
```
This brute-force solution is highly inefficient: there are $2^n$ subsets to consider, and checking each one takes $O(n^2)$ time, giving an overall complexity of $O(n^2 \times 2^n)$ — impractical for any but the smallest inputs.
More importantly, while working through this solution, a key observation emerges: `side_of_line` is called repeatedly on the same pairs of points. For instance, when testing subset $\{A, B, D\}$ we check all original points against the line through $A$ and $B$ — and then we do the same again when testing a different subset like $\{A, B, E, F\}$.
This observation motivates a much more efficient approach: for each pair of points $A$ and $B$, check whether all other points lie on the same side of the line through $A$ and $B$. If they do, then $A$ and $B$ must be on the convex hull. Repeating this for all pairs collects the complete hull in $O(n^3)$ time — far better than the original.
```cpp
subset convex_hull_brute_force_2(vector points) {
subset hull;
for each pair of points (A, B) in points { // O(n^2) pairs
if (all points are on the same side of the line A-B) { // O(n) to check all points
add A and B to hull;
}
}
return hull;
}
```
# Combinatorics
In the previous sections we saw that brute-force algorithms frequently require enumerating all subsets or all permutations of a set. We now discuss how to generate them systematically and efficiently.
## Generating subsets
### Inefficient solution
Our first approach uses the following recursive idea: given all subsets of the first $n-1$ elements, we can generate all subsets of $n$ elements by adding the $n$-th element to each existing subset. For example, all subsets of $\{0, 1\}$ are $\{\},\ \{0\},\ \{1\},\ \{0,1\}$. Adding element $2$ to each produces $\{2\},\ \{0,2\},\ \{1,2\},\ \{0,1,2\}$, giving us all $8$ subsets of $\{0,1,2\}$.
This approach generates all subsets correctly, but it requires storing all of them simultaneously, which is very memory-intensive for large $n$.
### Efficient solution
A more efficient approach uses **on-demand** generation, keeping only one subset in memory at a time. The key insight is that each subset of $n$ elements can be represented as an $n$-bit binary number, where bit $i$ is $1$ if element $i$ is included and $0$ otherwise. For example, for $n=3$, the subset $\{0, 2\}$ is represented as `101`. Iterating through all integers from $0$ to $2^n - 1$ in decimal is equivalent to iterating through all $n$-bit binary strings, and thus through all subsets:
```cpp
for (int i = 0; i < (1 << n); ++i) { // 1<
Note: `(1 << j)` is a bit-shift operation that produces the number $1$ shifted left by $j$ positions — that is, a binary number with a single $1$ at position $j$ and zeros everywhere else. The expression `i & (1 << j)` then performs a bitwise AND between $i$ and this mask. Since AND yields $0$ whenever either operand is $0$, the result is nonzero only if bit $j$ of $i$ is also $1$. This gives us a clean way to test whether a specific bit is set.
## Generating permutations
Consider the following puzzle: given a number, rearrange its digits to produce the next larger number. For example:
- `123` → next is `132`
- `63421` → next is `64123`
- `45321` → next is `51234`
- `654321` → no larger number can be formed from these digits
How do we solve this? Two guiding observations:
- We want to change the number as little as possible, so we should modify the rightmost (least significant) digits first.
- To make the number larger, we need to increase some digit at some position.
Combining these:
- Find the shortest suffix that is **not** in decreasing order (e.g., in `2641`, the suffix is the full number: a value, then a larger value, followed by decreasing values).
- Replace the leading digit of that suffix with the smallest digit in the suffix that is larger than it (in `2641`, the leading `2` becomes `4`).
- Sort the remaining digits in increasing order to form the smallest possible number from them (in `2641`, `261` becomes `126`), yielding the next number `4126`.
- If no such suffix exists — that is, the entire number is in decreasing order — there is no next larger number.
This process is exactly the algorithm for generating the next permutation in lexicographic order:
- 123 — use suffix `23`, advance to → 132
- 132 — use suffix `132`, advance to → 213
- 213 — use suffix `13`, advance to → 231
- 231 — use suffix `231`, advance to → 312
- 312 — use suffix `12`, advance to → 321
- 321 — done
Since the algorithm generates permutations in lexicographic order, we can enumerate all permutations by starting from the smallest (ascending order) and repeatedly applying the algorithm until we reach the largest (descending order).
Note that C++ provides `std::next_permutation`, which implements this algorithm. Here it is in use:
```cpp
#include
#include
#include //ostream_iterator
int main() {
int A[] = {1,2,3,4};
const int N = sizeof(A) / sizeof(int);
do
{
std::copy(A, A+N, std::ostream_iterator(std::cout, " "));
std::cout << std::endl;
}
while (std::next_permutation(A,A+N));
}
```
Notes:
- `std::next_permutation` rearranges the elements of the given range in-place into the next lexicographically greater permutation.
- If no such permutation exists (i.e., the range is in descending order), it resets the range to the first permutation (ascending order) and returns `false`.
# Minimal change ordering
In some applications, it is beneficial to traverse subsets (or permutations) in an order where consecutive elements differ as little as possible.
The motivation is that if the difference between consecutive elements is small, we can often reuse the result of the previous computation rather than starting from scratch. Compare the following two approaches:
```cpp
// Without minimal change ordering — every subset requires a full recomputation.
for each subset S {
perform some calculation for S from scratch; // expensive
use the result
}
```
With minimal change ordering, we only compute from scratch once:
```cpp
subset S_prev = first subset; // e.g., the empty set
perform some calculation for S_prev from scratch;
for each next subset S in minimal change order {
perform re-calculation for S using the result for S_prev; // cheap update
use the result;
S_prev = S;
}
```
This reduces the cost from $2^n$ expensive calculations to one expensive calculation followed by $2^n - 1$ cheap updates.
## Gray code
A *Gray code* is a sequence of binary numbers in which any two consecutive values differ in exactly one bit. For example, the Gray code for $n=3$ is:
```
000
001
011
010
110
111
101
100
```
It turns out that such a sequence exists for every $n$, and it can be computed with a simple formula:
```cpp
void generate_gray_code(int n) {
for (int i = 0; i < (1 << n); ++i) { // iterate from 0 to 2^n - 1
int gray = i ^ (i >> 1); // convert to the corresponding Gray code value
process gray;
}
}
```
## Steinhaus-Johnson-Trotter algorithm
For permutations, the smallest possible change between consecutive elements is a swap of two adjacent elements.
```
123
132
312
321
231
213
```
The *Steinhaus-Johnson-Trotter algorithm* generates all permutations of $n$ elements such that each permutation differs from the previous one by swapping two adjacent elements.
The algorithm uses an auxiliary data structure: each element carries a **direction arrow** pointing either left or right. Using these arrows, we define a *mobile element* as an element that is larger than its neighbor in the direction its arrow points. For example, in $\overrightarrow{3}\ \overrightarrow{1}\ \overleftarrow{2}$:
- `3` is mobile — its arrow points right toward `1`, and $3 > 1$
- `1` is not mobile — it is smaller than its neighbor in the direction it points
- `2` is mobile — its arrow points left toward `1`, and $2 > 1$
The algorithm proceeds as follows:
- Start with the elements in increasing order, all arrows pointing left.
- Repeatedly find the largest mobile element. If one exists:
- Swap it with its neighbor in the direction of its arrow.
- Reverse the direction of all elements larger than the one just moved.
- If no mobile element exists, all permutations have been generated.
Example trace for $n=3$:
- $\overleftarrow{1}\ \overleftarrow{2}\ \overleftarrow{3}$
— largest mobile is `3`; swap with `2`; no elements larger than `3` to reverse.
- $\overleftarrow{1}\ \overleftarrow{3}\ \overleftarrow{2}$
— largest mobile is `3`; swap with `1`; no elements larger than `3` to reverse.
- $\overleftarrow{3}\ \overleftarrow{1}\ \overleftarrow{2}$
— largest mobile is `2`; swap with `1`; reverse the direction of `3`.
- $\overrightarrow{3}\ \overleftarrow{2}\ \overleftarrow{1}$
— largest mobile is `3`; swap with `2`; no elements larger than `3` to reverse.
- $\overleftarrow{2}\ \overrightarrow{3}\ \overleftarrow{1}$
— largest mobile is `3`; swap with `1`; no elements larger than `3` to reverse.
- $\overleftarrow{2}\ \overleftarrow{1}\ \overrightarrow{3}$
— no mobile elements; done.
# Flashcards
Q: What is a brute-force algorithm?
A: A brute-force algorithm is an algorithm that tries all possible candidate solutions to a problem and checks which ones satisfy the required conditions.
Q: When can brute-force algorithms be practical?
A: Brute-force algorithms can be practical when the problem size is small, when the problem only needs to be solved once, or when no more efficient solution is known.
Q: What are the two common categories of brute-force problems?
A: Constraint satisfaction (feasibility) problems and optimization problems.
Q: What is a feasibility problem?
A: A feasibility problem searches for any solution that satisfies a set of constraints.
Q: What is an optimization problem?
A: An optimization problem searches for the best solution according to some objective function, such as maximizing or minimizing a value.
Q: What is a common feature of feasibility problems?
A: They often have an early exit because the algorithm can stop as soon as a valid solution is found.
Q: What is a common pattern in optimization problems?
A: The algorithm keeps track of the best solution found so far and updates it whenever a better solution is discovered.
Q: What is the convex hull problem?
A: Given a set of points in a plane, the goal is to find the convex polygon with vertices from the given set of points
that contains all the points.
Q: Why are brute-force solutions useful even when inefficient?
A: They help reveal patterns, repeated computations, and mathematical properties that can lead to faster algorithms.
Q: How can we determine which side of a line a point lies on?
A: Compute the line direction vector, compute a normal vector, form the vector to the point, and use the sign of the dot product between the normal and the point vector.
Q: How can subsets be represented using binary numbers?
A: Each bit indicates whether the corresponding element is included in the subset.
Q: What subset does the binary number `011` represent for n=3 assuming elements are indexed from 0 to 2 (left to right)?
A: The subset {1, 2}.
Q: Why generating subsets on demand using binary numbers is more memory efficient than generating all subsets at once?
A: Only one subset needs to exist in memory at a time.
Q: What does the expression (1 << j) mean?
A: It shifts the number 1 left by j bits, creating a binary number with a 1 at position j.
Q: What does the expression i & (1 << j) test?
A: Whether the j-th bit of i is set to 1.
Q: What is a permutation?
A: A rearrangement of the elements of a set.
Q: What is lexicographic order for permutations?
A: An ordering similar to dictionary order, comparing elements from left to right.
Q: What is minimal-change ordering?
A: An ordering where consecutive subsets or permutations differ as little as possible.
Q: Why is minimal-change ordering useful?
A: It allows reuse of previous computations, reducing recalculation cost.
Q: What is Gray code?
A: A sequence of binary numbers where consecutive numbers differ in exactly one bit.
# Exercises
For the written problems, provide pseudocode. Do not try to implement every detail — see the examples above, especially the convex hull. Use high-level descriptions such as:
- `for all subsets`
- `for all pairs of points`
- `for all triples of points`
- `sum the weights of items in the subset`
- `check if square is magic`
etc. to convey the main logic of the algorithm, without getting bogged down in implementation details. What matters is that your pseudocode faithfully follows the structure of either a constraint satisfaction problem or an optimization problem, as described in the introduction. Expected length: about 5–7 lines.
1. Written problem. Optimize `convex_hull_brute_force_2` to run in $O(n^2 \times k)$ time, where $k$ is the number of points on the convex hull. Hint: identify the leftmost point (smallest x-coordinate) of the set — it is guaranteed to be on the convex hull — and consider only pairs of points (lines) that pass through this point. Provide pseudocode for the optimized algorithm and explain why its time complexity is $O(n^2 \times k)$.
2. Programming assignment. Implement lexicographic permutation generation in C++, using the algorithm described in the permutation section. Test your implementation on a few examples to verify correctness. Do not use `std::next_permutation` — implement the algorithm from scratch.
3. Written problem. Solve the 0-1 knapsack problem using an iterative brute-force algorithm. Given a set of items, each with a weight and a monetary value, and a maximum weight capacity, determine the maximum total value of items that fit in the knapsack. Items cannot be split — each is either taken or left behind. Provide pseudocode for your solution and state its time complexity.
4. Written problem. The most isolated point: given a set of points in the plane, find the point that is farthest from its nearest neighbor.
5. Written problem. The magic square problem: given an integer $n$, construct an $n \times n$ matrix filled with the distinct integers from $1$ to $n^2$ such that every row, every column, and both main diagonals sum to the same value. Examples:
```
n=3, sum = 15:
8 1 6
3 5 7
4 9 2
```
and
```
n=4, sum = 34:
16 2 3 13
5 11 10 8
9 7 6 12
4 14 15 1
```
6. Written problem. Execute the first 13 steps of the Steinhaus-Johnson-Trotter algorithm for $n=4$. At each step, show the permutation and the direction of movement for each element. Suggested format:
```
1.
< < < <
1 2 3 4 4 is largest mobile
2.
< < < <
1 2 4 3 4 is largest mobile
3.
...
```