我正在参与「启航计划」
大家好,今天是 3T 选手小彭。
上周是 LeetCode 第 332 场周赛,你参与了吗?算法解题思维需求长期锻炼,加入咱们一起刷题吧~
小彭的 Android 沟通群 02 群现已建立啦,大众号回复 “加群” 加入咱们~
2562.找出数组的串联值(Easy)
标题地址
leetcode.cn/problems/fi…
标题描绘
给你一个下标从0开端的整数数组nums。
现定义两个数字的串联是由这两个数值串联起来形成的新数字。
- 例如,
15和49的串联是1549。
nums的串联值最初等于0。履行下述操作直到nums变为空:
- 假如
nums中存在不止一个数字,别离选中nums中的榜首个元素和最终一个元素,将二者串联得到的值加到nums的串联值上,然后从nums中删去榜首个和最终一个元素。 - 假如仅存在一个元素,则将该元素的值加到
nums的串联值上,然后删去这个元素。
回来履行完一切操作后nums的串联值。
题解
简略模拟题,运用双指针向中心逼近即可。
class Solution {
fun findTheArrayConcVal(nums: IntArray): Long {
var left = 0
var right = nums.size - 1
var result = 0L
while (left <= right) {
result += if (left == right) {
nums[left]
} else{
Integer.valueOf("${nums[left]}${nums[right]}")
}
left++
right--
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(n)O(n)
- 空间复杂度:O(1)O(1)
2563.核算公正数对的数目(Medium)
标题地址
leetcode.cn/problems/co…
标题描绘
给你一个下标从0开端、长度为n的整数数组nums,和两个整数lower和upper,回来公正数对的数目。
假如(i, j)数对满意以下状况,则认为它是一个公正数对:
-
0 <= i < j < n,且 lower <= nums[i] + nums[j] <= upper
题解一(排序 + 枚举组合)
标题要求寻觅 2 个方针数 nums[i] 和 nums[j] 满意两数之和处于区间 [lower, upper] 。尽管标题强调了下标 i 和下标 j 满意 0 <= i < j < n,但事实上两个数的次序并不重要,咱们挑选 nums[2] + nums[4] 与挑选 nums[4] + nums[2] 的结果是相同的。因而,榜首反应能够运用 “朴素组合模板”,时刻复杂度是 O(n2)O(n^2),但在这道题中会超出时刻约束。
// 组合模板
class Solution {
fun countFairPairs(nums: IntArray, lower: Int, upper: Int): Long {
val n = nums.size
var result = 0L
for (i in 0 until nums.size - 1) {
for (j in i + 1 until nums.size) {
val sum = nums[i] + nums[j]
if (sum in lower..upper) result++
}
}
return result
}
}
以示例 1 来说,咱们发现在外层循环挑选 nums[i] = 4 的一趟循环中,当内层循环挑选 [4+4][4 + 4] 组合不满意条件后,挑选一个比 44 更大的 [4+5][4 + 5] 组合显得没有必要。从这里简单想到运用 “排序” 剪去不必要的组合计划:咱们能够先对输入数据进行排序,当内层循环的 nums[j] 不再或许满意条件时提前终止内层循环:
class Solution {
fun countFairPairs(nums: IntArray, lower: Int, upper: Int): Long {
// 排序 + 枚举组合
var result = 0L
nums.sort()
for (i in 0 until nums.size - 1) {
for (j in i + 1 until nums.size) {
val sum = nums[i] + nums[j]
if (sum < lower) continue
if (sum > upper) break
result++
}
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(nlgn+n2)O(nlgn + n^2) 快速排序 + 组合的时刻,其间 O(n2)O(n^2) 是一个比较松的上界。
- 空间复杂度:O(lgn)O(lgn) 快速排序占用的递归栈空间。
题解二(排序 + 二分查找)
运用排序优化后依然无法满意标题要求,咱们发现:内层循环并不需求线性扫描,咱们能够运用 O(lgn)O(lgn) 二分查找寻觅:
- 榜首个大于等于 min 的数
- 最终一个小于等于 max 的数
再运用这 2 个边界数的下标相减,即可获得内层循环中的方针组合个数。
class Solution {
fun countFairPairs(nums: IntArray, lower: Int, upper: Int): Long {
// 排序 + 二分查找
var result = 0L
nums.sort()
for (i in 0 until nums.size - 1) {
// nums[i] + x >= lower
// nums[i] + x <= upper
// 方针数的范围:[lower - nums[i], upper - nums[i]]
val min = lower - nums[i]
val max = upper - nums[i]
// 二分查找优化:寻觅榜首个大于等于 min 的数
var left = i + 1
var right = nums.size - 1
while (left < right) {
val mid = (left + right - 1) ushr 1
if (nums[mid] < min) {
left = mid + 1
} else {
right = mid
}
}
val minIndex = if (nums[left] >= min) left else continue
// 二分查找优化:寻觅最终一个小于等于 max 的数
left = minIndex
right = nums.size - 1
while (left < right) {
val mid = (left + right + 1) ushr 1
if (nums[mid] > max) {
right = mid - 1
} else {
left = mid
}
}
val maxIndex = if (nums[left] <= max) left else continue
result += maxIndex - minIndex + 1
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(nlgn+nlgn)O(nlgn + nlgn) 快速排序 + 组合的时刻,内层循环中每次二分查找的时刻是 O(lgn)O(lgn)。
- 空间复杂度:O(lgn)O(lgn) 快速排序占用的递归栈空间。
2564.子字符串异或查询(Medium)
标题地址
leetcode.cn/problems/su…
标题描绘
给你一个二进制字符串s和一个整数数组queries,其间queries[i] = [firsti, secondi]。
关于第i个查询,找到s的最短子字符串,它对应的十进制值val与firsti按位异或得到secondi,换言之,val ^ firsti == secondi。
第i个查询的答案是子字符串[lefti, righti]的两个端点(下标从0开端),假如不存在这样的子字符串,则答案为[-1, -1]。假如有多个答案,请你挑选lefti最小的一个。
请你回来一个数组ans,其间ans[i] = [lefti, righti]是第i个查询的答案。
子字符串是一个字符串中一段接连非空的字符序列。
前置常识
记 ⊕ 为异或运算,异或运算满意以下性质:
- 基本性质:x ⊕ y = 0
- 交换律:x ⊕ y = y ⊕ x
- 结合律:(x ⊕ y) ⊕ z = x ⊕ (y ⊕ z)
- 自反律:x ⊕ y ⊕ y = x
题解一(滑动窗口)
标题要求字符串 s 的最短子字符串,使其满意其对应的数值 val ⊕ first = second,依据异或的自反律性质可知(等式两边同异或 first),标题等价于求满意 val = first ⊕ second 的最短子字符串。
简单想到的思路是:咱们独自处理 queries 数组中的每个查询,并核算方针异或值 target = first ⊕ second,而方针字符串的长度必定与 target 的二进制数的长度相同。所以,咱们先获取 target 的有用二进制长度 len,再运用长度为 len 的滑动窗口寻觅方针子字符串。因为标题要求 [left 最小的计划,所以需求在每次寻觅到答案后提前中止。
class Solution {
fun substringXorQueries(s: String, queries: Array<IntArray>): Array<IntArray> {
// 寻觅等于方针值的子字符串
// 滑动窗口
val n = s.length
val result = Array(queries.size) { intArrayOf(-1, -1) }
for ((index, query) in queries.withIndex()) {
val target = query[0] xor query[1]
// 核算 target 的二进制长度
var len = 1
var num = target
while (num >= 2) {
num = num ushr 1
len++
}
for (left in 0..n - len) {
val right = left + len - 1
if (s.substring(left, right + 1).toInt(2) == target) {
result[index][0] = left
result[index][1] = right
break
}
}
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(mn)O(mn),其间 m 是
queries数组的长度,n 是字符串的长度,在这道题中会超时。 - 空间复杂度:O(1)O(1),不考虑结果数组。
题解二(滑动窗口 + 分桶预处理)
事实上,假如每次都独自处理 queries 数组中的每个查询,那么标题将查询设置为数组就没有意义了,并且在遇到方针异或值 target 的二进制长度 len 相同时,会存在很多重复核算。因而,简单想到的思路是:咱们能够预先将 queries 数组中一切二进制长度 len 相同的查询划分为一组,使相同长度的滑动窗口只会核算一次。
另一个细节是标题的测试用例中存在相同的查询,所以咱们需求在映射表中运用 LinkedList 记载相同方针异或值 target 到查询下标 index 的联系。
class Solution {
fun substringXorQueries(s: String, queries: Array<IntArray>): Array<IntArray> {
// 寻觅等于方针值的子字符串
// 依据长度分桶:len to <target,index>
val lenMap = HashMap<Int, HashMap<Int, LinkedList<Int>>>()
for ((index, query) in queries.withIndex()) {
val target = query[0] xor query[1]
// 核算 target 的二进制长度
var len = 1
var num = target
while (num >= 2) {
num = num ushr 1
len++
}
lenMap.getOrPut(len) { HashMap<Int, LinkedList<Int>>() }.getOrPut(target) { LinkedList<Int>() }.add(index)
}
// 滑动窗口
val n = s.length
val result = Array(queries.size) { intArrayOf(-1, -1) }
for ((len, map) in lenMap) {
for (left in 0..n - len) {
val right = left + len - 1
val curValue = s.substring(left, right + 1).toInt(2)
if (map.containsKey(curValue)) {
for (index in map[curValue]!!) {
result[index][0] = left
result[index][1] = right
}
map.remove(curValue)
// 该长度查找结束
if (map.isEmpty()) break
}
}
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(m+Ln)O(m + Ln),其间 n 是字符串的长度, m 是
queries数组的长度,L 是不同长度的窗口个数,O(m)O(m) 是预处理的时刻。依据标题输入满意 109<23010^9 < 2^{30} 可知 L 的最大值是 30。 - 空间复杂度:O(m)O(m),散列表总共需求记载 m 个查询的映射联系。
题解三(滑动窗口 + 预处理字符串)
这道题的思路也是经过预处理过滤相同长度的滑动窗口,差异在于预处理的是输入字符串,咱们直接核算字符串 s 中一切或许出现的数字以及对应的 [left,right] 下标,再利用这份数据给予 queries 数组进行 O(1)O(1) 打表查询。
class Solution {
fun substringXorQueries(s: String, queries: Array<IntArray>): Array<IntArray> {
val n = s.length
// 预处理
val valueMap = HashMap<Int, IntArray>()
for (len in 1..Math.min(n,31)) {
for (left in 0..n - len) {
val right = left + len - 1
val num = s.substring(left, right + 1).toInt(2)
if (!valueMap.containsKey(num)) {
valueMap[num] = intArrayOf(left, right)
}
}
}
val result = Array(queries.size) { intArrayOf(-1, -1) }
for ((index, query) in queries.withIndex()) {
val target = query[0] xor query[1]
if (valueMap.containsKey(target)) {
result[index] = valueMap[target]!!
}
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(Ln+m)O(Ln + m),其间 n 是字符串的长度, m 是
queries数组的长度,L 是不同长度的窗口个数。O(Ln)O(Ln) 是预处理的时刻,依据标题输入满意 109<23010^9 < 2^{30} 可知 L 的最大值是 30。 - 空间复杂度:O(nL)O(nL),散列表总共需求记载 nL 个数的映射联系。
2565.最少得分子序列(Hard)
标题地址
leetcode.cn/problems/su…
标题描绘
给你两个字符串s和t。
你能够从字符串t中删去任意数目的字符。
假如没有从字符串t中删去字符,那么得分为0,否则:
- 令
left为删去字符中的最小下标。 - 令
right为删去字符中的最大下标。
字符串的得分为right - left + 1。
请你回来使t成为s子序列的最小得分。
一个字符串的子序列是从原字符串中删去一些字符后(也能够一个也不删去),剩下字符不改变次序得到的字符串。(比方说"ace"是"acde"的子序列,但是"aec"不是)。
题解(前后缀分化)
这道题榜首感觉是 LCS 最长公共子序列的衍生问题,咱们能够运用朴素 LCS 模板求解字符串 s 和字符串 t 的最长公共子序列 ,再运用 t 字符串的长度减去公共部分长度得到需求删去的字符个数。
但是,这道标题的输出得分取决于最左面被删去的字符下标 indexleftindex_{left} 和最右边被删去字符的下标 indexrightindex_{right},惯例套路显得无从下手。所以,咱们测验对原问题进行转化:
-
考虑 1: 假定删去
left和right两个字符后能够满意条件,那么删去[left,right]中心一切字符也相同能满意条件(贪心思路:删去更多字符后成为子序列的或许性更大); -
考虑 1 结论: 原问题等价于求删去字符串
t中的最短字符串[i,j],使得剩下部分[0, i - 1]和[j + 1, end]合并后成为字符串s的一个子序列。 -
考虑 2: 假如字符串 t 删去
[i, j]区间的字符后能够满意条件,那么必定存在剩下部分[0, i - 1]与字符串s的前缀匹配,而[j + 1, end]与字符串s的后缀匹配,并且这两段匹配的区域必定 “不存在” 交集。 -
考虑 2 结论: 咱们能够枚举字符串 s 中的一切切割点,别离位于切割点的
s前缀匹配t的前缀,用s的后缀匹配t的后缀,核算匹配后需求减去的子串长度,将一切枚举计划的解取最小值便是原标题的解。
思路参阅视频讲解:www.bilibili.com/video/BV1GY… —— 灵茶山艾府 著
class Solution {
fun minimumScore(s: String, t: String): Int {
// 前后缀分化
val n = s.length
val m = t.length
// s 的后缀和 t 的后缀匹配的最长子串的开始下标
val sub = IntArray(n + 1).apply {
var right = m - 1
for (index in n - 1 downTo 0) {
if (right >= 0 && s[index] == t[right]) right--
this[index] = right + 1
}
this[n] = m
}
// s 的前缀和 t 的前缀匹配的最长子串的终止下标
val pre = IntArray(n).apply {
var left = 0
for (index in 0..n - 1) {
if (left < m && s[index] == t[left]) left++
this[index] = left - 1
}
}
// 枚举切割点
var result = sub[0]
if (0 == result) return 0 // 整个 t 是 s 的子序列
for (index in 0 until n) {
result = Math.min(result, m - (m - sub[index + 1]) - (pre[index] + 1))
}
return result
}
}
复杂度剖析:
- 时刻复杂度:O(n)O(n),其间 n 是字符串 s 的长度,预处理和枚举的时刻复杂度都是 O(n)O(n)。
- 空间复杂度:O(n)O(n),前后缀数组的空间。
咱们下周见,有用请点赞重视!
本文已收录到 AndroidFamily,技能和职场问题,请重视大众号 [彭旭锐] 提问。






