Skip to content

Commit 29fc83e

Browse files
authored
feat: solve 1087. Brace Expansion with unit testing (#95)
closes #86
1 parent 669441b commit 29fc83e

File tree

8 files changed

+234
-4
lines changed

8 files changed

+234
-4
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
### 📊 Repository Stats
1616

1717
[![Last Commit](https://img.shields.io/github/last-commit/mathusanm6/LeetCode?style=for-the-badge&logo=git&logoColor=white&color=blue)](https://github.com/mathusanm6/LeetCode/commits/main)
18-
[![C++ Solutions](https://img.shields.io/badge/C%2B%2B%20Solutions-6-blue?style=for-the-badge&logo=cplusplus&logoColor=white)](https://github.com/mathusanm6/LeetCode/tree/main/problems)
19-
[![Python Solutions](https://img.shields.io/badge/Python%20Solutions-6-blue?style=for-the-badge&logo=python&logoColor=white)](https://github.com/mathusanm6/LeetCode/tree/main/problems)
18+
[![C++ Solutions](https://img.shields.io/badge/C%2B%2B%20Solutions-7-blue?style=for-the-badge&logo=cplusplus&logoColor=white)](https://github.com/mathusanm6/LeetCode/tree/main/problems)
19+
[![Python Solutions](https://img.shields.io/badge/Python%20Solutions-7-blue?style=for-the-badge&logo=python&logoColor=white)](https://github.com/mathusanm6/LeetCode/tree/main/problems)
2020

2121
</div>
2222

@@ -199,3 +199,9 @@ This repository covers a comprehensive range of algorithmic patterns and data st
199199
| # | Title | Solution | Time | Space | Difficulty | Tag | Note |
200200
|---|-------|----------|------|-------|------------|-----|------|
201201
| 2313 | [Minimum Flips in Binary Tree to Get Result](https://leetcode.com/problems/minimum-flips-in-binary-tree-to-get-result/) | [Python](./problems/minimum_flips_in_binary_tree_to_get_result/minimum_flips_in_binary_tree_to_get_result.py), [C++](./problems/minimum_flips_in_binary_tree_to_get_result/minimum_flips_in_binary_tree_to_get_result.cc) | _O(n)_ | _O(1)_ | Hard | | _n_ is the number of nodes in the binary tree. |
202+
203+
## Backtracking
204+
205+
| # | Title | Solution | Time | Space | Difficulty | Tag | Note |
206+
|---|-------|----------|------|-------|------------|-----|------|
207+
| 1087 | [Brace Expansion](https://leetcode.com/problems/brace-expansion/) | [Python](./problems/brace_expansion/brace_expansion.py), [C++](./problems/brace_expansion/brace_expansion.cc) | _O(M^K + M log M)_ | _O(M^K)_ | Medium | | M = max choices per brace set, K = number of brace sets. M^K for generating combinations, M log M for sorting. |
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#include "brace_expansion.h"
2+
3+
#include <algorithm>
4+
#include <cstddef>
5+
#include <regex>
6+
#include <string>
7+
#include <utility>
8+
#include <vector>
9+
10+
namespace {
11+
12+
std::vector<std::string> splitByComma(const std::string& str) {
13+
std::vector<std::string> out;
14+
std::size_t start = 0;
15+
while (true) {
16+
const std::size_t pos = str.find(',', start);
17+
if (pos == std::string::npos) {
18+
out.emplace_back(str.substr(start));
19+
break;
20+
}
21+
out.emplace_back(str.substr(start, pos - start));
22+
start = pos + 1;
23+
}
24+
return out;
25+
}
26+
27+
} // namespace
28+
29+
std::vector<std::string> braceExpansion(const std::string& input) {
30+
std::vector<std::string> prefixes;
31+
prefixes.emplace_back(""); // Start with an empty prefix
32+
std::string remaining = input;
33+
34+
// Regex captures (compile once; reuse):
35+
// 1) leading literal before { ([^{}]*)
36+
// 2) choices inside {} ([^}]*)
37+
// 3) literal after } ([^{}]*)
38+
// 4) remaining tail (.*)
39+
static const std::regex REGEX(R"(([^{}]*)\{([^}]*)\}([^{}]*)(.*))");
40+
41+
std::smatch match;
42+
// Iterate group by group until no more braces
43+
while (std::regex_match(remaining, match, REGEX)) {
44+
const std::string& lead = match[1].str();
45+
const std::string& choicesStr = match[2].str();
46+
const std::string& mid = match[3].str();
47+
std::string tail = match[4].str(); // will become new remaining
48+
49+
// Expand choices and sort them
50+
std::vector<std::string> choices = splitByComma(choicesStr);
51+
if (choices.size() > 1) {
52+
std::ranges::sort(choices);
53+
}
54+
55+
std::vector<std::string> newPrefixes;
56+
newPrefixes.reserve(prefixes.size() * std::max(choices.size(), 1UL));
57+
for (const auto& prefix : prefixes) {
58+
for (const auto& choice : choices) {
59+
std::string newPrefix = prefix;
60+
newPrefix += lead;
61+
newPrefix += choice;
62+
newPrefix += mid;
63+
newPrefixes.emplace_back(std::move(newPrefix));
64+
}
65+
}
66+
67+
prefixes.swap(newPrefixes); // reuse memory
68+
remaining.swap(tail); // continue with the tail
69+
}
70+
71+
// Append final remaining tail (no more braces)
72+
std::vector<std::string> results;
73+
results.reserve(prefixes.size());
74+
if (!prefixes.empty()) {
75+
for (const auto& prefix : prefixes) {
76+
std::string result = prefix;
77+
result += remaining;
78+
results.emplace_back(std::move(result));
79+
}
80+
} else {
81+
results.emplace_back(remaining); // input had no braces
82+
}
83+
84+
return results;
85+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#include <string>
2+
#include <vector>
3+
4+
std::vector<std::string> braceExpansion(const std::string& input);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import re
2+
3+
from typing import List
4+
5+
6+
def braceExpansion(s: str) -> List[str]:
7+
results: List[str] = []
8+
9+
def backtrack(prefixes: List[str], remaining: str) -> None:
10+
"""
11+
Recursive helper that expands the string.
12+
13+
prefixes : partial expansions built so far
14+
remaining: the remaining part of the string to process
15+
"""
16+
nonlocal results
17+
18+
# Regex captures:
19+
# 1) leading literal before { ([^{}]*)
20+
# 2) choices inside {} ([^}]*)
21+
# 3) literal after } ([^{}]*)
22+
# 4) remaining tail (.*)
23+
match = re.search(r"([^{}]*)\{([^}]*)\}([^{}]*)(.*)", remaining)
24+
25+
if not match:
26+
# Base case: no more braces left
27+
if prefixes:
28+
# Append remaining to each existing prefix
29+
results.extend(prefix + remaining for prefix in prefixes)
30+
else:
31+
# No prefix: just add the raw remaining string
32+
results.append(remaining)
33+
return
34+
35+
lead, choices_str, mid, tail = match.groups()
36+
choices = sorted(choices_str.split(",")) # expand into sorted list of choices
37+
38+
# If no prefixes yet, start from empty string
39+
base_prefixes = prefixes if prefixes else [""]
40+
new_prefixes: List[str] = []
41+
42+
# Build new partial expansions by combining each prefix with each choice
43+
for prefix in base_prefixes:
44+
for choice in choices:
45+
new_prefixes.append(prefix + lead + choice + mid)
46+
47+
# Recurse on the tail of the string
48+
backtrack(new_prefixes, tail)
49+
50+
# Start recursion with empty prefix and full string
51+
backtrack([], s)
52+
53+
return results
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#include "brace_expansion.h"
2+
3+
#include <gtest/gtest.h>
4+
#include <string>
5+
#include <vector>
6+
7+
struct BraceExpansionCase {
8+
const std::string test_name;
9+
const std::string input;
10+
const std::vector<std::string> expected;
11+
};
12+
13+
using BraceExpansionTest = ::testing::TestWithParam<BraceExpansionCase>;
14+
15+
TEST_P(BraceExpansionTest, TestCases) {
16+
const BraceExpansionCase &testCase = GetParam();
17+
const auto result = braceExpansion(testCase.input);
18+
EXPECT_EQ(result, testCase.expected);
19+
}
20+
21+
INSTANTIATE_TEST_SUITE_P(BraceExpansionTestCases, BraceExpansionTest,
22+
::testing::Values(
23+
BraceExpansionCase{
24+
.test_name = "BasicCase",
25+
.input = "{a,b}c{d,e}f",
26+
.expected = {"acdf", "acef", "bcdf", "bcef"},
27+
},
28+
BraceExpansionCase{
29+
.test_name = "NoBraces",
30+
.input = "abcd",
31+
.expected = {"abcd"},
32+
},
33+
BraceExpansionCase{
34+
.test_name = "LeadingLiteralFollowedByBraces",
35+
.input = "k{a,b,c,d,e,f,g}",
36+
.expected = {"ka", "kb", "kc", "kd", "ke", "kf", "kg"},
37+
}),
38+
[](const testing::TestParamInfo<BraceExpansionCase> &info) {
39+
return info.param.test_name;
40+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Test cases for the brace_expansion function."""
2+
3+
import pytest
4+
5+
from brace_expansion import braceExpansion
6+
7+
8+
@pytest.mark.parametrize(
9+
"s, expected",
10+
[
11+
("{a,b}c{d,e}f", ["acdf", "acef", "bcdf", "bcef"]), # Basic Case
12+
("abcd", ["abcd"]), # No Braces
13+
(
14+
"k{a,b,c,d,e,f,g}",
15+
["ka", "kb", "kc", "kd", "ke", "kf", "kg"],
16+
), # Leading Literal followed by Braces
17+
],
18+
ids=[
19+
"Basic Case",
20+
"No Braces",
21+
"Leading Literal followed by Braces",
22+
],
23+
)
24+
def test_brace_expansion(s, expected):
25+
assert braceExpansion(s) == expected
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
problem:
2+
number: 1087
3+
title: "Brace Expansion"
4+
leetcode_url: "https://leetcode.com/problems/brace-expansion/"
5+
difficulty: "medium"
6+
tags: ["Backtracking"]
7+
8+
solutions:
9+
python: "problems/brace_expansion/brace_expansion.py"
10+
cpp: "problems/brace_expansion/brace_expansion.cc"
11+
12+
complexity:
13+
time: "O(M^K + M log M)"
14+
space: "O(M^K)"
15+
16+
notes: "M = max choices per brace set, K = number of brace sets. M^K for generating combinations, M log M for sorting."
17+
readme_link: ""

problems/calculate_amount_paid_in_taxes/calculate_amount_paid_in_taxes_test.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ struct CalculateAmountPaidInTaxesCase {
1414
using CalculateAmountPaidInTaxesTest = ::testing::TestWithParam<CalculateAmountPaidInTaxesCase>;
1515

1616
TEST_P(CalculateAmountPaidInTaxesTest, TestCases) {
17-
CalculateAmountPaidInTaxesCase testCase = GetParam();
17+
const CalculateAmountPaidInTaxesCase& testCase = GetParam();
1818
const auto result = calculateAmountPaidInTaxes(testCase.brackets, testCase.income);
1919
EXPECT_EQ(result, testCase.expected);
2020
}
@@ -33,6 +33,6 @@ INSTANTIATE_TEST_SUITE_P(
3333
.brackets = {{2, 50}},
3434
.income = 0,
3535
.expected = 0.00000}),
36-
[](const testing::TestParamInfo<CalculateAmountPaidInTaxesCase> &info) {
36+
[](const testing::TestParamInfo<CalculateAmountPaidInTaxesCase>& info) {
3737
return info.param.test_name;
3838
});

0 commit comments

Comments
 (0)