题目来自leetcode第22题,在刷这道题的时候花了不少时间,个人认为,这道题有助于加深对于动态规划的理解。在这里分享自己的思考过程和心得。主要是自己的思考过程,从出错到发现问题到解决问题的过程,所以写在开头(狗头保命):如果你不想看前面的分析和错误过程,可以直接跳到最后。
其实,刚拿到这道题,很难一眼看出动态规划,这也是我不是一上来就讲动态规划的原因,但是相信大家看到题目应该会想到n次的所有排列结果肯定和n-1次的结果有关系,有一种青出于蓝的感觉,所以问题的核心转化为找到这样的关系具体是什么。
递归run 1(失败)
在这样的思路下,我进行了第一次尝试,通过观察n=1~3的结果,发现这样的一个规律,如下图所示,如果把第n次的排列结果即为A,那么第n+1次的结果为(A),()A,A()三种情况
这里我是使用递归的思想去做的,怀着激动的心情运行,发现n=4开始就都不对了。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
if n==0:return [""]
def combination(n):
if n==1:
return ["()"]
else:
res=[]
temp=combination(n-1)
res = ["()" + i for i in temp] + [i + "()" for i in temp] + ["(" + i + ")" for i in temp]
return list(set(res))
return combination(n)
我将n=4的结果与答案进行比对,发现少了一项(())(()),这一项是通过之前的思路无法得到的。
该思路的错误在于把n-1次的结果当成了一个整体,实际上不然,比如(())()的下一轮结果出来(a)或()a或a(),也就是((())()),()(())(),()(())()去重得到((())()),()(())()之外,还有(())(())、((()))、(),也就是不能把n-1次的所有结果都分别当成一个整体。
递归run 2(成功)
第二次尝试,发现第n次仅仅是比第n-1次多了一对括号,而左括号一定是加在最左边,所以问题的关键转换为寻找右括号的位置。
而右括号总是在上一轮结果的右括号后出现,所以我的思路就是遍历上一轮的每个结果的右括号位置进行添加新的右括号
but,漏掉了一种情况,就是在开头就加右括号
至此所有可能已经考虑,注意对结果要进行去重,一下是代码实现,同样还是采用的递归的思想。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
def combination(n):
if n==1:return ["()"]
last_res=combination(n-1)
l1=[]
for x in last_res:
for index,item in enumerate(x):
if item==")":
temp1="("+x[:index+1]+")"+x[index+1:]
if temp1 not in l1:
l1.append(temp1)
temp2 = "()" + x
if temp2 not in l1:
l1.append(temp2)
return l1
if n==0:return []
else:return combination(n)
外层循环对上一轮的每个结果进行遍历,内层循环对每个结果中的字符进行遍历以找出右括号的位置,关键是"("+x[:index+1]+")"+x[index+1:]这一步使用切片实现括号的添加,同时使用in语句判断是否重复。
执行后发现时间复杂度太高了,虽然只有两层循环,考虑使用集合进行优化,来减少in操作遍历消耗的时间复杂度。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
def combination(n):
if n==1:return ["()"]
last_res=combination(n-1)
l1=set()
for x in last_res:
for index,item in enumerate(x):
if item==")":
temp1="("+x[:index+1]+")"+x[index+1:]
l1.add(temp1)
temp2 = "()" + x
if temp2 not in l1:
l1.add(temp2)
return list(l1)
if n==0:return []
else:return combination(n)
好耶,击败8%到击败40~70%了,时间复杂度确实减少了,但似乎还不是更好。
动态规划
经过上述关于递归的一番猛如虎(bushi)的操作之后,我们其实可以发现,我们在找右括号的过程的本质就是把上一轮的每个结果分成两个部分,一个部分外层再套个括号,另一个部分不动,就完事儿了。在开头就加右括号其实也是分割操作,只不过呢,把一个字符串划分为自己和空字符串了。
你品你细品,每次的两个部分都是前n次的结果,None就是n=0是返回的是空字符串:
将n-1次的结果都存储在一个列表中(如上图),记作dp,dp[i]表示n=i的所有结果,n等于几每个结果中就有几对括号,由于两个部分p和q的括号对数和一定是n-1,所以当p=dp[j]时,q=dp[n-j-1],
动态规划的状态转移方程即为F(n+1)="("+F(i)+"("+F(n-i-1),i=0,1,2,...,n-1
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
def combination(n):
if n == 0:
return []
dp = [[] for _ in range(n + 1)]
dp[0].append("")
for i in range(1, n + 1):
for j in range(i):
left = dp[j]
right = dp[i - 1 - j]
for l in left:
for r in right:
dp[i].append("(" + l + ")" + r)
return dp[n]
if n==0:return []
else:return combination(n)
虽然使用了四层循环,但是由于动态规划是多阶段决策过程,优势一下子体现出来,速度很快。