Facebook Pixel

854. K-Similar Strings

Problem Description

This problem asks you to find the minimum number of swaps needed to transform one string into another string.

You are given two strings s1 and s2 that are anagrams of each other (meaning they contain the same characters with the same frequencies, just in different positions).

The concept of k-similarity is defined as follows: two strings are k-similar if you can transform s1 into s2 by performing exactly k swaps of character positions. Each swap involves selecting two positions in the string and exchanging the characters at those positions.

Your task is to find the smallest value of k such that s1 and s2 are k-similar. In other words, find the minimum number of swaps required to convert s1 into s2.

For example:

  • If s1 = "ab" and s2 = "ba", then they are 1-similar because one swap (swapping positions 0 and 1) transforms s1 into s2
  • If s1 = "abc" and s2 = "bca", then they are 2-similar because you need at least 2 swaps to transform s1 into s2

The solution uses a BFS (Breadth-First Search) approach to explore all possible swap sequences, finding the shortest path from s1 to s2. At each step, it identifies positions where characters don't match and generates all valid next states by performing strategic swaps. The next function finds the first mismatched position and swaps it with compatible positions further in the string. The algorithm continues level by level until it reaches the target string s2, ensuring the minimum number of swaps is found.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a graph where each node represents a string configuration, and edges connect strings that differ by exactly one swap. We're looking for the shortest path from s1 to s2 in this graph.

Is it a tree?

  • No: The graph is not a tree because there can be multiple paths to reach the same string configuration through different swap sequences.

Is the problem related to directed acyclic graphs?

  • No: The graph is not necessarily acyclic, and the problem isn't about DAG-specific properties.

Is the problem related to shortest paths?

  • Yes: We need to find the minimum number of swaps (edges) to transform s1 into s2, which is essentially finding the shortest path between two nodes in the graph.

Is the graph Weighted?

  • No: Each swap has the same cost (1), so all edges have equal weight. This makes it an unweighted graph shortest path problem.

BFS

  • Yes: For unweighted graphs where we need the shortest path, BFS is the optimal choice as it explores nodes level by level, guaranteeing the shortest path is found first.

Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the minimum number of swaps needed to transform one string into another. BFS is perfect here because:

  1. We're exploring a state space (different string configurations)
  2. Each transformation (swap) has equal cost
  3. We want the minimum number of transformations
  4. BFS guarantees finding the shortest path in an unweighted graph
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that this problem is fundamentally about finding the shortest transformation sequence between two states. Each state is a string configuration, and we can move from one state to another through a swap operation.

Think of it like a puzzle where you need to rearrange letters to match a target pattern. Every possible arrangement of the string forms a node in an imaginary graph, and swapping two characters creates an edge between nodes. Since we want the minimum number of swaps, we're looking for the shortest path in this graph.

Why BFS? When all moves have equal cost (each swap counts as 1), BFS naturally finds the shortest path by exploring all possibilities at distance 1, then distance 2, and so on. The first time we reach our target string s2, we've found it using the minimum number of steps.

The clever optimization in the solution comes from how we generate the next states. Instead of trying all possible swaps randomly, we use a strategic approach:

  1. Find the first position where s1 and s2 differ
  2. Only swap this mismatched character with positions that would place the correct character (s2[i]) in this position
  3. Additionally, we only swap with positions j where s[j] != s2[j] (the character at position j is also mismatched)

This strategy reduces the search space significantly. We're not wasting time on swaps that don't make progress toward the target. Each swap fixes at least one position and potentially sets up another character to be in a better position for future swaps.

The visited set (vis) ensures we don't revisit the same string configuration, preventing infinite loops and redundant work. Since BFS explores level by level, when we first encounter s2, we can immediately return the current level number as our answer - this represents the minimum number of swaps needed.

Learn more about Breadth-First Search patterns.

Solution Approach

The solution implements BFS using a queue to explore string states level by level. Let's walk through the implementation details:

Data Structures Used:

  • deque: A double-ended queue for BFS traversal, allowing efficient append and popleft operations
  • vis (set): Tracks visited string configurations to avoid revisiting states
  • ans: Counter for the number of swap levels (distance from start)

The Main BFS Loop:

q = deque([s1])
vis = {s1}
ans, n = 0, len(s1)

We initialize the queue with the starting string s1, mark it as visited, and set the answer counter to 0.

Level-by-level Processing:

while 1:
    for _ in range(len(q)):
        s = q.popleft()
        if s == s2:
            return ans

The outer while loop represents each level of BFS (each swap distance). The inner for loop processes all strings at the current distance before moving to the next level.

The next Function - Generating Valid Next States:

def next(s):
    i = 0
    while s[i] == s2[i]:
        i += 1

This function finds the first mismatched position between current string s and target s2. This is the position we'll fix with our swap.

res = []
for j in range(i + 1, n):
    if s[j] == s2[i] and s[j] != s2[j]:
        res.append(s2[: i + 1] + s[i + 1 : j] + s[i] + s[j + 1 :])

For the mismatched position i, we look for positions j where:

  • s[j] == s2[i]: Character at position j is what we need at position i
  • s[j] != s2[j]: Character at position j is also mismatched (not in its final position)

When both conditions are met, we perform the swap and create a new string state. The string construction s2[: i + 1] + s[i + 1 : j] + s[i] + s[j + 1 :] cleverly uses s2[: i + 1] to ensure all positions up to i are already correct.

Adding New States:

for nxt in next(s):
    if nxt not in vis:
        vis.add(nxt)
        q.append(nxt)
ans += 1

For each valid next state generated, if it hasn't been visited, we add it to both the visited set and the queue for future exploration. After processing all states at the current level, we increment ans to move to the next swap distance.

The algorithm guarantees finding the minimum number of swaps because BFS explores states in order of increasing distance from the start. The first time we encounter s2, we've found the shortest path.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with s1 = "abc" and s2 = "bca".

Initial State:

  • Queue: ["abc"]
  • Visited: {"abc"}
  • Answer: 0

Level 0 (ans = 0):

  • Process "abc":
    • Check if "abc" == "bca": No
    • Generate next states using the next function:
      • Find first mismatch: position 0 ('a' vs 'b')
      • Need 'b' at position 0, look for 'b' in positions 1-2:
        • Position 1: s[1] = 'b' (matches what we need) and s2[1] = 'c' (so 'b' != 'c', also mismatched) βœ“
        • Swap positions 0 and 1: "abc" β†’ "bac"
      • Return ["bac"]
    • Add "bac" to queue and visited set
  • Queue after level: ["bac"]
  • Increment ans to 1

Level 1 (ans = 1):

  • Process "bac":
    • Check if "bac" == "bca": No
    • Generate next states:
      • Find first mismatch: position 1 ('a' vs 'c')
      • Need 'c' at position 1, look for 'c' in position 2:
        • Position 2: s[2] = 'c' (matches what we need) and s2[2] = 'a' (so 'c' != 'a', also mismatched) βœ“
        • Swap positions 1 and 2: "bac" β†’ "bca"
      • Return ["bca"]
    • Add "bca" to queue and visited set
  • Queue after level: ["bca"]
  • Increment ans to 2

Level 2 (ans = 2):

  • Process "bca":
    • Check if "bca" == "bca": Yes! βœ“
    • Return ans = 2

The algorithm found that the minimum number of swaps needed is 2:

  1. Swap positions 0 and 1: "abc" β†’ "bac"
  2. Swap positions 1 and 2: "bac" β†’ "bca"

The key optimization is that at each step, we only consider swaps that:

  1. Fix the first mismatched position
  2. Use a character that's also currently misplaced

This strategic approach significantly reduces the search space compared to trying all possible swaps.

Solution Implementation

1from collections import deque
2from typing import List
3
4class Solution:
5    def kSimilarity(self, s1: str, s2: str) -> int:
6        def get_next_states(current_string: str) -> List[str]:
7            """
8            Generate all possible next states by performing one swap operation.
9            The swap should move us closer to the target string s2.
10            """
11            # Find the first position where current string differs from target
12            first_diff_index = 0
13            while current_string[first_diff_index] == s2[first_diff_index]:
14                first_diff_index += 1
15          
16            next_states = []
17            # Look for positions to swap with
18            for swap_index in range(first_diff_index + 1, string_length):
19                # Only swap if:
20                # 1. The character at swap_index matches what we need at first_diff_index
21                # 2. The character at swap_index is not already in its correct position
22                if (current_string[swap_index] == s2[first_diff_index] and 
23                    current_string[swap_index] != s2[swap_index]):
24                    # Create new string with characters swapped
25                    swapped_string = (s2[:first_diff_index + 1] + 
26                                    current_string[first_diff_index + 1:swap_index] + 
27                                    current_string[first_diff_index] + 
28                                    current_string[swap_index + 1:])
29                    next_states.append(swapped_string)
30          
31            return next_states
32      
33        # BFS initialization
34        queue = deque([s1])
35        visited = {s1}
36        swap_count = 0
37        string_length = len(s1)
38      
39        # BFS to find minimum swaps
40        while True:
41            # Process all strings at current level
42            for _ in range(len(queue)):
43                current = queue.popleft()
44              
45                # Check if we've reached the target
46                if current == s2:
47                    return swap_count
48              
49                # Generate and enqueue all valid next states
50                for next_state in get_next_states(current):
51                    if next_state not in visited:
52                        visited.add(next_state)
53                        queue.append(next_state)
54          
55            # Increment swap count after processing each level
56            swap_count += 1
57
1class Solution {
2    /**
3     * Finds the minimum number of swaps needed to transform s1 into s2
4     * using BFS approach where anagrams are guaranteed
5     * @param s1 source string
6     * @param s2 target string
7     * @return minimum number of swaps (k-similarity)
8     */
9    public int kSimilarity(String s1, String s2) {
10        // Initialize BFS queue and visited set
11        Deque<String> queue = new ArrayDeque<>();
12        Set<String> visited = new HashSet<>();
13      
14        // Start BFS from s1
15        queue.offer(s1);
16        visited.add(s1);
17      
18        // Track the number of swaps (levels in BFS)
19        int swapCount = 0;
20      
21        // BFS to find minimum swaps
22        while (true) {
23            // Process all strings at current level
24            int levelSize = queue.size();
25            for (int i = 0; i < levelSize; i++) {
26                String currentString = queue.pollFirst();
27              
28                // Check if we've reached the target
29                if (currentString.equals(s2)) {
30                    return swapCount;
31                }
32              
33                // Generate all possible next states with one swap
34                List<String> nextStates = next(currentString, s2);
35                for (String nextState : nextStates) {
36                    // Add unvisited states to queue
37                    if (!visited.contains(nextState)) {
38                        visited.add(nextState);
39                        queue.offer(nextState);
40                    }
41                }
42            }
43            // Increment swap count after processing each level
44            swapCount++;
45        }
46    }
47  
48    /**
49     * Generates all possible strings that can be obtained by making one valid swap
50     * A valid swap fixes the first mismatched position
51     * @param currentString current string state
52     * @param targetString target string we're trying to reach
53     * @return list of possible next states after one swap
54     */
55    private List<String> next(String currentString, String targetString) {
56        int length = currentString.length();
57        char[] charArray = currentString.toCharArray();
58      
59        // Find the first position where strings differ
60        int firstMismatchIndex = 0;
61        while (charArray[firstMismatchIndex] == targetString.charAt(firstMismatchIndex)) {
62            firstMismatchIndex++;
63        }
64      
65        List<String> nextStates = new ArrayList<>();
66      
67        // Try swapping with all valid positions after the first mismatch
68        for (int j = firstMismatchIndex + 1; j < length; j++) {
69            // Valid swap conditions:
70            // 1. Character at j matches what we need at firstMismatchIndex
71            // 2. Character at j is not already in its correct position
72            if (charArray[j] == targetString.charAt(firstMismatchIndex) && 
73                charArray[j] != targetString.charAt(j)) {
74                // Perform swap
75                swap(charArray, firstMismatchIndex, j);
76                nextStates.add(new String(charArray));
77                // Restore original state for next iteration
78                swap(charArray, firstMismatchIndex, j);
79            }
80        }
81      
82        return nextStates;
83    }
84  
85    /**
86     * Helper method to swap two characters in a character array
87     * @param charArray the character array
88     * @param i first index
89     * @param j second index
90     */
91    private void swap(char[] charArray, int i, int j) {
92        char temp = charArray[i];
93        charArray[i] = charArray[j];
94        charArray[j] = temp;
95    }
96}
97
1class Solution {
2public:
3    int kSimilarity(string s1, string s2) {
4        // Use BFS to find minimum number of swaps needed
5        queue<string> bfsQueue{{s1}};
6        unordered_set<string> visited{{s1}};
7        int swapCount = 0;
8      
9        // Continue BFS until we find the target string
10        while (true) {
11            // Process all strings at current level
12            int currentLevelSize = bfsQueue.size();
13            for (int i = 0; i < currentLevelSize; ++i) {
14                string currentString = bfsQueue.front();
15                bfsQueue.pop();
16              
17                // Check if we've reached the target string
18                if (currentString == s2) {
19                    return swapCount;
20                }
21              
22                // Generate all possible next states by swapping
23                vector<string> nextStates = generateNextStates(currentString, s2);
24                for (const string& nextState : nextStates) {
25                    // Only process unvisited states to avoid cycles
26                    if (visited.find(nextState) == visited.end()) {
27                        visited.insert(nextState);
28                        bfsQueue.push(nextState);
29                    }
30                }
31            }
32            // Increment swap count after processing each level
33            ++swapCount;
34        }
35    }
36
37private:
38    vector<string> generateNextStates(string& currentString, string& targetString) {
39        int stringLength = currentString.size();
40      
41        // Find the first position where strings differ
42        int firstDifferentPos = 0;
43        while (firstDifferentPos < stringLength && 
44               currentString[firstDifferentPos] == targetString[firstDifferentPos]) {
45            ++firstDifferentPos;
46        }
47      
48        vector<string> nextStates;
49      
50        // Try swapping with all valid positions after the first difference
51        for (int j = firstDifferentPos + 1; j < stringLength; ++j) {
52            // Only swap if:
53            // 1. Character at j matches what we need at firstDifferentPos
54            // 2. Character at j is not already in its correct position
55            if (currentString[j] == targetString[firstDifferentPos] && 
56                currentString[j] != targetString[j]) {
57                // Perform swap
58                swap(currentString[firstDifferentPos], currentString[j]);
59                nextStates.push_back(currentString);
60                // Restore original string for next iteration
61                swap(currentString[firstDifferentPos], currentString[j]);
62            }
63        }
64      
65        return nextStates;
66    }
67};
68
1function kSimilarity(s1: string, s2: string): number {
2    // Use BFS to find minimum number of swaps needed
3    const bfsQueue: string[] = [s1];
4    const visited: Set<string> = new Set([s1]);
5    let swapCount = 0;
6  
7    // Continue BFS until we find the target string
8    while (true) {
9        // Process all strings at current level
10        const currentLevelSize = bfsQueue.length;
11        for (let i = 0; i < currentLevelSize; i++) {
12            const currentString = bfsQueue.shift()!;
13          
14            // Check if we've reached the target string
15            if (currentString === s2) {
16                return swapCount;
17            }
18          
19            // Generate all possible next states by swapping
20            const nextStates = generateNextStates(currentString, s2);
21            for (const nextState of nextStates) {
22                // Only process unvisited states to avoid cycles
23                if (!visited.has(nextState)) {
24                    visited.add(nextState);
25                    bfsQueue.push(nextState);
26                }
27            }
28        }
29        // Increment swap count after processing each level
30        swapCount++;
31    }
32}
33
34function generateNextStates(currentString: string, targetString: string): string[] {
35    const stringLength = currentString.length;
36  
37    // Find the first position where strings differ
38    let firstDifferentPos = 0;
39    while (firstDifferentPos < stringLength && 
40           currentString[firstDifferentPos] === targetString[firstDifferentPos]) {
41        firstDifferentPos++;
42    }
43  
44    const nextStates: string[] = [];
45  
46    // Try swapping with all valid positions after the first difference
47    for (let j = firstDifferentPos + 1; j < stringLength; j++) {
48        // Only swap if:
49        // 1. Character at j matches what we need at firstDifferentPos
50        // 2. Character at j is not already in its correct position
51        if (currentString[j] === targetString[firstDifferentPos] && 
52            currentString[j] !== targetString[j]) {
53            // Perform swap by converting string to array, swapping, and converting back
54            const charArray = currentString.split('');
55            [charArray[firstDifferentPos], charArray[j]] = [charArray[j], charArray[firstDifferentPos]];
56            nextStates.push(charArray.join(''));
57        }
58    }
59  
60    return nextStates;
61}
62

Time and Space Complexity

Time Complexity: O(n! * n^2) in the worst case, where n is the length of the strings.

  • The algorithm uses BFS to explore all possible states (string permutations) that can be reached through swaps
  • In the worst case, we might need to explore up to O(n!) different permutations of the string
  • For each state processed, the next() function takes O(n^2) time:
    • Finding the first mismatched position: O(n)
    • Iterating through remaining positions to find swap candidates: O(n)
    • Creating new strings after swapping: O(n) for each swap
  • The total number of states visited is bounded by the number of permutations that differ from the target by at most k swaps, where k is the minimum number of swaps needed
  • Therefore, the overall time complexity is O(n! * n^2)

Space Complexity: O(n! * n) in the worst case.

  • The vis set can store up to O(n!) different string permutations in the worst case
  • Each string has length n, so storing each string takes O(n) space
  • The BFS queue can also contain up to O(n!) strings at any level
  • Therefore, the total space complexity is O(n! * n)

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

Common Pitfalls

1. Inefficient Swap Selection - Swapping Without Strategic Planning

A common mistake is to generate all possible swaps at each step, rather than focusing on productive swaps that fix at least one mismatched position. This leads to an exponential explosion of states and makes the algorithm impractically slow.

Problematic Approach:

# BAD: Trying all possible swaps
def get_all_swaps(s):
    result = []
    for i in range(len(s)):
        for j in range(i + 1, len(s)):
            # Swap any two positions
            swapped = list(s)
            swapped[i], swapped[j] = swapped[j], swapped[i]
            result.append(''.join(swapped))
    return result

Why it fails: This generates O(nΒ²) states at each level, leading to exponential growth in the search space. Many of these swaps don't make progress toward the target.

Solution: Always fix the first mismatched position and only swap with positions that:

  1. Have the correct character needed at the first mismatch
  2. Are themselves in wrong positions (to avoid wasting a swap)

2. String Reconstruction Error - Incorrect Slicing

When constructing the swapped string, a subtle but critical error is using the wrong base string for the prefix portion.

Problematic Code:

# WRONG: Using current string for prefix
swapped_string = (current_string[:first_diff_index + 1] +  # Bug here!
                 current_string[first_diff_index + 1:swap_index] + 
                 current_string[first_diff_index] + 
                 current_string[swap_index + 1:])

Why it fails: The positions before first_diff_index might not all be correct yet. By using current_string[:first_diff_index + 1], you include the mismatched character at position first_diff_index.

Correct Approach:

# CORRECT: Using target string for already-matched prefix
swapped_string = (s2[:first_diff_index + 1] +  # Use s2 for matched portion
                 current_string[first_diff_index + 1:swap_index] + 
                 current_string[first_diff_index] + 
                 current_string[swap_index + 1:])

This ensures that:

  • All positions up to and including first_diff_index are correct (matching s2)
  • The swap is properly reflected in the remaining portion

3. Redundant State Exploration - Missing Early Termination

Problematic Pattern:

# Processing unnecessary swaps even when strings match
def get_next_states(current_string):
    # Missing check for already matching strings
    next_states = []
    for i in range(len(current_string)):
        if current_string[i] != s2[i]:
            # Generate swaps...

Solution: Always check if the current position matches the target before trying to generate swaps:

def get_next_states(current_string):
    # Find first mismatch - will skip all matching positions
    first_diff_index = 0
    while first_diff_index < len(current_string) and 
          current_string[first_diff_index] == s2[first_diff_index]:
        first_diff_index += 1
  
    if first_diff_index == len(current_string):
        return []  # All positions match

4. Memory Inefficiency - Not Pruning Invalid Swaps

Issue: Including swaps where the character at position j is already in its correct position wastes memory and processing time.

Example: If current = "acb" and target = "abc", swapping positions 0 and 2 would give "bca", but this moves 'c' from its correct position, making it counterproductive.

Solution: Always check current_string[swap_index] != s2[swap_index] before including a swap in the next states. This ensures we only move characters that need to be moved eventually.

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

Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.

Which of the following method should we use to solve this problem?


Recommended Readings

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

Load More