From e5aaeca70c17b414cf18fbc0c93a52a1a2aa3af5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Feb 2026 15:21:45 +0000 Subject: [PATCH] feat: add solution for 3441. Minimum Cost Good Caption --- .../analysis.md | 52 +++++ .../solution.go | 205 ++++++++++++++++++ .../solution_test.go | 30 +++ 3 files changed, 287 insertions(+) create mode 100644 problems/3441-minimum-cost-good-caption/analysis.md create mode 100644 problems/3441-minimum-cost-good-caption/solution.go create mode 100644 problems/3441-minimum-cost-good-caption/solution_test.go diff --git a/problems/3441-minimum-cost-good-caption/analysis.md b/problems/3441-minimum-cost-good-caption/analysis.md new file mode 100644 index 0000000..4c9a712 --- /dev/null +++ b/problems/3441-minimum-cost-good-caption/analysis.md @@ -0,0 +1,52 @@ +# 3441. Minimum Cost Good Caption + +[LeetCode Link](https://leetcode.com/problems/minimum-cost-good-caption/) + +Difficulty: Hard +Topics: String, Dynamic Programming +Acceptance Rate: 20.5% + +## Hints + +### Hint 1 + +Think about partitioning the string into contiguous segments, where each segment will be assigned a single character. What constraint must each segment satisfy for the caption to be "good"? + +### Hint 2 + +Consider a DP where you track the current position, the character assigned to the current segment, and how many consecutive positions have used that character so far. Since we only care whether the group length has reached 3 or not, you can cap the consecutive count at 3 to keep the state space manageable. + +### Hint 3 + +Define `dp[i][c][k]` as the minimum cost to process positions `0..i` where position `i` is assigned character `c` and the current run of `c` has length `min(k, 3)`. Transitions either extend the current run (same character, increment k) or start a new character (only allowed if k >= 3). To recover the lexicographically smallest answer among all minimum-cost solutions, reconstruct the string by greedily choosing the smallest character at each position that preserves optimality. + +## Approach + +1. **State definition**: Let `dp[i][c][k]` be the minimum number of operations to process `caption[0..i]` such that: + - Position `i` is assigned character `c` (0-25 for 'a'-'z') + - The current consecutive run of `c` has length `k` (we track k = 1, 2, or 3+) + +2. **Base case**: `dp[0][c][1] = |caption[0] - c|` for each character `c`. + +3. **Transitions from dp[i][c][k]**: + - **Continue same character**: `dp[i+1][c][min(k+1, 3)]` = min of itself and `dp[i][c][k] + |caption[i+1] - c|` + - **Start new character** (only if k >= 3): `dp[i+1][c'][1]` = min of itself and `dp[i][c][3] + |caption[i+1] - c'|` + +4. **Answer**: The minimum cost is `min over all c of dp[n-1][c][3]`. If no state with k=3 is reachable at position n-1, return `""`. + +5. **Lexicographic reconstruction**: After computing the DP, reconstruct the answer from left to right. At each position, choose the smallest character `c` and appropriate `k` that allows reaching the overall minimum cost. This requires also knowing the "suffix" optimal costs, which can be computed via a backward pass or by storing parent pointers and carefully breaking ties. + +The approach used in the solution computes a forward DP, then reconstructs greedily by iterating characters in order ('a' to 'z') and checking if choosing that character still allows achieving the global optimum for the suffix. + +## Complexity Analysis + +Time Complexity: O(n * 26 * 3 * 26) = O(n * 26^2) which simplifies to O(n). The constant factor is 26 * 3 states per position with 26 possible transitions, giving roughly 2028 operations per position. + +Space Complexity: O(n * 26 * 3) for the DP table, which is O(n). + +## Edge Cases + +- **Length < 3**: Impossible to form any group of 3, so return `""`. +- **All same characters**: Already a good caption, return as-is with 0 cost. +- **Length exactly 3**: Must assign one character to all three positions; the optimal character is the median (or smallest among tied medians for lexicographic order). +- **Large input (n = 50000)**: The DP must be efficient; O(n * 26^2) with small constants is fast enough. diff --git a/problems/3441-minimum-cost-good-caption/solution.go b/problems/3441-minimum-cost-good-caption/solution.go new file mode 100644 index 0000000..fb9ab20 --- /dev/null +++ b/problems/3441-minimum-cost-good-caption/solution.go @@ -0,0 +1,205 @@ +package main + +// Approach: DP with states dp[i][c][k] where i is position, c is assigned character (0-25), +// and k is the consecutive run length of c ending at i (capped at 3). +// Transitions: extend run (k->min(k+1,3)) or start new char (only if k>=3). +// Reconstruct lexicographically smallest answer by greedy forward pass. + +const inf = 1<<60 + +func minCostGoodCaption(caption string) string { + n := len(caption) + if n < 3 { + return "" + } + + // dp[i][c][k]: min cost for caption[0..i] with position i assigned char c, + // run length = k (k: 0->1, 1->2, 2->3+) + // We use k in {0,1,2} representing run lengths {1,2,3+} + dp := make([][26][3]int, n) + parent := make([][26][3][3]int, n) // parent[i][c][k] = {prevC, prevK, -1 if no parent} + + for i := range dp { + for c := 0; c < 26; c++ { + for k := 0; k < 3; k++ { + dp[i][c][k] = inf + parent[i][c][k] = [3]int{-1, -1, -1} + } + } + } + + // Base case: position 0 + for c := 0; c < 26; c++ { + cost := abs(int(caption[0]) - ('a' + c)) + dp[0][c][0] = cost // run length 1 + } + + // Fill DP + for i := 0; i < n-1; i++ { + nextCost := func(c int) int { + return abs(int(caption[i+1]) - ('a' + c)) + } + for c := 0; c < 26; c++ { + for k := 0; k < 3; k++ { + if dp[i][c][k] == inf { + continue + } + val := dp[i][c][k] + + // Continue same character + nk := k + 1 + if nk > 2 { + nk = 2 + } + newVal := val + nextCost(c) + if newVal < dp[i+1][c][nk] { + dp[i+1][c][nk] = newVal + parent[i+1][c][nk] = [3]int{c, k, i} + } + + // Start new character (only if run >= 3, i.e., k == 2) + if k == 2 { + for nc := 0; nc < 26; nc++ { + if nc == c { + continue + } + newVal2 := val + nextCost(nc) + if newVal2 < dp[i+1][nc][0] { + dp[i+1][nc][0] = newVal2 + parent[i+1][nc][0] = [3]int{c, k, i} + } + } + } + } + } + } + + // Find minimum cost at position n-1 with k == 2 (run >= 3) + bestCost := inf + for c := 0; c < 26; c++ { + if dp[n-1][c][2] < bestCost { + bestCost = dp[n-1][c][2] + } + } + if bestCost == inf { + return "" + } + + // Reconstruct: find lexicographically smallest among all optimal solutions + // We need suffix DP for this. suffDP[i][c][k] = min cost from position i to n-1 + // given that position i is assigned char c with run length state k. + // Then reconstruct forward greedily. + + // Compute suffix DP + suffDP := make([][26][3]int, n) + for i := range suffDP { + for c := 0; c < 26; c++ { + for k := 0; k < 3; k++ { + suffDP[i][c][k] = inf + } + } + } + // Base: position n-1, must have k==2 for valid ending + for c := 0; c < 26; c++ { + suffDP[n-1][c][2] = abs(int(caption[n-1]) - ('a' + c)) + } + + // Fill suffix DP backwards + for i := n - 2; i >= 0; i-- { + curCost := func(c int) int { + return abs(int(caption[i]) - ('a' + c)) + } + for c := 0; c < 26; c++ { + for k := 0; k < 3; k++ { + // This state means: position i is char c, run length state is k + // What's the min cost from i to n-1? + cc := curCost(c) + + // Transition to i+1: continue same char + nk := k + 1 + if nk > 2 { + nk = 2 + } + if suffDP[i+1][c][nk] != inf { + val := cc + suffDP[i+1][c][nk] + if val < suffDP[i][c][k] { + suffDP[i][c][k] = val + } + } + + // Transition to i+1: start new char (only if k == 2) + if k == 2 { + for nc := 0; nc < 26; nc++ { + if nc == c { + continue + } + if suffDP[i+1][nc][0] != inf { + val := cc + suffDP[i+1][nc][0] + if val < suffDP[i][c][k] { + suffDP[i][c][k] = val + } + } + } + } + } + } + } + + // Greedy reconstruction + result := make([]byte, n) + + // Pick first character: smallest c with suffDP[0][c][0] == bestCost + curC, curK := -1, -1 + for c := 0; c < 26; c++ { + if suffDP[0][c][0] == bestCost { + curC = c + curK = 0 + break + } + } + result[0] = byte('a' + curC) + + for i := 1; i < n; i++ { + remaining := suffDP[i-1][curC][curK] - abs(int(caption[i-1])-('a'+curC)) + found := false + + // Try all characters in order for lexicographic smallest + for nc := 0; nc < 26; nc++ { + if nc == curC { + // Continue same character + nk := curK + 1 + if nk > 2 { + nk = 2 + } + if suffDP[i][nc][nk] == remaining { + result[i] = byte('a' + nc) + curK = nk + found = true + break + } + } else { + // Start new character (only allowed if current run >= 3) + if curK == 2 && suffDP[i][nc][0] == remaining { + result[i] = byte('a' + nc) + curC = nc + curK = 0 + found = true + break + } + } + } + + if !found { + return "" + } + } + + return string(result) +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/problems/3441-minimum-cost-good-caption/solution_test.go b/problems/3441-minimum-cost-good-caption/solution_test.go new file mode 100644 index 0000000..3a6b0db --- /dev/null +++ b/problems/3441-minimum-cost-good-caption/solution_test.go @@ -0,0 +1,30 @@ +package main + +import "testing" + +func TestMinCostGoodCaption(t *testing.T) { + tests := []struct { + name string + caption string + expected string + }{ + {"example 1: cdcd", "cdcd", "cccc"}, + {"example 2: aca", "aca", "aaa"}, + {"example 3: bc (too short)", "bc", ""}, + {"edge case: single character", "a", ""}, + {"edge case: two characters", "ab", ""}, + {"edge case: exactly 3 same", "aaa", "aaa"}, + {"edge case: exactly 3 different", "abc", "bbb"}, + {"edge case: length 6 two groups", "aaabbb", "aaabbb"}, + {"edge case: all same long", "aaaaaa", "aaaaaa"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := minCostGoodCaption(tt.caption) + if result != tt.expected { + t.Errorf("minCostGoodCaption(%q) = %q, want %q", tt.caption, result, tt.expected) + } + }) + } +}