我是靠谱客的博主 昏睡老师,最近开发中收集的这篇文章主要介绍leetcode算法集锦,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

本文是收集了LeetCode各路大神的思路,供自己和各位算友共同学习进不!!!
本文是收集了LeetCode各路大神的思路,供自己和各位算友共同学习进不!!!
本文是收集了LeetCode各路大神的思路,供自己和各位算友共同学习进不!!!

LCP 18. 早餐组合

小扣在秋日市集选择了一家早餐摊位,一维整型数组 staple 中记录了每种主食的价格,一维整型数组 drinks 中记录了每种饮料的价格。小扣的计划选择一份主食和一款饮料,且花费不超过 x 元。请返回小扣共有多少种购买方案。

注意:答案需要以 1e9 + 7 (1000000007) 为底取模,如:计算初始结果为:1000000008,请返回 1

示例 1:
输入:staple = [10,20,5], drinks = [5,5,2], x = 15
输出:6
解释:小扣有 6 种购买方案,所选主食与所选饮料在数组中对应的下标分别是:
第 1 种方案:staple[0] + drinks[0] = 10 + 5 = 15;
第 2 种方案:staple[0] + drinks[1] = 10 + 5 = 15;
第 3 种方案:staple[0] + drinks[2] = 10 + 2 = 12;
第 4 种方案:staple[2] + drinks[0] = 5 + 5 = 10;
第 5 种方案:staple[2] + drinks[1] = 5 + 5 = 10;
第 6 种方案:staple[2] + drinks[2] = 5 + 2 = 7。

示例 2:
输入:staple = [2,1,1], drinks = [8,9,5,1], x = 9
输出:8
解释:小扣有 8 种购买方案,所选主食与所选饮料在数组中对应的下标分别是:
第 1 种方案:staple[0] + drinks[2] = 2 + 5 = 7;
第 2 种方案:staple[0] + drinks[3] = 2 + 1 = 3;
第 3 种方案:staple[1] + drinks[0] = 1 + 8 = 9;
第 4 种方案:staple[1] + drinks[2] = 1 + 5 = 6;
第 5 种方案:staple[1] + drinks[3] = 1 + 1 = 2;
第 6 种方案:staple[2] + drinks[0] = 1 + 8 = 9;
第 7 种方案:staple[2] + drinks[2] = 1 + 5 = 6;
第 8 种方案:staple[2] + drinks[3] = 1 + 1 = 2;

提示:
1 <= staple.length <= 10^5
1 <= drinks.length <= 10^5
1 <= staple[i],drinks[i] <= 10^5
1 <= x <= 2*10^5

思路

1、排序
2、双指针
3、i 从前往后, j 从后往前,找到 staple[i] + drinks[j] <= x 的位置
4、就可以往答案里加了

class Solution {
public:
    int breakfastNumber(vector<int>& staple, vector<int>& drinks, int x) {
        const int mod = 1e9 + 7;
        int ans = 0;
        sort(staple.begin(), staple.end());
        sort(drinks.begin(), drinks.end());
        int j = drinks.size() - 1;
        for (int i = 0; i < staple.size(); i++) {
        	// i 从前往后, j 从后往前,找到 staple[i] + drinks[j] <= x 的位置
            while (j >= 0 && staple[i] + drinks[j] > x) j--;
            if (j == -1) break;
            ans += j + 1;
            ans %= mod;
        }
        return ans;
    }
};

LCP 19. 秋叶收藏集

小扣出去秋游,途中收集了一些红叶和黄叶,他利用这些叶子初步整理了一份秋叶收藏集 leaves, 字符串 leaves 仅包含小写字符 r 和 y, 其中字符 r 表示一片红叶,字符 y 表示一片黄叶。
出于美观整齐的考虑,小扣想要将收藏集中树叶的排列调整成「红、黄、红」三部分。每部分树叶数量可以不相等,但均需大于等于 1。每次调整操作,小扣可以将一片红叶替换成黄叶或者将一片黄叶替换成红叶。请问小扣最少需要多少次调整操作才能将秋叶收藏集调整完毕。

示例 1:
输入:leaves = “rrryyyrryyyrr”
输出:2
解释:调整两次,将中间的两片红叶替换成黄叶,得到 “rrryyyyyyyyrr”

示例 2:
输入:leaves = “ryr”
输出:0
解释:已符合要求,不需要额外操作

提示:
3 <= leaves.length <= 10^5
leaves 中只包含字符 ‘r’ 和字符 ‘y’

方法一:动态规划

思路与算法

由于我们想要将收藏集中树叶的排列调整成「红、黄、红」三部分,因此我们可以用 3 个状态分别表示其中的每一部分,即状态 0 和状态 2 分别表示前面和后面的红色部分,状态 1 表示黄色部分。

此时,我们就可以尝试使用动态规划解决本题了。我们用f[i][j] 表示对于第 0 片到第 i 片叶子(记为leaves[0…i])进行调整操作,并且第 i 片叶子处于状态 j 时的最小操作次数。在推导状态转移方程时,我们可以分别对于每一种状态进行分析。

当 j=0 时,我们需要将第 i 片叶子变成红色,并且第 i−1 片叶子也只能处于 j=0 的状态(因为没有编号更小的状态了),因此有状态转移方程:
f[i][0]=f[i−1][0]+isYellow(i)

其中 isYellow(i) 为示性函数,当第 i 片叶子为黄色时为 1,红色时为 0。

当 j=1 时,我们需要将第 i片叶子变成黄色,而第 i−1 片叶子既可以处于 j=1 的状态,也可以处于 j=0 的状态,我们选择其中的较小值,因此有状态转移方程:

f[i][1]=min{f[i−1][0],f[i−1][1]}+isRed(i)

其中 isRed(i) 为示性函数,当第 i片叶子为红色时为 1,黄色时为 0。

当 j=2 时,我们需要将第 i 片叶子变成红色,而第 i−1 片叶子即可以处于 j=2 的状态,也可以处于 j=1 的状态(注意这里不能处于 j=0 的状态,因为每一种状态包含的叶子数量必须至少为 1),我们选择其中的较小值,因此有状态转移方程:

f[i][2]=min{f[i−1][1],f[i−1][2]}+isYellow(i)

最终的答案即为 f[n−1][2],其中 nn 是字符串leaves 的长度,也就是树叶的总数。

细节

由于 因为每一种状态包含的叶子数量必须至少为 1,因此不仅需要特别注意 j=2 时的状态转移方程,还需要考虑每个状态本身是否是符合要求的。对于状态 f[i][j] 而言,它包含了leaves[0…i] 共 i+1 片叶子以及 j+1个状态,因此 叶子的数量必须大于等于状态的数量,即满足i≥j。这样一来,所有 i<j 的状态 f[i][j] 都是不符合要求的。观察上面的状态转移方程,我们在每一步转移时都是取最小值,因此我们可以将所有不符合要求的状态置为一个极大值(例如n+1 或整数类型的上限等)。

同时需要注意的是,当 i=0 时,f[i][…] 会从f[i−1][…] 转移而来,但后者是没有意义的,因此我们需要对 f[i][…] 进行特殊处理。由于当 i=0 时,j 也只能为 0,因此我们有:
f[0][0]=isYellow(0)
作为唯一的边界条件。

class Solution {
    public int minimumOperations(String leaves) {
        int n = leaves.length();
        int[][] f = new int[n][3];
        f[0][0] = leaves.charAt(0) == 'y' ? 1 : 0;
        f[0][1] = f[0][2] = f[1][2] = Integer.MAX_VALUE;
        for (int i = 1; i < n; ++i) {
            int isRed = leaves.charAt(i) == 'r' ? 1 : 0;
            int isYellow = leaves.charAt(i) == 'y' ? 1 : 0;
            f[i][0] = f[i - 1][0] + isYellow;
            f[i][1] = Math.min(f[i - 1][0], f[i - 1][1]) + isRed;
            if (i >= 2) {
                f[i][2] = Math.min(f[i - 1][1], f[i - 1][2]) + isYellow;
            }
        }
        return f[n - 1][2];
    }
}

复杂度分析

时间复杂度:O(n)O(n),其中 nn 是字符串 textit{leaves}leaves 的长度。
空间复杂度:O(n)O(n)。
可以使用「降维」优化,用三个变量代替状态数组,即可将空间复杂度降低到 O(1)O(1)。具体操作留给读者自行实现。

LCP 22. 黑白方格画

小扣注意到秋日市集上有一个创作黑白方格画的摊位。摊主给每个顾客提供一个固定在墙上的白色画板,画板不能转动。画板上有 n * n 的网格。绘画规则为,小扣可以选择任意多行以及任意多列的格子涂成黑色,所选行数、列数均可为 0。

小扣希望最终的成品上需要有 k 个黑色格子,请返回小扣共有多少种涂色方案。

注意:两个方案中任意一个相同位置的格子颜色不同,就视为不同的方案。

示例 1:
输入:n = 2, k = 2
输出:4
解释:一共有四种不同的方案:
第一种方案:涂第一列;
第二种方案:涂第二列;
第三种方案:涂第一行;
第四种方案:涂第二行。

示例 2:
输入:n = 2, k = 1
输出:0
解释:不可行,因为第一次涂色至少会涂两个黑格。

示例 3:
输入:n = 2, k = 4
输出:1
解释:共有 2*2=4 个格子,仅有一种涂色方案。

限制:
1 <= n <= 6
0 <= k <= n * n

简单的Java排列组合解法(超暴力)

首先我们依次枚举涂抹 N 行 M列的情况

每涂抹一行就相当于涂抹了 N * n个格子
每涂抹一列就相当于涂抹了 M * n个格子
而当我们选择涂抹 N 行 M 列时,重复涂过 N*M 个格子去掉这个重复数,再来判断是不是等于k

判断 : (N * n + M * N)— N * M == k
当我们得出某个 N 和 M 组合能够得到k,我们就需要通过数学计算排列组合总数。

class Solution {
    public int paintingPlan(int n, int k) {
        int res = 0;
        //边界问题
        if(k == 0)return 1;
        if(k == n * n)return 1;

        //第一层循环表示涂 i 行 第二层循环表示涂 j 列
        for(int i = 0;i <= n;i++){
            for(int j = 0;j <= n;j++)
                //当你涂了 i 行 j 列  则有 i * n + j * n个方格被涂过了
                //去掉重复计入的 i*j个方格 是否等于结果k
                if((i*n) + (j*n) - (i*j) == k) {
                    res += C(i,n) * C(j,n);
                }
        }
        return res;
    }

    //数学里的排列组合 C(愚蠢式写法,勿计较)
    public int C(int x,int y){
        if(x == 0)return 1;
        int n = 1;
        for(int i = 0;i < x;i++){
            n *= (y - i);
        }
        for(int i = 1;i <= x;i++){
            n /= i;
        }
        return n;
    }
}

440. 字典序的第K小数字

给定整数 n 和 k,找到 1 到 n 中字典序第 k 小的数字。

注意:1 ≤ k ≤ n ≤ 109。

示例 :
输入:
n: 13 k: 2
输出:
10

解释:
字典序的排列是 [1, 10, 11, 12, 13, 2, 3, 4, 5, 6, 7, 8, 9],所以第二小的数字是 10。

什么是字典序?

简而言之,就是根据数字的前缀进行排序,
比如 10 < 9,因为 10 的前缀是 1,比 9 小。
再比如 112 < 12,因为 112 的前缀 11 小于 12。

这样排序下来,会跟平常的升序排序会有非常大的不同。先给你一个直观的感受,一个数乘 10,或者加 1,哪个大?可能你会吃惊,后者会更大。
但其实掌握它的本质之后,你一点都不会吃惊。

问题建模:

画一个图你就懂了。
在这里插入图片描述
每一个节点都拥有 10 个孩子节点,因为作为一个前缀 ,它后面可以接 0~9 这十个数字。而且你可以非常容易地发现,整个字典序排列也就是对十叉树进行先序遍历。1, 10, 100, 101, … 11, 110 …

回到题目的意思,我们需要找到排在第k位的数。找到他的排位,需要搞清楚三件事情:

怎么确定一个前缀下所有子节点的个数?
如果第 k 个数在当前的前缀下,怎么继续往下面的子节点找?
如果第 k 个数不在当前的前缀,即当前的前缀比较小,如何扩大前缀,增大寻找的范围?
接下来 ,我们一一拆解这些问题。

理顺思路:

1. 确定指定前缀下所有子节点数

现在的任务就是给定一个前缀,返回下面子节点总数。
我们现在的思路就是用下一个前缀的起点减去当前前缀的起点,那么就是当前前缀下的所有子节点数总和啦。

//prefix是前缀,n是上界
var getCount = (prefix, n) => {
    let cur = prefix;
    let next = prefix + 1;//下一个前缀
    let count = 0;
    //当前的前缀当然不能大于上界
    while(cur <= n) {
        count += next - cur;//下一个前缀的起点减去当前前缀的起点
        cur *= 10; 
        next *= 10;
        // 如果说刚刚prefix是1,next是2,那么现在分别变成10和20
        // 1为前缀的子节点增加10个,十叉树增加一层, 变成了两层
        
        // 如果说现在prefix是10,next是20,那么现在分别变成100和200,
        // 1为前缀的子节点增加100个,十叉树又增加了一层,变成了三层
    }
    return count;//把当前前缀下的子节点和返回去。
}

当然,不知道大家发现一个问题没有,当 next 的值大于上界的时候,那以这个前缀为根节点的十叉树就不是满十叉树了啊,应该到上界那里,后面都不再有子节点。因此,count += next - curcount+=next−cur 还是有些问题的,我们来修正这个问题:

count += Math.min(n+1, next) - cur;

你可能会问:咦?怎么是 n+1 ,而不是 nn 呢?不是说好了 nn 是上界吗?

我举个例子,假若现在上界 nn为 12,算出以 1 为前缀的子节点数,首先 1 本身是一个节点,接下来要算下面 10,11,12,一共有 4 个子节点。

那么如果用 Math.min(n, next) - curMath.min(n,next)−cur 会怎么样?

这时候算出来会少一个,12 - 10 加上根节点,最后只有 3 个。因此我们务必要写 n+1。

现在,我们搞定了前缀的子节点数问题。

2. 第k个数在当前前缀下

现在无非就是往子树里面去看。
prefix这样处理就可以了。

prefix *= 10
3. 第k个数不在当前前缀下

说白了,当前的前缀小了嘛,我们扩大前缀。

prefix ++;

框架搭建:
整合一下刚刚的思路。

let findKthNumber = function(n, k) {
  let p = 1;//作为一个指针,指向当前所在位置,当p==k时,也就是到了排位第k的数
  let prefix = 1;//前缀
  while(p < k) {
    let count = getNumber(prefix, n);//获得当前前缀下所有子节点的和
    if(p + count > k) { //第k个数在当前前缀下
      prefix *= 10;
      p++; //把指针指向了第一个子节点的位置,比如11乘10后变成110,指针从11指向了110
    } else if(p + count <= k) { //第k个数不在当前前缀下
      prefix ++;
      p += count;//注意这里的操作,把指针指向了下一前缀的起点
    }
  }
  return prefix;
};

完整代码展示:

/**
 * @param {number} n
 * @param {number} k
 * @return {number}
 */
var findKthNumber = function(n, k) {
  let getCount = (prefix, n) => {
    let count =  0;
    for(let cur = prefix, next = prefix + 1; cur <= n; cur *= 10, next *= 10) 
      count += Math.min(next, n+1) - cur;
    return count;
  }
  let p = 1;
  let prefix = 1;
  while(p < k) {
    let count = getCount(prefix, n);
    if(p + count > k) {
      prefix *= 10;
      p++;
    } else if(p + count <= k) {
      prefix ++;
      p += count;
    }
  }
  return prefix;
};

最后

以上就是昏睡老师为你收集整理的leetcode算法集锦的全部内容,希望文章能够帮你解决leetcode算法集锦所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(51)

评论列表共有 0 条评论

立即
投稿
返回
顶部