STACK › PARENTHESES PROBLEM
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →Read the string from left to right and track the number of unmatched opening parentheses. A closing parenthesis is usable only when that count is positive. If the count is zero, no earlier opening parenthesis is available to match it, so keeping this closing parenthesis would make the current prefix invalid. Removing it is forced; no later character can repair a prefix that already has too many closings.
After that scan, every prefix has at least as many opening parentheses as closing parentheses, but the whole string may still contain unmatched openings at the end. Those openings cannot be paired with a later closing parenthesis because there is no later character left. Scan from right to left and remove an opening parenthesis whenever no unmatched closing parenthesis is available for it. Letters never affect either balance and are always preserved.
The two scans remove only forced characters. A closing parenthesis removed on the first pass cannot participate in a valid result, and an opening parenthesis removed on the second pass has no possible partner. Every parenthesis kept by the scans is paired with a parenthesis on the correct side, so the result is valid and the number of removals is minimum.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(n) | The first scan examines each input character once, and the second scan examines each character that survived the first scan, at most n characters. Reversing the result is another linear pass, so the total number of character operations is at most a constant number of passes over n characters. |
| Space | O(n) extra | The returned string is required output and is excluded, but ans is a separate working string that can contain n characters; result also temporarily stores the output in reverse order. The counters use O(1) space, while the extra string storage degrades to O(n) on inputs such as a long sequence of opening parentheses. |
#include <algorithm>
#include <string>
using namespace std;
class Solution {
public:
string minRemoveToMakeValid(string s) {
string ans;
int open = 0;
for (char c : s) {
if (c == '(') {
open++;
ans.push_back(c);
} else if (c == ')') {
if (open > 0) {
open--;
ans.push_back(c);
}
} else {
ans.push_back(c);
}
}
string result;
int close = 0;
for (int i = static_cast<int>(ans.size()) - 1; i >= 0; i--) {
char c = ans[i];
if (c == ')') {
close++;
result.push_back(c);
} else if (c == '(') {
if (close > 0) {
close--;
result.push_back(c);
}
} else {
result.push_back(c);
}
}
reverse(result.begin(), result.end());
return result;
}
};The important placement is the test before appending a closing parenthesis. When open is zero, that closing parenthesis is not merely inconvenient; it is impossible to match without changing the order or inserting a character, neither of which the problem allows. The backward pass uses the symmetric rule for opening parentheses, and the final reverse restores the original left-to-right order.
A natural alternative is to record the indices of unmatched opening parentheses in a stack and mark every unmatched closing parenthesis for deletion. When a closing parenthesis arrives, match it with the most recent opening index if one exists; otherwise mark the closing index. At the end, mark every opening index still in the stack, then build the answer from unmarked characters.
#include <string>
#include <vector>
using namespace std;
class Solution {
public:
string minRemoveToMakeValid(string s) {
vector<int> openIndices;
vector<bool> removed(s.size(), false);
for (int i = 0; i < static_cast<int>(s.size()); i++) {
if (s[i] == '(') {
openIndices.push_back(i);
} else if (s[i] == ')') {
if (!openIndices.empty()) {
openIndices.pop_back();
} else {
removed[i] = true;
}
}
}
for (int index : openIndices) {
removed[index] = true;
}
string result;
for (int i = 0; i < static_cast<int>(s.size()); i++) {
if (!removed[i]) {
result.push_back(s[i]);
}
}
return result;
}
};This is a different arrangement rather than an optimisation. It still takes O(n) time, but the stack of opening indices and the removal marks use O(n) extra space. It can be easier to extend when you need the positions of removed characters, while the two-pass counter solution is shorter and uses only counts to make its decisions.