- 10 шагов по решению задач в программировании
- 1. Прочитайте условия задачи как минимум трижды (или хотя бы столько раз, сколько вам будет удобно)
- 2. Пройдите по задаче вручную как минимум с тремя наборами данных
- 3. Упрощайте и оптимизируйте свой алгоритм
- 4. Пишите псевдокод
- 5. Преобразуйте псевдокод в нормальный код и отладьте его
- 6. Упрощайте и оптимизируйте код
- 7. Отлаживайте
- 8. Пишите полезные комментарии
- 9. Получайте отзывы посредством ревизии кода
- 10. Практикуйтесь, практикуйтесь, практикуйтесь
- Динамическое программирование. Классические задачи
- Последовательности
- Двумерное динамическое программирование
- Задачи на подпоследовательности
10 шагов по решению задач в программировании
Это сборник советов для разработчиков-новичков, которые смотрят на пустой экран и не знают, с чего начать. Нередко можно услышать от молодых разработчиков, работающих над решением каких-то задач в программировании, что они не уверены, за что нужно хвататься. Ты понимаешь саму задачу, логику, основы синтаксиса и так далее. Если ты видишь чей-то код, или тебе кто-то помогает, то можно всё сделать самому. Но бывает, что ты не уверен в своих силах, или поначалу тебе трудно реализовать свои мысли в коде, несмотря на то, что ты знаешь синтаксис и логику. Под катом — несколько советов по решению этой проблемы, которые помогут вам в повседневной работе.
1. Прочитайте условия задачи как минимум трижды (или хотя бы столько раз, сколько вам будет удобно)
Вы не сможете решить задачу, если не поймёте её. Есть разница между задачей и задачей, которую, как вам кажется, вы решаете. Можно прочитать первые несколько строк, а насчёт остального выстроить предположения, поскольку всё выглядит похожим на то, с чем вы сталкивались раньше. Даже если вы делаете популярную игру наподобие Виселицы, удостоверьтесь, что прочитали все её правила, пусть даже вы играли в неё раньше.
Иногда можно попробовать объяснить задачу другу и посмотреть, поймёт ли он ваше объяснение. Вы же не хотите пройти половину пути и обнаружить, что неправильно поняли требования. Так что лучше потратить в начале больше времени, чтобы всё прояснить. Чем лучше поймёте задачу, тем легче будет её решить.
Допустим, мы создаём простую функцию selectEvenNumbers , которая берёт массив чисел и возвращает массив evenNumbers с одними лишь чётными числами. Если чётных чисел в исходном массиве нет, то массив evenNumbers возвращается пустым.
Какие вопросы можно себе задать:
- Как компьютер может сказать, что число является чётным? Разделить на 2 и проверить, чтобы результат получился целым.
- Что я передаю этой функции? Массив.
- Что содержит этот массив? Одно или несколько чисел.
- Какие типы данных у элементов массива? Числа.
- Какова цель этой функции? Что я возвращаю в конце её выполнения? Цель — получить все чётные числа и вернуть их в массиве. Если нет чётных чисел, то массив возвращается пустым.
2. Пройдите по задаче вручную как минимум с тремя наборами данных
Возьмите лист бумаги и пройдите по задаче вручную. Выберите не менее трёх наборов данных для проверки. Выберите предельно допустимые и крайние случаи.
Предельно допустимые случаи: проблема или ситуация, возникающая за пределами нормальных параметров функционирования. Например, когда одновременно несколько переменных или состояний среды имеют экстремальные значения, даже если каждый из параметров находится в своём специфическом диапазоне.
Крайние случаи: проблемы или ситуации, возникающие только при экстремальных (минимальных или максимальных) значениях параметров функционирования.
Вот, к примеру, несколько наборов данных для использования:
[1]
[1, 2]
[1, 2, 3, 4, 5, 6]
[-200.25]
[-800.1, 2000, 3.1, -1000.25, 42, 600]
Когда только начинаешь, то зачастую пренебрегаешь какими-то шагами. Поскольку наш мозг уже знаком с чётными числами, то можно просто посмотреть на набор чисел и сразу передать в массив 2, 4, 6 и так далее, не думая о том, как наш мозг выбирает конкретные числа. Если вы замечаете это за собой, то лучше взять большой набор данных, чтобы помешать мозгу решать задачу, просто глядя на числа. Это поможет придерживаться настоящего алгоритма.
Давайте пройдём по массиву [1]
- Смотрим на единственный элемент массива [1] .
- Определяем, является ли он чётным. Не является.
- Замечаем, что других элементов в массиве нет.
- Определяем, что здесь нет чётных чисел.
- Возвращаем пустой массив.
Теперь пройдём по массиву [1, 2]
- Смотрим на первый элемент массива [1, 2]
- Это 1 .
- Определяем, является ли он чётным. Не является.
- Смотрим на следующий элемент.
- Это 2 .
- Определяем, является ли он чётным. Является.
- Создаём массив evenNumbers и добавляем в него 2 .
- Замечаем, что других элементов в массиве нет.
- Возвращаем массив evenNumbers — [2] .
Можно пройти по задаче ещё несколько раз. Обратите внимание, что количество шагов в алгоритме для [1] отличается от алгоритма для [1, 2] . Поэтому рекомендуется проходить по нескольким наборам данных. Например, с единственным элементом; смесь целых и нецелых чисел; многоразрядные числа; наборы с отрицательными числами.
3. Упрощайте и оптимизируйте свой алгоритм
Поищите подходящие паттерны, может быть, что-то удастся обобщить. Подумайте, можно ли уменьшить количество шагов.
- Создадим функцию selectEvenNumbers .
- Создадим новый пустой массив evenNumbers для хранения чётных чисел.
- Проходим по каждому элементу массива [1, 2] .
- Находим первый элемент.
- Делим его на 2 и определяем, чётный ли он. Если да, то прибавляем к evenNumbers .
- Находим следующий элемент.
- Повторяем шаг №4.
- Повторяем шаги №5 и №4, пока не кончатся элементы в массиве.
- Возвращаем массив evenNumbers вне зависимости от того, есть ли в нём что-то.
Этот подход напоминает математическую индукцию:
- Доказываем истинность для n = 1 , n = 2 , .
- Предполагаем, что будет истинно для n = k .
- Доказываем истинность для n = k + 1 .
4. Пишите псевдокод
После проработки основных шагов напишите псевдокод, который можно перевести в настоящий код. Это поможет определить структуру кода, да и вообще облегчит его написание. Пишите псевдокод построчно. Это можно делать на бумаге или в виде комментариев в редакторе. Если вы только начинаете и считаете, что пустой экран выглядит жутковато или отвлекает, то лучше писать на бумаге.
В целом не существует правил для написания псевдокода, но можно включать синтаксис из вашего языка, если так будет удобнее. Но сосредотачивайтесь не на синтаксисе, а на логике и этапах алгоритма.
Применительно к нашему случаю есть много разных вариантов. Например, можно использовать filter , но ради простоты примера воспользуемся простым циклом for (однако при последующем рефакторинге мы ещё столкнёмся с filter ).
Вот пример псевдокода, в основном состоящего из слов:
А вот псевдокод, в котором слов гораздо меньше:
Главное, писать код построчно и понимать логику каждой строки.
Вернитесь к задаче, чтобы удостовериться, что вы на правильном пути.
5. Преобразуйте псевдокод в нормальный код и отладьте его
Когда ваш псевдокод будет готов, преобразуйте каждую строку в реальный код на вашем языке. Здесь мы воспользуемся JavaScript.
Если вы писали на бумаге, то перенесите всё в редактор в виде комментариев, а затем замените каждую строку.
Теперь вызовите функцию и дайте ей некоторые из ранее использованных наборов данных. Так можно проверить, возвращает ли код нужный результат. Также можете написать тесты для проверки соответствия выходных данных ожидаемому результату.
После каждой переменной или строки можно использовать console.log() . Это поможет проверить, ведут ли себя значения и код так, как ожидается, прежде чем двигаться дальше. Таким образом вы выловите любые проблемы, не зайдя слишком далеко. Вот пример того, какие значения можно проверить при начале работы.
Ниже приведён код, полученный после обработки каждой строки псевдокода. Символы // обозначают строки из псевдокода. Жирным выделен реальный код на JavaScript.
Уберём псевдокод, чтобы не путаться.
Иногда разработчики-новички настолько увлекаются синтаксисом, что им трудно идти дальше. Помните, что со временем вам будет проще соблюдать синтаксис, и нет ничего стыдного в том, чтобы потом при написании кода обращаться к справочным материалам для корректного соблюдения синтаксиса.
6. Упрощайте и оптимизируйте код
Возможно, вы заметили, что упрощение и оптимизация — это повторяющиеся темы.
Эдсгер Дейкстра, нидерландский учёный и один из первопроходцев в ряде областей информатики
В нашем примере одним из путей оптимизации будет фильтрация элементов в массиве посредством возвращения нового массива с помощью filter . В этом случае нам не нужно определять переменную evenNumbers , потому что filter вернёт новый массив с копиями элементов, которые соответствуют фильтру. При этом исходный массив не изменится. Также нам не нужно использовать цикл for . filter пройдёт по каждому элементу, и если вернёт true, то элемент попадёт в массив, а если false , то будет пропущен.
Может потребоваться несколько итераций упрощения и оптимизации кода, по мере того как вы будете находить новые способы.
Задавайте себе такие вопросы:
- Каковы цели упрощения и оптимизации? Цели зависят от принятого в вашей команде стиля или ваших личных предпочтений. Вы пытаетесь максимально уплотнить код? Или вы хотите сделать его более читабельным? В таком случае вы можете добавить отдельные строки с определением переменной или вычислением чего-либо, а не пытаться всё сделать в одной строке.
- Как ещё можно сделать код более читабельным?
- Можно ли ещё уменьшить количество шагов?
- Есть ли переменные или функции, которые не нужны или не используются?
- Какие-то шаги повторяются? Посмотрите, можно ли определить в другой функции.
- Существуют ли более эффективные способы обработки крайних случаев?
«Программы должны быть написаны так, чтобы люди их читали, и лишь во вторую очередь — чтобы машины их исполняли».
Джеральд Сассман и Гарольд Абельсон, авторы “Structure and Interpretation of Computer Programs”
7. Отлаживайте
Этот шаг необходимо выполнять в ходе всего процесса. Сквозная отладка поможет раньше выловить любые ошибки синтаксиса или недостатки логики. Воспользуйтесь преимуществами своего IDE (Integrated Development Environment) и отладчика. При обнаружении бага рекомендуется просматривать код построчно, стараясь найти неожиданные вещи. Несколько советов:
- Читайте, что пишется в сообщения об ошибках в консоли. Иногда в них указываются номера строк для проверки. Это даст вам исходную точку для поисков, хотя иногда проблемы могут скрываться в других строках.
- Комментируйте блоки кода или строки, а также выходные данные, чтобы быстро подмечать поведение кода. При необходимости код можно всегда раскомментировать.
- Используйте другие образцы данных, если возникают непредусмотренные сценарии.
- Сохраняйте разные версии файла, если применяете другие подходы. Не стоит терять наработанное, если захочется откатиться к прежнему решению!
«Самый эффективный инструмент отладки — тщательное продумывание в сочетании с разумно размещёнными командами вывода на экран».
Брайан Керниган, профессор информатики в Принстонском университете
8. Пишите полезные комментарии
Через месяц вы можете и не вспомнить, что означает каждая строка кода. А тот, кто будет работать с вашим кодом, вообще этого не знает. Поэтому важно писать полезные комментарии, чтобы избежать проблем и сэкономить впоследствии время, когда придётся снова вернуться к этому коду.
Избегайте таких комментариев:
// Это массив. Итерируем его.
// Это переменная.
Старайтесь писать короткие, высокоуровневые комментарии, которые помогут понять, что тут происходит, если это не очевидно. Это пригодится при решении сложных задач, вы сможете быстро понять, что делает конкретная функция, и почему. Благодаря использованию понятных комментариев и имён переменных и функций вы (и другие люди) сможете понять:
9. Получайте отзывы посредством ревизии кода
Получайте отзывы от коллег, руководителей и других разработчиков. Читайте Stack Overflow. Смотрите, как другие решали аналогичные задачи и учитесь у них. Нередко бывает несколько способов решения задачи. Узнайте, что они собой представляют, и вам будет быстрее и проще приходить к ним самостоятельно.
«Неважно, насколько медленно вы пишете чистый код, вы всегда будете тратить больше времени, если пишете грязный код».
Дядя Боб Мартин, программный инженер и соавтор манифеста Agile
10. Практикуйтесь, практикуйтесь, практикуйтесь
Даже опытные разработчики постоянно практикуются и учатся. Если вы получили полезный отклик, внедрите его. Снова решите задачу, или аналогичные задачи. Заставляйте себя. С каждой решённой задачей вы становитесь лучше как разработчик. Радуйтесь каждому успеху и не забывайте, как много вы уже прошли. Помните, что программирование, как и любая деятельность, со временем будет даваться всё проще и легче.
«Гордитесь тем, сколько вы прошли. Верьте в то, что пройдёте ещё больше. Но не забывайте наслаждаться путешествием».
Источник
Динамическое программирование. Классические задачи
Здравствуй, Хабрахабр. В настоящий момент я работаю над учебным пособием по олимпиадному программированию, один из параграфов которого посвящен динамическому программированию. Ниже приведена выдержка из данного параграфа. Пытаясь объяснить данную тему как можно проще, я постарался сложные моменты сопроводить иллюстрациями. Мне интересно ваше мнение о том, насколько понятным получился данный материал. Также буду рад советам, какие еще задачи стоит включить в данный раздел.
Во многих олимпиадных задачах по программированию решение с помощью рекурсии или полного перебора требует выполнения очень большого числа операций. Попытка решить такие задачи, например, полным перебором, приводит к превышению времени выполнения.
Однако среди переборных и некоторых других задач можно выделить класс задач, обладающих одним хорошим свойством: имея решения некоторых подзадач (например, для меньшего числа 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
Источник