3

We are asked to modify a string by performing specific moves on it. Given a string of lowercase English characters ('a' - 'z'), two types of moves can be performed on any index, any number of times:

  1. Decrement the character by 1. The letter 'a' cannot be decremented.
  2. Increment the character by 1. Letter 'z' cannot be incremented.

Calculate the minimum number of moves required to modify the string to a good form. A good form string is one in which each character is adjacent to at least one equal character.

Example 1:

s = ‘aca’

Output: 2

Explanation: Decrement 'c' twice to get 'aaa'. A minimum of 2 moves is required.

Example 2:

s = 'abcdef'

Output: 3

Explanation: Decrement 'b' by 1 to become 'a'. Increment 'c' by 1 to become 'd'. Increment 'e' by 1 to become 'f'. This gets us "aaddff".

The first and last characters of the string only have one adjacent neighbor, and so they must be equal to that adjacent character.

What would be the best way to go about this?

I initially thought using two pointers, one for the left/start and right/end indices, and slowly moving them towards the center would be the best approach, using the thought that the edge of the string must be equal to the inner character in order to become equal.

But this doesn't give us a globally optimal solution. Would the best approach be something related to dynamic programming? If so, how would that look?

Def Lakos
  • 51
  • 4

1 Answers1

1

Yes, there’s a dynamic program, which can be extracted straightforwardly from an algorithm that verifies a solution, implemented below in Python.

def has_good_form(s):
    previous_letter = None
    needs_adjacent_copy = False
    for letter in s:
        if letter == previous_letter:
            needs_adjacent_copy = False
        elif not needs_adjacent_copy:
            previous_letter = letter
            needs_adjacent_copy = True
        else:
            return False
    return not needs_adjacent_copy


print(has_good_form("aca"))
print(has_good_form("aaa"))
print(has_good_form("abcdef"))
print(has_good_form("aaddff"))

has_good_form() remembers the previous letter and whether it is known to have an adjacent copy, for a total of 26×2 = 54 states, plus the starting state (so 55). The idea of the dynamic program is, for each state and time, what’s the cheapest set of modifications that will put has_good_form() in that state at that time? That looks something like this (untested).

import collections
import math
import string


def distance(a, b):
    return abs(ord(a) - ord(b))


def cheapest_good_form(s):
    state_to_min_cost = {(None, False): 0}
    for original_letter in s:
        options = collections.defaultdict(list)
        for state, min_cost in state_to_min_cost.items():
            previous_letter, needs_adjacent_copy = state
            for letter in string.ascii_lowercase:
                cost = min_cost + distance(original_letter, letter)
                if letter == previous_letter:
                    options[(letter, False)].append(cost)
                elif not needs_adjacent_copy:
                    options[(letter, True)].append(cost)
        state_to_min_cost = {state: min(costs) for (state, costs) in options.items()}
    return min(
        (
            cost
            for (
                (_, needs_adjacent_copy),
                cost,
            ) in state_to_min_cost.items()
            if not needs_adjacent_copy
        ),
        default=math.inf,
    )


print(cheapest_good_form(""))
print(cheapest_good_form("a"))
print(cheapest_good_form("aca"))
print(cheapest_good_form("abcdef"))
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120