暂时pass的题目的学习笔记(按类型分类 ):动态规划、回溯、递归

动态规划类

学习笔记来自公众号labuladong

  • 动态规划的一般形式就是求最值——其核心问题是穷举
  • 但动态规划的穷举有些特别,因为这类问题存在重叠子问题 如果暴力穷举的话效率会极其低下,所以需要**「备忘录」或者「DP table」**来优化穷举过程,避免不必要的计算
  • 动态规划问题一定具备最优子结构,才能通过子问题的最值得到原问题的最值,要符合“最优子结构”,子问题间必须互相独立。
  • 只有正确列出状态转移方程才能正确地穷举
  • 明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」-> 明确 base case
  1. 重叠子问题可以理解为重复计算的部分,解决重叠子问题可以通过备忘录和dptable:

①备忘录:
“明确了问题,其实就已经把问题解决了一半。即然耗时的原因是重复计算,那么我们可以造一个「备忘录」”
每次算出某个子问题答案后别急着返回,先记到备忘录中再返回;每次遇到一个子问题先去备忘录查一查,如果之前已经解决过了就直接拿来用,不要重复去计算。
一般使用数组或者哈希表充当备忘录
但带备忘录的递归解法叫做自顶向上,即从上向下延伸,一个规模较大的问题向下逐渐分解规模,直到触底,再逐层返回答案呢
自底向上,是直接从最底下,最简单,问题规模最小开始往上推,直到推到想要的答案,一般动态规划都是自底向上,“这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算”

② dp数组的迭代解法:在这张表上完成自底向上的推算

正则表达式匹配

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

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

所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
在这里插入图片描述

根据通配符进行大致分类

if(s[i] == p[j] || p[j] == '.')
{
     if(j<p.size()-1 && p[j+1] == '*')
     {
         //有* 可匹配0次或者多次
     }else{
         //无* 都+1再进行比较
         i++; j++;
     }

}else
{
    //如果有*
    if(j<p.size()-1 && p[j+1] == '*')
    {
        //匹配0次
    }else{
        //无* 肯定不行了
        return false;
    }
}

接下来就是在有*的情况下,把所有的可能选择都穷举一遍得到结果
状态和选择,状态就是i和j的位置,选择就是p[j]选择匹配几个字符

dp(s, i, p, j)=true,表示s[i…]可以匹配p[j…], 根据这个定义我们要的答案就是i= 0, j = 0时的答案,

if(s[i] == p[j] || p[j] == '.')
{
     if(j<p.size()-1 && p[j+1] == '*')
     {
         //有* 可匹配0次或者多次
         return dp(s, i ,p, j+2) || dp(s, i+1, j, p);
     }else{
         //无* 都+1再进行比较
         i++; j++;
         return dp(s, i+1, p, j+1);
     }

}else
{
    //如果有*
    if(j<p.size()-1 && p[j+1] == '*')
    {
        //匹配0次
        return dp(s, i, p, j+2);
    }else{
        //无* 肯定不行了
        return false;
    }
}

再考虑dp的base case 当j走到最后时,i有没有到最后; 到i走到最后时,p剩下的能不能匹配空字符串
注意当能匹配空字符串时,剩下的字符串一定会是偶数个? 而且一定是xyz*这样的形势,即第二个得是*
得到第一题动态规划的题解:

class Solution {
public:
      bool dp(string s,int i, string p, int j) {
      map<string, bool> memo;

        //1先写base case
        int m = s.size(), n = p.size();
        if(j == n)
        {
            return i == m;
        }
        if(i == m)
        {
            //是否能匹配空字符串
            //一定得是偶数个 且是以x*y*z*的形势
            if((n-j)%2 == 1)
            {
                return false;
            }
            for(; j+1<n; j+=2)
            {
                if(p[j+1] != '*')
                {
                    return false;
                }
            }
            return true;
        }
        //2 写状态转移方程 
        //用备忘录
        string key = to_string(i)+"_"+to_string(j);
        if(memo.count(key)) return memo[key];
        bool res = false;

        if(s[i] == p[j] || p[j] == '.')
        {
            if(j<n-1 && p[j+1] == '*')
            {
                res = dp(s, i, p, j+2)|| dp(s, i+1, p,j);

            }else{
                res = dp(s, i+1, p, j+1);
            }
            
        }else{
            if(j<n-1 && p[j+1] == '*')
            {
                res = dp(s, i, p, j+2);
            }else{
                res = false;
            }
        }

        memo[key] = res;
        return res;

    }

    bool isMatch(string s, string p) {
        int i = 0, j=0;
        return dp(s, i, p, j);
        
    }
};

不过按此方法超出内存限制,打算不再耽搁,往下继续,遇到新的题目再复习再理解

回溯

首先,什么时候用到回溯算法?
回溯算法通常在需要解决组合优化问题、排列组合问题、图论问题、搜索问题等情况下被使用
具体来说就是当需要在一个问题的解空间中搜索所有可能的解,并且需要找到满足特定条件的解时,就可以考虑。

解决一个回溯问题,就是一个决策树的遍历过程(各种搜索问题都是树的遍历问题)
考虑3个问题:
1 路径:已经做出的选择
2 选择列表:当前可以做的选择
3 结束条件:到达决策树底层,无法再做选择的条件

result = []
def backtrack(路径、选择列表)
	if 满足结束条件
		result.add(路径)
		return
	for 选择 in 选择列表
		做选择
		backtrack(路径,选择列表)
		撤销选择

核心就是for循环里的递归,在递归调用之前做选择,在递归调用之后撤销选择

for 选择 in 选择列表
	#做选择
	将该选择从选择列表移除
	路径.add(选择)
	backtrack(路径,选择列表)
	#撤销选择
	路径.remove(选择)
	再将选择加入选择列表

全排列:

class Solution {
public:
    void backtrack(vector<vector<int>> &res,vector<int>& nums,  vector<int> tarck)
    {
        //结束条件
        if(tarck.size() == nums.size())
        {
            res.push_back(tarck);//一个进去
        }
        //回溯的套路 遍历选择列表
        for(int i = 0; i<nums.size(); i++)
        {
            if(std::find(tarck.begin(), tarck.end(), nums[i]) != tarck.end()) //已经存在 跳过
                continue;
            //加入
            tarck.push_back(nums[i]);
            //进入下一层循环树
            backtrack(res, nums, tarck);
            tarck.pop_back();
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        vector<int> tarck;//记录的路径
        vector<vector<int>> res;//保存的路径
        backtrack(res, nums, tarck);//nums选择的路径
        return res;
    }

};

递归

但凡用到递归的问题,最好都画出递归树,这对分析算法的复杂度,需按照算法是否低效以及低效的原因都有帮助
递归的算法时间复杂度计算:子问题个数乘以解决一个子问题需要的时间

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值