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"
ands2 = "ba"
, then they are 1-similar because one swap (swapping positions 0 and 1) transformss1
intos2
- If
s1 = "abc"
ands2 = "bca"
, then they are 2-similar because you need at least 2 swaps to transforms1
intos2
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
tos2
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
intos2
, 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:
- We're exploring a state space (different string configurations)
- Each transformation (swap) has equal cost
- We want the minimum number of transformations
- BFS guarantees finding the shortest path in an unweighted graph
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:
- Find the first position where
s1
ands2
differ - Only swap this mismatched character with positions that would place the correct character (
s2[i]
) in this position - Additionally, we only swap with positions
j
wheres[j] != s2[j]
(the character at positionj
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 operationsvis
(set): Tracks visited string configurations to avoid revisiting statesans
: 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 positionj
is what we need at positioni
s[j] != s2[j]
: Character at positionj
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 EvaluatorExample 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) ands2[1] = 'c'
(so'b' != 'c'
, also mismatched) β - Swap positions 0 and 1:
"abc"
β"bac"
- Position 1:
- Return
["bac"]
- Find first mismatch: position 0 (
- Add
"bac"
to queue and visited set
- Check if
- 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) ands2[2] = 'a'
(so'c' != 'a'
, also mismatched) β - Swap positions 1 and 2:
"bac"
β"bca"
- Position 2:
- Return
["bca"]
- Find first mismatch: position 1 (
- Add
"bca"
to queue and visited set
- Check if
- Queue after level:
["bca"]
- Increment ans to 2
Level 2 (ans = 2):
- Process
"bca"
:- Check if
"bca" == "bca"
: Yes! β - Return ans = 2
- Check if
The algorithm found that the minimum number of swaps needed is 2:
- Swap positions 0 and 1:
"abc"
β"bac"
- Swap positions 1 and 2:
"bac"
β"bca"
The key optimization is that at each step, we only consider swaps that:
- Fix the first mismatched position
- 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 takesO(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
- Finding the first mismatched position:
- The total number of states visited is bounded by the number of permutations that differ from the target by at most
k
swaps, wherek
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 toO(n!)
different string permutations in the worst case - Each string has length
n
, so storing each string takesO(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:
- Have the correct character needed at the first mismatch
- 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.
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
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!