首页 > 其他分享 >LeetCode 周赛上分之旅 # 36 KMP 字符串匹配殊途同归

LeetCode 周赛上分之旅 # 36 KMP 字符串匹配殊途同归

时间:2023-08-01 19:58:20浏览次数:32  
标签:周赛 return nums int 复杂度 36 ret str KMP

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。

本文是 LeetCode 上分之旅系列的第 36 篇文章,往期回顾请移步到文章末尾~

周赛 356

T1. 满足目标工作时长的员工数目

  • 标签:模拟

T2. 统计完全子数组的数目

  • 标签:滑动窗口、散列表

T3. 包含三个字符串的最短字符串

  • 标签:贪心、全排列、前后缀分解、KMP

T4. 统计范围内的步进数字数

  • 标签:数位 DP、记忆化


T1. 满足目标工作时长的员工数目

https://leetcode.cn/problems/number-of-employees-who-met-the-target/

题解(模拟)

简单模拟题。

class Solution {
public:
    int numberOfEmployeesWhoMetTarget(vector<int>& hours, int target) {
        int ret = 0;
        for (int i = 0; i < hours.size(); i++) {
            if (hours[i] >= target) ret++;
        }
        return ret;
    }
};
class Solution:
    def numberOfEmployeesWhoMetTarget(self, hours: List[int], target: int) -> int:
        return sum(e >= target for e in hours)

复杂度分析:

  • 时间复杂度:$O(n)$ 线性扫描;
  • 空间复杂度:$O(1)$ 仅使用常量级别空间。

T2. 统计完全子数组的数目

https://leetcode.cn/problems/count-complete-subarrays-in-an-array/

题解一(枚举子数组 + 散列表)

枚举子数组,求满足条件的子数组数

class Solution {
public:
    int countCompleteSubarrays(vector<int>& nums) {
        int n = nums.size();
        int ret = 0;
        // 目标元素个数
        int target = unordered_set<int>(nums.begin(), nums.end()).size();
        // 枚举子数组
        for (int i = 0; i < nums.size(); i++) {
            unordered_set<int> curSet;
            for (int j = i; j < nums.size(); j++) {
                curSet.insert(nums[j]);
                if (curSet.size() == target) {
                    ret += n - j;
                    break;
                }
            }
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:$O(n^2)$ 枚举子数组时间;
  • 空间复杂度:$O(n)$ 散列表空间。

题解二(滑动窗口 + 散列表)

在题解一中,当子数组的满足条件时,我们不再需要扩展右指针 j,其实左指针 i 也类似。当存在子数组 [i, j] 满足条件时,我们可以收缩左指针到 [i+1, j],如果子数组依然满足条件,则可以继续记录子数组个数 n - j 个。

class Solution {
public:
    int countCompleteSubarrays(vector<int>& nums) {
        int n = nums.size();
        int ret = 0;
        // 目标元素个数
        int target = unordered_set<int>(nums.begin(), nums.end()).size();
        // 滑动窗口
        unordered_map<int, int> cnts;
        int i = 0;
        for (int j = 0; j < nums.size(); j++) {
            cnts[nums[j]]++;
            while (cnts.size() == target) {
                ret += n - j;
                if (--cnts[nums[i]] == 0) cnts.erase(nums[i]);
                i++;
            }
        }
        return ret;
    }
};

复杂度分析:

  • 时间复杂度:$O(n)$ 滑动窗口的 i 指针和 j 指针最多移动 n 次;
  • 空间复杂度:$O(n)$ 散列表空间。

相似题目:


T3. 包含三个字符串的最短字符串

https://leetcode.cn/problems/shortest-string-that-contains-three-strings/

题解一(贪心)

首先,合并字符串 a 和字符串 b 可以用前后缀分解来模拟:a 的最长后缀与 b 的最长前缀匹配,得到的合并字符串是最短的。而对于目标答案的合并方案来说,必然是 [a, b, c] 的全排列中的一种:

  • a + b + c
  • a + c + b
  • b + a + c
  • b + c + a
  • c + a + b
  • c + b + a

虽然,严谨来说局部贪心是错误的(即先将 a 和 b 合并得到最短字符串 ab,再将 ab 与 c 合并)。例如以下测试用例,这说明在第一次合并中选择最短的字符串,不一定是全局最短的字符串。但是,最优解必然可以通过全排列中的其他方案获得。因此,直接使用 “局部贪心” 即可。

a = "cdaa"
b = "aaef"
c = "daaae"
# a + b + c 其中 a + b = "cdaaef",无法与 c 合并得到最优解 “cdaaaef”
# a + c + b 可以得到最优解 “cdaaaef”
class Solution:
    def minimumString(self, a: str, b: str, c: str) -> str:
        def merge(a: str, b: str) -> str:
            if b in a: return a
            for i in range(min(len(a), len(b)), 0, -1):
                # 前后缀对比
                if a[-i:] == b[:i]: 
                    return a + b[i:]
            return a + b
        ret = ""
        for a, b, c in permutations((a, b, c)): 
            temp = merge(merge(a,b), c)
            # 优先最短字符串,再考虑字典序最小
            if (ret == "" or len(temp) < len(ret) or (len(temp) == len(ret) and temp < ret)):
                ret = temp
        return ret

复杂度分析:

  • 时间复杂度:$O(n^2)$ 单次合并的时间复杂度是 $O(n^2)$;
  • 空间复杂度:$O(n)$ 临时字符串空间。

题解二(KMP)

题解一时间复杂度的瓶颈在 merge 函数,对于两个字符串的最长的前后缀匹配长度,这正好就是 KMP 算法中求解 next 数组的步骤,而 KMP 算法的时间复杂度是 O(n),存在优化空间。

  • next[i] 的含义:s[:i] 的后缀与前缀的最长匹配长度

另外还有一个细节,在合并 a 和 b 时我们在中间插入分隔符 “#”,这是为了避免匹配长度大于 a 或 b的长度。例如:

a = "cac"
b = "aca"
# 那么 a + b = "cacaca" 会出现匹配长度大于 a 或 b的长度
class Solution:
    def minimumString(self, a: str, b: str, c: str) -> str:
        def merge(a: str, b: str) -> str:
            if b in a: return a
						# 拼接字符串,以计算 b 的后缀与 a 的前缀的匹配长度
            s = a + "#" + b
            # KMP 求 next 数组
            j, next = 0, [0] * len(s)
            for i in range(1, len(s)):
                while j > 0 and s[i] != s[j]:
                    j = next[j - 1]
                if s[i] == s[j]:
                    j += 1
                next[i] = j
            # next[-1]: s[-1] 的最长匹配前缀
            return b + a[next[-1]:]
        ret = ""
        for a, b, c in permutations((a, b, c)): 
            temp = merge(merge(a,b), c)
            # 优先最短字符串,再考虑字典序最小
            if (ret == "" or len(temp) < len(ret) or (len(temp) == len(ret) and temp < ret)):
                ret = temp
        return ret

复杂度分析:

  • 时间复杂度:$O(n)$ 单次合并的时间复杂度是 $O(n)$;
  • 空间复杂度:$O(n)$ 临时字符串空间。

T4. 统计范围内的步进数字数目

https://leetcode.cn/problems/count-stepping-numbers-in-range/

题解(数位 DP + 记忆化)

相对标准的数位 DP 模板题。

  • 1、数位 DP: 我们定义 dp[i, pre, isNumber, isLimit] 表示从第 i 位开始的合法方案数,其中:
    • pre 表示上一个数位选择的值;
    • isNumber 表示已填数位是否构造出合法数字;
    • isLimit 表示当前数位是否被当前数位的最大值约束。
  • 2、差值: 由于题目输入是字符串,要计算出 [low, high] 之间的合法方案数,我们可以计算出 [0, high] 和 [0, low] 之间合法方案数的差值,我们可以再单独判断 low 是否合法。
  • 3、记忆化: 对于相同 dp[i, …] 子问题,可能会重复计算,可以使用记忆化优化时间复杂度:
class Solution {
    
    val MOD = 1000000007
    
    fun countSteppingNumbers(low: String, high: String): Int {
        // 数位 DP
        return ((f(high) - f(low) + if (check(low)) 1 else 0) + MOD) % MOD
    }
    
    private fun f(num: String): Int {
        val memo = Array(num.length) { Array(10) { IntArray(2) { -1 } } }
        return dp(memo, 0, num, '0', false, true)
    }
    
    private fun check(num: String) : Boolean {
        for (i in 1 until num.length) {
            if (Math.abs(num[i] - num[i - 1]) != 1) return false
        }
        return true
    }
    
    // dp[i, pre, isNumber]
    private fun dp(memo: Array<Array<IntArray>>, i: Int, high: String, pre: Char, isNumber: Boolean, isLimit: Boolean): Int {
        // 终止条件
        if (i == high.length) {
            return if (isNumber) 1 else 0
        }
        // 读备忘录
        if (!isLimit && -1 != memo[i][pre - '0'][if (isNumber) 1 else 0]) {
            return memo[i][pre - '0'][if(isNumber) 1 else 0]
        }
        var ret = 0
        val lower = '0'
        val upper = if (isLimit) high[i] else '9'
        for (choice in lower .. upper) {
            if (!isNumber || Math.abs(choice - pre) == 1) {
                ret = (ret + dp(memo, i + 1, high, choice, isNumber || choice != '0', isLimit && choice == upper)) % MOD
            }
        }
        if (!isLimit) memo[i][pre - '0'][if (isNumber) 1 else 0] = ret
        return ret
    }
}

复杂度分析:

  • 时间复杂度:$O(nC·C)$ 其中 n 为数位长度,C 为字符集大小 ,总共有 n·C 个子状态,每个子状态的时间复杂度是 $O(C)$,整体时间复杂度是 $O(n·C^2)$
  • 空间复杂度:$O(n·C)$ 记忆化空间。

推荐阅读

LeetCode 上分之旅系列往期回顾:

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

标签:周赛,return,nums,int,复杂度,36,ret,str,KMP
From: https://www.cnblogs.com/pengxurui/p/17598920.html

相关文章

  • LOJ3677 「北大集训 2021」出题高手
    卡死人了。数据随机写在上面,就是让你预估一下区间长度不会太长的,数据里最长的不超过\(2000\)。暴力扫\(2000\)个显然过不了\(500000\)的点,但是\(500000\)的点\(m\)为\(1\)且必定询问整个序列。可以分析出,在随机情况下,前缀和最小最大数量是根号个的,平方后是四次根号级......
  • 笔记:KMP的复习
    Record一个重要的字符串算法,这是第三次复习。通过总结我认为之所以某个算法总是忘记,是因为大脑始终没有认可这种算法的逻辑(也就是脑回路)。本篇主要讲解从KMP的应用场景,再到算法知识,以及例题。Main现有两个字符串\(A,B\),求出\(A\)在\(B\)中出现的次数。范围:字符串长度......
  • 第 356 场周赛 - 力扣(LeetCode)
    第356场周赛-力扣(LeetCode)2798.满足目标工作时长的员工数目-力扣(LeetCode)一次遍历classSolution{public:intnumberOfEmployeesWhoMetTarget(vector<int>&hours,inttarget){intans=0;for(autoi:hours)ans+=......
  • AcWing,第114场周赛-5058双色球
    5058.双色球约翰和贝茜玩抽球游戏。一个盒子中有n个白球和m个黑球。双方轮流行动,由约翰先行。每当轮到一方行动时,其从盒中随机抽出一个球,盒子中的每个球被抽出的概率相同。率先抽出白球的一方获胜。此外,由于贝茜的手比较笨拙,所以每当她抽出一个球后,盒子都会剧烈摇晃,随后就......
  • 超强抗干扰单键/1通道触摸触控芯片VK36N2D SOP8 适用于厨房秤/筋膜枪等电源供电产品
    概述:VK36N2DSOP8具有2个触摸按键,可用来检测外部触摸按键上人手的触摸动作。该芯片具有较高的集成度,仅需极少的外部组件便可实现触摸按键的检测。提供了2个1对1输出脚,可通过IO脚选择上电输出电平,有直接输出和锁存输出2个型号可选。芯片内部采用特殊的集成电路,具有高电源电压抑制比......
  • P3375 【模板】KMP 字符串匹配 题解
    前言狗屁不是,建议别看!!! 题目链接P3375【模板】KMP字符串匹配-洛谷|计算机科学教育新生态(luogu.com.cn) 分析先给个例子s1:ABCABCABBs2:ABCABB若使用朴素算法匹配,当匹配到s1:ABCABCABBs2:ABCABB时,朴素算法会跳出,然后匹配下一位。最终匹配到s1:ABCABCABBs2:......
  • Gym103687K Dynamic Reachability
    一个很奇妙的题。回想起之前打的一场模拟赛,有一道题的部分问题是要维护动态图两两联通性的。可能不太一样,但是他有一个离线的思想,将没有修改过的边提前拎出来,把已知的联通性先求了,再用线段树分治一类的可撤销做法维护剩下边的修改。但是这样维护的复杂度跟修改次数相关非常大,如果......
  • c++字符串搜索之KMP
    classSolution{private:voidgetNext(int*arr,stringstr){intlen=str.length();arr[0]=0;intj=0;for(inti=1;i<len;i++){while(j>0&&str[i]!=str[j]){......
  • 链表/栈/队列/KMP
    链表用数组模拟,不同于结构体加指针调用new关键字开上万级别的节点非常慢,基本会超时单链表来构造邻接表用于存图与树基本结构:head表示头结点的下标e[i]表示节点i的值ne[i]表示节点i的下一个节点的下标idx存储当前已经用到了哪个节点,表示新节点基本操作:......
  • 防干扰/抗电压波动单按键/1路触摸触控芯片VK36N1D SOT23-6 适用于厨房秤/温控器/加湿
    触摸芯片是一种可感应人体触摸的微处理器,其工作原理是通过感应人体触摸带来的电容变化而实现的;当人体接近触摸屏幕表面时,会引起触摸屏与人体间的电容改变,并且形成了一个新的电场分布,芯片会根据这个电容改变来计算出具体的触摸位置和操作手势。目前触摸芯片应用涉及于消费类电子、......