The problem is
Given a non-negative integer
n
, return all valid parentheses strings withn
open parens andn
close parens.
My algorithm (represented by the MySolution
class below) keeps a count of the open parentheses on the stack, and does a recursive depth-first search of possible strings.
Another algorithm (represented by the TheirSolution
class) uses a similar strategy, but their solution reliably runs about twice as fast, when optimization is turned off. The outputs are identical.
The major difference is that their solution copies the current string on every recursive call to Dfs
, so they never have to delete any characters from the end of strings; they just throw away the whole copy when the current stack frame ends. Another difference is that they keep track of the number of open parens left to use (pos
) and close parens left to use (neg
) explicitly, while my code does so implicitly.
Their code involves roughly the same number of recursions that mine does.
int main()
{
constexpr int n_parens = 11;
MySolution sol1;
TheirSolution sol2;
sol1.generateParenthesis(n_parens);
sol2.generateParenthesis(n_parens);
}
Produces (with debug config)
There were 290511 recursions.
Timer took 0.0245487 seconds.
There were 290510 recursions.
Timer took 0.0118699 seconds.
Program ended with exit code: 0
int main()
{
constexpr int n_parens = 11;
MySolution sol1;
TheirSolution sol2;
std::vector<std::string> v = sol1.generateParenthesis(n_parens);
std::vector<std::string> w = sol2.generateParenthesis(n_parens);
std::cout << (int)(v == w) << std::endl;
}
Produces (with release config)
There were 290511 recursions.
Timer took 0.00570454 seconds.
There were 290510 recursions.
Timer took 0.00704376 seconds.
1
Program ended with exit code: 0
Here's the code!
#include <iostream>
#include <chrono>
#include <vector>
#include <string>
#include <iomanip>
#include <algorithm>
struct Timer
{
std::chrono::time_point<std::chrono::steady_clock> start, end;
std::chrono::duration<float> duration;
Timer()
{
start = std::chrono::high_resolution_clock::now();
}
~Timer()
{
end = std::chrono::high_resolution_clock::now();
duration = end - start;
std::cout << "Timer took " << duration.count() << " seconds." << std::endl;
}
};
class TheirSolution {
public:
void Dfs(int deep, int border, int cnt, char c, std::vector<std::string>& v, int pos, int neg, std::string s)
{
n_recursions++;
s.append(1, c);
if (deep == border && pos == 0 && neg == 0)
{
v.push_back(s);
return;
}
if (pos > 0)
{
Dfs(deep + 1, border, cnt + 1, '(', v, pos - 1, neg, s);
}
if (cnt > 0 && neg > 0)
{
Dfs(deep + 1, border, cnt - 1, ')', v, pos, neg - 1, s);
}
}
std::vector<std::string> generateParenthesis(int n) {
n_recursions = 0;
Timer timer;
int positive, negative;
positive = negative = n;
std::vector<std::string> answer;
std::string s = "";
Dfs(1, 2 * n, 1, '(', answer, positive - 1, negative, s);
std::cout << "There were " << n_recursions << " recursions." << std::endl;
return answer;
}
private:
long long n_recursions;
};
class MySolution {
public:
std::vector<std::string> generateParenthesis(int _n)
{
Timer timer;
n_recursions = 0;
n = _n;
dfs(0);
std::cout << "There were " << n_recursions << " recursions." << std::endl;
return ret;
}
private:
void dfs(int stack_count)
{
n_recursions++;
if (stack_count == -1)
return;
else if (current_string.size() == 2*n)
{
if (stack_count == 0)
ret.push_back(current_string);
return;
}
if (stack_count < 2*n - current_string.size())
{
current_string.push_back('(');
dfs(stack_count + 1);
current_string.pop_back();
}
if (stack_count > 0)
{
current_string.push_back(')');
dfs(stack_count - 1);
current_string.pop_back();
}
}
long long n_recursions;
int n;
std::string current_string;
std::vector<std::string> ret;
};
int main()
{
constexpr int n_parens = 11;
MySolution sol1;
TheirSolution sol2;
sol1.generateParenthesis(n_parens);
sol2.generateParenthesis(n_parens);
}
std::cout
calls) in your execution times. These should be outside the timed code block, as they can be relatively slow and introduce great variability in timings. \$\endgroup\$