|
| 1 | +# [Problem 726: Number of Atoms](https://leetcode.com/problems/number-of-atoms/description/?envType=daily-question) |
| 2 | + |
| 3 | +## Initial thoughts (stream-of-consciousness) |
| 4 | + |
| 5 | +- Hmmm... this one is labeled "hard", but it doesn't seem that bad, unless I'm missing something... |
| 6 | +- ah, the tricky part is going to be dealing with nested parentheses. But still, I don't think that will be too hard. |
| 7 | +- Okay, it seems like there will be some general things we'll need to deal with, and also some special cases I'll want to consider how to handle. |
| 8 | + |
| 9 | +### General stuff |
| 10 | + |
| 11 | +- I'll iterate through the `formula` string, and use a `collections.Counter` to keep track of the counts of each atom. Let's call this `elem_counts`. |
| 12 | +- I'll want to build up the name of the element currently being parsed, so I'll initialize an empty string, something like `curr_elem_name` |
| 13 | +- I think I'll also want to do the same thing for the *count* of the current element (or group), since we'll have to deal with multi-digit numbers. So I'll also initialize `curr_count_digits` to an empty string. |
| 14 | +- Then I'll start parsing `formula`: |
| 15 | + - if the current character is an uppercase letter, we're starting to parse a new element name, so we need to "finalize" either the element or group of elements we've been parsing up to this point. I think this is the trickiest case, in terms of actions we take based on characters we encounter, because what we do depends on the previous characters we've encountered. |
| 16 | + - If we just finished parsing an element name, we can finalize it by: |
| 17 | + - incrementing its count in `elem_counts` by the proper value |
| 18 | + - the "proper value" is 1 if `curr_count_digits == ''`, else `int(curr_count_digits)` |
| 19 | + - overwriting `curr_elem_name` with the current character |
| 20 | + - resetting `curr_count_digits` to `''`. |
| 21 | + - **Note**: how to handle the case where `curr_elem_name` is empty, i.e. we're at the beginning of the string -- there are a few options for this, and one might be better based on how I end up structuring other parts of my solution, but a few ideas: |
| 22 | + - initialize `curr_elem_name` to the first letter in `formula`, and then iterate over `formula[1:]` |
| 23 | + - probably not a great idea cause it's possible `formula` could start with parentheses |
| 24 | + - simply accept that we may add an empty string with a count of 1 to `elem_counts`, and `.pop()` it off before we build up the final string to return |
| 25 | + - this seems like the best option, so I'll plan on going with it for now |
| 26 | + - else (we just finished parsing a group of elements): |
| 27 | + - this is trickier, and I think I'll handle groups of elements as part of the process of handling parentheses, which I'll figure out below |
| 28 | + - elif the current character is a lowercase letter, it's part of the element name we're currently building up, so simply add it to `curr_elem_name` and continue on |
| 29 | + - elif the current character is a digit, we're currently building up a count for the preceding elemen (or group of elements), so add it to `curr_count_digits` and continue on |
| 30 | + - since we'll need to do this for both single elements and element groups, I might want to abstract this to a helper function. |
| 31 | + - elif the current character is `(`, we're about to start parsing an element group. The logic for this could be complex so I'll give it its own section. But we'll also need to "finalize" the element/group we've been building up up to this point |
| 32 | + - else (the current character is `)`), we're finishing parsing an element group. Gonna figure out parsing element groups below. |
| 33 | + |
| 34 | +### Handling element groups |
| 35 | + |
| 36 | +- Okay, at first glance I can think of two ways of dealing with this. One is a one-pass solution and the other is a two-pass solution: |
| 37 | + - The one-pass solution involves calling a recursive function each time we encounter a `(`, and returning from it when we encounter its matched `)` (or possibly after we parse the digits following the `)`, if applicable) |
| 38 | + - The two-pass solution involves using the first pass to resolve parentheses by removing them from the `formula` and multiplying any counts inside them as necessary. The second pass would then just require iterating through the `formula` and parsing element names and counts. |
| 39 | +- The catch is whether the test cases are going to be actual **real**—or at least **realistic**—chemical formulas. If so, then the one-pass solution would be faster. But if they aren't necessarily realistic formulas, we could get something like `((((((((((((((H))))))))))))))` (but with thousands of parentheses), which could cause the one-pass version to hit the recursion limit -- or also just be really slow on contrived test cases. |
| 40 | + - one of the constraints says that "*`formula` is always valid*", but it's not clear whether that means "*syntactically* valid" or "*chemically* valid", i.e., realistic. |
| 41 | +- Could I implement the one-pass solution using a stack instead of recursion? Something like: |
| 42 | + - if we encounter a `(`, initialize an empty stack and start pushing characters to it. |
| 43 | + - Keep track of the current "depth" of parentheses by initializing a counter, incrementing it when we push a `(`, and decrementing it when we push a `)`. |
| 44 | + - ... I don't think this would work. Any number of nested parentheses can have multipliers after them, which means we could need to multiply doubly+ nested parentheses multiple times, so this isn't a strict LIFO situation. |
| 45 | +- I'm gonna take note of the constraint that says the max length of the formula is 1000 characters, which means the worst possible case is 499 levels of nested parentheses. That's a lot, but Python's default max recursion depth is 1,000, so unless leetcode has its own lower limit, we *should* be okay with a recursive solution. |
| 46 | +- Though I'd still like to try to implement the two-pass version |
| 47 | + |
| 48 | +### Hang on a second... |
| 49 | + |
| 50 | +- all of the difficulty with this problem comes from the fact that we have to handle parentheses/element groups in a special/different way because they can be nested and each level of nesting can have its own multiplier, but we won't know what that is until we get to the end of a parenthesized group, so we might have to go back and multiply the counts for elements within nested groups multiple times. |
| 51 | +- But what if we knew the multiplier before we parsed each group? |
| 52 | +- What if we just parsed the whole `formula` right to left? |
| 53 | +- We could just take the inverse of the rules above, in terms of what characters imply what, and I think we could do the whole thing in a single pass. Basically I think we could: |
| 54 | + - initialize: |
| 55 | + - a `collections.Counter` to track element counts (`elem_counts`) |
| 56 | + - an empty string to build up the name of the current element (`curr_elem_name`) |
| 57 | + - an empty string to build up a count for the element/group *to be encountered next*, as we encounter digits (`curr_count_digits`) |
| 58 | + - an integer variable initialized to `1` to tell us what to multiply the count of elements inside the current group by when we increment `elem_counts` (`total_group_multiplier`) |
| 59 | + - **note**: this value is separate from the multiplier given by digits that immediately follow element names -- it only accounts for group-level multipliers |
| 60 | + - a stack to keep track of the individual multipliers for each level of nested parentheses (including 1's for parentheses *without* an explicit a multiplier), so we can `.append()` them and multiply `total_group_multiplier` by them when we encounter `)`s, and `.pop()` them off and divide `total_group_multiplier` by them when we encounter `(`s (`parens_multipliers`) |
| 61 | + - iterate through `reversed(formula)` |
| 62 | + - if the current character is a digit, we're building up a count for the next element or group, so: |
| 63 | + - append/prepend it to `curr_count_digits` and continue |
| 64 | + - **note**: there's a micro-optimization question of whether it's better to build up the counts and element names: |
| 65 | + - as strings, where we add new characters to the beginning of the string, i.e., `curr_count_digits = new_digit + curr_count_digits` |
| 66 | + - as strings, where we add new characters to the end of the string, then `reversed()` the string once we're done building it up |
| 67 | + - as a list, where we insert new characters at the beginning of the list, then `''.join()` the list once we're done building it up |
| 68 | + - as a list, where we append new characters to the end of the list, then `''.join(reversed())` the list once we're done building it up |
| 69 | + - as a `collections.deque`, where we `.appendleft()` new characters and then `''.join()` the deque once we're done building it up |
| 70 | + - I'm not sure which would actually be best here, because on one hand, inserting at the beginning of a list is $O(n)$ where $n$ is the length of the list, buton the other hand $n$ will always be <=2 **if** these are actually real formulas/element names. But I can't tell if they will be. |
| 71 | + - `.reversed()` is also $O(n)$, but it's very fast since it just returns an iterator, and again, $n$ will always be insignificantly small **if** these are real-life examples. |
| 72 | + - `.appendleft()` on a `deque` is $O(1)$, but compared to using strings/lists, it might not actually save time if we're working with a large number of short element names/few-digit numbers, cause the object itself will take longer to initialize than a built-in like `str` or `list`. |
| 73 | + - I'll just pick one of these for now and decide whether or not to micro-optimize later, depending on how my initial solution performs. |
| 74 | + - elif the current character is `)`, we're about to enter a new (potentially nested) group and need to update what we want to multiply element counts within this group by, so: |
| 75 | + - if `curr_count_digits` is non-empty, convert it to an int, set `this_group_multiplier` to it, and reset `curr_count_digits` to an empty string; otherwise, set `this_group_multiplier` to 1 |
| 76 | + - multiply `total_group_multiplier` by `this_group_multiplier` |
| 77 | + - push `this_group_multiplier` onto the `parens_multipliers` stack |
| 78 | + - elif the current character is `(`, we're about to exit a group and need to update what we want to multiply element counts within the next layout outward by, so: |
| 79 | + - pop the last element off `parens_multipliers` and divide `total_group_multiplier` by it |
| 80 | + - elif the current character is a lowercase letter, we're building up the name of an element, so add it to `curr_elem_name` and continue |
| 81 | + - **if** the test cases contain only real element names, we could simply *set* `curr_elem_name` to the current character, rather than appending/prepending it, since we know it can only ever be the 2nd and last character in an element name |
| 82 | + - **note**: although `curr_count_digits` might be non-empty at this point (if the element was 2+ letters and followed by a number), we don't need to deal with it in this branch since all elements will start with an upercase letter, so it'll be easier to always just deal with it there |
| 83 | + - else (the current character is an uppercase letter), we've finished building up an element name, so: |
| 84 | + - if `curr_count_digits` is non-empty, convert it to an int, set `this_elem_multiplier` to it, and reset `curr_count_digits` to an empty string; otherwise, set `this_elem_multiplier` to 1. |
| 85 | + - append/prepend the character to `curr_elem_name` |
| 86 | + - increment the count of `curr_elem_name` in `elem_counts` by `this_elem_multiplier * total_group_multiplier` |
| 87 | + - **note**: remember to reverse the name of the element if building it up by appending rather than prepending |
| 88 | + - reset `curr_elem_name` to an empty string |
| 89 | + - we now have the full set of element counts in `elem_counts`, so we need to iterate over a `sorted()` version of it, and build up the string we want to return |
| 90 | +- I'm gonna try implementing this version |
| 91 | + |
| 92 | +## Refining the problem, round 2 thoughts |
| 93 | + |
| 94 | +## Attempted solution(s) |
| 95 | + |
| 96 | +### Submission 1 |
| 97 | + |
| 98 | +```python |
| 99 | +class Solution: |
| 100 | + def countOfAtoms(self, formula: str) -> str: |
| 101 | + elem_counts = Counter() |
| 102 | + curr_elem_name = '' |
| 103 | + curr_count_digits = '' |
| 104 | + total_group_multiplier = 1 |
| 105 | + parens_multipliers = [] |
| 106 | + |
| 107 | + for char in reversed(formula): |
| 108 | + if char.isdigit(): |
| 109 | + curr_count_digits = char + curr_count_digits |
| 110 | + elif char == ')': |
| 111 | + if curr_count_digits: |
| 112 | + this_group_multiplier = int(curr_count_digits) |
| 113 | + curr_count_digits = '' |
| 114 | + else: |
| 115 | + this_group_multiplier = 1 |
| 116 | + total_group_multiplier *= this_group_multiplier |
| 117 | + parens_multipliers.append(this_group_multiplier) |
| 118 | + elif char == '(': |
| 119 | + total_group_multiplier //= parens_multipliers.pop() |
| 120 | + elif char.islower(): |
| 121 | + curr_elem_name = char + curr_elem_name |
| 122 | + else: |
| 123 | + if curr_count_digits: |
| 124 | + this_elem_multiplier = int(curr_count_digits) |
| 125 | + curr_count_digits = '' |
| 126 | + else: |
| 127 | + this_elem_multiplier = 1 |
| 128 | + curr_elem_name = char + curr_elem_name |
| 129 | + elem_counts[curr_elem_name] += this_elem_multiplier * total_group_multiplier |
| 130 | + curr_elem_name = '' |
| 131 | + |
| 132 | + return ''.join([elem + str(count) if count > 1 else elem for elem, count in sorted(elem_counts.items())]) |
| 133 | +``` |
| 134 | + |
| 135 | + |
| 136 | + |
| 137 | +I'm a bit surprised it's that comparatively slow... maybe prepending to the strings is taking more time than I thought. I'm gonna switch to using `collections.deque` and see if that improves it. |
| 138 | + |
| 139 | +### Submission 2 |
| 140 | + |
| 141 | +```python |
| 142 | +class Solution: |
| 143 | + def countOfAtoms(self, formula: str) -> str: |
| 144 | + elem_counts = Counter() |
| 145 | + curr_elem_name = deque() |
| 146 | + curr_count_digits = deque() |
| 147 | + total_group_multiplier = 1 |
| 148 | + parens_multipliers = [] |
| 149 | + |
| 150 | + for char in reversed(formula): |
| 151 | + if char.isdigit(): |
| 152 | + curr_count_digits.appendleft(char) |
| 153 | + elif char == ')': |
| 154 | + if curr_count_digits: |
| 155 | + this_group_multiplier = int(''.join(curr_count_digits)) |
| 156 | + curr_count_digits = deque() |
| 157 | + else: |
| 158 | + this_group_multiplier = 1 |
| 159 | + total_group_multiplier *= this_group_multiplier |
| 160 | + parens_multipliers.append(this_group_multiplier) |
| 161 | + elif char == '(': |
| 162 | + total_group_multiplier //= parens_multipliers.pop() |
| 163 | + elif char.islower(): |
| 164 | + curr_elem_name.appendleft(char) |
| 165 | + else: |
| 166 | + if curr_count_digits: |
| 167 | + this_elem_multiplier = int(''.join(curr_count_digits)) |
| 168 | + curr_count_digits = deque() |
| 169 | + else: |
| 170 | + this_elem_multiplier = 1 |
| 171 | + curr_elem_name.appendleft(char) |
| 172 | + elem_counts[''.join(curr_elem_name)] += this_elem_multiplier * total_group_multiplier |
| 173 | + curr_elem_name = deque() |
| 174 | + |
| 175 | + return ''.join([elem + str(count) if count > 1 else elem for elem, count in sorted(elem_counts.items())]) |
| 176 | +``` |
| 177 | + |
| 178 | + |
| 179 | + |
| 180 | +Nice 😎 🏃♂️💨 |
0 commit comments