Strings in C++: Hard Coding Round Problems
This chapter covers advanced string problems frequently encountered in technical interviews. By leveraging the Knuth-Morris-Pratt (KMP) algorithm and the Longest Prefix Suffix (LPS) array, we can optimize complex string manipulation tasks—such as palindromic extensions, circular pattern matching, and periodic repetitions—from naive polynomial time complexities down to highly efficient linear time complexities.
1. Why Do We Need This?
In technical coding rounds, string manipulation problems often seem straightforward at first glance. Interviewers frequently present problems that can be solved using intuitive brute-force methods (such as nested loops or greedy two-pointer approaches). However, these naive solutions usually run in or time complexity, which will result in a Time Limit Exceeded (TLE) status on modern competitive programming platforms.
To pass rigorous coding rounds, you must learn to recognize how advanced pattern-matching algorithms—specifically the LPS (Longest Prefix Suffix) array from the KMP algorithm—can be adapted to solve problems that do not look like standard pattern-matching tasks at first glance.
2. Problem 1: Minimum Characters to Add at the Start to Make a String Palindrome
Problem Statement
Given a string , you are allowed to add characters only at the start of the string. Your goal is to find the minimum number of characters to add to transform into a palindrome.
Examples
- Example 1:
S = "abc"- If we prepend
"cb", the string becomes"cbabc", which is a palindrome. - Minimum characters added =
2.
- If we prepend
- Example 2:
S = "rorasp"- If we prepend
"ps", the string becomes"psrorasp", which is a palindrome (p-s-r-o-r-a-s-pis not symmetric, but if we look closely at"ror", it is already a palindrome. Prepending"psa"to"rorasp"yields"asprorasp", which is still not a palindrome. Let's look at the prefix"ror". If we prepend"psa", we get"asprorasp"which is a palindrome:a-s-p-r-o-r-a-s-p). - Minimum characters added =
2(by prepending"ps"to make"psrorasp", wait: is"psrorasp"a palindrome? Let's check:p-s-r-o-r-a-s-preversed isp-s-a-r-o-r-s-p. They do not match because of the'a'. The correct prepended string to make"rorasp"a palindrome is"psa"to form"asprorasp"? No,"asprorasp"reversed is"psarorpsa". Let's re-evaluate: the longest palindromic prefix of"rorasp"is"ror". The remaining suffix is"asp". If we reverse"asp"to get"psa"and prepend it, we get"psarorasp". Let's check"psarorasp": reversed it is"psarorasp". Yes! This is a palindrome. The number of characters added is3("psa").
Note: The transcript mentions prepending
"p"and"s"to"rorasp"to get"psrorasp"as an intuitive trace, but mathematically, the longest palindromic prefix of"rorasp"is"ror"(length 3). The remaining characters are"asp"(length 3). Reversing"asp"gives"psa". Prepending"psa"gives"psarorasp", which is a valid palindrome of length 9. Thus, the minimum characters to add is indeed3. - If we prepend
3. Naive Two-Pointer Squeezing & Why It Fails
The Naive Mental Model
A common intuitive approach is to use two pointers, start at 0 and end at n-1, and attempt to match characters. If they match, we squeeze the pointers inward. If they mismatch, we assume we must prepend the character at end to the front of the string to force a match, and then decrement the end pointer.
Trace of the Naive Approach on S = "abc"
Step 1:
a b c
^ ^
S E (Mismatch: 'a' != 'c')
Action: Prepend 'c' to the virtual output. Decrement E.
Step 2:
a b c
^ ^
S E (Mismatch: 'a' != 'b')
Action: Prepend 'b' to the virtual output. Decrement E.
Step 3:
S and E meet at 'a'. Stop.
Characters prepended: "cb" (Count = 2).
Why This Greedy Approach Fails (Counter-Example)
Consider a more complex string like "aakaacaataa". If we apply this greedy squeezing logic:
- We compare the first
'a'and the last'a'. They match, so we move both pointers inward. - As soon as we hit a deep mismatch inside the string (e.g., comparing
'k'and't'), the greedy decision to simply prepend the mismatched character and decrement theendpointer breaks down. It fails to account for overlapping symmetric patterns and generates a sequence that is not a palindrome.
Thus, we cannot rely on a simple greedy two-pointer squeeze to solve this problem in time.
4. The Brute Force Approach
To find a foolproof solution, we must ask ourselves: What is the core structural requirement to make a string a palindrome by prepending characters?
If we prepend characters to to make it a palindrome, we are essentially looking for the Longest Palindromic Prefix of .
Real-Life Analogy: The Anchor
Think of the longest palindromic prefix as an "anchor" at the beginning of your string that is already perfectly symmetrical. You do not need to change or add anything to this anchor. You only need to take the remaining part of the string (the suffix that lies after the anchor), reverse it, and paste it at the very beginning.
Brute Force Algorithm
- Check if the entire string is a palindrome.
- If not, check if is a palindrome.
- If not, check if is a palindrome.
- Repeat this process, decrementing the end index by 1 each time, until you find a prefix that is a palindrome.
- If the longest palindromic prefix has length , the number of characters we need to add is .
Complexity Analysis
- Time Complexity: because in the worst case, we check if a substring is a palindrome times, and each check takes time.
- Space Complexity: auxiliary space.
5. The Optimal KMP/LPS Mental Model
To optimize this to , we must find the Longest Palindromic Prefix in linear time. We can achieve this by cleverly utilizing the Longest Prefix Suffix (LPS) array from the KMP algorithm.
The LPS Property
Recall that the LPS array stores the length of the longest proper prefix that is also a suffix for every substring ending at index i.
If we reverse the string to get , any palindromic prefix of will now appear at the end of as a suffix.
The Concatenation Trick
If we concatenate the original string and its reversed version with a special separator character $ in between:
The longest prefix of this combined string that is also a suffix of will correspond exactly to the longest palindromic prefix of .
Let S = "abc"
S_rev = "cba"
Combined String T = "abc$cba"
LPS Array of T:
Character: a b c $ c b a
Index: 0 1 2 3 4 5 6
LPS Value: 0 0 0 0 0 0 1
The last element of the LPS array is 1.
This means the longest palindromic prefix of "abc" has length 1 (which is "a").
Minimum characters to add = Original Length - LPS.back() = 3 - 1 = 2.
6. Step-by-Step Evolution: Why the Separator ($) is Critical
You might wonder: Why do we need to insert the $ separator? Can we not just concatenate ?
The Danger of Continuity (Overlapping Matches)
Consider the string .
- Without a separator: (length 8).
- If we compute the LPS array for
"aaaaaaaa":- The last element of the LPS array will be
7(since the proper prefix"aaaaaaa"matches the suffix"aaaaaaa"). - If we use our formula: . This is mathematically incorrect and physically meaningless.
- The last element of the LPS array will be
How the Separator Solves It
The separator $ acts as a barrier that prevents the prefix matching process from crossing over the boundary of the original string. Since $ is a unique character that does not exist in or , the maximum possible LPS value is capped at the length of .
With Separator: T = "aaaa$aaaa"
LPS Array of T:
Character: a b a a $ a a a a
Index: 0 1 2 3 4 5 6 7 8
LPS Value: 0 1 2 3 0 1 2 3 4
The last element of the LPS array is 4.
Minimum characters to add = 4 - 4 = 0. (Correct, "aaaa" is already a palindrome).
7. C++ Code Implementation for Problem 1
Below is the complete, production-grade C++ implementation to find the minimum characters to add at the start to make a string a palindrome.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
class Solution {
public:
int solve(string str) {
int original_size = str.size();
// Step 1: Create the reversed string
string rev = str;
reverse(rev.begin(), rev.end());
// Step 2: Concatenate original + "$" + reversed
string combined = str + "$" + rev;
int n = combined.size();
// Step 3: Compute the LPS array for the combined string
vector<int> lps(n, 0);
int len = 0; // Length of the previous longest prefix suffix
int i = 1;
while (i < n) {
if (combined[i] == combined[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
// Fall back to the previous longest prefix suffix
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
// Step 4: The last value of the LPS array gives the length of
// the longest palindromic prefix in the original string.
int longest_palindromic_prefix_len = lps[n - 1];
// Minimum characters to add = Original Size - Longest Palindromic Prefix Length
return original_size - longest_palindromic_prefix_len;
}
};
How to Retrieve the Actual Prepended String
If the interviewer asks you to return the actual modified palindromic string instead of just the count:
- Identify the number of characters to add: .
- Extract the last characters of the original string .
- Reverse these characters.
- Prepend them to .
8. Problem 2: Circular Pattern Matching
Problem Statement
You are given two strings: a text string and a pattern string . The text string is circular, meaning that the character after the last index wraps around to the first index . Determine if the pattern exists as a contiguous substring within this circular text .
Text A: c d e a a b r o a b
^ |
|_________________| (Circular Wrap-around)
The Concatenation Trick
Instead of writing complex pointer wrapping logic with modulo operations, we can use a simple structural trick: concatenate the circular string with itself.
Any circular substring of length (where ) will appear as a standard, contiguous substring within .
Let A = "cdeaabroab"
B = "abcde" (This wraps around from the end 'ab' to the start 'cde')
Doubled Text (A + A):
"cdeaabroab cdeaabroab"
^^^^^ ^
a b r o a b c d e ...
Notice that "abcde" appears contiguously inside A + A!
Algorithm Steps
- Concatenate with itself:
temp = A + A. - Run a standard KMP pattern search to find if is a substring of
temp. - If is found and , return
true(or1). Otherwise, returnfalse(or0).
Note: It is crucial to verify that . If , the pattern is longer than the circular text itself, which can lead to false positives where the pattern matches across multiple circular wraps.
9. Problem 3: Repeated String Match
Problem Statement
Given two strings and , find the minimum number of times we must repeat string such that becomes a substring of the repeated string. If it is impossible for to become a substring of no matter how many times we repeat , return -1.
Examples
- Example 1:
A = "abcd",B = "cdabcdab"- Repeat 1 time:
"abcd"(Length 4 < 8) - Repeat 2 times:
"abcdabcd"(Length 8 8). Is a substring? No. - Repeat 3 times:
"abcdabcdabcd"(Length 12). Is a substring? Yes:"ab[cdabcdab]cd". - Output:
3.
- Repeat 1 time:
- Example 2:
A = "abc",B = "cab"- Repeat 1 time:
"abc" - Repeat 2 times:
"abcabc". Is a substring? Yes:"a[cab]c". - Output:
2.
- Repeat 1 time:
10. Step-by-Step Walkthrough & Boundary Conditions of Repeated Match
The Core Intuition: How Many Times Must We Repeat?
To find the minimum repetitions, we can establish lower and upper bounds on the number of repetitions of .
- Lower Bound: We must repeat until its total length is at least the length of .
- Upper Bound: If is to be a substring of the repeated version of , it can start at any index within the first copy of .
- If it starts at index
0of the first copy, it fits within repetitions. - If it starts at index
1or later, it will spill over into an extra copy of . - Therefore, we only ever need to check at most and repetitions. If is not a substring after repetitions, it will never be a substring, because any further repetitions just repeat the same periodic pattern without changing the alignment boundaries.
- If it starts at index
Visualizing the Spillover
11. C++ Code Implementation for Problem 3
Here is the complete C++ solution using the KMP algorithm for pattern matching.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Solution {
private:
// Helper function: Standard KMP Pattern Matching
bool kmpSearch(const string& text, const string& pattern) {
int m = pattern.size();
int n = text.size();
if (m > n) return false;
// Step 1: Compute LPS array for the pattern
vector<int> lps(m, 0);
int len = 0;
int i = 1;
while (i < m) {
if (pattern[i] == pattern[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
// Step 2: Match pattern against text
int text_idx = 0;
int pat_idx = 0;
while (text_idx < n) {
if (text[text_idx] == pattern[pat_idx]) {
text_idx++;
pat_idx++;
}
if (pat_idx == m) {
return true; // Pattern found
} else if (text_idx < n && text[text_idx] != pattern[pat_idx]) {
if (pat_idx != 0) {
pat_idx = lps[pat_idx - 1];
} else {
text_idx++;
}
}
}
return false;
}
public:
int repeatedStringMatch(string A, string B) {
// Base Case: If strings are already identical
if (A == B) return 1;
int repeat_count = 1;
string temp = A;
// Step 1: Repeat A until its length is >= B's length
while (temp.size() < B.size()) {
temp += A;
repeat_count++;
}
// Step 2: Check if B is a substring of the current repeated string
if (kmpSearch(temp, B)) {
return repeat_count;
}
// Step 3: Append A one more time to handle boundary offsets
temp += A;
if (kmpSearch(temp, B)) {
return repeat_count + 1;
}
// Step 4: If still not found, it's impossible
return -1;
}
};
12. Synthesis and Comparison of Strategies
Summary and Key Takeaways
- The LPS array is highly versatile: It is not just for finding substrings. It can also find the longest repeating prefix-suffix patterns, which helps identify palindromic structures.
- The Longest Palindromic Prefix (LPP) of a string can be found in time by calculating the LPS of .
- Always use a unique separator (like
$or#) when concatenating strings for LPS calculations to prevent matches from crossing boundaries. - Circular string problems can almost always be simplified by doubling the string (), which converts circular indexing into a standard linear search.
- The repeated string match problem has a strict upper limit on repetitions: . If a match is not found within this limit, further repetitions will not help.