LeetCode刷题总结(四)10-13

(1)LeetCode10:正则表达式匹配

这是一个有难度的动态规划问题,dp问题的一般分析思路就确定状态(状态表示),并进行状态计算,以集合的角度来看待问题
看了上面这段文字,你也许还很懵,所以下来来更详细地阐述一下:
由于本题有两个字符串,所以状态表示应该是二维的f(i, j):
状态表示
状态集合是:所有s[1-i] 和 p[1-j]的匹配方案;
属性:bool,是否存在一个合法方案。
f(i, j)代表s的前i个字符和p的前j个字符是否匹配,在代码体现上就是f[i][j]。
状态计算
①. p[j] != ’ * ',
集合中没有星,(直接匹配 s[i] 和 p[j] || p[j] 中有没有点) & f(i - 1, j - 1),为什么?因为只要s的前i - 1个元素和 p的前 j -1个元素匹配了,那么就可以确定f(i, j)一定匹配成功
dp问题一般有两种书写思路,一种是循环来写,另一种是递归来写,一般而言,递归较为容易理解,但执行时间会长一些
②. p[i] != ’ * ‘,
(1)如果’ * ‘取0个字符,那么就是s的前i个字符和p的前 j-2 个字符匹配,因为’ * ‘和它前面的那个字符都没用了:f(i, j - 2)
(2)同样的,如果’ * ‘表示了一个字符,那么就是f(i - 1, j - 2),s的前 i-1 个字符和p的前 j-2 个字符匹配,因为 现在的s和p的数量对比是1:2,同时匹配了没有星的后,现在要有’ * ‘也匹配,即s[i] == p[j],综合起来,条件为:f(i - 1, j - 2) && s[i] == p[j]
(3)如果’*'表示两个字符,就是f(i - 2, j - 2) && s[i] == p[j] && s[i - 1] == p[j],
往下就以此类推 . . .
有了上面的推理,所以现在的状态表示就是:(s[i] == p[j] 简写为 si)
f(i, j) == f(i, j - 2) | f(i - 1, j - 2) & si | f(i - 2, j - 2) & si & si-1 … ①
但这样要这样列举所有的状态,且每个状态还要枚举s[i] == p[j],s[i - 1] == p[j] …,时间复杂度非常高,所以可以考虑做一个优化
现令 i = i - 1 ,由①得:就是把第i个元素去掉了:
f(i - 1, j) == f(i - 1, j - 2) | f(i - 2, j - 2) & si-1 | f(i - 3, j - 2) & si-1 & si-2 … ②
现在我们可以惊奇地发现,可以将f(i - 1, j)替换到f(i, j)中去(这是dp问题计算中经典的等价替换优化),则:
f(i, j) == f(i, j - 2) | f(i - 1, j) & s[i] == p[j]
,时间复杂度大大降低。

看完上面一大段阐述,还是very confused,那就看一看代码是怎么写的吧:
下面来一个循环遍历的方式(时间复杂度为O(nm)):(代码示例)

class Solution {
public:
    bool isMatch(string s, string p) {
       int n = s.size(), m = p.size(); // 记录两个子串的长度
       s = ' ' + s, p = ' ' + p; // 两个数组都从1开始,头部第一个元素都是空格,这是为了能让f[0][0]初始化确定为true,为后面的判断做准备
        vector<vector<bool>> f(n + 1, vector<bool>(m + 1)); // 状态转移方程,这里用vector创建了一个类似二维数组的结构,因为下标从1开始,所以从0-n有n+1个数,m也是同样的;其中的内容都是一个个的布尔数组
        // 如何理解上面的式子:
        f[0][0] = true; // 默认两个串都没有任何字符的情况下,一定匹配,所以值为true

        // 第一个串从0开始,为什么?因为s串中可能什么都没有,而p串中可以有'*'去和它匹配,这也是正确的
        for(int i = 0; i <= n; i ++)  // 由于第一个元素为空,所以循环时要加等号,包括n
            for(int j = 1; j <= m; j ++) { // 但j一定要从1开始,因为j的第1个字符为空,一定不匹配(一个空串和非空串s匹配没有意义)
                // 重要思想: 把'a*'看作一个整体, a后面有星就不能单独用! 单独用也没有意义,所以就跳过,但前提是a后面还有字符;下面也就对应着特判一下
                if(j + 1 <= m && p[j + 1] == '*')  continue;
                // 正式开始: p[j]不等于'*'的情况,非常简单,不用考虑*的不确定个数的情况
                if(i && p[j] != '*') { // 这里i一定要从>0开始计算,否则没有意义
                    f[i][j] = f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.' );// 注意这个二维数组是布尔值的
                } else if(p[j] == '*') { // 下面也用到了i - 1,所以要排除i==0的这种情况
                    // 下面就可以用到刚刚推导的等价方程代入
                    f[i][j] = f[i][j - 2] || i && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.');  // 这里还要注意一个,在判断s与p对应时,由于p[j]已经是*了,所以要用上一个元素来判断
                // 为什么要'|| i'放到正式语句中,而不是if语句:因为可能当i = 0时,一定正确,由于短路不用判断i - 1的越界情况了
                }
             }

            return f[n][m]; // 经过上面的过程最后的布尔值为多少?返回
    }
};

// 注意在判断true/ false时,一定要用 ’ == ',不要错写为 ’ = ',否则会很坑!

【小结】
1.本题的难点在’*'匹配的字母数量是不确定的,所以一般在处理的时候会进行暴力遍历,但这种方式非常耗时间,对于这类问题,我们可以去发现其到底有几个作用对象在题目中,确定它的维度,总结它的各种状态,并对这些状态进行状态计算,计算为相应的函数形式,再进行循环处理(循环层数可以减少)
2. 本题的难点还在于你是否了解 vector<vector> f(n + 1, vector(m + 1)) 的含义呢?

(2)LeetCode11:盛最多水的容器

暴力双循环:

class Solution {
public:
    int maxArea(vector<int>& height) {
        int max = 0;
        for(int i = 0; i < height.size() - 1; i ++ ) 
            for(int j = 1; j < height.size(); j ++) {   
                int x = (j - i) * min(height[j], height[i]);
                if (x < 0) x *= -1;
                if(max < x) max = x;
            }
        return max;
    }
};

However,上面的这种做法超时了!!!怎么办呢?其实大家应该都想到了,因为这里有两个同时在变的位置偏移量,所以可以采用双指针算法
但怎么具体实施呢?
这里有个很巧的方式,就是两个指针分别从头尾开始向中间移动,直到两指针相遇就停止循环,同时,不是两个人都同时移动,而是比较两个指针height的大小,小的一边才移动,移动之后将新的容器容积与原最大值比较。
但这里有个疑问:这种算法一定能找到最大值吗,因为这样可能会漏掉一些与暴力遍历相比时的情况?
证明:
不妨设左边指针i已经到达了最优解的位置,而右边还未到(一定会有一个指针先到达最优解的位置),由于这时左边应该停止向后运动了,所以右边应该运动,根据本题算法理论:右边的height此时应小于左边的height。
现反证法,假设我们的理论不成立,那么右边应比左边的height更大,但此时左边已经到最优解位置了,不会再移动了,容器的高度还是由其决定,故右边的height此时应小于左边的height,其还应向左移动去找大于左边的height。(以右边为参考的最优解位置也是同样的)
得到的结论:只要左边先到达最优解的位置,右边的高度一定是严格小于左边的。
由上面的分析过程,也可以看出,在循环时,一定是要遵循一定一动的规则,否则两者皆动,则会漏判情况。

代码示例:

class Solution {
public:
    int maxArea(vector<int>& height) {
        int res = 0;
        for(int i = 0, j = height.size() - 1; i <= j; ) { // 取不取等?可以不取,因为相等时体积一定为0,不会影响结果
            // 每次更新了高度之后,都要更新一下最大值
            res = max(res, (j - i) * min(height[i], height[j]));  // 将新高度下的容积与原容积进行比较,选出较大的
           if(height[i] >= height[j]) j --; // 谁的高度小一点,谁就要往后/前运动;这一思路的理论支撑是什么?
           else i ++; // 这里不能出现一次循环两次动的情况(i,j都动),即不能使用两个if做判断,else/elseif是可以的;必须使用一定一动,否则会漏掉一些情况 
        }
        return res;
    }
};

图解证明(更好理解):棕色为当前容积,灰色为最优解的体积
在这里插入图片描述
FurtherMore,这种方式的时间复杂度为O(n),空间复杂度为O(1)。
【小结】双指针向内收敛的时候要确定两个问题:动几个?谁动?
本题中是一次循环动一个;
高度小的才懂。

(3)LeetCode12:整数转罗马数字

规律图示:这都是剥出了各个位置的单独数位的,它们组合起来能构成各种数字在这里插入图片描述

从上面圈的几个数字可以看出来它们都是不符合罗马数字构成的一般规律的,但又与一般的数字构成相互联系(它们是其他数字的基数字),所以可以把它们作为if条件的分界点。
一个问题它们怎么相互联系,重点:这里的联系是本题的一个普遍规律,只要它在分段点的范围内,它就可以减去左边的分段点的值,那个被减去的值就是罗马数字的一部分,然后就一直减,减到原数字为0为止。

代码示例:

class Solution {
public:
    string intToRoman(int num) {
        // 这种类似编码的题:(将一种形式转化为同等的另一种形式:找规律,拆开分解),没有规律的项就单独剥出来
       int values[] = {
           1000, 
           900, 500, 400, 100, 
           90, 50, 40, 10, 
           9, 5, 4, 1
       };
       string reps[] = {
            "M", 
            "CM", "D", "CD", "C",
            "XC", "L", "XL", "X",
            "IX", "V", "IV", "I"
       };
       // 注:上面可以用哈希map的方式键值对存储
       string res;
       // 如何通过这些边界来获取相应数字的值
       for(int i = 0; i < sizeof(values)/sizeof(values[0]); i ++) { // 这个方法很强!!!但思想还是逐减拆分编码!!!
           // 把每个边界都遍历一遍,values.size()为边界点的总个数
           // 怎么确定这个数字的每个阶段都在一个边界内呢?
           // 在for的内部放一个循环
           while(num >= values[i]) {
               num -= values[i]; // 处理原本数字,将阿拉伯数字的千百十个位每一位扣出来,转化为对应的字符串
               res +=  reps[i];// 通过字符串拼接创建罗马数字
           }
       }
       return res;
     }
};

【小结】本题分段找分段点,找到 ‘ -= ’ 的普遍规律规律非常important,一般我们是想给所有数字都编码,然后拆分int整数一一对应来解决,分列枚举各种情况。但这里很巧妙:找到分段点一层一层迭代相减
问题:为什么要把那几个点单独拿出来?

  1. 这几个点有一类是基础点,是独特的,如I,V等
  2. 还有一类是不符合数字的一般规则的:如IV等
    问题:这些点怎么用?
    这些点可以用来构成其他元素

(4)LeetCode13:罗马数字转整数

由于本题是上一题的反转,所以我们把它们放到一起来分析,与上一题不同,上一题转罗马数字难在有很多种特殊的数字情况要考虑,而本题则只有两类特殊情况,其余的只需按照罗马数字的规则逐项相加即可。
两类特殊情况是4和9系列的,它们是小的在前面,而大的在后面:4,40,400,9,90,900。
处理该特殊情况的方法是:由于它的特殊在于是要前面字母对应的数要小于后面字母对应的数,所以应该是要减去前面那个数,故每次字符串遍历时先判断一下是否前面的字母对应的数小于后面字母对应的数

代码示例:

class Solution {
public:
    int romanToInt(string s) {
       unordered_map<char, int> hash;  // 如何将字母变成数字呢?开一个hash表即可,这在很多场景下都会使用
       // 这里要用char,因为s[i]的数据类型为char
        hash = {{'I', 1}, {'V', 5}, {'X', 10}, 
           {'L', 50}, {'C', 100}, {'D', 500}, 
           {'M', 1000}
        };
       int res = 0;
       for(int i = 0; i < s.size(); i ++) {
           if((i + 1 < s.size()) && hash[s[i]] < hash[s[i + 1]]) { 
                // 如果该数小于它后面一个数,那么应该是要减去它的
                res -= hash[s[i]];
           }
           else res += hash[s[i]];
       }
        return res;
    }
};

为什么int转罗马很难,而罗马转int很容易?因为int是有个十百千进制数的概念的,而罗马数字就是所有数字对应起来直接相加
【小结】

  1. hash表map可以用于编码表对应,把一个数转化为另一个数;
  2. 特殊情况先于普通情况判定
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值