-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5ef9975
commit 389521d
Showing
1 changed file
with
333 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,342 @@ | ||
# [Problem 592: Fraction Addition and Subtraction](https://leetcode.com/problems/fraction-addition-and-subtraction/description/?envType=daily-question) | ||
|
||
## Initial thoughts (stream-of-consciousness) | ||
- First we'll need to convert all of the fractions to a common denominator. The simplest way to do this is: | ||
- Take all of the unique denominators and multiply them together to get the new denominators | ||
- We can save some time-- first sort the denominators in ascending order. Then, as we're adding new denominators, check whether the new denominator `d` is evenly divisible by any of the denominators that have been added so far. If so, divide `d` by those factors in the list before adding it (assuming it's not already in the list). | ||
- Take each numerator (`n`) and divide it by (`common_denominator / d`), where `d` is the original denominator | ||
- Next, we'll add/subtract the adusted numerators to get the result's numerator | ||
- Then at the end, we'll need to reduce the fraction by dividing both the numerator and denominator of the result by any denominators in the `unique_denominators` list that they are both cleanly divisible by (e.g., `n % x == 0 and d % x == 0`) | ||
- With this approach we don't really need to worry about the special case where the answer is an integer, since the "reducing the fractions" step will result in 1 in that case, which is the desired format | ||
|
||
## Refining the problem, round 2 thoughts | ||
- Things to solve: | ||
- How do we get all of the numerators, denominators, and coefficients? We could just loop through and parse it all manually by searching for `"/"` characters. Something like this: | ||
```python | ||
def parse(expression): | ||
numerators = [] | ||
denominators = [] | ||
|
||
digits = '0123456789' | ||
operators = '+-' | ||
|
||
if expression[0] in digits: | ||
expression = '+' + expression | ||
|
||
sign = expression[0] | ||
expression = expression[1:] | ||
i = 0 | ||
current_number = '' | ||
while i < len(expression): | ||
if expression[i] in digits: | ||
current_number += expression[i] | ||
else: | ||
if len(numerators) == len(denominators): | ||
current_number = sign + current_number | ||
numerators.append(int(current_number)) | ||
else: | ||
denominators.append(int(current_number)) | ||
current_number = '' | ||
|
||
if expression[i] in operators: | ||
sign = expression[i] | ||
|
||
i += 1 | ||
denominators.append(int(current_number)) | ||
|
||
return numerators, denominators | ||
``` | ||
- How do we get the common denominator? | ||
```python | ||
import math | ||
|
||
def common_denominator(denoms): | ||
unique_denoms = [] | ||
for d in sorted(denoms): | ||
for x in unique_denoms: | ||
if d % x == 0: | ||
d /= x | ||
if d not in unique_denoms: | ||
unique_denoms.append(d) | ||
return int(math.prod(unique_denoms)), unique_denoms | ||
``` | ||
- Convert numerators to use the common denominator and sum them together | ||
```python | ||
def convert_and_sum(numerators, denominators, common_denom, unique_denoms): | ||
x = 0 | ||
for n, d in zip(numerators, denominators): | ||
x += int(n * (common_denom / d)) | ||
|
||
# now see if we can divide the sum and common denominator by a common number | ||
for d in unique_denoms: | ||
if (x % d == 0) and (common_denom % d == 0): | ||
x /= d | ||
common_denom /= d | ||
|
||
return f'{int(x)}/{int(common_denom)}' | ||
``` | ||
- I think that's everything; let's put the pieces together! | ||
|
||
## Attempted solution(s) | ||
```python | ||
class Solution: # paste your code here! | ||
... | ||
import math | ||
|
||
class Solution: | ||
def fractionAddition(self, expression: str) -> str: | ||
def parse(expression): | ||
numerators = [] | ||
denominators = [] | ||
|
||
digits = '0123456789' | ||
operators = '+-' | ||
|
||
if expression[0] in digits: | ||
expression = '+' + expression | ||
|
||
sign = expression[0] | ||
expression = expression[1:] | ||
i = 0 | ||
current_number = '' | ||
while i < len(expression): | ||
if expression[i] in digits: | ||
current_number += expression[i] | ||
else: | ||
if len(numerators) == len(denominators): | ||
current_number = sign + current_number | ||
numerators.append(int(current_number)) | ||
else: | ||
denominators.append(int(current_number)) | ||
current_number = '' | ||
|
||
if expression[i] in operators: | ||
sign = expression[i] | ||
|
||
i += 1 | ||
denominators.append(int(current_number)) | ||
|
||
return numerators, denominators | ||
|
||
def common_denominator(denoms): | ||
unique_denoms = [] | ||
for d in sorted(denoms): | ||
for x in unique_denoms: | ||
if d % x == 0: | ||
d /= x | ||
if d not in unique_denoms: | ||
unique_denoms.append(d) | ||
return int(math.prod(unique_denoms)), unique_denoms | ||
|
||
def convert_and_sum(numerators, denominators, common_denom, unique_denoms): | ||
x = 0 | ||
for n, d in zip(numerators, denominators): | ||
x += int(n * (common_denom / d)) | ||
|
||
# now see if we can divide the sum and common denominator by a common number | ||
for d in unique_denoms: | ||
if (x % d == 0) and (common_denom % d == 0): | ||
x /= d | ||
common_denom /= d | ||
|
||
return f'{int(x)}/{int(common_denom)}' | ||
|
||
nums, denoms = parse(expression) | ||
common_denom, unique_denoms = common_denominator(denoms) | ||
return convert_and_sum(nums, denoms, common_denom, unique_denoms) | ||
``` | ||
- Given test cases pass | ||
- Let's make up some other examples: | ||
```python | ||
n_terms = 10 | ||
def random_op(): | ||
if random.random() > 0.5: | ||
return '+' | ||
else: | ||
return '-' | ||
expression = ''.join([f'{random_op()}{random.randint(0, 10)}/{random.randint(1, 10)}' for _ in range(n_terms)]) | ||
if expression[0] == '+': | ||
expression = expression[1:] | ||
print(expression) | ||
``` | ||
|
||
- `expression = "-8/6-3/1+2/1+5/10-10/10+7/4+0/9-4/10+7/5+6/1"`: pass | ||
- `expression = "-2/9+6/2+9/1-9/10-4/10-9/6-0/5+7/5+4/4-6/4"`: fail! returns "99/10" instead of "889/90" -- maybe a rounding error? | ||
- I'm noticing a potential optimization: if the numerator is 0, it's actually not critical that we add the denominator. We can just add "1" to the denominator list instead. | ||
- The `common_denominator` function doesn't seem to be working correctly. For this example, it gives the common denominator as "30" but clearly this isn't correct, since neither 9 nor 4 divide evenly into 30. The correct answer is "270". So what's going wrong... 🤔. Let's debug: | ||
```python | ||
import math | ||
|
||
def common_denominator(denoms): | ||
unique_denoms = [] | ||
for d in denoms: | ||
if d not in unique_denoms: | ||
unique_denoms.append(d) | ||
|
||
print(f'sorted unique denominators: {list(sorted(unique_denoms))}') | ||
reduced_denoms = [] | ||
for d in sorted(unique_denoms): | ||
print(f'considering next: {d}') | ||
if d == 1: | ||
print('\tskipping (denominator is 1)') | ||
continue | ||
for x in reduced_denoms: | ||
if d % x == 0: | ||
print(f'\tdivisible by {x}') | ||
d /= x | ||
d = int(d) | ||
print(f'\tnew denominator: {d}') | ||
if d not in reduced_denoms: | ||
reduced_denoms.append(d) | ||
print(f'\tadding {d} to reduced_denoms; updated list: {reduced_denoms}') | ||
else: | ||
print(f'\t{d} has already been added to the list; skipping') | ||
return int(math.prod(reduced_denoms)) | ||
``` | ||
- For the given example, I'm getting this output: | ||
``` | ||
sorted unique denominators: [1, 2, 4, 5, 6, 9, 10] | ||
considering next: 1 | ||
skipping (denominator is 1) | ||
considering next: 2 | ||
adding 2 to reduced_denoms; updated list: [2] | ||
considering next: 4 | ||
divisible by 2 | ||
new denominator: 2 | ||
2 has already been added to the list; skipping | ||
considering next: 5 | ||
adding 5 to reduced_denoms; updated list: [2, 5] | ||
considering next: 6 | ||
divisible by 2 | ||
new denominator: 3 | ||
adding 3 to reduced_denoms; updated list: [2, 5, 3] | ||
considering next: 9 | ||
divisible by 3 | ||
new denominator: 3 | ||
3 has already been added to the list; skipping | ||
considering next: 10 | ||
divisible by 2 | ||
new denominator: 5 | ||
divisible by 5 | ||
new denominator: 1 | ||
adding 1 to reduced_denoms; updated list: [2, 5, 3, 1] | ||
``` | ||
- So now I can see what the issue is: the `d % x` test is actually filtering out the wrong numbers. E.g., when we consider 4, the 4 should replace the 2 instead of skipping the 4 because it's divisible by 2. Similarly, when we get to 9, the 9 should replace the 3 instead of skipping 9 because it's dividible by 3. So let's rewrite this accordingly (with debug statements): | ||
```python | ||
import math | ||
|
||
def common_denominator(denoms): | ||
unique_denoms = [] | ||
for d in denoms: | ||
if d not in unique_denoms: | ||
unique_denoms.append(d) | ||
|
||
print(f'sorted unique denominators: {list(sorted(unique_denoms))}') | ||
reduced_denoms = [] | ||
for d in sorted(unique_denoms): | ||
print(f'considering next: {d}') | ||
if d == 1: | ||
print('\tskipping (denominator is 1)') | ||
continue | ||
for i, x in enumerate(reduced_denoms): | ||
if d % x == 0: | ||
print(f'\tdivisible by {x}; replacing {x} with {d}') | ||
if d not in reduced_denoms: | ||
reduced_denoms[i] = d | ||
print(f'\tupdated list: {reduced_denoms}') | ||
if d not in reduced_denoms: | ||
reduced_denoms.append(d) | ||
print(f'\tupdated list: {reduced_denoms}') | ||
reduced_denoms = list(set(reduced_denoms)) | ||
print('after removing duplicates: ', reduced_denoms) | ||
return int(math.prod(reduced_denoms)) | ||
``` | ||
- For the "working example," we get: | ||
``` | ||
sorted unique denominators: [1, 2, 4, 5, 6, 9, 10] | ||
considering next: 1 | ||
skipping (denominator is 1) | ||
considering next: 2 | ||
updated list: [2] | ||
considering next: 4 | ||
divisible by 2; replacing 2 with 4 | ||
updated list: [4] | ||
considering next: 5 | ||
updated list: [4, 5] | ||
considering next: 6 | ||
updated list: [4, 5, 6] | ||
considering next: 9 | ||
updated list: [4, 5, 6, 9] | ||
considering next: 10 | ||
divisible by 5; replacing 5 with 10 | ||
updated list: [4, 10, 6, 9] | ||
after removing duplicates: [9, 10, 4, 6] | ||
``` | ||
- So...actually, this isn't correct either. The 10, 4, and 6 should all be divided by 2. From some googling, I see that there's actually a built-in function that will be useful here: `math.gcd` finds the greatest common divisor of two integers. So assuming we don't have any overflow issues, there's a much simpler solution: | ||
- Multiply all of the unique denominators together to get the common denominator | ||
- In the final step, use `math.gcd` to divide both the numerator and denominator by their greatest common divisor so that the fraction is reduced properly. | ||
- Revised solution: | ||
```python | ||
import math | ||
|
||
class Solution: | ||
def fractionAddition(self, expression: str) -> str: | ||
def parse(expression): | ||
numerators = [] | ||
denominators = [] | ||
|
||
digits = '0123456789' | ||
operators = '+-' | ||
|
||
if expression[0] in digits: | ||
expression = '+' + expression | ||
|
||
sign = expression[0] | ||
expression = expression[1:] | ||
i = 0 | ||
current_number = '' | ||
while i < len(expression): | ||
if expression[i] in digits: | ||
current_number += expression[i] | ||
else: | ||
if len(numerators) == len(denominators): | ||
current_number = sign + current_number | ||
numerators.append(int(current_number)) | ||
else: | ||
denominators.append(int(current_number)) | ||
current_number = '' | ||
|
||
if expression[i] in operators: | ||
sign = expression[i] | ||
|
||
i += 1 | ||
denominators.append(int(current_number)) | ||
|
||
return numerators, denominators | ||
|
||
def common_denominator(denoms): | ||
return math.prod(list(set(denoms))) | ||
|
||
def convert_and_sum(numerators, denominators, common_denom): | ||
x = 0 | ||
for n, d in zip(numerators, denominators): | ||
x += int(n * (common_denom / d)) | ||
|
||
# reduce the fraction | ||
y = math.gcd(x, common_denom) | ||
x /= y | ||
common_denom /= y | ||
|
||
return f'{int(x)}/{int(common_denom)}' | ||
|
||
nums, denoms = parse(expression) | ||
common_denom = common_denominator(denoms) | ||
return convert_and_sum(nums, denoms, common_denom) | ||
``` | ||
- Now all of these test cases work! | ||
- Let's try some more... | ||
- `expression = "-0/7+2/7+5/4-3/8+6/9-4/5-4/10+2/7+0/3-2/9"`: pass | ||
- `expression = "8/9+8/9+10/9-4/8+8/1-2/4+1/6-2/2-0/4+7/5"`: pass | ||
- Ok, submitting! | ||
|
||
![Screenshot 2024-08-23 at 12 07 43 AM](https://github.com/user-attachments/assets/28057729-c3bf-4ef5-9043-8c087711b561) | ||
|
||
Solved 🥳! |