Python解题 - 括号上色(递归)

题目

小艺酱又得到了一堆括号。括号是严格匹配的。现在给括号进行上色。上色有三个要求:

1、只有三种上色方案,不上色,上红色,上蓝色。

2、每对括号只有一个上色。

3、相邻的两个括号不能上相同的颜色,但是可以都不上色。

问括号上色有多少种方案?答案对1000000007取模。

输入描述:

输入括号序列s。(2<=|s|<=700)

输出描述:

输出方案数。

示例

输入 (())

输出 12

分析

CSDN每日一练里的题目,也曾出现在周赛的第十期。原题在这里(或者洛谷)。问哥之前没有做过此题,所以在周赛的时候也没能在规定时间解出来。问哥一开始的思路是想找到数学公式,但后来发现还是要考虑内外相邻括号的上色限制,依次向内计算,怎么都感觉是递归,但是又没有想好递归的条件,于是只能作罢。赛后也没有再去细想,直到今天在每日一练里又遇到此题,才重新琢磨起来。

此题的题解不少,但问哥认为都没说到点子上,也许有一部分人只是为了写题解,而把题解抄来抄去,没有认真去理解。比如很多题解上来就要开四维数组,N*N*3*3 的空间大小,在C里也许没问题,但是Python因为开数组的话一定要初始化,四维数组的空间实在是太浪费了(问哥没有尝试开四维数组的做法,直觉上认为内存可能会占用太多)。而实际上,完全没有必要开四维数组,甚至连数组都不需要。但是开数组的这个思想代表了动态规划,也是此题解法的突破口,如果能读懂四维数组的解法,那用Python解出此题自然不难,如果不懂,且听我细细道来。

此题难度中等偏上,但是相对又给了一些“友好的”暗示。比如:

1)括号是严格匹配的。也就是说左括号和右括号的排列一定是在数学上是合法的。比如 (()) 或 ()(),都是严格匹配的括号。而像 ))(( 或 )()( 乃至左右括号数量不一致是绝对不会出现的。而且也可以得出字符串的第一个字符一定是“(”,最后一个字符一定是“)”。

2)上色方案只有三种:不上色、上红色、上蓝色。

由第一条暗示我们可以猜测出字符串一定是以下三种形式之一:

  1. ((())) 括号嵌套
  2. ()()() 括号并排
  3. 以上两种混合

听起来像废话,但这却是递归的基础:要计算 (........) 括号的上色方案数,向内递归查询(或者如别的解法中说的“状态转移”),就一定能够转化成上面两种形式,而最后,也一定会递归到只有一对括号的情况——这便是递归的出口。

而由第二条暗示,如果不加题目的其他条件的话,我们可以得出一对括号的配色方案最多只有 3*3=9 种。也就是:

左括号不上色不上色不上色红色红色红色蓝色蓝色蓝色
右括号不上色红色蓝色不上色红色蓝色不上色红色蓝色

但是由于题目中的限制:每对括号只有、且必须有一个上色。(“且必须有”这几个字题目没有,但确实存在这个限制,问哥认为这是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的幂有关)。

我们假设字符串的形式为“(...)(...)”,而且我们已经知道了左右两边“(...)”的各自方案总数,那么根据我们上边介绍的九种上色方案(注意,这里不能只看四种,因为“(...)”不一定完全配对),假设左边的“(...)”选择了“左括号不上色,右括号上红色”,而这种方案数为x种,那么右边的“(...)”因为要考虑到相邻的“)(”不能上色相同,只能查看其它八种上色方案各自的个数,每种方案个数都要与x相乘,然后再加在一起,然后左边的“(...)”再选择其它上色方案,再继续检查右边。。。这不就是嵌套循环?

这三种情况就代表了所有可能的括号组合,所以,现在再来理一理思路:

我们要得出“(......)”的上色方案总数,等于计算左右括号共九种上色方案数的总和。我们可以用列表或字典来表示这九种方案。而这九种方案各自的个数,又分为于两种情况,1)这两个括号配对成功,则取决于内层括号的方案数,也就是把外面这两个括号“剥去”,再重复这个计算。2)这两个括号不是一对,则要找到左边括号的配对右括号(之所以找配对的右括号,是为了保证这个右括号相邻的右边第一个字符一定是“(”,想一想为什么?),然后再以这个右括号为界,转化成左右两个“(...)”的形式递归计算各自的方案数,然后再合并在一起通过嵌套循环相乘相加。

综上所述,我们最终得到的答案,应该是一个有9个元素的(3x3二维列表或字典)容器,包含了九种上色方案各自的个数,然后把它们加在一起,就是答案。

说了这么多,还有两个最基本的事情要完成:

1)如何表示左右括号代入计算?很显然,用左右括号在字符串中下标位置来表示是最好不过的了。0代表最左边的左括号,n-1代表最右边(字符串长度为n)的右括号。用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): # i,j分别表示内部“(...)”左右括号的上色方案,注意因为内部括号可能不匹配,所以9种方案都要考虑
                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 x,y in color.keys(): # 因为左右括号不配对,所以要统计所有九种方案的个数
            for i in range(3): # i表示左边“(...)”的右括号的上色方案
                for j in range(3): # j表示右边“(...)”的左括号的上色方案
                    if i==0 or i!=j: # 相邻要么都不上色,要么颜色不能相同
                        color[(x,y)] += l[(x,i)]*r[(j,y)]
    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没有这个问题,只不过为了通过测试。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值