Skip to content

Commit

Permalink
Merge pull request #20 from ghimiresdp/feature/problem-solving
Browse files Browse the repository at this point in the history
add Problem Solving
  • Loading branch information
ghimiresdp authored Feb 12, 2025
2 parents d502843 + 1991045 commit 05feb9b
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 1 deletion.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,24 @@ _[**Notes section**](#notes) first to get started with python._
- [Singleton Pattern](design_patterns/singleton.py)
- Strategy Pattern

## Notes
### [Problem Solving](problem_solving/README.md)

1. [Basic Problem Solving](problem_solving/basic/)
1. [Practical Number Solution](problem_solving/basic/practical_number.py)
2. Iteration with Comprehension
3. [Greatest Common Divisor](problem_solving/basic/gcd.py)
4. Matrix Multiplication
5. Median Calculation
6. [Reverse digits of an integer](problem_solving/basic/reverse_digits.py)

2. [Dynamic Programming](problem_solving/dp/)
1. [Coin Change Problem](problem_solving/dp/coin_change.py)
2. [Fibonacci Series Problem](problem_solving/dp/fibonacci.py)
3. Palindrome Partition Problem
4. Minimizing the sum of list of integers
5. Longest Common Subsequence Problem

### Notes
_(Links will be updated accordingly)_

1. **[Fundamentals of Python](notes/c01_basics)**
Expand Down
35 changes: 35 additions & 0 deletions problem_solving/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Problem Solving

This section will help us become more proficient with problem solving skills
with Python Programming Language.

This section primarily contains 2 different sections which are as follows:

## Basic Problem Solving

This section helps us warming up with basic problem solving skills. It's goal is
to review our previously learned python notes section.

Some of the Basic Problem Solving solutions are as follows:

1. [Practical Number Solution](basic/practical_number.py)
2. Iteration with Comprehension
3. [Greatest Common Divisor](basic/gcd.py)
4. Matrix Multiplication
5. Median Calculation
6. [Reverse digits of an integer](basic/reverse_digits.py)


## Dynamic Programming

This section helps us solving advanced problems more efficiently and using more
optimum solutions. Solving this type of problem helps us efficiently solve
problems while managing time and space complexity efficiently.

Some of the Dynamic Programming solutions are as follows:

1. [Coin Change Problem](dp/coin_change.py)
2. [Fibonacci Series Problem](dp/fibonacci.py)
3. Palindrome Partition Problem
4. Minimizing the sum of list of integers
5. Longest Common Subsequence Problem
58 changes: 58 additions & 0 deletions problem_solving/basic/gcd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Greatest Common Divisor (GCD)
-----------------------------
Greatest Common Divisor or the highest common factor is a common divisor of the
given numbers.
It is the largest positive integer that divides both numbers without leaving
remainders.
For example numbers 12 and 18 has GCD 6
"""

from functools import reduce


def _gcd(a: int, b: int):
"""
Finding out GCD between 2 numbers is so easy in python. we try to perform
modulo operator between 2 numbers until we find remainder to be 0.
For example we have 2 numbers 48 and 64
a = 48, b = 64
Step 1: (a, b) = (b, a%b) = (64, 48 % 64) = (64, 48) b != 0
Step 2: (a, b) = (48, 64 % 48) = (48, 16) b != 0
Step 3: (a, b) = (16, 48 % 16) = (16, 0) b == 0
Setp 4: b == 0, so GCD = a = 16
"""
# find out gcd between 2 numbers
while b:
a, b = b, a % b
return a


def gcd(numbers: list[int]) -> int:
"""
To find out gcd between multiple numbers, we simply reduce the above _gcd
function to each numbers in the list.
"""
return reduce(_gcd, numbers)


if __name__ == "__main__":
print(_gcd(1, 2)) # 1
print(_gcd(12, 18)) # 6
print(_gcd(9, 18)) # 9
print(_gcd(32, 40)) # 8

print(gcd([1, 2])) # 1
print(gcd([32, 40, 80])) # 8
print(gcd([32, 64, 96])) # 32
print(gcd([64, 96, 160, 320])) # 32
print(gcd([64, 96, 160, 320, 48])) # 16
87 changes: 87 additions & 0 deletions problem_solving/basic/practical_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
# Practical Number Solution
---------------------------
A practical number is a positive integer where every smaller can be expressed as
a sum of distinct divisors of that number.
In other words, a number is a practical number whose divisors can be combined to
create a number below it.
For example 12 is a practical number.
It's divisors are: 1, 2, 3, 4, and 6.
1 -> 1
2 -> 2
3 -> 3
4 -> 4
5 -> 4 + 1
6 -> 6
7 -> 7 + 1
8 -> 6 + 2
9 -> 6 + 3
10 -> 6 + 4
11 -> 6 + 4 + 1
"""

import math


def find_divisors(num: int) -> list[int]:
divisors = {1}
for n in range(2, int(math.sqrt(num)) + 1):
if num % n == 0:
divisors.add(n)
divisors.add(num // n)
return list(divisors)


def has_sum(target: int, _set: list[int]):
# if the number itself exists, or number return True
if target == 0 or target in _set:
return True

sums = {0}

for num in _set:
new_sums = {x + num for x in sums}
sums.update(new_sums)
if target in sums:
return True
return False


def is_practical_number(num: int) -> bool:
if num < 1:
return False
divisors = find_divisors(num)
sum_previous = 1

for i in range(1, len(divisors)):
if divisors[i] > sum_previous + 1:
return False
sum_previous += divisors[i]

for n in range(1, num):
if not has_sum(n, divisors):
return False
return True


if __name__ == "__main__":
for number in [8, 10, 12, 15]:
print(
f"{number:<5d}: {'✅' if is_practical_number(number) else '⛔ Not'} Practical"
)

"""
OUTPUT
---------
8 : ✅ Practical
10 : ⛔ Not Practical
12 : ✅ Practical
15 : ⛔ Not Practical
"""
39 changes: 39 additions & 0 deletions problem_solving/basic/reverse_digits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Reverse Digits of an integer
----------------------------
Given an integer, reverse all the digits of an integer mathematically. Example,
if an integer 12345 is given, the reversed integer should be 54321.
Mathematically reversing an integer requires a loop that divides the integer and
find outs the remainder to know the digit of one place and multiplying back by
10 and adding the remainder of the previous iteration until the number becomes 0.
Steps:
1. result = 0
2. divide the integer by 10 and find out remainder and result
- number = num // 10 -> 12345 // 10 = 1234
- r = num % 10 -> 12345 %10 = 5
- num = number -> 1234
3. now multiply the previous remainder by 10 and add with the current remainder
- result = result * 10 + r
4. Repeat steps 2 and 3 until number becomes 0
"""


def reverse_digits(number: int) -> int:
result = 0
if number < 0:
raise ValueError("Negative numbers not allowed")
if number < 10:
return number
while number > 0:
number, result = (number // 10, result * 10 + number % 10)
return result


if __name__ == "__main__":
print(reverse_digits(123)) # 321
print(reverse_digits(12345)) # 54321
print(reverse_digits(87214)) # 41278
110 changes: 110 additions & 0 deletions problem_solving/dp/coin_change.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Coin Change Problem
-------------------
A coin change problem requires us to find out the minimum number of coins from
the given set of coins to change the given amount considering that any number of
coins can be used from the given coin options.
The function should return the minimum number of coins that can be used to
change the given sum. If there are no changes, the function should return -1.
Example we have coins: 1, 2, 5 and we have to change coins with amount 11.
We may have different options:
1. 1 X 11 coins = 11 coins
2. 2 X 5 coins + 1 X 1 coins = 6 coins
3. 5 X 2 coins + 1 X 1 coins = 3 coins
4 1 X 6 coins + 5 X 1 coins = 7 coins
....
and so on
but the change with option 3 will have minimum number of coins to change. so the
solution will be `3`.
We solve it using dynamic programming
intermediate sums:
--------------------+-----------------------------------------------------------
n -> changes | Remarks
--------------------+-----------------------------------------------------------
0 -> 0 |
1 -> 1 | [1 coin of 1]
2 -> 1 | [1 coin of 2]
3 -> 2 | [ 1 + { table(n-2)) = 1 }] = 2
4 -> 2 | [ 1 + { table(n-2) = 1 }] = 2
5 -> 1 | [1 coin of 5]
6 -> 2 | [1 +{ table(n-5) = 1}] = 2
7 -> 2 | [1 +{ table(n-5) = 1}] = 2
8 -> 3 | [1 +{ table(n-5) = 2}] = 3
9 -> 3 | [1 +{ table(n-5) = 2}] = 3
10 -> 2 | [1 +{ table(n-5) = 1}] = 2
11 -> 3 | [1 +{ table(n-5) = 2}] = 3
--------------------+-----------------------------------------------------------
for sum 11, we have largest coin 5 so, we need to find num of changes for
sum of (11-5) = 6, similarly for 6, (6-5) = 1
number of change for 6 = (number of changes for 1) + 1
= 1 + 1
= 2
number of change for 11 = (number of changes for 6) + 1
= 2 + 1
= 3
"""

import sys


def coin_change(coins: list[int], sum: int) -> int:
if sum == 0:
return 0
if coins.__len__() == 0:
return -1

# initialize a list that stores max number of coins used to change the given
# number from amount 1 to the given sum.
table = [sys.maxsize for _ in range(sum + 1)]

# we always need 0 number of coins to change the sum 0 from a given options
table[0] = 0

# iterate from 1 upto the sum to find number of changes for each amount
# we call it an intermediate sum, which will later be used to find the sum
# for the next number
for intermediate_sum in range(1, sum + 1):
# iterate on each coins to find number of changes required
for coin in coins: # example: [1, 2, 5]
# if the intermediate sum is less than the coin, discard it
# since it cannot be changed with that coin
if intermediate_sum >= coin:
# We find the number of coins to change the amount `num` by
# adding 1 to the previous change that was `num - coin`.
# example: if we were about to change $50, and we already know that
# we have 5 changes for $45 and we have $5 coin for change, we do not need to
# find it from the start.
coins_for_prev_change = table[intermediate_sum - coin]

# check if the sub result is smallest when iterated over all the
# available coins.
# example: if sub_result when using coin 8 is smaller than using the coin 5,
# we should choose the sub-result with coin 8 since it will give us smallest
# number of coins to change the specified amount.
if (
coins_for_prev_change != sys.maxsize
and coins_for_prev_change + 1 < table[intermediate_sum]
):
table[intermediate_sum] = coins_for_prev_change + 1
# print(table)
if table[sum] == sys.maxsize:
return -1

return table[sum]


if __name__ == "__main__":
print(coin_change([1, 2, 5], 11)) # 3 -> 5,5,1
print(coin_change([1, 2, 5, 10], 49)) # 7 -> 10X4,5,2,2
print(coin_change([1, 2, 5, 10], 88)) # 11 -> 10X8,5,2,1
Loading

0 comments on commit 05feb9b

Please sign in to comment.