Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions problems/3441-minimum-cost-good-caption/analysis.md
Original file line number Diff line number Diff line change
@@ -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.
205 changes: 205 additions & 0 deletions problems/3441-minimum-cost-good-caption/solution.go
Original file line number Diff line number Diff line change
@@ -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
}
30 changes: 30 additions & 0 deletions problems/3441-minimum-cost-good-caption/solution_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}