Skip to content

Commit bdca77c

Browse files
authored
Merge pull request #158 from BrianLusina/feat/algorithms-dynamic-programming-interleaving-string
feat(algorithms, dynamic-programming): interleaving string
2 parents 4925964 + 62a86dc commit bdca77c

22 files changed

+237
-0
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
* [Test Max Duffle Bag Value](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/duffle_bug_value/test_max_duffle_bag_value.py)
6868
* House Robber
6969
* [Test House Robber](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/house_robber/test_house_robber.py)
70+
* Interleaving String
71+
* [Test Interleaving String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/interleaving_string/test_interleaving_string.py)
7072
* Knapsack 01
7173
* [Test Knapsack 01](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/dynamic_programming/knapsack_01/test_knapsack_01.py)
7274
* Longest Common Subsequence
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Interleaving String
2+
3+
You’re given three strings: s1, s2, and s3. Your task is to determine whether s3 can be formed by interleaving s1 and s2.
4+
5+
Interleaving means you take both strings and break them into smaller pieces (substrings), then merge those pieces while
6+
preserving the left-to-right order of characters within each original string.
7+
8+
- s = s1 + s2 + ... + sn
9+
- t = t1 + t2 + ... + tm
10+
- |n - m| <= 1
11+
12+
The final mixed string might look like any of the following:
13+
14+
1. s1 + t1 + s2 + t2 + s3 + t3 + ... or
15+
2. t1 + s1 + t2 + s2 + t3 + s3 + ...
16+
17+
The pieces from s1 and s2 must appear in alternating segments, although either one is allowed to start first. The number
18+
of segments taken from each string should differ by at most one.
19+
20+
## Constraints
21+
22+
- 0 <= s1.length, s2.length <= 100
23+
- 0 <= s3.length <= 200
24+
- s1, s2, and s3 consist of lowercase English letters.
25+
26+
## Topics
27+
28+
- Strings
29+
- Dynamic Programming
30+
31+
## Examples
32+
33+
![Example 1](./images/examples/interleaving_string_example_1.png)
34+
![Example 2](./images/examples/interleaving_string_example_2.png)
35+
![Example 3](./images/examples/interleaving_string_example_3.png)
36+
![Example 4](./images/examples/interleaving_string_example_4.png)
37+
![Example 5](./images/examples/interleaving_string_example_5.png)
38+
![Example 6](./images/examples/interleaving_string_example_6.png)
39+
![Example 7](./images/examples/interleaving_string_example_7.png)
40+
41+
> Input: s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
42+
> Output: true
43+
> Explanation: One way to obtain s3 is:
44+
> Split s1 into s1 = "aa" + "bc" + "c", and s2 into s2 = "dbbc" + "a".
45+
> Interleaving the two splits, we get "aa" + "dbbc" + "bc" + "a" + "c" = "aadbbcbcac".
46+
> Since s3 can be obtained by interleaving s1 and s2, we return true.
47+
48+
## Solution
49+
50+
The goal is to determine whether the string s3 can be formed by interleaving the characters of s1 and s2 while
51+
preserving the original character order of both strings. This problem naturally lends itself to a dynamic programming
52+
approach because whether a prefix of s3 is valid depends on whether smaller prefixes were valid before it.
53+
54+
Instead of building a full 2D DP table, we use a 1D DP array to save space. In this array, dp[j] represents whether the
55+
prefix s3[:i + j] can be formed by interleaving s1[:i] and s2[:j]. Although this 1D array conceptually corresponds to the
56+
(i, j)-cell of a 2D grid, we will reuse the same array row by row, updating values in place as it progresses through the
57+
characters of s1 and s2.
58+
59+
We start from the simplest case, two empty strings forming an empty string, and work our way up. For each character in
60+
s3, we check if it could have come from the next available character in s1 or s2.
61+
62+
A state is considered possible only if:
63+
64+
- The previous state was valid.
65+
- The character we take (from s1 or s2) matches the next character in s3.
66+
67+
The computation continues until all combinations of prefixes have been evaluated. The final answer resides in dp[len(s2)],
68+
which indicates whether all of s1 and s2 can interleave to form all of s3.
69+
70+
The algorithm can be broken down into the following steps:
71+
72+
1. We first check if the length of s3 is equal to the sum of the lengths of s1 and s2. If not, a valid interleaving is
73+
impossible, so we immediately return FALSE.
74+
2. Next, we create a 1D array, dp, of size len(s2) + 1, initially filled with FALSE values. This array represents a
75+
compressed version of the conceptual DP table, where dp[j] indicates whether s3[:i + j] can be formed using s1[:i] and
76+
s2[:j]. As the array is reused for each row, dp[j] always holds the result for the current (i, j) state.
77+
3. We iterate through all prefixes of s1 and s2 using two loops, i goes from 0 to len(s1), and j goes from 0 to len(s2):
78+
- For each pair (i, j), dp[j] is updated according to one of four cases:
79+
- If i == 0 and j == 0, we are matching empty prefixes. We set dp[0] to TRUE because two empty strings always form
80+
an empty string.
81+
- Else if i == 0 (we are in the first row), we can only use characters from s2. dp[j] becomes TRUE only if dp[j − 1]
82+
was TRUE and s2[j − 1] matches s3[j − 1].
83+
- Else if j == 0 (we are in the first column), we can only use characters from s1. Because dp is a 1D array, dp[0]
84+
still holds the result from the previous row (i − 1). We update dp[0] to TRUE only if its previous value was TRUE
85+
and s1[i − 1] matches s3[i − 1].
86+
- Otherwise, i > 0 and j > 0, we evaluate the general case, dp[j] becomes TRUE if either of the following is possible:
87+
- Taking the next character from s1: dp[j] must have been TRUE (its old value from the previous row), and s1[i − 1]
88+
must match s3[i + j − 1].
89+
- Taking the next character from s2: dp[j − 1] must be TRUE (already updated for the current row), and s2[j − 1]
90+
must match s3[i + j − 1].
91+
- If either option is valid, dp[j] is set to TRUE.
92+
4. After all iterations are complete, return dp[len(s2)] containing the final answer, telling all of s1 and s2 can
93+
interleave to form all of s3.
94+
95+
![Solution 1](./images/solutions/interleaving_string_solution_1.png)
96+
![Solution 2](./images/solutions/interleaving_string_solution_2.png)
97+
![Solution 3](./images/solutions/interleaving_string_solution_3.png)
98+
![Solution 4](./images/solutions/interleaving_string_solution_4.png)
99+
![Solution 5](./images/solutions/interleaving_string_solution_5.png)
100+
![Solution 6](./images/solutions/interleaving_string_solution_6.png)
101+
![Solution 7](./images/solutions/interleaving_string_solution_7.png)
102+
![Solution 8](./images/solutions/interleaving_string_solution_8.png)
103+
![Solution 9](./images/solutions/interleaving_string_solution_9.png)
104+
![Solution 10](./images/solutions/interleaving_string_solution_10.png)
105+
![Solution 11](./images/solutions/interleaving_string_solution_11.png)
106+
107+
### Time Complexity
108+
109+
The solution’s time complexity is O(m×n), where m is the length of string s1 and n is the length of string s2. The
110+
algorithm iterates through every combination of prefixes of s1 and s2. Specifically, the outer loop runs len(s1) + 1
111+
times and the inner loop runs len(s2) + 1 times, resulting in a total of m+1×n+1 iterations. Each iteration performs
112+
constant-time operations, so the total time is quadratic in the lengths of the input strings.
113+
114+
### Space Complexity
115+
116+
The solution’s space complexity is O(n) because instead of storing a full 2D DP table, it uses a 1D array of size
117+
len(s2) + 1. This array is updated in place for each row, and no additional data structures are used that are
118+
proportional to the input size.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from functools import cache
2+
3+
4+
def is_interleave_dp_top_down(s1: str, s2: str, s3: str) -> bool:
5+
"""
6+
Checks if it is possible to form string 3 from s1 and s2 by interleaving them such that either characters from s1
7+
start or characters from s2 start.
8+
Args:
9+
s1(str): first string
10+
s2(str): second string
11+
s3(string): potentially interleaved string
12+
Returns:
13+
bool: True if it is possible to interleave s1 and s2 to form s3, False otherwise
14+
"""
15+
len_s1, len_s2, len_s3 = len(s1), len(s2), len(s3)
16+
17+
# If the length of s1 and s2 sum up to a value less than s3, we cannot interleave them to form s3
18+
if (len_s1 + len_s2) != len_s3:
19+
return False
20+
21+
@cache
22+
def dfs(p1: int, p2: int) -> bool:
23+
"""
24+
Recursively check if can form the remaining part of s3 from s1 and s2
25+
Args:
26+
p1(int): current index at s1
27+
p2(int): current index at s2
28+
Returns:
29+
bool: True if characters at (p1 + p2) can form the remaining part of s3
30+
"""
31+
# base case, if we have used all the characters from both strings, we return True
32+
if p1 >= len_s1 and p2 >= len_s2:
33+
return True
34+
35+
# Get the current position in s3
36+
p3 = p1 + p2
37+
38+
# If we can still iterate through s1 and the character at the current position at s3 equals s1, we can take the
39+
# character from s1
40+
if p1 < len_s1 and s1[p1] == s3[p3]:
41+
# Recursively call dfs to check if the rest can form a valid interleaving string
42+
if dfs(p1 + 1, p2):
43+
return True
44+
if p2 < len_s2 and s2[p2] == s3[p3]:
45+
# Recursively call dfs to check if the rest can form a valid interleaving string
46+
if dfs(p1, p2 + 1):
47+
return True
48+
return False
49+
50+
return dfs(0, 0)
51+
52+
53+
def is_interleave_dp_bottom_up(s1, s2, s3):
54+
len_s1, len_s2, len_s3 = len(s1), len(s2), len(s3)
55+
# If lengths don't add up, interleaving is impossible
56+
if len_s3 != len_s1 + len_s2:
57+
return False
58+
59+
# 1D DP array; dp[j] represents using s1[:i] and s2[:j]
60+
dp = [False] * (len_s2 + 1)
61+
62+
# Iterate over prefixes of s1 (i) and s2 (j)
63+
for i in range(len_s1 + 1):
64+
for j in range(len_s2 + 1):
65+
if i == 0 and j == 0:
66+
# Empty + empty = empty → valid
67+
dp[j] = True
68+
69+
elif i == 0:
70+
# First row: can only use s2's characters
71+
dp[j] = dp[j - 1] and s2[j - 1] == s3[j - 1]
72+
73+
elif j == 0:
74+
# First column: can only use s1's characters
75+
dp[j] = dp[j] and s1[i - 1] == s3[i - 1]
76+
77+
else:
78+
# General case: take from s1 or s2 if they match s3
79+
take_from_s1 = dp[j] and s1[i - 1] == s3[i + j - 1]
80+
take_from_s2 = dp[j - 1] and s2[j - 1] == s3[i + j - 1]
81+
dp[j] = take_from_s1 or take_from_s2
82+
83+
return dp[len(s2)]
30.3 KB
Loading
33.9 KB
Loading
29.8 KB
Loading
40.2 KB
Loading
32.7 KB
Loading
36.8 KB
Loading
133 KB
Loading

0 commit comments

Comments
 (0)