Алгоритм имитации отжига (англ. simulated annealing) — эвристический алгоритм глобальной оптимизации, особенно эффективный при решении дискретных и комбинаторных задач.
Алгоритм вдохновлён процессом отжига в металлургии — техники, заключающейся в нагревании и контролируемом охлаждении металла, чтобы увеличить его кристаллизованность и уменьшить дефекты. Симулированние отжига в переборных задачах может быть использовано для приближённого нахождения глобального минимума функций с большим количеством свободных переменных.
Алгоритм вероятностный и не даёт почти никаких гарантий сходимости, однако хорошо работает на практике при решении NP-полных задач. Иногда на контестах им удаётся сдать сложные комбинаторные задачи, у которых есть нормальное решение: Ильдар Гайнуллин сдает отжигом div2E на динамику по подмножествам.
Для примера будем рассматривать задачу коммивояжёра:
Eсть
городов, соединённых между собой дорогами. Необходимо проложить между ними кратчайший замкнутый маршрут, проходящий через каждый город только один раз.
Пусть имеется некоторая функция
Возьмём в качестве базового решения какое-то состояние
Введём температуру
Пока не придём к оптимальному решению или пока не закончится время, будем повторять следующие шаги:
Уменьшим температуру
Выберем случайного соседа
С вероятностью
В каждом шаге есть много свободы при реализации. Основные эвристические соображения следующие:
В начале оптимизации наше решение и так плохое, и мы можем позволить себе высокую температуру и риск перейти в состояние хуже. В конце наоборот — наше решение почти оптимальное, и мы не хотим терять прогресс. Температура должна быть высокой в начале и медленно уменьшаться к концу.
Алгоритм будет работать лучше, если функция
Вероятность должна быть меньше, если новое состояние хуже, чем старое. Также вероятность должна быть больше при высокой температуре.
Например, можно действовать так:
В случае с перестановками этим минимальным изменением может быть, например, своп двух случайных элементов.
Если
Вообще, в выборе конкретных эвристик не существует «золотого правила». Все компоненты алгоритма сильно зависят друг от друга и от задачи.
На практике применим алгоритм к другой задаче:
Дана шахматная доска
и ферзей. Нужно расставить их так, чтобы они не били друг друга.
Будем кодировать состояние так же перестановкой чисел от
Такое представление кодирует не все возможные расстановки, но это даже хорошо: точно не учтутся те расстановки, где ферзи бьют друг друга по вертикали или горизонтали.
Выберем функцию
Примерно эквивалентный код на C++:
const int n = 100; // размер доски
const int k = 1000; // количество итераций алгоритма
int f(vector<int> &p) {
int s = 0;
for (int i = 0; i < n; i++) {
int d = 1;
for (int j = 0; j < i; j++)
if (abs(i - j) == abs(p[i] - p[j]))
= 0;
d += d;
s }
return s;
}
// генерирует действительное число от 0 до 1
double rnd() { return double(rand()) / RAND_MAX; }
int main() {
// генерируем начальную перестановку
<int> v(n);
vector(v.begin(), v.end(), 0);
iota(v.begin(), v.end());
shuffleint ans = f(v); // текущий лучший ответ
double t = 1;
for (int i = 0; i < k && ans < n; i++) {
*= 0.99;
t <int> u = v;
vector(u[rand() % n], u[rand() % n]);
swapint val = f(u);
if (val > ans || rnd() < exp((val - ans) / t)) {
= u;
v = val;
ans }
}
for (int x : v)
<< x + 1 << " ";
cout
return 0;
}
Здесь подсчёт количества свободных диагоналей работает за
Упражнение. Соптимизируйте пересчёт до
Примечание. На самом деле, задача о ферзях решается конструктивно.