LeetCode 22. 括号生成【字符串,回溯;动态规划】中等

news2024/12/22 16:50:28

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

输入:n = 1
输出:["()"]

提示:

  • 1 <= n <= 8

解法 暴力递归(生成 2 2 n 2^{2n} 22n 个序列)

每个位置可以使用 ( 或者 ) ,因此总共有 2 2 n 2^{2n} 22n 个序列。我们全部生成出来,然后检查每个是否有效即可。

为了生成所有序列,我们可以使用递归。长度为 n n n 的序列就是在长度为 n − 1 n - 1 n1 的序列前加一个 ()

为了检查序列是否有效,我们遍历这个序列,并使用一个变量 balance \textit{balance} balance 表示左括号的数量减去右括号的数量。如果在遍历过程中 balance \textit{balance} balance 的值小于零,或者结束时 balance \textit{balance} balance 的值不为零,那么该序列就是无效的,否则它是有效的。

class Solution {
private:
    vector<string> ans;
    bool valid(const string& s) {
        int balance = 0;
        for (char c : s) {
            if (c == '(') ++balance;
            else --balance;
            if (balance < 0) return false;
        }
        return balance == 0;
    }
    void dfs(string& s, int n) {
        if (n == s.size()) {
            if (valid(s)) ans.push_back(s); // 检查是否有效序列
            return;
        }
        s += '(';
        dfs(s, n);
        s.pop_back();
        s += ')';
        dfs(s, n);
        s.pop_back();
    }
public:
    vector<string> generateParenthesis(int n) {
        string current;
        dfs(current, n * 2);
        return ans;
    }
};

复杂度分析:

  • 时间复杂度: O ( 2 2 n n ) O(2^{2n}n) O(22nn) ,对于 2 2 n 2^{2n} 22n 个序列中的每一个,我们用于建立和验证该序列的复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n) ,除了答案数组之外,我们所需要的空间取决于递归栈的深度,每一层递归函数需要 O ( 1 ) O(1) O(1) 的空间,最多递归 2 n 2n 2n 层,因此空间复杂度为 O ( n ) O(n) O(n)

解法2 回溯剪枝

方法一还有改进的余地:我们可以只在序列仍然保持有效时才添加 (, ) ,而不是像方法一那样每次添加。我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点:

  • 如果已用左括号数量不大于 n n n ,我们可以放一个左括号。
  • 如果已用左括号的数量大于已用右括号数量,我们可以放一个右括号。
class Solution {
private:
    vector<string> ans;
    void dfs(const string& s, int l, int r) {
        if (l < 0 || l > r) return;
        if (l == 0 && r == 0) {
            ans.push_back(s);
            return;
        }
        dfs(s + '(', l - 1, r);
        dfs(s + ')', l, r - 1);
    }
public:
    vector<string> generateParenthesis(int n) {
        dfs("", n, n);
        return ans;
    }
};

复杂度分析:这依赖于理解 g e n e r a t e P a r e n t h e s i s ( n ) generateParenthesis(n) generateParenthesis(n) 中有多少个元素。这个分析超出了本文的范畴,但事实证明这是第 nnn 个卡特兰数 1 n + 1 ( 2 n n ) \dfrac{1}{n+1}\dbinom{2n}{n} n+11(n2n) ,这是由 4 n n n \dfrac{4^n}{n\sqrt{n}} nn 4n 渐近界定的。

  • 时间复杂度: O ( 4 n n ) O(\dfrac{4^n}{\sqrt{n}}) O(n 4n) ,在回溯过程中,每个答案需要 O ( n ) O(n) O(n) 的时间复制到答案数组中。
  • 空间复杂度: O ( n ) O(n) O(n) ,除了答案数组之外,我们所需要的空间取决于递归栈的深度,每一层递归函数需要 O ( 1 ) O(1) O(1) 的空间,最多递归 2 n 2n 2n 层,因此空间复杂度为 O ( n ) O(n) O(n)

解法3 按括号序列的长度递归(记忆化搜索)或动态规划

任何一个括号序列都一定是由 ( 开头,并且第一个 ( 一定有一个唯一与之对应的 ) 。这样一来,每一个括号序列可以用 ( a ) b (a)b (a)b 来表示,其中 a a a b b b 分别是一个合法的括号序列(可以为空)。

这样就把生成 n n n 对括号的所有序列这一问题,分解为「生成 a a a 对括号的所有序列」+「生成 b b b 对括号的所有序列」两个规模更小但是本质相同的子问题 a + b = n − 1 a+b=n-1 a+b=n1 ,接着在 a a a 对括号的所有序列外面加上一个括号,这样就得到了 n n n 对括号的所有序列,并且完成了问题的分解。

总结一下,就是找到了一个最优子结构,将原问题转换为较小子问题求解。这道题的动态规划解难就是因为这个最优子结构不好想到。

一个示例如下:

  • i = 0 i = 0 i=0 结果是空;
  • i = 1 i = 1 i=1 结果有一种: ( ) () ()
  • i = 2 i = 2 i=2 结果有两种: ( ) ( ) ,   ( ( ) ) ()(),\ (()) ()(), (())
  • i = 3 i = 3 i=3 的结果,使用公式 ( + a + ) + b ,有如下三种情况共5种结果(以花括号来表示新添加的括号):
    • a = 2 , b = 0 a = 2, b = 0 a=2,b=0 { ( ) ( ) } ,   { ( ( ) ) } \{()()\},\ \{(())\} {()()}, {(())}
    • a = 1 , b = 1 a = 1, b = 1 a=1,b=1 { ( ) } ( ) \{()\}() {()}()
    • a = 0 , b = 2 a= 0, b = 2 a=0,b=2 { } ( ) ( ) , { } ( ( ) ) \{\}()(), \{\}(()) {}()(),{}(())

为了生成所有长度为 2 n 2n 2n(具有 n n n 个括号)的括号序列,我们定义一个函数 g e n e r a t e ( n ) generate(n) generate(n) 来返回所有可能的括号序列。那么在函数 generate ( n ) \textit{generate}(n) generate(n) 的过程中:

  • 我们需要枚举 a a a ,从 0 0 0 开始一直到 n − 1 n - 1 n1 ,并递归调用 g e n e r a t e ( a ) generate(a) generate(a) 计算 a a a 的所有可能性;
  • 相应的, b b b n − 1 n - 1 n1 一直到 0 0 0 ,递归调用 g e n e r a t e ( b ) generate(b) generate(b) 即可计算 b b b 的所有可能性;
  • 遍历 a a a b b b 的所有可能性并拼接,即可得到所有长度为 2 n 2n 2n 的括号序列。

为了节省计算时间,我们在每次 g e n e r a t e ( i ) generate(i) generate(i) 函数返回之前,把返回值存储起来,下次再调用 g e n e r a t e ( i ) generate(i) generate(i) 时可以直接返回,不需要再递归计算(即记忆化搜索)。

class Solution {
private:
    shared_ptr<vector<string>> cache[100] = {nullptr}; // string[][]
    shared_ptr<vector<string>> generate(int n) {
        if (cache[n] != nullptr) return cache[n];
        if (n == 0) 
            cache[0] = shared_ptr<vector<string>>(new vector<string>{""});
        else {
            auto result = shared_ptr<vector<string>>(new vector<string>);
            for (int i = 0; i != n; ++i) {
                auto lefts = generate(i);
                auto rights = generate(n - i - 1);
                for (const string& left : *lefts)
                    for (const string& right : *rights)
                        result -> push_back("(" + left + ")" + right);
            }
            cache[n] = result;
        }
        return cache[n];
    }
public:
    vector<string> generateParenthesis(int n) {
        return *generate(n);
    }
};

动态规划解法如下:

class Solution {
public:
    vector<string> generateParenthesis(int n) {
        if (n == 0) return {};
        if (n == 1) return { "()" };
        vector<vector<string>> dp(n + 1);
        dp[0] = { "" };
        dp[1] = { "()" };
        for (int parenthesisNum = 2; parenthesisNum <= n; ++parenthesisNum) {
            for (int i = 0; i < parenthesisNum; ++i) {
    int j = parenthesisNum - i - 1;
                for (string &a : dp[i])
                    for (string &b : dp[j]) 
                            dp[parenthesisNum].push_back("(" + a + ")" + b);
            }
        }
        return dp[n];
    }
};

复杂度分析:

  • 时间复杂度: O ( 4 n n ) O(\dfrac{4^n}{\sqrt{n}}) O(n 4n) ,该分析与 方法二 类似
  • 空间复杂度: O ( 4 n n ) O(\dfrac{4^n}{\sqrt{n}}) O(n 4n) ,此方法除答案数组外,中间过程中会存储与答案数组同样数量级的临时数组,是我们所需要的空间复杂度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1126447.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Java的线上花店管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…

OJ题之反转链表

hello ~~~每日一练的分享来了。 今天up主将为大家分享一个 OJ题之反转链表 题目&#xff1a;将链表实现如下的变化 1.思路的讲解&#xff1a;对于原链表我们只需改变指针的指向&#xff08;箭头&#xff09;即可 那么问题就来了&#xff0c;我们如何实现此操作&#xff1f;…

Redis设计与实现(2)链表和链表节点

每一个链表节点 typedef struct listNode{//前置节点struct listNode *prev;//后置节点struct listNode *next;//节点值void *value }lisNode; 多个listNode可以通过pre和next指针组成双端链表 虽然只要使用多个listNode结构就可以组成链表&#xff0c;但使用adlist.h/list来…

NLP入门——语言结构/语言建模

一、Linguistics 语言学 wordsmorphology 形态学&#xff1a;词的构成和内部结构研究。如英语的dog、dogs和dog-catcher有相当的关系morpheme 语素&#xff1a;最小的语法单位&#xff0c;是最小的音义结合体lexeme 词位&#xff1a;词的意义的基本抽象单位&#xff0c;是一组…

C语言_字符串和内存函数

文章目录 前言一. strlen二. strcpy三.strcat四. strcmp &#xff08;字符串比较&#xff09;五. strncpy六. strncmp七. strstr八. strtok九 . strerror perror十. 字符分类函数十一. memcpy (内存拷贝&#xff09;十二. memmove(可以重叠拷贝 也可以实现不重叠的内存拷贝) 前…

CentOS7安装部署CDH6.2.1

文章目录 CentOS7安装部署CDH6.2.1一、前言1.简介2.架构3.环境 二、环境准备1.部署服务器2.安装包准备3.修改机器名4.关闭防火墙5.关闭 SELinux6.Hosts文件7.limits文件8.设置swap空间9.关闭透明巨页内存10.免密登录 三、安装CM管理端1.安装第三方依赖包2.安装Oracle的JDK3.安装…

Rockchip RK3399 - DRM crtc基础知识

一、LCD硬件原理 1.1 CRT介绍 CRT是阴极射线管(Cathode Ray Tube)的缩写&#xff0c;它是一种使用电子束在荧光屏上创建图像的显示设备。CRT显示器在过去很长一段时间内是主流的显示技术&#xff0c;现已被液晶显示屏或其他新兴技术所替代。 在CRT显示器中&#xff0c;扫描电子…

k8s-----6、pod的镜像拉取、重启策略、资源限制

镜像拉取、重启策略、资源限制 1、镜像拉取2、资源限制3、重启机制 1、镜像拉取 [rootmaster ~]# cat nginx.yaml apiVersion: v1 kind: Pod metadata:name: mypod spec:containers:- name: nginximage: nginx:1.14imagePullPolicy: Always# IfNotPresent: 默认值&#xff0c…

CPO是啥?

CPO是啥&#xff1f; CPO通常是“Chief Product Officer”&#xff08;首席产品官&#xff09;的缩写&#xff0c;是企业高层管理团队中负责产品管理和战略规划的主要负责人。CPO通常负责制定公司的产品战略、管理产品组合、带领产品团队以及推动产品的创新和优化。他或她需要有…

引用类型的按值传递

按值传递时&#xff0c;传递过去的是该引用类型实例的引用的一个拷贝&#xff0c;这样说可能不是很清楚&#xff0c;而且容易引起误解。所谓引用&#xff0c;就是分配在栈上的一小块内存区域&#xff0c;里面存放着该引用类型实例在托管堆上的地址。引用类型在按值传递的时候&a…

CUDA学习笔记(十三) Shared Memory

CUDA SHARED MEMORY shared memory在之前的博文有些介绍&#xff0c;这部分会专门讲解其内容。在global Memory部分&#xff0c;数据对齐和连续是很重要的话题&#xff0c;当使用L1的时候&#xff0c;对齐问题可以忽略&#xff0c;但是非连续的获取内存依然会降低性能。依赖于…

codeshell安装配置

codeshell安装配置 1 注意事项1.1 Python版本问题 2 codeshell环境搭建2.1 codeshell使用软件各版本2.2 软件下载2.3 codeshell使用环境安装2.3.1 python-3.10.9-amd64.exe安装2.3.2 Anaconda3-2022.10-Windows-x86_64.exe安装2.3.3 创建环境2.3.4 Pytorch安装2.3.5 transforme…

C++中的多态以及如何实现多态(近万字图文详解)

C中的多态 1. 多态的概念1.1 概念 2. 多态的定义及实现2.1多态的构成条件&#xff08;重点&#xff09;2.2 虚函数2.3 虚函数的重写(重点)2.4 C11 override 和 final2.5 重载、覆盖(重写)、隐藏(重定义)的对比 3. 抽象类3.1 概念3.2 接口继承和实现继承 4. 多态的原理4.1虚函数…

MySQL1——喵喵期末不挂科

宝宝&#xff0c;你不点个赞吗&#xff1f;不评个论吗&#xff1f;不收个藏吗&#xff1f; 最后的最后&#xff0c;关注我&#xff0c;关注我&#xff0c;关注我&#xff0c;你会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的很重要…

零代码编程:用ChatGPT多线程批量将PDF文档转换为word格式

pdf2docx是Python的一个库&#xff0c;可以很方便的将PDF文档转换为word格式&#xff0c;首先安装这个库。 然后在ChatGPT中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个文档格式转换的任务&#xff0c;具体步骤如下&#xff1a; 打开F盘的Books文件…

FreeRTOS基础(如何学好FreeRTOS?)

目录 基础知识 进阶内容 后期“摆烂” 基础知识 实时操作系统 (RTOS)&#xff1a;FreeRTOS是一个实时操作系统&#xff0c;它提供了任务管理、调度和同步等功能&#xff0c;在嵌入式系统中有效地管理多个任务。 任务&#xff08;Task&#xff09;&#xff1a;任务是在RTOS…

祝所有的程序猿们2023年的1024节快乐~

许久没更新Bolg了&#xff0c;眼看就要到1024节&#xff0c;其实也是没有可以更新的东西&#xff0c;目前在PhD&#xff0c;发现很多东西都还需要慢慢沉淀&#xff0c;放一doctoral college 开学的时候ppt的老图。 越往深处研究会陷入泥潭&#xff0c;考虑的细节将会越来越多&…

如何在Ubuntu下安装RabbitMQ服务并异地远程访问?

文章目录 前言1.安装erlang 语言2.安装rabbitMQ3. 内网穿透3.1 安装cpolar内网穿透(支持一键自动安装脚本)3.2 创建HTTP隧道 4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 RabbitMQ是一个在 AMQP(高级消息队列协议)基…

【Gensim概念】03/3 NLP玩转 word2vec

第三部分 对象函数 八 word2vec对象函数 该对象本质上包含单词和嵌入之间的映射。训练后&#xff0c;可以直接使用它以各种方式查询这些嵌入。有关示例&#xff0c;请参阅模块级别文档字符串。 类型 KeyedVectors 1&#xff09; add_lifecycle_event(event_name, log_level2…

Web前端—Flex布局:标准流、浮动、Flex布局、综合案例(短视频首页解决方案)

版本说明 当前版本号[20231024]。 20231024初版 目录 文章目录 版本说明目录Flex布局01-标准流02-浮动基本使用产品区域布局HTML标签CSS样式 清除浮动场景搭建额外标签法单伪元素法双伪元素法overfow法 03-Flex布局Flex组成主轴对齐方式侧轴对齐方式修改主轴方向弹性伸缩比弹…