三分算法学习笔记

0.概念

二分算法,顾名思义,就是每一次将区间分成 2 个一样的区间。

三分算法,同样顾名思义,就是每一次将区间分成 3 个一样的区间。

三分算法主要用来求解一些关于单调函数的问题。 而且考试中有一定的几率考到。


二分算法主要用来求一些简单的最值问题,例如在一些一次函数上。

三分算法主要用来求一些凹函数和凸函数的最值问题,典型地,在一些二次函数上,甚至是高次函数。

凸函数的定义:

  • 学术描述:对此函数进行二次求导,最后的值恒 \(<0\)

  • 人话描述:先上升后下降的函数。

凹函数的定义就是先下降后上升的函数。

当然,函数的形状各种各样,以上的定义不一定是完全正确的。

只需要记住,三分的函数一定是单峰函数。

而且大部分三分都是基于浮点数的。

1.思路

强烈推介学习二分的思想之后再来看这部分。

一开始,仿照二分的思想,我们需要确定一个区间,使得最值一定在这个区间里面。

设区间的左端点为 \(L\),区间的右端点为 \(R\)

我们在区间里面随便取两个点 \(lmid,rmid\) 使 \(lmid < rmid\),然后获取并比较函数在 \(lmid,rmid\) 的位置的值,记为 \(val_{lmid},val_{rmid}\)文章以后会继续沿用此定义)。

设我们需要三分的函数是凸函数,如果是凹函数则可以看为反过来。

毕竟我们需要做的题目要么就是凸函数,要么就是凹函数,一定要在思考三分做法的前面思考函数的形状

有以下三种情况:

第一种情况:\(val_{lmid} < val_{rmid}\)

则若峰值出现在 \(lmid\) 左边的某个位置,一定有一点比其更大(这里有一个现成的例子:\(rmid\)),矛盾。

所以,遇到这种情况时,我们就可以排除掉 \(lmid\) 左边的区间了。

第二种情况:\(val_{rmid} > val_{lmid}\)

则若峰值出现在 \(rmid\) 右边的某个位置,一定有一点更大(\(lmid\)),矛盾。

所以,遇到这种情况时,我们就可以排除掉 \(rmid\) 右边的区间了。

第三种情况:\(val_{lmid} = val_{rmid}\)

这种情况可以直接使用 else 判到第二种情况内。

得到新的 \(L,R\) 之后,再次选 \(lmid,rmid\) 即可。

这不是真正的三分,只是其中的一部分。三分是基于其进行了优化。


这时候,我们观察到一个区间被分为了三个部分\([L,lmid]\)\([lmid,rmid]\)\([rmid,R]\)

而不难注意到每一次都是第一个或者第三个子区间被排除。 因此引出三分的两个优化想法:

优化1:将以上三个区间的长度都尽量逼近至 \(\frac{1}{3} (R-L+1)\)
优化2:将中间的区间的长度留的极小,无限趋近但不等于 \(0\)。将留下来的继续平分给其他两个区间。

执行这两个优化之后,三分的复杂度可以看成 \(O(\log n)\),其中 \(n = R-L+1\)

但是因为 \(\log\) 的底数不管是什么值,答案总不会相差太大。所以加上那个优化都一样。

2.代码

浮点数三分:

// 寻找凸函数在[l, r]区间内的最值,eps 表示精度
double ternary_search_max(double l, double r, double eps) {
	// 当区间长度大于精度要求时持续三分
	while (r - l > eps) {
		// 得到两个中间点
		double lmid = l + (r - l) / 3.0;
		double rmid = r - (r - l) / 3.0;
		// 比较两个中间点的函数值,缩小搜索范围
		if (cal(lmid) < cal(rmid))
			l = lmid;  // 排除左半区间
		else
			r = rmid; // 排除右半区间
	}
	return r;//返回右端点
}

// 寻找凹函数在[l, r]区间内的最值,eps 表示精度
double ternary_search_min(double l, double r, double eps) {
	// 终止条件与最大值搜索相同
	while (r - l > eps) {
		double lmid = l + (r - l) / 3.0;
		double rmid = r - (r - l) / 3.0;
		// 比较逻辑与最大值搜索相反
		if (cal(lmid) > cal(rmid))
			l = lmid;  // 最小值在右半区间
		else
			r = rmid; // 最小值在左半区间
	}
	return l;//返回左端点
}

//实际上返回 l 还是 r 已经不重要了,因为此时 l 和 r 相差不大

整数三分:

// 寻找凸函数在[l, r]区间内的最值
int ternary_search_max(int l, int r) {
	// 当区间长度大于 3 时持续三分
	while (r - l > 3) {
		// 得到两个中间点
		int lmid = l + (r - l) / 3;
		int rmid = r - (r - l) / 3;
		// 比较两个中间点的函数值,缩小搜索范围
		if (cal(lmid) < cal(rmid))
			l = lmid;// 排除左半区间
		else
			r = rmid;// 排除右半区间
	}
	int ans = l;
	for (int i = l; i <= r; ++i)//这里直接暴力算,因为实际判断太复杂了
		if (cal(ans) < cal(i))
			ans = i;
	return ans;//返回结果
}

3.难点

乍一看,三分的代码以及思路实际上并不难,但是为什么三分的题目经常出现蓝紫题呢?

实际上,三分的难点在于判断函数是一个单峰函数。

面对一个问题,有数学功底的同学都会不难想到先将问题的值抽象成一个函数。

但是大部分人往往到这时就不会做了,反而会因为函数的其他性质被带偏:上贪心、动态规划……从而陷入深潭,拼尽全力无法做出。

但如果这时有人告诉你这是一个单峰函数,你会不会恍然大悟?


有很多正向得出单峰函数的方法。

  • 第一种,求二阶导数。

但是如果函数过于复杂,而且分段也无法求出,这样就很难做出问题了。

  • 第二种,抽点计数

因为在 NOI 系列竞赛中无法使用几何画板,所以我们就尝试写一个暴力,抽几个点绘制函数图像,看一下到底像不像单峰函数。

  • 第三种,证明

根据数学知识,凸函数主要满足以下公式:

\[\forall x_1\, x_2\in C,x_1\le x_2,\frac{1}{2}(f(x_1)+f(x_2))<f(\frac{1}{2}(x_1+x_2)) \]

当上述公式成立时,这个函数就是凸函数。

\[\forall x_1,x_2\in C,x_1\le x_2,\frac{1}{2}(f(x_1)+f(x_2))>f(\frac{1}{2}(x_1+x_2)) \]

成立时,这个函数就是凹(也称下凸)函数。

  • 第四种,求出峰值并证明

我们还可以对于函数找出一峰点 \(x\),并证明,\(x\) 左边开始下降 or 上升,右边上升 or 下降。

第一种方法较少用,一般以第四种和第二种为主。

因此,想要将三分题目做的得心应手,必须要有很扎实的数学功底。

4.练习

P1883 【模板】三分 | 函数

因为同时考虑 \(100\) 个函数的值过于困难了,所以我们考虑只有两个函数 \(f(x)\)\(g(x)\),并设 \(h(x)=\max(f(x),g(x))\)

很显然,由于 \(a\ge 0\),则 \(f,g\) 为下凸函数(即凹函数)。我们需要证明 \(h\) 也为下凸函数,即需要证明

\[\forall x_1,x_2\in C,x_1\le x_2,\frac{1}{2}(h(x_1)+h(x_2))>h(\frac{1}{2}(x_1+x_2)) \]

这个式子恒成立。


首先根据 \(h\) 的定义,将 \(f(x)\)\(g(x)\) 往里代换:

\[\frac{1}{2}(\max(f(x_1),g(x_1))+\max(f(x_2),g(x_2)))>\max(f(\frac{x_1+x_2}{2}),g(\frac{x_1+x_2}{2})) \]

因为 \(f,g\) 都为下凸函数,则可得:

\[\left\{\begin{matrix} \frac{1}{2}(f(x_1)+f(x_2))>f(\frac{1}{2}(x_1+x_2))\\\\\frac{1}{2}(g(x_1)+g(x_2))>g(\frac{1}{2}(x_1+x_2)) \end{matrix}\right. \]

将两个式子一比较,直接证毕。

考虑扩展到更多函数的情况:将得出来的下凸函数 \(h\) 再次与另一个下凸函数 \(k\) 进行 \(\max\) 合并,同理得到一个新的下凸函数;然后再次进行合并……

因此,可以知道:无论有多少个函数,只要都是下凸的,进行 \(\max\) 合并之后的出来的函数也一定下凸。

#include <bits/stdc++.h>
#define int long long
using namespace std;
int t, n;
const int N = 10010;
const double eps = 1e-9;//eps在不爆精度的情况下尽量小
int a[N], b[N], c[N];

double cal(double x) {//计算函数值
	double ans = -1e18;
	for (int i = 1; i <= n; i++)
		ans = max(ans, x * a[i] * x + b[i] * x + c[i]);
	return ans;
}

signed main() {
	cin >> t;
	while (t--) {
		cin >> n;
		for (int i = 1; i <= n; i++)
			cin >> a[i] >> b[i] >> c[i];
		double l = 0, r = 1000;//题目中规定了范围
		while (r - l > eps) {
			double lmid = l + (r - l) / 3.0;
			double rmid = r - (r - l) / 3.0;
			if (cal(lmid) > cal(rmid))
				l = lmid;
			else
				r = rmid;
		}
		printf("%.4lf\n", cal(l));
	}
	return 0;
}

CF201B Guess That Car!

简化描述

给出一个 \(n\times m\) 的方阵(行和列数从 \(1\) 开始),每一个格子的半径为 \(4\)(不要问我是从哪里来的,要问就去看样例解释)。每一个格子 \((i,j)\)中心都有一个数 \(C_{i,j}\)

你需要确定一个格点,算出这个格点到每一个格子 \((i,j)\)中心点欧几里得距离 \(d_{i,j}\),并最小化该式子:

\[\sum_{i=1}^{n} \sum_{j=1}^{m}C_{i,j}\times d_{i,j}^2 \]

思路

此题细节较多,又是中心又是格点,较难实现。注意要仔细阅读题目中的输出格式。

不妨按照输出格式设格点的位置为 \((x,y)\)

根据中心点的性质,可以算出 \((i,j)\) 格子的中心点的位置为 \((i-0.5,j-0.5)\)

不妨对 \(d_{i,j}\) 进行拆解:

\[d_{i,j}=\sqrt ((x-i+0.5)\times 4)^2+((y-j+0.5)\times 4)^2 \]

所以 \(d_{i,j}^2=((x-i+0.5)\times 4)^2+((y-j+0.5)\times 4)^2\)

因为这时候我们可以分 \(x,y\) 分别计算,直接针对横坐标和纵坐标跑三分即可。

而显然这是两个下凸函数。

#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m;
const int N = 1010;
int c[N][N];

int cal(int x, int val) {//val=1表示行,val=2表示列,下同
	int ans = 0;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++) {
			if (val == 1)
				ans += c[i][j] * ((x - i) * 4 + 2) * ((x - i) * 4 + 2);
			else
				ans += c[i][j] * ((x - j) * 4 + 2) * ((x - j) * 4 + 2);//套用公式
		}
	return ans;
}

int smin(int l, int r, int val) {//对ternary_search_max的一些小改版
	while (r - l > 3) {
		int lmid = l + (r - l) / 3;
		int rmid = r - (r - l) / 3;
		if (cal(lmid, val) > cal(rmid, val))
			l = lmid;
		else
			r = rmid;
	}
	int ans = l;
	for (int i = l; i <= r; i++)
		if (cal(ans, val) > cal(i, val))
			ans = i;
	return ans;
}

signed main() {
	scanf("%lld%lld", &n, &m);//注意使用正确的读入方式
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			scanf("%lld", &c[i][j]);
	int lans = smin(0, n, 1), rans = smin(0, m, 2);
	printf("%lld\n%lld %lld\n", cal(lans, 1) + cal(rans, 2), lans, rans);
	return 0;
}

5.凸函数的性质

经过一些的题目练习之后,我们可以总结出凸函数的一些性质:

  • \(f(x)\) 是定义在凸集 \(S\) 上的凸函数,对于任意非负数 \(c\ge 0\),函数 \(c f(x)\) 也是凸函数。
  • \(f(x),g(x)\) 都是定义在凸集 \(S\) 上的凸函数,则函数 \(f(x)+g(x)\) 也是凸函数。
  • \(f(x),g(x)\) 都是定义在凸集 \(S\) 上的凸函数,且 \(g\) 值递增,那么 \(h(x)=g(f(x))\) 凸函数。
  • \(f(x)\) 为定义在凸集 \(S\) 上的凸函数,则对于任意实数 \(c\),集合 \(S_c=\{x|x\in S,f(x)\le c\}\) 是凸集。
  • \(f(x)\) 为定义在凸集 \(S\) 上的凸函数,则它的任意一个极小点就是它在 \(S\) 上的全局极小点,而且所有极小点的集合是凸集。
  • 一个上升函数和一个下降函数取 \(\min\)\(\max\) 都会重新形成一个单峰函数。
posted @ 2025-02-22 17:04  wusixuan  阅读(135)  评论(0)    收藏  举报