Leetcode——括号生成 / 有效的括号字符串

1. 括号生成

在这里插入图片描述

(0)回溯(最容易懂的一版)

  • 剩余左右括号数相等,下一个只能用左括号
  • 剩余左括号小于右括号,下一个可以用左括号也可以用右括号
class Solution {
    List<String> res = new ArrayList<>();

    public List<String> generateParenthesis(int n) {
        if (n <= 0)
            return res;

        backTracking("", n, n);
        return res;
    }

    private void backTracking(String str, int left, int right){
        if (left == 0 && right == 0){
            res.add(str);
            return;
        }
        
        if (left == right) {
            //剩余左右括号数相等,下一个只能用左括号
            backTracking(str + "(", left - 1, right);
        } else if (left < right) {
            //剩余左括号小于右括号,下一个可以用左括号也可以用右括号
            if (left > 0) {
                backTracking(str + "(", left - 1, right);
            }
            backTracking(str + ")", left, right - 1);
        }
    }
}

(1)回溯法(减法)

  • 这里用的不是全局变量,每一次会覆盖前面的选择,相当于撤销 。但是用的如果是全局变量(类似迷宫访问那种,就要显式撤销)
  • 主要是跟字符串的特点有关哈,Java 里 + 生成了新的字符串,每次往下面传递的时候,都是新字符串。因此在搜索的时候不用回溯。
  • 可以想象搜索遍历的问题其实就像是做实验,每一次实验都用新的实验材料,那么做完了就废弃了。但是如果只使用一份材料,在做完一次以后,一定需要将它恢复成原样(就是这里「回溯」的意思),才可以做下一次尝试。
  • 下面也给出了 StringBuilder 全程只使用一份变量去搜索的做法,对比这两种解法
    在这里插入图片描述
import java.util.ArrayList;
import java.util.List;
public class Solution {

    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        if (n == 0) {
            return res;
        }

        StringBuilder path = new StringBuilder();
        dfs(path, n, n, res);
        return res;
    }

    /**
     * @param path  从根结点到任意结点的路径,全程只使用一份
     * @param left  左括号还有几个可以使用
     * @param right 右括号还有几个可以使用
     * @param res	结果list
     */
    private void dfs(StringBuilder path, int left, int right, List<String> res) {
        if (left == 0 && right == 0) {
            // path.toString() 生成了一个新的字符串,相当于做了一次拷贝
            res.add(path.toString());
            return;
        }

        // 剪枝(如图,左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
        // 只要当前已使用闭括号 大于 开括号了 ,直接就是无效的括号组合(翻译过来就是 left > right)
        if (left > right) {
            return;
        }

        if (left > 0) {
            path.append("(");
            dfs(path, left - 1, right, res);
            path.deleteCharAt(path.length() - 1);
        }

        if (right > 0) {
            path.append(")");
            dfs(path, left, right - 1, res);
            path.deleteCharAt(path.length() - 1);
        }
    }
}

其实可以直接这样的:

import java.util.ArrayList;
import java.util.List;
public class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        // 特判
        if (n == 0) {
            return res;
        }

        // 执行深度优先遍历,搜索可能的结果
        dfs("", n, n, res);
        return res;
    }
//这里用的不是全局变量,每一次会覆盖前面的选择,相当于撤销 。但是用的如果是全局变量(类似迷宫访问那种,就要显式撤销)
//主要是跟字符串的特点有关哈,Java 里 + 生成了新的字符串,每次往下面传递的时候,都是新字符串。因此在搜索的时候不用回溯。
//可以想象搜索遍历的问题其实就像是做实验,每一次实验都用新的实验材料,那么做完了就废弃了。但是如果只使用一份材料,在做完一次以后,一定需要将它恢复成原样(就是这里「回溯」的意思),才可以做下一次尝试。
//上面也给出了 StringBuilder 全程只使用一份变量去搜索的做法,对比这两种解法
    private void dfs(String curStr, int left, int right, List<String> res) {
        // 因为每一次尝试,都使用新的字符串变量,所以可以无需回溯
        // 在递归终止的时候,直接把它添加到结果集即可,注意与「力扣」第 46 题、第 39 题区分
        if (left == 0 && right == 0) {
            res.add(curStr);
            return;
        }

        // 剪枝(左括号可以使用的个数严格大于右括号可以使用的个数,才剪枝,注意这个细节)
        if (left > right) {
            return;
        }

        if (left > 0) {
            dfs(curStr + "(", left - 1, right, res);
        }

        if (right > 0) {
            dfs(curStr + ")", left, right - 1, res);
        }
    }
}

(2)回溯法(加法)

public class Solution { 
    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        if (n <= 0) 
            return res;
        dfs(n, "", res, 0, 0);
        return res;
    }
    /**
     * @param path  从根结点到任意结点的路径,全程只使用一份
     * @param open  左括号使用了多少个
     * @param close 右括号使用了多少个
     * @param res	结果list
     */
    private void dfs(int n, String path, List<String> res, int open, int close) {
        if (open > n || close > open)       //左括号使用大于n,当前已使用右括号数量大于左括号
            return;

        if (path.length() == 2 * n) {
            res.add(path);
            return;
        }

        dfs(n, path + "(", res, open + 1, close);
        dfs(n, path + ")", res, open, close + 1);
    }
}

换种写法也是可以的,思路跟上面的一致:

public List<String> generateParenthesis(int n) {
    List<String> res = new ArrayList<>();
    if (n <= 0) return res;
    dfs(n, "", res, 0, 0);
    return res;
}

private void dfs(int n, String path, List<String> res, int open, int close) {
    if (path.length() == 2 * n) {
        res.add(path);
        return;
    }
    if (open < n) {			//左括号小于n时,才能遍历左边
        dfs(n, path + "(", res, open + 1, close);
    }
    if (close < open) {		//右括号小于左括号时,才能遍历右边
        dfs(n, path + ")", res, open, close + 1);
    }
}

(3)广度优先遍历

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class Solution {

    class Node {    //建一个树

        private String res;
        private int left;       //剩余右左括号数量
        private int right;      //剩余右括号数量

        public Node(String str, int left, int right) {      
            this.res = str;
            this.left = left;
            this.right = right;
        }
    }

    public List<String> generateParenthesis(int n) {
        List<String> res = new ArrayList<>();
        if (n == 0) {
            return res;
        }
        Queue<Node> queue = new LinkedList<>();
        queue.offer(new Node("", n, n));

        while (!queue.isEmpty()) {

            Node curNode = queue.poll();

            if (curNode.left == 0 && curNode.right == 0) {  //左右括号都使用完时
                res.add(curNode.res);
            }
            
            if (curNode.left > 0) {     //如果左括号还有
                queue.offer(new Node(curNode.res + "(", curNode.left - 1, curNode.right));
            }

            if (curNode.right > 0 && curNode.left < curNode.right) {        //右括号还有, 且当前可用左括号 < 当前可用右括号 (已有的右括号不能大于左括号)
                queue.offer(new Node(curNode.res + ")", curNode.left, curNode.right - 1));
            }
        }
        return res;
    }
}

2. 有效的括号字符串

在这里插入图片描述

(1)暴力 (超时)

  • 深度优先搜索,直接暴搜
  • 先考虑不带星号的情况,这时候我们只要分别统计左括号和右括号的数量,最后判断左括号是否等于右括号即可
    • 如果中间遇到了右括号比左括号数量多了,说明先出现了未经匹配的右括号,直接返回
  • 星号,可以代表左括号、右括号、空,这三种只要有一种满足最后左右括号相等即可,那么,很简单,我们就尝试着让它分别代表左括号、右括号、空往下搜索即可
class Solution {
    public boolean checkValidString(String s) {
        if (s.isEmpty())
            return true;
        int n = s.length();
        return dfs(s, 0 , 0);
    }

    private boolean dfs (String s, int i, int count) {
        int n = s.length();
        if (count < 0)
            return false;
        
        if (i >= n) {
            //搜到最后一位且所有括号匹配上
            if (count == 0)
                return true;
            return false;
        }

        //分别判断左右括号与*
        boolean res;
        if (s.charAt(i) == '(')
            res = dfs(s, i + 1, count + 1);
        else if (s.charAt(i) == ')')
            res = dfs(s, i + 1, count - 1);
        else
            res = dfs(s, i + 1, count) || dfs(s, i + 1, count + 1) || dfs(s, i + 1, count - 1);

        return res;
    }
}

(2)记忆化搜索

可以发现,存在 * 的时候会出现大量的重复计算。

  • 比如,以 ****())) 为例,计算到第四个 * 的时候,假设 count=1 ,这个 1 有可能是前面任意一个位置贡献的,也有可能是前面三个位置有任意两个位置是左括号,另一个位置是右括号,但是不管前面怎么变化,对于后面的计算它们的结果是一样的,所以,我们可以在暴搜的基础上加上记忆化,所以,又称为 记忆化搜索。
    在这里插入图片描述
class Solution {
    public boolean checkValidString(String s) {
        if (s.isEmpty())
            return true;
        int n = s.length();
        int[][] memo = new int[n][n + 1];
        return dfs(s, 0 , 0, memo);
    }

    private boolean dfs (String s, int i, int count, int[][] memo) {
        int n = s.length();
        if (count < 0)
            return false;
        
        if (i >= n) {
            //搜到最后一位且所有括号匹配上
            if (count == 0)
                return true;
            return false;
        }

        //如果该情况已被记录
        if (memo[i][count] != 0) {
            return memo[i][count] == 1;
        }

        //分别判断左右括号与*
        boolean res;
        if (s.charAt(i) == '(')
            res = dfs(s, i + 1, count + 1, memo);
        else if (s.charAt(i) == ')')
            res = dfs(s, i + 1, count - 1, memo);
        else
            res = dfs(s, i + 1, count, memo) || dfs(s, i + 1, count + 1, memo) || dfs(s, i + 1, count - 1, memo);
        
        memo[i][count] = res ? 1 : -1;
        return res;
    }
}

(3)动态规划

在这里插入图片描述

class Solution {
    public boolean checkValidString(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n + 1][n + 1];
        dp[0][0] = true;

        // i 表示前i个,不是s的下标
        for (int i = 1; i <= n; i++) {
            // 前i个字符最多只会有i个左括号
            for (int c = 0; c <= i; c++) {
                char ch = s.charAt(i - 1);
                if (ch == '(') {
                    // 当前为左括号了,所以,左括号的数量肯定不会为0
                    if (c > 0) {
                        dp[i][c] = dp[i - 1][c - 1];
                    }
                } else if (ch == ')') {
                    if (c + 1 < n + 1) {
                        dp[i][c] = dp[i - 1][c + 1];
                    }
                } else if (ch == '*') {
                    dp[i][c] = dp[i - 1][c];
                    if (c > 0) {
                        dp[i][c] = dp[i][c] || dp[i - 1][c - 1];
                    }
                    if (c + 1 < n + 1) {
                        dp[i][c] = dp[i][c] || dp[i - 1][c + 1];
                    } 
                }
            }
        }

        return dp[n][0];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yawn__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值