难度分 1588:思路 + 技巧 = AC

Problem: 2844. 生成特殊数字的最少操作

  1. 前言

  2. 思路

    1. 题目改造

    2. 学习技巧

    3. 转变思路

      1. 错在哪里
      2. 怎么改
  3. 复杂度

  4. Code

前言

难度分 1588,说明此题在一众中等题里也不算泛泛之辈。

可以参考一下和它近似分数的这道题:Problem: 2970. 统计移除递增子数组的数目 I

当然这是后话,我早上起来看这道题的时候并没有想这么多,也不会因为它的分数而望而生畏。

本篇题解可以说并不是典型意义上的题解,而是记录了我自己解题的全过程:

  • 思路的从无到有
  • 试错和优化
  • 考虑技巧层面的提升【在此感谢灵神题解的点拨】

感兴趣的话,烦请继续往下看。

思路

还是和之前一样,我们面对一道新题的时候,想想办法让它和我们熟悉的领域靠近

题目名称里带有"最少操作",显而易见,这又是一道最值问题

最少,最值问题,我们可以联想一下我们以前写过的类似的最值问题的解法,都有哪些可以解决的方式?

比如:

等等....

由我上方举的例子,可以看到,解决最值问题是有挺多种方式的。因此针对本题,我们看看是否也可以让它往上面的几个例子靠一靠?

题目改造

👇我们仔细阅读一下题干信息👇

在一次操作中,您可以选择 num 的任意一位数字并将其删除。请注意,如果你删除 num 中的所有数字,则 num 变为 0。

返回最少需要多少次操作可以使 num 变成特殊数字。

我们可以任选一位数字删除,并且删除之后,还可以继续选。我们直接拿例子带入这个操作看看是什么样子:

比如示例一:num = "2245047"

我们可以选索引为0的数字删除

然后我们可以继续选索引为1的数字删除

此时我们不难发现,这个所谓的删除操作,实际上暴露了这样👇一条信息👇

每个数字只能选1次

而删除后还可以继续删除的继续操作,实际上暴露了这样👇一条信息👇

多个不同的数字可以组合

欸!发现没有,根据实际的操作和题干的信息,我们可以对这道题目做一个👇变式👇

即从给定的字符串中选择任意数量(最大不超过字符串的长度)的字符进行组合(即子序列),将这些字符删除后,看看剩余的字符串转成数字后能否被25整除。

这不就是一道枚举子序列的题么!

什么是子序列?

可以通过这一题回忆一下

392.判断子序列

还是拿示例一:num = "2245047"举例,它的子序列有这些:

  • 长度为1:2,2,4,5,0,4,7

  • 长度为2:

    • 22 , 24 , 25 , 20 , 24 , 27;
    • 24 , 25 , 20 , 24, 27;
    • 45 , 40 , 44 , 47;
    • 50 , 54 , 47
    • 04 , 07
    • 47
  • 长度为3:等等,不再举例。

可以看到由于字符串里的字符并不是唯一的,可以重复的,因此有些索引不同的字符组合而成的子序列看起来却是相同的,比如:

  • 索引0和索引2组合的子序列:24
  • 索引0和索引5组合的子序列:24

因此我们在写dfs的时候,注意存的得是索引值,而不是字符。

我们很快可以写出这样的dfs代码

ini 复制代码
/**
 * @param {string} num
 * @return {number}
 */
var minimumOperations = function (num) {
    let nums = Array.from(num)
    let count = 0;
    // 如果num本身就可以被25整除了,那么直接return 0;
    if (Number(num) % 25 === 0) {
        return 0;
    }
    // count表示当前枚举的子序列的长度  
    count++;
    // 用于存储索引值组合的数组
    let temp = [];
    // 用于判断在当前长度下的子序列的枚举中
    // 是否存在1个子序列能够满足题目的要求
    // 能,则直接return count,不再继续枚举。
    let flag = false;
    const dfs = (index) => {
        // 递归结束的边界条件
        // 即 索引值组合的数组长度 等于 子序列的长度
        if (temp.length === count) {
            let resNum = BigInt(nums.filter((item, index) => !temp.includes(index)).join(''))
            if (resNum % BigInt(25) === BigInt(0)) {
                flag = true;
            }
            return;
        }
        for (let i = index + 1; i < nums.length; i++) {
            temp.push(i)
            dfs(i)
            temp.pop()
        }
    }
    let res = 1;
    while (count < num.length) {

        for (let i = 0; i < nums.length; i++) {
            temp.push(i);
            dfs(i)
            if (flag) {
                return count;
            }
            temp.pop()
        }
        count++;
    }

    return count;

};

我们运行此程序,跑了几个用例,都过了,于是我们开心地点击提交

然后.........

靠,为啥?既然能通过500多个用例说明咱们的代码确实能够解决问题 ,但是为啥超时了?

我们查看一下题目给的信息:

再看一下我们的代码,我们根据字符串的子序列长度,从1到n要枚举每一位数字的子序列情况,即每次从n个元素中选取m(1 <= m <= n)个元素,我们可以写成这样的一个公式:

这也就意味着,咱们的时间复杂度是O(2^n)

而n最大可以是100

则咱们的时间复杂度就会变成O(2^100),这咋可能不超时啊???

从前面的分析来看,既然思路对了,为什么我们的解法会超时呢?

说明我们的思路并不对呀😭

由于这道题给的num的长度可以是100,我们枚举子序列的解法肯定会超时,那么是不是题干中还有一部分给的条件我们没有好好利用起来?

为什么一定是整除25?为什么一定是25这个数字,它特殊在哪里?

学习技巧

于是我点开题解,打算一探究竟,毕竟哥们知道25它特殊,但是真不知道特殊在哪里

@灵茶山艾府的题解

学习了一波灵神的题解后,我明白了,只有数字末尾是00255075的数才能被25整除(再加上一个特殊的数字,0

转变思路

错在哪里

我们不应该去枚举长度为(1~n)的子序列,然后去看在哪个长度能够满足题目条件。

这样的枚举做了太多无用功 了!!!!

还是拿示例一:num = "2245047"举例

我们从索引0的2开始做的枚举情况,是不是通通都是无用的?白白浪费时间?

对于从索引0开始的枚举情况,只有把全部数字都删除了,得到0,才能被25整除。

当子序列长度为2时,只有我们枚举到索引5和索引6组合的情况,此时得到的数才能被25整除,即,索引0、索引1、索引2、索引3、索引4的全部枚举都是在浪费时间。

怎么改

由于我们通过学习技巧,知道了只有末尾是00255075的数才能被25整除,因此,我们应该枚举的是末尾

我们从后往前 去遍历这个字符串,看看什么时候这个字符串能以00255075中的第二项结尾,即末尾是05

找到之后,从找到的地方的索引值开始,再去往前00255075中的第一项出现的位置,即0257出现的索引值。

如果我们使用的是js语言的话,可以直接用实例方法lastIndexOf快速找到指定字符最后出现的索引值。

根据上面的分析,我们可以写出这样的函数代码

js 复制代码
    const nlen = num.length;
    const suffixPairs = ['00', '50', '25', '75'];
    const findSuffix = (pair) => {
        const [pre, post] = pair;
        const postIndex = num.lastIndexOf(post);
        if (postIndex <= 0) {
            return nlen;
        }
        const preIndex = num.lastIndexOf(pre, postIndex - 1);
        // 2245047
        // 找'50'
        // 0 的index是4
        // 5 的index是3
        // 那留下的字符数量是 224 再加上 50,即 224:( 3 ),50:( + 2)
        // 被删除的字符数量就是 47,即 47:(7 - (3 + 2)) = 7 - 3 - 2 = 2
        // 也就是 nlen - preIndex - 2
        return preIndex === -1 ? nlen : nlen - preIndex - 2;
    }

    return Math.min(findSuffix(suffixPairs[0]), findSuffix(suffixPairs[1]), findSuffix(suffixPairs[2]), findSuffix(suffixPairs[3]));

不过这样写还没完哦,用这个代码过用例的话,用例: 10就过不去了,因为会输出2,而实际上的答案是1。

因为上面咱们也讨论过了,除了4种后缀的情况,我们还得考虑0这个数字。

即把num变成只剩一个0时,要删除的字符总数。

而这个字符总数,也要加入到Math.min的比较中去。

于是我们得到了最终版的代码

js 复制代码
/**
 * @param {string} num
 * @return {number}
 */
var minimumOperations = function (num) {
    const nlen = num.length;
    const suffixPairs = ['00', '50', '25', '75']
    const findSuffix = (pair) => {
        const [pre, post] = pair;
        const postIndex = num.lastIndexOf(post)
        if (postIndex <= 0) {
            return nlen;
        }
        // 第2个参数(postIndex - 1)表示反向搜索的起始位置  
        const preIndex = num.lastIndexOf(pre, postIndex - 1)
        // 2245047
        // 找'50'
        // 0 的index是4
        // 5 的index是3
        // 那留下的字符数量是 224 再加上 50,即 224:( 3 ),50:( + 2)
        // 被删除的字符数量就是 47,即 47:(7 - (3 + 2)) = 7 - 3 - 2 = 2
        // 也就是 nlen - preIndex - 2
        return preIndex === -1 ? nlen : nlen - preIndex - 2
    }
    const onlyZero = nlen - (num.includes('0') ? 1 : 0)
    return Math.min(onlyZero, findSuffix(suffixPairs[0]), findSuffix(suffixPairs[1]), findSuffix(suffixPairs[2]), findSuffix(suffixPairs[3]))

};

复杂度

  • 时间复杂度: O(N)
  • 空间复杂度: O(1)

Code

js 复制代码
/**
 * @param {string} num
 * @return {number}
 */
var minimumOperations = function (num) {
    const nlen = num.length;
    const suffixPairs = ['00', '50', '25', '75']
    const findSuffix = (pair) => {
        const [pre, post] = pair;
        const postIndex = num.lastIndexOf(post)
        if (postIndex <= 0) {
            return nlen;
        }
        // 第2个参数(postIndex - 1)表示反向搜索的起始位置  
        const preIndex = num.lastIndexOf(pre, postIndex - 1)
        // 2245047
        // 找'50'
        // 0 的index是4
        // 5 的index是3
        // 那留下的字符数量是 224 再加上 50,即 224:( 3 ),50:( + 2)
        // 被删除的字符数量就是 47,即 47:(7 - (3 + 2)) = 7 - 3 - 2 = 2
        // 也就是 nlen - preIndex - 2
        return preIndex === -1 ? nlen : nlen - preIndex - 2
    }
    const onlyZero = nlen - (num.includes('0') ? 1 : 0)
    return Math.min(onlyZero, findSuffix(suffixPairs[0]), findSuffix(suffixPairs[1]), findSuffix(suffixPairs[2]), findSuffix(suffixPairs[3]))

};
相关推荐
珠海西格电力5 小时前
云边端协同架构:零碳园区管理系统的技术底座
大数据·运维·人工智能·算法·架构·能源
还有多久拿退休金7 小时前
让飞书知识库跟着 commit 自己长:一套自动化知识库的真实实现
前端·算法·架构
KaMeidebaby8 小时前
卡梅德生物技术快报|小 RNA 适配体合成 + 多方法亲和力表征全流程标准化操作手册
前端·网络·数据库·人工智能·算法
是Dream呀8 小时前
基于深度学习的人类行为识别算法研究
人工智能·深度学习·算法
happyprince9 小时前
03_NVIDIA_ModelOpt-量化算法深入
人工智能·深度学习·算法
大鱼>9 小时前
AI+货物追踪:智能快递柜追踪系统
人工智能·深度学习·算法·机器学习
researcher-Jiang9 小时前
算法训练:堆 & 可并堆
算法
在书中成长10 小时前
HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计
算法·harmonyos
Frostnova丶10 小时前
(12)LeetCode 76. 最小覆盖子串
算法·leetcode·职场和发展