Динамическое программирование. Введение.
от ДубоХирург
Сегодня я хочу вам рассказать об идее(вернее, группе идей), весьма широко использующейся в программировании. Имя ей - динамическое программирование.
Что вообще представляет из себя эта идея? По википедии - это
способ решения сложных задач путём разбиения их на более простые подзадачи. Или
Динамическое программирование - это когда у нас
есть одна большая задача, которую непонятно как решать,
и мы разбиваем ее на меньшие задачи, которые тоже
непонятно как решать.
Рассмотрим эту идею на примере. Решим задачу:
Назовём число интересным, если цифры в нём идут в порядке неубывания. Так, числа 123 или 448899 являются интересными, а число 123452 - нет. Посчитайте количество интересных чисел на отрезке
Попытка №1 Первое, что приходит в голову - давайте переберём все числа данного промежутка и проверим каждое на интересность. OK, пишем код:
Однако, сколько будет работать такое решение? Проще всего это подсчитать для границ 1 и 10^k. У нас получится:
Не очень-то быстро, не правда ли? Уже для k = 25 последнее слагаемое равно 25*9*10^24, т. е. подсчёт только чисел длины k будет производиться 25*9*10^13 секунд или 71347031 лет. Хотя казалось бы, 10^25 - не такое уж большое число. Окей, нам нужно решение быстрей.
Попытка №2 На минуту забудем про остальные варианты и будем считать что , а . Давайте насчитаем такую табличку: , где будем хранить количество чисел длины j, начинающихся на i. Как её заполнять? Очевидно, , ведь есть ровно одно число, состоящее из одной цифры и начинающееся с неё - сама эта цифра.
С базой разобрались, но как перейти от j к j+1? К каким числам мы можем приписать спереди цифру i так, чтобы они остались интересными? Конечно же тем, которые начинаются с цифр, не меньших, чем i. Таким образом, получаем, что
Заметим, что для получения ответа нам достаточно просуммировать , где j пробегает значения от 1 до (k+1).
Код решения:
Посчитаем сложность этого решения. Легко видеть, она равна 10*(k+1).
Уже лучше, но мы всё ещё не умеем находить ответ для любых l и r. Поэтому
Попытка №3 и полное решение К сожалению, такое пока писать не научился, поэтому кода тут не будет. Суть идеи останется прежней, только теперь мы будем перебирать длину общего префикса(первые цифры) для интересного числа и r и смотреть, сколько чисел с таким префиксом таких, что следующая цифра строго меньше соответствующей цифры из r. Пересчитывается таблица так же, меняются только начальные значения.
Теперь, когда мы умеем для любого n находить количество интересных чисел от 1 до n, понятно, как найти ответ для отрезка : посчитаем ответ для r и для (l-1). Разница и будет искомым ответом.
В заключение В этой статье я попытался дать общее представление об идее под названием динамическое программирование и показать его использование на сферическом примере в вакууме. В следующий раз попробую дать классификацию методов ДП и более практические задачи с его использованием.
Что вообще представляет из себя эта идея? По википедии - это
способ решения сложных задач путём разбиения их на более простые подзадачи. Или
Динамическое программирование - это когда у нас
есть одна большая задача, которую непонятно как решать,
и мы разбиваем ее на меньшие задачи, которые тоже
непонятно как решать.
Рассмотрим эту идею на примере. Решим задачу:
Назовём число интересным, если цифры в нём идут в порядке неубывания. Так, числа 123 или 448899 являются интересными, а число 123452 - нет. Посчитайте количество интересных чисел на отрезке
Попытка №1 Первое, что приходит в голову - давайте переберём все числа данного промежутка и проверим каждое на интересность. OK, пишем код:
- long long countinteresting(long long l, long long r)
- {
- long long cnt = 0;
- for(long long currnumber = l; currnumber <= r; ++currnumber)
- {
- bool isnotdescending = true;
- int prevdigit = 0, currdigit = 0;
- long long tmp = currnumber;
- long long power = 1; // Наибольшая степень десятки, не большая нашего числа
- while(power<=tmp) power *= 10;
- power /= 10;
- while(power&&isnotdescending)
- {
- prevdigit = currdigit;
- currdigit = tmp/power;
- if(currdigit<prevdigit) isnotdescending = false;
- else
- {
- tmp %= power;
- power /= 10;
- }
- }
- if(isnotdescending) ++cnt;
- }
- return cnt;
- }
Однако, сколько будет работать такое решение? Проще всего это подсчитать для границ 1 и 10^k. У нас получится:
Не очень-то быстро, не правда ли? Уже для k = 25 последнее слагаемое равно 25*9*10^24, т. е. подсчёт только чисел длины k будет производиться 25*9*10^13 секунд или 71347031 лет. Хотя казалось бы, 10^25 - не такое уж большое число. Окей, нам нужно решение быстрей.
Попытка №2 На минуту забудем про остальные варианты и будем считать что , а . Давайте насчитаем такую табличку: , где будем хранить количество чисел длины j, начинающихся на i. Как её заполнять? Очевидно, , ведь есть ровно одно число, состоящее из одной цифры и начинающееся с неё - сама эта цифра.
С базой разобрались, но как перейти от j к j+1? К каким числам мы можем приписать спереди цифру i так, чтобы они остались интересными? Конечно же тем, которые начинаются с цифр, не меньших, чем i. Таким образом, получаем, что
Заметим, что для получения ответа нам достаточно просуммировать , где j пробегает значения от 1 до (k+1).
Код решения:
- long long countinteresting(long long length)
- {
- vector<vector<int>> table(length+1, vector<int>(10, 1));
- long long answer = 0;
- for(long long j = 1; j<=length; ++j)
- {
- long long sum = 0;
- for(int i = 9; i>=1; --i)
- {
- sum += table[j-1][i];
- table[j][i] = sum;
- }
- answer += table[j][1];
- }
- return answer;
- }
Уже лучше, но мы всё ещё не умеем находить ответ для любых l и r. Поэтому
Попытка №3 и полное решение К сожалению, такое пока писать не научился, поэтому кода тут не будет. Суть идеи останется прежней, только теперь мы будем перебирать длину общего префикса(первые цифры) для интересного числа и r и смотреть, сколько чисел с таким префиксом таких, что следующая цифра строго меньше соответствующей цифры из r. Пересчитывается таблица так же, меняются только начальные значения.
Теперь, когда мы умеем для любого n находить количество интересных чисел от 1 до n, понятно, как найти ответ для отрезка : посчитаем ответ для r и для (l-1). Разница и будет искомым ответом.
В заключение В этой статье я попытался дать общее представление об идее под названием динамическое программирование и показать его использование на сферическом примере в вакууме. В следующий раз попробую дать классификацию методов ДП и более практические задачи с его использованием.