LeetCode 周賽上分之旅 #44 同余前綴和問題與經(jīng)典倍增 LCA 算法

T1. 統(tǒng)計(jì)對稱整數(shù)的數(shù)目(Easy)

  • 標(biāo)簽:模擬

T2. 生成特殊數(shù)字的最少操作(Medium)

  • 標(biāo)簽:思維、回溯硼端、雙指針

T3. 統(tǒng)計(jì)趣味子數(shù)組的數(shù)目(Medium)

  • 標(biāo)簽:同余定理萧芙、前綴和给梅、散列表

T4. 邊權(quán)重均等查詢(Hard)

  • 標(biāo)簽:圖、倍增双揪、LCA动羽、樹上差分

T1. 統(tǒng)計(jì)對稱整數(shù)的數(shù)目(Easy)

https://leetcode.cn/problems/count-symmetric-integers/

題解(模擬)

根據(jù)題意模擬,亦可以使用前綴和預(yù)處理優(yōu)化渔期。

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
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O((high-low)lg^{high}) 單次檢查時(shí)間為 O(lg^{high})运吓;
  • 空間復(fù)雜度:O(1) 僅使用常量級別空間渴邦。

T2. 生成特殊數(shù)字的最少操作(Easy)

https://leetcode.cn/problems/minimum-operations-to-make-a-special-number/

題解一(回溯)

思維題,這道卡了多少人拘哨。

  • 閱讀理解: 在一次操作中谋梭,您可以選擇 num 的任意一位數(shù)字并將其刪除,求最少需要多少次操作可以使 num 變成 25 的倍數(shù)倦青;
  • 規(guī)律: 對于 25 的倍數(shù)瓮床,當(dāng)且僅當(dāng)結(jié)尾為「00、25姨夹、50纤垂、75」這 4 種情況時(shí)成立,我們嘗試構(gòu)造出尾部符合兩個(gè)數(shù)字能被 25 整除的情況磷账。

可以用回溯解決:

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)
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n^2·m) 最多有 n^2 種子狀態(tài)峭沦,其中 m 是字符串的平均長度,O(m) 是構(gòu)造中間字符串的時(shí)間逃糟;
  • 空間復(fù)雜度:O(n) 回溯遞歸椇鹩悖空間。

題解二(雙指針)

初步分析:

  • 模擬: 事實(shí)上绰咽,問題的方案最多只有 4 種菇肃,回溯的中間過程事實(shí)在嘗試很多無意義的方案。我們直接枚舉這 4 種方案取募,刪除尾部不屬于該方案的字符琐谤。以 25 為例,就是刪除 5 后面的字符以及刪除 2 與 5 中間的字符玩敏;
  • 抽象: 本質(zhì)上是一個(gè)最短匹配子序列的問題斗忌,即 「找到 nums 中最靠后的匹配的最短子序列」問題,可以用雙指針模擬旺聚。

具體實(shí)現(xiàn):

  • 雙指針: 我們找到滿足條件的最靠左的下標(biāo) i织阳,并刪除末尾除了目標(biāo)數(shù)字外的整段元素,即 ret = n - i - 2砰粹;
  • 特殊情況: 在 4 種構(gòu)造合法的特殊數(shù)字外唧躲,還存在刪除所有非 0 數(shù)字后構(gòu)造出 0 的方案;
  • 是否要驗(yàn)證數(shù)據(jù)含有前導(dǎo)零: 對于構(gòu)造「00」的情況碱璃,是否會(huì)存在刪到最后剩下多個(gè) 0 的情況呢弄痹?其實(shí)是不存在的。因?yàn)轭}目說明輸入數(shù)據(jù) num 本身是不包含前導(dǎo)零的厘贼,如果最后剩下多個(gè) 0 界酒,那么在最左邊的 0 左側(cè)一定存在非 0 數(shù)字,否則與題目說明矛盾嘴秸。
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
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n) 4 種方案和特殊方案均是線性遍歷;
  • 空間復(fù)雜度:O(1) 僅使用常量級別空間。

T3. 統(tǒng)計(jì)趣味子數(shù)組的數(shù)目(Medium)

https://leetcode.cn/problems/count-of-interesting-subarrays/

題解(同余 + 前綴和 + 散列表)

初步分析:

  • 問題目標(biāo): 統(tǒng)計(jì)數(shù)組中滿足目標(biāo)條件的子數(shù)組岳掐;
  • 目標(biāo)條件: 在子數(shù)組范圍 [l, r] 內(nèi)凭疮,設(shè) cnt 為滿足 nums[i] % m == k 的索引 i 的數(shù)量,并且 cnt % m == k串述。大白話就是算一下有多少數(shù)的模是 k执解,再判斷個(gè)數(shù)的模是不是也是 k
  • 權(quán)重: 對于滿足 nums[i] % m == k 的元素纲酗,它對結(jié)果的貢獻(xiàn)是 1衰腌,否則是 0

分析到這里觅赊,容易想到用前綴和實(shí)現(xiàn):

  • 前綴和: 記錄從起點(diǎn)到 [i] 位置的 [0, i] 區(qū)間范圍內(nèi)滿足目標(biāo)的權(quán)重?cái)?shù)右蕊;
  • 兩數(shù)之和: 從左到右枚舉 [i],并尋找已經(jīng)遍歷的位置中滿足 (preSum[i] - preSum[j]) \% m == k 的方案數(shù)記入結(jié)果吮螺;
  • 公式轉(zhuǎn)換: 上式帶有取模運(yùn)算饶囚,我們需要轉(zhuǎn)換一下:
    • 原式 (preSum[i] - preSum[j]) \% m == k
    • 考慮 preSum[i] \% m - preSum[j] \% m 是正數(shù)數(shù)的的情況,原式等價(jià)于:preSum[i] \% m - preSum[j] \% m == k
    • 考慮 preSum[i] \% m - preSum[j] \% m 是負(fù)數(shù)的的情況鸠补,我們在等式左邊增加補(bǔ)數(shù):(preSum[i] \% m - preSum[j] \% m + m) %m == k
    • 聯(lián)合正數(shù)和負(fù)數(shù)兩種情況萝风,即我們需要找到前綴和為 (preSum[i] \% m - k + m) \% m 的元素;
  • 修正前綴和定義: 最后紫岩,我們修改前綴和的定義為權(quán)重 \% m规惰。

組合以上技巧:

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 // 注意空數(shù)組的狀態(tài)
        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
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(n) 線性遍歷,單次查詢時(shí)間為 O(1)泉蝌;
  • 空間復(fù)雜度:O(m) 散列表空間歇万。

相似題目:


T4. 邊權(quán)重均等查詢(Hard)

https://leetcode.cn/problems/minimum-edge-weight-equilibrium-queries-in-a-tree/

題解(倍增求 LCA、樹上差分)

初步分析:

  • 問題目標(biāo): 給定若干個(gè)查詢 [x, y]梨与,要求計(jì)算將 <x, y> 的路徑上的每條邊修改為相同權(quán)重的最少操作次數(shù)堕花;
  • 問題要件: 對于每個(gè)查詢 [x, y],我們需要計(jì)算 <x, y> 的路徑長度 l粥鞋,以及邊權(quán)重的眾數(shù)的出現(xiàn)次數(shù) c缘挽,而要修改的操作次數(shù)就是 l - c
  • 技巧: 對于 “樹上路徑” 問題有一種經(jīng)典技巧呻粹,我們可以把 <x, y> 的路徑轉(zhuǎn)換為從 <x, lca> 的路徑與 <lca, y> 的兩條路徑壕曼;

思考實(shí)現(xiàn):

  • 長度: 將問題轉(zhuǎn)換為經(jīng)過 lca 中轉(zhuǎn)的路徑后,路徑長度 l 可以用深度來計(jì)算:l = depth[x] + depth[y] - 2 * depth[lca]等浊;
  • 權(quán)重: 同理腮郊,權(quán)重 w[x,y] 可以通過 w[x, lca]w[lca, y] 累加計(jì)算;

現(xiàn)在的關(guān)鍵問題是筹燕,如何快速地找到 <x, y> 的最近公共祖先 LCA轧飞?

對于單次 LCA 操作來說衅鹿,我們可以走 DFS 實(shí)現(xiàn) O(n) 時(shí)間復(fù)雜度的算法,而對于多次 LCA 操作可以使用 倍增算法 預(yù)處理以空間換時(shí)間过咬,單次 LCA 操作的時(shí)間復(fù)雜度進(jìn)位 O(lgn)大渤。

在 LeetCode 有倍增的模板題 1483. 樹節(jié)點(diǎn)的第 K 個(gè)祖先

在求 LCA 時(shí)掸绞,我們先把 <x, y> 跳到相同高度泵三,再利用倍增算法向上跳 2^j 個(gè)父節(jié)點(diǎn),直到到達(dá)相同節(jié)點(diǎn)即為最近公共祖先衔掸。

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))
        }

        // 預(yù)處理深度烫幕、倍增祖先節(jié)點(diǎn)、倍增路徑信息
        val m = 32 - Integer.numberOfLeadingZeros(n - 1)
        val depth = IntArray(n)
        val parent = Array(n) { IntArray(m) { -1 }} // parent[i][j] 表示 i 的第 2^j 個(gè)父節(jié)點(diǎn)
        val cnt = Array(n) { Array(m) { IntArray(U) }} // cnt[i][j] 表示 <i - 2^j> 個(gè)父節(jié)點(diǎn)的路徑信息
        
        fun dfs(i: Int, par: Int) {
            for ((to, w) in graph[i]) {
                if (to == par) continue // 避免回環(huán)
                depth[to] = depth[i] + 1
                parent[to][0] = i
                cnt[to][0][w] = 1
                dfs(to, i)
            }
        }

        dfs(0, -1) // 選擇 0 作為根節(jié)點(diǎn)

        // 預(yù)處理倍增
        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) // 二進(jìn)制分解
                w.indices.forEach { w[it] += cnt[x][j][it] } // 記錄路徑信息
                x = parent[x][j] // 向上跳 2^j 個(gè)父節(jié)點(diǎn)
                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 個(gè)父節(jié)點(diǎn)
                }
                // 最后再跳一次就是 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
    }
}

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(nlgn·U) 預(yù)處理的時(shí)間復(fù)雜度是 O(nlgn·U)敞映,單次查詢的時(shí)間是 O(lgn·U)较曼;
  • 空間復(fù)雜度:O(nlgn·U) 預(yù)處理倍增信息空間。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末驱显,一起剝皮案震驚了整個(gè)濱河市诗芜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌埃疫,老刑警劉巖伏恐,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異栓霜,居然都是意外死亡翠桦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門胳蛮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來销凑,“玉大人,你說我怎么就攤上這事仅炊《酚祝” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵抚垄,是天一觀的道長蜕窿。 經(jīng)常有香客問我,道長呆馁,這世上最難降的妖魔是什么桐经? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮浙滤,結(jié)果婚禮上阴挣,老公的妹妹穿的比我還像新娘。我一直安慰自己纺腊,他們只是感情好畔咧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布茎芭。 她就那樣靜靜地躺著,像睡著了一般盒卸。 火紅的嫁衣襯著肌膚如雪骗爆。 梳的紋絲不亂的頭發(fā)上次氨,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天蔽介,我揣著相機(jī)與錄音,去河邊找鬼煮寡。 笑死虹蓄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幸撕。 我是一名探鬼主播薇组,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼坐儿!你這毒婦竟也來了律胀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤貌矿,失蹤者是張志新(化名)和其女友劉穎炭菌,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逛漫,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡黑低,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酌毡。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片克握。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖枷踏,靈堂內(nèi)的尸體忽然破棺而出菩暗,到底是詐尸還是另有隱情,我是刑警寧澤旭蠕,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布停团,位于F島的核電站,受9級特大地震影響下梢,放射性物質(zhì)發(fā)生泄漏客蹋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一孽江、第九天 我趴在偏房一處隱蔽的房頂上張望讶坯。 院中可真熱鬧,春花似錦岗屏、人聲如沸辆琅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婉烟。三九已至娩井,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間似袁,已是汗流浹背洞辣。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昙衅,地道東北人扬霜。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像而涉,于是被迫代替她去往敵國和親著瓶。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容