最值


题意

你只知道函数 \(f(x) = \sum_{i=1}^{n} |x - x_i|,(\forall i \in [1, n], x_i \in [1, m] ∩ Z)\) 和值域 \([1,m], m\leq10^7\)。 你可以询问 \(x\) 点的取值,在 \(34\) 次询问内,求 \(f(x)\) 的最小值。

三分法

假设 \(x_i\) 是排好序的,那么其实能发现这个函数是一个 凹函数

更强的结论,答案的取值的点就是序列 \(x_i\) 的中位数,不过没有用,无法找出。

能发现这这种函数的斜率其实在单调递增,一般就是二分斜率,找到斜率为 0 的地方,也叫三分法。

一般的三分每缩小一次范围要求两个三等分点的值,那么就能在 \(2 \log_2 m\) 次询问中求出最值。

但是 \(2 \log_2 m \approx 46 \geq 34\)

斐波那契,黄金分割

每次都要求两个三等分点的取值,下一次就重新再求另外两个,这样显得很浪费。

能否重复利用上一次询问的点呢?

设现在的区间是 \([l,r]\), \(f_0,f_1\) 分别是第一个等分点和第二个等分点,\(l\)\(f_1\) 的距离是 \(\phi\), \(l\)\(f_0\) 的距离是 \(1\)

假如缩小范围 \([l',r'],l' = l, r' = f_1\), 设 \(f_0\) 是现在区间的第二个等分点,那么 第二等分点到左端点的距离 占 总长的 \(\frac{1}{\phi}\), 因此推出现在的 \([l, r]\)\(\phi^2\)

假如缩小范围 \([f_0, r]\),由于是要较为等分,看得出来现在的区间和上面假设的区间是对称的,这时 \(f_1\) 到右端点 和 \(l\)\(f_0\) 的距离相等,都为一。

  • 于是就有 \(1 + \phi = \phi^2\) ,解得 \(\phi = \frac{1 + \sqrt{5}}{2}\)

实际上就是黄金分割点。

  • \(f_0\) 占总长的 \(\frac{1}{1+\phi} = \frac{3-\sqrt{5}}{2}\),

  • \(f_1\) 占总长的 \(\frac{\phi}{1 + \phi} = \frac{\sqrt{5} - 1}{2}\)

这样下一次的决策就能用到当前的算出的值。

要注意:由于取值是整数,可能有所误差,要四色五入,直接转换类型是向下取整。

代码:

在实现上避免误差可以用斐波那契数列。

#include  
using namespace std;

extern long long get(int x);

long long min(long long a, long long b) {
	return a > b ? b : a; 
}

const double eps = 1e-8;	
const double f0 = 1.0 * (3 - sqrt(5)) / 2, f1 = 1.0 * (sqrt(5) - 1) / 2;

map mp; 
long long Get(int x) {
	if (mp.find(x) != mp.end()) return mp[x]; 
	return mp[x] = get(x); 
}

long long guess(int m) {
	double l = 1, r = m;
	while (1) {
		double F0 = (r - l) * f0 + l, F1 = (r - l) * f1 + l; 
		int mid = round(F0), midd = round(F1); 
		if ((int)l == mid || (int)r == midd) {
			long long ans = 1e18;
			for (int i = l; i <= r; i ++) 
				ans = min(ans, Get(i));
			return ans;
		}
		long long val1 = Get(mid), val2 = Get(midd); 
		if (val2 > val1) r = F1;
		else l = F0; 
	}
}