求最长合法括号序列问题
给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。
leetcode
解题思路:
- 利用一个前缀和的思想,顺序遍历,遇到左括号和加一;遇到右括号,和减一。当和为0的时候,就是一个合法序列。如果和为负数,那么以当前起点产生的序列不再可能产生合法的序列,因此,将起点设置到当前点的下一个位置,并将和置零重新累加。需要注意的是,如果只从一个方向遍历,会漏掉有效括号在在中间的情况,因此,在逆序遍历一遍就好了,同时,条件也要相反。
class Solution {
public int longestValidParentheses(String s) {
int res = 0, sum = 0;
for(int i = 0, j = 0; i < s.length(); i++) {
if(s.charAt(i) == '(') {
sum ++;
} else {
sum --;
}
if(sum == 0) {
res = Math.max(res, i - j + 1);
} else if(sum < 0) {
j = i + 1;
sum = 0;
}
}
sum = 0;
for(int i = s.length() - 1, j = s.length() - 1; i >= 0 ; i--) {
if(s.charAt(i) == ')') {
sum ++;
} else {
sum --;
}
if(sum == 0) {
res = Math.max(res, j - i + 1);
} else if(sum < 0) {
j = i - 1;
sum = 0;
}
}
return res;
}
}
删除无效的括号
删除最小数量的无效括号,使得输入的字符串有效,返回所有可能的结果。
说明: 输入可能包含了除 ( 和 ) 以外的字符。
leetcode
解题思路:因为要求最少的删除数,所以,一定是要删除括号中多的那一类,或者是次序混乱的部分。
- 首先通过一次遍历,判断需要删除的左右括号的数量。如果左括号多的数量为0,出现了右括号那么这个右括号就是该删掉的,如果出现了左括号,那先记着这个左括号是改删的,待会儿出现了右括号于其配对,那么该删的左括号减一。
- 然后进行dfs,从一个空低字符串开始添加。
- 如果遇到不是括号的字符,就直接添加进去;
- 如果遇到左括号,那么就判断左括号是不是需要删除,如果需要删除,就增加遍历索引,同时新构造的字符串中不添加这个左括号。
- 右括号相似处理。
- 直到遍历完这个字符串,查看左右括号数是否相等,如果相等,就是一个合法的串。
- 这里利用到了HashSet,因为再遍历连续相同的括号的时候,会产生一样的结果。
class Solution {
HashSet<String> set;
public List<String> removeInvalidParentheses(String s) {
List<String> res = new ArrayList<>();
set = new HashSet<>();
int r = 0, l = 0;
// 统计该删除的左右括号的数量
for(char c : s.toCharArray()) {
if(c == '(') l ++;
else if(c == ')'){
if(l == 0) r ++;
else {
l --;
}
}
}
dfs(s, 0, "", 0, l, r);
for(String ss : set) res.add(ss);
return res;
}
// s: 就是这个需要遍历的原始字符串
// u:遍历字符串的时候需要使用的索引
// path:一种结果
// cnt:统计左右括号的和
// l:需要删除的左括号的数量
// r:需要删除的右括号的数量
public void dfs(String s, int u, String path, int cnt, int l, int r) {
// 如果括号和小于零,那就是一个非法序列
if(cnt < 0) return;
if(s.length() == u) {
// 遍历到了最后 如果括号和为0,说明是个合法序列
if(cnt == 0) set.add(path);
return;
}
// 如果不是括号,就直接添加到当前序列后面
if(s.charAt(u) != '(' && s.charAt(u) != ')') dfs(s, u + 1, path + s.charAt(u), cnt, l, r);
else {
if(s.charAt(u) == '(') {
// 如果有需要删除的左括号,那就忽略这个括号,同时需要删除的左括号数量减一
if(l > 0) dfs(s, u + 1, path, cnt, l - 1, r);
// 先不删这个左括号,括号和加一
dfs(s, u + 1, path + '(', cnt + 1, l, r);
} else {
// 同上
if(r > 0) dfs(s, u + 1, path, cnt, l, r - 1);
dfs(s, u + 1, path + ')', cnt - 1, l, r);
}
}
}
}
下面这个是改进版本,不太好理解。
就是将连续相同的括号进行特殊处理。
class Solution {
List<String> res;
public List<String> removeInvalidParentheses(String s) {
res = new ArrayList<>();
int r = 0, l = 0;
for(char c : s.toCharArray()) {
if(c == '(') l ++;
else if(c == ')'){
if(l == 0) r ++;
else {
l --;
}
}
}
dfs(s, 0, "", 0, l, r);
return res;
}
public void dfs(String s, int u, String path, int cnt, int l, int r) {
if(s.length() == u) {
if(cnt == 0) res.add(path);
return;
}
if(s.charAt(u) != '(' && s.charAt(u) != ')') dfs(s, u + 1, path + s.charAt(u), cnt, l, r);
else {
if(s.charAt(u) == '(') {
int k = u;
while(k < s.length() && s.charAt(k) == '(') k++;
l -= k - u;
for(int i = k - u; i >= 0; i--) {
if(l >= 0) dfs(s, k, path, cnt, l, r);
path += '(';
cnt ++;
l ++;
}
} else {
int k = u;
while(k < s.length() && s.charAt(k) == ')') k++;
r -= k - u;
for(int i = k - u; i >= 0; i--) {
if(cnt >= 0 && r >= 0) dfs(s, k, path, cnt, l, r);
path += ')';
cnt --;
r ++;
}
}
}
}
}
衍生问题
最长匹配对
给定一个放有字符和数字的数组,找到最长的子数组,且包含的字符和数字的个数相同。
返回该子数组,若存在多个最长子数组,返回左端点最小的。若不存在这样的数组,返回一个空数组。
leetcode
解题思路:
-
同样利用前缀和的思想,遇到字符和加一,遇到数字和减一,正确序列的和一定是0。如果只是利用前缀和,需要两层循环,枚举所有可能的组合才行,这样时间是n方的。
-
这里,我们可以借助哈希表,对前面已经运算过的前缀和进行存储,同时对于相同的前缀和,只记录最左边的下标。当遇到相同的前缀和,那么这两个下标之间就是一个正确值,因为和的差为0,说明这个范围内的的数字和字符数量是一样的,是一个合法的答案。
-
不同于括号问题,这里的字符和数字的前后顺序没有关系,只需要数量匹配即可。
class Solution {
public String[] findLongestSubarray(String[] array) {
int n = array.length;
int sum = 0;
Map<Integer, Integer> map = new HashMap<>();
int x = 0, y = 0;
map.put(0, -1);
for(int i = 0; i < n; i++) {
char cur = array[i].charAt(0);
if(Character.isDigit(cur)) {
sum++;
} else {
sum--;
}
if(!map.containsKey(sum)) map.put(sum, i);
else {
if(x - y < i - map.get(sum)){
x = i + 1;
y = map.get(sum) + 1;
}
}
}
return Arrays.copyOfRange(array, y, x);
}
}