我理解的三分算法其实就是用来解决一类这种求解函数的极值的方法。
可以用来单峰或者单谷函数求极值点的问题。
我们确定下来左端点L和右端点R后,我们令mid = L + (R - L) / 3,midmid = R - (L + R) / 3,然后比较这两处的函数值的大小, 如果我们求得是极大值,那么假如此时F(mid) < F(midmid),那么就说明小的那部分外面的我们其实就可以舍去不要了,同理求极小值的话,就是大的部分那外面就可以不要了。这样的话,最后通过不断的逼近,我们就可以得到极值点的值了。
//这是第一种三分的写法 我觉得这样比较好
int mid = l + (r - l) / 3;
int midmid = r - (r - l) / 3;
//第二种就是
int mid = (l + r) / 2;
int midmid = (mid + r) / 2;
哪种习惯用那种。
另外三分经常用来解决浮点数的极值问题,假如我们求得是一个连续的函数的极值点,那么最好用
for(int i = 1;i <= 100;i ++)
来控制三分的次数因为while(r - l >= eps)有时候会被卡。
EX1:HDU 4355
题意:给你数轴上n个点的坐标还有他们权重w,然后让你找一个点,使得这个点到其他点的距离的三次方再乘上位权最小。可以盲猜三分,然后求解决了。有时候单峰函数也不是那么好证明,猜对也行qwq。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
const int INF = 0x3f3f3f3f;
const double eps = 1e-6;
double x[MAXN],w[MAXN];
int n;
//直接盲猜 三分 单峰函数
double f(double p){
double ans = 0;
for(int i = 1;i <= n;i ++){
double t = abs(p - x[i]);
ans += t * t * t * w[i];
}
return ans;
}
int main(){
int T,cas = 0;scanf("%d",&T);
while(T--){
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%lf%lf",&x[i],&w[i]);
double l = 0,r = 1000000.0;
for(int i = 1;i <= 100;i ++) {
double mid = l + (r - l) / 3.0;
double midmid = r - (r - l) / 3.0;
if(f(mid) > f(midmid)) l = mid;
else r = midmid;
}
printf("Case #%d: %.0f\n",++cas,f(r));
}
return 0;
}
// 1
// 4
// 0.6 5
// 3.9 10
// 5.1 7
// 8.4 10
EX2:HDU 3400
题意:在一个二维平面上给定你两条传送带线段。AB和CD,然后在AB带上的速度是P,在CD带上的速度是Q,在平面上其他部分的速度的R,然后问你从A点到D点的最段时间花费是怎么样的?
思路:这是一个很经典的三分问题模型,既然要从A到D我们一定选择了任意的位置从AB上离开,然后任意的位置到达CD上。(当然也可以完全不走这两部分)
这样的话我们就设从AB上离开的点的位置是E,然后到达CD的位置是F。
得到两个关于函数:
f = |AE| / P , g = |EF| / R + |FD| / Q
很明显f函数一个单调递增的函数,而g函数根据F点选择的位置不同,是一个递减再递增的函数。
这样的话,我们的答案受这俩个函数的影响也会是一个单谷函数。
既然如此,我们就可以先三分E点的位置,然后对于每一个E的位置再去三分F的位置,因为这两个函数都是单谷的,最后就可以得到最短的时间了。
关键就是把模型抽象出来,然后确定它的增减性,之后就可以做题了。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const double eps = 1e-6;
struct node{
double x,y;
node(){};
node(double _x,double _y):x(_x),y(_y){};
}a,b,c,d,e,f;
double P,Q,R;
double dis(node a,node b){ return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); }
double cal(node a,node b,node c){
return dis(a,b)/R + dis(b,c)/Q;
}
double solve(node t){//提供的是假设已知的E点 即t
node l = c,r = d;
for(int i = 1;i <= 100;i ++){
node mid((l.x+(r.x-l.x)/3.0),(l.y+(r.y-l.y)/3.0));
node midmid(r.x-(r.x-l.x)/3.0,(r.y-(r.y-l.y)/3.0));
double f_mid = cal(t,mid,d);
double f_midmid = cal(t,midmid,d);
if(f_mid > f_midmid) l = mid;
else r = midmid;
}
return cal(t,r,d);
}
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%lf%lf%lf%lf",&a.x,&a.y,&b.x,&b.y);// ab
scanf("%lf%lf%lf%lf",&c.x,&c.y,&d.x,&d.y);
scanf("%lf%lf%lf",&P,&Q,&R);
node l = a,r = b;
for(int i = 1;i <= 100;i ++){
node mid(l.x+(r.x-l.x)/3.0,l.y+(r.y-l.y)/3.0);
node midmid(r.x-(r.x-l.x)/3.0,r.y-(r.y-l.y)/3.0);
double fmid = dis(mid,a)/P + solve(mid);
double fmidmid = dis(midmid,a)/P + solve(midmid);
if(fmid > fmidmid) l = mid;
else r = midmid;
}
printf("%.2f\n",dis(r,a)/P + solve(r));
}
return 0;
}
EX3:HDU4454
题意:在二维平面上,给定你一个起始点的坐标,然后同时给定你一个圆还有一个矩形,问你从起始点到圆再到矩形的最短距离是多少,只要碰到了边界的点就算到达了相应的图形了。
画一画图可以发现,对于一个给定的这三个图形,是可以确定0到PI和PI到2*PI这两个部分一定一个是单峰的一个是单谷的,但是我们也不知道,哪个是,所以碰到这中情况的时候我们就可以分段就行三分,我们要的答案一定在这我们分的这几个段里,然后取个min就可以了。
用圆心和半径和旋转角确定圆上的点,然后求这个点到矩形的最短距离即可,也就是会求点到线段的最短距离就可以了。
对于这种分段具有极值的函数我们无法确定它在具体的那一段上,此时我们就分段求解最后取个max或者min就是我们要的答案了。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
const int INF = 0x3f3f3f3f;
const double eps = 1e-6;
const double PI = acos(-1.0);
double cx,cy,r;
int sign(double x){
if(fabs(x) <= eps) return 0;
if(x > 0) return 1;
else return -1;
}
struct Point{
double x,y;
Point(){}
//定义运算
Point(double _x,double _y){x = _x;y = _y;}
Point operator + (const Point &b)const{
return Point(x+b.x,y+b.y);
}
Point operator - (const Point &b)const{
return Point(x-b.x,y-b.y);
}
Point operator * (const double &k)const{//乘常数
return Point(x*k,y*k);
}
Point operator / (const double &k)const{
return Point(x/k,y/k);
}
}p[17],st;
struct Line {
Point a,b;
}line[7];
typedef Point Vector;
double dis(Point a,Point b){ return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); }
double len(Vector vx){ return sqrt(vx.x * vx.x + vx.y * vx.y); }
double cross(Vector a,Vector b){ return a.x * b.y - a.y * b.x; }
double dot(Vector a,Vector b){ return a.x * b.x + a.y * b.y; }
double distoline(Point a,Point b,Point c){//点到线段的距离
Vector v1 = b - a,v2 = c - a,v3 = c - b;
if(sign(dot(v1,v2)) < 0) return len(v2);
else if(sign(dot(v1,v3)) > 0) return len(v3);
else return fabs(cross(v1,v2)) / len(v1);
}
double cal(double rad){
double tx = cx + r * cos(rad),ty = cy + r * sin(rad);
Point t;
t.x = tx,t.y = ty;
double ans = 1.0 * INF;
for(int i = 1;i <= 4;i ++){
ans = min(ans,distoline(line[i].a,line[i].b,t));
}
ans += dis(t,st);
return ans;
}
double solve(double l,double r){
for(int i = 1;i <= 100;i ++){
double mid = l + (r - l) / 3.0;
double midmid = r - (r - l) / 3.0;
double f_mid = cal(mid),f_midmid = cal(midmid);
if(f_mid > f_midmid) l = mid;
else r = midmid;
}
return cal(r);
}
int main(){
while(~scanf("%lf%lf",&st.x,&st.y)){
if(fabs(st.x) < eps && fabs(st.y) < eps) break;
scanf("%lf%lf%lf",&cx,&cy,&r);
scanf("%lf%lf%lf%lf",&p[1].x,&p[1].y,&p[3].x,&p[3].y);
if(p[1].y < p[3].y) swap(p[1].x,p[3].x),swap(p[1].y,p[3].y);
p[2].x = p[1].x,p[2].y = p[3].y;
p[4].x = p[3].x,p[4].y = p[1].y;
for(int i = 1;i <= 4;i ++){//矩形的四条边
line[i].a = p[i],line[i].b = p[(i%4)+1];
}
double res = 1.0 * INF;
res = min(res,solve(0,PI));
res = min(res,solve(PI,2.0*PI));
printf("%.2f\n",res);
}
return 0;
}
EX4: CF1355E
题意:给定你一组长度为n的数,然后你会有三种可选择的操作,第一种就是选择一个数是他的值加一,第二种就是选择一个数然后使他的值减一,第三种选择就是一个数使它减一并同时选择一个数使它加一。
这三种操作的花费也会给出 A,R,M。然后让你求出如果要把这n个数都变成同样的高度需要最少的花费为多少。
思路:这题好像是贪心三分都可。
求最少我们直接三分他们最后变成同的高度是多少,然后对于这样的一个高度,我们计算花费的时候要考虑一次M花费的操作,效果是和一次A花费的操作加上一次R花费的操作效果是一样的。所以这时候我们就看一下M 和 A + R的大小关系,如果是M大的话我们就不用他了,否则的话先用M的抹平他们和枚举高度之间的差距。
然后计算的时候就是求出每个数和我们当前枚举高度之间的差值,然后用这个差值算答案,三分即可。
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
const double eps = 1e-6;
const ll INF = 0x3f3f3f3f3f;
ll h[MAXN];
ll a,r,m;int n;
//由题意可得 一个m的效果等于一个a+r
ll f(ll x){//对应高度为x的函数
ll add = 0,sub = 0;
for(int i = 1;i <= n;i ++){
if(h[i] < x) add += x - h[i];
if(h[i] > x) sub += h[i] - x;
}
// cout<<add<<' '<<sub<<endl;
ll ans = 0;
if(m < (a + r)){
if(add > sub){
ans += sub * m + (add-sub) * a;
}
else{
ans += add * m + (sub-add) * r;
}
}
else{
ans += add * a + sub * r;
}
return ans;
}
int main(){
scanf("%d%lld%lld%lld",&n,&a,&r,&m);
for(int i = 1;i <= n;i ++) scanf("%lld",&h[i]);
ll l = INF,r = 0;
for(int i = 1;i <= n;i ++){
l = min(l,h[i]);
r = max(r,h[i]);
}
while(l <= r){
ll mid = l + (r - l) / 3,midmid = r - (r - l) / 3;
if(f(mid) > f(midmid))
l = mid + 1;
else
r = midmid - 1;
}
printf("%lld\n",f(l));
return 0;
}