Skip to content

Update all_permutations.py #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ set -e

# Determine base branch by looking at the upstream. If no upstream is set, fall back to "master".
detect_base_branch() {
# Try to get the upstream name, e.g. "origin/master" or "origin/main"
# If there’s an upstream (e.g. origin/master or origin/main), use that.
if base="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)"; then
echo "$base"
else
echo "master"
fi
}

# Find all directories under start_dir that contain a CMakeLists.txt
# Find all directories (under start_dir) that contain a CMakeLists.txt
find_cmake_subdirs() {
local start_dir="$1"
find "$start_dir" -type f -name 'CMakeLists.txt' -printf '%h\n' | sort -u
}

# Find all directories under start_dir that contain a __init__.py
# Find only those Python-package roots that actually have a tests/ subdirectory.
# In other words, look for “*/tests” and return the parent directory.
find_python_subdirs() {
local start_dir="$1"
find "$start_dir" -type f -name '__init__.py' -printf '%h\n' | sort -u
find "$start_dir" -type d -name 'tests' -printf '%h\n' | sort -u
}

# Get the list of files changed since the last commit on base branch
Expand All @@ -34,15 +35,15 @@ test_cpp_projects() {
local current_dir
current_dir=$(pwd)

# Find every directory that has a CMakeLists.txt
# All C++ project directories (where CMakeLists.txt lives)
local all_subdirs
all_subdirs=$(find_cmake_subdirs .)

# Which files changed in this PR (relative to base)
# Files modified in this PR relative to base
local modified_files
modified_files=$(get_modified_files)

# Filter to only those C++ subdirs where at least one file was modified
# Filter to only those C++ subdirs in which at least one file was modified
local modified_subdirs=""
for subdir in $all_subdirs; do
# strip leading "./" for comparison against git output
Expand Down Expand Up @@ -85,14 +86,20 @@ test_cpp_projects() {
cleanup
cd "$current_dir"

# Safely extract “<number> tests from” (if any) or default to 0
local cpp_total_tests
cpp_total_tests=$(grep -oP '\d+(?= tests from)' "$cpp_test_log" | tail -1 || echo 0)
cpp_total_tests="${cpp_total_tests:-0}"

# Safely extract passed count (if any) or default to 0
local cpp_passed_tests
cpp_passed_tests=$(grep -oP '(?<=\[ *PASSED *\] )\d+' "$cpp_test_log" | tail -1 || echo 0)
local cpp_failed_tests=$((cpp_total_tests - cpp_passed_tests))
cpp_passed_tests="${cpp_passed_tests:-0}"

local cpp_failed_tests=$(( cpp_total_tests - cpp_passed_tests ))

total_passed_tests=$((total_passed_tests + cpp_passed_tests))
total_failed_tests=$((total_failed_tests + cpp_failed_tests))
total_passed_tests=$(( total_passed_tests + cpp_passed_tests ))
total_failed_tests=$(( total_failed_tests + cpp_failed_tests ))

echo "C++ Tests summary for $subdir:"
echo -e " Passed: \e[32m$cpp_passed_tests\e[0m, Failed: \e[31m$cpp_failed_tests\e[0m"
Expand All @@ -106,15 +113,15 @@ test_python_projects() {
local current_dir
current_dir=$(pwd)

# Find every directory that has an __init__.py
# Only pick up directories that actually have a “tests/” folder
local all_subdirs
all_subdirs=$(find_python_subdirs .)

# Which files changed in this PR (relative to base)
local modified_files
modified_files=$(get_modified_files)

# Filter to only those Python subdirs where at least one file was modified
# Filter to only those Python-root dirs where at least one file was modified
local modified_subdirs=""
for subdir in $all_subdirs; do
local sub="${subdir#./}"
Expand All @@ -140,17 +147,25 @@ test_python_projects() {
python_test_log="$current_dir/python_test_$(echo "$subdir" | tr '/' '_').log"
: > "$python_test_log"

# Run unittest discovery; any output goes into the log
python3 -m unittest discover -v 2>&1 | tee -a "$python_test_log"
cd "$current_dir"

# Safely grab “Ran X tests” (default to 0 if none found)
local python_total_tests
python_total_tests=$(grep -oP '(?<=Ran )\d+' "$python_test_log" | head -1 || echo 0)
python_total_tests="${python_total_tests:-0}"

# Count how many “... ok” lines (default to 0)
local python_passed_tests
python_passed_tests=$(grep -c '\.\.\. ok' "$python_test_log" || echo 0)
local python_failed_tests=$((python_total_tests - python_passed_tests))
python_passed_tests="${python_passed_tests:-0}"

# Compute failures
local python_failed_tests=$(( python_total_tests - python_passed_tests ))

total_passed_tests=$((total_passed_tests + python_passed_tests))
total_failed_tests=$((total_failed_tests + python_failed_tests))
total_passed_tests=$(( total_passed_tests + python_passed_tests ))
total_failed_tests=$(( total_failed_tests + python_failed_tests ))

echo "Python Tests summary for $subdir:"
echo -e " Passed: \e[32m$python_passed_tests\e[0m, Failed: \e[31m$python_failed_tests\e[0m"
Expand Down
113 changes: 97 additions & 16 deletions src/backtracking/python/all_permutations/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
## Generating all permutations of a list of elements
## Generating All Permutations of a List

Given a list of elements, the problem is to generate all possible permutations of these elements.

For example, if the input list is `[1, 2, 3]`, the possible permutations are:
Given a list of *n* distinct elements, the goal is to produce every possible ordering (permutation) of those elements. For example, if the input is `[1, 2, 3]`, the six permutations are:

```
[1, 2, 3]
Expand All @@ -13,22 +11,105 @@ For example, if the input list is `[1, 2, 3]`, the possible permutations are:
[3, 2, 1]
```

## Approach
Because there are *n!* permutations of *n* items, any algorithm will inevitably take at least *O(n!)* time just to enumerate them. In practice, two common ways to generate all permutations in Python are:

1. **Using a built-in library function (e.g., `itertools.permutations`)**
2. **Writing a pure-Python backtracking routine**

Below is a conceptual overview of each approach, along with its time complexity and a brief discussion of advantages and trade-offs.

---

### 1. Built-in Permutations (Conceptual)

Most languages have a library routine that directly yields all permutations of a sequence in an efficient, low-level implementation. In Python, for instance, `itertools.permutations` returns every ordering as a tuple. Converting those tuples to lists (if needed) simply costs an extra *O(n)* per permutation.

* **Core idea**

* Defer the heavy lifting to a built-in routine that is usually implemented in C (or another compiled language).
* Each call to the library function produces one permutation in constant (amortized) time.
* If you need the result as lists instead of tuples, you convert each tuple to a list before collecting it.

* **Time Complexity**

* There are *n!* total permutations.
* Generating each tuple is effectively *O(1)* (amortized), but converting to a list of length *n* costs *O(n)*.
* Overall: **O(n! · n)**.

* **Pros**

* Very concise—just a single function call.
* Relies on a battle-tested standard library implementation.
* Highly optimized in C under the hood.

* **Cons**

* Still incurs the *O(n)* conversion for each permutation if you need lists.
* Less educational (you don’t see how the algorithm actually works).

---

### 2. In-Place Backtracking (Conceptual)

A backtracking algorithm builds each permutation by swapping elements in the original list in place, one position at a time. Once every position is “fixed,” you record a copy of the list in that order. Then you swap back and proceed to the next possibility.

* **Core idea (pseudocode)**

1. Let the input list be `A` of length *n*.
2. Define a recursive routine `backtrack(pos)` that means “choose which element goes into index `pos`.”
3. If `pos == n`, then all indices are filled—append a copy of `A` to the results.
4. Otherwise, for each index `i` from `pos` to `n−1`:

* Swap `A[pos]` and `A[i]`.
* Recursively call `backtrack(pos + 1)`.
* Swap back to restore the original order before trying the next `i`.

* **Time Complexity**

* Exactly *n!* permutations will be generated.
* Each time you reach `pos == n`, you copy the current list (cost *O(n)*).
* Swapping elements is *O(1)*, and there are lower-order operations for looping.
* Overall: **O(n! · n)**.

* **Pros**

* Pure-Python and fairly straightforward to implement once you understand swapping + recursion.
* Does not require constructing new intermediate lists at every recursive call—only one final copy per permutation.
* In-place swapping keeps overhead minimal (aside from the final copy).

* **Cons**

* A bit more code and recursion overhead.
* Uses recursion up to depth *n* (though that is usually acceptable unless *n* is quite large).
* Easier to make an off-by-one mistake if you forget to swap back.

---

## Time Complexity (Both Approaches)

No matter which method you choose, you must generate all *n!* permutations and produce them as lists/arrays. Since each complete permutation takes at least *O(n)* time to output or copy, the combined runtime is always:

```
O(n! · n)
```

* *n!* choices of permutation
* Copying or formatting each permutation (length n) costs an extra n

One approach to solve this problem is to use a backtracking algorithm.
---

A backtracking algorithm works by starting with an empty list and adding elements to it one at a time, then exploring the resulting permutations, and backtracking (removing the last added element) when it is no longer possible to generate new permutations.
## When to Use Each Approach

To implement the backtracking algorithm, we can use a recursive function that takes two arguments:
* **Built-in library function**

* `input_list`: the list of elements from which the permutations are generated.
* `output_list`: the current permutation being generated.
* Ideal if you want minimal code and maximum reliability.
* Use whenever your language provides a standard “permutations” routine.
* Particularly helpful if you only need to iterate lazily over permutations (you can yield one tuple at a time).

The function can follow these steps:
* **In-place backtracking**

* If the length of `output_list` is equal to the length of `input_list`, it means that a permutation of all the elements has been found. In this case, the function can append the permutation to a list of results and return.
* If the length of `output_list` is not equal to the length of `input_list`, the function can enter a loop that enumerates the elements of the input list.
* For each element, the function can check if it is present in the `output_list`. If the element is not present, the function can call itself recursively with `input_list and output_list + [element]` (to add the element to the permutation).
* After the loop, the function can return the list of results.
* Educational: you see how swapping and recursion produce every ordering.
* Useful if you need to integrate custom pruning or constraints (e.g., skip certain permutations early).
* Allows you to avoid tuple-to-list conversions if you directly output lists, although you still pay an *O(n)* copy cost per permutation.

The time complexity of this approach is $O(n! * n)$, where n is the length of the input list.
In most practical scenarios—unless you have a specialized constraint or want to illustrate the algorithm—using the built-in routine is recommended for brevity and performance. However, if you need to customize the traversal (e.g., skip certain branches), an in-place backtracking solution makes it easy to check conditions before fully expanding each branch.
50 changes: 41 additions & 9 deletions src/backtracking/python/all_permutations/src/all_permutations.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,45 @@
def all_permutations(input_list):
def _all_permutations(input_list, output_list):
if len(output_list) == len(input_list):
result.append(output_list)
from typing import Any, List
import itertools


def all_permutations_itertools(input_list: List[Any]) -> List[List[Any]]:
"""
Return all permutations of input_list using itertools.permutations,
converted into lists.

Time complexity:
O(n! * n) where n = len(input_list). itertools.permutations generates each
of the n! permutations in O(1) amortized time, but converting each tuple to a list
costs O(n), so the overall complexity is O(n! * n).
"""
return [list(p) for p in itertools.permutations(input_list)]


def all_permutations_backtracking(input_list: List[Any]) -> List[List[Any]]:
"""
Return all permutations of input_list using in-place backtracking.

Time complexity:
O(n! * n) where n = len(input_list).
We generate n! permutations, and each time we reach a full permutation,
we append a copy of length-n list (O(n) copy cost). Swapping operations
also contribute lower-order overhead but do not change the factorial growth.
"""

def _backtrack(start_index: int):
# If we have fixed positions up to the end, record a copy of the current list
if start_index == n:
result.append(input_list.copy())
return

for element in input_list:
if element not in output_list:
_all_permutations(input_list, output_list + [element])
for i in range(start_index, n):
# Swap element i into the 'start_index' position
input_list[start_index], input_list[i] = input_list[i], input_list[start_index]
_backtrack(start_index + 1)
# Swap back to restore original order before the next iteration
input_list[start_index], input_list[i] = input_list[i], input_list[start_index]

result = []
_all_permutations(input_list, [])
n = len(input_list)
result: List[List[Any]] = []
_backtrack(0)
return result
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
from src.all_permutations import all_permutations
# tests/test_all_permutations.py

import unittest

from src.all_permutations import (
all_permutations_itertools,
all_permutations_backtracking
)


class TestAllPermutations(unittest.TestCase):
def setUp(self):
# We will test these two implementations side by side in every test.
self.funcs_to_test = [
all_permutations_itertools,
all_permutations_backtracking,
]

def test_two_elements(self):
input_list = [1, 2]
excepted = [[1, 2], [2, 1]]
actual = all_permutations(input_list)
expected = [[1, 2], [2, 1]]

self.assertListEqual(sorted(excepted), sorted(actual))
for func in self.funcs_to_test:
with self.subTest(func=func.__name__):
actual = func(input_list[:]) # pass a fresh copy to avoid side‐effects
# Sort both lists of lists before comparing
self.assertListEqual(sorted(expected), sorted(actual))

def test_three_elements(self):
input_list = [3, 1, 2]
excepted = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]

actual = all_permutations(input_list)
expected = [
[1, 2, 3],
[1, 3, 2],
[2, 1, 3],
[2, 3, 1],
[3, 2, 1],
[3, 1, 2],
]

self.assertListEqual(sorted(excepted), sorted(actual))
for func in self.funcs_to_test:
with self.subTest(func=func.__name__):
actual = func(input_list[:])
self.assertListEqual(sorted(expected), sorted(actual))

def test_three_strings(self):
input_list = ["A", "B", "C"]
excepted = [
expected = [
["A", "B", "C"],
["A", "C", "B"],
["B", "A", "C"],
Expand All @@ -29,13 +53,14 @@ def test_three_strings(self):
["C", "B", "A"],
]

actual = all_permutations(input_list)

self.assertListEqual(sorted(excepted), sorted(actual))
for func in self.funcs_to_test:
with self.subTest(func=func.__name__):
actual = func(input_list[:])
self.assertListEqual(sorted(expected), sorted(actual))

def test_four_elements(self):
input_list = [3, 1, 2, 4]
excepted = [
expected = [
[3, 1, 2, 4],
[3, 1, 4, 2],
[3, 2, 1, 4],
Expand All @@ -62,9 +87,10 @@ def test_four_elements(self):
[4, 2, 1, 3],
]

actual = all_permutations(input_list)

self.assertListEqual(sorted(excepted), sorted(actual))
for func in self.funcs_to_test:
with self.subTest(func=func.__name__):
actual = func(input_list[:])
self.assertListEqual(sorted(expected), sorted(actual))


if __name__ == "__main__":
Expand Down