动态规划

981:求环形子数组最大和
  • 预备知识:
    Kadane算法:用于求子数组最大和
    伪代码:
#Kadane's algorithm
ans = cur = None
for x in A:
    cur = x + max(cur, 0)
    ans = max(ans, cur)
return ans
  • method1:邻接数组:
    环形数组多了一种情况:区间 [0, i],[j,A.length−1]之和,特点是首尾两个元素是定的,所以其实就是求两个半动态子数组的最大和。一般的做法是事先求出值为[j,A.length−1]的数组,然后以i为外层循环,j为内层循环。这样的时间复杂度是O(N^2)。但是动态规划就是把需要遍历找到最佳起始位置的问题转化成不需遍历直接得到在当前情况下的最佳答案(也就是不需要得到多余的最佳答案的位置在哪里)。所以内层循环可以不要,换成预先的动态规划求每个pos右边的最大子数组和。但是要注意sum[i]应该和i+2位置处的最佳答案相加,避免出现子数组即整个数组的情况。

     def maxSubarraySumCircular(A):
          """
          :type A: List[int]
          :rtype: int
          """
          a=len(A)
          ans=cur=float("-inf")    #此处不能是0 因为会全都是负数!!
          for x in A:
              cur=x+max(cur,0)
              ans=max(ans,cur)
      
          rightsum=[0]*a
          rightsum[-1]=A[-1]
          for i in range(a-2,-1,-1):
              rightsum[i]=rightsum[i+1]+A[i]
      
          maxright=[0]*a
          maxright[-1]=A[-1]
          for i in range(a-2,-1,-1):
              maxright[i]=max(rightsum[i],maxright[i+1])
      
          leftsum=0
          for i in range(a-2):
              leftsum+=A[i]
              ans=max(ans,leftsum+maxright[i+2])
      
          return ans
    
  • method2:Kadane变形
    对于 [0, i],[j,A.length−1]之和,因为首尾元素固定,所以需要讨论的只有中间不包含的[i-j]部分。 [0, i]+[j,A.length−1]=sum-[i,j],从这个视角来看,就把不连续的两个数组变成连续的数组(求补思想),只需要求[i,j]的最小值,在Kadane基础上稍作修改即可。注意:同样要避免覆盖全数组的情况,所以要分别去掉首、尾元素分成 A[1:] 和 A[:-1] 两个区间考虑。

    def maxSubarraySumCircular(A):
          """
          :type A: List[int]
          :rtype: int
          """
          a=len(A)
          ans1=cur=float("-inf")    
          for x in A:
              cur=x+max(cur,0)
              ans1=max(ans1,cur)
          # print(ans)
      
          ans2=cur=float("inf")
          for x in A[:a-1]:
              cur=x+min(cur,0)
              ans2=min(ans2,cur)
          ans2=sum(A)-ans2
      
          ans3=cur=float("inf")
          for x in A[1:a]:
              cur=x+min(cur,0)
              ans3=min(ans3,cur)
          ans3=sum(A)-ans3
      
          return max(ans1,ans3,ans2)
    

5:最长回文字符串
  • method1:动态规划
    感觉此处动态规划像是伪递归:dp[i][j]代表str[i:j]是否为回文字符串
    因为回文串是中心扩展的,所以在str[i:j]与str[i+1:j-1]之间存在关系
    需要注意的是此处嵌套循环最外层索引不是作为行标
  def longestPalindrome( s):
       if s is None:
           return ''
       ret = ''
       lens = len(s)
       max = 0
       dp = [[0] * lens for i in range(lens)] #注意此处的定义二维数组的方式!!
       
       "'
       注意此处循环顺序,并不是将最外层索引j作为行:
       填充dp上三角,有两种顺序
       因为要在dp[i:j]之前得知dp[i+1,j-1](左下角),所以应当以垂直方向来填充
       填充顺序不会改变什么,i仍然是字符串起点,j仍然是字符串终点
       "'
       for j in range(lens):
           for i in range(j + 1):
               dp[i][j] = (( s[i] == s[j] ) and (j - i <= 2 or dp[i + 1][j - 1]))  #是以2为边界!不是1!
               if dp[i][j] and j - i + 1 > max:
                   max = j - i + 1
                   ret = s[i:j + 1]
       return ret
  • Manacher算法
    相较于对每个点都做中心扩展,该方法充分利用了回文字符串的对称性,时间复杂度O(N)
    回文字符串的中心可能是一个字符,也可能是两个字符中间,这取决于回文字符串长度奇偶。通过向字符串的空位插入’#’,来避免奇偶长度分类
    维护当前右侧边界最右的回文字符串的中心pos和右侧边界maxright,p[i]表示以s[i]为中心的最长回文字符串的半径,关于p[i]的值需要分类讨论:
    1、i在maxright内(不含):

    • 1)i以pos为对称轴的对称点j,若j-p[j]没有超出pos-p[pos],则根据对称性,p[i]=p[j]
    • 2)超出,则先令p[i]=maxright-i,之后再做中心扩展

    2、i在maxright上:先令p[i]=0,之后再做中心扩展

def longestPalindrome(s):
    s='#'+'#'.join(s)+'#'          #.join()的用法
    l=len(s)
    p=[0]*l
    pos=maxright=center=0

    for i in range(l):
        if i<maxright:   
            p[i]=min(p[2*center-i],maxright-i)
        #开始扩展
        while i-p[i]-1>=0 and i+p[i]+1<l and s[i-p[i]-1]==s[i+p[i]+1]:
            p[i]+=1
        if i+p[i]>maxright:
            maxright=i+p[i]
            center=i
        if p[i]>p[pos]:
            pos=i
            
	#不需要复杂的+-/,直接取加工后的s切片,把#替换掉即可
    print (s[pos-p[pos]:pos+p[pos]+1].replace('#',''))   

32:最长有效括号
  • method1:栈
    python没有内置栈这个结构,需要自己用list定义
    当’)‘时,若栈非空则匹配成功,需要更新目前有效括号长度,所以在弹栈之后要再判断。若栈是空的,则说明所有的’(‘都配对成功了,所以这段有效括号的起点就是start,所以i-start+1;若非空,又不知道剩下的’(‘后面能不能再配对成功,所以暂时将start定为此刻栈顶的索引i-stack.top(),对于这里的做法需要先理解:一段有效括号必定是从’('开始的,中间可能有n对配对成功的’()’,但是最早的一段配对成功的’()‘前面一定是’(’。
class Stack():
    def __init__(self):
        self.stack=[]

    def push(self,value):
        self.stack.append(value)

    def pop(self):
        if(self.stack):
            return self.stack.pop()
        else:
            raise LookupError('stack is empty!')

    def not_empty(self):
        return len(self.stack)

    def top(self):
        return self.stack[-1]

def longestValidParentheses(s):
    """
    :type s: str
    :rtype: int
    """
    stack=Stack()
    maxlen=0
    start=0
    l=len(s)
    for i in range(l):
        if s[i]=='(':
            stack.push(i)    #栈存放索引
        else:
            if stack.not_empty():
                print(stack.pop())
                if stack.not_empty()==0:
                    maxlen=max(maxlen,i-start+1)
                else:
                    maxlen=max(maxlen,i-stack.top())
            else:
                start=i+1
        
    return maxlen
  • method2:动态规划:
    dp[i]表示以i为结尾的最长有效括号长度
    凡是配对成功一次,不管这个’('是贴着的还是隔了很多,都要考虑和前面有效括号拼接,当然前提是存在前面有效括号段
def longestValidParentheses(s):
    l=len(s)
    ans=0
    dp=[0]*l
    for i in range(1,l):
        if s[i]==')':
            #与前一个配对成功,或许能与以i-2为终点的有效括号连起来
            if s[i-1]=='(' :
                if i>=2:
                    dp[i]=dp[i-2]+2
                else:
                    dp[i]=2
            #前一个也是')',则要看前一段有效括号起点之前是否有数据且为'(',否则直接0
            elif i-dp[i-1]>0 and s[i-1-dp[i-1]]=='(':
                #匹配成功之后,可以和前面的有效括号连起来,同样也要先判断是否存在“前面的”
                if i-dp[i-1]-1>0:
                    dp[i]=dp[i-1]+2+dp[i-1-dp[i-1]-1]
                else:
                    dp[i]=dp[i-1]+2
        ans=max(ans,dp[i])
    print (ans)
  • 遍历法:
    首先从左到右边遍历边计数,’(‘应该保持比’)‘数目少,若相等则是一段有效括号(但是不一定就此终结),也需要暂时记录下长度(就是’('数的两倍),若有括号多了就要把计数都置为0
    从右到左也进行一遍,防止漏解,如:’((()’
    本质上配对成功就是数目相等,与顺序无关

回溯、递归、动态规划的关系
  • 回溯:选择、限制、结束条件 ;通过递归实现
  • 从顶向下的递归:记忆化搜索/备忘录;类似于回溯,控制结构相同,区别在于备忘录方式为每个解过的子问题建立备忘录。
  • DP:状态&状态转移方程(从底向上 (每个子问题只解一次))
    怎么转换成DP?递归的暴力解法 -> 带备忘录的递归解法 -> 非递归的动态规划解法。
    例题:leetcode10:正则表达式匹配
    思路的理解
    三种做法
    注意:
    1、dp[i][j]代表text的前i个字符和pattern的j个字符匹配(所以维度要注意)
    2、
dp = [[False] * (len(pattern) + 1) for _ in range(len(text) + 1)]
dp[-1][-1] = True

此处dp的维度比length大1,因为在前面的递归方法中有下面这一步:

if j == len(pattern):
	ans = i == len(text)

dp[-1][-1]初始化为true,而dp[i][-1]初始化为false也是由于在前面的递归中若i=len(text)则first_match为false,最后返回的也是false。所以外层循环i从text.length开始而内层循环从pattern.length-1开始。

类似题目:通配符匹配

有两种自底向上的动态规划:
method1:

 def isMatch(self, s, p):
   """
   :type s: str
   :type p: str
    :rtype: bool
    """
    ls,lp=len(s),len(p)
    dp=[[False]*(lp+1) for _ in range(ls+1)]
    dp[0][0]=True

    for i in range(1,lp+1):
        if p[i-1]=='*':
            dp[0][i]=dp[0][i-1]

    for i in range(1,ls+1):
        for j in range(1,lp+1):
            if s[i-1]==p[j-1] or p[j-1]=='?':
                dp[i][j]=dp[i-1][j-1]
            elif p[j-1]=='*':
                dp[i][j]=dp[i-1][j] or dp[i][j-1]
    return dp[-1][-1]

method2:

def isMatch(self, s, p):	
    """
      :type s: str
      :type p: str
      :rtype: bool
      """
      ls,lp=len(s),len(p)
      dp=[[False]*(lp+1) for _ in range(ls+1)]
      dp[-1][-1]=True

      for i in range(ls,-1,-1):
          for j in range(lp-1,-1,-1):
              first_match = i < ls and p[j] in {s[i], '?'}  #这里的i<ls的意思和递归的思想一样
              if first_match:
                  dp[i][j]=dp[i+1][j+1]
              elif p[j] == '*':
                  dp[i][j] = dp[i][j+1] or i<ls and dp[i+1][j]
      return dp[0][0]

72:编辑距离

不需要思考每种情况会在什么条件下被选择,直接把所有选择得到的答案列出来,选择最小的即可,不需要思考什么情况下会得到该解!!

115:

给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。

  • method1:记忆化搜索
def numDistinct(s, t):
    """
    :type s: str
    :type t: str
    :rtype: int
    """
    demo = dict()
    l=len(s)
    ll=len(t)
    def dfs(i,j):
    	if (i,j) in demo:
    		return demo[(i,j)]  #先搜索记忆 
    	if j==ll+1:             #结束条件
    		demo[(i,j)]=1  
    		return demo[(i,j)]
    	demo[(i,j)]=0
    	for k in range(i,l+j-ll+1):#开始递归 一定要搞清楚i,j的范围and含义!!!
    		if s[k-1]==t[j-1]:
    			demo[(i,j)]+=dfs(k+1,j+1)
    	return demo[(i,j)]
    return dfs(1,1)
  • method2:动态规划
int numDistinct(string s, string t) {
     int l1,l2;
     l1=s.length();
     l2=t.length();
     vector<vector<long> > dp(l1+1, vector<long>(l2+1, 0));
     for(int i=0;i<l1+1;i++)
     {
         dp[i][0]=1;
     }
     for(int j=0;j<l2+1;j++)
     {
     	dp[0][j]=0;
	 }
	 dp[0][0]=1;
     for(int i=1;i<l1+1;i++)
     {
         for(int j=1;j<l2+1;j++)
         {
	        if(s[i-1]==t[j-1])
	            dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
	        else
	         	dp[i][j]=dp[i-1][j];
         }
     }
     return dp[l1][l2];
 }

动态规划和记忆化搜索常常方向不同,记忆化搜索的思维方式是比较自然的,动态规划往往是s的前i个字符与t的前j个字符的关系,其实dp[i][j]=dp[i-1][j-1]+dp[i-1][j]的第一部分就是递归的demo[(i,j)]+=dfs(k+1,j+1),第二部分是“继承”,可以理解为节省了递归中for k in range(i,l+j-ll+1)的循环部分,也就是不用每次都遍历一遍s,去除了重复的循环,变成了累积。

139(WordBreak) & 132(回文字符串II)

132比139多了dp数组
139题解:

bool wordBreak(string s, vector<string>& wordDict) {
	int len;
	string tmp;
	len=s.length();
	vector<bool> f(len+1,false);//f[i]代表前i个字符组成的字符串能够被分割 
	f[0]=true;
	for(int i=1;i<=len;i++)
    {
    	if(!f[i-1])//如果前i-1个不能被分割,则没有讨论从i开始的必要 
    		continue;
    	for(int j=i;j<=len;j++)
    	{
    		if(f[j])//如果前j个字符已经得到答案,则没必要继续判定 
    			continue;
    		tmp=s.substr(i-1,j-i+1);
    		if((find(wordDict.begin(), wordDict.end(),tmp) != wordDict.end()))//包含 
				f[j]=true;
    	}
    }
     return f[len]; 
}

132题解:

int minCut(string s) {
   int len;
   len=s.length();
   vector<vector<int> >dp(len,vector<int>(len,0));
   vector<int> f(len,0);
   for(int i=0;i<len;i++)
   	dp[i][i]=1;
   for(int i=0;i<len;i++)
   	f[i]=i; 
   for(int j=0;j<len;j++)
   {
   	for(int i=0;i<=j;i++)
   	{
   		if(s[i]==s[j]&&j-i>1&&dp[i+1][j-1])
   			dp[i][j]=1;
   		else if(s[i]==s[j]&&j-i==1)
   			dp[i][j]=1;
   		if(dp[i][j]&&i)
   			f[j]=min(f[j],f[i-1]+1);
   		else if(dp[i][j]&&!i)
   			f[j]=0;//不需要切割 
   	}
   }
   return f[len-1];
}

139的f[i]代表:前i个字符组成的字符串能否被分割。139不需要存储dp[i][j]来表示i-j能否被分割,因为中间过程的存储是为了状态转移方程为了得到最佳的f[j],而139中并没有当前最佳情况,因为f[j]的值只有yes or no;而132需要dp,则是因为关于“最少”这样一个最佳情况的讨论,f[i]代表划分最少次数,需要权衡前面到底如何划分能够得到最佳答案。
132是最近做到的新题型,有两个动态数组(不过f其实并不是严格的动态,和139中的f作用一样,是个备忘录)。


140:WordBreakII

140比WordBreak多了返回所有被分割情况,所以相较139,在dp部分要记录下dp[i][j]=true;,而且if(f[j]) continue;这种已经确定前j个字符可被分割的要注释掉。
在dp之后,要开始dfs分割句子。如果不用unordered_map会超时,所以要用unordered_map<int, vector<string> > m;记录下后面i个字符组成的字符串被分割的所有情况(为什么是后面i个不是前面i个?因为dfs是递归/回溯,此处引入map之后就是记忆化回溯
像这样要写出所有情况的题目:dp(进行判断)+记忆dfs(返回结果)
记忆化dfs模板:
1、如果有,直接返回
2、判断是否到了结尾,特殊处理
3、从当前index下往后进行for循环,递归
4、返回当前情况的结果集合vector

附上dfs代码:

vector<string> dfs(vector<vector<bool> > dp,string s,int sta,unordered_map<int, vector<string> > &m)
{
	if (m.count(sta)) {
        return m[sta];
    }
    vector<string> ret;
    if (sta == s.size()+1) {
    	ret.push_back("");
    }
	else
    {
		for (int i = sta; i <= s.size(); i++) 
		{
			if (dp[sta][i]) 
			{
	            vector<string> tmp = dfs(dp,s,i+1, m);
	            for (int j=0;j<tmp.size();j++)
                	ret.push_back(s.substr(sta-1, i - sta+1) + (tmp[j].empty()?"":" ")+ tmp[j]);
        	}
    	}
	}
	m[sta] = ret;
	return ret;
}

174地下城游戏

详细题解——从递归->记忆化搜索->动态规划的思路

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值