Facebook Pixel

3579. Minimum Steps to Convert String with Operations

Problem Description

You are given two strings, word1 and word2, of equal length. Your task is to transform word1 into word2.

To do this, you can divide word1 into one or more contiguous non-empty substrings. For each such substring substr, you are allowed to perform these operations:

  1. Replace: Replace the character at any one index of substr with another lowercase English letter.
  2. Swap: Swap any two characters in substr.
  3. Reverse Substring: Reverse the entire substring substr.

Each operation counts as one step, and each character in a substring can be used in each operation type at most once (no single index may be involved in more than one replace, one swap, or one reverse).

Return the minimum number of operations required to transform word1 into word2.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since we can divide word1 into substrings and perform powerful operations on them (replace, swap, reverse), it's important to find the optimal way to split word1 and select which operations to use on each substring. If a substring can be turned into the target segment of word2 with fewer operations by using a reverse at the start, we should consider that.

The key insight is to think about dynamic programming: for every position in the string, keep track of the minimum number of operations needed to convert the first part of word1 up to that point. For each substring, try both normal and reversed forms to see which requires fewer steps. Within a substring, count mismatches between word1 and word2 and see if swaps can help reduce replace operations (since swapping can pair up mismatches). This way, we always combine substrings in the most efficient order, exploring all possible splits and using the substring operations smartly.

Solution Approach

We use dynamic programming to solve this problem efficiently. Define f[i] as the minimum number of operations needed to convert the first i characters of word1 into the first i characters of word2. Our goal is to compute f[n], where n is the length of the words.

For each position i (1 to n), we consider all possible previous split points j (0 to i-1), which represent dividing word1[0:i] into two parts: word1[0:j] (already converted) and word1[j:i] (current substring to process). For each substring word1[j:i], we check two scenarios:

  • Transform it as-is (rev=False)
  • Reverse it first, then transform (rev=True, costing one operation for reversing)

For either case, we use a helper function calc(l, r, rev) to determine the minimum number of operations required to turn word1[l:r] (or its reverse) into word2[l:r]. Inside calc, we use a counter to identify character mismatches. If there are mismatched pairs (like a in word1 and b in word2), a swap can sometimes fix two characters in one operation. We use a dictionary to match up such pairs and count the number of changes.

The core DP formula is:

f[i] = min(
    f[j] + min(calc(j, i-1, False), 1 + calc(j, i-1, True))
    for all j < i
)
  • calc(j, i-1, False): Cost to make substring match without reversing.
  • 1 + calc(j, i-1, True): One operation for reversing, plus the cost to match after reverse.

We initialize f[0] = 0 and iterate through each position, updating f[i] based on the best possible split.

Finally, the answer is f[n], the minimum number of operations to convert word1 to word2.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Consider word1 = "abca" and word2 = "caba".

Let's walk through how the dynamic programming approach works step-by-step:

  1. Initialization: Set f[0] = 0. We'll compute f[i] for each i from 1 to 4.

  2. Step-by-step DP computation:

    i = 1

    • Only possible split: j=0, substring: "a" vs "c"
    • No reverse: need 1 replace (ac). Cost = 1
    • Reverse is the same as original (since length 1), also cost = 1
    • f[1] = min(0 + 1, 0 + 1 + 0) = 1

    i = 2

    • Possible splits:
      • j=0, substr: "ab" vs "ca"
        • No reverse: 'a' vs 'c' (replace), 'b' vs 'a' (replace), total 2 replaces = 2
        • Reversed: "ba" vs "ca"
          • 'b' vs 'c' (replace), 'a' vs 'a' (match), total 1 replace
          • Need 1 reverse operation, so total = 1 (reverse) + 1 (replace) = 2
        • Min cost for this split: 2
      • j=1, substr: "b" vs "a"
        • No reverse: need 1 replace
        • Reversed: same as original, cost = 1
        • Min cost for this split: 1
      • Total:
        • f[2] = min(0 + 2, 1 + 1) = 2

    i = 3

    • Possible splits:
      • j=0, substr: "abc" vs "cab"
        • No reverse: positions 0: a vs c (replace), 1: b vs a (replace), 2: c vs b (replace) → 3 replaces
        • Try swaps:
          • 'a' wanted to be 'c' at pos 0, 'b' wanted to be 'a' at pos 1, 'c' wanted to be 'b' at pos 2.
          • Possible swap: swap pos 0 and pos 2, "cba" → now at pos 0, c matches c, at pos 2, a vs b (still needs replace), at pos 1, b vs a (replace), total: 1 swap + 2 replaces = 3 steps
        • Reversed: "cba" vs "cab"
          • Positions: 0: c vs c (match), 1: b vs a (replace), 2: a vs b (replace) → 2 replaces
          • Plus 1 for reverse = 3
        • Min cost: 3
      • j=1, substr: "bc" vs "ab"
        • No reverse: b vs a (replace), c vs b (replace) = 2
        • Try swaps: no better option
        • Reversed: "cb" vs "ab": c vs a (replace), b vs b (match) = 1 replace + 1 reverse = 2
        • Take better one: 2
        • Add DP: f[1] + 2 = 1 + 2 = 3
      • j=2, substr: "c" vs "b"
        • Needs 1 replace
        • DP: f[2] + 1 = 2 + 1 = 3
      • So f[3] = min(3, 3, 3) = 3

    i = 4

    • Try all splits:
      • j=0, substr: "abca" vs "caba":
        • No reverse: a/c, b/a, c/b, a/a; need 3 replaces
        • Try swaps: swap pos 0 (a) and pos 2 (c): "cb a a" vs "caba"
          • Now c/c, b/a, a/b, a/a; b/a and a/b: can swap pos 1 and 2 to form c a b a vs caba; now only need 0 replaces, 2 swaps (0,2 and 1,2)
          • However, swapping twice not allowed at same position in one op; must check constraints, so in this example, just use replace for each pair.
        • Reversed: "acba" vs "caba":
          • a/c (replace), c/a (replace), b/b (match), a/a (match) = 2 replaces + 1 reverse = 3
      • j=1, substr: "bca" vs "aba":
        • No reverse: b/a, c/b, a/a → 2 replaces
        • Swaps: swap b and c: cba vs aba → c/a (replace), b/b, a/a ⇒ 1 replace + 1 swap = 2
        • Reverse: "acb" vs "aba": a/a, c/b, b/a: needs c/b and b/a (can swap), so 1 swap + 1 reverse = 2
        • DP: f[1] + 2 = 1+2=3
      • j=2, substr: "ca" vs "ba":
        • No reverse: c/b (replace), a/a (match) = 1 replace
        • Reverse: a c vs b a: a/b (replace), c/a (replace) = 2 + 1(rev)=3
        • DP: f[2] + 1 = 2+1=3
      • j=3, substr: "a" vs "a": match
        • No op needed
        • DP: f[3]+0=3+0=3
      • So overall, f[4] = 3

Result: The minimum number of operations to convert "abca" to "caba" is 3.

This example shows how the DP approach considers all ways to split the string and whether reversing or swapping can reduce operations for each substring.

Solution Implementation

1from collections import Counter
2from math import inf
3
4class Solution:
5    def minOperations(self, word1: str, word2: str) -> int:
6        # Helper function to calculate min operations needed for transforming
7        # word1[l:r+1] into word2[l:r+1], optionally reversing word1's substring.
8        def calc(left: int, right: int, reverse: bool) -> int:
9            counter = Counter()
10            ops = 0
11            for i in range(left, right + 1):
12                # Determine index in word1 (reverse if needed)
13                idx_word1 = right - (i - left) if reverse else i
14                char1 = word1[idx_word1]
15                char2 = word2[i]
16                if char1 != char2:
17                    # Look for a previous operation to pair with (swap operation)
18                    if counter[(char2, char1)] > 0:
19                        counter[(char2, char1)] -= 1
20                    else:
21                        counter[(char1, char2)] += 1
22                        ops += 1  # new operation needed
23            return ops
24
25        n = len(word1)
26        # DP array: f[i] = minimum ops to make word1[:i] and word2[:i] equal
27        dp = [inf] * (n + 1)
28        dp[0] = 0
29
30        for i in range(1, n + 1):
31            for j in range(i):
32                # Try normal order and reversed segment
33                # Option 1: No reverse on segment word1[j:i] (inclusive of j, exclusive of i)
34                # Option 2: Reverse segment word1[j:i]
35                normal_ops = calc(j, i - 1, False)
36                reversed_ops = 1 + calc(j, i - 1, True)
37                # Take the minimum operations between no-reverse and 1 (for reversing) + reverse-ops
38                min_ops = min(normal_ops, reversed_ops)
39                dp[i] = min(dp[i], dp[j] + min_ops)
40
41        return dp[n]
42
1class Solution {
2    // Calculates the minimum operations to convert word1 to word2
3    public int minOperations(String word1, String word2) {
4        int n = word1.length();
5        int[] dp = new int[n + 1]; // dp[i]: min operations for word1[0...i-1]
6        Arrays.fill(dp, Integer.MAX_VALUE);
7        dp[0] = 0; // Base case: no operations needed for empty substring
8
9        // For each prefix, try splitting at every possible previous point
10        for (int end = 1; end <= n; end++) {
11            for (int start = 0; start < end; start++) {
12                // a: operations if not reversing this segment
13                int opsNoReverse = calc(word1, word2, start, end - 1, false);
14                // b: operations if reversing this segment (+1 for the reversal operation)
15                int opsReverse = 1 + calc(word1, word2, start, end - 1, true);
16                int minOps = Math.min(opsNoReverse, opsReverse);
17                dp[end] = Math.min(dp[end], dp[start] + minOps);
18            }
19        }
20        return dp[n];
21    }
22
23    // Calculates the minimum substitutions needed for word1[l..r] to match word2[l..r]
24    // If rev==true, word1[l..r] is reversed before comparing
25    private int calc(String word1, String word2, int left, int right, boolean rev) {
26        // cnt[x][y]: unpaired x->y changes needed in current segment
27        int[][] count = new int[26][26];
28        int res = 0;
29        for (int i = left; i <= right; i++) {
30            // Find corresponding index in word1 (reversed or not)
31            int idx1 = rev ? (right - (i - left)) : i;
32            // Get characters at current positions
33            int char1 = word1.charAt(idx1) - 'a';
34            int char2 = word2.charAt(i) - 'a';
35            if (char1 != char2) {
36                if (count[char2][char1] > 0) {
37                    // Can pair with previous (opposite) change to minimize operations
38                    count[char2][char1]--;
39                } else {
40                    count[char1][char2]++;
41                    res++;
42                }
43            }
44        }
45        return res;
46    }
47}
48
1#include <vector>
2#include <string>
3#include <climits>
4using namespace std;
5
6class Solution {
7public:
8    // Returns the minimum number of operations to convert word1 to word2.
9    int minOperations(string word1, string word2) {
10        int n = word1.length();
11        // f[i]: minimum operations to convert first i characters
12        vector<int> f(n + 1, INT_MAX);
13        f[0] = 0;
14
15        // Iterate over all positions in word1
16        for (int i = 1; i <= n; ++i) {
17            // Try all possible partition points
18            for (int j = 0; j < i; ++j) {
19                // Option 1: Regular transform from word1[j..i-1] to word2[j..i-1]
20                int normalTransform = calc(word1, word2, j, i - 1, false);
21                // Option 2: Reversed transform with extra operation (hence +1)
22                int reverseTransform = 1 + calc(word1, word2, j, i - 1, true);
23                // Take the minimum of transforming this segment normally or reversed
24                int minTransform = min(normalTransform, reverseTransform);
25                // Update the dp value
26                f[i] = min(f[i], f[j] + minTransform);
27            }
28        }
29
30        return f[n];
31    }
32
33private:
34    // Helper to calculate minimum swaps in segment [l, r], optionally reversing word1
35    int calc(const string& word1, const string& word2, int left, int right, bool reversed) {
36        int count[26][26] = {0}; // Letter pair frequency table
37        int operations = 0;
38
39        // Process each character in segment [left, right]
40        for (int i = left; i <= right; ++i) {
41            // If reversed, map i to the mirrored position
42            int word1Idx = reversed ? right - (i - left) : i;
43            int charA = word1[word1Idx] - 'a'; // Character from word1
44            int charB = word2[i] - 'a';        // Corresponding character from word2
45
46            if (charA != charB) {
47                // Check if reverse swap exists; cancel out if possible
48                if (count[charB][charA] > 0) {
49                    count[charB][charA]--;
50                } else {
51                    // Otherwise, increment need and count an operation
52                    count[charA][charB]++;
53                    operations++;
54                }
55            }
56        }
57        return operations;
58    }
59};
60
1/**
2 * Calculates the minimum number of operations to make word1 equal to word2.
3 * Operations are based on character swaps or segment reversals.
4 *
5 * @param word1 - The original string.
6 * @param word2 - The target string.
7 * @returns The minimum number of operations required.
8 */
9function minOperations(word1: string, word2: string): number {
10    const n = word1.length;
11
12    // f[i]: minimum operations to convert word1[0:i] to word2[0:i]
13    const f: number[] = Array(n + 1).fill(Number.MAX_SAFE_INTEGER);
14    f[0] = 0;
15
16    /**
17     * Calculates minimum swaps needed to convert word1[l:r] to word2[l:r].
18     * If rev is true, considers the reversed substring from word1.
19     *
20     * @param l - Left index (inclusive)
21     * @param r - Right index (inclusive)
22     * @param rev - Whether to reverse the segment
23     * @returns Number of swaps required
24     */
25    function calc(l: number, r: number, rev: boolean): number {
26        // cnt[x][y] counts unmatched (x, y) character pairs.
27        const cnt: number[][] = Array.from({ length: 26 }, () => Array(26).fill(0));
28        let res = 0;
29
30        for (let i = l; i <= r; i++) {
31            // j is the index in word1 (normal or reversed)
32            const j = rev ? r - (i - l) : i;
33            const a = word1.charCodeAt(j) - 97; // char index in word1
34            const b = word2.charCodeAt(i) - 97; // char index in word2
35
36            if (a !== b) {
37                // Try to match with a stored opposite swap
38                if (cnt[b][a] > 0) {
39                    cnt[b][a]--;
40                } else {
41                    cnt[a][b]++;
42                    res++;
43                }
44            }
45        }
46        return res;
47    }
48
49    // DP to consider all substrings word1[j:i] to word2[j:i]
50    for (let i = 1; i <= n; i++) {
51        for (let j = 0; j < i; j++) {
52            // a: cost without reversing, b: cost with reversing (+1 for the reversal)
53            const a = calc(j, i - 1, false);
54            const b = 1 + calc(j, i - 1, true);
55            const minCost = Math.min(a, b);
56
57            f[i] = Math.min(f[i], f[j] + minCost);
58        }
59    }
60    return f[n];
61}
62

Time and Space Complexity

The time complexity of the code is O(n^3 + |\Sigma|^2), where n is the length of the input strings and |\Sigma| is the size of the character set (which is 26 for lowercase English letters). This complexity arises because for each of the O(n^2) pairs (j, i), the function calc is called, which itself takes O(n) time due to the traversal over substring. The |\Sigma|^2 term comes from the size of the counter used for all possible character pairs.

The space complexity of the code is O(n + |\Sigma|^2), where O(n) accounts for the DP array f, and O(|\Sigma|^2) comes from the Counter used in calc for all character pair combinations.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More