Skip to content

Commit 1791c05

Browse files
committed
feat(algorithms, backtracking, two-pointers): append chars and combination
1 parent 9feb82a commit 1791c05

File tree

7 files changed

+343
-22
lines changed

7 files changed

+343
-22
lines changed

algorithms/backtracking/combination/README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,123 @@ Using 4 different numbers in the range [1,9], the smallest sum we can get is 1+2
7474

7575
- Array
7676
- Backtracking
77+
78+
## Combination
79+
80+
Given two integers n and k, return all possible combinations of k numbers chosen from the range [1, n].
81+
82+
You may return the answer in any order.
83+
84+
> Note: Combinations are unordered, i.e., [1, 2] and [2, 1] are considered the same combination.
85+
86+
### Examples
87+
88+
Example 1:
89+
```text
90+
Input: n = 4, k = 2
91+
Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
92+
Explanation: There are 4 choose 2 = 6 total combinations.
93+
Note that combinations are unordered, i.e., [1,2] and [2,1] are considered to be the same combination.
94+
```
95+
96+
Example 2:
97+
```text
98+
Input: n = 1, k = 1
99+
Output: [[1]]
100+
Explanation: There is 1 choose 1 = 1 total combination.
101+
```
102+
103+
### Constraints
104+
105+
- 1 <= n <= 20
106+
- 1 <= k <= n
107+
108+
### Topics
109+
110+
- Backtracking
111+
112+
### Solutions
113+
114+
#### Variation 1
115+
116+
- Create an empty list `result` to store the final combinations and an empty list `stack` to store the current combination being
117+
formed.
118+
- Define a recursive function `backtrack(start)`, which will generate all possible combinations of size `k` from the
119+
numbers starting from `start` up to `n`.
120+
- In the backtrack function:
121+
- If the length of `stack` becomes equal to `k`, it means we have formed a valid combination, so we append a copy of
122+
the current comb list to the `result` list. We use `stack[:]` to create a copy of the list since lists are mutable
123+
in Python, and we want to preserve the combination at this point without being modified later.
124+
- If the length of `stack` is not equal to k, we continue the recursion.
125+
- Within the `backtrack` function, use a loop to iterate over the numbers starting from `start` up to `n`.
126+
- For each number `num` in the range, add it to the current `stack` list to form the combination.
127+
- Make a recursive call to `backtrack` with `start` incremented by 1. This ensures that each number can only be used
128+
once in each combination, avoiding duplicate combinations.
129+
- After the recursive call, remove the last added number from the `stack` list using stack.pop(). This allows us to
130+
backtrack and try other numbers for the current position in the combination.
131+
- Start the recursion by calling `backtrack(1)` with start initially set to 1, as we want to start forming combinations
132+
with the numbers from 1 to n.
133+
- After the recursion is complete, the `result` list will contain all the valid combinations of size `k` formed from the
134+
numbers 1 to n. Return `result` as the final result.
135+
136+
The code uses a recursive backtracking approach to generate all the combinations efficiently. It explores all possible
137+
combinations, avoiding duplicates and forming valid combinations of size k. The result res will contain all such
138+
combinations at the end.
139+
140+
##### Complexity
141+
142+
###### Time complexity: O(n * k)
143+
144+
`n` is the number of elements and `k` is the size of the subset. The backtrack function is called n times, because there
145+
are n possible starting points for the subset. For each starting point, the backtrack function iterates through all k
146+
elements. This is because the `stack` list must contain all k elements in order for it to be a valid subset.
147+
148+
> The time complexity is T(n,k)=O((n/k) * k), as there are(n/k) possible combinations and each requires O(k) work to build
149+
and copy.
150+
151+
###### Space complexity: O(k)
152+
153+
The `stack` list stores at most k elements. This is because the `backtrack` function only adds elements to the `stack`
154+
list when the subset is not yet complete.
155+
156+
#### Variation 2
157+
158+
The algorithm uses a backtracking strategy to generate all possible combinations of size k from the range [1…n]. It
159+
builds each combination step by step, keeping a temporary list path representing the current sequence of chosen numbers.
160+
At every recursive call, the algorithm selects a new number to add to the path and then continues exploring with the next
161+
larger number. If the length of the path becomes equal to k, the combination is complete and is added to the result list.
162+
After exploring one choice, the algorithm backtracks by removing the last number, allowing it to try different alternatives.
163+
164+
To avoid unnecessary work, the recursion stops early whenever the remaining numbers are too few to complete a valid
165+
combination. This pruning ensures the algorithm explores only those paths that can lead to valid results, making it
166+
efficient and systematic in covering all unique combinations without repetition.
167+
168+
The steps of the algorithm are as follows:
169+
170+
1. Create an empty list, `ans`, to store all valid combinations.
171+
2. Define recursive function, `backtrack(path, start)`, where:
172+
- `path` stores the current partial combination.
173+
- `start` is the smallest number that can be chosen next.
174+
3. The base case is a complete combination: when `len(path) == k`, save it to ans and return.
175+
4. Calculate remaining needs:
176+
- Compute `need = k - len(path)` (how many more numbers are required).
177+
- Compute `max_start = n - need + 1` (the largest possible starting number that still leaves room to complete the
178+
combination).
179+
5. Recursive exploration: For each number `num` from `start` to `max_start`:
180+
- Append `num` to `path` (choose).
181+
- Call `backtrack(path, num + 1)` (explore further).
182+
- Remove `num` from `path` (backtrack/undo choice).
183+
6. Start recursion by calling `backtrack([], 1)` with an empty path starting from 1.
184+
7. After recursion finishes, return `ans`, which contains all valid combinations.
185+
186+
##### Complexity
187+
188+
###### Time complexity: O(n * k)
189+
190+
The time complexity is T(n,k)=O((n/k) * k), as there are(n/k) possible combinations and each requires O(k) work to build
191+
and copy.
192+
193+
###### Space complexity: O(k)
194+
195+
The space complexity is O(k), as the recursion depth can reach k and the path itself holds at most k numbers during
196+
recursion.

algorithms/backtracking/combination/__init__.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import List
2+
from itertools import combinations
23

34

45
def combination_sum_3(k: int, n: int) -> List[List[int]]:
@@ -20,6 +21,51 @@ def backtrack(num: int, stack: List[int], target: int):
2021
return result
2122

2223

24+
def combine(n: int, k: int) -> List[List[int]]:
25+
result = []
26+
stack = []
27+
28+
def backtrack(start: int):
29+
if len(stack) == k:
30+
result.append(stack[:])
31+
return
32+
33+
for num in range(start, n + 1):
34+
stack.append(num)
35+
backtrack(num + 1)
36+
stack.pop()
37+
38+
backtrack(1)
39+
return result
40+
41+
42+
def combine_2(n: int, k: int) -> List[List[int]]:
43+
ans = []
44+
45+
def backtrack(path: List[int], start: int):
46+
# If the combination is complete, save it
47+
if len(path) == k:
48+
ans.append(path[:])
49+
return
50+
51+
# How many more numbers we still need
52+
need = k - len(path)
53+
54+
# Compute the maximum valid starting number
55+
# Ensures enough numbers remain to finish the combination
56+
max_start = n - need + 1
57+
58+
# Try each possible next number
59+
for num in range(start, max_start + 1):
60+
path.append(num) # choose
61+
backtrack(path, num + 1) # explore
62+
path.pop() # un-choose (backtrack)
63+
64+
# Start recursion with an empty combination
65+
backtrack([], 1)
66+
return ans
67+
68+
2369
def combination_sum_2(candidates: List[int], target: int) -> List[List[int]]:
2470
result = []
2571

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.backtracking.combination import combine, combine_2
5+
6+
COMBINATION_TEST_CASES = [
7+
(1, 1, [[1]]),
8+
(2, 2, [[1, 2]]),
9+
(3, 3, [[1, 2, 3]]),
10+
(3, 1, [[1], [2], [3]]),
11+
(4, 2, [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]),
12+
(
13+
5,
14+
3,
15+
[
16+
[1, 2, 3],
17+
[1, 2, 4],
18+
[1, 2, 5],
19+
[1, 3, 4],
20+
[1, 3, 5],
21+
[1, 4, 5],
22+
[2, 3, 4],
23+
[2, 3, 5],
24+
[2, 4, 5],
25+
[3, 4, 5],
26+
],
27+
),
28+
]
29+
30+
31+
class CombinationSumTestCase(unittest.TestCase):
32+
@parameterized.expand(COMBINATION_TEST_CASES)
33+
def test_combination(self, n: int, k: int, expected: List[List[int]]):
34+
actual = combine(n, k)
35+
self.assertEqual(expected, actual)
36+
37+
@parameterized.expand(COMBINATION_TEST_CASES)
38+
def test_combination_2(self, n: int, k: int, expected: List[List[int]]):
39+
actual = combine_2(n, k)
40+
self.assertEqual(expected, actual)
41+
42+
43+
if __name__ == "__main__":
44+
unittest.main()

algorithms/backtracking/combination/test_combination_3.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,19 @@
11
import unittest
2-
from . import combination_sum_3
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.backtracking.combination import combination_sum_3
35

6+
COMBINATION_SUM_3_TEST_CASES = [
7+
(3, 7, [[1, 2, 4]]),
8+
(3, 9, [[1, 2, 6], [1, 3, 5], [2, 3, 4]]),
9+
(4, 1, []),
10+
(1, 1, [[1]]),
11+
]
412

5-
class CombinationSumTestCase(unittest.TestCase):
6-
def test_1(self):
7-
"""should return [[1,2,4]] for k=3 & n=7"""
8-
k = 3
9-
n = 7
10-
expected = [[1, 2, 4]]
11-
actual = combination_sum_3(k, n)
12-
self.assertEqual(expected, actual)
1313

14-
def test_2(self):
15-
"""should return [[1,2,4]] for k=3 & n=9"""
16-
k = 3
17-
n = 9
18-
expected = [[1, 2, 6], [1, 3, 5], [2, 3, 4]]
19-
actual = combination_sum_3(k, n)
20-
self.assertEqual(expected, actual)
21-
22-
def test_3(self):
23-
"""should return [] for k=4 & n=1"""
24-
k = 4
25-
n = 1
26-
expected = []
14+
class CombinationSumTestCase(unittest.TestCase):
15+
@parameterized.expand(COMBINATION_SUM_3_TEST_CASES)
16+
def test_combination_sum_3(self, k: int, n: int, expected: List[List[int]]):
2717
actual = combination_sum_3(k, n)
2818
self.assertEqual(expected, actual)
2919

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Append Characters to String to Make Subsequence
2+
3+
You are given two strings s and t consisting of only lowercase English letters.
4+
5+
Return the minimum number of characters that need to be appended to the end of s so that t becomes a subsequence of s.
6+
7+
A subsequence is a string that can be derived from another string by deleting some or no characters without changing the
8+
order of the remaining characters.
9+
10+
## Examples
11+
12+
13+
Example 1:
14+
15+
```text
16+
Input: s = "coaching", t = "coding"
17+
Output: 4
18+
Explanation: Append the characters "ding" to the end of s so that s = "coachingding".
19+
Now, t is a subsequence of s ("coachingding").
20+
It can be shown that appending any 3 characters to the end of s will never make t a subsequence.
21+
```
22+
23+
Example 2:
24+
25+
```text
26+
Input: s = "abcde", t = "a"
27+
Output: 0
28+
Explanation: t is already a subsequence of s ("abcde").
29+
```
30+
31+
Example 3:
32+
```text
33+
Input: s = "z", t = "abcde"
34+
Output: 5
35+
Explanation: Append the characters "abcde" to the end of s so that s = "zabcde".
36+
Now, t is a subsequence of s ("zabcde").
37+
It can be shown that appending any 4 characters to the end of s will never make t a subsequence.
38+
```
39+
40+
## Constraints
41+
42+
- 1 <= s.length, t.length <= 105
43+
- `s` and `t` consist only of lowercase English letters.
44+
45+
## Topics
46+
47+
- Two Pointers
48+
- String
49+
- Greedy
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
def append_characters(source: str, target: str) -> int:
2+
if source == target:
3+
return 0
4+
5+
target_len = len(target)
6+
ptr_target = 0
7+
8+
for char in source:
9+
if ptr_target >= target_len:
10+
break
11+
12+
target_char = target[ptr_target]
13+
if char == target_char:
14+
ptr_target += 1
15+
16+
return target_len - ptr_target
17+
18+
19+
def append_characters_2(source, target):
20+
source_index = 0 # current position in source
21+
target_index = 0 # next character index to match in target
22+
source_length = len(source)
23+
target_length = len(target)
24+
25+
# Walk through source and try to match target in order
26+
while source_index < source_length and target_index < target_length:
27+
if source[source_index] == target[target_index]:
28+
target_index += (
29+
1 # matched target[target_index], move to the next needed char
30+
)
31+
source_index += 1 # always advance in source
32+
33+
# target_length - target_index is exactly how many characters remain unmatched in target
34+
# and therefore must be appended to source.
35+
return target_length - target_index
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from algorithms.two_pointers.append_chars_to_make_subsequence import (
4+
append_characters,
5+
append_characters_2,
6+
)
7+
8+
APPEND_CHARS_TO_MAKE_SUBSEQUENCE_TEST_CASES = [
9+
("a", "a", 0),
10+
("a", "b", 1),
11+
("z", "zzzz", 3),
12+
("cba", "abc", 2),
13+
("abcde", "ace", 0),
14+
("xyz", "abc", 3),
15+
("axbyc", "abcde", 2),
16+
("ab", "aba", 1),
17+
("abc", "abcbc", 2),
18+
("abcde", "a", 0),
19+
("coaching", "coding", 4),
20+
("z", "abcde", 5),
21+
]
22+
23+
24+
class AppendCharactersTestCase(unittest.TestCase):
25+
@parameterized.expand(APPEND_CHARS_TO_MAKE_SUBSEQUENCE_TEST_CASES)
26+
def test_append_characters(self, source: str, target: str, expected: int):
27+
actual = append_characters(source, target)
28+
self.assertEqual(expected, actual)
29+
30+
@parameterized.expand(APPEND_CHARS_TO_MAKE_SUBSEQUENCE_TEST_CASES)
31+
def test_append_characters_2(self, source: str, target: str, expected: int):
32+
actual = append_characters_2(source, target)
33+
self.assertEqual(expected, actual)
34+
35+
36+
if __name__ == "__main__":
37+
unittest.main()

0 commit comments

Comments
 (0)