
题目分析 :
看完题觉得题目挺难的,考虑过用dp做,但无奈想不出来合适的转移方程。
后来看了题解,对于各个题解中方案数的说明并不透彻,写下这篇题解,包含一些自己的理解。
对于 '(' 和 ')' ,并不是我们遇到一个不匹配的括号就需要加上与之匹配的括号。比如(((),第一个括号为左括号,第二个括号也为左括号,但跟第一个括号匹配的括号我们也不一定加在第二个括号处,可以等着从后面的括号处加,再或者后面会出现右括号与第一个左括号匹配。所以我们对于一个括号序列最少需要添加多少个是必须遍历到n才知道的,也就是说我们希望尽可能出现更多配对的括号来让我们更少地添加括号。
其次,左右括号的添加是独立的,我们添加括号只能在原括号序列的空隙里,我们先添加左括号,再添加右括号,那么这个空隙里的左右括号顺序是否能任意呢?答案是否定的,如下图:

可见只有 3 号添加序列是合法的,也就是说只有一种顺序,所以左右括号的添加是独立的。
状态表示
f[i][j]代表目前为前 i 个括号,左括号比右括号多 j 个时,只添加左括号或者右括号可以满足合法序列的总方案数。
我思考了很久这个表示意义,最终想了出来,这里的i 和 j 仅仅表示当前的状况,即左括号比右括号多 j 个, 但是f [i][j] 里面存的是添加左或右括号使序列合法的方案数,这俩并没有什么直接关系!
f [n][j] 前n个括号,左括号比右括号多j (j>=0)时,仅添加左/右括号可以使序列合法的总方案数。我们需要的是添加最少的括号使序列合法的方案总数,但这个最小值我们是未知的,所以需要再遍历一遍f[n][i](1<=i<=n),才能知道添加最少括号时的总方案数。那么我们又知道左右括号的添加相互独立,那么最终答案显然就是这两个方案数相乘。
避免重复
为了求这个方案数,我们还需要想出来怎么避免重复,我们在求最少添加左括号方案数时,规定只在右括号前一个空隙添加。(求右括号方案数时相反)这样就避免了重复,为什么呢,下面这张图可以帮助理解。

可见,我们在两个区间的左括号前添加左括号,与仅在右括号前添加左括号,最终得出来的括号序列是相同的也就是重复的。
状态转移方程
那么怎么理解这个方案数呢?
对于状态 f[i][j],左括号比右括号多 j 个时,只添加左括号使序列合法的总方案数。
如果第 i 个为 左括号,对于 f [i][j] 表示左括号比右括号多 j 个时,我们规定只在右括号前添加括号,故而左括号不添加,故直接延续,即前i个中左括号比右括号多j个,则上一个状态是前i-1个括号中,左括号比右括号多j-1个,即有f [i][j] = f[i-1][j-1]。//延续的尽头是上一次遇到右括号可以添加的总方案数。
如果第i个位 右括号,那么我们需要添加左括号了,f [i][j] 即前i个括号中左括号比右括号多 j 个时,那我们就需要考虑在第i个括号前添加几个左括号,因为在这是左括号已经比右括号多j个了,所以在这个空隙我们至多添加 j+1 个左括号(第i位右括号和j+1个左括号中一个抵消,刚好多出左括号比右括号多j个),至少是一个也不添加。所以就需要在 0 到 j+1 区间内枚举添加左括号的个数,将每个状态下合法方案数累加,就是总方案数。所以就有f[i][j]=f[i-1][j+1]+f[i-1][j]+…+f[i-1][0]。值得注意的是我们题目的数据需要 n方 的复杂度,如果再枚举一遍个数就相当于n的三次方了。我们又有f[i][j-1]=f[i-1][j]+f[i-1][j-1]+…+f[i-1][0]。所以f[i][j] = f[i-1][j+1]+f[i][j-1],如此一来就可以O(1)求出总方案数了。还有f [i][0] 时,不可以用这个递推式,会越界,所以我们特判一下,由f[i][j]=f[i-1][j+1]+f[i-1][j]+…+f[i-1][0],将i,j==0代入就有了f[i][0]=f[i-1][1]+f[i-1][0];
还有一点是我们求两种方案数的时候,可以用一个函数,我们仅需把原序列倒置,左右括号互换,就可以得出另外一种方案数了。
最后给出代码。
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5001,mod=1e9+7;
typedef long long ll;
int n;
char str[MAXN];
ll f[MAXN][MAXN];
//f[i][j] 前i个括号中,左括号比右括号多j个时,仅添加左/右括号使序列合法的总方案数
ll fun()
{
memset(f,0,sizeof(f));
f[0][0]=1;
for(int i=1;i<=n;i++)
{
if(str[i]=='(')
{
for(int j=1;j<=n;j++)
f[i][j]=f[i-1][j-1];
}
else
{
f[i][0]=(f[i-1][1]+f[i-1][0])%mod;
for(int j=1;j<=n;j++)
{
f[i][j]=(f[i-1][j+1]+f[i][j-1])%mod;
}
}
}
for(int i=0;i<=n;i++)
if(f[n][i]) return f[n][i];
}
int main()
{
scanf("%s",str+1);
n=strlen(str+1);
ll x=fun();//添加最少的左括号的合法方案数
reverse(str+1,str+n+1);
for(int i=1;i<=n;i++)
{
if(str[i]==')') str[i]='(';
else str[i]=')';
}
ll y=fun();//添加最少的右括号的合法方案数
cout<<(x*y)%mod;
return 0;
}
本文详细分析了一道关于括号序列的算法问题,探讨了如何通过动态规划找到最少添加括号使序列合法的方案数。讲解了状态表示、状态转移方程,并给出了O(n^2)复杂度的解决方案。此外,还提到利用序列反转求解添加最少右括号的方案数,最后给出了完整C++代码实现。

1569

被折叠的 条评论
为什么被折叠?



