括号序列(DP优化)

Question:

Solve:

声明:全文为蓝桥杯官方题解的重新思考整理,众所周知我写这道题写出事故了,可能解释的也会很难懂~

不难想到是dp,但是怎么dp真的不好想

参数解释:

cntl, cntr  原括号序列想要合法所需填充的左、右括号数

pos1, pos2  原括号序列所含有的左(右)、右(左)括号数

dp[ i ][ j ]   

dp数组,数值表示第 i 个左(右)括号位置前一共填充 j 个右(左)括号的方案数

pre[ i ] 

前缀和维护数组,填充括号数小于等于 i 的总方案数

minn[ i ] 

最小填充数组,表示第 i 个左(右)括号位置前面至少要填充的右(左)括号数

解题历程:

step 1:原括号序列想要合法所需填充的左、右括号数计算

方法:

遍历s,遇到 " ) " 时 cnt 减一,反之加一,当cnt < 0 时,cnt清零,同时cntl加一,遍历结束之后,cnt 的值也就是所需要的左括号数目,下面举个例子:

step 2:主体,只填充某种括号的方案数计算

dp基本思考:

用左括号的填充为例~

(dp第一维)

每一次进行括号填充的位置一定是在每一个右括号所在的位置,所以,以每一个右括号的位置 pos 为切入点,思考填充方案:

(dp第二维)

首先,假定最近两个右括号的位置分别为pos_1, pos_2 (pos_2 > pos_1)

那么,对于前 pos_2 的子串

所填充的左括号数必须大于最小填充数 minn[pos_2],小于总填充的最大数目 cntl

(dp第三维)

在上述范围内随便取一个数 num ,我们就可以选择在前 pos_1 的子串里添加 num1 个左括号,然后在 pos_2 的位置填充(num - num1)个左括号,从而实现前 pos_2 的子串一共填充 num 个左括号的要求,不难知道num1的范围是[ 0 ~ num ],严格来说应该是[ minn[pos_1] ~ num ]

那么前 pos_2 的子串一共填充 num 个左括号的方案数也就是:对 num1 从 0 取到 num 时 pos_1位置的填充方案求和 

状态转移方程:

上述已经分析出 pos_2 位置的方案数可以用 pos_1 位置的方案数来表示,现在建立 dp 主体:

第一维的核心在于右括号的位置,其实也是右括号的出现序数,所以用 dp[ i ] 来表示第 i 个右括号

第二维的核心在于填充的左括号数目范围

第三维的核心在于对每个填充数的具体讨论

所以dp[ i ][ j ] 的 j 来表示填充数,结合所求的是方案数,所以数组整体含义:第 i 个右括号位置前一共填充 j 个左括号的方案数

结合所有分析,现在就可以得到一个关系式了:

dp[ i ][ j ] = sum( dp[i-1][0] + ... + dp[i-1][j])  

dp优化方向:

得到上述的关系式以后,我们思考程序:

1.明确这是一个三层的循环

第一层从 1 到 右括号的总数目,是对dp数组的 i 遍历

第二层从第 i 个右括号位置的最小填充数到最大填充数,是对dp数组的 j 遍历

第三层是从 0 到 j 的遍历,表示关系式里的求和赋值

只有经过这三层循环之后,才能得到最终的结果dp[ pos1 ][ cntl ]

可以注意到,在第三层循环里,会不断的计算前缀和这个东西,那我就可以再开一个 pre 数组去保存前缀和,从而把第三层循环去掉,变成直接赋值,保证程序能够AC

2.具体怎么实现二维优化

我们知道,在第二层里填充数有一个范围[ minn[pos], cntl ]

而变成赋值之后,也就是去掉第三层循环, 添加式子 dp[ i ][ j ] = pre[ j ] (pre[ j ]含义看文章开头)

所以可以得到这样的一个逻辑式

并且,赋值前后要先将这个逻辑式实现 

step 3:反转计算 

上面的过程只是分析了填充左括号的方案数,还有填充右括号,二者是独立的,所以需要再次执行上述的过程,然后将两次的方案数相乘

其中涉及到对于原序列括号数、最小填充数minn[]的重新计算,可以将原括号序列彻底翻转来进行计算,比如括号序列")()((()" ,翻转为"()))()("

整个的具体实现看代码吧~

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int mod = 1e9+7;
string s;
int cntl, cntr, cnt;
ll dp[5010][5010], pre[5010], minn[5010];
//函数参数(需要填充的对应括号数,是否为左括号填充)
ll solve(int ans, bool isl){
    if(ans == 0) return 1;
    //初始化
    memset(dp,0,sizeof(dp));
    memset(pre,0,sizeof(pre));
    memset(minn,0,sizeof(minn));
    //括号反转
    if(!isl){
        for(int i = 0; s[i]; i++){
            if(s[i] == '(') s[i] = ')';
            else s[i] = '(';
        }
        reverse(s.begin(), s.end());
    }
    //计算第pos个左(右)括号前最少需要的右(左)括号数minn[pos]
    int pos1 = 0, pos2 = 0;
    for(int i = 0; s[i]; i++){
        if(s[i] == ')')
            minn[++pos1] = pos2;
        else
            pos2++;
    }
    //dp,pre初始化
    if(minn[1] > 0) pre[0] = dp[1][0] = 1;
    for(int i = 1; i <= ans; i++){
        dp[1][i] = 1; pre[i] = pre[i-1] + 1;
    }
    //第一维:从2到所含有的最大对应括号数
    for(int i = 2; i <= pos1; i++){
        //第二维
        //小于最小填充数部分前缀和更新为0
        for(int j = 0; j < i-minn[i]; j++) pre[j] = 0;
        for(int j = i-minn[i]; j <= ans; j++){
            dp[i][j] = pre[j];
            //在填充范围内部分更新前缀和
            if(j - 1 < 0) pre[j] = dp[i][j];
            else pre[j] = (pre[j-1] + dp[i][j]) % mod;
        }
    }
    return dp[pos1][ans];
}
int main(void) {
    cin >> s;
    //计算所需括号数量
    cnt = cntl = cntr = 0;
    for (int i = 0; s[i]; i++) {
        if (s[i] == '(') cnt++;
        else cnt--;
        if (cnt < 0) {
            cntl++;
            cnt = 0;
        }
    }
    cntr = cnt;
    //调用函数输出结果
    cout << solve(cntl, true) * solve(cntr, false) % mod;
    return 0;
}

最后附上蓝桥杯汇总链接:蓝桥杯C/C++A组省赛历年真题题解

声明:图片均来源于蓝桥杯官网,以个人刷题整理为目的,如若侵权,请联系删除~

  • 12
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

UmVfX1BvaW50

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

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

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

打赏作者

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

抵扣说明:

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

余额充值