Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1055. Shortest Way to Form String - Leetcode Solution

Code Implementation

class Solution:
    def shortestWay(self, source: str, target: str) -> int:
        # Preprocess: build a mapping from char to all positions in source
        from collections import defaultdict
        pos = defaultdict(list)
        for idx, ch in enumerate(source):
            pos[ch].append(idx)
        res = 1
        i = 0  # pointer in target
        n = len(target)
        m = len(source)
        src_idx = 0  # pointer in source
        while i < n:
            ch = target[i]
            if ch not in pos:
                return -1
            # Binary search for first position >= src_idx
            import bisect
            idx_list = pos[ch]
            k = bisect.bisect_left(idx_list, src_idx)
            if k == len(idx_list):
                # Need to start a new subsequence
                res += 1
                src_idx = 0
            else:
                src_idx = idx_list[k] + 1
                i += 1
        return res
    
class Solution {
public:
    int shortestWay(string source, string target) {
        // Preprocess: map char to all indices
        unordered_map<char, vector<int>> pos;
        for (int i = 0; i < source.size(); ++i)
            pos[source[i]].push_back(i);
        int res = 1, i = 0, src_idx = 0;
        while (i < target.size()) {
            char ch = target[i];
            if (!pos.count(ch))
                return -1;
            auto& idxs = pos[ch];
            auto it = lower_bound(idxs.begin(), idxs.end(), src_idx);
            if (it == idxs.end()) {
                res++;
                src_idx = 0;
            } else {
                src_idx = *it + 1;
                i++;
            }
        }
        return res;
    }
};
    
class Solution {
    public int shortestWay(String source, String target) {
        Map<Character, List<Integer>> pos = new HashMap<>();
        for (int i = 0; i < source.length(); ++i) {
            pos.computeIfAbsent(source.charAt(i), k -> new ArrayList<>()).add(i);
        }
        int res = 1, i = 0, srcIdx = 0;
        while (i < target.length()) {
            char ch = target.charAt(i);
            if (!pos.containsKey(ch)) return -1;
            List<Integer> idxs = pos.get(ch);
            int k = Collections.binarySearch(idxs, srcIdx);
            if (k < 0) k = -k - 1;
            if (k == idxs.size()) {
                res++;
                srcIdx = 0;
            } else {
                srcIdx = idxs.get(k) + 1;
                i++;
            }
        }
        return res;
    }
}
    
var shortestWay = function(source, target) {
    // Preprocess: map char to all indices in source
    let pos = {};
    for (let i = 0; i < source.length; ++i) {
        if (!pos[source[i]]) pos[source[i]] = [];
        pos[source[i]].push(i);
    }
    let res = 1, i = 0, srcIdx = 0;
    while (i < target.length) {
        let ch = target[i];
        if (!pos[ch]) return -1;
        let idxs = pos[ch];
        // Binary search for first index >= srcIdx
        let l = 0, r = idxs.length;
        while (l < r) {
            let mid = (l + r) >> 1;
            if (idxs[mid] < srcIdx) l = mid + 1;
            else r = mid;
        }
        if (l == idxs.length) {
            res++;
            srcIdx = 0;
        } else {
            srcIdx = idxs[l] + 1;
            i++;
        }
    }
    return res;
};
    

Problem Description

You are given two strings, source and target. You are allowed to form the target string by repeatedly choosing subsequences from source and concatenating them together. A subsequence is formed by deleting some (possibly zero) characters from source without changing the order of the remaining characters. Your task is to find the minimum number of times you must take a subsequence from source (possibly reusing source multiple times) so that their concatenation equals target. If it is impossible to form target from source, return -1.

Key constraints:
  • Each subsequence must be built in order from source (cannot rearrange characters).
  • You may reuse source as many times as needed, but you must always start from the beginning after each use.
  • If a character in target does not appear in source, return -1.

Thought Process

At first glance, it seems you could try to match each character of target to source one by one, possibly restarting source whenever you reach the end or can't find a match. However, a brute-force approach of trying all possible subsequences would be far too slow.

The key insight is to process target left-to-right, greedily matching as many characters as possible in a single pass through source. Each time you can no longer match the next character (either because you reached the end of source or the character doesn't appear later), you restart from the beginning of source and keep going. To do this efficiently, you need a way to quickly find the next occurrence of each character in source after a certain index (this is where preprocessing and binary search come in).

Solution Approach

To solve this problem efficiently, follow these steps:
  1. Preprocessing: For each character in source, record all the indices where it appears. This lets you quickly find where a character occurs in source using a hash map or array.
  2. Iterate through target: Use two pointers: one for target and one for source. For each character in target, use binary search to find the next occurrence of that character in source at or after the current source pointer.
  3. Handling restarts:
    • If you find a valid match, move both pointers forward (advance source pointer to just after the matched character, and move to the next target character).
    • If you can't find a match (i.e., you've reached the end of source), increment your answer (number of subsequences), and restart the source pointer from the beginning.
    • If a character in target doesn't exist in source at all, return -1 immediately.
  4. Repeat this process: Continue until you've matched all characters in target. The total number of restarts plus the initial pass gives you the minimum number of subsequences needed.

By using a mapping from character to indices and binary search, you avoid scanning source repeatedly, making the solution efficient even for large strings.

Example Walkthrough

Example:
source = "abc", target = "abcbc"
Let's trace the process:
  1. First pass through source:
    • Match 'a' (source[0]), move to next character in both.
    • Match 'b' (source[1]), move to next character in both.
    • Try to match 'c' (source[2]), move to next character in both. Now, source is exhausted.
  2. Restart source (second subsequence):
    • Next target char is 'b'. Find 'b' at source[1], move both pointers.
    • Next target char is 'c'. Find 'c' at source[2], move both pointers.
  3. Now, all of target has been matched using 2 subsequences of source.

If target had a character not in source (e.g. 'd'), we would return -1.

Time and Space Complexity

  • Brute-force approach: Trying all possible subsequences would be exponential in time and not feasible for strings of any reasonable length.
  • Optimized approach (using preprocessing and binary search):
    • Time complexity: O(n + m * log n), where n is the length of source, and m is the length of target. Each character in target may require a binary search over the positions in source.
    • Space complexity: O(n), for storing the positions of each character in source.

This makes the solution efficient and practical for large inputs.

Summary

The key to solving "Shortest Way to Form String" efficiently is to process target greedily, always matching as many characters as possible in a single pass through source. By preprocessing source into a mapping of character positions and using binary search, we avoid unnecessary repeated scans and achieve optimal performance. This approach balances clarity and efficiency, making it an elegant solution to the problem.