Hard

题目描述

给定一个长度为 n 的字符串 caption。好字幕是指每个字符都以至少 3 个连续出现的形式存在的字符串。

例如:

  • “aaabbb” 和 “aaaaccc” 是好字幕。
  • “aabbb” 和 “ccccd” 不是好字幕。

你可以执行以下操作任意次数:

选择一个索引 i(其中 0 <= i < n),并将该索引处的字符更改为:

  • 字母表中紧邻的前一个字符(如果 caption[i] != ‘a’)
  • 字母表中紧邻的后一个字符(如果 caption[i] != ‘z’)

你的任务是使用最少的操作次数将给定的字幕转换为好字幕,并返回结果。如果有多个可能的好字幕,返回其中字典序最小的一个。如果无法创建好字幕,返回空字符串 “"。

示例 1:

输入: caption = "cdcd"
输出: "cccc"
解释: 可以证明给定的字幕不能用少于 2 次操作转换为好字幕。使用恰好 2 次操作可以创建的可能好字幕有:
- "dddd": 将 caption[0] 和 caption[2] 更改为下一个字符 'd'。
- "cccc": 将 caption[1] 和 caption[3] 更改为前一个字符 'c'。
由于 "cccc" 在字典序上小于 "dddd",返回 "cccc"。

示例 2:

输入: caption = "aca"
输出: "aaa"
解释: 可以证明给定的字幕至少需要 2 次操作才能转换为好字幕。使用恰好 2 次操作可以获得的唯一好字幕如下:
操作 1: 将 caption[1] 更改为 'b'。caption = "aba"。
操作 2: 将 caption[1] 更改为 'a'。caption = "aaa"。
因此,返回 "aaa"。

示例 3:

输入: caption = "bc"
输出: ""
解释: 可以证明给定的字幕不能通过任何操作转换为好字幕。

约束条件:

  • 1 <= caption.length <= 5 * 10^4
  • caption 仅由小写英文字母组成

解题思路

这道题需要将字符串转换为"好字幕”,即每个字符都至少连续出现3次。我们需要用动态规划来解决。

核心思路:

  1. 状态定义dp[i][c][cnt] 表示处理到第i个位置,当前字符为c,连续相同字符个数为cnt的最小操作次数。

  2. 分段处理:由于好字幕要求每个字符至少连续出现3次,我们需要将字符串分成若干段,每段内字符相同且长度至少为3。

  3. 贪心策略:为了获得字典序最小的结果,我们优先尝试较小的字符。对于每个分段,尝试所有可能的字符,选择代价最小且字典序最小的方案。

  4. 转移方程

    • 如果当前字符与前一个相同,连续计数+1
    • 如果不同,需要确保前一个分段长度至少为3,然后开始新分段
  5. 边界处理

    • 字符串长度小于3时,无法形成好字幕
    • 最后一个分段也必须满足长度≥3的要求

算法流程:

  1. 使用三维DP记录状态
  2. 枚举每个位置可能的字符选择
  3. 计算操作代价(字符间距离)
  4. 确保每个分段长度至少为3
  5. 返回最优解,如果不存在则返回空字符串

时间复杂度主要来自于状态转移,需要仔细处理分段边界和字典序要求。

代码实现

class Solution {
public:
    string minCostGoodCaption(string caption) {
        int n = caption.length();
        if (n < 3) return "";
        
        // dp[i][c][cnt] = minimum cost to make first i chars valid, 
        // ending with char c appearing cnt consecutive times
        vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(26, vector<int>(n + 1, INT_MAX)));
        vector<vector<vector<pair<int, int>>>> parent(n + 1, vector<vector<pair<int, int>>>(26, vector<pair<int, int>>(n + 1, {-1, -1})));
        
        // Initialize
        for (int c = 0; c < 26; c++) {
            dp[1][c][1] = abs(caption[0] - ('a' + c));
        }
        
        for (int i = 1; i < n; i++) {
            for (int c = 0; c < 26; c++) {
                for (int cnt = 1; cnt <= n; cnt++) {
                    if (dp[i][c][cnt] == INT_MAX) continue;
                    
                    int cost = abs(caption[i] - ('a' + c));
                    
                    // Continue with same character
                    if (cnt + 1 <= n) {
                        if (dp[i + 1][c][cnt + 1] > dp[i][c][cnt] + cost) {
                            dp[i + 1][c][cnt + 1] = dp[i][c][cnt] + cost;
                            parent[i + 1][c][cnt + 1] = {c, cnt};
                        }
                    }
                    
                    // Start new segment (only if current segment >= 3)
                    if (cnt >= 3) {
                        for (int nc = 0; nc < 26; nc++) {
                            int newCost = abs(caption[i] - ('a' + nc));
                            if (dp[i + 1][nc][1] > dp[i][c][cnt] + newCost) {
                                dp[i + 1][nc][1] = dp[i][c][cnt] + newCost;
                                parent[i + 1][nc][1] = {c, cnt};
                            }
                        }
                    }
                }
            }
        }
        
        // Find minimum cost ending
        int minCost = INT_MAX;
        int bestC = -1, bestCnt = -1;
        
        for (int c = 0; c < 26; c++) {
            for (int cnt = 3; cnt <= n; cnt++) {
                if (dp[n][c][cnt] < minCost || (dp[n][c][cnt] == minCost && (bestC == -1 || c < bestC))) {
                    minCost = dp[n][c][cnt];
                    bestC = c;
                    bestCnt = cnt;
                }
            }
        }
        
        if (minCost == INT_MAX) return "";
        
        // Reconstruct answer
        string result(n, 'a');
        int pos = n, curC = bestC, curCnt = bestCnt;
        
        while (pos > 0 && parent[pos][curC][curCnt].first != -1) {
            char ch = 'a' + curC;
            for (int j = 0; j < curCnt; j++) {
                result[pos - 1 - j] = ch;
            }
            pos -= curCnt;
            auto p = parent[pos + curCnt][curC][curCnt];
            curC = p.first;
            curCnt = p.second;
        }
        
        return result;
    }
};
class Solution:
    def minCostGoodCaption(self, caption: str) -> str:
        n = len(caption)
        if n < 3:
            return ""
        
        # dp[i][c][cnt] = minimum cost to process first i characters
        # ending with character c appearing cnt consecutive times
        dp = [[[float('inf')] * (n + 1) for _ in range(26)] for _ in range(n + 1)]
        parent = [[[(-1, -1)] * (n + 1) for _ in range(26)] for _ in range(n + 1)]
        
        # Initialize first position
        for c in range(26):
            cost = abs(ord(caption[0]) - ord('a') - c)
            dp[1][c][1] = cost
        
        # Fill DP table
        for i in range(1, n):
            for c in range(26):
                for cnt in range(1, n + 1):
                    if dp[i][c][cnt] == float('inf'):
                        continue
                    
                    cost = abs(ord(caption[i]) - ord('a') - c)
                    
                    # Continue same character
                    if cnt + 1 <= n:
                        new_cost = dp[i][c][cnt] + cost
                        if new_cost < dp[i + 1][c][cnt + 1]:
                            dp[i + 1][c][cnt + 1] = new_cost
                            parent[i + 1][c][cnt + 1] = (c, cnt)
                    
                    # Start new segment (if current segment >= 3)
                    if cnt >= 3:
                        for nc in range(26):
                            new_cost = dp[i][c][cnt] + abs(ord(caption[i]) - ord('a') - nc)
                            if new_cost < dp[i + 1][nc][1]:
                                dp[i + 1][nc][1] = new_cost
                                parent[i + 1][nc][1] = (c, cnt)
        
        # Find best ending
        min_cost = float('inf')
        best_c = best_cnt = -1
        
        for c in range(26):
            for cnt in range(3, n + 1):
                if dp[n][c][cnt] < min_cost or (dp[n][c][cnt] == min_cost and (best_c == -1 or c < best_c)):
                    min_cost = dp[n][c][cnt]
                    best_c = c
                    best_cnt = cnt
        
        if min_cost == float('inf'):
            return ""
        
        # Reconstruct result
        result = ['a'] * n
        pos, cur_c, cur_cnt = n, best_c, best_cnt
        
        while pos > 0 and parent[pos][cur_c][cur_cnt][0] != -1:
            ch = chr(ord('a') + cur_c)
            for j in range(cur_cnt):
                result[pos - 1 - j] = ch
            pos -= cur_cnt
            cur_c, cur_cnt = parent[pos + cur_cnt][cur_c][cur_cnt]
        
        return ''.join(result)
public class Solution {
    public string MinCostGoodCaption(string caption) {
        int n = caption.Length;
        if (n < 3) return "";
        
        // dp[i][c][cnt] = minimum cost to process first i characters
        int[,,] dp = new int[n + 1, 26, n + 1];
        (int, int)[,,] parent = new (int, int)[n + 1, 26, n + 1];
        
        // Initialize with max values
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j < 26; j++) {
                for (int k = 0; k <= n; k++) {
                    dp[i, j, k] = int.MaxValue;
                    parent[i, j, k] = (-1, -1);
                }
            }
        }
        
        // Initialize first position
        for (int c = 0; c < 26; c++) {
            int cost = Math.Abs(caption[0] - ('a' + c));
            dp[1, c, 1] = cost;
        }
        
        // Fill DP table
        for (int i = 1; i < n; i++) {
            for (int c = 0; c < 26; c++) {
                for (int cnt = 1; cnt <= n; cnt++) {
                    if (dp[i, c, cnt] == int.MaxValue) continue;
                    
                    int cost = Math.Abs(caption[i] - ('a' + c));
                    
                    // Continue same character
                    if (cnt + 1 <= n) {
                        int newCost = dp[i, c, cnt] + cost;
                        if (newCost < dp[i + 1, c, cnt + 1]) {
                            dp[i + 1, c, cnt + 1] = newCost;
                            parent[i + 1, c, cnt + 1] = (c, cnt);
                        }
                    }
                    
                    // Start new segment
                    if (cnt >= 3) {
                        for (int nc = 0; nc < 26; nc++) {
                            int newCost = dp[i, c, cnt] + Math.Abs(caption[i] - ('a' + nc));
                            if (newCost < dp[i + 1, nc, 1]) {
                                dp[i + 1, nc, 1] = newCost;
                                parent[i + 1, nc, 1] = (c, cnt);
                            }
                        }
                    }
                }
            }
        }
        
        // Find best ending
        int minCost = int.MaxValue;
        int bestC = -1, bestCnt = -1;
        
        for (int c = 0; c < 26; c++) {
            for (int cnt = 3; cnt <= n; cnt++) {
                if (dp[n, c, cnt] < minCost || (dp[n, c, cnt] == minCost && (bestC == -1 || c < bestC))) {
                    minCost = dp[n, c, cnt];
                    bestC = c;
                    bestCnt = cnt;
                }
            }
        }
        
        if (minCost == int.MaxValue) return "";
        
        // Reconstruct result
        char[] result = new char[n];
        int pos = n, curC = bestC, curCnt = bestCnt;
        
        while (pos > 0 && parent[pos, curC, curCnt].Item1 != -1) {
            char ch = (char)('a' + curC);
            for (int j = 0; j < curCnt; j++) {
                result[pos - 1 - j] = ch;
            }
            pos -= curCnt;
            (curC, curCnt) = parent[pos + curCnt, curC, curCnt];
        }
        
        return new string(result);
    }
}
var minCostGoodCaption = function(caption) {
    const n = caption.length;
    if (n < 3) return "";
    
    let minCost = Infinity;
    let bestResult = "";
    
    // Try each character as the target for the entire string
    for (let targetChar = 0; targetChar < 26; targetChar++) {
        const char = String.fromCharCode(97 + targetChar);
        let cost = 0;
        
        for (let i = 0; i < n; i++) {
            cost += Math.abs(caption.charCodeAt(i) - (97 + targetChar));
        }
        
        if (cost < minCost) {
            minCost = cost;
            bestResult = char.repeat(n);
        }
    }
    
    // Try dynamic programming approach for more complex patterns
    const dp = new Array(n).fill(null).map(() => new Array(26).fill(Infinity));
    
    // Initialize first position
    for (let c = 0; c < 26; c++) {
        dp[0][c] = Math.abs(caption.charCodeAt(0) - (97 + c));
    }
    
    for (let i = 1; i < n; i++) {
        for (let c = 0; c < 26; c++) {
            const changeCost = Math.abs(caption.charCodeAt(i) - (97 + c));
            
            // Try extending from previous character
            for (let prevC = 0; prevC < 26; prevC++) {
                if (dp[i-1][prevC] === Infinity) continue;
                
                // Check if we can form a valid group
                let canPlace = false;
                
                if (c === prevC) {
                    canPlace = true;
                } else {
                    // Check if we're starting a new group and previous groups are valid
                    canPlace = true;
                }
                
                if (canPlace) {
                    dp[i][c] = Math.min(dp[i][c], dp[i-1][prevC] + changeCost);
                }
            }
        }
    }
    
    // Validate the result by checking groups
    function isValidCaption(s) {
        let i = 0;
        while (i < s.length) {
            let count = 1;
            while (i + count < s.length && s[i + count] === s[i]) {
                count++;
            }
            if (count < 3) return false;
            i += count;
        }
        return true;
    }
    
    // Try all possible group arrangements
    function solve(pos, current, currentCost) {
        if (pos === n) {
            if (isValidCaption(current) && currentCost < minCost) {
                minCost = currentCost;
                bestResult = current;
            } else if (isValidCaption(current) && currentCost === minCost && current < bestResult) {
                bestResult = current;
            }
            return;
        }
        
        if (currentCost >= minCost) return;
        
        // Try each character
        for (let c = 0; c < 26; c++) {
            const char = String.fromCharCode(97 + c);
            const cost = Math.abs(caption.charCodeAt(pos) - (97 + c));
            
            // Try placing at least 3 consecutive characters
            for (let len = 3; len <= Math.min(n - pos, 26); len++) {
                if (pos + len <= n) {
                    let groupCost = 0;
                    let newCurrent = current;
                    
                    for (let j = 0; j < len; j++) {
                        groupCost += Math.abs(caption.charCodeAt(pos + j) - (97 + c));
                        newCurrent += char;
                    }
                    
                    if (currentCost + groupCost < minCost) {
                        solve(pos + len, newCurrent, currentCost + groupCost);
                    }
                }
            }
        }
    }
    
    minCost = Infinity;
    bestResult = "";
    solve(0, "", 0);
    
    return bestResult;
};

复杂度分析

指标复杂度
时间-
空间-