Understand and apply the fundamental counting principle
Use factorial notation to solve arrangement problems
Distinguish between permutations and combinations
Solve ordering problems with and without replacement
Advanced Techniques:
Calculate permutations of subsets: \(P(n,k)\)
Determine combinations without regard to order: \(\binom{n}{k}\)
Solve counting problems with constraints
Handle indistinguishable objects and repetition cases
Success Criteria: You will solve real-world counting problems by selecting appropriate combinatorial techniques and justifying your mathematical approach.
Introduction to Counting Principles
Combinatorics is the branch of mathematics concerned with counting, arranging, and selecting objects. These principles form the foundation for probability theory, statistics, and computational complexity analysis.
Efficient counting methods are fundamental to data science, enabling us to:
Determine the number of possible outcomes in sample spaces
Analyze algorithm complexity and computational feasibility
Design experiments and sample surveys
Understand combinatorial structures in data
Historical Context
The formal study of combinatorics began in the 17th century with Blaise Pascal and Pierre de Fermat, who developed the foundations of probability theory through correspondence about gambling problems in 1654.
The notation for combinations, \(\binom{n}{k}\), appeared in Pascal’s Traité du triangle arithmétique (1665), which introduced Pascal’s triangle and established the mathematical framework for binomial coefficients.
Why Counting Matters
Counting principles enable us to:
Quantify possibilities in decision-making scenarios
Determine favorable outcomes in various scenarios
Analyze complexity of algorithms and data structures
Design experiments by understanding sample space size
Solve optimization problems through enumeration
Data Science Applications
Counting principles appear throughout data science practice in several contexts:
Feature Engineering: With \(5\) categorical variables, each having \(10\) levels, interaction features generate \(10^5 = 100,000\) possible combinations, requiring computational consideration.
Permutation Testing: Computing exact \(p\)-values requires counting all possible rearrangements of observed data to establish the null distribution.
Hyperparameter Optimization: A grid search over \(5\) hyperparameters with \(10\) values each examines \(10^5\) configurations, making the search space size critical for feasibility.
Experimental Design: Determining valid treatment assignments in randomized trials depends on combinatorial analysis to ensure proper randomization.
Model Selection: With multiple feature engineering options, cross-validation strategies, and hyperparameter choices, the combinatorial explosion requires systematic enumeration.
Without systematic counting methods, exhaustive enumeration becomes impractical. An \(8\)-character alphanumeric password has \(62^8 \approx 2.18 \times 10^{14}\) possibilities. In genomics, enumerating all \(k\)-mer sequences of length \(20\) from DNA yields \(4^{20} \approx 1.1 \times 10^{12}\) combinations, which is computationally prohibitive.
The Fundamental Counting Principle
graph TB
A["Fundamental Counting Principle"] --> B["Task 1<br/>m choices"]
A --> C["Task 2<br/>n choices"]
A --> D["Task 3<br/>p choices"]
B --> E["Total Ways"]
C --> E
D --> E
E --> F["m × n × p"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#1565C0,color:#FFFFFF
style E fill:#E65100,color:#FFFFFF
style F fill:#6A1B9A,color:#FFFFFF
Principle: If a task can be performed in \(m\) ways, and for each of these ways a second task can be performed in \(n\) ways, then the two tasks together can be performed in \(m \times n\) ways.
This generalizes to any number of sequential tasks: if tasks have \(m_1, m_2, \ldots, m_k\) choices respectively, the total number of ways to complete all tasks is:
Equivalently, we can define factorials recursively:
\[
n! = \begin{cases}
1 & \text{if } n = 0 \\
n \times (n-1)! & \text{if } n > 0
\end{cases}
\]
Interpretation: The value \(n!\) represents the number of ways to arrange \(n\) distinct objects in a sequence.
Python Modules and Importing
Before we begin working with factorials computationally, we need to understand how to access Python’s built-in mathematical functions through modules.
What is a Module?
A module is a file containing Python definitions and statements. Python organizes its standard library into modules to group related functionality together. This organizational approach:
Keeps the core language clean and efficient
Allows you to load only the functions you need
Prevents naming conflicts between different functions
Enables code reuse across projects
Think of modules as toolboxes. Python comes with many toolboxes (modules), each containing specialized tools (functions). The math module, for instance, contains mathematical functions beyond basic arithmetic.
Importing Modules
To use functions from a module, you must first import the module using the import statement:
import math# Now we can use math module functionsresult_5 = math.factorial(5)result_10 = math.factorial(10)print(f"5! = {result_5}")print(f"10! = {result_10}")
5! = 120
10! = 3628800
Alternative Import Syntax
Python offers several ways to import modules:
# Method 1: Import entire moduleimport mathvalue1 = math.factorial(6)# Method 2: Import specific functionfrom math import factorialvalue2 = factorial(6)# Method 3: Import with aliasimport math as mvalue3 = m.factorial(6)print(f"Method 1: {value1}")print(f"Method 2: {value2}")print(f"Method 3: {value3}")
Method 1: 720
Method 2: 720
Method 3: 720
Best Practice: For standard modules like math, use import math rather than from math import *. This makes your code more readable by clearly showing where functions come from.
Computing Factorials
Manual Implementation
Understanding how factorials are computed helps build computational thinking:
def factorial_iterative(n):""" Calculate factorial using iterative approach. Args: n (int): Non-negative integer Returns: int: n! """if n <0:raiseValueError("Factorial not defined for negative numbers") result =1for i inrange(2, n +1): result *= ireturn result# Test the implementationfor n inrange(8):print(f"{n}! = {factorial_iterative(n):,}")
The recursive definition translates directly into code:
def factorial_recursive(n):""" Calculate factorial using recursion. Args: n (int): Non-negative integer Returns: int: n! """if n <0:raiseValueError("Factorial not defined for negative numbers")# Base casesif n ==0or n ==1:return1# Recursive casereturn n * factorial_recursive(n -1)# Test recursive implementationprint("Recursive factorial:")for n inrange(8):print(f"{n}! = {factorial_recursive(n):,}")
The built-in math.factorial() is implemented in C and optimized for performance. For production code, always use the built-in implementation. Manual implementations are valuable for understanding the underlying algorithm. Note that Python’s default recursion limit is approximately 1000, so recursive factorial will raise RecursionError for \(n > 1000\).
Example: Arranging Books
Problem: In how many ways can you arrange 5 different books on a shelf?
Solution:
We need to determine the number of distinct orderings of 5 books.
Step 1: Identify the problem type
This is an arrangement problem where all objects are distinct and order matters.
Step 2: Apply the counting principle
For each position on the shelf, we have a decreasing number of choices:
First position: 5 choices (any of the 5 books)
Second position: 4 remaining choices
Third position: 3 remaining choices
Fourth position: 2 remaining choices
Fifth position: 1 remaining choice
Step 3: Calculate the total
By the multiplication principle:
num_books =5arrangements = math.factorial(num_books)print(f"Number of ways to arrange {num_books} books: {arrangements}")# Verify with all arrangements for 3 booksfrom itertools import permutationsbooks = ['A', 'B', 'C']all_arrangements =list(permutations(books))print(f"\nFor {len(books)} books, there are {math.factorial(len(books))} arrangements:")for i, arrangement inenumerate(all_arrangements, 1):print(f" {i}. {' '.join(arrangement)}")
Number of ways to arrange 5 books: 120
For 3 books, there are 6 arrangements:
1. A B C
2. A C B
3. B A C
4. B C A
5. C A B
6. C B A
Answer: There are 120 distinct ways to arrange 5 different books on a shelf.
Factorial Growth
Factorials grow extremely rapidly, faster than exponential functions. This rapid growth has important implications for computational complexity and algorithm design.
Note that \(20! = 2,432,902,008,176,640,000\) is already beyond what can be stored in standard 64-bit integers in many programming languages. Python handles arbitrary precision integers natively, but this comes with performance costs. The rapid growth of factorials explains why exhaustive search algorithms become infeasible quickly. Even modest values of \(n\) create astronomical numbers of possibilities.
Permutations
Definition and Notation
A permutation is an ordered arrangement of objects. When we care about the order in which objects appear, we use permutations.
Notation: The number of ways to arrange \(k\) objects selected from \(n\) distinct objects is denoted \(P(n,k)\) or \(_nP_k\) or \(^nP_k\).
Formula
\[
P(n,k) = \frac{n!}{(n-k)!}
\]
This formula can be understood through the fundamental counting principle:
First position: \(n\) choices
Second position: \(n-1\) choices (one already selected)
n_letters =26password_length =4num_passwords = math.perm(n_letters, password_length)print(f"Number of {password_length}-character passwords (no repetition): {num_passwords:,}")# Manual calculation for verificationmanual_calc =26*25*24*23print(f"Manual calculation: {manual_calc:,}")print(f"Match: {num_passwords == manual_calc}")# Compare with repetition allowedwith_repetition = n_letters ** password_lengthprint(f"\nWith repetition allowed: {with_repetition:,}")print(f"Increase factor: {with_repetition / num_passwords:.2f}x")
Number of 4-character passwords (no repetition): 358,800
Manual calculation: 358,800
Match: True
With repetition allowed: 456,976
Increase factor: 1.27x
Answer: There are 358,800 distinct 4-character passwords that can be formed from 26 lowercase letters without repetition.
For comparison, if repetition were allowed, the number would increase to \(26^4 = 456,976\) passwords, representing a 1.27x increase.
Example: Race Podium
Problem: In a race with 12 runners, how many different ways can the gold, silver, and bronze medals be awarded?
Solution:
Step 1: Identify the problem type
This is a permutation problem because:
We select 3 runners from 12
Order matters (gold, silver, bronze are distinct positions)
No repetition (one runner cannot receive multiple medals)
Step 2: Define parameters
- Total runners: \(n = 12\) - Medals to award: \(k = 3\)
runners =12medals =3podium_arrangements = math.perm(runners, medals)print(f"Number of possible podium arrangements: {podium_arrangements:,}")# Simulate small example with 4 runnersfrom itertools import permutationsrunners_small = ['A', 'B', 'C', 'D']podium_small =list(permutations(runners_small, 3))print(f"\nFor {len(runners_small)} runners and 3 medals:")print(f"Calculated: P({len(runners_small)}, 3) = {math.perm(len(runners_small), 3)}")print(f"Enumerated: {len(podium_small)} arrangements")print("\nFirst 10 arrangements:")for i, arrangement inenumerate(podium_small[:10], 1):print(f" {i}. Gold: {arrangement[0]}, Silver: {arrangement[1]}, Bronze: {arrangement[2]}")
Number of possible podium arrangements: 1,320
For 4 runners and 3 medals:
Calculated: P(4, 3) = 24
Enumerated: 24 arrangements
First 10 arrangements:
1. Gold: A, Silver: B, Bronze: C
2. Gold: A, Silver: B, Bronze: D
3. Gold: A, Silver: C, Bronze: B
4. Gold: A, Silver: C, Bronze: D
5. Gold: A, Silver: D, Bronze: B
6. Gold: A, Silver: D, Bronze: C
7. Gold: B, Silver: A, Bronze: C
8. Gold: B, Silver: A, Bronze: D
9. Gold: B, Silver: C, Bronze: A
10. Gold: B, Silver: C, Bronze: D
Answer: There are 1,320 different ways to award the gold, silver, and bronze medals among 12 runners.
Note that if we only cared about which 3 runners received medals (not their specific ranks), we would use combinations instead, which would give us \(\binom{12}{3} = 220\) possibilities.
Permutations with Repetition
When objects are not all distinct, we must account for indistinguishable arrangements.
Formula: If we have \(n\) objects where \(n_1\) are of type 1, \(n_2\) are of type 2, …, and \(n_k\) are of type \(k\), the number of distinct permutations is:
We divide by the factorials of repeated elements because swapping identical objects produces the same arrangement.
Example: Anagrams
Problem: How many distinct arrangements can be made from the letters in “MISSISSIPPI”?
Solution:
Step 1: Count the letters
- Total letters: \(n = 11\) - Letter frequencies:
M: 1
I: 4
S: 4
P: 2
Step 2: Identify the problem type
This is a permutation with repetition problem because we have identical letters that are indistinguishable from each other.
Step 3: Apply the formula
For permutations with repetition:
Answer: There are 34,650 distinct arrangements of the letters in “MISSISSIPPI”.
The division by the factorials of repeated letters accounts for the fact that swapping identical letters (e.g., swapping two I’s or two S’s) produces the same arrangement.
Combinations
Definition and Notation
A combination is a selection of objects where order does not matter. When we only care about which objects are selected (not their arrangement), we use combinations.
Notation: The number of ways to select \(k\) objects from \(n\) distinct objects is denoted \(\binom{n}{k}\) (read as “n choose k”) or \(C(n,k)\) or \(_nC_k\).
Formula
\[
\binom{n}{k} = \frac{n!}{k!(n-k)!}
\]
This formula follows from the relationship between permutations and combinations:
This explains why we divide by \(k!\) when computing combinations: we are removing the \(k!\) different orderings that all represent the same selection.
Computing Combinations in Python
Python provides math.comb() for computing combinations:
deck_size =52hand_size =5num_hands = math.comb(deck_size, hand_size)print(f"Number of possible {hand_size}-card hands from {deck_size} cards: {num_hands:,}")# Compare with permutationsif_order_mattered = math.perm(deck_size, hand_size)print(f"\nIf order mattered: {if_order_mattered:,}")print(f"Reduction factor: {if_order_mattered / num_hands:.0f}x (which is {hand_size}!)")# Probability of specific hand typesroyal_flush_count =4# One per suitroyal_flush_prob = royal_flush_count / num_handsprint(f"\nProbability of royal flush: {royal_flush_prob:.10f}")print(f"Or approximately 1 in {num_hands / royal_flush_count:,.0f}")
Number of possible 5-card hands from 52 cards: 2,598,960
If order mattered: 311,875,200
Reduction factor: 120x (which is 5!)
Probability of royal flush: 0.0000015391
Or approximately 1 in 649,740
Answer: There are 2,598,960 distinct 5-card poker hands possible from a standard 52-card deck.
The probability of being dealt a royal flush (10, J, Q, K, A of the same suit) is approximately 1 in 649,740, demonstrating why it is the rarest poker hand.
Properties of Combinations
Combinations satisfy several important mathematical properties:
Symmetry Property
\[
\binom{n}{k} = \binom{n}{n-k}
\]
Selecting \(k\) objects to include is equivalent to selecting \(n-k\) objects to exclude.
This identity forms the basis of Pascal’s triangle and provides a recursive method to compute combinations.
Sum of All Combinations
\[
\sum_{k=0}^{n} \binom{n}{k} = 2^n
\]
The sum of all ways to select any number of objects from \(n\) objects equals \(2^n\) (corresponding to each object being either selected or not selected).
Pascal’s Triangle
Pascal’s triangle provides a visual representation of combinations and their relationships:
Pascal’s triangle exhibits several interesting patterns:
Symmetry: The triangle is symmetric (left-to-right), reflecting the property \(\binom{n}{k} = \binom{n}{n-k}\)
Pascal’s identity: Each number is the sum of the two numbers above it
Row sums: Each row sums to \(2^n\), where \(n\) is the row number
Hockey stick pattern: The sum along any diagonal equals the next element below and to the right
Pascal’s triangle appeared in many cultures before Pascal, including Persian and Chinese mathematics. Pascal’s contribution was the systematic study of its properties and their connection to probability theory.
Advanced Counting Problems
Combinations with Repetition
When selecting objects with replacement allowed, we use a different formula.
Problem: How many ways can we select \(k\) objects from \(n\) types, where repetition is allowed?
n =100# Calculate set sizesdiv_by_2 = n //2div_by_3 = n //3div_by_6 = n //6# Divisible by both 2 and 3# Inclusion-exclusiondiv_by_2_or_3 = div_by_2 + div_by_3 - div_by_6print(f"Integers from 1 to {n}:")print(f" Divisible by 2: {div_by_2}")print(f" Divisible by 3: {div_by_3}")print(f" Divisible by both 2 and 3 (i.e., by 6): {div_by_6}")print(f" Divisible by 2 or 3: {div_by_2_or_3}")# Verify by enumerationcount =sum(1for i inrange(1, n +1) if i %2==0or i %3==0)print(f"\nVerification by enumeration: {count}")print(f"Match: {div_by_2_or_3 == count}")
Integers from 1 to 100:
Divisible by 2: 50
Divisible by 3: 33
Divisible by both 2 and 3 (i.e., by 6): 16
Divisible by 2 or 3: 67
Verification by enumeration: 67
Match: True
Answer: There are 67 integers from 1 to 100 that are divisible by 2 or 3.
Practical Example: Arranging Books with Constraints
Problem: You have 10 books: 4 Math, 3 Physics, and 3 Chemistry books. In how many ways can you arrange them on a shelf if:
Books of the same subject must be together?
No two Math books can be adjacent?
Solution:
Part (a): Same subjects together
Step 1: Treat each subject as a single unit
We have 3 groups: Math, Physics, Chemistry
Step 2: Arrange the groups
Number of ways to arrange 3 groups: \(3! = 6\)
math_books =4physics_books =3chemistry_books =3# Arrange the 3 subject groupsgroup_arrangements = math.factorial(3)# Arrange books within each subjectmath_arrangements = math.factorial(math_books)physics_arrangements = math.factorial(physics_books)chemistry_arrangements = math.factorial(chemistry_books)# Total arrangementstotal_a = group_arrangements * math_arrangements * physics_arrangements * chemistry_arrangementsprint(f"Part (a) - Same subjects together:")print(f" Arrange 3 groups: {group_arrangements}")print(f" Arrange Math books: {math_arrangements}")print(f" Arrange Physics books: {physics_arrangements}")print(f" Arrange Chemistry books: {chemistry_arrangements}")print(f" Total: {total_a:,}")
Part (a) - Same subjects together:
Arrange 3 groups: 6
Arrange Math books: 24
Arrange Physics books: 6
Arrange Chemistry books: 6
Total: 5,184
Part (b): No two Math books adjacent
Step 1: Arrange non-Math books first
Total non-Math books: \(3 + 3 = 6\)
Number of arrangements: \(6!\) (if all different) or \(\frac{6!}{3! \times 3!} = 20\) (Physics and Chemistry are identical within subjects)
Step 2: Create gaps for Math books
Arranging 6 non-Math books creates 7 possible positions (gaps):
_ B _ B _ B _ B _ B _ B _
Step 3: Select positions for Math books
Choose 4 of these 7 positions for the 4 Math books: \(\binom{7}{4} = 35\)
Step 4: Arrange Math books in selected positions
Number of ways to arrange 4 distinct Math books: \(4! = 24\)
Step 5: Calculate total
If all books are distinct: \[
6! \times \binom{7}{4} \times 4! = 720 \times 35 \times 24 = 604,800
\]
If books within subjects are identical: \[
\frac{6!}{3! \times 3!} \times \binom{7}{4} = 20 \times 35 = 700
\]
# Assuming all books are distinctnon_math_books = physics_books + chemistry_booksnon_math_arrangements = math.factorial(non_math_books)# Gaps created by arranging non-Math booksgaps = non_math_books +1# Choose positions for Math bookspositions_for_math = math.comb(gaps, math_books)# Arrange Math booksmath_book_arrangements = math.factorial(math_books)# Total arrangementstotal_b_distinct = non_math_arrangements * positions_for_math * math_book_arrangementsprint(f"\nPart (b) - No two Math books adjacent (all books distinct):")print(f" Arrange {non_math_books} non-Math books: {non_math_arrangements:,}")print(f" Choose {math_books} of {gaps} positions: {positions_for_math}")print(f" Arrange Math books: {math_book_arrangements}")print(f" Total: {total_b_distinct:,}")# If books within subjects are identicalnon_math_identical = math.factorial(non_math_books) // (math.factorial(physics_books) * math.factorial(chemistry_books))total_b_identical = non_math_identical * positions_for_mathprint(f"\nPart (b) - No two Math books adjacent (books within subjects identical):")print(f" Arrange non-Math (Physics and Chemistry identical): {non_math_identical}")print(f" Choose positions for Math: {positions_for_math}")print(f" Total: {total_b_identical:,}")
Part (b) - No two Math books adjacent (all books distinct):
Arrange 6 non-Math books: 720
Choose 4 of 7 positions: 35
Arrange Math books: 24
Total: 604,800
Part (b) - No two Math books adjacent (books within subjects identical):
Arrange non-Math (Physics and Chemistry identical): 20
Choose positions for Math: 35
Total: 700
Answers:
5,184 arrangements (same subjects together)
604,800 arrangements (no two Math books adjacent, all books distinct) or 700 arrangements (books within subjects identical)
The strategy for “no adjacent” problems involves arranging the unrestricted items first to create natural barriers (gaps), then placing the restricted items in these gaps.
Computational Complexity and Numerical Considerations
Complexity Analysis of Counting Operations
Understanding the computational cost of counting algorithms is essential for practical applications.
Operation
Formula
Direct
Using Logs
Complexity
Factorial
\(n!\)
\(O(n)\)
\(O(n \log n)\)
Linear multiplications
Permutation
\(P(n,k)\)
\(O(k)\)
\(O(k \log n)\)
\(k\) multiplications
Combination
\(\binom{n}{k}\)
\(O(k)\)
\(O(k \log n)\)
\(k\) multiplications + division
Generate all permutations
\(n!\) items
\(O(n \cdot n!)\)
N/A
Factorial time
Generate all combinations
\(\binom{n}{k}\) items
\(O(k \cdot \binom{n}{k})\)
N/A
Exponential time
Best Practices and Problem-Solving Strategies
Problem-Solving Framework
When approaching counting problems, follow this systematic framework:
Step 1: Identify the Type
Questions to ask:
Does order matter? → Permutation vs. Combination
Is replacement allowed? → With vs. without replacement
Are there constraints? → Apply inclusion-exclusion or divide by repeated elements
Is it a sequence of choices? → Fundamental counting principle
Are there groups of indistinguishable objects? → Divide by group factorials
Step 2: Set Up the Calculation
Define \(n\) (total objects) and \(k\) (objects selected)
Identify any groups of indistinguishable objects
Note any constraints or restrictions
Determine if complementary counting would be easier
With repetition: Use multiplication principle or stars and bars
With constraints: Use inclusion-exclusion or complementary counting
Step 4: Verify Reasonableness
Is the answer within expected bounds?
Does it satisfy any known relationships?
Can you verify with a simpler special case?
Does the result make intuitive sense?
Decision Tree for Counting Problems
graph TD
A[Counting Problem] --> B{Order matters?}
B -->|Yes| C{Replacement?}
B -->|No| D{Replacement?}
C -->|Yes| E["Multiplication Principle<br/>n^k"]
C -->|No| F["Permutation<br/>P(n,k)"]
D -->|Yes| G["Combination with<br/>Repetition<br/>C(n+k-1,k)"]
D -->|No| H["Combination<br/>C(n,k)"]
F --> I{All objects used?}
I -->|Yes| J["Full Permutation<br/>n!"]
I -->|No| F
H --> K{Indistinguishable<br/>groups?}
K -->|Yes| L["Divide by<br/>group factorials"]
K -->|No| H
E --> M{Constraints?}
F --> M
G --> M
H --> M
M -->|Yes| N["Apply<br/>Inclusion-Exclusion<br/>or Complement"]
M -->|No| O["Direct<br/>Calculation"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#1565C0,color:#FFFFFF
style E fill:#E65100,color:#FFFFFF
style F fill:#E65100,color:#FFFFFF
style G fill:#E65100,color:#FFFFFF
style H fill:#E65100,color:#FFFFFF
style M fill:#6A1B9A,color:#FFFFFF
style N fill:#C62828,color:#FFFFFF
style O fill:#C62828,color:#FFFFFF
Example: Complete Problem-Solving Process
Problem: A committee of 8 people needs to select a subcommittee of 4 members, including exactly 2 from the 3 senior members. How many ways can this be done?
Solution:
Step 1: Identify the problem type
This is a combination problem with constraints. We need to:
Select exactly 2 senior members from 3
Select exactly 2 non-senior members from the remaining 5
Order does not matter
Step 2: Break into subproblems
Subproblem 1: Select 2 from 3 senior members
Subproblem 2: Select 2 from 5 non-senior members (since 8 total - 3 senior = 5 non-senior)
Step 4: Combine using multiplication principle
Total ways: \[
\binom{3}{2} \times \binom{5}{2} = 3 \times 10 = 30
\]
Step 5: Verify
total_members =8senior_members =3non_senior_members = total_members - senior_members# Need 2 senior and 2 non-senior for subcommittee of 4senior_needed =2non_senior_needed =2# Calculate waysways_senior = math.comb(senior_members, senior_needed)ways_non_senior = math.comb(non_senior_members, non_senior_needed)total_ways = ways_senior * ways_non_seniorprint(f"Committee composition:")print(f" Total members: {total_members}")print(f" Senior members: {senior_members}")print(f" Non-senior members: {non_senior_members}")print(f"\nSubcommittee requirements:")print(f" Senior members needed: {senior_needed}")print(f" Non-senior members needed: {non_senior_needed}")print(f"\nCalculation:")print(f" Ways to select {senior_needed} from {senior_members} senior: C({senior_members},{senior_needed}) = {ways_senior}")print(f" Ways to select {non_senior_needed} from {non_senior_members} non-senior: C({non_senior_members},{non_senior_needed}) = {ways_non_senior}")print(f" Total ways: {ways_senior} × {ways_non_senior} = {total_ways}")
Committee composition:
Total members: 8
Senior members: 3
Non-senior members: 5
Subcommittee requirements:
Senior members needed: 2
Non-senior members needed: 2
Calculation:
Ways to select 2 from 3 senior: C(3,2) = 3
Ways to select 2 from 5 non-senior: C(5,2) = 10
Total ways: 3 × 10 = 30
Answer: There are 30 ways to form the subcommittee.
This example demonstrates the complete problem-solving framework. Breaking complex problems into simpler parts using the multiplication principle is often more intuitive than attempting to find a single formula.
Practice Problems
Problem 1: Basic Permutations
Question: A library has 12 different books on data science. In how many ways can you select and arrange 4 of these books on a shelf?
Solution:
Step 1: Identify the problem type
This is a permutation problem because we are both selecting and arranging books, which means order matters.
Step 2: Define parameters
- Total books: \(n = 12\) - Books to arrange: \(k = 4\)
Answer: There are 11,880 ways to select and arrange 4 books from 12 data science books on a shelf.
Problem 2: Team Selection
Question: A basketball coach needs to select 5 starting players from a roster of 15 players. How many different starting lineups are possible?
Solution:
Step 1: Identify the problem type
This is a combination problem because the order does not matter. All 5 selected players have the same role (starting player), so selecting players A, B, C, D, E is the same as selecting E, D, C, B, A.
Step 2: Define parameters
- Total players: \(n = 15\) - Players to select: \(k = 5\)
graph LR
A["15 Players<br/>on Roster"] --> B["Select 5<br/>(order doesn't matter)"]
B --> C["Combination<br/>C(15,5)"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#E65100,color:#FFFFFF
n, k =15, 5result = math.comb(n, k)print(f"C({n},{k}) = {result:,}")# Verify with manual calculationnumerator =15*14*13*12*11denominator = math.factorial(5)manual = numerator // denominatorprint(f"Manual: {manual:,}")
C(15,5) = 3,003
Manual: 3,003
Answer: There are 3,003 different possible starting lineups.
Problem 3: Password with Repetition
Question: How many 6-character passwords can be formed using the 26 lowercase letters if:
Repetition is allowed?
Repetition is not allowed?
Solution:
Part (a): Repetition allowed
Step 1: Identify the problem type
With repetition allowed, each position can independently have any of the 26 letters. This is an application of the multiplication principle.
Step 2: Apply the multiplication principle
For each of 6 positions, we have 26 choices:
\[
26^6 = 308,915,776
\]
Part (b): Repetition not allowed
Step 1: Identify the problem type
Without repetition, this becomes a permutation problem where we select and arrange 6 letters from 26.
letters =26length =6# Part (a): with repetitionwith_rep = letters ** lengthprint(f"Part (a) - With repetition: {with_rep:,}")# Part (b): without repetitionwithout_rep = math.perm(letters, length)print(f"Part (b) - Without repetition: {without_rep:,}")print(f"\nReduction factor: {with_rep / without_rep:.2f}x")
Part (a) - With repetition: 308,915,776
Part (b) - Without repetition: 165,765,600
Reduction factor: 1.86x
Answers: - (a) 308,915,776 passwords with repetition allowed - (b) 165,765,600 passwords without repetition
Note that allowing repetition increases the number of possible passwords by a factor of approximately 1.86.
Problem 4: Arranging Letters with Repetition
Question: How many distinct arrangements can be made from the letters in the word “STATISTICS”?
Solution:
Step 1: Count letter frequencies
- Word: STATISTICS - Total letters: 10 - Letter counts: - S: 3 - T: 3 - A: 1 - I: 2 - C: 1
graph TD
A["STATISTICS<br/>10 letters total"] --> B["S: 3"]
A --> C["T: 3"]
A --> D["A: 1"]
A --> E["I: 2"]
A --> F["C: 1"]
B --> G["Divide by<br/>repeated factorials"]
C --> G
D --> G
E --> G
F --> G
G --> H["10! / (3! × 3! × 1! × 2! × 1!)"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#1565C0,color:#FFFFFF
style E fill:#1565C0,color:#FFFFFF
style F fill:#1565C0,color:#FFFFFF
style G fill:#E65100,color:#FFFFFF
style H fill:#6A1B9A,color:#FFFFFF
Step 2: Identify the problem type
This is a permutation with repetition problem. We have identical letters that are indistinguishable from each other.
Step 3: Apply the formula for permutations with repetition
Answer: There are 50,400 distinct arrangements of the letters in “STATISTICS”.
The division by the factorials of repeated letters accounts for the indistinguishability of identical letters within each group.
Problem 5: Committee with Constraints
Question: A committee of 6 people is to be formed from 8 men and 7 women. In how many ways can this be done if the committee must have exactly 4 men and 2 women?
Solution:
Step 1: Identify the problem type
This is a combination problem with constraints. We need to select from two separate groups while meeting specific requirements.
Step 2: Break into subproblems
- Subproblem 1: Select 4 men from 8 - Subproblem 2: Select 2 women from 7
graph TD
A["Form Committee<br/>6 people total"] --> B["Select 4 Men<br/>from 8"]
A --> C["Select 2 Women<br/>from 7"]
B --> D["C(8,4)"]
C --> E["C(7,2)"]
D --> F["Multiply"]
E --> F
F --> G["C(8,4) × C(7,2)"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#E65100,color:#FFFFFF
style E fill:#E65100,color:#FFFFFF
style F fill:#6A1B9A,color:#FFFFFF
style G fill:#C62828,color:#FFFFFF
men_total =8women_total =7men_needed =4women_needed =2ways_men = math.comb(men_total, men_needed)ways_women = math.comb(women_total, women_needed)total_ways = ways_men * ways_womenprint(f"Select {men_needed} men from {men_total}: C({men_total},{men_needed}) = {ways_men}")print(f"Select {women_needed} women from {women_total}: C({women_total},{women_needed}) = {ways_women}")print(f"Total ways: {ways_men} × {ways_women} = {total_ways:,}")
Select 4 men from 8: C(8,4) = 70
Select 2 women from 7: C(7,2) = 21
Total ways: 70 × 21 = 1,470
Answer: There are 1,470 ways to form a committee of 6 people with exactly 4 men and 2 women.
Problem 6: Circular Arrangements
Question: In how many ways can 8 people be seated around a circular table?
Solution:
Step 1: Understand circular arrangements
In circular arrangements, rotations of the same arrangement are considered identical. To account for this, we fix one person’s position to break the rotational symmetry.
Step 2: Fix one person’s position
Once we fix one person’s position (say person A is at the “top” of the table), we need to arrange the remaining 7 people.
graph TD
A["8 People<br/>Circular Table"] --> B["Fix one person's<br/>position"]
B --> C["Arrange remaining<br/>7 people"]
C --> D["(n-1)! = 7!"]
D --> E["If reflections<br/>are same"]
E --> F["Divide by 2"]
F --> G["7! / 2"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#E65100,color:#FFFFFF
style E fill:#6A1B9A,color:#FFFFFF
style F fill:#6A1B9A,color:#FFFFFF
style G fill:#C62828,color:#FFFFFF
Step 3: Apply the formula for circular arrangements
For \(n\) people arranged in a circle: \[
\text{Circular arrangements} = (n-1)! = 7! = 5,040
\]
Step 4: Consider reflectional symmetry (if applicable)
If clockwise and counterclockwise arrangements are considered the same (mirror images), we divide by 2:
n =8# Without reflectional symmetrycircular = math.factorial(n -1)print(f"Circular arrangements (distinct rotations): {circular:,}")# With reflectional symmetrycircular_with_reflection = circular //2print(f"Circular arrangements (no mirror images): {circular_with_reflection:,}")# Compare with linear arrangementslinear = math.factorial(n)print(f"\nLinear arrangements for comparison: {linear:,}")print(f"Reduction factor: {linear / circular:.0f}x")
Circular arrangements (distinct rotations): 5,040
Circular arrangements (no mirror images): 2,520
Linear arrangements for comparison: 40,320
Reduction factor: 8x
Answer: - 5,040 ways if rotations are distinct (clockwise vs counterclockwise matters) - 2,520 ways if mirror images are considered identical
The formula \((n-1)!\) for circular arrangements arises because fixing one person’s position eliminates the \(n\) rotational symmetries present in circular arrangements.
Problem 7: Distributing Objects
Question: In how many ways can 12 identical chocolates be distributed among 4 children if each child must receive at least one chocolate?
Solution:
Step 1: Handle the constraint
To satisfy the “at least one” constraint, we first give each child 1 chocolate. This leaves \(12 - 4 = 8\) chocolates to distribute freely.
graph TD
A["12 Chocolates<br/>4 Children"] --> B["Give 1 to each<br/>(satisfy constraint)"]
B --> C["8 Chocolates<br/>remaining"]
C --> D["Stars and Bars<br/>C(n+k-1, k-1)"]
D --> E["C(8+4-1, 4-1)"]
E --> F["C(11, 3)"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#E65100,color:#FFFFFF
style E fill:#6A1B9A,color:#FFFFFF
style F fill:#C62828,color:#FFFFFF
Step 2: Identify the problem type
After satisfying the constraint, we need to distribute 8 identical chocolates among 4 children with no restrictions. This is a combination with repetition problem (stars and bars).
Step 3: Apply the stars and bars formula
For distributing \(k\) identical objects among \(n\) recipients: \[
\binom{n+k-1}{k-1} \text{ or equivalently } \binom{n+k-1}{n-1}
\]
With \(k = 8\) chocolates and \(n = 4\) children: \[
\binom{8+4-1}{4-1} = \binom{11}{3}
\]
chocolates =12children =4min_per_child =1# After giving minimum to each childremaining = chocolates - (children * min_per_child)print(f"Total chocolates: {chocolates}")print(f"After giving {min_per_child} to each of {children} children: {remaining} remaining")# Stars and bars: distribute remaining among childrenways = math.comb(remaining + children -1, children -1)print(f"\nWays to distribute: C({remaining + children -1}, {children -1}) = {ways}")
Total chocolates: 12
After giving 1 to each of 4 children: 8 remaining
Ways to distribute: C(11, 3) = 165
Answer: There are 165 ways to distribute 12 identical chocolates among 4 children such that each child receives at least one chocolate.
The stars and bars method visualizes this as arranging 8 stars (chocolates) and 3 bars (dividers between children), where each arrangement corresponds to a distinct distribution.
Problem 8: Inclusion-Exclusion
Question: How many integers from 1 to 300 are divisible by 3 or 5?
Solution:
Step 1: Define the sets
Let \(A\) = set of integers from 1 to 300 divisible by 3
Let \(B\) = set of integers from 1 to 300 divisible by 5
graph TD
A["Integers 1-300"] --> B["Divisible by 3<br/>|A| = 100"]
A --> C["Divisible by 5<br/>|B| = 60"]
B --> D["Both 3 and 5<br/>|A ∩ B| = 20"]
C --> D
D --> E["Inclusion-Exclusion<br/>|A ∪ B| = |A| + |B| - |A ∩ B|"]
E --> F["100 + 60 - 20 = 140"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#E65100,color:#FFFFFF
style E fill:#6A1B9A,color:#FFFFFF
style F fill:#C62828,color:#FFFFFF
n =300div_by_3 = n //3div_by_5 = n //5div_by_15 = n //15# LCM(3,5) = 15# Inclusion-exclusiondiv_by_3_or_5 = div_by_3 + div_by_5 - div_by_15print(f"Integers from 1 to {n}:")print(f" Divisible by 3: {div_by_3}")print(f" Divisible by 5: {div_by_5}")print(f" Divisible by 15 (both 3 and 5): {div_by_15}")print(f" Divisible by 3 or 5: {div_by_3_or_5}")# Verify by enumerationcount =sum(1for i inrange(1, n +1) if i %3==0or i %5==0)print(f"\nVerification by enumeration: {count}")
Integers from 1 to 300:
Divisible by 3: 100
Divisible by 5: 60
Divisible by 15 (both 3 and 5): 20
Divisible by 3 or 5: 140
Verification by enumeration: 140
Answer: There are 140 integers from 1 to 300 that are divisible by 3 or 5.
The inclusion-exclusion principle is essential here because simply adding the counts would double-count integers divisible by both 3 and 5 (multiples of 15).
Problem 9: Probability and Counting
Question: A standard deck has 52 cards. If you draw 5 cards, how many hands contain:
Exactly 3 aces?
At least 2 aces?
Solution:
graph TD
A["52-card Deck"] --> B["4 Aces"]
A --> C["48 Non-aces"]
B --> D["Exactly 3 Aces<br/>C(4,3) × C(48,2)"]
B --> E["At Least 2 Aces"]
C --> E
E --> F["2 aces: C(4,2) × C(48,3)"]
E --> G["3 aces: C(4,3) × C(48,2)"]
E --> H["4 aces: C(4,4) × C(48,1)"]
F --> I["Sum all cases"]
G --> I
H --> I
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#E65100,color:#FFFFFF
style E fill:#6A1B9A,color:#FFFFFF
style I fill:#C62828,color:#FFFFFF
aces =4non_aces =48hand_size =5# Part (a): Exactly 3 acesexactly_3 = math.comb(aces, 3) * math.comb(non_aces, 2)print(f"Part (a) - Exactly 3 aces: {exactly_3:,}")# Part (b): At least 2 acesexactly_2 = math.comb(aces, 2) * math.comb(non_aces, 3)exactly_3 = math.comb(aces, 3) * math.comb(non_aces, 2)exactly_4 = math.comb(aces, 4) * math.comb(non_aces, 1)at_least_2 = exactly_2 + exactly_3 + exactly_4print(f"\nPart (b) - At least 2 aces:")print(f" Exactly 2 aces: {exactly_2:,}")print(f" Exactly 3 aces: {exactly_3:,}")print(f" Exactly 4 aces: {exactly_4:,}")print(f" Total: {at_least_2:,}")# Context: total 5-card handstotal_hands = math.comb(52, 5)print(f"\nTotal possible 5-card hands: {total_hands:,}")print(f"Probability of at least 2 aces: {at_least_2/total_hands:.6f}")
Part (a) - Exactly 3 aces: 4,512
Part (b) - At least 2 aces:
Exactly 2 aces: 103,776
Exactly 3 aces: 4,512
Exactly 4 aces: 48
Total: 108,336
Total possible 5-card hands: 2,598,960
Probability of at least 2 aces: 0.041684
Answers:
4,512 hands contain exactly 3 aces
108,336 hands contain at least 2 aces
The probability of being dealt at least 2 aces is approximately 4.17%, demonstrating that hands with multiple aces are relatively uncommon.
Problem 10: Complex Arrangement with Multiple Constraints
Question: You have 12 flags: 5 red, 4 blue, and 3 green. In how many ways can you arrange them in a row if:
There are no restrictions?
All flags of the same color must be together?
No two red flags can be adjacent?
Solution:
graph TD
A["12 Flags<br/>5R, 4B, 3G"] --> B["Part a:<br/>No restrictions"]
A --> C["Part b:<br/>Colors together"]
A --> D["Part c:<br/>No adjacent reds"]
B --> E["12! / (5! × 4! × 3!)"]
C --> F["Arrange 3 color groups: 3!"]
D --> G["Arrange 7 non-red"]
G --> H["Create 8 gaps"]
H --> I["Choose 5 gaps for red"]
I --> J["[7!/(4!×3!)] × C(8,5)"]
style A fill:#2E7D32,color:#FFFFFF
style B fill:#1565C0,color:#FFFFFF
style C fill:#1565C0,color:#FFFFFF
style D fill:#1565C0,color:#FFFFFF
style E fill:#E65100,color:#FFFFFF
style F fill:#E65100,color:#FFFFFF
style J fill:#E65100,color:#FFFFFF
Part (a): No restrictions
Step 1: Identify the problem type
This is a permutation with repetition problem because we have identical flags within each color group.
red =5blue =4green =3total = red + blue + green# Part (a): No restrictionspart_a = (math.factorial(total) // (math.factorial(red) * math.factorial(blue) * math.factorial(green)))print(f"Part (a) - No restrictions: {part_a:,}")# Part (b): Same colors togetherpart_b = math.factorial(3) # Arrange 3 color groupsprint(f"Part (b) - Same colors together: {part_b}")# Part (c): No two red flags adjacentnon_red = blue + greennon_red_arrangements = math.factorial(non_red) // (math.factorial(blue) * math.factorial(green))gaps = non_red +1red_positions = math.comb(gaps, red)part_c = non_red_arrangements * red_positionsprint(f"\nPart (c) - No two red flags adjacent:")print(f" Arrange {non_red} non-red flags: {non_red_arrangements}")print(f" Choose {red} of {gaps} positions for red: {red_positions}")print(f" Total: {part_c:,}")
Part (a) - No restrictions: 27,720
Part (b) - Same colors together: 6
Part (c) - No two red flags adjacent:
Arrange 7 non-red flags: 35
Choose 5 of 8 positions for red: 56
Total: 1,960
Answers:
27,720 arrangements with no restrictions
6 arrangements with same colors together
1,960 arrangements with no two red flags adjacent
Part (c) demonstrates an important technique: when dealing with “no adjacent” constraints, arrange the unrestricted items first to create natural barriers, then place the restricted items in the gaps.
Summary and Key Takeaways
Fundamental Concepts Covered
This lecture established the mathematical foundations for systematic counting:
Counting Foundations:
The fundamental counting principle multiplies choices across sequential tasks
Factorial notation \(n!\) counts arrangements of \(n\) distinct objects
Order relevance determines whether to use permutations or combinations
Replacement affects which formula applies
Permutations and Combinations:
Permutations: \(P(n,k) = \frac{n!}{(n-k)!}\) when order matters
Combinations: \(\binom{n}{k} = \frac{n!}{k!(n-k)!}\) when order does not matter
Permutations with repetition: \(\frac{n!}{n_1! n_2! \cdots n_k!}\) for identical groups
Combinations with repetition: \(\binom{n+k-1}{k}\) using stars and bars
Problem-Solving Framework
Decision Process:
Order relevance?
Order matters → Permutation or multiplication principle
Order irrelevant → Combination
Replacement allowed?
With replacement → Powers (\(n^k\)) or combinations with repetition
Without replacement → Standard permutations or combinations
Constraints present?
Apply inclusion-exclusion principle
Use complement counting
Divide by repeated element factorials
Break into simpler subproblems
Verification:
Check boundary cases
Verify mathematical identities
Ensure reasonableness of result
Test with smaller examples
Applications and Extensions
Connections to Data Science:
Probability Theory: Counting forms the foundation for probability calculations (covered in next lecture)
Statistical Inference: Sampling distributions, hypothesis testing, and bootstrap methods
Algorithm Analysis: Complexity theory and computational feasibility
Experimental Design: Randomization strategies and treatment allocation
Machine Learning: Feature combinations, hyperparameter spaces, and cross-validation schemes
Cryptography: Keyspace analysis and security estimation