⭐️ 本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 和 BaguTree Pro 知識星球提問。
學習資料結構與演演算法的關鍵在於掌握問題背後的演演算法思維框架,你的思考越抽象,它能覆蓋的問題域就越廣,理解難度也更復雜。在這個專欄裡,小彭與你分享每場 LeetCode 周賽的解題報告,一起體會上分之旅。
本文是 LeetCode 上分之旅系列的第 44 篇文章,往期回顧請移步到文章末尾~
T1. 統計對稱整數的數目(Easy)
T2. 生成特殊數位的最少操作(Medium)
T3. 統計趣味子陣列的數目(Medium)
T4. 邊權重均等查詢(Hard)
https://leetcode.cn/problems/count-symmetric-integers/
根據題意模擬,亦可以使用字首和預處理優化。
class Solution {
fun countSymmetricIntegers(low: Int, high: Int): Int {
var ret = 0
for (x in low..high) {
val s = "$x"
val n = s.length
if (n % 2 != 0) continue
var diff = 0
for (i in 0 until n / 2) {
diff += s[i] - '0'
diff -= s[n - 1 - i] - '0'
}
if (diff == 0) ret += 1
}
return ret
}
}
複雜度分析:
https://leetcode.cn/problems/minimum-operations-to-make-a-special-number/
思維題,這道卡了多少人。
可以用回溯解決:
class Solution {
fun minimumOperations(num: String): Int {
val memo = HashMap<String, Int>()
fun count(x: String): Int {
val n = x.length
if (n == 1) return if (x == "0") 0 else 1
if (((x[n - 2] - '0') * 10 + (x[n - 1]- '0')) % 25 == 0) return 0
if(memo.containsKey(x))return memo[x]!!
val builder1 = StringBuilder(x)
builder1.deleteCharAt(n - 1)
val builder2 = StringBuilder(x)
builder2.deleteCharAt(n - 2)
val ret = 1 + min(count(builder1.toString()), count(builder2.toString()))
memo[x]=ret
return ret
}
return count(num)
}
}
複雜度分析:
初步分析:
具體實現:
class Solution {
fun minimumOperations(num: String): Int {
val n = num.length
var ret = n
for (choice in arrayOf("00", "25", "50", "75")) {
// 雙指標
var j = 1
for (i in n - 1 downTo 0) {
if (choice[j] != num[i]) continue
if (--j == -1) {
ret = min(ret, n - i - 2)
break
}
}
}
// 特殊情況
ret = min(ret, n - num.count { it == '0'})
return ret
}
}
複雜度分析:
https://leetcode.cn/problems/count-of-interesting-subarrays/
初步分析:
分析到這裡,容易想到用字首和實現:
組合以上技巧:
class Solution {
fun countInterestingSubarrays(nums: List<Int>, m: Int, k: Int): Long {
val n = nums.size
var ret = 0L
val preSum = HashMap<Int, Int>()
preSum[0] = 1 // 注意空陣列的狀態
var cur = 0
for (i in 0 until n) {
if (nums[i] % m == k) cur ++ // 更新字首和
val key = cur % m
val target = (key - k + m) % m
ret += preSum.getOrDefault(target, 0) // 記錄方案
preSum[key] = preSum.getOrDefault(key, 0) + 1 // 記錄字首和
}
return ret
}
}
複雜度分析:
相似題目:
https://leetcode.cn/problems/minimum-edge-weight-equilibrium-queries-in-a-tree/
初步分析:
思考實現:
現在的關鍵問題是,如何快速地找到 $<x, y>$ 的最近公共祖先 LCA?
對於單次 LCA 操作來說,我們可以走 DFS 實現 $O(n)$ 時間複雜度的演演算法,而對於多次 LCA 操作可以使用 倍增演演算法 預處理以空間換時間,單次 LCA 操作的時間複雜度進位 $O(lgn)$。
在 LeetCode 有倍增的模板題 1483. 樹節點的第 K 個祖先。
在求 LCA 時,我們先把 $<x, y>$ 跳到相同高度,再利用倍增演演算法向上跳 $2^j$ 個父節點,直到到達相同節點即為最近公共祖先。
class Solution {
fun minOperationsQueries(n: Int, edges: Array<IntArray>, queries: Array<IntArray>): IntArray {
val U = 26
// 建圖
val graph = Array(n) { LinkedList<IntArray>() }
for (edge in edges) {
graph[edge[0]].add(intArrayOf(edge[1], edge[2] - 1))
graph[edge[1]].add(intArrayOf(edge[0], edge[2] - 1))
}
// 預處理深度、倍增祖先節點、倍增路徑資訊
val m = 32 - Integer.numberOfLeadingZeros(n - 1)
val depth = IntArray(n)
val parent = Array(n) { IntArray(m) { -1 }} // parent[i][j] 表示 i 的第 2^j 個父節點
val cnt = Array(n) { Array(m) { IntArray(U) }} // cnt[i][j] 表示 <i - 2^j> 個父節點的路徑資訊
fun dfs(i: Int, par: Int) {
for ((to, w) in graph[i]) {
if (to == par) continue // 避免迴環
depth[to] = depth[i] + 1
parent[to][0] = i
cnt[to][0][w] = 1
dfs(to, i)
}
}
dfs(0, -1) // 選擇 0 作為根節點
// 預處理倍增
for (j in 1 until m) {
for (i in 0 until n) {
val from = parent[i][j - 1]
if (-1 != from) {
parent[i][j] = parent[from][j - 1]
cnt[i][j] = cnt[i][j - 1].zip(cnt[from][j - 1]) { e1, e2 -> e1 + e2 }.toIntArray()
}
}
}
// 查詢
val q = queries.size
val ret = IntArray(q)
for ((i, query) in queries.withIndex()) {
var (x, y) = query
// 特判
if (x == y || parent[x][0] == y || parent[y][0] == x) {
ret[i] = 0
}
val w = IntArray(U) // 記錄路徑資訊
var path = depth[x] + depth[y] // 記錄路徑長度
// 先跳到相同高度
if (depth[y] > depth[x]) {
val temp = x
x = y
y = temp
}
var k = depth[x] - depth[y]
while (k > 0) {
val j = Integer.numberOfTrailingZeros(k) // 二進位制分解
w.indices.forEach { w[it] += cnt[x][j][it] } // 記錄路徑資訊
x = parent[x][j] // 向上跳 2^j 個父節點
k = k and (k - 1)
}
// 再使用倍增找 LCA
if (x != y) {
for (j in m - 1 downTo 0) { // 最多跳 m - 1 次
if (parent[x][j] == parent[y][j]) continue // 跳上去相同就不跳
w.indices.forEach { w[it] += cnt[x][j][it] } // 記錄路徑資訊
w.indices.forEach { w[it] += cnt[y][j][it] } // 記錄路徑資訊
x = parent[x][j]
y = parent[y][j] // 向上跳 2^j 個父節點
}
// 最後再跳一次就是 lca
w.indices.forEach { w[it] += cnt[x][0][it] } // 記錄路徑資訊
w.indices.forEach { w[it] += cnt[y][0][it] } // 記錄路徑資訊
x = parent[x][0]
}
// 減去重鏈長度
ret[i] = path - 2 * depth[x] - w.max()
}
return ret
}
}
複雜度分析:
推薦閱讀
LeetCode 上分之旅系列往期回顧:
⭐️ 永遠相信美好的事情即將發生,歡迎加入小彭的 Android 交流社群~