Understanding how to calculate the number of ways to rearrange letters of a word is a fundamental concept in combinatorics, and Python provides powerful tools to solve it efficiently. And whether you are preparing for a coding interview, solving competitive programming problems, or building a word game, mastering this calculation is essential. This guide explores the mathematical theory behind permutations, demonstrates how to implement solutions using Python’s standard library, and addresses edge cases like duplicate letters and large inputs The details matter here..
The Mathematical Foundation: Permutations
Before writing any code, it is crucial to understand the mathematics driving the solution. The arrangement of letters where order matters is called a permutation That's the whole idea..
Unique Letters (Factorials)
If a word consists of n distinct letters (e.g., "PYTHON"), the number of unique arrangements is simply n! (n factorial) Most people skip this — try not to..
$ P(n) = n \times (n-1) \times (n-2) \times \dots \times 1 $
For "PYTHON" (6 unique letters), the calculation is $6! = 720$ Worth keeping that in mind..
Repeated Letters (Multinomial Coefficients)
Real-world words often contain duplicate letters (e.That said, g. This leads to , "BANANA" has 3 'A's and 2 'N's). Swapping two identical 'A's does not create a new unique arrangement Practical, not theoretical..
$ \frac{n!}{n_1! \times n_2! \times \dots \times n_k!} $
Where:
- $n$ = total length of the word.
- $n_1, n_2, \dots, n_k$ = frequencies of each distinct letter.
For "BANANA" ($n=6$, 3 'A's, 2 'N's, 1 'B'): $ \frac{6!}{3! \times 2! \times 1!
Python Implementation: The math Module
Python’s built-in math module is the most performant and readable way to calculate factorials and permutations for standard use cases. Since Python 3.Because of that, combandmath. 8, math.Think about it: perm exist, but for arrangements with duplicates, we typically implement the multinomial formula directly using math. factorial Still holds up..
Basic Unique Permutations
import math
def count_unique_arrangements(word):
"""Calculates arrangements assuming all letters are unique."""
n = len(word)
return math.factorial(n)
print(count_unique_arrangements("PYTHON")) # Output: 720
Handling Duplicate Letters (The General Solution)
This is the solid function you will use most often. It uses collections.Counter to tally letter frequencies and applies the multinomial formula No workaround needed..
import math
from collections import Counter
def count_arrangements(word):
"""
Calculates the number of distinct permutations of a word,
correctly handling duplicate letters.
On top of that, count frequency of each character
freq = Counter(word)
# 2. On top of that, calculate numerator: n! Day to day, calculate denominator: product of (freq_i! On the flip side, values():
denominator *= math. n = len(word)
numerator = math.)
denominator = 1
for count in freq.factorial(n)
# 3. Practically speaking, """
# 1. factorial(count)
# 4.
# Test cases
print(count_arrangements("PYTHON")) # 720 (all unique)
print(count_arrangements("BANANA")) # 60
print(count_arrangements("MISSISSIPPI")) # 34650
print(count_arrangements("AAB")) # 3 (AAB, ABA, BAA)
Why Counter? It provides an $O(N)$ pass to build the frequency map, making the overall complexity $O(N + K)$ where $K$ is the number of unique characters (usually small, max 26 for English alphabet).
Generating the Actual Permutations
Sometimes the problem requires listing the arrangements, not just counting them. The itertools module is the standard tool for this.
Using itertools.permutations
itertools.permutations treats elements as unique based on their position, not their value. So, feeding it "AAB" yields 6 tuples, including duplicates.
import itertools
def generate_all_permutations(word):
# Returns tuples of characters
return itertools.permutations(word)
# "AAB" -> 3! = 6 tuples
perms = generate_all_permutations("AAB")
for p in perms:
print("".join(p))
# Output:
# AAB
# ABA
# AAB (Duplicate)
# ABA (Duplicate)
# BAA
# BAA (Duplicate)
Generating Unique Permutations Only
To get only distinct arrangements, convert the result to a set (removes duplicates) or use a recursive backtracking approach. The set approach is concise but memory-heavy for large $n$.
import itertools
def generate_unique_permutations(word):
# Generate all, convert to set to deduplicate, then join
return sorted(set("".join(p) for p in itertools.permutations(word)))
unique = generate_unique_permutations("AAB")
print(unique) # ['AAB', 'ABA', 'BAA']
print(len(unique)) # 3
Performance Warning: Generating permutations is $O(N!)$. This is only feasible for very short strings (typically $N \le 9$ or $10$). For counting, always use the mathematical formula ($O(N)$), never generate and count.
Advanced Scenarios & Optimizations
1. Case Sensitivity
By default, Python treats 'A' and 'a' as different characters. Decide if your problem requires case-insensitivity.
# Case insensitive count
word = "Python"
count = count_arrangements(word.lower()) # Treats 'P' and 'p' as same
2. Handling Large Numbers (Modulo Arithmetic)
In competitive programming (e.g., LeetCode, Codeforces), answers are often required modulo $10^9 + 7$ because factorials grow explosively ($20!$ exceeds 64-bit integer limits, though Python handles arbitrary precision natively) Practical, not theoretical..
If you need modulo arithmetic, you cannot simply divide. You must use the Modular Multiplicative Inverse (Fermat's Little Theorem).
MOD = 10**9 + 7
def mod_factorial(n, mod):
res = 1
for i in range(2, n + 1):
res = (res * i) % mod
return res
def mod_inverse(x, mod):
# Fermat's Little Theorem: x^(mod-2) % mod
return pow(x, mod - 2, mod)
def count_arrangements_mod(word, mod=MOD):
freq = Counter(word)
n = len(word)
num = mod_factorial(n, mod)
den = 1
for count in freq.values():
den = (den * mod_factorial(count, mod)) % mod
return (num * mod_inverse(den, mod)) % mod
# Example: Large word
print(count_arrangements_mod("A" * 1000 + "B" * 500))
3. Pre-computing Factorials for Multiple Queries
If you need to answer many queries for different words (or substrings), pre-computing factorials and inverse factorials up to a maximum $N$ reduces per-query time to $O(K)$ (where $K$ is alphabet size).
MAX_N = 10**6 #
Pre-computing factorials and their modular inverses allows you to answer multiple queries efficiently. Here's how to set it up:
```python
MOD = 10**9 + 7
# Pre-compute factorials and inverse factorials up to MAX_N
MAX_N = 10**6
fact = [1] * (MAX_N + 1)
inv_fact = [1] * (MAX_N + 1)
for i in range(1, MAX_N + 1):
fact[i] = (fact[i-1] * i) % MOD
inv_fact[MAX_N] = pow(fact[MAX_N], MOD-2, MOD)
for i in range(MAX_N, 0, -1):
inv_fact[i-1] = (inv_fact[i] * i) % MOD
def count_arrangements_fast(word):
from collections import Counter
freq = Counter(word)
n = len(word)
num = fact[n]
den = 1
for count in freq.values():
den = (den * inv_fact[count]) % MOD
return (num * den) % MOD
# Example: Process multiple queries
queries = ["AAB", "MISSISSIPPI", "PERMUTATION"]
for q in queries:
print(f"{q}: {count_arrangements_fast(q)}")
This approach reduces the per-query time to $O(K)$ (where $K$ is the number of distinct characters) after an $O(MAX_N)$ pre-computation step.
Conclusion
Understanding permutations with repetition is crucial for solving combinatorial problems in competitive programming, cryptography, and statistical analysis. The key formula:
$\frac{N!}{n_1! \times n_2! \times \dots \times n_k!}$
provides an efficient way to count distinct arrangements without generating them. Still, for large numbers, always use modular arithmetic with pre-computed factorials to avoid overflow and ensure efficiency. While generating permutations is feasible only for very small inputs, the mathematical approach scales to strings of arbitrary length, making it the preferred method for counting distinct arrangements It's one of those things that adds up. Which is the point..