Minimum Length of Anagram Concatenation
MedPrompt
You are given a string s, which is known to be a concatenation of anagrams of some string t.
Return the minimum possible length of the string t.
An anagram is formed by rearranging the letters of a string. For example, "aab", "aba", and, "baa" are anagrams of "aab".
Example 1:
Input: s = "abba"
Output: 2
Explanation:
One possible string t could be "ba".
Example 2:
Input: s = "cdef"
Output: 4
Explanation:
One possible string t could be "cdef", notice that t can be equal to s.
Example 2:
Input: s = "abcbcacabbaccba"
Output: 3
Constraints:
1 <= s.length <= 105sconsist only of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
A straightforward approach is to test every possible length k for the string t. Since s is a concatenation of anagrams of t, the length of t, k, must be a divisor of the length of s, n. We can iterate through all divisors of n from smallest to largest. For each potential length k, we verify if s can be partitioned into n/k segments, all of which are anagrams of the first segment s[0...k-1]. Anagram checking is done by sorting each segment and comparing it to the sorted version of the first segment.
Algorithm
- The fundamental observation is that if
sis a concatenation of anagrams of a stringtof lengthk, thenkmust be a divisor of the length ofs, let's call itn. - This approach iterates through all possible lengths
kthat are divisors ofn. - To check if two strings are anagrams, we can sort their characters. If the sorted strings are equal, they are anagrams.
- The algorithm is as follows:
- Find all divisors of
n = s.length(). - Sort the divisors in ascending order.
- For each divisor
k: a. Take the first substring of lengthk,s.substring(0, k). Sort its characters to create a canonical representation,canonical_t. b. Iterate through the rest of the string in chunks of sizek. c. For each chunk, sort its characters and compare it withcanonical_t. d. If any chunk's sorted version doesn't matchcanonical_t, thenkis not a valid length. We break and try the next larger divisor. e. If all chunks match, we have found the smallest possible length fort. We returnk.
- Find all divisors of
Walkthrough
import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.List; class Solution { public int minAnagramLength(String s) { int n = s.length(); List<Integer> divisors = findDivisors(n); Collections.sort(divisors); for (int k : divisors) { if (isPossible(s, k)) { return k; } } return n; // Should not be reached given problem constraints } private List<Integer> findDivisors(int n) { List<Integer> divisors = new ArrayList<>(); for (int i = 1; i * i <= n; i++) { if (n % i == 0) { divisors.add(i); if (i * i != n) { divisors.add(n / i); } } } return divisors; } private boolean isPossible(String s, int k) { int n = s.length(); String firstSub = s.substring(0, k); char[] firstChars = firstSub.toCharArray(); Arrays.sort(firstChars); String sortedFirst = new String(firstChars); for (int i = k; i < n; i += k) { String currentSub = s.substring(i, i + k); char[] currentChars = currentSub.toCharArray(); Arrays.sort(currentChars); String sortedCurrent = new String(currentChars); if (!sortedFirst.equals(sortedCurrent)) { return false; } } return true; }}Complexity
Time
O(d(n) * n log n). Finding divisors takes `O(sqrt(n))`. For each of the `d(n)` divisors, we perform a check. The check for a given length `k` involves `n/k` substrings. Sorting each substring of length `k` takes `O(k log k)`. Thus, the check costs `O((n/k) * k log k) = O(n log k)`. The total complexity is dominated by checks for larger `k`, approaching `O(d(n) * n log n)`.
Space
O(d(n) + n). `O(d(n))` to store the divisors, where `d(n)` is the number of divisors of `n`. Additionally, `O(k)` space is required for character arrays during sorting, which can be up to `O(n)` in the worst case.
Trade-offs
Pros
Conceptually simple and easy to follow.
Correctly identifies the minimal length by checking divisors in increasing order.
Cons
The process of creating substrings and sorting them repeatedly is computationally expensive.
Time complexity is high, making it unsuitable for large inputs as it might lead to a 'Time Limit Exceeded' error.
We can optimize the anagram checking part of the previous approach. Sorting each substring is inefficient. A better way to check if two strings are anagrams is to compare their character counts. We can use an array of size 26 to store the frequency of each character. This reduces the cost of checking one block from O(k log k) to O(k), leading to a significant overall performance improvement.
Algorithm
- This approach is similar to the first one but uses a more efficient method for the anagram check.
- Instead of sorting, we use frequency maps (e.g., an array of size 26 for lowercase English letters) to represent the character counts of a string.
- Two strings are anagrams if and only if their frequency maps are identical.
- The algorithm is as follows:
- Find all divisors of
n = s.length()and sort them. - For each divisor
k: a. Compute the frequency map of the first substrings[0...k-1]. b. Iterate through the subsequentn/k - 1blocks of lengthk. c. For each block, compute its frequency map. d. Compare the current block's frequency map with the first block's map. This comparison is a constant time operation (O(26)). e. If any map differs,kis invalid. Continue to the next divisor. f. If all blocks have identical frequency maps,kis the minimal length. Returnk.
- Find all divisors of
Walkthrough
import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.List; class Solution { public int minAnagramLength(String s) { int n = s.length(); List<Integer> divisors = findDivisors(n); Collections.sort(divisors); for (int k : divisors) { if (isPossible(s, k)) { return k; } } return n; } private List<Integer> findDivisors(int n) { List<Integer> divisors = new ArrayList<>(); for (int i = 1; i * i <= n; i++) { if (n % i == 0) { divisors.add(i); if (i * i != n) { divisors.add(n / i); } } } return divisors; } private boolean isPossible(String s, int k) { int n = s.length(); int[] firstBlockFreq = new int[26]; for (int i = 0; i < k; i++) { firstBlockFreq[s.charAt(i) - 'a']++; } for (int i = k; i < n; i += k) { int[] currentBlockFreq = new int[26]; for (int j = 0; j < k; j++) { currentBlockFreq[s.charAt(i + j) - 'a']++; } if (!Arrays.equals(firstBlockFreq, currentBlockFreq)) { return false; } } return true; }}Complexity
Time
O(d(n) * n). Finding divisors is `O(sqrt(n))`. For each divisor `k`, the check takes `O((n/k) * k) = O(n)`. The total time is `O(d(n) * n)`. Since `d(n)` is significantly smaller than `n` (for `n=10^5`, max `d(n)` is 128), this is a major improvement.
Space
O(d(n)). `O(d(n))` to store divisors. The frequency maps require constant space, `O(26)`.
Trade-offs
Pros
More efficient than the sorting-based approach.
Reduces the complexity of the anagram check from
O(k log k)toO(k).
Cons
While better than sorting, the check for each divisor still requires iterating through the entire string, which can be inefficient if the number of divisors is large or the correct
kis large.
To achieve the best performance, we can further optimize the check for each divisor. The previous approach re-calculates the frequency map for each block from scratch, which involves iterating over the block's characters. We can avoid this by pre-calculating the prefix sums of character frequencies for the entire string s. With this pre-computation, we can find the character counts of any substring in constant time. This dramatically speeds up the check for each divisor k from O(n) to O(n/k).
Algorithm
- This approach optimizes the check for each divisor
kby pre-calculating prefix sums of character frequencies. - A prefix sum table allows us to find the frequency map of any substring
s[i...j]inO(1)time (specificallyO(26)), by subtracting the prefix sums atifrom the prefix sums atj+1. - The algorithm is as follows:
- Precompute a prefix sum table
prefixCountsof size(n+1) x 26.prefixCounts[i][c]stores the count of charactercins[0...i-1]. This takesO(n)time. - Find and sort all divisors of
n. - For each divisor
k: a. Determine the frequency map of the first blocks[0...k-1]usingprefixCounts[k]. This is anO(1)operation. b. Iterate through the subsequent blocks. For a block starting at indexi, its frequency map is found byprefixCounts[i+k] - prefixCounts[i], also anO(1)operation. c. Compare this map with the first block's map. d. If there's a mismatch,kis invalid. e. If all blocks match,kis the answer. Returnk.
- Precompute a prefix sum table
Walkthrough
import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.List; class Solution { public int minAnagramLength(String s) { int n = s.length(); int[][] prefixCounts = new int[n + 1][26]; for (int i = 0; i < n; i++) { for (int j = 0; j < 26; j++) { prefixCounts[i + 1][j] = prefixCounts[i][j]; } prefixCounts[i + 1][s.charAt(i) - 'a']++; } List<Integer> divisors = findDivisors(n); Collections.sort(divisors); for (int k : divisors) { if (isPossible(n, k, prefixCounts)) { return k; } } return n; } private List<Integer> findDivisors(int n) { List<Integer> divisors = new ArrayList<>(); for (int i = 1; i * i <= n; i++) { if (n % i == 0) { divisors.add(i); if (i * i != n) { divisors.add(n / i); } } } return divisors; } private boolean isPossible(int n, int k, int[][] prefixCounts) { int[] firstBlockFreq = new int[26]; for (int j = 0; j < 26; j++) { firstBlockFreq[j] = prefixCounts[k][j]; } for (int i = k; i < n; i += k) { int[] currentBlockFreq = new int[26]; for (int j = 0; j < 26; j++) { currentBlockFreq[j] = prefixCounts[i + k][j] - prefixCounts[i][j]; } if (!Arrays.equals(firstBlockFreq, currentBlockFreq)) { return false; } } return true; }}Complexity
Time
O(n + σ₁(n)). The precomputation of prefix sums takes `O(n)`. The check for a given divisor `k` takes `O((n/k) * 26) = O(n/k)`. The total time for checking all divisors is `Σ_{k|n} O(n/k) = O(Σ_{k|n} n/k) = O(Σ_{d|n} d) = O(σ₁(n))`, where `σ₁(n)` is the sum of the divisors of `n`. This complexity is very efficient, roughly `O(n log log n)` for typical numbers.
Space
O(n). The prefix sum table requires `O(n * 26)` space, which simplifies to `O(n)`.
Trade-offs
Pros
Highly efficient and the optimal solution for this problem.
Reduces the check for each divisor
ktoO(n/k), making the total time complexity much better.
Cons
Requires additional space proportional to the input string length, which might be a concern for very large strings in memory-constrained environments.
Solutions
Solution
class Solution {private int n;private char[] s;private int[] cnt = new int[26];public int minAnagramLength(String s) { n = s.length(); this.s = s.toCharArray(); for (int i = 0; i < n; ++i) { ++cnt[this.s[i] - 'a']; } for (int i = 1;; ++i) { if (n % i == 0 && check(i)) { return i; } } }private boolean check(int k) { for (int i = 0; i < n; i += k) { int[] cnt1 = new int[26]; for (int j = i; j < i + k; ++j) { ++cnt1[s[j] - 'a']; } for (int j = 0; j < 26; ++j) { if (cnt1[j] * (n / k) != cnt[j]) { return false; } } } return true; }}class Solution {public: int minAnagramLength(string s) { int n = s.size(); int cnt[26]{}; for (char c : s) { cnt[c - 'a']++; } auto check = [&](int k) { for (int i = 0; i < n; i += k) { int cnt1[26]{}; for (int j = i; j < i + k; ++j) { cnt1[s[j] - 'a']++; } for (int j = 0; j < 26; ++j) { if (cnt1[j] * (n / k) != cnt[j]) { return false; } } } return true; }; for (int i = 1;; ++i) { if (n % i == 0 && check(i)) { return i; } } }};class Solution: def minAnagramLength(self, s: str) -> int: def check(k: int) -> bool: for i in range(0, n, k): cnt1 = Counter(s[i: i + k]) for c, v in cnt . items(): if cnt1[c] * (n // k) != v: return False return True cnt = Counter(s) n = len(s) for i in range(1, n + 1): if n % i == 0 and check(i): return iVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.