Hadron DaVinci

Minimum Number From Increasing and Decreasing Digits

05 Dec 2020 - Hadron DaVinci


Problem

Given a string containing only I’s and D’s, where I’s represent an increase of 1 between adjacent digits
and D’s represent a decrease of 1 between adjacent digits, create a function named foo which determines
the minimum number formed by digits 1 through 9 that matches that pattern, given that no digits can repeat.

Examples

foo("I") = 12

foo("II") = 123

foo("ID") = 132

foo("IID") = 1243

foo("IIDD") = 12543

foo("IIDDD") = 126543

foo("D") = 21

foo("DD") = 321

foo("DI") = 213

foo("DDI") = 3214

foo("DDII") = 32145

foo("DDIDDIID") = 321654798



Truths

Based on the many examples from above, we can extract some insights.

Truth 1

Truth 2

Truth 3

Truth 4

Truth 5


Methods

Method 1

Time Complexity: n
Spatial Complexity: n

Method 1 Summary
This method shadows the by hand approach discussed in Truth 5 exactly.
Specifically, for each trend, do the following steps:

  1. Find the current trend character
  2. Count how many characters make up the current trend
  3. Create a result only for current trend
    • this result is called a local_result
    • local_result starts incrementing at the highest number in previous trend
      • digit_index allows code to keep track of this information
  4. Modify local_result if current trend character is ‘I’
    • when there is a ‘D’ trend to the right, remove right digit
    • when there is a ‘D’ trend to the left, remove left digit
  5. Append local_result to result

Method 1 Solution

class Zoo:
    def __init__(self, _str):
        """
        :param _str: input string of I's and D's
        """
        self.input = _str
        self.digit_index = 1
        self.current_trend = None
        self.result = ''
    def main(self) -> int:
        """
        Runs main logic
        """
        i = 0
        while i < len(self.input):
            current_trend = self.input[i]
            trend_count = self.get_trend_count(current_trend, i)
            local_result = self.get_trend_result(current_trend, trend_count)
            local_result = self.modify_local_result(current_trend, trend_count, local_result, i)
            self.result += local_result
            i += trend_count
        return self.result
    def get_trend_count(self, trend: str, index: int) -> int:
        """
        Counts the number of trend characters which are the same
        in this part of input.
        For example:
            with 'DDDDII',  at i=0 (the trend that starts at beginning of input string), there are 4 D's
            with 'IIDDIII', at i=0 (the trend that starts at beginning of input string), there are 2 I's
        :param trend: current string being counted. Can be either an 'I' or a 'D'
        :param index: location where the trend starts in input string
        """
        start_index = index
        while self.input[index] == trend:
            index += 1
            if index == len(self.input):
                break
        return index - start_index
    def get_trend_result(self, trend: str, trend_count: int):
        """
        Uses information about the trend, trend_count, and digit index to calculate
        the trend result.
        For example,
            with 4 D's and a digit index of 1, this produces: 4321
            with 4 D's and a digit index of 2, this produces: 5432
            with 4 D's and a digit index of 3, this produces: 6543
            with 4 I's and a digit index of 1, this produces: 1234
            with 4 I's and a digit index of 2, this produces: 2345
            with 4 I's and a digit index of 3, this produces: 3456
        :param trend: current string being counted. Can be either an 'I' or a 'D'
        :param trend_count: number of characters in trend
        """
        range_end = self.digit_index + trend_count + 1
        local_result = ''
        index = self.digit_index
        for index in range(index, range_end):
            if trend == 'I':
                local_result = local_result + str(index)
            elif trend == 'D':
                local_result = str(index) + local_result
        self.digit_index = index
        return local_result
    def modify_local_result(self, trend: str, trend_count: int, local_result: str, index: int) -> str:
        """
        Modifies local_result when trend is 'I' by removing digits it shares with adjacent 'D' trends
        When (index + trend_count) < len(_input), this 'I' trend finishes before end of string which implies
        there is a D trend to the right
        When index != 0. this 'I' trend is not the first trend which implies
        there is a D trend to the left
        :param trend: the current string being counted. Can be either an 'I' or a 'D'
        :param trend_count: number of characters in trend
        :param local_result: string being modified
        :param index: location where the trend starts in input string
        """
        if trend == 'D':                                # don't modify if trend is D
            return local_result
        if (index + trend_count) < len(self.input):     # when there is a D trend to the right, remove right digit
            local_result = local_result[:-1]
        if index != 0:                                  # when there is a D trend to the left, remove left digit
            local_result = local_result[1:]
        return local_result
def foo(_input_str):
    f = Zoo(_input_str)
    result = f.main()
    print(f"{_input_str}:", result)
    return int(result)
# Try Code
if __name__ == "__main__":
    assert foo("I") == 12
    assert foo("II") == 123
    assert foo("ID") == 132
    assert foo("IID") == 1243
    assert foo("IIDD") == 12543
    assert foo("IIDDD") == 126543
    assert foo("D") == 21
    assert foo("DD") == 321
    assert foo("DI") == 213
    assert foo("DDI") == 3214
    assert foo("DDII") ==32145
    assert foo("DDIDDIID") == 321654798


Method 1 Result

I: 12
II: 123
ID: 132
IID: 1243
IIDD: 12543
IIDDD: 126543
D: 21
DD: 321
DI: 213
DDI: 3214
DDII: 32145
DDIDDIID: 321654798

Method 2

Time Complexity: n
Spatial Complexity: n

Method 2 Summary
This method is similar to Method 1 except it:

In general, Method 2 is simpler. Having that said, it wasn’t as intuitive to me at first
but when I saw someone else explain this approach, it made sense to me. The key insight in Method 2:
is that we can use the properties of a stack to reverse the input. Specifically, given a sequence of
elements, we can reverse it by putting the entire sequence onto a stack and popping the stack
until it’s empty.

Note the reason why ‘I’ trends digits aren’t reversed in Method 2 is b/c each ‘I’ trend digit is popped off
the stack before the next ‘I’ trend digit is placed onto the stack. On the other hand, ‘D’ trend digits only
start getting popped off when an ‘I’ character is encountered or when the string ends.

For Method 2, for each index between 0 and (length of input_string) + 1
do the following steps:

  1. Build stack
    • append (i + 1)
  2. If current trend is ‘I’
    • process previous trend data
  3. If no trends are remaining
    • process previous trend data
    • happens when input_string[i] produces an IndexError

Method 2 Solution

def process_trend_data(result: str, stack: list) -> (str, list):
    """
    Uses information about the trend contained in stack to append digits to result.
    :param result: resulting string of digits
    :param stack:
    """
    while len(stack):
        result = result + stack.pop()
    return result, stack
def foo(_str: str) -> int:
    """
    Contains the main logic
    :param _str: input string
    :return: number formed by digits
    """
    stack = []
    result = ""
    for i in range(len(_str) + 1):                              # must iterate to one more than trend
        try:
            current_trend = _str[i]
        except IndexError:
            current_trend = None
        stack.append(str(i + 1))                                # build stack for all trends, I and D
        if current_trend is None:                               # when no trends are remaining, process previous trend
            result, stack = process_trend_data(result, stack)
        elif current_trend == 'I':                              # when the trend is I, process previous trend
            result, stack = process_trend_data(result, stack)
    print(f"{_str}:", result)
    return int(result)
# Try Code
if __name__ == "__main__":
    assert foo("I") == 12
    assert foo("II") == 123
    assert foo("ID") == 132
    assert foo("IID") == 1243
    assert foo("IIDD") == 12543
    assert foo("IIDDD") == 126543
    assert foo("D") == 21
    assert foo("DD") == 321
    assert foo("DI") == 213
    assert foo("DDI") == 3214
    assert foo("DDII") ==32145
    assert foo("DDIDDIID") == 321654798


Method 2 Result

I: 12
II: 123
ID: 132
IID: 1243
IIDD: 12543
IIDDD: 126543
D: 21
DD: 321
DI: 213
DDI: 3214
DDII: 32145
DDIDDIID: 321654798


Files

method1.py
method2.py