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组省赛历年真题题解
声明:图片均来源于蓝桥杯官网,以个人刷题整理为目的,如若侵权,请联系删除~