本文已收錄到 AndroidFamily,技術和職場問題,請關注公衆號 [彭旭銳] 提問。
大家好,今天是 3T 選手小彭。
上週是 LeetCode 第 332 場周賽,你參加了嗎?算法解題思維需要長時間鍛鍊,加入我們一起刷題吧~
小彭的 Android 交流羣 02 羣已經建立啦,公衆號回覆 “加羣” 加入我們~
2562. 找出數組的串聯值(Easy)
題目地址
https://leetcode.cn/problems/find-the-array-concatenation-value/
題目描述
給你一個下標從 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
}
}
複雜度分析:
- 時間複雜度:
- 空間複雜度:
2563. 統計公平數對的數目(Medium)
題目地址
https://leetcode.cn/problems/count-the-number-of-fair-pairs/
題目描述
給你一個下標從 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]
的結果是相同的。因此,第一反應可以使用 “樸素組合模板”,時間複雜度是 ,但在這道題中會超出時間限制。
// 組合模板
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
的一趟循環中,當內層循環選擇 組合不滿足條件後,選擇一個比 更大的 組合顯得沒有必要。從這裏容易想到使用 “排序” 剪去不必要的組合方案:我們可以先對輸入數據進行排序,當內層循環的 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
}
}
複雜度分析:
- 時間複雜度: 快速排序 + 組合的時間,其中 是一個比較松的上界。
- 空間複雜度: 快速排序佔用的遞歸棧空間。
題解二(排序 + 二分查找)
使用排序優化後依然無法滿足題目要求,我們發現:內層循環並不需要線性掃描,我們可以使用 二分查找尋找:
- 第一個大於等於 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
}
}
複雜度分析:
- 時間複雜度: 快速排序 + 組合的時間,內層循環中每次二分查找的時間是 。
- 空間複雜度: 快速排序佔用的遞歸棧空間。
2564. 子字符串異或查詢(Medium)
題目地址
https://leetcode.cn/problems/substring-xor-queries/
題目描述
給你一個 二進制字符串 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
}
}
複雜度分析:
- 時間複雜度:,其中 m 是
queries
數組的長度,n 是字符串的長度,在這道題中會超時。 - 空間複雜度:,不考慮結果數組。
題解二(滑動窗口 + 分桶預處理)
事實上,如果每次都單獨處理 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
}
}
複雜度分析:
- 時間複雜度:,其中 n 是字符串的長度, m 是
queries
數組的長度,L 是不同長度的窗口個數, 是預處理的時間。根據題目輸入滿足 可知 L 的最大值是 30。 - 空間複雜度:,散列表總共需要記錄 m 個查詢的映射關係。
題解三(滑動窗口 + 預處理字符串)
這道題的思路也是通過預處理過濾相同長度的滑動窗口,區別在於預處理的是輸入字符串,我們直接計算字符串 s
中所有可能出現的數字以及對應的 [left,right]
下標,再利用這份數據給予 queries
數組進行 打表查詢。
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
}
}
複雜度分析:
- 時間複雜度:,其中 n 是字符串的長度, m 是
queries
數組的長度,L 是不同長度的窗口個數。 是預處理的時間,根據題目輸入滿足 可知 L 的最大值是 30。 - 空間複雜度:,散列表總共需要記錄 nL 個數的映射關係。
2565. 最少得分子序列(Hard)
題目地址
https://leetcode.cn/problems/subsequence-with-the-minimum-score/
題目描述
給你兩個字符串 s
和 t
。
你可以從字符串 t
中刪除任意數目的字符。
如果沒有從字符串 t
中刪除字符,那麼得分爲 0
,否則:
- 令
left
爲刪除字符中的最小下標。 - 令
right
爲刪除字符中的最大下標。
字符串的得分爲 right - left + 1
。
請你返回使 t
成爲 s
子序列的最小得分。
一個字符串的 子序列 是從原字符串中刪除一些字符後(也可以一個也不刪除),剩餘字符不改變順序得到的字符串。(比方說 "ace"
是 "acde"
的子序列,但是 "aec"
不是)。
題解(前後綴分解)
這道題第一感覺是 LCS 最長公共子序列的衍生問題,我們可以使用樸素 LCS 模板求解字符串 s
和字符串 t
的最長公共子序列 ,再使用 t
字符串的長度減去公共部分長度得到需要刪除的字符個數。
然而,這道題目的輸出得分取決於最左邊被刪除的字符下標 和最右邊被刪除字符的下標 ,常規套路顯得無從下手。所以,我們嘗試對原問題進行轉換:
-
思考 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
的後綴,計算匹配後需要減去的子串長度,將所有枚舉方案的解取最小值就是原題目的解。
思路參考視頻講解:https://www.bilibili.com/video/BV1GY411i7RP/ —— 靈茶山艾府 著
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
}
}
複雜度分析:
- 時間複雜度:,其中 n 是字符串 s 的長度,預處理和枚舉的時間複雜度都是 。
- 空間複雜度:,前後綴數組的空間。
我們下週見,有用請讚賞上榜!想看小彭的更多題解代碼,可關注 Github:https://github.com/pengxurui/LeetCode-Kotlin/tree/main/leetcode