本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 提問。
大家好,我是小彭。
今天下午有力扣杯戰隊賽,不知道官方是不是故意調低早上週賽難度給選手們練練手。
T1. 找出不同元素數目差陣列(Easy)
標籤:模擬、計數、雜湊表
T2. 頻率跟蹤器(Medium)
標籤:模擬、計數、雜湊表、設計
T3. 有相同顏色的相鄰元素數目(Medium)
標籤:模擬、計數、貪心
T4. 使二元樹所有路徑值相等的最小代價(Medium)
標籤:二元樹、DFS、貪心
https://leetcode.cn/problems/find-the-distinct-difference-array/
寫法 1:
class Solution {
fun distinctDifferenceArray(nums: IntArray): IntArray {
val n = nums.size
val ret = IntArray(n)
val leftCnts = HashMap<Int, Int>()
val rightCnts = HashMap<Int, Int>()
for (e in nums) {
rightCnts[e] = rightCnts.getOrDefault(e, 0) + 1
}
for (i in nums.indices) {
val e = nums[i]
leftCnts[e] = leftCnts.getOrDefault(e, 0) + 1
if (rightCnts[e]!! > 1) rightCnts[e] = rightCnts[e]!! - 1 else rightCnts.remove(e)
ret[i] = leftCnts.size - rightCnts.size
}
return ret
}
}
寫法 2:
class Solution {
fun distinctDifferenceArray(nums: IntArray): IntArray {
val n = nums.size
val ret = IntArray(n)
val set = HashSet<Int>()
// 字尾
for (i in nums.size - 1 downTo 0) {
ret[i] = -set.size
set.add(nums[i])
}
set.clear()
// 字首
for (i in nums.indices) {
set.add(nums[i])
ret[i] += set.size
}
return ret
}
}
複雜度分析:
https://leetcode.cn/problems/frequency-tracker/
簡單設計題,使用一個雜湊表記錄數位出現次數,再使用另一個雜湊表記錄出現次數的出現次數:
class FrequencyTracker() {
// 計數
private val cnts = HashMap<Int, Int>()
// 頻率計數
private val freqs = HashMap<Int, Int>()
fun add(number: Int) {
// 舊計數
val oldCnt = cnts.getOrDefault(number, 0)
// 增加計數
cnts[number] = oldCnt + 1
// 減少舊頻率計數
if (freqs.getOrDefault(oldCnt, 0) > 0) // 容錯
freqs[oldCnt] = freqs[oldCnt]!! - 1
// 增加新頻率計數
freqs[oldCnt + 1] = freqs.getOrDefault(oldCnt + 1, 0) + 1
}
fun deleteOne(number: Int) {
// 未包含
if (!cnts.contains(number)) return
// 減少計數
val oldCnt = cnts[number]!!
if (0 == oldCnt - 1) cnts.remove(number) else cnts[number] = oldCnt - 1
// 減少舊頻率計數
freqs[oldCnt] = freqs.getOrDefault(oldCnt, 0) - 1
// 增加新頻率計數
freqs[oldCnt - 1] = freqs.getOrDefault(oldCnt - 1, 0) + 1
}
fun hasFrequency(frequency: Int): Boolean {
// O(1)
return freqs.getOrDefault(frequency, 0) > 0
}
}
複雜度分析:
https://leetcode.cn/problems/number-of-adjacent-elements-with-the-same-color/description/
給你一個下標從 0 開始、長度為 n
的陣列 nums
。一開始,所有元素都是 未染色 (值為 0
)的。
給你一個二維整數陣列 queries
,其中 queries[i] = [indexi, colori]
。
對於每個操作,你需要將陣列 nums
中下標為 indexi
的格子染色為 colori
。
請你返回一個長度與 queries
相等的陣列 **answer
**,其中 **answer[i]
是前 i
個操作 之後 ,相鄰元素顏色相同的數目。
更正式的,answer[i]
是執行完前 i
個操作後,0 <= j < n - 1
的下標 j
中,滿足 nums[j] == nums[j + 1]
且 nums[j] != 0
的數目。
範例 1:
輸入:n = 4, queries = [[0,2],[1,2],[3,1],[1,1],[2,1]]
輸出:[0,1,1,0,2]
解釋:一開始陣列 nums = [0,0,0,0] ,0 表示陣列中還沒染色的元素。
- 第 1 個操作後,nums = [2,0,0,0] 。相鄰元素顏色相同的數目為 0 。
- 第 2 個操作後,nums = [2,2,0,0] 。相鄰元素顏色相同的數目為 1 。
- 第 3 個操作後,nums = [2,2,0,1] 。相鄰元素顏色相同的數目為 1 。
- 第 4 個操作後,nums = [2,1,0,1] 。相鄰元素顏色相同的數目為 0 。
- 第 5 個操作後,nums = [2,1,1,1] 。相鄰元素顏色相同的數目為 2 。
範例 2:
輸入:n = 1, queries = [[0,100000]]
輸出:[0]
解釋:一開始陣列 nums = [0] ,0 表示陣列中還沒染色的元素。
- 第 1 個操作後,nums = [100000] 。相鄰元素顏色相同的數目為 0 。
提示:
1 <= n <= 105
1 <= queries.length <= 105
queries[i].length == 2
0 <= indexi <= n - 1
1 <= colori <= 105
1、概括問題目標
計算每次塗色後相鄰顏色的數目個數(與前一個位置顏色相同)。
2、觀察問題資料
3、具體化解決手段
class Solution {
fun colorTheArray(n: Int, queries: Array<IntArray>): IntArray {
// 只觀察 (i - 1, i) 與 (i, i + 1) 兩個數對
if (n <= 0) return intArrayOf(0) // 容錯
val colors = IntArray(n)
val ret = IntArray(queries.size)
for (i in queries.indices) {
val j = queries[i][0]
val color = queries[i][1]
if (j < 0 || j >= n) continue // 容錯
colors[j] = color
for (j in 1 until n) {
if (0 != colors[j] && colors[j] == colors[j - 1]) ret[i] ++
}
}
return ret
}
}
複雜度分析:
class Solution {
fun colorTheArray(n: Int, queries: Array<IntArray>): IntArray {
// 只觀察 (i - 1, i) 與 (i, i + 1) 兩個數對
if (n <= 0) return intArrayOf(0) // 容錯
val colors = IntArray(n)
val ret = IntArray(queries.size)
// 計數
var cnt = 0
for (i in queries.indices) {
val j = queries[i][0]
val color = queries[i][1]
if (j < 0 || j >= n) continue // 容錯
// 消除舊顏色的影響
if (colors[j] != 0 && j > 0 && colors[j - 1] == colors[j]) cnt--
// 增加新顏色的影響
if (colors[j] != 0 && j < n - 1 && colors[j] == colors[j + 1]) cnt--
if (color != 0) { // 容錯
colors[j] = color
if (j > 0 && colors[j - 1] == colors[j]) cnt++
if (j < n - 1 && colors[j] == colors[j + 1]) cnt++
}
ret[i] = cnt
}
return ret
}
}
複雜度分析:
相似題目:
https://leetcode.cn/problems/make-costs-of-paths-equal-in-a-binary-tree/
給你一個整數 n
表示一棵 滿二元樹 裡面節點的數目,節點編號從 1
到 n
。根節點編號為 1
,樹中每個非葉子節點 i
都有兩個孩子,分別是左孩子 2 * i
和右孩子 2 * i + 1
。
樹中每個節點都有一個值,用下標從 0 開始、長度為 n
的整數陣列 cost
表示,其中 cost[i]
是第 i + 1
個節點的值。每次操作,你可以將樹中 任意 節點的值 增加 1
。你可以執行操作 任意 次。
你的目標是讓根到每一個 葉子結點 的路徑值相等。請你返回 最少 需要執行增加操作多少次。
注意:
範例 1:
輸入:n = 7, cost = [1,5,2,2,3,3,1]
輸出:6
解釋:我們執行以下的增加操作:
- 將節點 4 的值增加一次。
- 將節點 3 的值增加三次。
- 將節點 7 的值增加兩次。
從根到葉子的每一條路徑值都為 9 。
總共增加次數為 1 + 3 + 2 = 6 。
這是最小的答案。
範例 2:
輸入:n = 3, cost = [5,3,3]
輸出:0
解釋:兩條路徑已經有相等的路徑值,所以不需要執行任何增加操作。
提示:
3 <= n <= 105
n + 1
是 2
的冪cost.length == n
1 <= cost[i] <= 104
1、概括問題目標
計算將所有「根到葉子結點的路徑和」調整到相同值的操作次數。
2、分析問題要件
在每一次操作中,可以提高二元樹中某個節點的數值,最終使得該路徑和與所有二元樹中其他所有路徑和相同。
3、觀察問題資料
4、提高抽象程度
5、具體化解決方案
如何解決問題?
結合「公共路徑」思考,由於從根節點走到節點「2」的路徑和對於兩個子節點的影響是相同的,因此對於節點「2」來說,不需要關心父節點的路徑和,只需要保證以節點「2」為根節點的子樹上所有路徑和是相同的。這是一個規模更小的相似子問題,可以用遞迴解決。
示意圖
如何實現遞迴函數?
至此,我們的遞迴函數框架確定:
全域性變數 int ret
// 返回值:調整後的子樹和
fun dfs (i) : Int {
val sumL = dfs(L)
val sumR = dfs(R)
ret += max(sumL, sumR) - min(sumL, sumR)
return cost[i] + max(sumL, sumR)
}
6、是否有優化空間
我們使用遞迴自頂向下地分解子問題,再自底向上地求解原問題。由於這道題的輸入是陣列形式的滿二元樹,對於陣列實現的二元樹我們可以直接地從子節點返回到父節點,而不需要藉助「遞迴棧」後進先出的邏輯,可以翻譯為迭代來優化空間。
7、答疑
雖然我們保證子樹上的子路徑是相同的,但是如何保證最終所有子路徑都和「最大路徑和」相同?
由於我們不斷地將左右子樹的路徑和向較大的路徑和對齊,因此最終一定會將所有路徑對齊到最大路徑和。
為什麼演演算法的操作次數是最少的?
首先,由於左右子樹存在「公共路徑」,因此必須把左右子樹的子路徑和調整到相同數值,才能保證最終所有子路徑和的長度相同。
其次,當在大規模子樹中需要增大路徑和時,在父節點操作可以同時作用於左右子路徑,因此在父節點操作可以節省操作次數,每個子樹只關心影響當前子樹問題合法性的因素。
根據「問題結構化」分析的遞迴虛擬碼實現:
class Solution {
private var ret = 0
fun minIncrements(n: Int, cost: IntArray): Int {
dfs(n, cost, 1)
return ret
}
// i : base 1
// cost : base 0
// return: 調整後的子路徑和
private fun dfs(n: Int, cost: IntArray, i: Int): Int {
// 終止條件
if (i > n / 2) return cost[i - 1] // 最後一層是葉子節點
// 子問題
val leftSum = dfs(n, cost, i * 2)
val rightSum = dfs(n, cost, i * 2 + 1)
// 向較大的子路徑對齊
ret += Math.max(leftSum, rightSum) - Math.min(leftSum, rightSum)
return cost[i - 1] + Math.max(leftSum, rightSum)
}
}
複雜度分析:
由於輸入資料是滿二元樹,而且是以陣列的形式提供,因此我們可以跳過遞迴分解子問題的過程,直接自底向上合併子問題:
class Solution {
fun minIncrements(n: Int, cost: IntArray): Int {
var ret = 0
// 從葉子的上一層開始
for (i in n / 2 downTo 1) {
ret += Math.abs(cost[i * 2 - 1] - cost[i * 2])
// 藉助 cost 陣列記錄子樹的子路徑和
cost[i - 1] += Math.max(cost[i * 2 - 1], cost[i * 2])
}
return ret
}
}
複雜度分析:
往期回顧