25 Essential Python Programming Exercises: From Mathematical Puzzles to Classic Algorithms
Problem 1: Narcissistic Numbers (Armstrong Numbers)
A Narcissistic number (also known as an Armstrong number or PPDI) is a 3-digit number where the sum of the cubes of its digits equals the number itself. For example: $1^3 + 5^3 + 3^3 = 153$.
for candidate in range(100, 1000):
hundreds = candidate // 100
tens = (candidate // 10) % 10
units = candidate % 10
if hundreds ** 3 + tens ** 3 + units ** 3 == candidate:
print(f"{candidate} is a Narcissistic number")
# Output: 153, 370, 371, 407
Problem 2: Four-Leaf Rose Numbers
These are 4-digit numbers where the sum of the fourth powers of each digit equals the number itself. This extends the concept of Narcissistic numbers to 4 digits.
for num in range(1000, 10000):
d1 = num // 1000
d2 = (num // 100) % 10
d3 = (num // 10) % 10
d4 = num % 10
if d1 ** 4 + d2 ** 4 + d3 ** 4 + d4 ** 4 == num:
print(f"{num} is a Four-Leaf Rose number")
# Output: 1634, 8208, 9474
Problem 3: Reverse String Output
Method 1: Slicing
text = input("Enter a string: ")
print(text[::-1])
Method 2: Loop Construction
text = input("Enter a string: ")
chars = []
for idx in range(len(text) - 1, -1, -1):
chars.append(text[idx])
print(''.join(chars))
Problem 4: Number Guessing Game
Generate a random integer between 0 and 100. The player has 10 attempts to guess the number with hints indicating if the guess is too high or too low.
import random
target = random.randint(0, 100)
max_attempts = 10
for attempt in range(max_attempts):
guess = int(input("Enter your guess: "))
if guess > target:
print("Too high!")
elif guess < target:
print("Too low!")
else:
print(f"Correct! You won in {attempt + 1} attempts.")
break
print(f"Remaining attempts: {max_attempts - attempt - 1}")
else:
print("Game over. You ran out of attempts.")
Problem 5: Hundred Chickens Problem
A classic Chinese mathematical puzzle: With 100 yuan, buy exactly 100 chickens. Roosters cost 5 yuan each, hens cost 3 yuan each, and chicks cost 1 yuan for 3. All three types must be purchased.
Mathematical Model:
- $x + y + z = 100$ (total chickens)
- $5x + 3y + z/3 = 100$ (total cost)
solutions = 0
for roosters in range(1, 20): # Max 20 roosters (5*20=100)
for hens in range(1, 34): # Max 33 hens (3*33=99)
chicks = 100 - roosters - hens
if chicks > 0 and 5 * roosters + 3 * hens + chicks / 3 == 100:
solutions += 1
print(f"Solution {solutions}: {roosters} roosters, {hens} hens, {chicks} chicks")
Problem 6: Advanced Leap Year Calculation
Input a year, month, and day. Determine if it's a leap year and calculate the day of the year.
Leap Year Rules:
- Divisible by 4 but not by 100, OR
- Divisible by 400
year = int(input("Enter year: "))
month = int(input("Enter month: "))
day = int(input("Enter day: "))
month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# Check leap year
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
print(f"{year} is a leap year")
month_days[1] = 29
else:
print(f"{year} is not a leap year")
# Calculate day of year
day_count = day
for m in range(month - 1):
day_count += month_days[m]
print(f"{year}-{month:02d}-{day:02d} is day {day_count} of the year")
Problem 7: Monkey and Peaches
A monkey picks peaches. On day 1, it eats half plus one more. This pattern continues each morning. On the morning of day 10, only 1 peach remains. How many were picked originally?
Approach: Work backwards from day 10.
peaches = 1
print(f"Day 10 morning: {peaches} peach(es)")
for day in range(9, 0, -1):
peaches = (peaches + 1) * 2
print(f"Day {day} morning: {peaches} peach(es)")
print(f"\nOriginal count: {peaches} peaches")
Problem 8: Bubble Sort
Bubble sort repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. Larger elements "bubble up" to the end.
import numpy as np
data = np.random.randint(100, size=6)
n = len(data)
print(f"Original: {data}")
for outer in range(n - 1):
for inner in range(n - outer - 1):
if data[inner] > data[inner + 1]:
data[inner], data[inner + 1] = data[inner + 1], data[inner]
print(f"Sorted: {data}")
Problem 9: Binary Search
Binary search efficiently locates a target value in a sorted array by repeatedly dividing the search interval in half.
Method 1: Iterative
def binary_search_iterative(sorted_arr, target):
low, high = 0, len(sorted_arr) - 1
iterations = 0
while low <= high:
mid = (low + high) // 2
iterations += 1
if target > sorted_arr[mid]:
low = mid + 1
elif target < sorted_arr[mid]:
high = mid - 1
else:
print(f"Found {target} at index {mid} in {iterations} iterations")
return mid
print(f"{target} not found")
return -1
binary_search_iterative([5, 7, 11, 22, 27, 33, 39, 52, 58], 11)
Method 2: Recursive
def binary_search_recursive(sorted_arr, target, low, high):
if low > high:
return -1
mid = (low + high) // 2
if target < sorted_arr[mid]:
return binary_search_recursive(sorted_arr, target, low, mid - 1)
elif target > sorted_arr[mid]:
return binary_search_recursive(sorted_arr, target, mid + 1, high)
else:
return mid
print(binary_search_recursive([5, 7, 11, 22, 27, 33, 39, 52, 58], 11, 0, 8))
Problem 10: Selection Sort
Selection sort divides the list into a sorted portion (left) and unsorted portion (right). It repeatedly finds the minimum element from the unsorted portion and moves it to the end of the sorted portion.
import random
values = [random.randint(1, 100) for _ in range(8)]
size = len(values)
print(f"Unsorted: {values}")
for current in range(size - 1):
min_pos = current
for comparison in range(current + 1, size):
if values[min_pos] > values[comparison]:
min_pos = comparison
values[current], values[min_pos] = values[min_pos], values[current]
print(f"After round {current + 1}: {values}")
print(f"Final sorted: {values}")
Problem 11: Rock Paper Scissors Game
Players start with 100 points. Winning adds 10 points, losing subtracts 10. Reach 200 to win, drop to 0 to lose.
- 1 = Scissors
- 2 = Rock
- 3 = Paper
import random
choices = {1: "Scissors", 2: "Rock", 3: "Paper"}
player_score = 100
print("Rock Paper Scissors - Start with 100 points")
print("Reach 200 to win, 0 to lose")
while True:
computer_move = random.randint(1, 3)
player_input = input("Your choice (1-3): ")
if player_input not in '123':
print("Invalid choice")
continue
player_move = int(player_input)
print(f"Computer: {choices[computer_move]}, You: {choices[player_move]}")
if (player_move == 1 and computer_move == 3) or \
(player_move == 2 and computer_move == 1) or \
(player_move == 3 and computer_move == 2):
player_score += 10
print(f"You win! Score: {player_score}")
elif player_move == computer_move:
print(f"Draw. Score: {player_score}")
else:
player_score -= 10
print(f"You lose. Score: {player_score}")
if player_score >= 200:
print("Congratulations! You won the match!")
break
elif player_score <= 0:
print("Game over. You lost.")
break
Problem 12: Happy Numbers
A happy number eventually reaches 1 when replaced by the sum of the squares of its digits. If it loops endlessly without reaching 1, it's unhappy.
def digit_square_sum(n):
total = 0
for digit in str(n):
total += int(digit) ** 2
return total
current = int(input("Enter a number: "))
seen = []
while digit_square_sum(current) not in seen:
current = digit_square_sum(current)
seen.append(current)
if current == 1:
print("Happy number!")
else:
print("Not a happy number.")
Problem 13: Age Puzzle (Part 1)
Two sisters say: "The product of our ages is 6 times the sum of our ages." They are not twins (different ages) and their age difference is less than 8 years. Find the younger sister's age.
for age1 in range(1, 100):
for age2 in range(1, age1):
if age1 * age2 == 6 * (age1 + age2) and age1 - age2 < 8:
print(f"Ages: {age1} and {age2}")
print(f"Younger sister is {age2}")
Problem 14: Age Puzzle (Part 2)
Norbert Wiener's puzzle: "My age cubed is a 4-digit number. My age to the fourth power is a 6-digit number. Together, these 10 digits contain each digit 0-9 exactly once."
for age in range(10, 30):
cube = str(age ** 3)
fourth = str(age ** 4)
if len(cube) == 4 and len(fourth) == 6:
combined = cube + fourth
if len(set(combined)) == 10:
print(f"Age: {age}")
print(f"Combined digits: {combined}")
Problem 15: Custom Split Implementation
Implement Python's split() method functionality manually. Track indices and slice the string when delimiters are encountered.
def custom_split(text, delimiter="", max_splits=0):
result = []
start_idx = 0
split_count = 0
for idx, char in enumerate(text):
if max_splits > 0 and split_count == max_splits:
result.append(text[start_idx:])
break
if char == delimiter:
result.append(text[start_idx:idx])
start_idx = idx + 1
split_count += 1
elif idx == len(text) - 1:
result.append(text[start_idx:idx + 1])
return result
# Test
print(custom_split("life-is-short-you-need-python", "-"))
# ['life', 'is', 'short', 'you', 'need', 'python']
print(custom_split("life-is-short-you-need-python", "-", 2))
# ['life', 'is', 'short-you-need-python']
Problem 16: Dayan Sequence
An ancient Chinese sequence: 0, 2, 4, 8, 12, 18, 24, 32, 40, 50...
- For even indices $n$: $n^2 / 2$
- For odd indices $n$: $(n^2 - 1) / 2$
for n in range(1, 101):
if n % 2 == 0:
value = (n ** 2) // 2
else:
value = (n ** 2 - 1) // 2
print(value)
Problem 17: Character Frequency Analysis
Given a word, find the character that appears most frequently and its count.
def analyze_frequency(text):
frequency = {}
for char in text:
frequency[char] = frequency.get(char, 0) + 1
most_common = max(frequency, key=frequency.get)
print(most_common)
print(frequency[most_common])
analyze_frequency("HelloWorld")
# Output: 'l' and 3
Problem 18: Diamond Pattern Using Stack
Print a diamond pattern of given size using a stack to reverse the pattern for the lower half.
def print_diamond(size):
lines = []
for row in range(1, 2 * size):
if row <= size:
spaces = ' ' * (size - row)
stars = '*' * (2 * row - 1)
line = spaces + stars
if row != size:
lines.append(line)
print(line)
else:
print(lines.pop())
print_diamond(5)
Problem 19: Understanding Recursion
Recursion occurs when a function calls itself. Each call creates a new stack frame. The function must have a base case to prevent infinite recursion.
def demonstrate_recursion(level):
if level == 0:
return
print(f"Pre-recursion: {level}")
demonstrate_recursion(level - 1)
print(f"Post-recursion: {level}")
demonstrate_recursion(5)
Problem 20: Fibonacci with Recursion
The Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13... where each number is the sum of the two preceding ones.
def fibonacci(n):
if n <= 2:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
Note: Eacch recursive call pushes a frame onto the call stack. The stack unwinds when base cases are reached, following the Last-In-First-Out (LIFO) principle.
Problem 21: Maximum of Three Numbers
Find the maximum of three values using only conditional statements (no max() function or lists).
x, y, z = 10, 6, 18
if x > y:
maximum = x
else:
maximum = y
if z > maximum:
maximum = z
print(maximum) # 18
Problem 22: Perfect Numbers
A perfect number equals the sum of its proper divisors (excluding itself). For example: $6 = 1 + 2 + 3$.
def sum_of_factors(number):
total = 0
for divisor in range(1, number):
if number % divisor == 0:
total += divisor
return total
for candidate in range(1, 1000):
if candidate == sum_of_factors(candidate):
print(candidate)
# Output: 6, 28, 496
Problem 23: Factorial Sum
Calculate $1! + 2! + 3! + ... + 10!$ using recursion.
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)
total_sum = 0
for k in range(1, 11):
total_sum += factorial(k)
print(total_sum) # 4037913
Problem 24: Valid Parentheses
Determine if a string containing just (, ), {, }, [, and ] is valid. Valid strings have matching pairs in correct order.
Method 1: String Replacement
def is_valid_replace(s):
if len(s) % 2 == 1:
return False
while '()' in s or '[]' in s or '{}' in s:
s = s.replace('()', '').replace('[]', '').replace('{}', '')
return s == ''
Method 2: Stack-Based
def is_valid_stack(s):
if len(s) % 2 == 1:
return False
brackets = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
if not brackets or brackets.pop() != mapping[char]:
return False
else:
brackets.append(char)
return not brackets
print(is_valid_stack("(){}[({[]})]")) # True
print(is_valid_stack("(){}[({[)})]")) # False
Problem 25: Palindrome Number
A palindrome reads the same forwards and backwards.
Method 1: String Conversion
def check_palindrome_str(num):
if num < 0 or (num > 0 and num % 10 == 0):
return False
as_string = str(num)
return as_string == as_string[::-1]
Method 2: Reverse Half
def check_palindrome_half(num):
if num < 0 or (num % 10 == 0 and num != 0):
return False
reversed_half = 0
while num > reversed_half:
reversed_half = reversed_half * 10 + num % 10
num //= 10
return num == reversed_half or num == reversed_half // 10
print(check_palindrome_half(1221)) # True
print(check_palindrome_half(12321)) # True
print(check_palindrome_half(12345)) # False