本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 提問。
大家好,我是小彭。
上週末是 LeetCode 第 339 場周賽,你參加了嗎?這場周賽覆蓋的知識點比較少,前三題很簡單,第四題上難度。
2609. 最長平衡子字串(Easy)
2610. 轉換二維陣列(Medium)
2611. 老鼠和乳酪(Medium)
2612. 最少翻轉運算元(Hard)
https://leetcode.cn/problems/find-the-longest-balanced-substring-of-a-binary-string/
給你一個僅由 0
和 1
組成的二進位制字串 s
。
如果子字串中 所有的 0
都在 1
之前 且其中 0
的數量等於 1
的數量,則認為 s
的這個子字串是平衡子字串。請注意,空子字串也視作平衡子字串。
返回 s
中最長的平衡子字串長度。
子字串是字串中的一個連續字元序列。
簡單模擬題。
維護連續 0 的計數 cnt0
和連續 1 的計數 cnt1
,並在 cnt0 == cnt1
時更新最長平衡子串長度為 2 * cnt1
。另外,在每段 0 的起始位置重新計數。
class Solution {
fun findTheLongestBalancedSubstring(s: String): Int {
var index = 0
var cnt0 = 0
var cnt1 = 0
var ret = 0
while (index < s.length) {
if (s[index] == '0') {
// 每段 0 的起始位置清零
if (index > 0 && s[index - 1] == '1') {
cnt0 = 0
cnt1 = 0
}
cnt0++
} else {
cnt1++
}
if (cnt1 <= cnt0) ret = Math.max(ret, cnt1 * 2)
index++
}
return ret
}
}
複雜度分析:
https://leetcode.cn/problems/convert-an-array-into-a-2d-array-with-conditions/
給你一個整數陣列 nums
。請你建立一個滿足以下條件的二維陣列:
nums
中的元素。返回結果陣列。如果存在多種答案,則返回其中任何一種。
請注意,二維陣列的每一行上可以存在不同數量的元素。
貪心思路:首先計算每個元素的出現次數,為了避免同一行的重複,將重複元素從上到下排列到不同行中。
優化:可以在一次遍歷中完成,在出現更大出現次數時增加一行,在更新元素技術 cnt 後插入到第 cnt - 1 行。
class Solution {
fun findMatrix(nums: IntArray): List<List<Int>> {
val cnts = IntArray(201)
val ret = LinkedList<LinkedList<Int>>()
var maxCnt = 0
// 計數
for (num in nums) {
// 累加
val curCnt = ++cnts[num]
// 建立新行
if (curCnt > maxCnt) {
maxCnt = curCnt
ret.add(LinkedList<Int>())
}
// 分佈
ret[curCnt - 1].add(num)
}
return ret
}
}
複雜度分析:
https://leetcode.cn/problems/mice-and-cheese/
有兩隻老鼠和 n
塊不同型別的乳酪,每塊乳酪都只能被其中一隻老鼠吃掉。
下標為 i
處的乳酪被吃掉的得分為:
reward1[i]
。reward2[i]
。給你一個正整數陣列 reward1
,一個正整數陣列 reward2
,和一個非負整數 k
。
請你返回第一隻老鼠恰好吃掉 k
塊乳酪的情況下,最大 得分為多少。
容易理解:為了使最終得分最大,應該讓每隻老鼠吃到儘可能大的乳酪。
由於兩隻老鼠吃的乳酪是互斥關係,因此我們可以先假設所有乳酪被第一隻老鼠食得,然後再挑選 n - k
個乳酪還給第二隻老鼠。
那麼,對於每個位置 i
,將乳酪從第一隻老鼠還給第二隻老鼠存在差值 diff = reward2[i] - reward1[i]
,表示得分的差值為 diff
。差值為正得分變大,差值為負得分降低,顯然降低越少越好。
因此,我們的演演算法是對 diff
排序,將得分降低越大的位置保留給第一隻老鼠,其他還給第二隻老鼠。
class Solution {
fun miceAndCheese(reward1: IntArray, reward2: IntArray, k: Int): Int {
// 貪心:優先選擇差值最大的位置
val n = reward1.size
var ret = 0
val indexs = Array(n) { it }
// 升序
Arrays.sort(indexs) { i1, i2 ->
(reward2[i1] - reward1[i1]) - (reward2[i2] - reward1[i2])
}
for (i in 0 until n) {
ret += if (i < k) {
reward1[indexs[i]]
} else {
reward2[indexs[i]]
}
}
return ret
}
}
複雜度分析:
https://leetcode.cn/problems/minimum-reverse-operations/
給你一個整數 n
和一個在範圍 [0, n - 1]
以內的整數 p
,它們表示一個長度為 n
且下標從 0 開始的陣列 arr
,陣列中除了下標為 p
處是 1
以外,其他所有數都是 0
。
同時給你一個整數陣列 banned
,它包含陣列中的一些位置。banned
中第 i 個位置表示 arr[banned[i]] = 0
,題目保證 banned[i] != p
。
你可以對 arr
進行 若干次 操作。一次操作中,你選擇大小為 k
的一個 子陣列 ,並將它 翻轉 。在任何一次翻轉操作後,你都需要確保 arr
中唯一的 1
不會到達任何 banned
中的位置。換句話說,arr[banned[i]]
始終 保持 0
。
請你返回一個陣列 ans
,對於 **[0, n - 1]
之間的任意下標 i
,ans[i]
是將 1
放到位置 i
處的 最少 翻轉操作次數,如果無法放到位置 i
處,此數為 -1
。
i
,ans[i]
相互之間獨立計算。分析 1:對於翻轉視窗 [L, R] 中的位置 i,翻轉後的下標為 $\frac{L+R}{2} + (\frac{L+R}{2} - i) = L + R - i$
分析 2:首先位置 p
的翻轉次數恆等於 0,而 banned
陣列表示的位置翻轉次數恆等於 -1。
分析 3:當位置 i
位於翻轉視窗的左半部分時,將翻轉到更大位置;當位置 i
位於翻轉視窗的右半部分時,將翻轉到更小位置;
分析 4:現在我們需要分析位置 i
(初始 i 為 0 )可以翻轉到的位置:
i
作為翻轉視窗的左右邊界,則有:
i + k - 1
;i - k + 1
。2
的等差數列。因此,i 可以翻轉的區間為 [i - k + 1, i + k - 1] 中間隔 2 的位置(排除 banned 陣列),或者理解為奇偶性相同的下標。
分析 5:由於翻轉視窗有位置限制,會限制翻轉:
0
時,且 i
位於翻轉視窗的右半部分時(準備向左翻),則翻轉後的位置是 0 + (k - 1) - i = k - 1 - i
。由於視窗無法繼續左移,所以小於 k - i - 1
的位置都不可達;n - 1
時,且 i
位於翻轉視窗的左邊部分時(準備向右翻),則翻轉後的位置是 (n - k) + (n - 1) - i = 2n - k - i - 1
。由於視窗無法繼續右移,所以大於 2n - k - i - 1
的位置都不可達。綜上,可得翻轉後區間為 [max(i - k + 1, k - i - 1), min(i + k - 1, 2n - k - i - 1)] 中與 i 奇偶性相同的位置。
至此,容易發現問題可以用拓撲排序(BFS 寫法)解決:初始時將 p 位置入隊,隨後每一輪的翻轉次數 + 1,並將該位置入隊。
class Solution {
fun minReverseOperations(n: Int, p: Int, banned: IntArray, k: Int): IntArray {
val ret = IntArray(n) { -1 }
// 初始位
ret[p] = 0
// 禁止位
val bannedSet = banned.toHashSet()
// BFS(最小跳轉索引)
val queue = LinkedList<Int>()
queue.offer(p)
while (!queue.isEmpty()) {
val i = queue.poll()!!
val min = Math.max(i - k + 1, k - i - 1)
val max = Math.min(i + k - 1, 2 * n - k - i - 1)
val curStep = ret[i] + 1
for (j in min..max step 2) {
// 不可達
if (bannedSet.contains(j)) continue
// 已存取
if (ret[j] != -1) continue
// 可達
ret[j] = curStep
// 入隊
queue.offer(j)
}
}
return ret
}
}
複雜度分析:
在題解一中,當 k
比較大時每輪 BFS 中會重複判斷已經被標記過的位置,如何避免呢?我們可以提前將所有下標加入到雜湊表中,在每次標記後將下標從雜湊表移除,這樣能避免重複存取已經標記過的位置。
其次,由於每輪中需要標記的區間位於 [min, max]
,那麼我們可以將雜湊表升級為基於平衡二元樹的 TreeSet,以便在 O(lgn) 時間內找到區間中的元素。具體方式是尋找樹中大於等於 min
的最小元素(且小於等於 max
),將其標記和移除。
最後,由於偶數下標和奇數下標是分開的,所以需要建立兩個平衡二元樹。
class Solution {
fun minReverseOperations(n: Int, p: Int, banned: IntArray, k: Int): IntArray {
val ret = IntArray(n) { -1 }
// 初始位
ret[p] = 0
// 禁止位
val bannedSet = banned.toHashSet()
// 平衡二元樹
val sets = Array(2) { TreeSet<Int>() }
for (i in 0 until n) {
if (i != p && !bannedSet.contains(i)) sets[i % 2].add(i)
}
// BFS(最小跳轉索引)
val queue = LinkedList<Int>()
queue.offer(p)
while (!queue.isEmpty()) {
val i = queue.poll()!!
val min = Math.max(i - k + 1, k - i - 1)
val max = Math.min(i + k - 1, 2 * n - k - i - 1)
val curStep = ret[i] + 1
// 根據左端點確定奇偶性(右端點也行)
val set = sets[min % 2]
// 列舉平衡樹中的 [min,max] 區間
while (true) {
val index = set.ceiling(min) ?: break // 大於等於 min 的最小鍵值
if (index > max) break
// 標記並刪除
set.remove(index)
ret[index] = curStep
// 入隊
queue.offer(index)
}
}
return ret
}
}
複雜度分析:
點選上方按鈕關注
每週持續原創更新
與你一起深度思考
The End
—— 我 們 下 次 見 ——