本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 提問。
大家好,我是小彭。
今天早上是 LeetCode 第 336 場周賽,你參加了嗎?這場周賽整體質量比較高,但是最後一題是老題,CV 能過。但是輸入資料範圍被降低了,這操作也是沒誰了。
https://leetcode.cn/problems/count-the-number-of-vowel-strings-in-range/
給你一個下標從 0 開始的字串陣列 words
和兩個整數:left
和 right
。
如果字串以母音字母開頭並以母音字母結尾,那麼該字串就是一個 母音字串 ,其中母音字母是 'a'
、'e'
、'i'
、'o'
、'u'
。
返回 words[i]
是母音字串的數目,其中 i
在閉區間 [left, right]
內。
簡單模擬題。
class Solution {
fun vowelStrings(words: Array<String>, left: Int, right: Int): Int {
val set = hashSetOf('a', 'e', 'i', 'o', 'u')
var count = 0
for (index in left..right) {
val word = words[index]
if (set.contains(word[0]) && set.contains(word[word.length - 1])) count++
}
return count
}
}
複雜度分析:
https://leetcode.cn/problems/rearrange-array-to-maximize-prefix-score/
給你一個下標從 0 開始的整數陣列 nums
。你可以將 nums
中的元素按 任意順序 重排(包括給定順序)。
令 prefix
為一個陣列,它包含了 nums
重新排列後的字首和。換句話說,prefix[i]
是 nums
重新排列後下標從 0
到 i
的元素之和。nums
的 分數 是 prefix
陣列中正整數的個數。
返回可以得到的最大分數。
貪心思路:負數會降低字首和,為了延緩字首和變小的速度,正權值應該放在儘可能前的位置,負權值放在儘可能後的位置,即對陣列降序排序。
class Solution {
fun maxScore(nums: IntArray): Int {
// 3 2 1 0 -1 -3 -3
// 3 5 6 6 5 2 -1
nums.sortDescending()
var preSum = 0L
for (index in nums.indices) {
preSum += nums[index]
if (preSum <= 0L) return index
}
return nums.size
}
}
複雜度分析:
https://leetcode.cn/problems/count-the-number-of-beautiful-subarrays/
給你一個下標從 0 開始的整數陣列nums
。每次操作中,你可以:
0 <= i, j < nums.length
的不同下標 i
和 j
。k
,滿足 nums[i]
和 nums[j]
在二進位制下的第 k
位(下標編號從 0 開始)是 1
。nums[i]
和 nums[j]
都減去 2k
。如果一個子陣列內執行上述操作若干次後,該子陣列可以變成一個全為 0
的陣列,那麼我們稱它是一個 美麗 的子陣列。
請你返回陣列 nums
中 美麗子陣列 的數目。
子陣列是一個陣列中一段連續 非空 的元素序列。
分析題目操作:當兩個數在某一位都是 1 時,可以執行一次消除操作。因此,在滿足題目要去的子陣列中,所有位上 1 出現的次數要麼是 0,要麼是大於 0 的偶數,符合互斥或的性質。於是,我們可以將題目轉換為求 「互斥或值為 0 的子陣列」 個數,與以下題目類似:
樸素的解法我們考慮列舉所有子陣列:
class Solution {
fun beautifulSubarrays(nums: IntArray): Long {
val n = nums.size
var count = 0L
for (left in 0 until n) {
var xorSum = 0
for (right in left until n) {
xorSum = xorSum xor nums[right]
if (xorSum == 0) count++
}
}
return count
}
}
複雜度分析:
「和為 k 的子陣列」 有 $O(n)$ 的解法:
class Solution {
fun beautifulSubarrays(nums: IntArray): Long {
val n = nums.size
var count = 0L
// xorSun - index
val xorSumMap = HashMap<Int, Int>().apply {
this[0] = 1
}
var preXorSum = 0
for (num in nums) {
preXorSum = preXorSum xor num
if (xorSumMap.containsKey(preXorSum)) {
count += xorSumMap[preXorSum]!!
}
xorSumMap[preXorSum] = xorSumMap.getOrDefault(preXorSum, 0) + 1
}
return count
}
}
複雜度分析:
https://leetcode.cn/problems/minimum-time-to-complete-all-tasks/
你有一臺電腦,它可以 同時 執行無數個任務。給你一個二維整數陣列 tasks
,其中 tasks[i] = [starti, endi, durationi]
表示第 i
個任務需要在 閉區間 時間段 [starti, endi]
內執行 durationi
個整數時間點(但不需要連續)。
當電腦需要執行任務時,你可以開啟電腦,如果空閒時,你可以將電腦關閉。
請你返回完成所有任務的情況下,電腦最少需要執行多少秒。
這道題其實是 LCP 原題:LCP 32. 批次處理任務
區間任務問題一般有按照 「開始時間」 排序或 「結束時間」 排序兩種排序方法:
另外,由於任務的開機時間允許不連續,所以我們需要用一個額外的陣列儲存開機時間。在處理每個任務時,我們先講已開始時間去掉,再將剩下的時間安排在儘可能晚的時間。
class Solution {
fun findMinimumTime(tasks: Array<IntArray>): Int {
// 按照結束時間排序
Arrays.sort(tasks) { e1, e2 ->
e1[1] - e2[1]
}
val used = BooleanArray(2001)
var time = 0
for (task in tasks) {
var count = task[2]
// 消除已開機時間
for (index in task[0]..task[1]) {
if (used[index]) count--
}
if (count <= 0) continue
time += count
// 推遲最晚開機時間
for (index in task[1] downTo task[0]) {
if (used[index]) continue
used[index] = true
if (--count == 0) break
}
}
return time
}
}
複雜度分析:
回顧題解一中的 2個關鍵操作:
因此,我們發現題目可能存線上段樹、樹狀陣列之類優化手段:類似的區間求和問題,我們先回顧一下解決方案:
這道題涉及 「區間更新」 和 「區間求和」,所以屬於線段樹的覆蓋範圍。相對於在函數中重複傳遞節點所代表的區間範圍(例如 update(i: int, l: int, r: int, L: int, R: int)
),使用 Node 節點記錄更為方便。
class Solution {
fun findMinimumTime(tasks: Array<IntArray>): Int {
// 按照結束時間排序
Arrays.sort(tasks) { e1, e2 ->
e1[1] - e2[1]
}
// 線段樹
val tree = SegmentTree(2001)
for (task in tasks) {
// 消除已開機時間
val count = task[2] - tree.query(task[0], task[1])
if (count <= 0) continue
// 推遲最晚開機時間
tree.update(task[0], task[1], count)
}
// 根節點即為所有開機時間
return tree.query(1, 2000)
}
private class SegmentTree(private val n: Int) {
// 線段樹節點(區間範圍與區間值)
private class Node(val left: Int, val right: Int, var value: Int)
// 線段樹陣列
private val tree = Array<Node?>(n * 4) { null } as Array<Node>
// 左子節點索引
private val Int.left get() = 2 * this + 1
// 右子節點索引
private val Int.right get() = 2 * this + 2
init {
// 建樹
buildNode(0, 0, n - 1)
}
private fun buildNode(index: Int, left: Int, right: Int) {
// 葉子節點
if (left == right) {
tree[index] = Node(left, right, 0)
return
}
val mid = (left + right) ushr 1
// 構建左子節點
buildNode(index.left, left, mid)
// 構建右子節點
buildNode(index.right, mid + 1, right)
// 合併左右子節點
tree[index] = Node(left, right, tree[index.left].value + tree[index.right].value)
}
// 開機(推遲到最晚時間)
fun update(left: Int, right: Int, count: Int) {
update(0, left, right, count)
}
// return:有效修改個數
private fun update(index: Int, left: Int, right: Int, count: Int): Int {
// 1、當前節點不處於區間內
if (tree[index].left > right || tree[index].right < left) return 0
// 2、葉子結點
if (tree[index].left == tree[index].right) {
// 開機
if (0 == tree[index].value) {
tree[index].value = 1
return 1
} else {
return 0
}
}
// 3、更新右子樹(貪心思路:推遲開機)
var realUpdate = update(index.right, left, right, count)
if (count - realUpdate > 0) {
// 4、更新左子樹
realUpdate += update(index.left, left, right, count - realUpdate)
}
// 5、合併左右子節點
tree[index].value = tree[index.left].value + tree[index.right].value
return realUpdate
}
// 查詢
fun query(left: Int, right: Int): Int {
return query(0, left, right)
}
private fun query(index: Int, left: Int, right: Int): Int {
// 1、當前節點不處於區間範圍內
if (tree[index].left > right || tree[index].right < left) return 0
// 2、當前節點完全處於區間範圍內
if (tree[index].left >= left && tree[index].right <= right) return tree[index].value
// 3、合併左右子節點
return query(index.left, left, right) + query(index.right, left, right)
}
}
}
複雜度分析:
樸素線段樹的效能瓶頸在於:區間更新需要改動從根節點到葉子節點中所有與目標區間有交集的節點,因此單次區間更新操作的時間複雜度是 $O(n)$。
懶更新線段樹的核心思想是:當一個節點代表的區間完全包含於目標區間內時,我們沒有必要繼續向下遞迴更新,而是在當前節點上標記 Lazy Tag 。只有將來更新該節點的某個子區間時,才會將懶更新 pushdown 到子區間。
class Solution {
fun findMinimumTime(tasks: Array<IntArray>): Int {
// 按照結束時間排序
Arrays.sort(tasks) { e1, e2 ->
e1[1] - e2[1]
}
// 線段樹
val tree = SegmentTree(2001)
for (task in tasks) {
// 消除已開機時間
val count = task[2] - tree.query(task[0], task[1])
if (count <= 0) continue
// 推遲最晚開機時間
tree.update(task[0], task[1], count)
}
// 根節點即為所有開機時間
return tree.query(1, 2000)
}
private class SegmentTree(private val n: Int) {
// 線段樹節點(區間範圍與區間值)
private class Node(val left: Int, val right: Int, var value: Int, var lazy: Boolean = false)
// 線段樹陣列
private val tree = Array<Node?>(n * 4) { null } as Array<Node>
// 左子節點索引
private val Int.left get() = 2 * this + 1
// 右子節點索引
private val Int.right get() = 2 * this + 2
init {
// 建樹
buildNode(0, 0, n - 1)
}
private fun buildNode(index: Int, left: Int, right: Int) {
// 葉子節點
if (left == right) {
tree[index] = Node(left, right, 0)
return
}
val mid = (left + right) ushr 1
// 構建左子節點
buildNode(index.left, left, mid)
// 構建右子節點
buildNode(index.right, mid + 1, right)
// 合併左右子節點
tree[index] = Node(left, right, tree[index.left].value + tree[index.right].value)
}
// 開機(推遲到最晚時間)
fun update(left: Int, right: Int, count: Int) {
update(0, left, right, count)
}
// return:有效修改個數
private fun update(index: Int, left: Int, right: Int, count: Int): Int {
// 1、當前節點不處於區間內
if (tree[index].left > right || tree[index].right < left) return 0
val size = tree[index].right - tree[index].left + 1
val unUsedSize = size - tree[index].value
if (unUsedSize == 0) return 0 // 整個區間已開機
// 2、當前節點完全處於區間範圍之內
if (tree[index].left >= left && tree[index].right <= right && unUsedSize <= count) {
// 整個區間可以改為開機
lazyUpdate(index)
return unUsedSize
}
// pushdown
if (tree[index].lazy) {
lazyUpdate(index.left)
lazyUpdate(index.right)
tree[index].lazy = false
}
// 3、更新右子樹(貪心思路:推遲開機)
var realUpdate = update(index.right, left, right, count)
if (count - realUpdate > 0) {
// 4、更新左子樹
realUpdate += update(index.left, left, right, count - realUpdate)
}
// 5、合併左右子節點
tree[index].value = tree[index.left].value + tree[index.right].value
return realUpdate
}
private fun lazyUpdate(index: Int) {
tree[index].value = tree[index].right - tree[index].left + 1
tree[index].lazy = true
}
// 查詢
fun query(left: Int, right: Int): Int {
return query(0, left, right)
}
private fun query(index: Int, left: Int, right: Int): Int {
// 1、當前節點不處於區間範圍內
if (tree[index].left > right || tree[index].right < left) return 0
// 2、當前節點完全處於區間範圍內
if (tree[index].left >= left && tree[index].right <= right) return tree[index].value
// pushdown
if (tree[index].lazy) {
lazyUpdate(index.left)
lazyUpdate(index.right)
tree[index].lazy = false
}
// 3、合併左右子節點
return query(index.left, left, right) + query(index.right, left, right)
}
}
}
複雜度分析: