算法——括号问题及其衍生

求最长合法括号序列问题

给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。
leetcode

解题思路:

  1. 利用一个前缀和的思想,顺序遍历,遇到左括号和加一;遇到右括号,和减一。当和为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

解题思路:因为要求最少的删除数,所以,一定是要删除括号中多的那一类,或者是次序混乱的部分。

  1. 首先通过一次遍历,判断需要删除的左右括号的数量。如果左括号多的数量为0,出现了右括号那么这个右括号就是该删掉的,如果出现了左括号,那先记着这个左括号是改删的,待会儿出现了右括号于其配对,那么该删的左括号减一。
  2. 然后进行dfs,从一个空低字符串开始添加。
    1. 如果遇到不是括号的字符,就直接添加进去;
    2. 如果遇到左括号,那么就判断左括号是不是需要删除,如果需要删除,就增加遍历索引,同时新构造的字符串中不添加这个左括号。
    3. 右括号相似处理。
  3. 直到遍历完这个字符串,查看左右括号数是否相等,如果相等,就是一个合法的串。
  4. 这里利用到了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);
    }
}
原文地址:https://www.cnblogs.com/lippon/p/14117636.html