Facebook Pixel

3138. Minimum Length of Anagram Concatenation

MediumHash TableStringCounting
Leetcode Link

Problem Description

You are given a string s that is formed by concatenating multiple anagrams of some unknown string t. Your task is to find the minimum possible length of string t.

An anagram is a rearrangement of the letters of a string. For example, "aab", "aba", and "baa" are all anagrams of each other.

The key insight is that string s is composed entirely of anagrams of t placed one after another. For instance:

  • If t = "ab", then s could be "abba" (concatenation of "ab" and "ba", both anagrams of "ab")
  • If t = "abc", then s could be "abcbcacab" (concatenation of "abc", "bca", and "cab")

Since s is made up of complete anagrams of t, the length of t must be a divisor of the length of s. The algorithm checks each possible divisor k of the length of s, starting from the smallest, to see if s can be partitioned into segments of length k where each segment is an anagram of the others.

The checking process works by:

  1. Counting the frequency of each character in the entire string s
  2. For each potential length k, verifying that when s is divided into segments of length k, each segment contains the same characters with the same frequencies
  3. This is validated by ensuring that the character count in any segment, when multiplied by the number of segments (n/k), equals the total character count in s

The function returns the smallest valid length k that satisfies these conditions.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that if string s is formed by concatenating anagrams of string t, then s must be divisible into equal-length segments where each segment is an anagram of t. This means the length of t must divide the length of s evenly.

Let's think about what this means mathematically. If s has length n and is made up of anagrams of t with length k, then we must have n = m * k for some integer m (the number of anagrams concatenated). This tells us that k must be a divisor of n.

Now, how do we verify if a particular length k is valid? If we split s into segments of length k, each segment should be an anagram of the others. Anagrams have the same character frequencies, so each segment should have identical character counts.

Here's the clever part: instead of comparing all segments pairwise, we can use a global property. If each segment has the same character frequencies, then the total count of any character c in the entire string s should equal the count of c in one segment multiplied by the number of segments. In other words, if character c appears x times in one segment and there are n/k segments, then c should appear exactly x * (n/k) times in the entire string s.

Since we want the minimum possible length, we enumerate divisors of n from smallest to largest. The first divisor that passes our validation check must be the answer, because any smaller valid length would have been found earlier in our enumeration.

This approach is efficient because we only need to count characters once for the entire string, and then for each potential length k, we just need to verify the character counts in the segments match our expected pattern.

Solution Approach

The implementation follows a systematic enumeration and validation approach:

Step 1: Character Frequency Counting

First, we count the frequency of each character in the entire string s using a Counter (hash table), storing it in cnt. This gives us the total occurrence of each character in the string.

Step 2: Enumerate Possible Lengths

We iterate through all possible lengths i from 1 to n (where n = len(s)). For each i, we check if it's a divisor of n by verifying n % i == 0. Only divisors are valid candidates for the length of string t.

Step 3: Validation Function check(k)

For each valid divisor k, we validate whether s can be split into anagrams of length k:

  • We iterate through s in chunks of size k using range(0, n, k)
  • For each chunk starting at position i, we extract the substring s[i : i + k]
  • We count the character frequencies in this chunk using Counter(s[i : i + k]), storing it in cnt1
  • We then verify that for every character c in the original cnt:
    • The equation cnt1[c] * (n // k) == cnt[c] must hold
    • This means: (count of c in one segment) × (number of segments) = (total count of c in s)
  • If any character fails this check, we return False
  • If all chunks pass the validation, we return True

Step 4: Return the Minimum Length

Since we enumerate lengths from smallest to largest, the first length i that passes the validation check is immediately returned as the answer. This guarantees we find the minimum possible length.

Time Complexity Analysis:

  • Counting characters in s: O(n)
  • For each divisor k, the check function examines n/k segments, each of length k, resulting in O(n) time
  • The number of divisors of n is at most O(√n)
  • Overall complexity: O(n√n)

Space Complexity: O(1) or O(26) for the character counter, since we're dealing with lowercase English letters.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with s = "abcabcbb".

Step 1: Count character frequencies in the entire string

  • cnt = {'a': 2, 'b': 4, 'c': 2}
  • Length n = 8

Step 2: Try each divisor of 8 from smallest to largest

Try length 1:

  • Check if 8 % 1 == 0? Yes, it's a divisor
  • Split into 8 segments: ['a'], ['b'], ['c'], ['a'], ['b'], ['c'], ['b'], ['b']
  • Take first segment ['a']: cnt1 = {'a': 1}
  • Validate: Does cnt1['a'] * 8 = 1 * 8 = 8 equal cnt['a'] = 2? No! (8 ≠ 2)
  • Length 1 fails ❌

Try length 2:

  • Check if 8 % 2 == 0? Yes, it's a divisor
  • Split into 4 segments: ['ab'], ['ca'], ['bc'], ['bb']
  • Take first segment ['ab']: cnt1 = {'a': 1, 'b': 1}
  • Validate:
    • Does cnt1['a'] * 4 = 1 * 4 = 4 equal cnt['a'] = 2? No! (4 ≠ 2)
  • Length 2 fails ❌

Try length 4:

  • Check if 8 % 4 == 0? Yes, it's a divisor
  • Split into 2 segments: ['abca'], ['bcbb']
  • Take first segment ['abca']: cnt1 = {'a': 2, 'b': 1, 'c': 1}
  • Validate:
    • Does cnt1['a'] * 2 = 2 * 2 = 4 equal cnt['a'] = 2? No! (4 ≠ 2)
  • Length 4 fails ❌

Try length 8:

  • Check if 8 % 8 == 0? Yes, it's a divisor
  • Split into 1 segment: ['abcabcbb']
  • Take first segment ['abcabcbb']: cnt1 = {'a': 2, 'b': 4, 'c': 2}
  • Validate:
    • Does cnt1['a'] * 1 = 2 * 1 = 2 equal cnt['a'] = 2? Yes! ✓
    • Does cnt1['b'] * 1 = 4 * 1 = 4 equal cnt['b'] = 4? Yes! ✓
    • Does cnt1['c'] * 1 = 2 * 1 = 2 equal cnt['c'] = 2? Yes! ✓
  • Length 8 passes! ✓

Answer: 8

In this case, the string cannot be formed by concatenating anagrams of any smaller string, so the minimum length is the entire string itself.

Solution Implementation

1from collections import Counter
2
3class Solution:
4    def minAnagramLength(self, s: str) -> int:
5        """
6        Find the minimum length of string t such that s can be formed by 
7        concatenating anagrams of t.
8      
9        Args:
10            s: Input string to analyze
11          
12        Returns:
13            Minimum length of the base string t
14        """
15        def is_valid_length(substring_length: int) -> bool:
16            """
17            Check if the string can be divided into equal parts where each part
18            is an anagram of the same base string.
19          
20            Args:
21                substring_length: Length of each potential substring
22              
23            Returns:
24                True if valid division exists, False otherwise
25            """
26            # Check each substring of the given length
27            for start_idx in range(0, total_length, substring_length):
28                # Get character frequency for current substring
29                current_substring_freq = Counter(s[start_idx : start_idx + substring_length])
30              
31                # Verify that each character appears proportionally
32                # across all substrings
33                for char, total_count in overall_char_freq.items():
34                    expected_count_per_substring = current_substring_freq[char]
35                    num_substrings = total_length // substring_length
36                  
37                    # Check if total count matches expected distribution
38                    if expected_count_per_substring * num_substrings != total_count:
39                        return False
40          
41            return True
42      
43        # Count frequency of all characters in the string
44        overall_char_freq = Counter(s)
45        total_length = len(s)
46      
47        # Try each possible substring length from smallest to largest
48        for candidate_length in range(1, total_length + 1):
49            # Only check lengths that evenly divide the total length
50            if total_length % candidate_length == 0:
51                if is_valid_length(candidate_length):
52                    return candidate_length
53
1class Solution {
2    private int stringLength;
3    private char[] charArray;
4    private int[] globalCharFrequency = new int[26];
5
6    public int minAnagramLength(String s) {
7        // Initialize instance variables
8        stringLength = s.length();
9        this.charArray = s.toCharArray();
10      
11        // Count frequency of each character in the entire string
12        for (int i = 0; i < stringLength; i++) {
13            globalCharFrequency[charArray[i] - 'a']++;
14        }
15      
16        // Try each possible substring length starting from 1
17        for (int substringLength = 1; ; substringLength++) {
18            // Only check lengths that evenly divide the total string length
19            if (stringLength % substringLength == 0 && isValidSubstringLength(substringLength)) {
20                return substringLength;
21            }
22        }
23    }
24
25    private boolean isValidSubstringLength(int substringLength) {
26        // Check each substring of the given length
27        for (int startIndex = 0; startIndex < stringLength; startIndex += substringLength) {
28            int[] localCharFrequency = new int[26];
29          
30            // Count character frequencies in current substring
31            for (int currentIndex = startIndex; currentIndex < startIndex + substringLength; currentIndex++) {
32                localCharFrequency[charArray[currentIndex] - 'a']++;
33            }
34          
35            // Verify that each character appears proportionally
36            // Each substring should have the same character distribution
37            int numberOfSubstrings = stringLength / substringLength;
38            for (int charIndex = 0; charIndex < 26; charIndex++) {
39                // Total occurrences should equal local occurrences * number of substrings
40                if (localCharFrequency[charIndex] * numberOfSubstrings != globalCharFrequency[charIndex]) {
41                    return false;
42                }
43            }
44        }
45        return true;
46    }
47}
48
1class Solution {
2public:
3    int minAnagramLength(string s) {
4        int n = s.size();
5      
6        // Count frequency of each character in the entire string
7        int totalFreq[26] = {0};
8        for (char c : s) {
9            totalFreq[c - 'a']++;
10        }
11      
12        // Lambda function to check if string can be divided into anagram substrings of length k
13        auto canDivideIntoAnagrams = [&](int substringLength) {
14            // Check each substring of length k
15            for (int startIdx = 0; startIdx < n; startIdx += substringLength) {
16                // Count frequency of characters in current substring
17                int substringFreq[26] = {0};
18                for (int j = startIdx; j < startIdx + substringLength; ++j) {
19                    substringFreq[s[j] - 'a']++;
20                }
21              
22                // Verify if current substring frequency scaled up matches total frequency
23                // Each character's frequency in substring * number of substrings should equal total frequency
24                int numSubstrings = n / substringLength;
25                for (int charIdx = 0; charIdx < 26; ++charIdx) {
26                    if (substringFreq[charIdx] * numSubstrings != totalFreq[charIdx]) {
27                        return false;
28                    }
29                }
30            }
31            return true;
32        };
33      
34        // Try each possible substring length starting from 1
35        // Return the first valid length (which will be the minimum)
36        for (int length = 1; ; ++length) {
37            // Only check lengths that evenly divide the string length
38            if (n % length == 0 && canDivideIntoAnagrams(length)) {
39                return length;
40            }
41        }
42    }
43};
44
1/**
2 * Finds the minimum length of an anagram that can be repeated to form the given string
3 * @param s - The input string to analyze
4 * @returns The minimum length of the repeating anagram pattern
5 */
6function minAnagramLength(s: string): number {
7    const stringLength: number = s.length;
8  
9    // Count frequency of each character in the entire string
10    const characterFrequency: Record<string, number> = {};
11    for (let i = 0; i < stringLength; i++) {
12        const char: string = s[i];
13        characterFrequency[char] = (characterFrequency[char] || 0) + 1;
14    }
15  
16    /**
17     * Checks if the string can be divided into segments of length k
18     * where each segment is an anagram of the others
19     * @param segmentLength - The length of each segment to test
20     * @returns True if all segments are anagrams of each other
21     */
22    const isValidSegmentLength = (segmentLength: number): boolean => {
23        const numberOfSegments: number = Math.floor(stringLength / segmentLength);
24      
25        // Check each segment of length segmentLength
26        for (let startIndex = 0; startIndex < stringLength; startIndex += segmentLength) {
27            // Count character frequency in current segment
28            const segmentFrequency: Record<string, number> = {};
29            for (let j = startIndex; j < startIndex + segmentLength; j++) {
30                const char: string = s[j];
31                segmentFrequency[char] = (segmentFrequency[char] || 0) + 1;
32            }
33          
34            // Verify that each character appears the correct number of times
35            // Each character's total count should equal its segment count * number of segments
36            for (const [character, totalCount] of Object.entries(characterFrequency)) {
37                const expectedSegmentCount: number = segmentFrequency[character] || 0;
38                if (expectedSegmentCount * numberOfSegments !== totalCount) {
39                    return false;
40                }
41            }
42        }
43        return true;
44    };
45  
46    // Try each possible segment length starting from 1
47    for (let candidateLength = 1; ; candidateLength++) {
48        // Only test lengths that evenly divide the string length
49        if (stringLength % candidateLength === 0 && isValidSegmentLength(candidateLength)) {
50            return candidateLength;
51        }
52    }
53}
54

Time and Space Complexity

Time Complexity: O(n × A), where n is the length of string s, and A is the number of factors (divisors) of n.

The analysis breaks down as follows:

  • The outer loop iterates through all possible lengths from 1 to n, but only performs the check function when n % i == 0, meaning i is a divisor of n. The number of divisors of n is A.
  • For each valid divisor i, the check function is called:
    • The check function iterates through the string in chunks of size i, creating n/i segments
    • For the first segment, it creates a Counter which takes O(i) time
    • Then it iterates through all unique characters in the original Counter (at most 26 for lowercase letters), performing constant-time operations
    • Overall, the check function takes O(i + |Σ|) time, which simplifies to O(i) since i can be up to n
  • Summing across all divisors: Σ(i for all divisors i of n) ≤ n × A
  • Creating the initial Counter takes O(n) time

Therefore, the total time complexity is O(n × A).

Space Complexity: O(|Σ|), where Σ is the character set (lowercase letters in this case).

The space analysis:

  • The Counter cnt stores at most 26 character-frequency pairs for lowercase letters: O(|Σ|)
  • The Counter cnt1 inside the check function also stores at most 26 entries: O(|Σ|)
  • Other variables use constant space

The total space complexity is O(|Σ|), which equals O(26) = O(1) for lowercase English letters.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Redundant Validation of All Segments

The Pitfall: The current implementation checks every segment of length k in the string, even though mathematically, if all segments are anagrams of each other, they must all have the same character distribution. This means we're doing unnecessary work by validating each segment individually.

Why It Happens: The code iterates through all segments (for start_idx in range(0, total_length, substring_length)) and validates the character count for each one. However, if the string is valid for a given length k, all segments must be identical in character composition.

The Fix: Instead of checking all segments, we only need to:

  1. Check if the total character counts are divisible by the number of segments
  2. Optionally verify that the first segment's character distribution matches the expected pattern

Improved Solution:

from collections import Counter

class Solution:
    def minAnagramLength(self, s: str) -> int:
        def is_valid_length(substring_length: int) -> bool:
            num_segments = total_length // substring_length
          
            # Get the character frequency of the first segment as reference
            first_segment_freq = Counter(s[:substring_length])
          
            # Check if total counts are consistent with this pattern
            for char, count in first_segment_freq.items():
                if overall_char_freq[char] != count * num_segments:
                    return False
          
            # Also check that no extra characters exist in the total count
            for char in overall_char_freq:
                if char not in first_segment_freq:
                    return False
                  
            return True
      
        overall_char_freq = Counter(s)
        total_length = len(s)
      
        for candidate_length in range(1, total_length + 1):
            if total_length % candidate_length == 0:
                if is_valid_length(candidate_length):
                    return candidate_length

2. Not Handling Character Distribution Divisibility Early

The Pitfall: The solution doesn't pre-check whether the character frequencies are compatible with a given segment count before doing detailed validation.

Why It Matters: If we have 7 'a's in the string and we're checking if the string can be divided into 3 segments, it's impossible since 7 is not divisible by 3. We can eliminate such cases immediately.

Optimized Approach:

def is_valid_length(substring_length: int) -> bool:
    num_segments = total_length // substring_length
  
    # Early check: all character counts must be divisible by num_segments
    for char, count in overall_char_freq.items():
        if count % num_segments != 0:
            return False
  
    # Now verify the first segment matches expected pattern
    first_segment_freq = Counter(s[:substring_length])
    for char, count in first_segment_freq.items():
        if overall_char_freq[char] != count * num_segments:
            return False
          
    return True

This optimization can significantly reduce computation time, especially for longer strings with many potential divisors.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More