【动态规划篇】欣赏概率论与镜像法融合下,别出心裁探索解答括号序列问题

news2025/1/11 13:05:25

          本篇鸡汤:没有人能替你承受痛苦,也没有人能拿走你的坚强. 

欢迎拜访羑悻的小杀马特.-CSDN博客

本篇主题:带你解答洛谷的括号序列问题(绝对巧解)

制作日期:2025.01.10

隶属专栏:C/C++题海汇总

目录

本篇简介:

一·题目介绍:

二·思路叙述:

2.1判断独立性:

2.2空隙法及填充gap表:

2.3填充dp表: 

2.3.1dp状态方程规定:

2.3.2dp的状态转移方程推导及填充:

2.4·镜像法:

2.5细节处理:

三·代码汇总:

四.个人小结:


本篇简介:

本篇主体还是动态规划,之前篇介绍了对它的讲解概念,因此本篇就不做多解释,下面就是利用动态规划,结合概率论推导独立性公式,两步走,并采用镜像法优化一下,最后动态规划得到答案后,采用独立性求积方法得出答案。

一·题目介绍:

 

洛谷链接: [蓝桥杯 2021 省 AB] 括号序列 - 洛谷

测试用例: 

输入:((() 

输出:5 

二·思路叙述:

当我们看到了括号填充类似问题,是不是想到了leetcode的有道括号生成问题,看着好像:

链接:LCR 085. 括号生成 - 力扣(LeetCode) 

下面我们展示下leetcode的代码:


//思路:对于返回的字符串组合,可以考虑是否是决策树的叶子然后用决策树+dfs:即根据n也就是判断递归条件(剪枝)左右支为'('')'它们的选择,
//然后画出决策树分析递归条件,完成dfs设计

class Solution {
public:
  //全局变量的设计:
        int left=0;
        int right=0;
        string path;
         vector<string> ret;
    vector<string> generateParenthesis(int n) {
            dfs(n);
            return ret;
    }
    void dfs(int &n){
        //递归出口:
        if(path.size()==2*n){
            ret.emplace_back(path);
            return;
        }
        //判断进入递归的条件(剪枝逆向):
        if(left<n){
            path+='(';left++;
            dfs(n);
            path.pop_back();left--;//回溯
         }
         if(right<left){
          path+=')';right++;
            dfs(n);
            path.pop_back();right--;//回溯
         }
    }
};

但是我们仔细一下看,本题并不是自己生成所有种类的正确情况,而是让我们自己在它给例子去填充让它有效。

看到这里,根据我们上一篇的经验就很容易想到动态规划去解答,因此leetcode上面深度优先遍历的方法就寄掉了。

因此下面我们就用动态规划思想去解答;但是它也是不好想到;甚至我们去看题解,是不是都看不懂比如:

  

这里可以看出要么就是三层for嵌套循环填dp,以及很多人会发现题解甚至都看不懂,即便看懂还要琢磨很久才会明白(当然这里博主也是);因此博主在这出一篇文章来解释一下,让大家可以更加明白这道题是如何解答的。

那么下面我们就以题目给的例子来畅谈:

2.1判断独立性:

首先,我们的任务就是要么填充右括号来干掉左括号,要么填充左括号来干掉右括号;反正就是要得到这样以最小的添加括号让它合法的添加方法数;那么下面我们说一下结论:

这里可以知道添加右括号使它合法(也就是利用右括号干掉左括号)的方法数和添加左括号方法数是独立的---->而题目要求是添加左括号和右括号是都行的,因此最后就可以转化成添加左括号方案数与右括号方案数之积(两者是独立的) 。

证明: 

这里为什么可以得到上面说的结论,直接就把问题简单化,单一化了:

这里需要用到的概率论的公式:

说白了就是A B两个事件如果互不干扰,互不影响,那么此时两种同时发生的概率就是两者单独发生概率之积, 而本题呢?

我们给它的目的简化一下,使得它更贴近这,题目其实就是要求让我们要么补充左括号,要么补充右括号,对应把相反括号干掉,也就是所说的使得左右括号都合法;而我们补充右括号使得左括号合法并没有影响其他右括号的合法性(因为我们没有补充左括号)-->所以左右括号的合法性是独立的。

因此我们可以拆开来分别求它们单独的合法性种类然后求个积就是题目要的使得例子左右括号都合法的种类数。

 这里注意下:题目所说最少填充括号的意思就是比如对于((()我们不能无缘无故让添加一对括号让它合法:((()()()()))类似这样。

因此我们可以得到一个公式:

 ret=(re_left*re_right)%1e9+7   这里题目要求结果太大要取模 

 解释一下:也就是我们使左括号合法的种类*使右括号合法的种类。

那么也就是我们怎么求左括号和发的种类:这里我们引入一下空隙法(后面我们得知独立性后就基于判断左括号合法性来讲解)。

2.2空隙法及填充gap表:

这里我们以左括号为例,就是我们每当多一个左括号就相当于多了一个可以填充右括号的空隙;但是当我们遍历到右括号,空隙个数可以理解成没变;但遍历到当前(0~当前空隙)能填充右括号个数要少一个(保证非负性,大于0才减少)

然后就是我们搞一个空隙数组记录的就是前i个空隙中最多可以放多少个括号:gap[i](这里为了方便我们一一对应,因此下标从1开始,对应的是前多少个空隙)

下面我们举个例子吧:

比如:((() :这里有三个空隙,具体变化gap数组的值(随着遍历):1-->2-->3-->2(这注意是吧第三个空隙的最大容量减1)。

再比如:

因为多少个左括号就意味着可以补充多少右括号;但是空隙最大容量也就是补充括号的个数是随着与之反作用的括号相关的

因此下面重点:我们总结一下空隙规则(这里我们都以补充右括号使左括号合法来谈):

①空隙的个数也就是左括号的个数;

②当遇到右括号的话空隙所容纳的括号数就减少(前提是大于0)。

下面就根据上述所讲填充空隙表gap:

l:记录的左括号个数也就是空隙个数

void init_gap() {//填充gap数组,也就是判断多少个空隙以及前多少个空隙的
//最大容量
    for (int i = 0; i < N; i++) {
        if (s[i] == '(') {
            l++;
            gap[l] = gap[l - 1] + 1;
        }
        else  gap[l] > 0 ? gap[l]-- : 1;//这里注意当干右括号的时候
        //因为是求得最少补充的左括号数,故不能出现负
    }
}

但是这里我们就可以得到公式也就是判断填右括号使得左括号合法的规定:

我们gap数组存的都是遍历到当前空隙开始从0~i补充右括号最大个数;因此我们当填充dp表的时候遍历填写括号个数的要加一个判断:

填充括号个数<=gap[遍历到当前的空隙]  即 i<=gap[j]。

 那又要问了为什么动态规划不是填充dp表为啥搞个空隙表;因为后面我们会填充dp就是根据多少个空隙(遍历到哪里),最多可以填充多少个括号来填充的故肯定是有用的。

2.3填充dp表: 

2.3.1dp状态方程规定:

那么首先我们肯定要先定义dp表的状态:

因为它是遍历到哪里,然后又要在这段区间填充括号(多少个)使它变得合法,因此最好搞一个二维dp表。

那么我们结合上面搞的空隙表,规定状态:

dp[i][j]代表遍历到第j个空隙处,可以在0~i个空隙内可以填充i个括号的种类数(这里我们遍历只按照左括号走)

2.3.2dp的状态转移方程推导及填充:

这里由于让下标与实际意义对应及可能会出现状态方程自己初始化需要前边的值,我们选择多开,并手动初始化。 

首先呢对于大多数人直接用脑子按照这个逻辑去想状态方程是啥样,肯定是困难的,因此我们搞一个简单的例子带大家总结一下状态转移方程:

首先我们根据这个例子自己按照dp状态规定填好dp表;但是为什么第一行都填0呢 ?

这就是我们状态方程自己循环填充要用到的,需要我们提前进行初始化:

也就是把0个括号插入到0-l个空隙插入的方案:这里我们可以理解成把0个即插入“空气”这种插法,故只有一种情况(虽然有些勉强);其次就是我们只能根据找规律,细心的话如果我们第一行不填写,可以发现如果都填写1的话可以得到一个公式:

dp[i][j]=dp[i][j-1]+dp[i-1][j-1]+dp[i-2][j-1]+...+dp[0][j-1]

其实就是我们那个填写dp表的一个“1”形状dp值之和;我们可以根据这个公式来往上递推成“/”形状的dp值这样就无需再来一层for循环(就像上面说的o(N^3)一样复杂度的三层for了)只需取前面填充的两个dp值就好了。

如:

 

因此公式化简(也就是我们填充dp表要用的公式):

dp[i][j]=dp[i - 1][j] + dp[i][j - 1] 

当然了后面我们写的时候要对1e9+7取模(题目说明,就不用说了吧,还有就是long long的奇妙之处了) 

那dp填表的过程这个公式就ok吗?

当然还会有条件限制,这里我们在填充空隙表gap数组的时候就说了:

加上这个条件后填充dp就ok了。

这个条件就解释了为什么最后填充三个括号为0以及这么多位置填写0的原因了。

最后我们要的是在0~j个空隙能填充的最大括号数的方案数(遍历到对应空隙,此空隙里面放的gap数组值)即:

dp[gap[l]][l];

这样我们就得到了上面所说的re_left值了(记住也是long long) 。

后面我们在去求re_right难道也是写一个类似的两个函数去完成吗?

当然不用了,直接给他镜像一下,就等同于判断左括号的合法性了;这也就是本篇标题提到的镜像法的巧妙了。

2.4·镜像法:

这里虽然名字起的多好高级,其实并不难理解,之所以这么操作就是为了让我们不用再多写函数就可以完成对右括号像左括号一样类似的检验;所以为什么起这个名字呢,下面看张图片:

这样就可以看出了我们要检查右括号的合法性(补充左括号);其实就是把它镜像一下然后再检验一次左括号的合法性就行了。  

其实也不难操作:就是遍历一下原串,把左括号改成右括号,右括号改成左括号;然后再利用迭代器逆置一下就好

最后我们根据独立性返回两者之积就好了(但是注意范围long long ,其次就是取模)。 

2.5细节处理:

也就是分享一下博主在写这道题遇到的困难,即一些细节问题没注意到而导致的:

比如:

①博主写的代码这里大都用的全局变量(好处就是可以让函数不用传参,坏处就是如果再次使用就要重新初始化)

 l=0;//再次填充gap是需要对它初始化0
 memset(dp,0,sizeof(dp));//再次用填充dp表也要对它初始化0

 ②就是结果要是long long类型否则即不能通过(比如和dp值有关都要是long long,最后答案等);否则就这样:

这就是re_left和re_right没有long long的结果;果真映射那句话:“不加long long 见祖宗” 。

 ③其次就是那些细节问题,比如填充gap表注意:

填充dp表要注意:

这样细节都处理好就大差不大了。

三·代码汇总:

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
#define MAX 1000000007
#define C 5000
ll dp[C+1][C + 1] = { 0 };//表示把i个括号插入到前j个空隙的方案数
int gap[C + 1] = { 0 };//下标是第几个空隙,值是空隙的最大左括号容量
string s; 
int N; 
int l=0;//记录左括号数量
void init_gap() {//填充gap数组,也就是判断多少个空隙以及前多少个空隙的
//最大容量
    for (int i = 0; i < N; i++) {
        if (s[i] == '(') {
            l++;
            gap[l] = gap[l - 1] + 1;
        }
        else  gap[l] > 0 ? gap[l]-- : 1;//这里注意当干右括号的时候
        //因为是求得最少补充的左括号数,故不能出现负
    }
}
ll get_ans() {
    for (int i = 0; i <= l; i++) dp[0][i] = 1;
    for (int i = 1; i <= l; i++) {
        for (int j = 1; j <= l; j++) 
 dp[i][j] = gap[j] >= i ?  (dp[i - 1][j] + dp[i][j - 1]) % MAX:0;
      //填充的括号数量不能超过前多少个空隙所能容下的最大     
        
    }
    return dp[gap[l]][l];//输出的是把最大空隙数所能容的括号数填入的最多方式数
}
void mapping_r_to_l(){//补充左括号干掉右括号转化成补充右括号干掉左:
//镜像一下子,使得写的干左括号的函数还可以用
     reverse(s.begin(), s.end());
   for (int i = 0; i < N; i++)s[i] =(s[i] == '(' ?  ')' : '(');
}
int main() {
    cin >> s;
    N = s.size();
    init_gap();
    ll re_left = get_ans();
    
   mapping_r_to_l();
     l=0;//全局劣势
    init_gap();
    memset(dp,0,sizeof(dp));//全局劣势
    ll re_right = get_ans();
    cout << (re_left * re_right) % MAX << endl;//利用独立性
    return 0;

} 

最后也是满分AC了:

四.个人小结:

这里对于动态规划的题型做法就不总结了,因为上篇总结过啦,具体可看:【动态规划篇】步步带你深入解答成功AC最优包含问题(通俗易懂版)-CSDN博客

此外就是对于不熟悉的题目,我们要对看看题解,看看大佬们是用什么奇妙的方法解答的,当然可能会看不懂,但是我们不要放弃,一天看不懂就多看几天,只要愿意学习别人的解法,总是会可以悟懂的(这里说一下博主写这篇文章其实也是学习了大佬的写法,当然也是看了好几天),所以我们自己不熟练就要多多学习必然的解法做好总结,尽最大可能去吸收它。这里博主建议如果看题解看不懂,尽量找写的比较详细的博客去学习(博主自荐一下:可以参考向博主这样写的文章去看,去学习,如有不足,指出来博主会尽力修改)

还是那句话,只要愿意学肯定能学会的,关键还是在于你的抉择。

对此,我之所以能写出这篇文章还要感谢一位博主写的它的文章,通过阅读,揣摩那位博主的文章;加上自己的理解才可创作出这篇文章(相当于对那位博主的文章的深刻解释以及加上了自己的理解吧)  

借鉴的博主文章链接:第十二届蓝桥杯B组省赛括号序列题解_蓝桥杯判断括号合法-CSDN博客

 当然还是先建议把博主的文章读懂再去读这位博主的文章,毕竟个人认为自己的文章对它解释了很多地方,更方便读者阅读。

感谢大家阅读!!! 

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

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

相关文章

点击底部的 tabBar 属于 wx.switchTab 跳转方式,目标页面的 onLoad 不会触发(除非是第一次加载)

文章目录 1. tabBar 的跳转方式2. tabBar 跳转的特点3. 你的配置分析4. 生命周期触发情况5. 总结 很多人不明白什么是第一次加载&#xff0c;两种情况讨论&#xff0c;第一种情况假设我是开发者&#xff0c;第一次加载就是指点击微信开发者工具上边的编译按钮&#xff0c;每点击…

CUDA、CUDNN以及tensorRT的版本对应关系

各版本的对应除了可以看文件的命名上还可以查看TensorRT的Release日志&#xff1a; Release Notes :: NVIDIA Deep Learning TensorRT Documentation 这个是官方测试TensorRT的Release日志&#xff0c;里面指明了当前测试的TensorRT版本是在哪个CUDNN等库下的测试结果&#x…

设计模式(观察者模式)

设计模式&#xff08;观察者模式&#xff09; 第三章 设计模式之观察者模式 观察者模式介绍 观察者模式&#xff08;Observer Design Pattern&#xff09; 也被称为发布订阅模式 。模式定义&#xff1a;在对象之间定义一个一对多的依赖&#xff0c;当一个对象状态改变的时候…

Helm部署activemq

1.helm create activemq 创建helm文件目录 2.修改values.yaml 修改image和port 3. helm template activemq 渲染并输出 4. helm install activemq activemq/ -n chemical-park // 安装 5.启动成功

Untiy中如何嵌入前端页面,从而播放推流视频?

最近工作中频繁用到unity,虽然楼主之前也搞过一些UNTY游戏开发项目&#xff0c;但对于视频这块还是不太了解&#xff0c;所以我们采用的方案是在Unity里寻找一个插件来播放推流视频。经过我的一番寻找&#xff0c;发现了这款Vuplex 3D WebView&#xff0c;它可以完美的打通Unit…

rabbitmq的三个交换机及简单使用

提前说一下&#xff0c;创建队列&#xff0c;交换机&#xff0c;绑定交换机和队列都是在生产者。消费者只负责监听就行了&#xff0c;不用配其他的。 完成这个场景需要两个服务哦。 1直连交换机-生产者的代码。 在配置类中创建队列&#xff0c;交换机&#xff0c;绑定交换机…

Excel 技巧07 - 如何计算到两个日期之间的工作日数?(★)如何排除节假日计算两个日期之间的工作日数?

本文讲了如何在Excel中计算两个日期之间的工作日数&#xff0c;以及如何排除节假日计算两个日期之间的工作日数。 1&#xff0c;如何计算到两个日期之间的工作日数&#xff1f; 其实就是利用 NETWORKDAYS.INTL 函数 - weekend: 1 - 星期六&#xff0c;星期日 2&#xff0c;如…

初学stm32 --- DAC模数转换器工作原理

目录 什么是DAC&#xff1f; DAC的特性参数 STM32各系列DAC的主要特性 DAC框图简介&#xff08;F1/F4/F7&#xff09; 参考电压/模拟部分电压 触发源 关闭触发时(TEN0)的转换时序图 DMA请求 DAC输出电压 什么是DAC&#xff1f; DAC&#xff0c;全称&#xff1a;Digital…

从预训练的BERT中提取Embedding

文章目录 背景前置准备思路利用Transformer 库实现 背景 假设要执行一项情感分析任务&#xff0c;样本数据如下 可以看到几个句子及其对应的标签&#xff0c;其中1表示正面情绪&#xff0c;0表示负面情绪。我们可以利用给定的数据集训练一个分类器&#xff0c;对句子所表达的…

从CentOS到龙蜥:企业级Linux迁移实践记录(系统安装)

引言&#xff1a; 随着CentOS项目宣布停止维护CentOS 8并转向CentOS Stream&#xff0c;许多企业和组织面临着寻找可靠替代方案的挑战。在这个背景下&#xff0c;龙蜥操作系统&#xff08;OpenAnolis&#xff09;作为一个稳定、高性能且完全兼容的企业级Linux发行版&#xff0…

车联网安全--TLS握手过程详解

目录 1. TLS协议概述 2. 为什么要握手 2.1 Hello 2.2 协商 2.3 同意 3.总共握了几次手&#xff1f; 1. TLS协议概述 车内各ECU间基于CAN的安全通讯--SecOC&#xff0c;想必现目前多数通信工程师们都已经搞的差不多了&#xff08;不要再问FvM了&#xff09;&#xff1b;…

iOS实际开发中使用Alamofire实现多文件上传(以个人相册为例)

引言 在移动应用中&#xff0c;图片上传是一个常见的功能&#xff0c;尤其是在个人中心或社交平台场景中&#xff0c;用户经常需要上传图片到服务器&#xff0c;用以展示个人风采或记录美好瞬间。然而&#xff0c;实现多图片上传的过程中&#xff0c;如何设计高效的上传逻辑并…

基于phpstudy快速搭建本地php环境(Windows)

好好生活&#xff0c;别睡太晚&#xff0c;别爱太满&#xff0c;别想太多。 2025.1.07 声明 仅作为个人学习使用&#xff0c;仅供参考 对于CTF-Web手而言&#xff0c;本地PHP环境必不可少&#xff0c;但对于新手来说从下载PHP安装包到配置PHP环境是个非常繁琐的事情&#xff0…

ffmpeg 编译遇到的坑

makeinfo: error parsing ./doc/t2h.pm: Undefined subroutine &Texinfo::Config::set_from_init_file called at ./doc/t2h.pm line 24. 编译选项添加&#xff1a; --disable-htmlpages

Git:merge合并、冲突解决、强行回退的终极解决方案

首先还是得避免冲突的发生&#xff0c;无法避免时再去解决冲突&#xff0c;避免冲突方法&#xff1a; 时常做pull、fatch操作&#xff0c;不要让自己本地仓库落后太多版本&#xff1b;在分支操作&#xff0c;如切换分支、合并分支、拉取分支前&#xff0c;及时清理Change&#…

国内外网络安全政策动态(2024年12月)

▶︎ 1.2项网络安全国家标准获批发布 2024年12月6日&#xff0c;根据2024年11月28日国家市场监督管理总局、国家标准化管理委员会发布的中华人民共和国国家标准公告&#xff08;2024年第29号&#xff09;&#xff0c;全国网络安全标准化技术委员会归口的2项网络安全国家标准正…

新兴的开源 AI Agent 智能体全景技术栈

新兴的开源 AI Agent 智能体全景技术栈 LLMs&#xff1a;开源大模型嵌入模型&#xff1a;开源嵌入模型模型的访问和部署&#xff1a;Ollama数据存储和检索&#xff1a;PostgreSQL, pgvector 和 pgai后端&#xff1a;FastAPI前端&#xff1a;NextJS缺失的一环&#xff1a;评估和…

通过一个含多个包且引用外部jar包的项目实例感受Maven的便利性

目录 1 引言2 手工构建3 基于Maven的构建4 总结 1 引言 最近在阅读一本Java Web的书籍1时&#xff0c;手工实现书上的一个含多个Packages的例子&#xff0c;手工进行编译、运行&#xff0c;最终实现了效果。但感觉到整个构建过程非常繁琐&#xff0c;不仅要手写各个源文件的编…

信息科技伦理与道德3:智能决策

1 概述 1.1 发展历史 1950s-1980s&#xff1a;人工智能的诞生与早期发展热潮 1950年&#xff1a;图灵发表了一篇划时代的论文&#xff0c;并提出了著名的“图灵测试”&#xff1b;1956年&#xff1a;达特茅斯会议首次提出“人工智能”概念&#xff1b;1956年-20世纪70年代&a…

Sql 创建用户

Sql server 创建用户 Sql server 创建用户SQL MI 创建用户修改其他用户密码 Sql server 创建用户 在对应的数据库执行&#xff0c;该用户得到该库的所有权限 test.database.chinacloudapi.cn DB–01 DB–02 创建服务器登录用户 CREATE LOGIN test WITH PASSWORD zDgXI7rsafkak…