- Как решать задачи на программирование
- Универсальный подход к решению задач
- Шаг 1: понять задачу
- Каковы здесь входящие данные?
- Каковы должны быть результаты?
- Придумайте простые примеры
- Придумайте сложные примеры
- Шаг 2: разработать план решения задачи
- 3. Шаг 3: осуществить план (решить задачу!)
- Шаг 4: оглянуться назад
- Итоги
- Динамическое программирование. Классические задачи
- Последовательности
- Двумерное динамическое программирование
- Задачи на подпоследовательности
Как решать задачи на программирование
Перевод статьи «How to Solve Coding Problems with a Simple Four Step Method».
Я потратила два месяца на подготовку к своему первому техническому собеседованию и думала, что уже готова. Но когда подошла к нему уже вплотную, внезапно меня озарило: я ж понятия не имею, как решать задачи на программирование.
Ни в одном туториале из тех, по которым я училась программированию, не рассказывалось о решении задач. А мне определенно нужно было овладеть этим искусством: от этого зависела вся моя будущая карьера разработчика. И я немедленно взялась за поиски.
То, что я нашла, было не просто подходом, а бесценной стратегией. Это был проверенный временем алгоритм действий в четыре шага, который по какой-то причине остался незамеченным в сфере разработки.
В этой статье я подробно расскажу об этой стратегии, чтобы и вы могли начать уверенно подходить к решению задач на программирование.
Решение задач — это не только часть процедуры собеседований. Это то, чем разработчики занимаются ежедневно. В конечном итоге, само написание кода — это и есть решение задач.
Универсальный подход к решению задач
Этот метод изложен в книге «Как решать задачу» Дьёрдя Пойа. Первое издание вышло еще в 1945 году, было продано больше миллиона экземпляров. (На русском языке книга публиковалась как пособие для учителей еще в 1959 году. — Прим. перев.).
Метод Пойа используют многие программисты, от профессоров информатики (см. курс «Intro to CS» на Udacity, который ведет профессор Дэвид Эванс) до преподавателей современной веб-разработки вроде Кольта Стила.
Давайте пройдемся по решению простой задачи на программирование с применением метода Пойа. Это позволит увидеть работу метода на практике и в результате лучше разобраться в нем. Для примера будем использовать язык JavaScript.
Создайте функцию, которая будет складывать два числа и возвращать их сумму.
Вот четыре шага по решению любой задачи:
- Понять задачу.
- Разработать план.
- Осуществить план.
- Оглянуться назад.
«Во-первых, мы должны понять задачу; мы должны ясно видеть, что в ней является искомым. Во-вторых, мы должны усмотреть, как связаны друг с другом различные элементы задачи, как неизвестное связано с данными. Это необходимо, чтобы получить представление о решении, чтобы составить план. В-третьих, мы осуществляем наш план. В-четвертых, оглядываясь назад на полученное решение, мы вновь изучаем и анализируем его», — «Как решать задачу», 1959 г.
Давайте сделаем первый шаг.
Шаг 1: понять задачу
Когда вы на собеседовании получаете задачу на программирование, возникает соблазн тут же приступить к написанию кода. Этому искушению трудно противостоять, особенно, если время ограничено.
Тем не менее, постарайтесь удержаться. Прежде чем приступать к решению задачи, убедитесь, что вы хорошо поняли, что от вас требуется.
Прочтите текст задачи. Можно даже читать вслух, если это поможет вам притормозить.
Прочитав текст задачи, выясните все моменты, которые вам не понятны. Если дело происходит на собеседовании, можно задать уточняющие вопросы интервьюеру. Если вы решаете задачу в гордом одиночестве, обдумайте непонятные части или даже поищите в Google ответы на возникшие вопросы.
Этот первый шаг жизненно важен. Мы часто не уделяем достаточного количества времени на то, чтобы разобраться в постановке задачи. А если вы не до конца понимаете задачу, вам будет куда труднее ее решить.
Чтобы помочь себе разобраться, спросите себя:
Каковы здесь входящие данные?
Какого рода input-ы следует ожидать? В нашем примере это аргументы, принимаемые функцией.
Читая текст задачи, мы понимаем, что входящими данными будут числа. Если подходить к делу более скрупулезно, мы можем спросить:
- всегда ли будет только два числа?
- что будет, если функция получит в качестве входящих данных три числа?
Эти вопросы мы можем задать интервьюеру или поискать ответ в описании задачи. (К задаче может прилагаться примечание типа «Исходите из того, что в функцию всегда будут передаваться только два числа». Конечно, это сразу все прояснит).
Можно задаться и другими вопросами. Всегда ли входящими данными будут числа? Что должна делать функция, если получит в качестве аргументов «a» и «b»? Уточните, всегда ли input будет числовым.
В качестве варианта — можно записать вероятные входящие данные в комментарии к коду, чтобы иметь представление о том, как они должны выглядеть.
Далее следует спросить себя:
Каковы должны быть результаты?
Что функция будет возвращать? В нашем случае это должно быть оно число — сумма двух чисел, переданных в качестве аргументов. Убедитесь, что вы хорошо понимаете, каким должен быть output.
Придумайте простые примеры
Разобравшись в сути задачи и зная вероятные input-ы и output-ы, можно начать работать над конкретными примерами.
Примеры также могут использоваться для тестирования вашего решения. На техническом собеседовании или при подготовке к нему на сайтах вроде Codewars или HackerRank используются специальные редакторы. Большинство из них содержат уже готовые примеры или test cases. Несмотря на это, написание собственных примеров может помочь вам упрочить понимание задачи.
Начните с написания одного-двух простых примеров.
Давайте вернемся к нашей складывающей функции. Назовем ее «add».
Каким может быть input? Ну, допустим, таким:
Каким будет результат при таких входящих данных? Записать это можно так:
Это показывает, что наша функция принимает в качестве input 2 и 3, а как output возвращает 5.
Придумайте сложные примеры
Продумывая более сложные примеры, вы можете уделить время поиску крайних случаев, которые стоит учесть.
Например, что будет, если наши входящие данные будут не числами, а строками? Что, если мы получим в качестве аргументов две строки, например, add(‘a’, ‘b’)?
Ваш интервьюер может сказать вам вернуть сообщение об ошибке, если входящих данных не будет или если они будут не того типа. В таком случае вы можете добавить соответствующий комментарий — в качестве напоминания себе.
Как вариант, интервьюер может сказать, что входящие данные всегда будут числами. В таком случае вам не понадобится писать никакой дополнительный код для обработки этого крайнего случая.
Если вы не на собеседовании, а просто решаете задачу, в ней может быть обозначено, что должно происходить при вводе невалидных данных.
Например, в задаче может говориться «При отсутствии input-а верните undefined». В таком случае можно написать комментарий:
В нашем случае мы будем исходить из того, что input всегда будет числовым. Но в общем всегда лучше подумать о крайних случаях.
Профессор информатики Эванс советует писать т. н. безопасный код. Всегда думайте о том, что может пойти не по плану и как защитить свой код от возможных ошибок.
Прежде чем перейти ко второму шагу, давайте кратко повторим, что нужно сделать на первом шаге:
- прочитать задачу
- выяснить, какими будут входящие данные
- выяснить, каким должен быть результат
- создать простые примеры, затем — более сложные.
Шаг 2: разработать план решения задачи
Далее нужно составить план того, как вы, собственно, собираетесь решать задачу. Придумав план, запишите его в виде псевдокода.
Псевдокод — это изложенные простым языком шаги алгоритма. Иными словами, это пошаговый план решения задачи.
Опишите каждый этап решения. Если задача сложная, этапов может быть много. Если говорить о нашей задаче, мы можем написать:
Теперь у вас есть четырехэтапный план решения задачи.
Для более сложных задач профессор Эванс советует: «Полагайтесь на то, как задачи решаются людьми». То есть, забудьте на мгновение о том, как задачу может решить код, и подумайте о том, как ее решали бы вы — человек. Это может помочь вам более четко увидеть нужные шаги.
3. Шаг 3: осуществить план (решить задачу!)
Следующий шаг — собственно решение задачи. Руководствуясь псевдокодом, напишите настоящий код.
Профессор Эванс рекомендует сфокусироваться на простом, механическом решении. Чем проще и легче ваше решение, тем вероятнее, что вы напишете код правильно.
Если взять наш псевдокод, мы можем написать следующее:
Помните, что не следует преждевременно оптимизировать код. Написав решение, вы можете подумать, что ваш код неэффективный, и захотеть его улучшить. Но для начала создайте простое, механическое решение.
Что, если вы не можете решить задачу полностью? Например, если вы так и не нашли решения для какой-то ее части? Кольт Стил советует в таких ситуациях игнорировать эту сложную часть. Сконцентрируйтесь на всем остальном — том, что вы уже можете написать. Когда напишете, возвращайтесь к части, которая вызвала у вас затруднения.
Шаг 4: оглянуться назад
Когда у вас на руках будет готовое рабочее решение, подумайте, как его можно улучшить. На этом этапе можно провести рефакторинг и преобразовать свое решение в более эффективное.
Просматривая свою работу, вы можете задать себе несколько вопросов:
- Можно ли получить этот результат как-то иначе? Какие еще подходы есть?
- Понятно ли это решение с первого взгляда?
- Можно ли использовать результат или метод для какой-то другой задачи?
- Можно ли улучшить производительность решения?
- Не приходят ли вам на ум какие-то способы рефакторинга для этого решения?
- Как эту задачу решают другие люди?
Возвращаясь к нашему коду, мы можем сделать его более лаконичным, если удалим нашу переменную и используем имплицитный return:
Дойдя до четвертого шага, вы понимаете, что можете никогда не довести свое решение до совершенства. Даже великие разработчики пишут код, который впоследствии хотят изменить. Так что вопросы из приведенного списка — лишь направляющие, которые могут вам помочь.
Если вы решаете задачу на собеседовании, то для четвертого шага у вас может просто не остаться времени. Впрочем, если время есть, попробуйте улучшить свое решение. Ну а если вы просто решаете какую-нибудь задачу, обязательно проходите все описанные шаги.
Когда я практиковалась в решении задач, я практически всегда смотрела и чужие решения, которые часто были элегантнее и эффективнее моих.
Итоги
В этой статье мы рассмотрели четырехэтапную стратегию решения задач на программирование. Давайте повторим:
- Шаг 1: понять задачу
- Шаг2: создать пошаговый план решения
- Шаг 3: реализовать план и написать код решения
- Шаг 4: оглянуться назад и по возможности улучшить решение.
Применение этого подхода очень помогло мне при прохождении технических собеседований, да и вообще в моей работе.
Если вы не чувствуете себя уверенно, решая задачи, помните, что умение решать задачи — это навык. Чем больше практики, тем лучше вам это будет удаваться.
Источник
Динамическое программирование. Классические задачи
Здравствуй, Хабрахабр. В настоящий момент я работаю над учебным пособием по олимпиадному программированию, один из параграфов которого посвящен динамическому программированию. Ниже приведена выдержка из данного параграфа. Пытаясь объяснить данную тему как можно проще, я постарался сложные моменты сопроводить иллюстрациями. Мне интересно ваше мнение о том, насколько понятным получился данный материал. Также буду рад советам, какие еще задачи стоит включить в данный раздел.
Во многих олимпиадных задачах по программированию решение с помощью рекурсии или полного перебора требует выполнения очень большого числа операций. Попытка решить такие задачи, например, полным перебором, приводит к превышению времени выполнения.
Однако среди переборных и некоторых других задач можно выделить класс задач, обладающих одним хорошим свойством: имея решения некоторых подзадач (например, для меньшего числа n), можно практически без перебора найти решение исходной задачи.
Такие задачи решают методом динамического программирования, а под самим динамическим программированием понимают сведение задачи к подзадачам.
Последовательности
Классической задачей на последовательности является следующая.
Один из способов решения, который может показаться логичным и эффективным, — решение с помощью рекурсии:
Используя такую функцию, мы будем решать задачу «с конца» — будем шаг за шагом уменьшать n, пока не дойдем до известных значений.
Но как можно заметить, такая, казалось бы, простая программа уже при n = 40 работает заметно долго. Это связано с тем, что одни и те же промежуточные данные вычисляются по несколько раз — число операций нарастает с той же скоростью, с какой растут числа Фибоначчи — экспоненциально.
Один из выходов из данной ситуации — сохранение уже найденных промежуточных результатов с целью их повторного использования:
Приведенное решение является корректным и эффективным. Но для данной задачи применимо и более простое решение:
Такое решение можно назвать решением «с начала» — мы первым делом заполняем известные значения, затем находим первое неизвестное значение (F3), потом следующее и т.д., пока не дойдем до нужного.
Именно такое решение и является классическим для динамического программирования: мы сначала решили все подзадачи (нашли все Fi для i 2, соответственно.
Следующая задача одномерного динамического программирования встречается в различных вариациях.
Задача 1. Посчитать число последовательностей нулей и единиц длины n, в которых не встречаются две идущие подряд единицы.
При n 2. То есть данная задача фактически сводится к нахождению чисел Фибоначчи.
Двумерное динамическое программирование
Классической задачей двумерного динамического программирования является задача о маршрутах на прямоугольном поле.
В разных формулировках необходимо посчитать число маршрутов или найти маршрут, который является лучшим в некотором смысле.
Приведем пару формулировок таких задач:
Задача 2. Дано прямоугольное поле размером n*m клеток. Можно совершать шаги длиной в одну клетку вправо или вниз. Посчитать, сколькими способами можно попасть из левой верхней клетки в правую нижнюю.
Задача 3. Дано прямоугольное поле размером n*m клеток. Можно совершать шаги длиной в одну клетку вправо, вниз или по диагонали вправо-вниз. В каждой клетке записано некоторое натуральное число. Необходимо попасть из верхней левой клетки в правую нижнюю. Вес маршрута вычисляется как сумма чисел со всех посещенных клеток. Необходимо найти маршрут с минимальным весом.
Для всех таких задач характерным является то, что каждый отдельный маршрут не может пройти два или более раз по одной и той же клетке.
Рассмотрим более подробно задачу 2. В некоторую клетку с координатами (i,j) можно прийти только сверху или слева, то есть из клеток с координатами (i – 1, j) и (i, j – 1):
Таким образом, для клетки (i, j) число маршрутов A[i][j] будет равно
A[i – 1][j] + A[i][j – 1], то есть задача сводится к двум подзадачам. В данной реализации используется два параметра — i и j — поэтому применительно к данной задаче мы говорим о двумерном динамическом программировании.
Теперь мы можем пройти последовательно по строкам (или по столбцам) массива A, находя число маршрутов для текущей клетки по приведенной выше формуле. Предварительно в A[0][0] необходимо поместить число 1.
В задаче 3 в клетку с координатами (i, j) мы можем попасть из клеток с координатами
(i – 1, j), (i, j – 1) и (i – 1, j – 1). Допустим, что для каждой из этих трех клеток мы уже нашли маршрут минимального веса, а сами веса поместили в W[i – 1][j], W[i][j – 1],
W[i – 1][j – 1]. Чтобы найти минимальный вес для (i, j), необходимо выбрать минимальный из весов W[i – 1][j], W[i][j – 1], W[i – 1][j – 1] и прибавить к нему число, записанное в текущей клетке:
W[i][j] = min(W[i–1][j], W[i][j – 1], W[i – 1][j – 1]) + A[i][j];
Данная задача осложнена тем, что необходимо найти не только минимальный вес, но и сам маршрут. Поэтому в другой массив мы дополнительно для каждой клетки будем записывать, с какой стороны в нее надо попасть.
На следующем рисунке приведен пример исходных данных и одного из шагов алгоритма.
В каждую из уже пройденных клеток ведет ровно одна стрелка. Эта стрелка показывает, с какой стороны необходимо прийти в эту клетку, чтобы получить минимальный вес, записанный в клетке.
После прохождения всего массива необходимо будет проследить сам маршрут из последней клетки, следуя по стрелкам в обратную сторону.
Задачи на подпоследовательности
Рассмотрим задачу о возрастающей подпоследовательности.
Задача 4. Дана последовательность целых чисел. Необходимо найти ее самую длинную строго возрастающую подпоследовательность.
Начнем решать задачу с начала — будем искать ответ, начиная с первых членов данной последовательности. Для каждого номера i будем искать наибольшую возрастающую подпоследовательность, оканчивающуюся элементом в позиции i. Пусть исходная последовательность хранится в массиве A. В массиве L будем записывать длины максимальных подпоследовательностей, оканчивающихся текущим элементом. Пусть мы нашли все L[i] для 1
Источник