Разделение и завоевание вашего пути через проблему

Пролог

Только что пройдя тест RB109 Interview Assessment для основной учебной программы Launch School, я решил, что сейчас самое подходящее время, чтобы поделиться некоторыми мыслями и идеями, которые я получил на этом пути (по общему признанию, это крошечная часть долгого пути, который мне еще предстоит пройти). В частности, я хотел поделиться своим подходом к бесценному процессу PEDAC, поддерживаемому Launch School, а также некоторыми адаптациями, которые до сих пор работали для меня.

Здесь присутствуют все основные элементы PEDAC от Launch School, хотя иногда и в слегка переодетом виде. Это то, что щелкнуло для меня, и это результат нескольких моментов «ага», полученных в течение бесчисленных часов самообучения, сеансов кодирования в реальном времени, учебных занятий под руководством ТА и учебных групп SPOT, когда я готовился к живая оценка интервью[1]. Учитывая, как рано я нахожусь в основной учебной программе, мой подход PEDAC, вероятно, будет меняться, развиваться и адаптироваться по мере того, как я сталкиваюсь с новыми проблемами и новыми способами мышления. Хороший подход к решению проблем должен быть адаптируемым, и я хочу верить, что этот не будет исключением. Если что-то из нижеследующего окажется полезным, не стесняйтесь использовать его, изменять, делать по-своему.

Понимание проблемы

(PEDAC)

Ввод, вывод

Если бы решение проблемы было путешествием (путешествием данных), ввод был бы его отправной точкой, а вывод – пунктом назначения.

Возьмем следующее описание проблемы:

Для заданной строки найдите все содержащиеся в ней подстроки, состоящие не менее чем из двух символов, в которых подстрока находится рядом с подстрокой, противоположной самой себе.[2]

Начнем наше путешествие со струны. Это наш вход, данные перед их великим преобразующим приключением. Когда мы достигнем конца нашего путешествия, много логических шагов спустя, эти данные будут видеть многое и претерпевать множество испытаний. Он изменится в ответ на требования проблемы. Его окончательное преобразование, с которым мы вернемся домой, будет выходным: набором подстрок, отвечающих определенным критериям (длиной два или более символа, примыкающих к подстрока, обратная самой себе).

При первом приближении к проблеме полезно обратить внимание на эти две ключевые точки пути: начало и цель. Запишите их. Это не только послужит напоминанием о предстоящей более крупной миссии (откуда мы идем? кем мы должны стать и куда мы должны прибыть?), но, с более прагматической точки зрения, это хороший способ установить история в движении, как бы разбивая двумя ключевыми битами информации первоначальную пустоту страницы (первое из многих небольших достижений на этом пути):

# INPUT: String
# OUTPUT: Array of substrings

Теперь, когда мы знаем, откуда мы начинаем (строка) и куда мы идем (массив, содержащий все подстроки из входной строки, которые соответствуют определенному набору критериев), все, что нам нужно сделать, это выйти туда в мир и сделать шаги, которые приведут нас от А до Я. Миссия выполнена.

Ну, не так быстро… Теперь, когда мы знаем нашу отправную точку и конечный пункт, мы должны найти время, чтобы спланировать наше путешествие. Есть много способов добраться от А до Я, включая остановки у каждой лужи воды и маленького городка по пути (от А до Б, от В до С, от С, до D, до Е, до…), пока мы не потерять из виду, куда мы шли. Давайте не будем этого делать. Вместо этого, прежде чем выскочить за дверь и оставить нашу карту и компас позади, давайте определим ключевые стратегические точки остановки — перерывы в пути, которые сделают долгое путешествие более управляемым и, если повезет, возможно, даже более приятным.

Именно здесь мы должны смотреть на проблему с точки зрения высокого уровня, сверху вниз (разве это не та точка зрения, которую предлагает карта?) и получить общую картину того, о чем спрашивают и как выполнить это.

Поймите, откуда вы пришли

(PEDAC)

Важно знать, откуда вы едете, чтобы затем наилучшим образом выбрать путь к месту назначения. В контексте решения проблем это означает, что нужно потратить достаточно времени на понимание проблемы. Мне нравится разбивать этот этап на следующие компоненты:

  1. Переформулируйте проблему. Перепишите проблему своими словами и произнесите ее вслух своим голосом. Вы с большей вероятностью узнаете, о чем вас спрашивают, если перефразируете это более узнаваемым тоном, используя более знакомые термины. При этом вы больше не пассивно повторяете слова, которые, возможно, не понимаете: поскольку вы активно переводите проблему в свои собственные слова, вы уже делаете выбор, оказываетесь на перекрестке и берете на себя ответственность за шаги, которые вы уже начинаете делать. брать.
  2. Понимание примеров и тестовых случаев. Если вам посчастливилось получить примеры и тестовые примеры (а для оценки интервью RB109 нам очень повезло), проведите с ними время. Разбейте их. Посмотрите на их входные данные и попытайтесь предсказать их результаты, а затем подтвердите свои прогнозы. Это ценные примеры того, как ввод соотносится с выводом, как источник и пункт назначения формируются и формируются друг другом. Изучите их, чтобы подтвердить явные правила задачи и выявить неявные. Часто бывает полезно выбрать один или два тестовых случая (скажем, очень простой, а другой чуть более сложный) и попытаться вручную воссоздать путь от ввода до вывода, шаг за шагом. Это поможет вам понять, что было возвращено, и начнет давать вам представление о возможных путях, поворотах и ​​поворотах, предпринятых входом в его триумфальном путешествии к выходу.
  3. Сформулируйте цель. Переформулировав задачу своими словами и поняв требования задачи и их значение, просмотрев тест-кейсы, я считаю полезным в общих чертах сформулировать стоящую перед ней задачу своими словами. Это послужит напоминанием о том, чего я в конечном итоге хочу достичь.

Имея в виду вышеизложенное, давайте снова посмотрим на наш рабочий пример. Нам дают строку в качестве входных данных, а затем нас просят

… найти все подстроки, состоящие не менее чем из 2 символов, в которых подстрока соседствует с подстрокой, противоположной самой себе.

Не бойтесь внимательно изучить описание проблемы и задать (себе или экзаменатору, или обоим) все вопросы, которые вам нужно задать, даже если они поначалу кажутся (вам) смущающе очевидными:

  • Что такое подстрока?
  • Что означает, что подстрока «смежна» с другой подстрокой?
  • Что означает, что подстрока является «обратной» другой подстроке?

Задавая эти вопросы, даже когда (или особенно когда) они кажутся вам очевидными, вы можете понять, что получили ценное понимание проблемы и что из них можно извлечь многообещающие стратегии решения проблем. (Например, знание того, что «подстрока» — это не то же самое, что «слог», может в значительной степени помочь вам направить вашу стратегию в правильном направлении и решить, какие операции вы хотели бы выполнять над строкой). данные, с которыми вы работаете.)

На этом этапе полезно взглянуть на предоставленные примеры и тест-кейсы, чтобы лучше понять требования задачи (как явные, так и неявные) и более четко сформулировать поставленную цель. Давайте подойдем к ним один за другим:

find_reverse_substrings('1221345543') == ["12", "21", "345", "543", "45", "54"]
find_reverse_substrings('beeekkeeper') == ["eek", "kee", "ek", "ke"]
find_reverse_substrings('1111111') == ["11", "111"]
find_reverse_substrings('hellolleh') == []
  • Из первого теста мы получаем хорошее представление о том, что подразумевается под смежными подстроками в контексте задачи. Смежные здесь означают, что подстроки идут подряд, впритык друг к другу. Мы также можем получить более четкое представление о втором требовании задачи: подстрока, смежная с подстрокой, обратной самой себе. В рассматриваемом тестовом примере возвращаемые подстроки не только изначально находились вплотную друг к другу во входных данных (например, за "12" следует "21" во входной строке), но они также оказались зеркальными. изображения друг друга: 12|21 , 345|543 .

  • Второй тестовый пример еще больше подкрепляет предыдущий вывод и поясняет, что подстроки подстрок также соответствуют критериям допустимых выходных данных: "eek" и "kee" включены вместе с "ek" и "ke" в возвращаемый массив.
  • Третий тестовый пример, однако, приносит с собой новое понимание требований задачи: количество возвращаемых подстрок ["11", "111"] кажется меньше, чем можно было ожидать до этого момента. Внимательное наблюдение говорит нам, что среди возвращаемых подстрок не должно быть дубликатов: возвращаемые подстроки уникальны.
  • Наконец, четвертый тестовый пример, возвращающий пустой массив, говорит нам, что недостаточно, чтобы подстроки были смежными изеркальными изображениями друг друга: две соседние подстроки не могут иметь символ, который разделяет один и тот же индекс между ними. (["helloolleh"] соответствует критериям, но ["hellolleh"] нет, поэтому возвращается пустая строка).

Внимательное прохождение каждого теста позволяет сформулировать цель задачи своими словами, а также отметить несколько явных и неявных требований:

# GOAL: Return all substrings of 2 characters or more that are back-to-back with a substring that is a mirror-image of itself.
# - Substrings must be at least 2 characters in length
# - Substrings of substrings, as long as they meet the criteria, must be included in the return array (example 2)
# - The return result must not contain duplicates (example 3)
# - The two adjacent substrings must not share an element at the same index (example 4)
# - If no substrings qualify, return an empty array (example 4)

Приключения и трансформации данных, оборотень

(PEDAC)

Я часто нахожу полезным отслеживать множество метаморфоз, которые претерпевают входные данные на пути к выходу. Акцент здесь меньше на входе и выходе (начало и конец) и больше на середине. Иными словами, теперь основное внимание уделяется промежуточным формам, которые структура данных примет на пути к результату. Имея представление обо всех преобразованиях, которым будет подвергаться структура данных, можно предложить возможные стратегии решения проблем и соответствующие операции, которые сделают эти преобразования возможными.

Поскольку этот шаг происходит непосредственно перед тем, как мы разбиваем процесс решения проблемы на этапы алгоритма (т. е. поскольку мы еще не разбили процесс на действенные шаги по направлению к решению), есть интуитивный, несколько предварительный аспект это. Я пытаюсь «спроектировать» или «визуализировать» множество форм, которые будут принимать данные по мере их перехода от ввода к выводу. Я обнаружил, что чем больше проблем вы решаете, тем лучше вы знакомитесь с возможными шаблонами и тем легче становится визуализировать преобразования данных.

Если наша интуиция поначалу не окажется на 100 % точной, ничего страшного. Всегда можно точно настроить маршрут, по которому будет следовать структура данных, по мере того, как мы разбираем проблему и получаем более четкое представление о шагах, которые мы хотим предпринять. Не бойтесь возвращаться к своим заметкам PEDAC и вносить в них новые идеи и открытия.

Вернемся к нашему примеру. Мы знаем, что хотим найти все подходящие подстроки из входной строки. Чтобы найти подстроки, вполне вероятно, что мы могли бы захотеть выполнить итерацию по символам входной строки. Имея достаточный опыт решения проблем, мы, вероятно, знаем, что массив символов подходит именно для той операции, которая нам нужна. Затем мы хотели бы вернуть соответствующие подстроки в массиве. Учитывая вышеизложенное, путь, который наша исходная структура данных, вероятно, проделает от ввода до вывода, выглядит следующим образом:

String -> Array of characters -> Array of substrings

В зависимости от разработанного или реализованного решения промежуточные этапы пути, пройденного данными, могут различаться. Все в порядке. Процесс гибкий и призван действовать как руководство: если бы судьба была абсолютной и непоколебимой, высеченной в камне с самого начала, насколько героическим было бы на самом деле путешествие героя?

Алгоритм исполнения желаний

(PEDAC)

Теперь, когда мы поняли и переформулировали проблему в более узнаваемых терминах (P), проверили наше понимание и отметили как явные, так и неявные требования (E), и мы в общих чертах обрисовали многие формы, которые структура данных может принять от ввода до вывода (D), пришло время наметить шаги, которые предстоит пройти в этом путешествии. Пришло время для нашего алгоритма (A). Но держись, бесстрашный странник, пока не мочи свой палец, чтобы измерить ветер и измерить нанодюймы от твоей двери до улицы! Посмотрите вперед и бросьте быстрый взгляд на горизонт перед собой, прежде чем посмотреть вниз и медленно двигаться к месту назначения. Вспомните свою карту и перспективу сверху вниз, которую она обеспечивает. Примите вид с высоты птичьего полета и получите представление об общем подходе, прежде чем потеряться в тонкостях его деталей.

В этом духе мне нравится начинать свой алгоритм с высокоуровневой формулировки шагов, которые необходимо предпринять. Здесь мне может пригодиться параллель с концепцией разбиения на части: я пытаюсь сгруппировать (разделить на части) многие части проблемы или подзадачи в соответствии с их функциональностью в целом. Например, если задача состоит в том, чтобы разбить массив на все возможные подмассивы, я не буду начинать свой алгоритм с перечисления всех шагов, необходимых для выполнения этой задачи. На самом деле, на данном этапе меня даже не волнуют эти шаги — я могу даже не знать, что они из себя представляют. Вместо этого я буду разбивать или абстрагировать эти шаги от того, чего они должны достичь. Я хочу, чтобы у меня был метод, который проделал бы за меня все эти шаги и как по волшебству вручил бы мне достигнутую цель, и напишет мой алгоритм так, как будто этот метод уже существовал.

Возвращаясь к нашему примеру: мы хотели бы найти все подстроки из двух или более символов из входной подстроки, которые соседствуют с другой подстрокой, противоположной самой себе. Вместо того, чтобы сосредотачиваться на каждом необходимом маленьком шаге в низкоуровневой микроориентированной манере и писать что-то вроде этого —

# - Initialize an empty array to collect results
# - Set a counter start_index to 0
# - Iterate over the input string starting at index 0 up to the length of the string minus 1
#  - Iterate over the input string again starting at.... 
#  - etc. etc.

— Вместо этого я хотел бы, чтобы существовал метод, который позаботился бы обо всех необходимых шагах, и, разбив мою большую проблему на более мелкие, более управляемые подзадачи, я бы просто написал это:

# ALGORITHM: 
# - Find all substrings of 2 characters or more (wish: HELPER METHOD #1)
# - Check whether any of the substrings are consecutive mirror-images of each other (wish: HELPER METHOD #2)
# - If so, push them to a `results` array
# - Return `results`

И вуаля!

Ах, если бы все было так просто, я слышал, вы говорите… Действительно. Эти желанные методы сами себя не напишут…

Но вот где кроется настоящее волшебство: желая, чтобы все было так просто, и при этом абстрагируясь от множества шагов и сосредотачиваясь на цели (или подцелях) чтобы добиться этого, мы фактически упрощаем процесс. Я могу добавить желаемый метод или методы в свой список пожеланий. И если я вел себя хорошо и не торопился, чтобы понять и разобрать проблему, эти методы — как программистский эквивалент помощников Санты — возвращаются как вспомогательные методы, один за другим, чтобы помочь мне решить эту сложную задачу. задача проще и решаемей. Разделяя свои проблемы на подзадачи, каждая со своим вспомогательным методом, я могу решать одну проблему за раз, не чувствуя себя перегруженным и, самое главное, не теряя из виду общую картину. Или, выражаясь эпическим тоном, которого, кажется, желает эта статья: я разделяю и властвую.

Еще один способ понять вышеизложенное — подумать о различии между декларативным и процедурным подходом к программированию. В процедурном (или императивном) подходе к программированию основное внимание уделяется как, то есть всем шагам, необходимым для выполнения данной задачи. В декларативном подходе к программированию вместо этого основное внимание уделяется что: мы предполагаем, что как позаботится сам по себе и вместо этого сосредоточьтесь на что необходимо выполнить, заявляя об этом по ходу дела. При необходимости мы всегда можем обратиться к как отдельно как к отдельной подзадаче, тем самым разделив наши интересы. Представьте, например, что мне нужно забрать ребенка из школы в определенное время дня. Ниже представлено решение этой проблемы, написанное процедурно и декларативно соответственно. (Оба подхода решают одну и ту же задачу, но не могли бы вы сказать мне, какой из них подвергается большему риску упустить из виду более широкую картину?)

[Procedural approach]
- Check the time
- If time is equal to or greater than 4:30pm :
 - Stop studying for Launch School for the time being
 - Grab the car key
 - Open the apartment door
 - Leave the apartment
 - Close the apartment door
 - Turn the key in the lock to lock the apartment door
 - Remove key from lock
 - Walk to the car in the parking lot
 - Open the car door
 - Enter the car
 - Close the car door
 - Turn on the car
 - etc.
[Declarative approach]
- If time is equal to or greater than 4:30pm :
 - Go get kid at school 
 - Return home safely
- Otherwise, continue studying for Launch School.

ХОРОШО. Теперь, когда мой ребенок вернулся из школы, я могу вернуться к путешествию моего героя. Давайте посмотрим на весь наш процесс PEDAC:

# Problem: 
# Given a string, find all those substrings contained in it of #at least two characters where the substring is adjacent to a substring that is the reverse of itself.
# INPUT: String
# OUTPUT: Array of substrings
# GOAL: Return all substrings of 2 characters or more that are back-to-back with a substring that is a mirror-image of itself.
# - Substrings must be at least 2 characters in length
# - Substrings of substrings, as long as they meet the criteria, must be included in the return array (example 2)
# - The return result must not contain duplicates (example 3)
# - The two adjacent substrings must not share an element at the same index (example 4)
# - If no substrings qualify, return an empty array (example 4)
# DATA STRUCTURE:
# String -> Array of characters -> Array of substrings
# ALGORITHM (High-Level): 
# - Find all substrings of 2 characters or more (wish: HELPER METHOD #1)
# - Check whether any of the substrings are consecutive mirror-images of each other (wish: HELPER METHOD #2)
# - If so, push them to a `results` array
# - Return `results`

Заключение — кодирование с намерением

(PEDAC)

Вы сделали это! Поздравляю, попутчик! Вы спланировали свое путешествие, подготовились, тщательно выбрали провизию и припасы, чтобы взять с собой. Теперь у вас есть все, что нужно, чтобы идти туда и совершать великие дела с намерением и контролем. Теперь вам осталось только открыть дверь, сделать первый шаг наружу и — «Привет, мир!» — приступайте к реализации своих планов, превращая их в действия.

Что ж, если приведенный выше вывод звучит несколько удручающе, если кажется, что большая часть вашего путешествия прошла в безопасных пределах вашего подвала, прежде чем у вас даже появился шанс сделать свой самый первый шаг в реальном мире кода, не будьте обескураженный.

Как бы заманчиво эпично это ни звучало, мы не хотели бы ломать голову над проблемой, в одиночку и неподготовленные в реальном мире синтаксиса, встроенных методов и сообщений об ошибках.
Вышеуказанные шаги существуют именно для того, чтобы мы могли намеренно пройти через проблему, полностью контролируя наш код: от разбивки проблемы до алгоритма и до его реализации. Хотя реализация нашего решения в коде и наблюдение за тем, как оно проходит все тестовые случаи, — это триумфальный финал, на который мы надеялись, это также, возможно, наименее важный из всех шагов, которые мы можем предпринять на нашем пути. Наиболее важными шагами являются те, которые делают последний шаг возможным. PEDA, которая прокладывает путь к C.

Тем не менее, этот последний шаг — реализация кода с намерением — ни в коем случае не является неважным. В конце концов, только после реализации нашего алгоритма в виде кода мы можем проверить, работает ли логика нашего кода, и протестировать его в формальных границах данного языка. Еще одна причина, по которой этот последний шаг также важен, заключается в том, что процесс решения проблем не является линейным. Между алгоритмом и кодом, между высокоуровневыми формулировками и их низкоуровневыми аналогами происходит здоровая и необходимая обратная связь.

Давайте посмотрим на это на практике, шаг за шагом, используя наш процесс PEDAC до сих пор и наш высокоуровневый алгоритм, в частности, для руководства нашей реализацией решения в коде.

Coda — Внедрение решения шаг за шагом

До этого момента наш алгоритм намеренно оставался на высоком уровне, фокусируясь на что, а не на как. Давайте еще раз взглянем на него:

# ALGORITHM (High-Level): 
# - Find all substrings of 2 characters or more (wish: HELPER METHOD #1)
# - Check whether any of the substrings are consecutive mirror-images of each other (wish: HELPER METHOD #2)
# - If so, push them to a `results` array
# - Return `results`

Из приведенного выше высокоуровневого алгоритма мы можем приступить к реализации каркаса нашего кода, высокоуровневой структуры, в которой мы декларативно указываем что мы хотим выполнить, и желание, чтобы методы, которые еще не существуют, позаботились о том, как это сделать:

Попробуем описать, что происходит построчно.

В строке 2 я хочу, чтобы метод find_substrings принимал строку в качестве входных данных и возвращал все подстроки размера два и выше. В строке 3, предполагая, что у меня есть подстроки, возвращаемые find_substrings , теперь мне нужен второй метод mirror_image_substrings, который брал бы подстроки из предыдущей строки и возвращал бы только те подстроки, которые соседствуют с зеркальными изображениями самих себя, тем самым удовлетворяя задаче. требования. Если бы эти два метода существовали, проблема была бы решена. Эти леса - все, что нужно моему основному методу (более крупной проблеме). К нему не нужно добавлять дополнительную строку кода. Разделив свои заботы, теперь все, что мне нужно, это сосредоточиться на двух оставшихся подпроблемах, по одной за раз. Разделяй и властвуй.

Захотев два пока еще не существующих вспомогательных метода, и поскольку не все герои наделены эпической памятью, мне нравится добавлять в свой код заглушки (или сигнатуры) этих желанных методов. Это мой список желаний. Это также мой список дел: напоминание о том, что еще нужно сделать. С заглушками мой код выглядит так:

Теперь, наметив каркас основного метода и пожелав два вспомогательных метода, самое время вернуться к нашему алгоритму. Мы можем начать разбивать пожелания метода на более конкретные шаги более низкого уровня (вы ведь не верили, что они материализуются сами по себе, как по волшебству, не так ли?):

# HELPER-METHOD 1: Find substrings of 2 or more characters
# - Initialize an empty array `results`
# - Iterate over input string starting at 0 up to the length of string minus 2 (start_index)
#   - Iterate over input string starting at start_index up to the length of string minus 1 (end_index)
#      - Push substrings to `results`
# - Return `results`

Теперь, когда я конкретизировал шаги более низкого уровня для моего вспомогательного метода № 1, я чувствую, что готов реализовать его с намерением:

Важно тестировать наш код часто и заранее. Это в равной степени верно, когда речь идет о вспомогательных методах. Приведенный выше метод возвращает все подстроки данного ввода длиной два или более символов, как и ожидалось. Конечно, есть место для рефакторинга, но пока нам не нужно об этом беспокоиться. Зная, что вспомогательный метод работает должным образом, мы знаем, что строка 2 нашего основного метода (вызов вспомогательного метода) также работает. Разделив наши проблемы и разбив общую проблему на более мелкие, мы можем позаботиться о первом шаге нашего алгоритма высокого уровня:

- Find all substrings of 2 characters or more

Теперь, когда у нас есть все подстроки из двух или более символов, все, что нам нужно сделать, это решить последнюю подзадачу, возможно, самую сложную. Повторим нашу задачу:

Возвращает все подстроки из 2 символов или более, которые расположены вплотную друг к другу с подстрокой, которая является зеркальным отражением самой себя.

Как мы можем проверить, какие из всех подстрок, которые мы смогли собрать на шаге выше, являются смежными и противоположными друг другу?

Как и в случае многих других проблем, здесь есть много решений, некоторые из которых сложнее, чем другие. Моя стратегия заключалась в том, чтобы попытаться взглянуть на требования под другим углом, с которого они казались бы проще, чем кажутся на первый взгляд. Вместо того, чтобы пытаться отслеживать, какие перевернутые подстроки были смежными с другими или какие смежные подстроки были противоположными другим, все время пытаясь извлечь их из входной строки, деликатно уравновешивая бесчисленные движущиеся части, разве не было бы здорово — Хотел бы я — если бы мы могли просто выбрать те составные подстроки, которые уже несут в себе своих обратных близнецов? (Представьте себе двойное существо, подобное человеку, стоящему спиной к зеркалу, обладающему двумя головами, двумя животами, четырьмя руками и четырьмя ногами, существо, которое мы могли бы разделить на две совершенно симметричные половины — половины, которые были бы смежными). и зеркальные отражения друг друга по своей природе, без каких-либо дополнительных усилий с нашей стороны.)

Другими словами: если бы мы могли выбирать подстроки, которые изначально были такими —

а затем просто разрежьте их на две равные половинки, зеркально отражающие друг друга, вот так:

— мы бы достигли нашей цели, затратив лишь часть усилий, которые нам пришлось бы потратить в противном случае. Это слишком много, чтобы желать?

Ну не совсем. Если, например, мы внимательно посмотрим на подстроки, возвращаемые нашим методом find_substrings для теста «пчеловод», мы заметим, что такие подстроки, как «eekkee» и «ekke», являются идеальными аналогами приведенной выше антропоморфной аналогии. Все, что нам нужно было бы сделать после того, как мы выбрали эти подстроки соединенных близнецов, — это тривиально разрезать их на две идеальные половины, которые затем мы могли бы поместить в наш возвращаемый массив.

Но вот важный момент, который я хотел бы подчеркнуть: семя, которое сделало это понимание возможным и позволило мне взглянуть на эту более простую стратегию решения проблемы, было посажено в самом начале процесса PEDAC, когда я был переформулирую проблему своими словами. Напомним исходную формулировку задачи:

Для заданной строки найти все содержащиеся в ней подстроки, состоящие не менее чем из двух символов, в которых подстрока находится рядом с подстрокой, противоположной самой себе.

А теперь давайте сравним это с моим переформулированием:

GOAL: Return all substrings of 2 characters or more that are back-to-back with a substring that is a mirror-image of itself.

Я не могу в достаточной степени подчеркнуть важность переформулирования проблемы таким образом, который ближе вам. В моей переформулировке эти, казалось бы, невинные выборы слов — спина к спине, зеркальные образы — уже содержали, хотя я еще не осознавал этого, семена моего будущего подхода к решению проблемы. Они предоставили бы мне достаточное изменение точки зрения, чтобы позволить мне переопределить проблему в терминах, которые я мог бы легче распознать, и более легко и намеренно реализовать в коде.

Это позволило мне конкретизировать следующий шаг моего высокоуровневого алгоритма (мой вспомогательный метод № 2) следующим образом:

# ALGORITHM (High-Level):
# Check whether any of the substrings are consecutive mirror images of each other
# HELPER-METHOD 2: Find Mirror-image substrings
# - Initialize empty `results` array
# - Iterate over the array of substrings
# - For each substring, break it down into two halves
#   - If the first half is equal to the second half in reverse, push both halves to the `results` array
# - Return results array

Это, в свою очередь, позволило мне вернуться к алгоритму моего вспомогательного метода №1 и немного его рефакторить, чтобы выбрать только те подстроки, которые были четными по размеру (поскольку меня интересовали только те подстроки, которые можно было разделить на идеальные половины ) и четыре символа в длину или более (поскольку определяющие подстроки должны быть разделены пополам). Вот как на данный момент выглядит реализация обоих вспомогательных методов:

На этом этапе оставалось только убедиться, что я учел все крайние случаи и требования задачи. Просматривая свои заметки, мне напомнили, что возвращаемые результаты не должны содержать дубликатов. Это легко решить, вызвав метод Array#uniq для возвращаемого значения mirror_image_substrings .

Разбив, казалось бы, довольно сложную проблему на более мелкие подзадачи, приняв нисходящий подход и двигаясь от простого к сложному, от более высокого уровня к более низкому, стало возможным решать одну проблему за раз, сохраняя кластеры управляемой логики. И таким образом, от маленькой победы к маленькой победе, я смог разделять и завоевывать свой путь к решению, которое в противном случае оставалось бы вне моей досягаемости.

В заключение, вот весь путь одним беглым взглядом от А до Я (точнее, от Р до С):

[1] Многие ассистенты, лидеры SPOT и однокурсники щедро делились своим временем и помогали своими отзывами, оставив свой след в моем понимании процесса PEDAC и решения проблем в целом. Мне повезло быть частью удивительно поддерживающего сообщества в Launch School. Спасибо всем вам.

[2] Благодарность за проблему принадлежит Уэйну Олсону, который заставил меня поработать над ней на одном из сеансов SPOT. Возвращение к проблеме в течение нескольких недель заставило меня усовершенствовать мой подход к PEDAC и более внимательно относиться к тому, как разбить проблему на более мелкие подзадачи, всегда помня о том, как части связаны с целым, и как целое относится к частям.