Facebook Pixel

2896. Apply Operations to Make Two Strings Equal

Problem Description

You have two binary strings s1 and s2 of the same length n (containing only '0's and '1's), and a positive integer x. Your goal is to make s1 equal to s2 using the minimum total cost.

You can perform two types of operations on s1:

  1. Operation 1: Choose any two indices i and j, then flip both s1[i] and s1[j]. This costs x.
  2. Operation 2: Choose an index i where i < n - 1, then flip both s1[i] and s1[i + 1] (two adjacent characters). This costs 1.

Flipping means changing '0' to '1' or '1' to '0'.

The problem asks you to find the minimum cost needed to transform s1 into s2. If it's impossible to make them equal, return -1.

For example, if you have positions where s1 and s2 differ, you need to flip those positions in s1. Since each operation flips exactly two characters, if there's an odd number of positions where the strings differ, it's impossible to make them equal (you can't flip an odd number of positions using operations that always flip pairs).

The key insight is that you need to identify all positions where s1[i] ≠ s2[i], then find the optimal way to pair up these positions for flipping using either the expensive operation (cost x for any two positions) or the cheap operation (cost 1 for adjacent positions, but you might need multiple operations to connect positions that aren't originally adjacent).

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

Intuition

Let's first observe what positions actually need to be changed. If s1[i] ≠ s2[i], then position i needs to be flipped. Since each operation flips exactly two positions, we need an even number of differing positions - otherwise it's impossible.

Once we identify all differing positions and store them in an array idx, the problem becomes: how do we optimally pair up these positions for flipping?

Consider we have positions [idx[0], idx[1], ..., idx[m-1]] that need flipping. We have two ways to flip any pair:

  • Use Operation 1 (cost x) to directly flip any two positions
  • Use Operation 2 multiple times to "connect" two positions through adjacent flips

For Operation 2, if we want to flip positions idx[i] and idx[j] where i < j, we can achieve this by performing adjacent flips from idx[i] to idx[j]. The cost would be idx[j] - idx[i] (the distance between them).

Now comes the key insight: for any subarray of positions idx[i..j], we need to decide how to pair them up. For the endpoints idx[i] and idx[j], we have three choices:

  1. Pair them together: Use Operation 1 with cost x to flip idx[i] and idx[j] directly. This leaves us with the subproblem idx[i+1..j-1].

  2. Pair idx[i] with idx[i+1]: Use Operation 2 to connect these adjacent positions in our list with cost idx[i+1] - idx[i]. This leaves us with the subproblem idx[i+2..j].

  3. Pair idx[j-1] with idx[j]: Use Operation 2 to connect these adjacent positions with cost idx[j] - idx[j-1]. This leaves us with the subproblem idx[i..j-2].

This recursive structure naturally leads to a dynamic programming solution where dfs(i, j) represents the minimum cost to handle all positions from index i to j in our idx array. The base case is when i > j, meaning no positions left to handle, so the cost is 0.

The memoization helps avoid recalculating the same subproblems, as different choices might lead to the same subproblem configuration.

Learn more about Dynamic Programming patterns.

Solution Approach

First, we extract all positions where s1[i] ≠ s2[i] and store them in an array idx. Let m be the length of this array. If m is odd (checked using m & 1), we immediately return -1 since it's impossible to make the strings equal.

The core of the solution is a recursive function dfs(i, j) that computes the minimum cost to flip all positions in idx[i..j]. We use Python's @cache decorator for memoization to avoid redundant calculations.

Base Case:

  • When i > j, there are no positions to flip, so we return 0.

Recursive Cases: For each subproblem dfs(i, j), we consider three options:

  1. Option A - Pair the endpoints:

    • Flip idx[i] and idx[j] using Operation 1
    • Cost: x + dfs(i + 1, j - 1)
    • This removes both endpoints and recursively solves the inner subarray
  2. Option B - Pair the first two positions:

    • Flip idx[i] and idx[i + 1] using Operation 2
    • Cost: (idx[i + 1] - idx[i]) + dfs(i + 2, j)
    • The distance idx[i + 1] - idx[i] represents the number of Operation 2 moves needed
    • This removes the first two positions and recursively solves the remaining right portion
  3. Option C - Pair the last two positions:

    • Flip idx[j - 1] and idx[j] using Operation 2
    • Cost: (idx[j] - idx[j - 1]) + dfs(i, j - 2)
    • This removes the last two positions and recursively solves the remaining left portion

The function returns the minimum of these three options: min(a, b, c).

Implementation Details:

@cache
def dfs(i: int, j: int) -> int:
    if i > j:
        return 0
    a = dfs(i + 1, j - 1) + x
    b = dfs(i + 2, j) + idx[i + 1] - idx[i]
    c = dfs(i, j - 2) + idx[j] - idx[j - 1]
    return min(a, b, c)

The final answer is dfs(0, m - 1), which represents the minimum cost to handle all differing positions from the first to the last in our idx array.

The memoization ensures that each unique subproblem (i, j) is computed only once, giving us a time complexity of O(m²) where m is the number of differing positions.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand the solution approach.

Example:

  • s1 = "10110"
  • s2 = "00101"
  • x = 4

Step 1: Identify differing positions

Compare each position:

  • Position 0: s1[0]='1', s2[0]='0' → differs
  • Position 1: s1[1]='0', s2[1]='0' → same
  • Position 2: s1[2]='1', s2[2]='1' → same
  • Position 3: s1[3]='1', s2[3]='0' → differs
  • Position 4: s1[4]='0', s2[4]='1' → differs

So idx = [0, 3, 4] (positions that need flipping)

Since we have 3 differing positions (odd number), it's impossible to make the strings equal. Return -1.


Let's modify the example for a solvable case:

  • s1 = "10110"
  • s2 = "00111"
  • x = 4

Differing positions: idx = [0, 3] (even number, so it's solvable)

Step 2: Apply dynamic programming

We need to find dfs(0, 1) - the minimum cost to handle positions 0 through 1 in our idx array.

For dfs(0, 1), we have three options:

  1. Option A - Pair endpoints (idx[0]=0 and idx[1]=3):

    • Use Operation 1 to flip positions 0 and 3
    • Cost: x + dfs(1, 0) = 4 + 0 = 4
    • (dfs(1, 0) returns 0 since i > j)
  2. Option B - Pair first two positions:

    • This would be idx[0] and idx[1], which are positions 0 and 3
    • Use Operation 2 to connect them: need to flip (0,1), then (1,2), then (2,3)
    • Cost: (idx[1] - idx[0]) + dfs(2, 1) = (3 - 0) + 0 = 3
  3. Option C - Pair last two positions:

    • Same as Option B since we only have two positions total
    • Cost: (idx[1] - idx[0]) + dfs(0, -1) = (3 - 0) + 0 = 3

The minimum is min(4, 3, 3) = 3.

Result: The minimum cost is 3, achieved by using Operation 2 three times:

  • Flip positions (0,1): s1 becomes "01110"
  • Flip positions (1,2): s1 becomes "00010"
  • Flip positions (2,3): s1 becomes "00111" = s2 ✓

Another example with more positions:

  • idx = [1, 2, 5, 6] and x = 3

For dfs(0, 3):

  1. Option A: Pair idx[0]=1 with idx[3]=6

    • Cost: 3 + dfs(1, 2)
    • Need to solve dfs(1, 2) for positions [2, 5]
    • dfs(1, 2) = min(3 + 0, 3 + 0, 3 + 0) = 3
    • Total: 3 + 3 = 6
  2. Option B: Pair idx[0]=1 with idx[1]=2

    • Cost: (2-1) + dfs(2, 3) = 1 + dfs(2, 3)
    • dfs(2, 3) for positions [5, 6] = min(3, 1, 1) = 1
    • Total: 1 + 1 = 2
  3. Option C: Pair idx[2]=5 with idx[3]=6

    • Cost: (6-5) + dfs(0, 1) = 1 + dfs(0, 1)
    • dfs(0, 1) for positions [1, 2] = min(3, 1, 1) = 1
    • Total: 1 + 1 = 2

The minimum is min(6, 2, 2) = 2, achieved by pairing adjacent positions in our idx array.

Solution Implementation

1class Solution:
2    def minOperations(self, s1: str, s2: str, x: int) -> int:
3        """
4        Find minimum cost to transform s1 to s2 using operations.
5      
6        Args:
7            s1: Source string
8            s2: Target string  
9            x: Cost for pairing non-adjacent positions
10          
11        Returns:
12            Minimum cost of operations, or -1 if impossible
13        """
14        from functools import cache
15      
16        @cache
17        def dfs(left: int, right: int) -> int:
18            """
19            Dynamic programming function to find minimum cost for range [left, right].
20          
21            Args:
22                left: Left boundary index in diff_positions array
23                right: Right boundary index in diff_positions array
24              
25            Returns:
26                Minimum cost to fix all differences in range
27            """
28            # Base case: no more positions to process
29            if left > right:
30                return 0
31          
32            # Option 1: Pair leftmost and rightmost positions (cost x)
33            pair_ends = dfs(left + 1, right - 1) + x
34          
35            # Option 2: Pair two leftmost consecutive positions (cost is their distance)
36            pair_left = dfs(left + 2, right) + diff_positions[left + 1] - diff_positions[left]
37          
38            # Option 3: Pair two rightmost consecutive positions (cost is their distance)
39            pair_right = dfs(left, right - 2) + diff_positions[right] - diff_positions[right - 1]
40          
41            # Return minimum cost among all options
42            return min(pair_ends, pair_left, pair_right)
43      
44        # Find all positions where s1 and s2 differ
45        n = len(s1)
46        diff_positions = [i for i in range(n) if s1[i] != s2[i]]
47      
48        # Count of different positions
49        num_differences = len(diff_positions)
50      
51        # Odd number of differences cannot be paired
52        if num_differences & 1:  # Bitwise AND with 1 checks if odd
53            return -1
54          
55        # Calculate minimum cost using DP
56        return dfs(0, num_differences - 1)
57
1class Solution {
2    // List to store indices where s1 and s2 differ
3    private List<Integer> diffIndices = new ArrayList<>();
4    // Memoization table for dynamic programming
5    private Integer[][] memo;
6    // Cost of using the global operation
7    private int globalCost;
8
9    public int minOperations(String s1, String s2, int x) {
10        int length = s1.length();
11      
12        // Find all positions where s1 and s2 differ
13        for (int i = 0; i < length; ++i) {
14            if (s1.charAt(i) != s2.charAt(i)) {
15                diffIndices.add(i);
16            }
17        }
18      
19        int diffCount = diffIndices.size();
20      
21        // If odd number of differences, impossible to make strings equal
22        if (diffCount % 2 == 1) {
23            return -1;
24        }
25      
26        this.globalCost = x;
27        memo = new Integer[diffCount][diffCount];
28      
29        // Start dynamic programming from the full range
30        return dfs(0, diffCount - 1);
31    }
32
33    /**
34     * Dynamic programming function to find minimum cost
35     * @param left Starting index in diffIndices
36     * @param right Ending index in diffIndices
37     * @return Minimum cost to fix all differences in range [left, right]
38     */
39    private int dfs(int left, int right) {
40        // Base case: no more differences to fix
41        if (left > right) {
42            return 0;
43        }
44      
45        // Return memoized result if already computed
46        if (memo[left][right] != null) {
47            return memo[left][right];
48        }
49      
50        // Option 1: Use global operation to fix positions at left and right
51        // This pairs the leftmost and rightmost differences
52        memo[left][right] = dfs(left + 1, right - 1) + globalCost;
53      
54        // Option 2: Use local operation to fix adjacent differences on the left
55        // Pairs left and left+1, cost is the distance between them
56        memo[left][right] = Math.min(memo[left][right], 
57            dfs(left + 2, right) + diffIndices.get(left + 1) - diffIndices.get(left));
58      
59        // Option 3: Use local operation to fix adjacent differences on the right
60        // Pairs right-1 and right, cost is the distance between them
61        memo[left][right] = Math.min(memo[left][right], 
62            dfs(left, right - 2) + diffIndices.get(right) - diffIndices.get(right - 1));
63      
64        return memo[left][right];
65    }
66}
67
1class Solution {
2public:
3    int minOperations(string s1, string s2, int x) {
4        // Find all positions where s1 and s2 differ
5        vector<int> diffPositions;
6        for (int i = 0; i < s1.size(); ++i) {
7            if (s1[i] != s2[i]) {
8                diffPositions.push_back(i);
9            }
10        }
11      
12        int numDiffs = diffPositions.size();
13      
14        // If odd number of differences, impossible to make strings equal
15        if (numDiffs & 1) {
16            return -1;
17        }
18      
19        // If strings are already equal
20        if (numDiffs == 0) {
21            return 0;
22        }
23      
24        // dp[i][j] = minimum cost to fix differences from index i to j in diffPositions array
25        int dp[numDiffs][numDiffs];
26        memset(dp, -1, sizeof(dp));
27      
28        // DFS with memoization to find minimum cost
29        function<int(int, int)> dfs = [&](int left, int right) {
30            // Base case: no more differences to fix
31            if (left > right) {
32                return 0;
33            }
34          
35            // Return memoized result if already computed
36            if (dp[left][right] != -1) {
37                return dp[left][right];
38            }
39          
40            // Three options to fix differences:
41            // 1. Pair leftmost and rightmost differences (cost x)
42            // 2. Pair first two differences from left (cost is their distance)
43            // 3. Pair last two differences from right (cost is their distance)
44            dp[left][right] = min({
45                dfs(left + 1, right - 1) + x,                                    // Option 1: pair outer elements
46                dfs(left + 2, right) + diffPositions[left + 1] - diffPositions[left],   // Option 2: pair left adjacent
47                dfs(left, right - 2) + diffPositions[right] - diffPositions[right - 1]  // Option 3: pair right adjacent
48            });
49          
50            return dp[left][right];
51        };
52      
53        return dfs(0, numDiffs - 1);
54    }
55};
56
1/**
2 * Finds the minimum cost to make two binary strings equal by flipping pairs of bits
3 * @param s1 - First binary string
4 * @param s2 - Second binary string  
5 * @param x - Cost of flipping any two bits (can be non-adjacent)
6 * @returns Minimum cost to make strings equal, or -1 if impossible
7 */
8function minOperations(s1: string, s2: string, x: number): number {
9    // Collect indices where the two strings differ
10    const diffIndices: number[] = [];
11    for (let i = 0; i < s1.length; i++) {
12        if (s1[i] !== s2[i]) {
13            diffIndices.push(i);
14        }
15    }
16  
17    const numDifferences = diffIndices.length;
18  
19    // If odd number of differences, impossible to make strings equal
20    if (numDifferences % 2 === 1) {
21        return -1;
22    }
23  
24    // If strings are already equal, no operations needed
25    if (numDifferences === 0) {
26        return 0;
27    }
28  
29    // Initialize memoization table for dynamic programming
30    // memo[i][j] stores the minimum cost to fix differences from index i to j
31    const memo: number[][] = Array.from(
32        { length: numDifferences }, 
33        () => Array.from({ length: numDifferences }, () => -1)
34    );
35  
36    /**
37     * Dynamic programming function to find minimum cost for a range
38     * @param left - Starting index in diffIndices array
39     * @param right - Ending index in diffIndices array
40     * @returns Minimum cost to fix all differences in range [left, right]
41     */
42    const findMinCost = (left: number, right: number): number => {
43        // Base case: no differences left to fix
44        if (left > right) {
45            return 0;
46        }
47      
48        // Return memoized result if already computed
49        if (memo[left][right] !== -1) {
50            return memo[left][right];
51        }
52      
53        // Option 1: Use general operation (cost x) to flip leftmost and rightmost differences
54        let minCost = findMinCost(left + 1, right - 1) + x;
55      
56        // Option 2: Use adjacent operation on the two leftmost differences
57        // Cost is the distance between these adjacent positions in original string
58        minCost = Math.min(
59            minCost, 
60            findMinCost(left + 2, right) + diffIndices[left + 1] - diffIndices[left]
61        );
62      
63        // Option 3: Use adjacent operation on the two rightmost differences
64        // Cost is the distance between these adjacent positions in original string
65        minCost = Math.min(
66            minCost, 
67            findMinCost(left, right - 2) + diffIndices[right] - diffIndices[right - 1]
68        );
69      
70        // Store and return the minimum cost
71        memo[left][right] = minCost;
72        return minCost;
73    };
74  
75    // Start the recursive calculation from the full range
76    return findMinCost(0, numDifferences - 1);
77}
78

Time and Space Complexity

The time complexity is O(m^2) where m is the number of mismatched positions between s1 and s2. Since m ≤ n where n is the length of the strings, and in the worst case m = n, the time complexity can be expressed as O(n^2).

The recursive function dfs(i, j) explores all possible states where i and j represent indices in the idx array. There are O(m^2) possible states since i can range from 0 to m-1 and j can range from 0 to m-1. Each state is computed once due to memoization using @cache, and each computation takes O(1) time.

The space complexity is O(m^2) which is also O(n^2) in the worst case. This comes from:

  • The memoization cache storing up to O(m^2) states
  • The recursion stack depth which can go up to O(m) in the worst case
  • The idx array which takes O(m) space

Since O(m^2) dominates O(m), the overall space complexity is O(m^2) or O(n^2) when considering the worst case where all positions are mismatched.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Integer Division vs Float Division Pitfall

A common mistake is using float division when calculating costs for consecutive positions. Some developers might write:

# WRONG - creates floating point result
pair_left = dfs(left + 2, right) + (diff_positions[left + 1] - diff_positions[left]) / 2

This happens when misunderstanding that the cost between adjacent positions is their distance (number of Operation 2 moves needed), not half their distance. The correct cost is simply the difference between indices.

Solution: Use the actual distance without division:

# CORRECT
pair_left = dfs(left + 2, right) + diff_positions[left + 1] - diff_positions[left]

2. Incorrect Base Case Handling

Another pitfall is not properly handling edge cases when the recursion reaches boundaries:

# WRONG - doesn't check array bounds
def dfs(left: int, right: int) -> int:
    if left > right:
        return 0
    # This will cause IndexError when left + 1 >= len(diff_positions)
    pair_left = dfs(left + 2, right) + diff_positions[left + 1] - diff_positions[left]

While the memoized solution handles this implicitly (since left + 1 won't exceed bounds when left <= right), it's still a conceptual pitfall.

Solution: The current implementation is actually safe because when left > right, we return immediately, and when left == right, we have a single element which is invalid (odd count). The recursion structure ensures we never access out-of-bounds indices.

3. Not Caching/Memoizing the Recursive Function

Forgetting to use memoization leads to exponential time complexity:

# WRONG - no memoization, causes timeout
def dfs(left: int, right: int) -> int:
    if left > right:
        return 0
    # ... rest of the logic

This creates overlapping subproblems that get recalculated multiple times.

Solution: Always use @cache decorator or manual memoization:

# CORRECT
from functools import cache

@cache
def dfs(left: int, right: int) -> int:
    # ... implementation

4. Misunderstanding the Cost Calculation for Adjacent Flips

Some might think that flipping adjacent positions always costs 1:

# WRONG - assumes all adjacent flips cost 1
pair_left = dfs(left + 2, right) + 1

However, if positions are at indices 2 and 5, you need 3 Operation 2 moves to connect them (flip 2-3, then 3-4, then 4-5).

Solution: Calculate the actual distance between positions:

# CORRECT - uses actual distance
pair_left = dfs(left + 2, right) + diff_positions[left + 1] - diff_positions[left]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More