LeetCode22 生成所有括号对
本文始发于个人公众号:TechFlow,原创不易,求个关注
链接
难度
Medium
描述
Given n pairs of parentheses, write a function to generate all combinations
of well-formed parentheses.
给定n对括号,要求返回所有这些括号组成的不同的合法的字符串
For example, given n = 3, a solution set is:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
题解
这道题目非常有意思,解法也很多,还是老规矩,我们先由易到难,先从最简单的方法开始入手。
我们来简单分析一下题目,n个括号对意味着字符串的长度是2n,我们利用排列组合可以计算出,所有的组合种数一共有\(C_{2n}^n\)种。算一下会知道,这个数是很大的,也就是说我们哪怕一开始就知道答案,把答案遍历一遍也会有很高的耗时,所以这道题对于时间复杂度的要求应该不会很高。
暴力
能想到最简单的方法,当然是暴力,不要看不起这个朴素的算法,很多时候灵感都是从暴力当中获取的。但是这道题暴力不太容易写,因为会有一种无从入手的感觉,我们知道要暴力,但是并不知道应该怎样暴力。这道题不存在可以直接枚举的朴素元素,必须要我们拐个弯才行。
怎么拐弯呢,其实答案我刚才已经说出来了。n个括号对,也就是说一共2n个字符,我们可以枚举n个'('分别放在什么位置,剩下的自然就是')'了。看起来很有道理,但是有一个问题,就是这个思路并没有办法通过循环直接实现。这其实已经进化成了一个搜索问题了,我们要搜索所有可以摆放括号的可能性。
如果你能从暴力方法跳跃到搜索问题,那么说明你离写出代码已经很接近了。如果不行,那么我建议你花点时间去学习一下搜索算法专题。
对于搜索问题而言,这已经很简单了,我们搜索的空间是明确的,2n个位置,搜索的内容,对于每个位置我们可以摆放'('也可以摆放')'。那么代码自然而然呼之欲出:
def dfs(pos, left, right, n, ret, cur_str):
"""
pos: 当前枚举的位置
left: 已经放置的左括号的数量
right: 已经放置的右括号的数量
n: 括号的数量
ret: 放置答案的数组
cur_str: 当前的字符串
"""
if pos == 2*n:
ret.append(cur_str)
return
if left < n:
dfs(pos+1, left+1, right, n, ret, cur_str+'(')
if right < n:
dfs(pos+1, left, right+1, n, ret, cur_str+')')
这个程序遍历运行之后还没有结束,我们还需要判断生成出来的括号是否合法,也就是说括号需要匹配上。我们可以用一个栈来判断括号是否能够匹配,比如我们遇见左括号就进栈,遇见右括号则判断栈顶,如果栈顶是左括号,那么栈顶的左括号出栈,否则则入栈,最后判断栈是否为空。这个算法实现当然不难,但是如果你仔细去想了,你会发现完全没有必要用栈,因为如果我们遇到右括号的时候,栈顶不为左括号,那么一定最后是无法匹配的。因为后面出现的左括号不能匹配前面出现的右括号,正所谓往者不可追就是这个道理。【狗头】
优化
我们来思考一个问题:什么情况会出现右括号遇不到左括号呢?只有一种情况,就是当前出现右括号的个数超过了左括号,也就是说我们遍历一下字符串,如果中途出现右括号数量超过左括号的情况,那么就说明这个字符串是非法的。看起来没毛病对吧,但是有问题,我们为什么不在枚举的时候就判断呢,如果左括号放入的数量已经等于右括号了,那么就不往里防止右括号,这样不就可以保证搜索到的一定是合法的字符串吗?
如果你能想到这一层,说明你对搜索的理解已经很不错了。我们看一下改动之后的代码:
def dfs(pos, left, right, n, ret, cur_str):
"""
pos: 当前枚举的位置
left: 已经放置的左括号的数量
right: 已经放置的右括号的数量
n: 括号的数量
ret: 放置答案的数组
cur_str: 当前的字符串
"""
if pos == 2*n:
ret.append(cur_str)
return
if left < n:
dfs(pos+1, left+1, right, n, ret, cur_str+'(')
if right < n and right < left:
dfs(pos+1, left, right+1, n, ret, cur_str+')')
大部分代码都没有变化,只是在right < n后面加入了一个right < left这个条件。看似只有一个条件,但是这个条件起到的作用至关重要。整个算法的效率有了质的提升,实际上这也是效率最高的算法。
构造
上面的方案在LeetCode官方当中都有收入,也是比较常规的解法,下面要介绍的方法是我的原创,我个人感觉也比较有意思,分享给大家。
在之前的文章当中我们介绍过分治法,分治法的核心是将一个看似无法求解的大问题,分解成比较容易解决的小问题,最后加以解决。这道题当中,我们直接求n时的解法是比较困难的,没办法直接获得,我们能不能也试着使用分治的方法来解决呢?
我们来观察一下数据,当n=1的时候,很简单,结果是(),只有这一种。当n=2呢?有两种,分别是(())和()(),当n=3呢?有5种:((())), ()(()), ()()(), (()()), (())()。这当中有没有规律呢?
我们用solution(n)表示n对应的解法,那么我们可以写出solution(n)对应的公式:
\]
上面这个式子有点像是动态规划的状态转移方程,虽然不完全一样,但是大概是那么回事。也就是说我们可以用比答案规模小的答案组装成现在的答案。比如n=3时的答案,等于n=2时的答案和n=1时答案的拼接。
比如: solution(1) + solution(2) 可以得到: ()()()和()(()),solution(2) + solution(1)可以得到 ()()()和(())()。但是还有一种答案无法通过拼接得到就是( solution(2) )。也就是说在solution(2)的答案外面包一层括号。那为什么不用考虑solution(1)的答案外面包两层括号呢?答案很简单,因为solution(2)已经包括了这样的情况,所以我们只用往下考虑一层。
不过还没有结束,还有一点小问题,就是这样得到的答案可能会有重复,所以我们需要去重,利用set我们可以很简单做到这点,让我们一起来看代码:
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
solutionMap = {}
# 记录下n=0和1时的答案
solutionMap[0] = set([""])
solutionMap[1] = set(["()"])
# 遍历小于n的所有长度
for i in range(2, n+1):
cur = set()
# 遍历小于n的所有长度
for j in range(1, i):
# 构造答案
ans1 = solutionMap[j]
ans2 = solutionMap[i-j]
for s in ans1:
for t in ans2:
cur.add(s + t)
# 构造 ( solution(n-1) )这种答案
for s in solutionMap[i-1]:
cur.add("(" + s + ")")
solutionMap[i] = cur
return list(solutionMap[n])
在C++当中,这两种方法的效率差不多,但是使用Python的话,构造的方法要更快一些。和搜索这种方法相比,搜索是不知道答案去搜寻答案,而构造法是知道答案大概长什么样子,依据一定的规则生产答案。可以说是两种不同思路的解法,也是我本人很喜欢这道题的原因。
这道题的代码都不长,但是思路挺有意思,希望大家会喜欢。
今天的文章就是这些,如果觉得有所收获,请顺手扫码点个关注吧,你们的举手之劳对我来说很重要。
LeetCode22 生成所有括号对的更多相关文章
- Leetcode22.Generate Parentheses括号生成
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合. 例如,给出 n = 3,生成结果为: [ "((()))", "(()())& ...
- 22.Generate Parentheses[M]括号生成
题目 Given n pairs of parentheses, write a function to generate all combinations of well-formed parent ...
- [LeetCode] 22. 括号生成(回溯/DP)
题目 给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合. 例如,给出 n = 3,生成结果为: [ "((()))", "(()( ...
- c++生成算式并计算(《构建之法》第一章课后第一题)
c++实现计算器(自动生成算式并计算) 要满足的需求有以下几个: 自动生成随机的四则运算算式,包含括号和小数. 对生成的算式计算出结果. 算式.结果分别存储到不同的文件. 一 生成算式 由上述需求可知 ...
- 【C】四则运算生成和核对器----by郁卓、谢明浩
[Github项目地址] 完成功能: 1. 使用 -n 参数控制生成题目的个数 2. 使用 -r 参数控制题目中数值(自然数.真分数和真分数分母)的范围 3. 生成的题目中计算过程不能产生负数,也就是 ...
- java 自动生成四则运算式
本篇文章将要介绍一个“自动生成四则运算式”的java程序,在没有阅读<构建之法>之前,我已经通过一个类的形式实现了要求的功能,但是当阅读完成<构建之法>之后,我意识到自己所写程 ...
- P5658 括号树
P5658 括号树 题解 太菜了啥都不会写只能水5分数据 啥都不会写只能翻题解 题解大大我错了 我们手动找一下规律 我们设 w[ i ] 为从根节点到结点 i 对答案的贡献,也就是走到结点 i ,合 ...
- idea使用心得(1)-快捷键用法
快捷键: Ctrl+F12,可以显示当前文件的结构,Alt+7,可在左侧生成固定框体控件,适合类复杂的情况 Ctrl+Alt+O,优化导入的类和包 Ctrl+X,删除行 删除光标所在的哪一行,对尤其是 ...
- javaweb-四则运算
这次作业,我们选择的是网页开发,后来我们小组才知道自己这方面的知识还是太匮乏了. 主要代码: public class calcu extends HttpServlet{ public void d ...
随机推荐
- Elasticsearch调优篇-慢查询分析笔记
前言 elasticsearch提供了非常灵活的搜索条件给我们使用,在使用复杂表达式的同时,如果使用不当,可能也会为我们带来了潜在的风险,因为影响查询性能的因素很多很多,这篇笔记主要记录一下慢查询可能 ...
- 从零开始学asyncio(上)
这篇文章主要是介绍生成器和IO多路复用机制, 算是学习asyncio需要的预备知识. 这个系列还有另外两篇文章: 从零开始学asyncio(中) 从零开始学asyncio(下) 一. 简单爬虫实例 首 ...
- Notepad++中安装json格式化插件
在线工具固然好,一旦没网就凉凉 Notepad++编辑器中提供了 json 数据格式化显示的插件 安装插件过程如下: 注意: 安装过程需要联网状态 插件安装过程会自动退出程序,等待几秒钟后插件安装完成 ...
- AVR单片机教程——ADC
ADC 计算机的世界是0和1的.单片机可以通过读取0和1来确定按键状态,也可以输出0和1来控制LED.即使是看起来不太0和1的PWM,好像可以输出0到5V之间的电压一样,达到0和1之间的效果,但本质上 ...
- c++中减字符0的作用(转)
在刷OJ题的时候遇到要读取“2013-3-1”形式的日期然后计算这个日期是该年的第几天, 显然我们读取的是字符串,但是计算第几天却要整型数来计算,这是这个问题的难点,下面是解决这个问题的代码: int ...
- Spring Boot 入门(十):集成Redis哨兵模式,实现Mybatis二级缓存
本片文章续<Spring Boot 入门(九):集成Quartz定时任务>.本文主要基于redis实现了mybatis二级缓存.较redis缓存,mybaits自带缓存存在缺点(自行谷歌) ...
- Android 平台JS调试技术
1. 测试技术简介 Android平台微信公众号一般以H5的形式开发,测试发现流量一般都通过js进行加密传输,导致无法对越权.SQL注入等风险点进行测试.针对此难点,本手册会介绍包括Android环 ...
- 序列化之二(将"\/Date(942289871000)\/"格式的时间替换成"yyyy-MM-dd HH:mm:ss"格式)
序列化就是一种用来处理对象流的机制.所谓对象流也就是将对象的内容进行流化,流的概念这里不用多说(就是I/O).我们可以对流化后的对象进行读写 操作,也可将流化后的对象传输于网络之间(注:要想将对象传输 ...
- 【python系统学习06】一张图看懂列表并学会操作
点击跳转-原文地址 数据类型 - 列表(list) 「目录:」 一张图了解列表 列表是什么 列表长啥样 语法格式 代码示例 格式特征 列表定义 列表操作 - 提取单个:偏移量 什么是偏移量 偏移量提取 ...
- 8.for循环及练习
For循环: 虽然所有循环结构都可以用 while 或者 do...while 表示,但Java提供了另一种语句— —for循环,使一些循环结构变的更加简单. for 循环语句是支持迭代的一种通用 ...