From fa93407233805e7f203459821e20ab2086a1dd90 Mon Sep 17 00:00:00 2001 From: mathusanm6 Date: Mon, 15 Sep 2025 11:45:27 +0200 Subject: [PATCH] feat: solve 1087. Brace Expansion with unit testing --- README.md | 10 ++- problems/brace_expansion/brace_expansion.cc | 85 +++++++++++++++++++ problems/brace_expansion/brace_expansion.h | 4 + problems/brace_expansion/brace_expansion.py | 53 ++++++++++++ .../brace_expansion/brace_expansion_test.cc | 40 +++++++++ .../brace_expansion/brace_expansion_test.py | 25 ++++++ problems/brace_expansion/config.yml | 17 ++++ .../calculate_amount_paid_in_taxes_test.cc | 4 +- 8 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 problems/brace_expansion/brace_expansion.cc create mode 100644 problems/brace_expansion/brace_expansion.h create mode 100644 problems/brace_expansion/brace_expansion.py create mode 100644 problems/brace_expansion/brace_expansion_test.cc create mode 100644 problems/brace_expansion/brace_expansion_test.py create mode 100644 problems/brace_expansion/config.yml diff --git a/README.md b/README.md index 8cc9aa4..64e7137 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ ### 📊 Repository Stats [![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) -[![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) -[![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) +[![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) +[![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) @@ -199,3 +199,9 @@ This repository covers a comprehensive range of algorithmic patterns and data st | # | Title | Solution | Time | Space | Difficulty | Tag | Note | |---|-------|----------|------|-------|------------|-----|------| | 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. | + +## Backtracking + +| # | Title | Solution | Time | Space | Difficulty | Tag | Note | +|---|-------|----------|------|-------|------------|-----|------| +| 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. | diff --git a/problems/brace_expansion/brace_expansion.cc b/problems/brace_expansion/brace_expansion.cc new file mode 100644 index 0000000..5ec84a6 --- /dev/null +++ b/problems/brace_expansion/brace_expansion.cc @@ -0,0 +1,85 @@ +#include "brace_expansion.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +std::vector splitByComma(const std::string& str) { + std::vector out; + std::size_t start = 0; + while (true) { + const std::size_t pos = str.find(',', start); + if (pos == std::string::npos) { + out.emplace_back(str.substr(start)); + break; + } + out.emplace_back(str.substr(start, pos - start)); + start = pos + 1; + } + return out; +} + +} // namespace + +std::vector braceExpansion(const std::string& input) { + std::vector prefixes; + prefixes.emplace_back(""); // Start with an empty prefix + std::string remaining = input; + + // Regex captures (compile once; reuse): + // 1) leading literal before { ([^{}]*) + // 2) choices inside {} ([^}]*) + // 3) literal after } ([^{}]*) + // 4) remaining tail (.*) + static const std::regex REGEX(R"(([^{}]*)\{([^}]*)\}([^{}]*)(.*))"); + + std::smatch match; + // Iterate group by group until no more braces + while (std::regex_match(remaining, match, REGEX)) { + const std::string& lead = match[1].str(); + const std::string& choicesStr = match[2].str(); + const std::string& mid = match[3].str(); + std::string tail = match[4].str(); // will become new remaining + + // Expand choices and sort them + std::vector choices = splitByComma(choicesStr); + if (choices.size() > 1) { + std::ranges::sort(choices); + } + + std::vector newPrefixes; + newPrefixes.reserve(prefixes.size() * std::max(choices.size(), 1UL)); + for (const auto& prefix : prefixes) { + for (const auto& choice : choices) { + std::string newPrefix = prefix; + newPrefix += lead; + newPrefix += choice; + newPrefix += mid; + newPrefixes.emplace_back(std::move(newPrefix)); + } + } + + prefixes.swap(newPrefixes); // reuse memory + remaining.swap(tail); // continue with the tail + } + + // Append final remaining tail (no more braces) + std::vector results; + results.reserve(prefixes.size()); + if (!prefixes.empty()) { + for (const auto& prefix : prefixes) { + std::string result = prefix; + result += remaining; + results.emplace_back(std::move(result)); + } + } else { + results.emplace_back(remaining); // input had no braces + } + + return results; +} diff --git a/problems/brace_expansion/brace_expansion.h b/problems/brace_expansion/brace_expansion.h new file mode 100644 index 0000000..7fb3d24 --- /dev/null +++ b/problems/brace_expansion/brace_expansion.h @@ -0,0 +1,4 @@ +#include +#include + +std::vector braceExpansion(const std::string& input); diff --git a/problems/brace_expansion/brace_expansion.py b/problems/brace_expansion/brace_expansion.py new file mode 100644 index 0000000..aeb4235 --- /dev/null +++ b/problems/brace_expansion/brace_expansion.py @@ -0,0 +1,53 @@ +import re + +from typing import List + + +def braceExpansion(s: str) -> List[str]: + results: List[str] = [] + + def backtrack(prefixes: List[str], remaining: str) -> None: + """ + Recursive helper that expands the string. + + prefixes : partial expansions built so far + remaining: the remaining part of the string to process + """ + nonlocal results + + # Regex captures: + # 1) leading literal before { ([^{}]*) + # 2) choices inside {} ([^}]*) + # 3) literal after } ([^{}]*) + # 4) remaining tail (.*) + match = re.search(r"([^{}]*)\{([^}]*)\}([^{}]*)(.*)", remaining) + + if not match: + # Base case: no more braces left + if prefixes: + # Append remaining to each existing prefix + results.extend(prefix + remaining for prefix in prefixes) + else: + # No prefix: just add the raw remaining string + results.append(remaining) + return + + lead, choices_str, mid, tail = match.groups() + choices = sorted(choices_str.split(",")) # expand into sorted list of choices + + # If no prefixes yet, start from empty string + base_prefixes = prefixes if prefixes else [""] + new_prefixes: List[str] = [] + + # Build new partial expansions by combining each prefix with each choice + for prefix in base_prefixes: + for choice in choices: + new_prefixes.append(prefix + lead + choice + mid) + + # Recurse on the tail of the string + backtrack(new_prefixes, tail) + + # Start recursion with empty prefix and full string + backtrack([], s) + + return results diff --git a/problems/brace_expansion/brace_expansion_test.cc b/problems/brace_expansion/brace_expansion_test.cc new file mode 100644 index 0000000..b06a17e --- /dev/null +++ b/problems/brace_expansion/brace_expansion_test.cc @@ -0,0 +1,40 @@ +#include "brace_expansion.h" + +#include +#include +#include + +struct BraceExpansionCase { + const std::string test_name; + const std::string input; + const std::vector expected; +}; + +using BraceExpansionTest = ::testing::TestWithParam; + +TEST_P(BraceExpansionTest, TestCases) { + const BraceExpansionCase &testCase = GetParam(); + const auto result = braceExpansion(testCase.input); + EXPECT_EQ(result, testCase.expected); +} + +INSTANTIATE_TEST_SUITE_P(BraceExpansionTestCases, BraceExpansionTest, + ::testing::Values( + BraceExpansionCase{ + .test_name = "BasicCase", + .input = "{a,b}c{d,e}f", + .expected = {"acdf", "acef", "bcdf", "bcef"}, + }, + BraceExpansionCase{ + .test_name = "NoBraces", + .input = "abcd", + .expected = {"abcd"}, + }, + BraceExpansionCase{ + .test_name = "LeadingLiteralFollowedByBraces", + .input = "k{a,b,c,d,e,f,g}", + .expected = {"ka", "kb", "kc", "kd", "ke", "kf", "kg"}, + }), + [](const testing::TestParamInfo &info) { + return info.param.test_name; + }); diff --git a/problems/brace_expansion/brace_expansion_test.py b/problems/brace_expansion/brace_expansion_test.py new file mode 100644 index 0000000..ce5b2b4 --- /dev/null +++ b/problems/brace_expansion/brace_expansion_test.py @@ -0,0 +1,25 @@ +"""Test cases for the brace_expansion function.""" + +import pytest + +from brace_expansion import braceExpansion + + +@pytest.mark.parametrize( + "s, expected", + [ + ("{a,b}c{d,e}f", ["acdf", "acef", "bcdf", "bcef"]), # Basic Case + ("abcd", ["abcd"]), # No Braces + ( + "k{a,b,c,d,e,f,g}", + ["ka", "kb", "kc", "kd", "ke", "kf", "kg"], + ), # Leading Literal followed by Braces + ], + ids=[ + "Basic Case", + "No Braces", + "Leading Literal followed by Braces", + ], +) +def test_brace_expansion(s, expected): + assert braceExpansion(s) == expected diff --git a/problems/brace_expansion/config.yml b/problems/brace_expansion/config.yml new file mode 100644 index 0000000..bc05a05 --- /dev/null +++ b/problems/brace_expansion/config.yml @@ -0,0 +1,17 @@ +problem: + number: 1087 + title: "Brace Expansion" + leetcode_url: "https://leetcode.com/problems/brace-expansion/" + difficulty: "medium" + tags: ["Backtracking"] + +solutions: + python: "problems/brace_expansion/brace_expansion.py" + cpp: "problems/brace_expansion/brace_expansion.cc" + +complexity: + time: "O(M^K + M log M)" + space: "O(M^K)" + +notes: "M = max choices per brace set, K = number of brace sets. M^K for generating combinations, M log M for sorting." +readme_link: "" diff --git a/problems/calculate_amount_paid_in_taxes/calculate_amount_paid_in_taxes_test.cc b/problems/calculate_amount_paid_in_taxes/calculate_amount_paid_in_taxes_test.cc index cda7227..15889b4 100644 --- a/problems/calculate_amount_paid_in_taxes/calculate_amount_paid_in_taxes_test.cc +++ b/problems/calculate_amount_paid_in_taxes/calculate_amount_paid_in_taxes_test.cc @@ -14,7 +14,7 @@ struct CalculateAmountPaidInTaxesCase { using CalculateAmountPaidInTaxesTest = ::testing::TestWithParam; TEST_P(CalculateAmountPaidInTaxesTest, TestCases) { - CalculateAmountPaidInTaxesCase testCase = GetParam(); + const CalculateAmountPaidInTaxesCase& testCase = GetParam(); const auto result = calculateAmountPaidInTaxes(testCase.brackets, testCase.income); EXPECT_EQ(result, testCase.expected); } @@ -33,6 +33,6 @@ INSTANTIATE_TEST_SUITE_P( .brackets = {{2, 50}}, .income = 0, .expected = 0.00000}), - [](const testing::TestParamInfo &info) { + [](const testing::TestParamInfo& info) { return info.param.test_name; });