Полный переборЗачастую, когда говорят о качестве решения некоторой задачи, для того что бы определить наихудший вариант, приводят пример "полного перебора". Сколько раз я уже слышал от разных людей: "если задача решена не простым перебором, то это уже хорошо"... На самом деле, если человек никогда сам полный перебор ни разу не организовывал, то, скорее всего, эта задача вызовет у него некоторые сложности. Связано это просто с отсутствием понимания того, как перебор надо устраивать, что для этого надо хранить в памяти и как этим пользоваться. И вообще, есть ситуации, когда перебор --- единственный способ найти решение; самым сложным тогда становится ограничить область перебора. Вообще говоря, перебор имеет смысл в том случае, когда каким-то образом ограничено множество возможных решений задачи и, кроме того, это множество является счетным. Тогда, в принципе, можно взять каждое потенциальное решение из этого множества и просто проверить его, подставив в условие задачи. Ключевыми словами в предыдущем абзаце является счетность и конечность. В этом случае, все элементы множества потенциальных решений можно будет просто пронумеровать от 0 до некоторого N (вполне определенное число для конкретной задачи) и в цикле от нуля до N произвести подстановку вариантов решения в условие. На самом деле, лучше выделить переход от известного элемента этого множества к следующему, использование которого позволит более ясным способом (без приписывания номеров элементам множества) произвести перебор. Что бы не быть голословным, приведу пример обычной задачи на перебор. Задача предлагалась на олимпиаде ВМК в 1998 году (задача C, "парламент"):
Полный текст задачи (основную его часть я привел выше) можно посмотреть здесь. Таким образом, надо рассчитать, кому из депутатов надо дать некоторое количество "тугриков"; кроме того, решение должно быть оптимальным в некотором смысле (описанном в третьем абзаце условия задачи). Вообще, оптимальность решения зачастую указывает именно на то, что потребуется перебор (хотя есть теория оптимизации, которая занимается поиском методов решения подобных задач без перебора, существует множество задач, для которых пока что нет иных способов точного (!) решения). Решением задачи, по сути, будет являться набор индексов депутатов, которым надо будет заплатить. Для каждого из депутатов есть только два варианта: либо ему платят, либо нет; соответственно множество возможных решений есть набор упорядоченных N-ок, на i-ой позиции в которых находится флаг "платить/не платить" для i-го депутата. Нетрудно видеть, что эти "упорядоченные N-ки" являются N-разрядными числами, представленными в двоичной системе счисления. Соответственно, множество возможных решений будет содержать в себе все возможные варианты таких чисел, т.е. элементом этого множества будет являться число от 000..02 до 111..12. Соответственно, очень просто сделать перебор элементов множества в естественном порядке "по возрастанию", прибавляя 1 к текущему числу, начиная с 0 и кончая 2N-1. Выглядит это примерно следующим образом:
struct deputat
{
float p0;
float p1;
int K;
};
deputat* d;
bool* state;
int N, X, M;
В переменных double Q; int Td, Te, Tp; Эти переменные имеют смысл в рассматриваемой задаче и в них будут находится текущие "лучшие" значения требуемых параметров. Теперь опишем подпрограмму, которая будет по текущему элементу перебираемого множества строить следующий элемент.
bool next()
{
int i = N-1;
for(; i >= 0; i--)
if(state[i])
state[i] = false;
else
{
state[i] = true;
break;
}
return(i < 0 && !state[0]);
}
Подпрограмма очень простая и всего навсего реализует
прибавление 1 к числу, записанному в двоичной системе счисления.
Возвращаемое значение --- Кроме того, понадобится подпрограмма "проверки качества" текущего решения (по отношению к уже отобранному).
void check()
{
double Q = 0.;
int Td = 0, Te, Tp;
/*
* Вычисляем значения локальных
* переменных Q, Td, Te и Tp исходя
* из state.
*/
// ...
/*
* Если значения локальных переменных
* Q, Td, Te и Tp "лучше", чем значения
* глобальных переменных ::Q, ::Td, ::Te
* и ::Tp, то заменяем значения глобальных
* переменных локальными.
*/
if( ... )
{
::Te = Te;
::Td = Td;
::Tp = Tp;
::Q = Q;
}
}
Сами вычисления нас сейчас не особенно интересуют, потому что это --- ненужные тонкости. Дополняет общую картину еще один цикл, который обе эти функции связывает воедино:
for(;;)
{
check();
if(next()) break;
}
Собственно, это все: после завершения работы этого цикла в глобальных
переменных Теперь уж о совсем грустном... если для некоторого N работа программы, построенной при помощи полного перебора еще хоть как-то удовлетворяет, то для N+1, скорее всего, скорость работы перестанет быть удовлетворительной. Это логично: потому что при включении еще одного депутата в парламент, количество вычислений удвоится! Что терпимо для относительно маленьких N, но совершенно не подходит для больших N. Проблема в данном случае состоит еще и в том, что ее нельзя решить простым наращиванием ресурсов компьютера (купив более быстрый процессор, или увеличив количество оперативной памяти), потому что скорости компьютера увеличиваются линейно, а количество вычислений при помощи предложенного алгоритма --- по экспоненте. О том, как практически попытаться улучшить полный перебор, я расскажу позже, а пока что намечу основной путь решения. Понятно, что если "красивого" решения у задачи не существует, то ничем принципиальным перебор ускорить будет нельзя. Тем не менее, если вы внимательно посмотрите на предложенную схему его реализации, то заметите одну достаточно забавную вещь: если для некоторого подмножества депутатов улучшить значения глобальных переменных, отвечающих за "качество" решения нельзя, т.е. если эти депутаты будут находится в любом другом множестве "оплаченных" депутатов, то решение задачи заведомо будет хуже уже найденного, следовательно все решения задачи, которые включают этот набор депутатов, можно даже не проверять. В реализованной схеме решения задачи достаточно сложно учитывать подобные моменты, но тем не менее, возможно применить подобные размышления... например, просто убрав из исходного множества всех депутатов, у которых значение Ki больше всего имеющегося в наличии у президента "тугриков". Это очень просто сделать, но каждый такой депутат уменьшит количество вычислений по отношению к исходной задаче вдвое! Тем самым, мы подошли к методу перебора, который называется "методом ветвей и границ". Он не может дать качественного изменения поведения перебора для всех возможных начальных данных, но в целом очень сильно ускоряет процесс. Еще один недостаток описанного подхода заключается в том, что значения переменных постоянно пересчитываются заново при переходе от одного числа к другому. Вообще говоря, если сменить метод перехода от элемента к элементу, можно в некоторых случаях использовать значения, посчитанные на предыдущем шаге (просто прибавляя или вычитая из них некоторое значение). Кроме того, можно хранить вообще все расчеты, используя их при последующих итерациях (это будет полезно сделать, если вы сможете организовать удобный и быстрый способ доступа к ним впоследствии) --- такое улучшение описанного решения приведет к решению задачи другим методом, называемом "динамическим программированием". Такой алгоритм ускорит работу программы за счет больших требований к оперативной памяти. Более подробно эти методы я рассмотрю позже, а если кто-то заинтересовался, то ссылки на книги можно найти внизу этой заметки. РезюмеТо, что полный перебор неэффективен, еще не значит, что его просто использовать в своих программах. Зачастую мешается много деталей, которые программисты упускают из вида при начале программирования. Кроме того, надо знать основные способы модификации полного перебора, при помощи которых можно несколько ускорить работу программы. PSЗадача решена методом полного перебора для того, что бы продемонстрировать этот метод. В данном случае полный перебор не является самым эффективным решением задачи.
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
© 2000-2008, Andrey L. Kalinin mailto:andrey@kalinin.ru |
|