Given an array of strings words[], the task is to return the longest string chain. A string chain is defined as a sequence of words where:
Each word (except the first) can be formed by inserting exactly one character into the previous word.
The first word of the chain can be any word from the words[] array.
The task is to determine the length of the longest chain.
Input: words = ["a", "ab", "abc", "abcd", "abcde"]
Output: 5
Explanation: The longest chain is ["a", "ab", "abc", "abcd", "abcde"].
Each word in the chain is formed by adding exactly one character to the previous word.
Input: words = ["dog", "dogs", "dots", "dot", "d", "do"]
Output: 4
Explanation: The longest chain is ["d", "do", "dot", "dots"].
Each word is formed by inserting one character into the previous word.
Input: words = ["a", "aa", "aaa", "aaaa", "b", "bb", "bbb"]
A naive approach to solve this problem is to generate all the possible chains of string (using Power Set or Recursion method), and then store the longest among them. Clearly, this approach will not be a useful approach for the given problem as it will end up taking O(2N), where N is the number of elements in the given array, i.e., exponential time complexity.
/*It is pseudocode and it is not tied to
any specific programming language*/
/* Function to return the length of LIS starting from
index i with the element prev_ind element refers to
the previously selected element in the subsequence*/
func(int i, int prev_ind, int arr[]) {
// Not Take case
int notTake = func(i+1, prev_ind, arr);
// Take case
int take = 0;
// If no element is chosen till now
if(prev_ind == -1)
take = func(i+1, i, arr) + 1;
/* Else the current element can be
taken if it is strictly increasing */
else if(arr[i] > arr[prev_ind])
take = func(i+1, i, arr) + 1;
// Return the maximum length obtained from both cases
return max(take, notTake);
}
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
int longestStrChain(vector<string>& words) {
unordered_map<std::string, int> chains; // Stores the max chain length for each word
vector<std::string> sortedWords = words;
sort(sortedWords.begin(), sortedWords.end(), [](const std::string& a, const std::string& b) {
return a.length() < b.length(); // Sort words by length
});
for (const std::string& word : sortedWords) {
chains[word] = 1; // Initialize the chain length for the current word
for (int i = 0; i < word.length(); i++) {
std::string pred = word.substr(0, i) + word.substr(i + 1); // Generate predecessor by removing one character
if (chains.find(pred) != chains.end()) {
chains[word] = std::max(chains[word], chains[pred] + 1);
}
}
}
int maxChainLength = 0;
for (const auto& entry : chains) {
maxChainLength = std::max(maxChainLength, entry.second);
}
return maxChainLength;
}
};
int main() {
vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
// Creating an object of Solution class
Solution sol;
int lengthOfLIS = sol.LIS(nums);
cout << "The length of the LIS for the given array is: " << lengthOfLIS << endl;
return 0;
}
import java.util.*;
class Solution {
public int longestStrChain(String[] words) {
Map<String, Integer> chains = new HashMap<>(); // Stores the max chain length for each word
String[] sortedWords = Arrays.copyOf(words, words.length);
Arrays.sort(sortedWords, (a, b) -> a.length() - b.length()); // Sort words by length
for (String word : sortedWords) {
chains.put(word, 1); // Initialize the chain length for the current word
for (int i = 0; i < word.length(); i++) {
String pred = word.substring(0, i) + word.substring(i + 1); // Generate predecessor by removing one character
if (chains.containsKey(pred)) {
chains.put(word, Math.max(chains.getOrDefault(word, 0), chains.get(pred) + 1));
}
}
}
int maxChainLength = chains.values().stream().mapToInt(Integer::intValue).max().orElse(0);
return maxChainLength;
}
}
class Main {
public static void main(String[] args) {
int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
// Creating an object of Solution class
Solution sol = new Solution();
int lengthOfLIS = sol.LIS(nums);
System.out.println("The length of the LIS for the given array is: " + lengthOfLIS);
}
}
class Solution:
def longestStrChain(self, words: List[str]) -> int:
chains = {} # Stores the max chain length for each word
sorted_words = sorted(words, key=len) # Sort words by length
for word in sorted_words:
chains[word] = 1 # Initialize the chain length for the current word
for i in range(len(word)):
# Generate predecessor by removing one character
pred = word[:i] + word[i+1:]
if pred in chains:
chains[word] = max(chains[word], chains[pred] + 1)
return max(chains.values())
# Example usage
nums = [10, 9, 2, 5, 3, 7, 101, 18]
# Creating an object of Solution class
sol = Solution()
lengthOfLIS = sol.LIS(nums)
print("The length of the LIS for the given array is:", lengthOfLIS)
var longestStrChain = function(words) {
const chains = new Map(); // Stores the max chain length for each word
const sortedWords = words.slice().sort((a, b) => a.length - b.length); // Sort words by length
for (const word of sortedWords) {
chains.set(word, 1); // Initialize the chain length for the current word
for (let i = 0; i < word.length; i++) {
const pred = word.slice(0, i) + word.slice(i + 1); // Generate predecessor by removing one character
if (chains.has(pred)) {
chains.set(word, Math.max(chains.get(word) || 0, chains.get(pred) + 1));
}
}
}
return Math.max(...Array.from(chains.values())); // Return the maximum chain length
};
// Example usage
let nums = [10, 9, 2, 5, 3, 7, 101, 18];
// Creating an object of Solution class
let sol = new Solution();
let lengthOfLIS = sol.LIS(nums);
console.log("The length of the LIS for the given array is:", lengthOfLIS);
If you draw the recursion tree, you will see that there are overlapping subproblems. Hence the DP approache can be applied to the recursive solution to optimise it.
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
int longestStrChain(vector<string>& words) {
unordered_map<std::string, int> chains; // Stores the max chain length for each word
vector<std::string> sortedWords = words;
sort(sortedWords.begin(), sortedWords.end(), [](const std::string& a, const std::string& b) {
return a.length() < b.length(); // Sort words by length
});
for (const std::string& word : sortedWords) {
chains[word] = 1; // Initialize the chain length for the current word
for (int i = 0; i < word.length(); i++) {
std::string pred = word.substr(0, i) + word.substr(i + 1); // Generate predecessor by removing one character
if (chains.find(pred) != chains.end()) {
chains[word] = std::max(chains[word], chains[pred] + 1);
}
}
}
int maxChainLength = 0;
for (const auto& entry : chains) {
maxChainLength = std::max(maxChainLength, entry.second);
}
return maxChainLength;
}
};
int main() {
vector<int> nums = {10, 9, 2, 5, 3, 7, 101, 18};
// Creating an object of Solution class
Solution sol;
int lengthOfLIS = sol.LIS(nums);
cout << "The length of the LIS for the given array is: " << lengthOfLIS << endl;
return 0;
}
import java.util.*;
class Solution {
public int longestStrChain(String[] words) {
Map<String, Integer> chains = new HashMap<>(); // Stores the max chain length for each word
String[] sortedWords = Arrays.copyOf(words, words.length);
Arrays.sort(sortedWords, (a, b) -> a.length() - b.length()); // Sort words by length
for (String word : sortedWords) {
chains.put(word, 1); // Initialize the chain length for the current word
for (int i = 0; i < word.length(); i++) {
String pred = word.substring(0, i) + word.substring(i + 1); // Generate predecessor by removing one character
if (chains.containsKey(pred)) {
chains.put(word, Math.max(chains.getOrDefault(word, 0), chains.get(pred) + 1));
}
}
}
int maxChainLength = chains.values().stream().mapToInt(Integer::intValue).max().orElse(0);
return maxChainLength;
}
}
class Main {
public static void main(String[] args) {
int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
// Creating an object of Solution class
Solution sol = new Solution();
int lengthOfLIS = sol.LIS(nums);
System.out.println("The length of the LIS for the given array is: " + lengthOfLIS);
}
}
class Solution:
def longestStrChain(self, words: List[str]) -> int:
chains = {} # Stores the max chain length for each word
sorted_words = sorted(words, key=len) # Sort words by length
for word in sorted_words:
chains[word] = 1 # Initialize the chain length for the current word
for i in range(len(word)):
# Generate predecessor by removing one character
pred = word[:i] + word[i+1:]
if pred in chains:
chains[word] = max(chains[word], chains[pred] + 1)
return max(chains.values())
# Example usage
nums = [10, 9, 2, 5, 3, 7, 101, 18]
# Creating an object of Solution class
sol = Solution()
lengthOfLIS = sol.LIS(nums)
print("The length of the LIS for the given array is:", lengthOfLIS)
var longestStrChain = function(words) {
const chains = new Map(); // Stores the max chain length for each word
const sortedWords = words.slice().sort((a, b) => a.length - b.length); // Sort words by length
for (const word of sortedWords) {
chains.set(word, 1); // Initialize the chain length for the current word
for (let i = 0; i < word.length; i++) {
const pred = word.slice(0, i) + word.slice(i + 1); // Generate predecessor by removing one character
if (chains.has(pred)) {
chains.set(word, Math.max(chains.get(word) || 0, chains.get(pred) + 1));
}
}
}
return Math.max(...Array.from(chains.values())); // Return the maximum chain length
};
// Example usage
let nums = [10, 9, 2, 5, 3, 7, 101, 18];
// Creating an object of Solution class
let sol = new Solution();
let lengthOfLIS = sol.LIS(nums);
console.log("The length of the LIS for the given array is:", lengthOfLIS);
Q: Why do we sort words[] by length?
A: Sorting ensures that when processing words[i], all potential shorter predecessors are already considered. This allows bottom-up DP processing to track valid chains efficiently.
Q: Why is this similar to DAG longest path?
A: This problem can be modeled as a Directed Acyclic Graph (DAG) where an edge exists from prev → curr if prev is a valid predecessor. The longest path in this DAG gives the answer.
Q: How would you reconstruct the actual longest string chain instead of just its length?
A: Maintain a parent map where parent[word] = previous_word for the longest chain found. Start from the word with the highest dp[word] and backtrack.
Q: What if insertions, deletions, and replacements were allowed?
A: This becomes the Edit Distance Problem instead of a Predecessor Chain Problem, requiring dynamic programming with three operations.