Leetcode刷题总结--动态规划(一)

Leetcode刷题总结

动态规划问题(字符串)

动态规划是一种思想,本质上是用来解决递归中的低效性。在递归解决问题的过程中,会将问题分解为许多小问题,但是很多小问题会被多次重复计算,斐波那契数列的递归过程 就是一个很好的说明。

因此,如果在递归过程中,将子问题处理结果记录下来,对递归树进行剪枝,从而优化时间,这一种方法又叫做记忆化搜索,也是动态规划的一种实现方式。

然后,如果我们直到原问题和子问题之间的关系,那么我们可以通过先求解规模小的子问题,然后通过转移方程反推原问题,这就是动态规划的思想。

个人认为,动态规划不是一蹴而就的过程,没有一拿到题目就知道状态转移方程,我们需要从基本做起,写递归版本,从递归版本中理解问题之间的关系。涉及字符串的问题,我们就可以往动态规划的方面去思考。因为原字符串和子字符串就是原问题和子问题的关系,我们可以比较容易的写出递归版本的代码,根据递归代码给出的思路,分析得到原问题和子问题的状态转移方程,这样就可以写出大部分动态规划问题的代码。


Leetcode44 通配符匹配

题目描述:

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '?''*' 的正则表达式匹配。

'?'匹配任意单个字符,'*'匹配任意长度字符串,也可以是空字符串

示例

输入:
s = “aab”
p = “c * a * b”
输出: true
解释: 因为 ‘*’ 表示任意,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

算法思路:

看到题目,没有想到啥算法,因此先进行尝试。从两个字符串的头部开始:

  1. 如果第一个字符是匹配的,那么可以同时判sp的下一个字符是否匹配。如果第一个字符不匹配,那么就分情况分析。

  2. 如果p串的第一个字符是?,那么无论s串第一个元素是什么,都可以匹配。

  3. 如果p串的第一个字符是*,那么这个字符可以匹配s串之后任意长度的串,所以p串不动,s串向后匹配一个字符;或者把*当做空字符串,直接匹配p的后一个字符串。

  4. 如果p串匹配到结尾,判断s串是否到达结尾,返回true或者false

因此,可以写出递归版本的代码,如下所示

string s,p; 
bool isMatch(int i,int j) //i表示s串当前匹配位置 j表示p串当前匹配位置
{
    if(j == p.size()) return i==s.size();
    bool first_match = (s[i]==p[j] || p[j]=='?');
    if(p[j] == '*')
    {
        // 当前位置为* 默认匹配掉一个位置 或者 默认*代表空字符串
        return isMatch(i+1,j) || isMatch(i,j+1);
    }
    else
    {
        // 后面是否匹配 且 当前位置是否匹配
        return first_match && isMatch(i+1,j+1);
    }
}

通过递归过程,我们就可以分析出原问题和子问题的结构

原问题:字符串s和字符串p是否匹配。 子问题:字符串s和字符串p除了第一个字符串之外的子串是否匹配。

def:

d p [ i ] [ j ] dp[i][j] dp[i][j]s串从 i i i 开始的子串 与 p串从 j j j 开始的子串是否匹配

目标状态: d p [ 0 ] [ 0 ] dp[0][0] dp[0][0],表示字符串s和字符串p是否匹配。是需要求解的目标状态

初始状态 d p [ s . s i z e ( ) ] [ p , s i z e ( ) ] dp[s.size()][p,size()] dp[s.size()][p,size()],表示字符串""和字符串""是否匹配。是求解的,肯定为true

状态转移方程:

d p [ i ] [ j ] = d p [ i ] [ j + 1 ] o r d p [ i + 1 ] [ j ] ( i f p [ i ] = = ′ ∗ ′ ) dp[i][j] = dp[i][j+1]\quad or\quad dp[i+1][j] \quad(if\quad p[i]=='*') dp[i][j]=dp[i][j+1]ordp[i+1][j](ifp[i]==)

d p [ i ] [ j ] = f i r s t m a t c h a n d d p [ i + 1 ] [ j + 1 ] ( o t h e r w i s e ) dp[i][j] = firstmatch\quad and\quad dp[i+1][j+1]\quad(otherwise) dp[i][j]=firstmatchanddp[i+1][j+1](otherwise)

代码

bool isMatch(string s, string p) 
{
    if(p.size()==0) return s.size()==0;
    int m = s.size();
    int n = p.size();
    vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
    dp[m][n] = true;
    for(int j=n-1;j>=0;j--)
    {
         if(p[j]!='*') break;
         dp[m][j] = (p[j]=='*');
    }
        
    for(int i=m-1;i>=0;i--)
    {
        for(int j=n-1;j>=0;j--)
        {
            bool fm = (s[i]==p[j] || p[j]=='?');
            if(p[j]=='*')
            {
                dp[i][j] = dp[i][j+1] || dp[i+1][j];
            }
            else
            {
                dp[i][j] = fm && dp[i+1][j+1];
            }
        }
    }
    return dp[0][0];
}

Leetcode10 正则表达式匹配

题目描述:

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.''*' 的正则表达式匹配。

'.'匹配任意单个字符,'*'匹配零个或多个前面的那一个元素

示例:

输入:
s = “aab”
p = “c * a * b”
输出: true
解释: 因为 ‘*’ 表示任意,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

算法思路:

本题和上面的题目几乎是同一个题,唯一的区别就在于'*'的定义不同。这里'*'表示重复前一个字符多次(包含0次)

沿用上一个题的思路,根据'*'的定义的特殊性,因此对每个符号,不仅要判断当前位置,还要判断下一位置是否为'*'

  1. 如果第一个字符是匹配的,那么可以同时判sp的下一个字符是否匹配。如果第一个字符不匹配,那么就分情况分析。
  2. 如果p串的第一个字符是.,那么无论s串第一个元素是什么,都可以匹配。
  3. 如果p串的下一个字符是*,那么可以当前字符被重复多次,所以p串不动,s串向后匹配一个字符;或者把*当做匹配0次处理,直接匹配p的后两个字符串。

递归版本代码如下:

bool process(string s,int i,string p,int j)
{
    // 字符串s从i位置到结尾 能否与 p从j位置到结尾向匹配
    if(j == p.size()) return i==s.size();
    bool first_match = false;
    if(s[i] == p[j] || p[j]=='.') first_match = true;
    if(p[j+1] == '*') // 下一位是*号
    {
       	// 可以选择不要第一位的匹配结果 或者  保留第一位匹配结果
        return process(s,i,p,j+2) || (first_match && process(s,i+1,p,j));
    }
    else // 下一位不是*号,向后试探
    {
        return (first_match && process(s,i+1,p,j+1));
    }
}

然后在根据递归过程改写动态规划,相关定义与上一题相同。

代码:

bool isMatch(string s, string p) 
{
    int m = s.size();
    int n = p.size();
    vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
    dp[m][n] = true;
    for(int i=m;i>=0;i--)
    {
        for(int j=n-1;j>=0;j--)
        {
            bool first_match = i<m? (s[i] == p[j] || p[j]=='.'):false;
            if(j+1<n && p[j+1] == '*')
            {
                dp[i][j] = dp[i][j+2] || (first_match && dp[i+1][j]);
            }
            else
                dp[i][j] = (first_match && dp[i+1][j+1]);
     	}
    }
   	return dp[0][0];
}

Leetcode139 单词拆分

题目描述:

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

示例:

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

算法思路:

首先,仍然采用尝试的方法,对于每一个位置,都尝试能不能分割,如果可以,就继续往下分割。

  1. 当前分割方式,找到了字典中的单词,在对应位置截取字符串,然后递归子串
  2. 如果当前串没有分割点,且当前串长度不为空,返回false
  3. 如果当前串为空,返回true

递归代码如下:

bool process(string s)
{
	if(s.size() == 0) return true;
    bool find = false;
    for(int i=0;i<s.size();i++)
    {
        if(m.count(s.substr(0,i+1)) == 1)
        {
            find = true;
            return process(s.substr(i+1)); //从i位置开始截取新的字符串
        }
    }
    return find;
}

根据递归代码,我们可以找到动态规划的思路:

d p [ i ] dp[i] dp[i]表示从i开始的子串能否被完全拆分,如果可以i位置被记录为一个拆分点。每一个拆分点就是对应有解的子问题。对下一个位置遍历的时候,需要考虑所有可能的拆分点,判断从当前位置到拆分点是否存在于词典中。状态转移方程如下 :

d p [ j ] = d p [ i ] a n d i s s e a r c h ( s , s u b s t r ( j , i − j ) ) ; f o r i i n 所 有 拆 分 点 dp[j]\quad=\quad dp[i]\quad and\quad issearch(s,substr(j,i-j));\quad for\quad i\quad in\quad {所有拆分点} dp[j]=dp[i]andissearch(s,substr(j,ij));foriin

初始状态: d p [ s . s i z e ( ) ] = t r u e dp[s.size()] = true dp[s.size()]=true,表示空字符串不用分割

目标状态: d p [ 0 ] dp[0] dp[0]

代码:

unordered_map<string,int> m;
bool wordBreak(string s, vector<string>& wordDict) 
{
    for(auto s:wordDict)
    {
        m[s]++;
    }
    vector<bool> dp(s.size()+1,false);
    unordered_map<int,int> split_point; // 使用map加速查找过程
    dp[s.size()] = true;
    split_point[s.size()] = 1;
    int end = s.size();
    for(int j=s.size()-1;j>=0;j--)
    {
        for(auto p:split_point) // 遍历所有分割点
        {
            if(m.count(s.substr(j,p.first-j)) == 1)
            {
                dp[j] = dp[j] || dp[p.first];
                split_point[j] = 1;
            }
        }  
    }
    return dp[0];
}

小总结:

感觉字符串的dp问题,dp状态都比较好定义,无非就是从i位置开始的子串 或者是 i位置之前的子串 是否满足题目要求。另外,通过递归去找潜在的逻辑关系不失为一种好方法,可以让自己更加深刻的理解题意。不过动态规划的内容还是很多,很多地方自己也没有很理解,上面三个题只是我做的一些题目自认为比较典型而且好理解的题目。

革命尚未成功,同志仍需努力!

(未完待续!)

你可能感兴趣的:(Leetcode刷题总结)