题目
小艺酱又得到了一堆括号。括号是严格匹配的。现在给括号进行上色。上色有三个要求:
1、只有三种上色方案,不上色,上红色,上蓝色。
2、每对括号只有一个上色。
3、相邻的两个括号不能上相同的颜色,但是可以都不上色。
问括号上色有多少种方案?答案对1000000007取模。
输入描述:
输入括号序列s。(2<=|s|<=700)
输出描述:
输出方案数。
示例
输入 (())
输出 12
分析
CSDN每日一练里的题目,也曾出现在周赛的第十期。原题在这里(或者洛谷)。问哥之前没有做过此题,所以在周赛的时候也没能在规定时间解出来。问哥一开始的思路是想找到数学公式,但后来发现还是要考虑内外相邻括号的上色限制,依次向内计算,怎么都感觉是递归,但是又没有想好递归的条件,于是只能作罢。赛后也没有再去细想,直到今天在每日一练里又遇到此题,才重新琢磨起来。
此题的题解不少,但问哥认为都没说到点子上,也许有一部分人只是为了写题解,而把题解抄来抄去,没有认真去理解。比如很多题解上来就要开四维数组, 的空间大小,在C里也许没问题,但是Python因为开数组的话一定要初始化,四维数组的空间实在是太浪费了(问哥没有尝试开四维数组的做法,直觉上认为内存可能会占用太多)。而实际上,完全没有必要开四维数组,甚至连数组都不需要。但是开数组的这个思想代表了动态规划,也是此题解法的突破口,如果能读懂四维数组的解法,那用Python解出此题自然不难,如果不懂,且听我细细道来。
此题难度中等偏上,但是相对又给了一些“友好的”暗示。比如:
1)括号是严格匹配的。也就是说左括号和右括号的排列一定是在数学上是合法的。比如 或 ,都是严格匹配的括号。而像 或 乃至左右括号数量不一致是绝对不会出现的。而且也可以得出字符串的第一个字符一定是“(”,最后一个字符一定是“)”。
2)上色方案只有三种:不上色、上红色、上蓝色。
由第一条暗示我们可以猜测出字符串一定是以下三种形式之一:
- 括号嵌套
- 括号并排
- 以上两种混合
听起来像废话,但这却是递归的基础:要计算 (........) 括号的上色方案数,向内递归查询(或者如别的解法中说的“状态转移”),就一定能够转化成上面两种形式,而最后,也一定会递归到只有一对括号的情况——这便是递归的出口。
而由第二条暗示,如果不加题目的其他条件的话,我们可以得出一对括号的配色方案最多只有 种。也就是:
左括号 | 不上色 | 不上色 | 不上色 | 红色 | 红色 | 红色 | 蓝色 | 蓝色 | 蓝色 |
右括号 | 不上色 | 红色 | 蓝色 | 不上色 | 红色 | 蓝色 | 不上色 | 红色 | 蓝色 |
但是由于题目中的限制:每对括号只有、且必须有一个上色。(“且必须有”这几个字题目没有,但确实存在这个限制,问哥认为这是CSDN抄题及其不严谨,也因此让我浪费不少时间),所以实际上一对成对的括号的上色方案只有四种:
左括号 | 不上色 | 不上色 | 红色 | 蓝色 |
右括号 | 红色 | 蓝色 | 不上色 | 不上色 |
这里就要考虑代码设计的数据结构问题了,用什么样的数据结构来表示不同的上色方案呢?基本上所有题解给出的都是3x3的二维数组,这也是合理、而且直观的,我们先来看下:
右括号 -> 左括号 | 0 - 不上色 | 1 - 红色 | 2 - 蓝色 |
0 - 不上色 | (0, 0) | (0, 1) | (0, 2) |
1 - 红色 | (1, 0) | (1, 1) | (1, 2) |
2 - 蓝色 | (2, 0) | (2, 1) | (2, 2) |
其中绿色标出的就是单独成对的括号的合法上色方案。(其实明白了解法,不用数组,用Python的字典也可以完成,问哥下面给出的代码就是使用的字典)
但是为什么要研究一对括号的配色方案呢?因为如刚才提到的,所有可能的符号组合,都是由一对对成对的括号组合在一起的,这些最基本的配色方案,决定(限制)了和它们相邻或包含它们的括号的配色方案。而当只有这一对括号时,总的配色方案个数就是这几个位置的方案数加在一起(4种)。举个例子,当对应坐标(0,1)的时候,就相当于这对括号“左括号不上色,右括号上红色”的方案有多少,因为只有一对括号,括号内再无其他配色可能,自然就只有一种方案,而四种合法的方案加在一起,就是4。所以,对于只有一对括号的基本情况,我们对这四个位置赋值为1,而其余不合法的五个位置,赋值为0。
由此扩展开来,我们要找形如“(...)”的括号组合的配色方案,就是要找这九种可能的方案各是多少,然后加在一起,便是总数。
基本思路有了,那我们来分析一下形如“(...)”的字符串可能出现的情况:
1)只有左右括号“()”。基础情况,上面已经讨论过,四个位置的方案数各为一,总数为4.
2)中间包含其它括号,但左右最外边的括号是一对。既然是一对,就符合上述的限制,所以最外面括号合法的配色方案只有4种,但是如果考虑到里面的括号,就需要进行递归累加了。但是,也只需要累加这四个位置的方案数。比如最简单的“(())”,就只需要计算当外面的括号分别取这四种配色方案时,里面的括号各有多少种配色方案即可。而在累加的过程中,就需要遵循“相邻的括号取色不能相同”,通过判断跳过一些方案。
看出来没,最外面一层的(总)配色方案数取决于向内一层的括号的配色方案数。那我们就不断进行递归就好了,递归到最后,一定能得到只有一对括号的情况。。。除了括号的并列。
3)左右两边的括号不是一对。这种就是括号并列的情况,比如“()()”。如果你的直觉是用乘法(怎样乘先不管),那我要恭喜你,的确是用乘法。但是怎样乘呢?我们已知一对括号的最多方案数是4,那难道是4x4=16?肯定不是,别忘了,相邻的括号“)(”配色不能相同,除非不上色。所以还要减去这两个括号都上红色,和都上蓝色的方案,所以只有14种。但是有没有公式呢?很遗憾,因为左右的括号对里还可能包含更多的括号,情况可能更加复杂,所有问哥并没有找到通用的数学公式(没有找到,也许并不代表没有,假如有的话,问哥直觉上应该和2的幂有关)。
我们假设字符串的形式为“(...)(...)”,而且我们已经知道了左右两边“(...)”的各自方案总数,那么根据我们上边介绍的九种上色方案(注意,这里不能只看四种,因为“(...)”不一定完全配对),假设左边的“(...)”选择了“左括号不上色,右括号上红色”,而这种方案数为种,那么右边的“(...)”因为要考虑到相邻的“)(”不能上色相同,只能查看其它八种上色方案各自的个数,每种方案个数都要与相乘,然后再加在一起,然后左边的“(...)”再选择其它上色方案,再继续检查右边。。。这不就是嵌套循环?
这三种情况就代表了所有可能的括号组合,所以,现在再来理一理思路:
我们要得出“(......)”的上色方案总数,等于计算左右括号共九种上色方案数的总和。我们可以用列表或字典来表示这九种方案。而这九种方案各自的个数,又分为于两种情况,1)这两个括号配对成功,则取决于内层括号的方案数,也就是把外面这两个括号“剥去”,再重复这个计算。2)这两个括号不是一对,则要找到左边括号的配对右括号(之所以找配对的右括号,是为了保证这个右括号相邻的右边第一个字符一定是“(”,想一想为什么?),然后再以这个右括号为界,转化成左右两个“(...)”的形式递归计算各自的方案数,然后再合并在一起通过嵌套循环相乘相加。
综上所述,我们最终得到的答案,应该是一个有9个元素的(3x3二维列表或字典)容器,包含了九种上色方案各自的个数,然后把它们加在一起,就是答案。
说了这么多,还有两个最基本的事情要完成:
1)如何表示左右括号代入计算?很显然,用左右括号在字符串中下标位置来表示是最好不过的了。0代表最左边的左括号,代表最右边(字符串长度为)的右括号。用left表示左括号的坐标,right表示右括号的坐标,由此可见,如果只有一对括号,right-left=1。
2)要完成括号的配对。简单说,就是要找出每个左括号所对应的右括号的位置,用来判断left和right是不是能够配对,从而决定不同的递归计算方法。
完成括号的配对是比较简单的栈操作了,注意的是我们这里要记录的是左右配对括号在字符串中的下标位置,而且在后面要通过左括号的位置查找配对的右括号的位置,所以使用字典来保存较为方便:左括号的位置作为键,右括号的位置作为值。具体实现方法可以参考下面的代码的后半部分。
参考代码
def dp(left, right):
color = {(i,j):0 for i in range(3) for j in range(3)} # 用字典代替二维列表表示九种方案,初始值均为0
if right-left == 1: # 只有一对括号的情况
for x in [(0,1), (0,2), (1,0), (2,0)]: # 一对配对的括号只有这四种合法的上色方案,下同
color[x] = 1 # 合法方案为1
elif bracket[left] == right: # 左右括号配对成功
inner = dp(left+1,right-1) # 递归计算内部“(...)”的方案
for x,y in [(0,1), (0,2), (1,0), (2,0)]:
for i in range(3):
for j in range(3):
if y==0 and i!=x or x==0 and j!=y: # 相邻颜色不能相同
color[(x,y)] += inner[(i,j)]
else: # 左右括号配对不成功
l = dp(left, bracket[left]) # 递归计算左边“(...)”的方案
r = dp(bracket[left]+1, right) # 递归计算右边“(...)”的方案
for i in range(3):
for j in range(3):
for x in range(3):
for y in range(3):
if x==0 or x!=y: # 相邻要么都不上色,要么颜色不能相同
color[(i,j)] += l[(i,x)]*r[(y,j)]
return color # 返回包含九种方案个数的字典
s = input()
bracket = dict()
stack = [] # 查找配对括号的栈操作
for i in range(len(s)):
if s[i] == "(":
stack.append(i)
else:
bracket[stack.pop()] = i
result = sum(dp(0,len(s)-1).values()) # 要计算的结果就是把九种方案各自的个数加在一起,因为使用的是字典,所以直接对字典的值进行求和即可
print(result%1000000007)
有几个测试例子的返回结果太大,传统编程语言可能无法表示,所以要求把结果对10e9+7取模。其实python没有这个问题,只不过为了通过测试。