试题题目:
本题为编程题第五题,最后压轴题目,故难度系数较高
解题思路:
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[i−1][0]+dp[i−1][1]+……+dp[i−1][j]
第
i
i
i 个右括号
′
)
′
')'
′)′ 前添加
j
j
j 个左括号
′
(
′
'('
′(′ 的方案数量,等于第
i
−
1
i-1
i−1 个右括号
′
)
′
')'
′)′ 前添加
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]
i−ArryR[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]
i−ArryR[i] 个左括号
′
(
′
'('
′(′ 。当需要添加的左括号
′
(
′
'('
′(′ 数量比原有的右括号
′
)
′
')'
′)′ 数量少时,即不需要再添加右括号
′
)
′
')'
′)′ 了,当
i
−
A
r
r
y
R
[
i
]
<
0
i-ArryR[i]<0
i−ArryR[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
i−1 个左括号
′
(
′
'('
′(′ 前添加
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[i−1][0]+dp[i−1][1]+……+dp[i−1][j−1])+dp[i−1][j]=dp[i][j−1]+dp[i−1][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[i−1][0]+dp[i−1][1]+……+dp[i−1][j−1]=pre_Sum[j−1];
故:
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[j−1]+dp[i−1][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[j−1]+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(i−arry[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];
}