From d9d73d9f63c208a6fa8f9a0b0a1c1324620030fc Mon Sep 17 00:00:00 2001 From: Adam Djellouli <37275728+djeada@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:33:28 +0200 Subject: [PATCH 1/5] Update all_permutations.py --- .../all_permutations/src/all_permutations.py | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/backtracking/python/all_permutations/src/all_permutations.py b/src/backtracking/python/all_permutations/src/all_permutations.py index 198bf0a..d845899 100644 --- a/src/backtracking/python/all_permutations/src/all_permutations.py +++ b/src/backtracking/python/all_permutations/src/all_permutations.py @@ -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 From b5a36a8c3a4bc5c1af76ba12fde7b3cf7ca9b9f3 Mon Sep 17 00:00:00 2001 From: Adam Djellouli <37275728+djeada@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:34:16 +0200 Subject: [PATCH 2/5] Update test_all_permutations.py --- .../tests/test_all_permutations.py | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/src/backtracking/python/all_permutations/tests/test_all_permutations.py b/src/backtracking/python/all_permutations/tests/test_all_permutations.py index de2b9d5..41d54c2 100644 --- a/src/backtracking/python/all_permutations/tests/test_all_permutations.py +++ b/src/backtracking/python/all_permutations/tests/test_all_permutations.py @@ -1,26 +1,51 @@ -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, + all_permutations # this is aliased to backtracking by default +) + 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"], @@ -29,13 +54,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], @@ -62,9 +88,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__": From 16511243419e0d3663ec115bfa1f8e7fc60ece9c Mon Sep 17 00:00:00 2001 From: Adam Djellouli <37275728+djeada@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:34:50 +0200 Subject: [PATCH 3/5] Update test_all_permutations.py --- .../python/all_permutations/tests/test_all_permutations.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backtracking/python/all_permutations/tests/test_all_permutations.py b/src/backtracking/python/all_permutations/tests/test_all_permutations.py index 41d54c2..c9aac21 100644 --- a/src/backtracking/python/all_permutations/tests/test_all_permutations.py +++ b/src/backtracking/python/all_permutations/tests/test_all_permutations.py @@ -4,8 +4,7 @@ from src.all_permutations import ( all_permutations_itertools, - all_permutations_backtracking, - all_permutations # this is aliased to backtracking by default + all_permutations_backtracking ) From 69853912d7025dcbf942c2a7046369c50de98811 Mon Sep 17 00:00:00 2001 From: Adam Djellouli <37275728+djeada@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:38:23 +0200 Subject: [PATCH 4/5] Update README.md --- .../python/all_permutations/README.md | 113 +++++++++++++++--- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/src/backtracking/python/all_permutations/README.md b/src/backtracking/python/all_permutations/README.md index d2a2704..8584bf7 100644 --- a/src/backtracking/python/all_permutations/README.md +++ b/src/backtracking/python/all_permutations/README.md @@ -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] @@ -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. From 5e2fd4edc811c86e5ea1ef65393c0ae06635297c Mon Sep 17 00:00:00 2001 From: Adam Djellouli <37275728+djeada@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:41:13 +0200 Subject: [PATCH 5/5] Update run_tests.sh --- run_tests.sh | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 07cd3bf..29e9d6a 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -3,7 +3,7 @@ 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 @@ -11,16 +11,17 @@ detect_base_branch() { 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 @@ -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 @@ -85,14 +86,20 @@ test_cpp_projects() { cleanup cd "$current_dir" + # Safely extract “ 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" @@ -106,7 +113,7 @@ 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 .) @@ -114,7 +121,7 @@ test_python_projects() { 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#./}" @@ -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"