关于求函数极值,通常有二分、三分、爬山、模拟退火等。
当然,不同的算法适应不同的函数类型,比如上述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
#include
#include
#include
int n;
template 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
#include
#include
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;
}