LeetCode 周赛上分之旅 # 36 KMP 字符串匹配殊途同归
wxin55 2024-11-17 16:49 72 浏览 0 评论
?? 本文已收录到 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) 散列表空间。
相似题目:
- 3. 无重复字符的最长子串
- 159. 至多包含两个不同字符的最长子串
- 209. 长度最小的子数组
- 424. 替换后的最长重复字符
- 713. 乘积小于 K 的子数组
- 992. K 个不同整数的子数组
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 上分之旅系列往期回顾:
LeetCode 单周赛第 355 场 · 两题坐牢,菜鸡现出原形
LeetCode 单周赛第 354 场 · 摩尔投票派上用场
LeetCode 双周赛第 109 场 · 按部就班地解决动态规划问题
LeetCode 双周赛第 107 场 · 很有意思的 T2 题
?? 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~
- 上一篇:字符串:听说你对KMP还有这些疑问?
- 下一篇:C语言 多算法实现字符串匹配
相关推荐
- Shiro学习系列教程三:集成web(web集成环境)
-
相关推荐:《Shiro学习系列教程一:Shiro之helloworld》《Shiro学习系列教程三:集成web》《Shiro学习系列教程四:集成web(二)》《Shiro学习系列教程五:自定义Real...
- 写了这么多年代码,这样的登录方式还是头一回见
-
SpringSecurity系列还没搞完,最近还在研究。有的时候我不禁想,如果从SpringSecurity诞生的第一天开始,我们就一直在追踪它,那么今天再去看它的源码一定很简单,因为我们了...
- Shiro框架:认证和授权原理(shiro框架授权的四种方式)
-
优质文章,及时送达前言Shiro作为解决权限问题的常用框架,常用于解决认证、授权、加密、会话管理等场景。本文将对Shiro的认证和授权原理进行介绍:Shiro可以做什么?、Shiro是由什么组成的?举...
- Spring Boot 整合 Shiro-登录认证和权限管理
-
这篇文章我们来学习如何使用SpringBoot集成ApacheShiro。安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求。在Java领域一般有SpringS...
- Apache Shiro权限管理解析二Apache Shiro核心组件
-
ApacheShiro核心组件Subject(用户主体)Subject是Shiro中的核心概念之一,表示当前用户(可以是登录的用户或匿名用户)。它是与用户交互的主要接口,提供了对用户身份验证...
- 详细介绍一下Apache Shiro的实现原理?
-
ApacheShiro是一个强大、灵活的Java安全框架,设计目标是简化复杂的安全需求,提供灵活的API,使开发者能方便地将安全功能集成到任何应用中。主要作用是用于管理身份验证、授权、会话管理和加...
- 什么是Apache Shiro?SpringBoot中如何整合Apache Shiro?
-
ApacheShiro是一个功能强大且易于使用的Java安全框架,主要用于构建安全的企业应用程序,例如在应用中处理身份验证(Authentication)、授权(Authorization)、加密(...
- Apache Shiro权限管理解析三Apache Shiro应用
-
Shiro的优势与适用场景优势简单易用:API设计直观,适合中小型项目快速实现权限管理。灵活性高:支持多种数据源(数据库、LDAP等),并允许开发者自定义Realm。跨平台支持:不仅限于We...
- 那些通用清除软件不曾注意的秘密(清理不需要的应用)
-
系统清理就像卫生检查前的大扫除,即使你使出吃奶的劲儿把一切可能的地方都打扫过,还会留下边边角角的遗漏。随着大家电脑安全意识的提高,越来越多的朋友开始关注自己的电脑安全,也知道安装360系列软件来"武装...
- JWT在跨域认证中的奇妙应用(jq解决跨域)
-
JWT在跨域认证中的奇妙应用什么是JWT?让我们先来聊聊JWT(JSONWebToken)。它是一种轻量级的认证机制,就像一张电子车票,能让用户在不同的站点间通行无阻。JWT由三部分组成:头部(H...
- 开启无痕浏览模式真能保护个人隐私吗?
-
在访问网站页面时,你是否有过这样的疑虑,自己访问的会不会是山寨网站?用公用电脑上网,个人信息会被别人看到吗?这时,有人会说,使用浏览器的“无痕浏览”模式不就行了,可以在操作中不留下“蛛丝马迹”,但,真...
- 辅助上网为啥会被抛弃 曲奇(Cookie)虽甜但有毒
-
近期有个小新闻,大概很多小伙伴都没有注意到,那就是谷歌Chrome浏览器要弃用Cookie了!说到Cookie功能,很多小伙伴大概觉得不怎么熟悉,有可能还不如前一段时间被弃用的Flash“出名”,但它...
- cookie、session和token(cookie,session和token的区别)
-
Cookie的概念最早是在1994年由NetscapeCommunications的程序员LouMontulli发明的,目的是为了解决当时早期互联网的一个关键问题:HTTP无状态协...
- 小白都能看懂的session与cookie的区别理解
-
cookie/session都是跟踪识别浏览器用户身份的一个东西。cookie的理解:我们要知道,服务器和客户端之间进行数据传输,需要使用到一个超文本传输协议(http协议),而http协议本身是个...
- 面试:网易一面:支撑10万QPS的电商购物车系统如何架构设计呢?
-
1.需求分析:10万QPS的购物车系统需要满足哪些需求?回答:10万QPS的购物车系统需要满足以下核心需求和挑战:核心功能:添加、删除、修改购物车商品实时查看购物车列表支持高并发读写(10万QPS)...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Shiro学习系列教程三:集成web(web集成环境)
- 写了这么多年代码,这样的登录方式还是头一回见
- Shiro框架:认证和授权原理(shiro框架授权的四种方式)
- Spring Boot 整合 Shiro-登录认证和权限管理
- Apache Shiro权限管理解析二Apache Shiro核心组件
- 详细介绍一下Apache Shiro的实现原理?
- 什么是Apache Shiro?SpringBoot中如何整合Apache Shiro?
- Apache Shiro权限管理解析三Apache Shiro应用
- 那些通用清除软件不曾注意的秘密(清理不需要的应用)
- JWT在跨域认证中的奇妙应用(jq解决跨域)
- 标签列表
-
- hive行转列函数 (63)
- sourcemap文件是什么 (54)
- display none 隐藏后怎么显示 (56)
- 共享锁和排他锁的区别 (51)
- httpservletrequest 获取参数 (64)
- jstl包 (64)
- qsharedmemory (50)
- watch computed (53)
- java中switch (68)
- date.now (55)
- git-bash (56)
- 盒子垂直居中 (68)
- npm是什么命令 (62)
- python中+=代表什么 (70)
- fsimage (51)
- nginx break (61)
- mysql分区表的优缺点 (53)
- centos7切换到图形界面 (55)
- 前端深拷贝 (62)
- kmp模式匹配算法 (57)
- jsjson字符串转json对象 (53)
- jdbc connection (61)
- javascript字符串转换为数字 (54)
- mybatis 使用 (73)
- 安装mysql数据库 (55)