求函數極值的有關算法


關於求函數極值,通常有二分、三分、爬山、模擬退火等。

當然,不同的算法適應不同的函數類型,比如上述4種算法的前三種通常用來處理單峰函數,其中爬山算法也可以處理多峰函數,但是容易陷入局部最優解。

當然,爬山算法和模擬退火算法都屬於隨機化算法(騙分用的),所以不要總是使用。

1.二分

這個算法但凡學過OI的人應該都會的,求最值的操作也很簡單。

不必多講,上例題:

Codeforces Round #700 Searching Local Minimum

這是一道交互題,題面意思大概就是有一個未知的數組,給定長度 $ n $ , 每次可以詢問該數組的任意一個位置的數, 在詢問次數不超過 $ 100 $ 的

情況下求出該數組的Local Minimum $ k $ 。 其中若 $ a_i < min (a_{i - 1}, a_{i + 1}) $ ,則 $ k = i $ 。

保證該數組僅存在一個 $ k $, 且 $ a_0 = a_{n + 1} = +\infty $ 。

其實這個題的思路也類似於三分了, 由於該函數實質上是一個散點型函數,所以我們每次比較 $ mid $ 和 $ mid + 1 $ 即可。

具體細節看代碼:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
int n;
template <class I> inline void read(I &x){
    x = 0; int f = 1; char ch;
    do { ch = getchar(); if(ch == '-') f = -1; } while(ch < '0' || ch > '9');
    do { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); } while(ch >= '0' && ch <= '9');
    x *= f; return;
}
int Binary_Search(void){
    int l = 1, r = n;
    int mid = (l + r) >> 1;
    int a1, a2;
    while(l < r){
        printf("? %d\n", mid); std::cout.flush();
        read(a1);
        printf("? %d\n", mid + 1); std::cout.flush();
        read(a2);
        if(a1 < a2) r = mid;
        else l = mid + 1;
        mid = (l + r) >> 1;
    }
    return l;
}
int main(){
    read(n);
    printf("! %d\n", Binary_Search());
    std::cout.flush();
    return 0;
}

三分

上個題的思想本質上就是三分了。 通常三分的分段方法是: 取 $ l $ 和 $ r $ 的中點 $ mid $ ,再取 $ mid $ 和 $ r $ 的中點 $ mmid $, 以此將

求解區間分成3段。然后與上個題的思路類似,每次縮小求解區間,最后得出答案。

需要注意的是,三分通常運用在正常的連續函數上,所以三分的控制條件一般設為 $ l + esp < r$ 。 \(esp\) 按照題目的精度要求來取,一般情況下 \(1e-6\) 左右。

爬山算法

由於爬山算法能完成的任務完全可以由三分或者更優秀的模擬退火取代,所以這里不多介紹。

SKIP

模擬退火

模擬退火這種算法,相較於爬山算法的優點就是在求解過程中有一定概率接受一個相較於當前最優解而言更劣的解。所以,這就在一定程度上大大減少了算法陷入局部最優解的可能。

我們定義當前溫度為 $ T $ ,新狀態與已知狀態(由已知狀態通過隨機的方式得到)之間的能量(值)差為 $ \Delta E $,則發生狀態轉移(修改最優解)的概率為 :

1.新狀態更優,概率為 $ 1 $。

2.新狀態更劣,概率為 $ e^{\frac{-\Delta E}{T}} $。

注意:我們有時為了使得到的解更有質量,會在模擬退火結束后,以當前溫度在得到的解附近多次隨機狀態,嘗試得到更優的解(其過程與模擬退火相似)。

模擬退火時我們有三個參數:初始溫度 $ T_0 $,降溫系數 $ d $ ,終止溫度 $ T_k$ 。其中 $ T_0 $是一個比較大的數, $ d $是一個非常接近 $ 1 $ 但是小於 $ 1 $ 的數。

首先讓溫度 $ T = T_0 $,然后按照上述步驟進行一次轉移嘗試,再讓 $ T = T \times d $。當 $ T \leq T_k $ 時模擬退火過程結束,當前最優解即為最終的最優解。

注意為了使得解更為精確,我們通常不直接取當前解作為答案,而是在退火過程中維護遇到的所有解的最優值。

咋寫呢?以 Luogu P1337 為例(代碼摘自OI-Wiki):

#include <cstdio>
#include <cstdlib>
#include <ctime>

const int N = 10005;
int n, x[N], y[N], w[N];
double ansx, ansy, dis;

double Rand() { return (double)rand() / RAND_MAX; }
double calc(double xx, double yy) {
  double res = 0;
  for (int i = 1; i <= n; ++i) {
    double dx = x[i] - xx, dy = y[i] - yy;
    res += sqrt(dx * dx + dy * dy) * w[i];
  }
  if (res < dis) dis = res, ansx = xx, ansy = yy;
  return res;
}
void simulateAnneal() {
  double t = 100000;
  double nowx = ansx, nowy = ansy;
  while (t > 0.001) {
    double nxtx = nowx + t * (Rand() * 2 - 1);
    double nxty = nowy + t * (Rand() * 2 - 1);
    double delta = calc(nxtx, nxty) - calc(nowx, nowy);
    if (exp(-delta / t) > Rand()) nowx = nxtx, nowy = nxty;
    t *= 0.97;
  }
  for (int i = 1; i <= 1000; ++i) {
    double nxtx = ansx + t * (Rand() * 2 - 1);
    double nxty = ansy + t * (Rand() * 2 - 1);
    calc(nxtx, nxty);
  }
}
int main() {
  srand(time(0));
  scanf("%d", &n);
  for (int i = 1; i <= n; ++i) {
    scanf("%d%d%d", &x[i], &y[i], &w[i]);
    ansx += x[i], ansy += y[i];
  }
  ansx /= n, ansy /= n, dis = calc(ansx, ansy);
  simulateAnneal();
  printf("%.3lf %.3lf\n", ansx, ansy);
  return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM