第十二届蓝桥杯省赛 C/C++大学B组 试题J:括号序列

试题题目
在这里插入图片描述
本题为编程题第五题,最后压轴题目,故难度系数较高
解题思路:

1、分析

  当我们拿到数学题时,我们都会首先去审题,可以从题干中寻找有效信息,或者寻找隐藏的信息。
  那么对于本题目,我们可以获得哪些信息呢?直接获得的信息有如下两点:
  <1>、序列中原有的左括号 ′ ( ′ '(' ( 数量,存储在 i n t int int类型变量 c o u n t O r i g L countOrigL countOrigL 中;
  <2>、序列中原有的右括号 ′ ) ′ ')' ) 数量,存储在 i n t int int类型变量 c o u n t O r i g R countOrigR countOrigR 中;

  根据题目意思,要求让序列合法化,那么至少需要添加多少个左括号 ′ ( ′ '(' ( 或者右括号 ′ ) ′ ')' ) 呢?我们需要从原始数列中读取到隐藏的信息了,采用入栈的方法来统计至少需要添加括号的数量。
  <3>、序列中至少缺省的左括号 ′ ( ′ '(' ( 数量,存储在 i n t int int类型变量 c o u n t M i s s L countMissL countMissL 中;
  <4>、序列中至少缺省的右括号 ′ ) ′ ')' ) 数量,存储在 i n t int int类型变量 c o u n t M i s s R countMissR countMissR 中;

  拥有了以上四点数据后,我们还需要考虑什么呢?是否可以求出方案数了呢?还不行。那么什么是合法方案数呢?在左括号 ′ ( ′ '(' ( 前面添加右括号 ′ ) ′ ')' ),在右括号 ′ ) ′ ')' ) 后面添加左括号 ′ ( ′ '(' (,直到缺省的左括号 ′ ( ′ '(' ( 和右括号 ′ ) ′ ')' ) 数量全部添加完成。统计出添加右括号 ‘ ( ’ ‘(’ ( 方案数乘以添加左括号 ‘ ) ’ ‘)’ ) 方案数,分布乘法原理,便是最终答案。
  在右括号 ′ ) ′ ')' ) 前面能添加几个左括号 ′ ( ′ '(' (,在左括号 ′ ( ′ '(' ( 前面能添加几个右括号 ′ ) ′ ')' )。这时我们需要统计另外两个隐藏的信息:
  <5>、从左往右数,序列中第几个右括号 ′ ) ′ ')' ) 前面已经有了多少个左括号 ′ ( ′ '(' (,存储在 i n t int int 类型数组变量 A r r y R [ c o u n t O r i g R ] ArryR[countOrigR] ArryR[countOrigR] 中;
  <6>、从右往左数,序列中第几个左括号 ′ ( ′ '(' ( 前面已经有了多少个右括号 ′ ) ′ ')' ),存储在 i n t int int 类型数组变量 A r r y L [ c o u n t O r i g L ] ArryL[countOrigL] ArryL[countOrigL] 中;
  分析到这里,便可以计算方案数,采用动态规划思想解题。

统计信息代码设计与实现

	int ArryR[5001],ArryL[5001]; 
	int countMissR = 0, countMissL = 0;  //记录序列缺少的 '(' 和 ')' 的数量;
	int countOrigR = 0, countOrigL = 0;	 //记录序列原有的 '(' 和 ')' 的数量;	
	for(i=0; i<len; i++){                //入栈思想
		if(Str[i] == '('){ countOrigL++; countMissR++; }
		if(Str[i] == ')'){
			ArryR[++k] = countOrigL;
			if(countMissR > 0){ countMissR--;}   //查询到右括号')'时,发现其前方有了左括号'('与之匹配,缺少的右括号')'数需要减1;
			else{  countMissL++;  }
		}
		j = len-i-1;                	 //从右往左数,即镜像反转,第几个左括号后有几个右括号的数量数组。
		if(Str[j] == ')') { countOrigR++; }
		if(Str[j] == '(') { ArryL[++n] = countOrigR; }
	}

2、动态规划——添加左括号 ′ ( ′ '(' (

  <1>状态。 d p [ i ] [ j ] dp[i][j] dp[i][j];表示在第 i i i 个右括号 ′ ) ′ ')' ) 前添加 j j j 个左括号 ′ ( ′ '(' ( 的方案数量。

  <2>状态转移方程。
  d p [ i ] [ j ] = d p [ i − 1 ] [ 0 ] + d p [ i − 1 ] [ 1 ] + … … + d p [ i − 1 ] [ j ]   \ dp[i][j]=dp[i-1][0] + dp[i-1][1] + …… + dp[i-1][j] \,  dp[i][j]=dp[i1][0]+dp[i1][1]+……+dp[i1][j]
  第 i i i 个右括号 ′ ) ′ ')' ) 前添加 j j j 个左括号 ′ ( ′ '(' ( 的方案数量,等于第 i − 1 i-1 i1 个右括号 ′ ) ′ ')' ) 前添加 0 , 1 , . . . . . . , j 0,1,......,j 0,1,......,j 个左括号 ′ ( ′ '(' ( 的方案数的总和,得到以上方程。
  其中,a、 i i i的取值范围: 1 1 1 ~ c o u n t O r i g R countOrigR countOrigR。遍历序列中所有的右括号 ′ ) ′ ')' ) 。在每一个右括号 ′ ) ′ ')' ) 前都可以添加左括号 ′ ( ′ '(' (
     b、 j j j的取值范围: i − A r r y R [ i ] i-ArryR[i] iArryR[i] ~ c o u n t M i s s L countMissL countMissL。右括号 ′ ) ′ ')' ) 前可以添加的左括号 ′ ( ′ '(' ( 数量。如果右括号 ′ ) ′ ')' )已经有了 A r r y R [ i ] ArryR[i] ArryR[i] 数量的左括号 ′ ( ′ '(' (,那么最少需要添加 i − A r r y R [ i ] i-ArryR[i] iArryR[i] 个左括号 ′ ( ′ '(' ( 。当需要添加的左括号 ′ ( ′ '(' ( 数量比原有的右括号 ′ ) ′ ')' ) 数量少时,即不需要再添加右括号 ′ ) ′ ')' ) 了,当 i − A r r y R [ i ] < 0 i-ArryR[i]<0 iArryR[i]<0 时,取 0 0 0

  <3>边界。
  a、推知第 1 1 1 个右括号 ′ ) ′ ')' ) 前添加任何多个左括号 ′ ( ′ '(' ( 的方案数都是 1 1 1,即:
  d p [ 1 ] [ 1 ] 、 . . . . . . 、 d p [ 1 ] [ c o u n t M i s s L ] = 1 ;   \ dp[1][1]、......、dp[1][countMissL] = 1;\  dp[1][1]......dp[1][countMissL]=1; 
  b、当第 1 1 1 个右括号 ′ ) ′ ')' ) 前原本就有左括号 ′ ( ′ '(' ( ;那么第 1 1 1个右括号 ′ ) ′ ')' ) 前方不添加左括号 ′ ( ′ '(' (的方案数是也是 1 1 1。即:
A r r y R [ 1 ] > 0 时, d p [ 1 ] [ 0 ] = 1 ArryR[1]>0时,dp[1][0] = 1 ArryR[1]>0时,dp[1][0]=1

3、动态规划——添加右括号 ′ ( ′ '(' (
  与添加左括号 ′ ( ′ '(' ( 的原理相同。把序列从右往左看,第 i i i 个左括号 ′ ( ′ '(' ( 后面添加 j j j 个右括号 ′ ) ′ ')' ) 的方案数量,等于第 i − 1 i-1 i1 个左括号 ′ ( ′ '(' ( 前添加 0 , 1 , . . . . . . , j 0,1,......,j 0,1,......,j 个右括号 ′ ) ′ ')' ) 方案数的总和;
  就相当于把原有的序列镜像反转。镜像反转后,原序列中的左括号 ′ ( ′ '(' ( 数量就是新序列中的右括号 ′ ) ′ ')' ) 数量,原序列中 A r r y L [ ] ArryL[] ArryL[] 数组就是新序列中的 A r r y R [ ] ArryR[] ArryR[] 数组;

动态规划代码设计与实现

int calc(int n,int count,int arry[]){             // n是序列中右括号')'的数量;
	for(i=0;i<=n;i++)                             // count是序列中待添加的左括号'('的数量;
		for(j=0;j<=n;j++)                         // arry[i]表示第几个右括号')'前有多少个左括号'('的数量数组;
			dp[i][j] = 0;
	if(count == 0){ return 1; }                   // 当序列中不需要添加左括号'('时,方案数为1;
	if(arry[1] > 0){                              // 边界。第1个右括号')'前原本就有左括号'('时,第1个右括号')'前方不添加左括号'('的方案数是1;
		dp[1][0] = 1;
	}
	for(i=1; i<=count; i++){                      // 边界。第1个右括号')'前添加任何多个左括号'('的方案数都是1;
		dp[1][i] = 1;
	}
	for(i=2; i<=n; i++){						  // 状态转移方程的实现。从第2个右括号')'开始遍历;
		int Startj = i-arry[i] > 0 ? i-arry[i] : 0; 
		for(j= Startj; j<=count; j++){
			for(k=0; k<=j; k++){
				dp[i][j] = (dp[i][j] + dp[i-1][k]) % Mod;
			}
		}
	}
	return dp[n][count];
}

4、综合以上完整实现代码,添加取模因子、字符串最大数量灯全局变量或者局部变量。注意初始化以及类型。

#include<stdio.h>
#include<string.h>
#define Mod 1000000007      // 取模因子 
#define Max 5001		    // 字符串最大数量 
int i,j,k,n;                // 循环因子    
int dp[Max][Max], ArryR[Max], ArryL[Max]; // 状态 	
char Str[Max];
/*function*/
int calc(int, int, int []);
int main(){
	scanf("%s",&Str);
	int len = strlen(Str);	             // 记录字符串长度 
	int countMissR = 0, countMissL = 0;  // 记录序列缺少的 "("和")"的数量。
	int countOrigR = 0, countOrigL = 0;	 // 记录序列原有的 "("和")"的数量。
	long long Ins1,Ins2;
	for(i=0; i<len; i++){
		if(Str[i] == '('){ countOrigL++; countMissR++; }
		if(Str[i] == ')'){
			ArryR[++k] = countOrigL;
			if(countMissR > 0){ countMissR--; }
			else{  countMissL++;  }
		}
		j = len - i - 1;
		if(Str[j] == ')') { countOrigR++; }
		if(Str[j] == '(') { ArryL[++n] = countOrigR; }	
	}
	Ins1 = calc(countOrigR, countMissL, ArryR); 
	Ins2 = calc(countOrigL, countMissR, ArryL);
    printf("%d\n",(Ins1*Ins2) % Mod);	
    return 0;
}

int calc(int n, int Count, int arry[]){
	if(Count == 0) return 1;
	for(i=0; i<=n; i++)
		for(j=0; j<=n; j++)
			dp[i][j] = 0;
	if(arry[1] > 0){ dp[1][0] = 1; }
	for(i=1; i<=Count; i++){ dp[1][i] = 1; }	
	for(i=2; i<=n; i++){
		int Startj = i-arry[i] > 0 ? i-arry[i] : 0;           
		for(j= Startj; j<=Count; j++){
			for(k=0; k<=j; k++){
				dp[i][j] = (dp[i][j] + dp[i-1][k]) % Mod;
			}
		}
	}
	return dp[n][Count];
}

5、优化
  经过测评发现,有运行超时而不得分的情况,那么需要对算法进行时间优化。这里发现在计算转移方程时,可以进行前缀和优化:
  原动态转移方程:
d p [ i ] [ j ] = ( d p [ i − 1 ] [ 0 ] + d p [ i − 1 ] [ 1 ] + … … + d p [ i − 1 ] [ j − 1 ] ) + d p [ i − 1 ] [ j ] = d p [ i ] [ j − 1 ] + d p [ i − 1 ] [ j ] ; dp[i][j] = (dp[i-1][0] + dp[i-1][1] + …… + dp[i-1][j-1]) + dp[i-1][j] = dp[i][j-1]+dp[i-1][j]; dp[i][j]=(dp[i1][0]+dp[i1][1]+……+dp[i1][j1])+dp[i1][j]=dp[i][j1]+dp[i1][j]
  进一步,用 p r e _ S u m [ ] pre\_Sum[] pre_Sum[] 数组来统计 d p [ i ] [ j ] dp[i][j] dp[i][j] 的前 j j j 项和。那么就有了:
d p [ i − 1 ] [ 0 ] + d p [ i − 1 ] [ 1 ] + … … + d p [ i − 1 ] [ j − 1 ] = p r e _ S u m [ j − 1 ] ; dp[i-1][0] + dp[i-1][1] + …… + dp[i-1][j-1] = pre\_Sum[j-1]; dp[i1][0]+dp[i1][1]+……+dp[i1][j1]=pre_Sum[j1]
  故:
d p [ i ] [ j ] = p r e _ S u m [ j − 1 ] + d p [ i − 1 ] [ j ] ; dp[i][j] = pre\_Sum[j-1]+dp[i-1][j]; dp[i][j]=pre_Sum[j1]+dp[i1][j]
  前缀和数组推导:
p r e _ S u m [ 0 ] = d p [ i ] [ 0 ] ; pre\_Sum[0] = dp[i][0]; pre_Sum[0]=dp[i][0];
p r e _ S u m [ 1 ] = d p [ i ] [ 0 ] + d p [ i ] [ 1 ] ; pre\_Sum[1] = dp[i][0]+dp[i][1]; pre_Sum[1]=dp[i][0]+dp[i][1];
p r e _ S u m [ 2 ] = d p [ i ] [ 0 ] + d p [ i ] [ 1 ] + d p [ i ] [ 2 ] ; pre\_Sum[2] = dp[i][0]+dp[i][1]+dp[i][2]; pre_Sum[2]=dp[i][0]+dp[i][1]+dp[i][2];
p r e _ S u m [ j ] = d p [ i ] [ 0 ] + d p [ i ] [ 1 ] + d p [ i ] [ 2 ] + . . . . . . + d p [ i ] [ j ] = p r e _ S u m [ j − 1 ] + d p [ i ] [ j ] ; pre\_Sum[j] = dp[i][0]+dp[i][1]+dp[i][2]+......+dp[i][j] = pre\_Sum[j-1]+dp[i][j] ; pre_Sum[j]=dp[i][0]+dp[i][1]+dp[i][2]+......+dp[i][j]=pre_Sum[j1]+dp[i][j];
  注意: d p [ i ] [ j ] dp[i][j] dp[i][j] 是二维数组,每一次 i i i 的循环都是需要一个记录前 j j j 项和的数组。进入循环后都需要对 p r e _ S u m [ j ] pre\_Sum[j] pre_Sum[j] 清零,那么需要另一个数组 p r e _ V a l u e [ ] pre\_Value[] pre_Value[] 存储上一次循环记录下的值。且只在闭区间 [ m a x ( i − a r r y [ i ] , 0 ) , C o u n t ] [max(i-arry[i],0),Count] [max(iarry[i],0),Count] 中成立。优化后的详细代码如下:

#include<stdio.h>
#include<string.h>
#define Mod 1000000007      // 取模因子 
#define Max 5001		    // 字符串最大数量 
int i,j,k,n;                // 循环因子    
int dp[Max][Max], ArryR[Max], ArryL[Max]; // 状态 	
char Str[Max];
/*function*/
int calc(int, int, int []);
int main(){
	scanf("%s",&Str);
	int len = strlen(Str);	             // 记录字符串长度 
	int countMissR = 0, countMissL = 0;  // 记录序列缺少的 "("和")"的数量。
	int countOrigR = 0, countOrigL = 0;	 // 记录序列原有的 "("和")"的数量。
	long long Ins1,Ins2;
	for(i=0; i<len; i++){
		if(Str[i] == '('){ countOrigL++; countMissR++; }
		if(Str[i] == ')'){
			ArryR[++k] = countOrigL;
			if(countMissR > 0){ countMissR--; }
			else{  countMissL++;  }
		}
		j = len - i - 1;
		if(Str[j] == ')') { countOrigR++; }
		if(Str[j] == '(') { ArryL[++n] = countOrigR; }	
	}
	Ins1 = calc(countOrigR, countMissL, ArryR); 
	Ins2 = calc(countOrigL, countMissR, ArryL);
    printf("%d\n",(Ins1*Ins2) % Mod);	
    return 0;
}

int calc(int n, int Count, int arry[]){
	int pre_Sum[Max],pre_Value[Max];
	if(Count == 0) return 1;
	for(i=0; i<=n; i++){
		for(j=0; j<=n; j++)
			dp[i][j] = 0;
		pre_Sum[i] = 0;
		pre_Value[i] = 0;
	}
	if(arry[1] > 0){ 
		dp[1][0] = 1; 
		pre_Sum[0] = 1; 
	}
	for(i=1; i<=Count; i++){ 
		dp[1][i] = 1; 
		pre_Sum[i] = pre_Sum[i-1] + 1;
	}	
	for(i=2; i<=n; i++){
		for(j=0; j<=Count; j++){
			pre_Value[j] = pre_Sum[j];
			pre_Sum[j] = 0;
		}
		int Startj = i-arry[i] > 0 ? i-arry[i] : 0;           
		for(j= Startj; j<=Count; j++){
			dp[i][j] =  pre_Value[j];
			if(j == 0)  pre_Sum[j] = dp[i][0];
			else pre_Sum[j] = (pre_Sum[j-1] + dp[i][j]) % Mod;
		}
	}
	return dp[n][Count];
}




  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值