leetcode刷题详解十二

回文子串

首先回文子串问题涉及到的都是单个字符串,所以如果是单个字符串用动态规划的基本都是二维的,i-j

其次,回文字符串,都是从后往前遍历的,这个要记住。因为dp的状态转移方程特性决定的

647. 回文子串

还是用动态规划吧,中心扩散没太看懂

参考链接

说一下自己的想法,因为就单个字符串,因此我之前说过单个字符串的话是要有范围的。

为什么外循环会从len-1开始,对于字符串“cabac来说”,如果s[0]的c=s[4]的c,那么只需要看“aba”即可,如果你的for循环从0开始的换,你都从0过来了还看什么aba,只有从后往前,才能看aba吧

int countSubstrings(string s) {
    int len = s.size();
    vector> dp(len, vector(len, false));
    int count = 0;
    for(int i = len - 1; i >= 0; i--){
        for(int j = i; j < len; j++){
            if(s[i] == s[j] ){
                if(j-i <= 1){
                    count++;
                    dp[i][j] = true;
                }
                else if(dp[i + 1][j - 1]){
                    count++;
                    dp[i][j] = true;
                }
            }
        }
    }
    return count;
}
516. 最长回文子序列

没思路看这个

思路:这道题一定要和647放在一起看,这两道题是一模一样的类型。

这道题的难点主要在于dp的状态转移过程,来分析一下

如果s[i]与s[j]相同,那么 d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j] = dp[i + 1][j - 1] + 2 dp[i][j]=dp[i+1][j1]+2,因为回文串的个数嘛,+2

如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

加入s[j]的回文子序列长度为 d p [ i + 1 ] [ j ] dp[i + 1][j] dp[i+1][j]

加入s[i]的回文子序列长度为 d p [ i ] [ j − 1 ] dp[i][j - 1] dp[i][j1]

那么dp[i][j]一定是取最大的,即: d p [ i ] [ j ] = m a x ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) dp[i][j]=max(dp[i+1][j],dp[i][j1])

 int longestPalindromeSubseq(string s) {
     int len = s.size();
     vector> dp(len+1, vector(len+1, 0));
     for(int i = 1; i < len + 1; i++ ){
         dp[i][i] = 1;
     }
     for(int i = len ; i >= 1; i--){
         //这里j从i+1开始,和上一题不一样,因为不考虑本身
         for(int j = i + 1; j < len+1; j++){
             if(s[i-1] == s[j-1]){
                 dp[i][j] = dp[i+1][j-1] + 2;
             }else{
                 //不构成回文串了,不能+1了
                 dp[i][j] = max(dp[i][j-1], dp[i+1][j]);
             }
         }
     }
     return dp[1][len];
 }
1312. 让字符串成为回文串的最少插入次数

其他应用题

另类的DP套路!

887. 鸡蛋掉落

参考链接

最重要的是下面的这张图:

leetcode刷题详解十二_第1张图片
  • 注意事项,一定要注意两层是 d p [ i ] [ j ] = a dp[i][j]=a dp[i][j]=a,三层的话肯定是最里面一层for有个变量temp,然后跳开里面这层才是dp赋值
int superEggDrop(int k, int n) {
        //dp数组的含义是到第n层有k个鸡蛋可以进行的最小的操作次数
        vector> dp(n+1, vector(k+1, 0));
        for(int i = 1; i < k + 1; i++){
            dp[1][i] = 1;
        }
        //一个鸡蛋扔肯定每楼扔一次
        for(int i = 1; i < n + 1; i++){
            dp[i][1] = i;
        }
        for(int i = 2; i < n + 1; i++){
            for(int j = 2;j < k + 1; j++){
                //楼层区间
                int temp = INT_MAX;
                for(int m = 1; m <= i; m++){
                    //最坏就是最大
                    temp = min(temp, max(dp[m-1][j-1], dp[i-m][j])+1);
                }
                dp[i][j] = temp;
            }
        }
        return dp[n][k];
    }

这样子写会超时!不过重要的是思路

  • 第二种思路!!!!

    其实第二种思路最主要的就是对于dp数组的设计了,dp设计好了一道题也就自然而然的解开了

    我们把dp设计成 d p [ i ] [ j ] dp[i][j] dp[i][j]表示有i个鸡蛋,走了m步能到的层数,因此 d p [ i ] [ j ] dp[i][j] dp[i][j]就是层数,只要层数大于等于n,就可以返回j

    状态转移方程这样理解:

    当我们扔鸡蛋的时候,都是两种情况,碎或者不碎,不管碎没碎,都用掉了一步(+1),

    无论你在哪层楼扔鸡蛋,鸡蛋只可能摔碎或者没摔碎,碎了的话就测楼下,没碎的话就测楼上

    无论你上楼还是下楼,总的楼层数 = 楼上的楼层数 + 楼下的楼层数 + 1(当前这层楼)

    因此 d p [ i ] [ j ] dp[i][j] dp[i][j]的i是次数,不管碎没碎总是i-1!!

    至于为啥初始化dp数组的时候要用k+1和n+1是因为最多次数不可能超过楼层数吧!!!

    int superEggDrop(int k, int n) {
            vector> dp(n+1, vector(k+1, 0));
            for(int i = 1; i < n + 1; i++){
                for(int j = 1; j < k + 1; j++){
                    dp[i][j] = dp[i-1][j]/*鸡蛋没碎,注意这里面i是次数!*/ + dp[i-1][j-1]/*鸡蛋碎了*/ + 1;
                    if(dp[i][j] >= n){
                        return i;
                    }
                }
            }
            return n;
        }
    
312. 戳气球
  • 思路分析,其实这道题dp应该这样定义,即 d p [ i ] [ j ] dp[i][j] dp[i][j]指的是区间i到j中,所得到的气球的最大值。

    那么经过前面的洗礼,我们很自然而然的就会想到在i和j之间用一个参数k来分割,因此自然而然是三个for循环,然后

    d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , 状态转移 ) dp[i][j] = max(dp[i][j], 状态转移) dp[i][j]=max(dp[i][j],状态转移)

    很自然而然

    但是这道题很经典在于,k这个气球是最后一个被戳爆的,一定要记住!!!

    根据状态转移方程可以画一下图,看看求 d p [ i ] [ j ] dp[i][j] dp[i][j]需要先求那一行一列,很清楚明白

    参考链接

int maxCoins(vector& nums) {
        int n = nums.size();
        vector temp_nums(n+2);
        temp_nums[0] = 1;
        for(int i = 0; i < n ; i++){
            temp_nums[i+1] = nums[i];
        }
        temp_nums[n+1] = 1;
        vector> dp(n+2, vector(n+2, 0));
        for(int i = n; i >= 0; i--){
            for(int j = i + 1; j < n + 2; j++){
                for(int k = i + 1; k < j; k++){
                    dp[i][j] = max(dp[i][j], dp[i][k]+dp[k][j]+(temp_nums[k]* temp_nums[i]* temp_nums[j]));
                }
            }
        }
        return dp[0][n+1];
    }

贪心算法

<思想>

寻找最优解问题,一般将求解过程分成若干个步骤,每个步骤都应用贪心原则,当前(局部)最优的选择,从局部最优策略扩展到全局的最优解。基本步骤如下:

  1. 从某个初始解出发
  2. 采用迭代的过程,当可以向目标前进一步时,根据局部最优策略,得到一部分解然后缩小问题规模
  3. 将所有解综合起来。

<简单题>

455. 分发饼干
  • 简单的贪心算法,排序+双指针
int findContentChildren(vector& g, vector& s) {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        cout<
1005. K 次取反后最大化的数组和
  • 刚开始想着从小到大排序然后把小的变成负的就行,但是这样是有问题的,因为没有考虑负数的问题。

    因此思路变成,计算一个数组中负数的个数,然后和k次比较,如果k大于负数的个数m,则将所有负数变成整数然后重新排序。

    如果k小于负数的个数m,则最小的负数变成正数就行

int largestSumAfterKNegations(vector& nums, int k) {
        int neg_num = 0;
        int len = nums.size();
        int sum = 0;
        for(int i = 0; i < len; i++){
            if(nums[i] < 0){
                neg_num++;
            }
        }
        sort(nums.begin(), nums.end());
        if(k > neg_num){
            //先把负数变成正数
            for(int i = 0; i < neg_num; i++){
                nums[i] = -nums[i];
            }
            k = (k - neg_num) % 2;
            //接下来全部变成正数数组
            sort(nums.begin(), nums.end());
            for(int i = 0; i < k; i++){
                nums[i] = -nums[i];
            }
            for(int i = 0; i < len; i++){
                sum+=nums[i];
            }
        }else{
            for(int i = 0; i < k; i++){
                nums[i] = -nums[i];
            }
            for(int i = 0; i < len; i++){
                sum+=nums[i];
            }
        }
        return sum;
    }
860. 柠檬水找零
bool lemonadeChange(vector& bills) {
        int len = bills.size();
        if(bills[0] != 5){
            return false;
        }
        //注意只有5,10,20的面值
        int five = 0;
        int ten = 0;
        int twenty = 0;
        int temp = 0;
        for(int i = 0; i < len; i++){
            if(bills[i] == 5){
                five++;
            }
            else if(bills[i] == 10){
                ten++;
            }
            else if(bills[i] == 20){
                twenty++;
            }
            temp = bills[i] - 5;
            if(temp == 5){
                five--;
            }
            else if(temp == 15){
                //更倾向于10+5这种方式找零
                if(ten > 0 ){
                    ten--;
                    five--;
                }else{       
                    five = five - 3;
                }

            }
            if(five < 0 || ten < 0 || twenty <0){
                return false;
            }
        }
        return true;
    }

<中等偏上>

376. ❤摆动序列
  • 贪心算法: 局部最优然后达到全局最优

    这道题想到错了这么多次

    首先要注意,峰值最右边的永远有1个,因此count初值=1,但是这道题为什么卡这么久,因为首先越界不报错,我真是服了,第二就是判定条件,一定要看仔细!!!!

int wiggleMaxLength(vector& nums) {
        //峰值法
        int pre = 0;
        int now = 0;
        int n = nums.size();
        if(n <= 1){
            return n;
        }
        int count = 1;
        for(int i = 0; i < n-1 ; i++){
            now = nums[i+1] - nums[i];
            if((pre <= 0 && now > 0) || (pre >= 0 && now < 0)){
                count++;
                pre = now;
            }
        }
        return count;
    }
738. 单调递增的数字
  • 思路:如果说是暴力破解的话,肯定是不可以的。

    可以考虑局部最优,单调递增就意味着最后一位最大为9。

    参考链接

    本来我没有加flag标志位,代码如下:

    int monotoneIncreasingDigits(int n) {
            string str_num = to_string(n);
            int len = str_num.size();
            if(n < 10){
                return n;
            }
            for(int i = len - 1; i > 0; i--){
                if(str_num[i] < str_num[i-1]){
                    cout<<"str_num[i]:"<

    这样写是错误的,遇见100这个用例就知道了,进入if的时候i为1,因此要加一个flag标志位,从该标志为往后都设置为9!

代码如下:

int monotoneIncreasingDigits(int n) {
        string str_num = to_string(n);
        int len = str_num.size();
        if(n < 10){
            return n;
        }
        int flag = len;
        for(int i = len - 1; i > 0; i--){
            if(str_num[i] < str_num[i-1]){
                flag = i;
                str_num[i-1]--; 
            }
        }
        for(int i = flag; i < len; i++){
            str_num[i] = '9';
        }
        return stoi(str_num);
    }
135. 分发糖果
  • 思路

    “相邻的孩子中评分高的孩子必须获得更多的糖果”这句话拆分成为了两个规则:

    1. 从数组左边开始遍历,当ratings[i] > rating[i-1]时,则必须保证第i个孩子的糖果比第i-1个的多

      这个是后比较是不完整的,比如说[1,0,2]这个数组,只比较了0,12,0,对于0,21,0这个顺序没有对比

    2. 因此还要从数组的右边开始遍历,比对一次,当ratings[i] > ratings[i]+1的时候,保证第i个孩子的糖果比第i+1个的多

    因此加入有个数组时[1,0,2],左边开始遍历得到数组[1,1,2],右边开始遍历的到数组[2,1,1],对于两个数组的同一个索引取最大值,即2+1+2=5

  • 代码

    int candy(vector& ratings) {
            int n = ratings.size();
            if(n <= 1){
                return 1;
            }
            vector left(n, 1);
            vector right(n, 1);
            for(int i = 1; i < n; i++){
                if(ratings[i] > ratings[i - 1]){
                    left[i] = left[i - 1] + 1;
                }
            }
            for(int i = n - 2; i >= 0; i-- ){
                if(ratings[i] > ratings[i + 1]){
                    right[i] = right[i + 1] + 1;
                }
            }
            int candy_num = 0;
            for(int i = 0; i < n; i++){
                candy_num +=max(left[i], right[i]);
            }
            return candy_num;
        }
    
406. 根据身高重建队列
  • 思路:

    这道题就是排序,让数组变得有意义起来

    思路很简单,首先根据第一个值倒序排序,因为第二个值的含义是前面有多少个大于等于第一个值的人,因此我们肯定先倒序。然后看第二个值,依次遍历数组,第二个值和索引比较,大于等于索引的就push_back,小于的就insert。

  • 代码:

    //加static是因为my_function函数其实有三个形参,第三个是this指针,但是sort中只用到了两个参数,参数不匹配
        //所以要加static,因为static成员函数没有this指针
        static bool my_function(vector& vec1, vector& vec2){
            return vec1[0] > vec2[0] || (vec1[0] == vec2[0] && vec1[1] < vec2[1]);
        }
        vector> reconstructQueue(vector>& people) {
           sort(people.begin(), people.end(), my_function);
           vector> temp;
           for(int i = 0; i < people.size(); i++){
               if(people[i][1] >= i){
                   temp.push_back(people[i]);
               }else{
                   temp.insert(temp.begin() + people[i][1], people[i]);
               }
           }
           return temp;
        }
    
  • 补充

    当数组时(key,value)类似类型的时候如何比较大小?我写到了代码集合里面

55. 跳跃游戏
  • 思路

    其实这道题应该换个问法,即通过下面数组的跳跃规则,最多可以跳多远?这样如果跳出去最远超过了数组长度,直接返回true就好了,小于数组长度说明跳不到最后一个格子。

  • 代码

    bool canJump(vector& nums) {
        int jump_to_index = 0;
        int n = nums.size();
        for(int i = 0; i < n  ; i++){
            if(i > jump_to_index){
                return false;
            }
            jump_to_index = max(jump_to_index, i + nums[i]);
        }
        return true;
    }
    

    首先我觉得这个题用“能跳跃到的索引”表示是最好的,因为 i + nums[i]指的就是能够跳跃到的最大的索引。

    有了上面这个理解下面就好理解很多,之前代码错就是将for里面的判断语句放到了下面,其实应该先判断在计算,先判断就表明对于下一个索引,jump_to_index能否到达。

45. 跳跃游戏 II
  • 思路

    贪婪贪婪,选择一个能调的最远的往下走,肯定就是最小值了

  • 代码

     int jump(vector& nums) {
            int jump_to_index = 0;
            int end_index = 0;
            int step = 0;
            int n = nums.size();
            for(int i = 0; i < n-1; i++){
                jump_to_index = max(jump_to_index, i+nums[i]);
                if(i == end_index){
                    step++;
                    end_index = jump_to_index;
                }
            }
            return step;
        }
    

    i < n-1是因为最后一次到达最后一个位置就不用再跳跃了。

    为什么是i == end_index,因为题目上说了“假设你总是可以到达数组的最后一个位置”,因此我总能到达最后一个位置,不用考虑越界的情况

你可能感兴趣的:(leetcode,算法,职场和发展)