weekly contest 428。
補題發現其實沒很難,主要是前面 Q3 讓人整個心態崩潰,根本沒時間看 Q4。

題目

輸入字串 s。

若一個字串中的字元出現頻率都相等,則稱作好的

你可以執行以下操作任意次:

  • 從 s 刪除一個字母。
  • 對 s 插入一個字母。
  • 對 s 的一個字母變成字母表中下一個字母。

注意:’z’ 無法變成 ‘a’。

求使得 s 變成好的字串所需的最小操作次數。

解法

字串中至多 26 個字母。先統計各字母的頻率,可對應數字 [0,25],各頻率記做 cnt[i]。
問題轉換成:

把 cnt[0,25] 的頻率透過操作都變成目標值 t。

若選定目標值 t,則所有 cnt 都要變成 t 或是 0


對於某頻率 x,可以用前兩種操作變成 t 或 0:

  • x 變成 0,成本 x 次
  • x 變成 t,成本 abs(t-x) 次

即 min(x, abs(t-x))。


x 的下一個頻率記做 y。

第三種操作會減少 x、增加 y。
只有在 y 小於 t 時才有意義,否則多加的還是要扣掉,多此一舉。

那如果三個以上連續頻率 x,y,z,使用操作三是否可行?

x,y,z = [3,2,1], t = 2
對 x 使用操作三,變成 [2,3,1]
對 y 使用操作三,變成 [2,2,2]

實際上變化量只有 2,不如乾脆用操作一二就好。
如果超過連續三個,用操作三根本是虧錢。
操作三只適兩個連續的字母


在 y < t 的前題下,討論 x 的情況:

  • x 大於 t,變成 t 最好,成本 x-t
  • x 小於 t,變成 0 最好,成本 x

根據 x,y,t 的值不同,有時操作完 x 還沒變成 t 或 0,需要補幾次操作二;或是 y 還沒變 t,需要補操作一。
所以要求兩者對目標值的距離,也就是:

  • x 改 t,成本 max(t-y, x-t)
  • x 改 0,成本 max(t-y, x)

對 x 做操作三會影響到下一個頻率 y,因此考慮從 0 開始考慮操作種類。
因為找不到明顯的規律,只好往暴力枚舉去猜想,嘗試枚舉操作一加二,或是使用三加二的情況。

原本問題是:

cnt[0,25] 變成 t 的最小操作次數。

只用操作一或二,會變成:

對 0 的操作次數 + cnt[1,25] 變成 t 的最小操作次數。

用操作三加二,會變成:

對 0, 1 的操作次數 + cnt[2,25] 變成 t 的最小操作次數。

不同的選法會產生重疊的子問題,考慮 dp。

定義 dp(i, t):把 [0, 25] 頻率都變成 t 的最小操作次數。
轉移:dp(i, t) = min(x 成本 + dp(i+1, t), xy 成本 + dp(i+2, t))。
base:當 i = 26 時,修改完成,答案 0。


已經知道改成 t 的最小成本,但是不知道 t 是多少。
同樣,沒有明顯的規律,只好暴力枚舉 t。

各頻率最小值是 0,所以 t 下限為 0。
而改成超過現有頻率也沒意義,所以 t 上限為 max(cnt),最大為 N。
其實上限直接放 N 也行,只是 py 會爆記憶體。

dp 時間複雜度 O(N)。
至多算 t = O(N) 次。

時間複雜度 O(N^2)。
空間複雜度 O(N)。

"""
分類討論 目標值 t 或是 0
cnt[i] = x
cnt[i+1] = y

- 直接把 x 改成 t 或是 0
    成本 min(x, abs(t-x))
- 如果 y<t,可以用操作三,把 y 改成 t。成本至少 t-y
    操作三結束,可能 x,y 其中一個還不夠,需要補操作
    - x 改 t,成本 max(t-y, x-t)
    - x 改 0,成本 max(t-y, x)
"""
class Solution:
    def makeStringGood(self, s: str) -> int:
        cnt = [0] * 26
        for c in s:
            cnt[ord(c) - 97] += 1

        @cache
        def dp(i, t):
            if i == 26:
                return 0

            x = cnt[i]
            # only use op1 or op2
            res = min(x, abs(t-x)) + dp(i+1, t)

            # use op3 for y
            # then use op2 for extra x
            if i < 25 and cnt[i+1] < t:
                y = cnt[i+1]
                if x > t:
                    cost = max(t-y, x-t)
                else:
                    cost = max(t-y, x)
                res = min(res, cost + dp(i+2, t))
            return res

        ans = inf
        for target in range(0, max(cnt) + 1):
            ans = min(ans, dp(0, target))

        return ans