题解-洛谷P2571 [SCOI2010]传送带

本文主要讲述了某蒟蒻历经一天一夜尝试 O(1) 做法的一点点思考,以及对梯度下降法及各种优化算法较为详细的介绍

如有不足之处,或有更好的想法,望不吝赐教~


题目传送门

洛谷P2571 [SCOI2010]传送带




O(1) 做法の尝试

推柿子

容易得到,最优路径一定是从 A 移动到 AB 上的某点 M,再由 M 经平面移动到 CD 上的某点 N,最后由 N 移动到 D

所花时间:

t=AMP+MNR+NDQ

找到最优的 MN 即可。

直接设坐标比较麻烦,可以设线段的比值「取 0 ~ 1

AM=xAB,ND=yCD(x,y[0,1])

则:

xMxA=x(xBxA),yMyA=x(yByA)xM=xA+x(xBxA),yM=yA+x(yByA)

同理:

xNxD=y(xCxD),yNyD=y(yCyD)xN=xD+y(xCxD),yN=yD+y(yCyD)

t=xABP+MNR+yCDQ

MN=(xMxN)2+(yMyN)2=(xA+x(xBxA)xDy(xCxD))2+(yA+x(yByA)yDy(yCyD))2)

这一大串式子看起来很难受,不妨简化一下。

设:

c1=ABPc2=CDQc3=1R

t=c1x+c2y+c3MN

设:

c4=xBxAc5=xDxCc6=xAxDc7=yByAc8=yDyCc9=yAyD

MN=(c4x+c5y+c6)2+(c7x+c8y+c9)2

t=f(x,y)=c1x+c2y+c3(c4x+c5y+c6)2+(c7x+c8y+c9)2

c1*x+c2*y+c3*sqrt((c4*x+c5*y+c6)*(c4*x+c5*y+c6)+(c7*x+c8*y+c9)*(c7*x+c8*y+c9))

答案即为求解 f(x,y)x,y[0,1] 的最小值。

xy 求偏导:

fx=c1+c32(c4x+c5y+c6)c4+2(c7x+c8y+c9)c72(c4x+c5y+c6)2+(c7x+c8y+c9)2=c1+c3(c4x+c5y+c6)c4+(c7x+c8y+c9)c7(c4x+c5y+c6)2+(c7x+c8y+c9)2fy=c2+c32(c4x+c5y+c6)c5+2(c7x+c8y+c9)c82(c4x+c5y+c6)2+(c7x+c8y+c9)2=c2+c3(c4x+c5y+c6)c5+(c7x+c8y+c9)c8(c4x+c5y+c6)2+(c7x+c8y+c9)2

fx=fy=0,解出所有驻点:

{c1+c3(c4x+c5y+c6)c4+(c7x+c8y+c9)c7(c4x+c5y+c6)2+(c7x+c8y+c9)2=0c2+c3(c4x+c5y+c6)c5+(c7x+c8y+c9)c8(c4x+c5y+c6)2+(c7x+c8y+c9)2=0

c1c2 移到等号右边,将 (c4x+c5y+c6)2+(c7x+c8y+c9)2 乘过去,两边开平方:

{(1)c32[(c4x+c5y+c6)c4+(c7x+c8y+c9)c7]2=c12[(c4x+c5y+c6)2+(c7x+c8y+c9)2](2)c32[(c4x+c5y+c6)c5+(c7x+c8y+c9)c8]2=c22[(c4x+c5y+c6)2+(c7x+c8y+c9)2]

这样一来,方程组的形式就优美了许多,我们可以轻松求解

用正则表达式稍作整理:

c32((c4x+c5y+c6)c4+(c7x+c8y+c9)c7)2=c12((c4x+c5y+c6)2+(c7x+c8y+c9)2),c32((c4x+c5y+c6)c5+(c7x+c8y+c9)c8)2=c22((c4x+c5y+c6)2+(c7x+c8y+c9)2)

塞到求解方程组的计算器中,即可求出 xy 的值:

1 {x=c5c9c6c8c4c8c5c7y=c4c9c6c7c4c8c5c7=c6c7c4c9c4c8c5c7
x=(c5*c9-c6*c8)/(c4*c8-c5*c7),y=(c6*c7-c4*c9)/(c4*c8-c5*c7)

xy 代入原方程,可以利用表达式化简工具验证一下:

(c3)2(((c4)(c5c9c6c8)/(c4c8c5c7)+(c5)(c6c7c4c9)/(c4c8c5c7)+c6)(c4)+((c7)(c5c9c6c8)/(c4c8c5c7)+(c8)(c6c7c4c9)/(c4c8c5c7)+(c9))(c7))2((c1)2(((c4)(c5c9c6c8)/(c4c8c5c7)+(c5)(c6c7c4c9)/(c4c8c5c7)+(c6))2+((c7)(c5c9c6c8)/(c4c8c5c7)+(c8)(c6c7c4c9)/(c4c8c5c7)+(c9))2))

(c3)2(((c4)(c5c9c6c8)/(c4c8c5c7)+(c5)(c6c7c4c9)/(c4c8c5c7)+(c6))(c5)+((c7)(c5c9c6c8)/(c4c8c5c7)+(c8)(c6c7c4c9)/(c4c8c5c7)+(c9))(c8))2((c2)2(((c4)(c5c9c6c8)/(c4c8c5c7)+(c5)(c6c7c4c9)/(c4c8c5c7)+(c6))2+((c7)(c5c9c6c8)/(c4c8c5c7)+(c8)(c6c7c4c9)/(c4c8c5c7)+(c9))2))

上面两个式子的值都为 0,答案正确。

此时,xy 的取值即为 f(x,y) 的极值点。

xy 代入 f(x,y),得:

c1(c5c9c6c8)/(c4c8c5c7)+c2(c6c7c4c9)/(c4c8c5c7)+c3sqrt((c4(c5c9c6c8)/(c4c8c5c7)+c5(c6c7c4c9)/(c4c8c5c7)+c6)(c4(c5c9c6c8)/(c4c8c5c7)+c5(c6c7c4c9)/(c4c8c5c7)+c6)+(c7(c5c9c6c8)/(c4c8c5c7)+c8(c6c7c4c9)/(c4c8c5c7)+c9)(c7(c5c9c6c8)/(c4c8c5c7)+c8(c6c7c4c9)/(c4c8c5c7)+c9))

化简,得:

((c1c5c2c4)c9c1c6c8+c2c6c7)/(c4c8c5c7)

f(x,y)=(c1c5c2c4)c9c1c6c8+c2c6c7c4c8c5c7

((c1*c5-c2*c4)*c9-c1*c6*c8+c2*c6*c7)/(c4*c8-c5*c7)



这就完了?

当然没有。。。


首先,你不知道解出来的 f(x,y) 是极大值还是极小值。

其次,就算是极小值,你也别忘了 x,y[0,1] 的条件。

再者,边界值也有可能是合法区间内的最值。


因此,可以先判断 xy 的范围,若均在 [0,1] 内则用 f(x,y) 更新答案;然后用四个边界值 f(0,0),f(0,1),f(1,0),f(1,1) 更新答案

code:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<climits>
#define Min(a,b,c,d,e) (min(a,min(b,min(c,min(d,e)))))

using namespace std;

long double P,Q,R,ans=INT_MAX;
long double c1,c2,c3,c4,c5,c6,c7,c8,c9;
long double x,y;

struct node{
    long double x,y;
}A,B,C,D,M,N;

inline long double dis(node a,node b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

inline long double f(long double x,long double y)
{
    return c1*x+c2*y+c3*sqrt((c4*x+c5*y+c6)*(c4*x+c5*y+c6)+(c7*x+c8*y+c9)*(c7*x+c8*y+c9));
}

signed main()
{
    scanf("%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf",&A.x,&A.y,&B.x,&B.y,&C.x,&C.y,&D.x,&D.y,&P,&Q,&R);
    c1=dis(A,B)/P,c2=dis(C,D)/Q,c3=1/R;
    c4=B.x-A.x,c5=D.x-C.x,c6=A.x-D.x,c7=B.y-A.y,c8=D.y-C.y,c9=A.y-D.y;
    x=(c5*c9-c6*c8)/(c4*c8-c5*c7),y=(c6*c7-c4*c9)/(c4*c8-c5*c7);
    if(x>=0&&x<=1&&y>=0&&y<=1) ans=min(ans,f(x,y));
    ans=Min(f(0,0),f(0,1),f(1,0),f(1,1),ans);
    printf("%.2Lf\n",ans);
    return 0;
}



特判——「一元」

若只想到这里,你只能获得 60pts

2

还有一点需要处理:分母为 0 的情况。

考虑 xyf(x,y)c4c8c5c7=0 时没有定义。

展开发现,此时 (xBxA)(yDyC)=(xDxC)(yByA)

注意 ABCD 可能为 0

特判这两种情况:

CD=0,那么容易想到,最优路径一定是从 A 移动到 AB 上的某点 M,再由 M 经平面移动到 D

显然这是原先的一种特殊情况:NCD 重合

此时只需对原式略作修改即可。

所花时间:

t=AMP+MDR=xABP+MDR

MD=MN=(xMxN)2+(yMyN)2=(xA+x(xBxA)xDy(xCxD))2+(yA+x(yByA)yDy(yCyD))2)=(xA+x(xBxA)xD)2+(yA+x(yByA)yD)2)

容易发现,此时 y 的取值不再影响结果,t 变成了关于 x 的一元函数。

重新用一下前面定义的几个参数:

c1=ABPc2=CDQ=0c3=1R

c4=xBxAc5=xDxC=0c6=xAxDc7=yByAc8=yDyC=0c9=yAyD

MD=(c4x+c6)2+(c7x+c9)2

t=f(x)=c1x+c3(c4x+c6)2+(c7x+c9)2

答案即为求解 f(x)x[0,1]的最小值

直接求导:

dfdx=c1+c32(c4x+c6)c4+2(c7x+c9)c72(c4x+c6)2+(c7x+c9)2=c1+c3(c4x+c6)c4+(c7x+c9)c7(c4x+c6)2+(c7x+c9)2

dfdx=0,解出所有驻点:

c1+c3(c4x+c6)c4+(c7x+c9)c7(c4x+c6)2+(c7x+c9)2=0

稍作化简:

c32[(c4x+c6)c4+(c7x+c9)c7]2=c12[(c4x+c6)2+(c7x+c9)2]

不就是个一元二次方程嘛,手解也能解出来「就是懒得解」

再次用正则表达式整理:

c32((c4x+c6)c4+(c7x+c9)c7)2=c12((c4x+c6)2+(c7x+c9)2)

塞进那个工具里,然后就得到了两个解:

x1=c32c72+c32c42c12(c1c4c9c1c6c7)+((c12c32c42)c7c32c73)c9c32c4c6c72+(c12c4c32c43)c6c32c74+(2c32c42c12)c72+c32c44c12c42x2=c32c72+c32c42c12(c1c4c9c1c6c7)+(c32c73+(c32c42c12)c7)c9+c32c4c6c72+(c32c43c12c4)c6c32c74+(2c32c42c12)c72+c32c44c12c42

x1=(sqrt(c3*c3*c7*c7+c3*c3*c4*c4-c1*c1)*(c1*c4*c9-c1*c6*c7)+((c1*c1-c3*c3*c4*c4)*c7-c3*c3*c7*c7*c7)*c9-c3*c3*c4*c6*c7*c7+(c1*c1*c4-c3*c3*c4*c4*c4)*c6)/(c3*c3*c7*c7*c7*c7+(2*c3*c3*c4*c4-c1*c1)*c7*c7+c3*c3*c4*c4*c4*c4-c1*c1*c4*c4)
x2=-(sqrt(c3*c3*c7*c7+c3*c3*c4*c4-c1*c1)*(c1*c4*c9-c1*c6*c7)+(c3*c3*c7*c7*c7+(c3*c3*c4*c4-c1*c1)*c7)*c9+c3*c3*c4*c6*c7*c7+(c3*c3*c4*c4*c4-c1*c1*c4)*c6)/(c3*c3*c7*c7*c7*c7+(2*c3*c3*c4*c4-c1*c1)*c7*c7+c3*c3*c4*c4*c4*c4-c1*c1*c4*c4)

同理,当 AB=0 时,MAB 重合:

c1=ABP=0c2=CDQc3=1R

c4=xBxA=0c5=xDxCc6=xAxDc7=yByA=0c8=yDyCc9=yAyD

t=ANR+NDQ=ANR+yCDQ=c2y+c3AN

AN=MN=(xMxN)2+(yMyN)2=(xA+x(xBxA)xDy(xCxD))2+(yA+x(yByA)yDy(yCyD))2)=(xAxDy(xCxD))2+(yAyDy(yCyD))2)=(c5y+c6)2+(c8y+c9)2)

t=f(y)=c2y+c3(c5y+c6)2+(c8y+c9)2)

后面步骤相似,最后化简为:

c32[(c5y+c6)c5+(c8y+c9)c8]2=c22[(c5y+c6)2+(c8y+c9)2]

c32((c5y+c6)c5+(c8y+c9)c8)2=c22((c5y+c6)2+(c8y+c9)2)

y1=c32c82+c32c52c22(c2c5c9c2c6c8)+((c22c32c52)c8c32c83)c9c32c5c6c82+(c22c5c32c53)c6c32c84+(2c32c52c22)c82+c32c54c22c52y2=c32c82+c32c52c22(c2c5c9c2c6c8)+(c32c83+(c32c52c22)c8)c9+c32c5c6c82+(c32c53c22c5)c6c32c84+(2c32c52c22)c82+c32c54c22c52

y1=(sqrt(c3*c3*c8*c8+c3*c3*c5*c5-c2*c2)*(c2*c5*c9-c2*c6*c8)+((c2*c2-c3*c3*c5*c5)*c8-c3*c3*c8*c8*c8)*c9-c3*c3*c5*c6*c8*c8+(c2*c2*c5-c3*c3*c5*c5*c5)*c6)/(c3*c3*c8*c8*c8*c8+(2*c3*c3*c5*c5-c2*c2)*c8*c8+c3*c3*c5*c5*c5*c5-c2*c2*c5*c5)
y2=-(sqrt(c3*c3*c8*c8+c3*c3*c5*c5-c2*c2)*(c2*c5*c9-c2*c6*c8)+(c3*c3*c8*c8*c8+(c3*c3*c5*c5-c2*c2)*c8)*c9+c3*c3*c5*c6*c8*c8+(c3*c3*c5*c5*c5-c2*c2*c5)*c6)/(c3*c3*c8*c8*c8*c8+(2*c3*c3*c5*c5-c2*c2)*c8*c8+c3*c3*c5*c5*c5*c5-c2*c2*c5*c5)

至于 ABCD 同时为 0 的情况,只需计算 AD 间距离即可。


code:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<climits>
#define Min(a,b,c,d,e) (min(a,min(b,min(c,min(d,e)))))

using namespace std;

long double P,Q,R,ans=INT_MAX;
long double c1,c2,c3,c4,c5,c6,c7,c8,c9;
long double x,y;

struct node{
    long double x,y;
}A,B,C,D,M,N,t;

inline long double dis(node a,node b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

inline long double f(long double x,long double y)
{
    return c1*x+c2*y+c3*sqrt((c4*x+c5*y+c6)*(c4*x+c5*y+c6)+(c7*x+c8*y+c9)*(c7*x+c8*y+c9));
}

signed main()
{
    scanf("%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf",&A.x,&A.y,&B.x,&B.y,&C.x,&C.y,&D.x,&D.y,&P,&Q,&R);
    c1=dis(A,B)/P,c2=dis(C,D)/Q,c3=1/R;
    c4=B.x-A.x,c5=D.x-C.x,c6=A.x-D.x,c7=B.y-A.y,c8=D.y-C.y,c9=A.y-D.y;
    if(c4*c8-c5*c7)
    {
        x=(c5*c9-c6*c8)/(c4*c8-c5*c7),y=(c6*c7-c4*c9)/(c4*c8-c5*c7);
        if(x>=0&&x<=1&&y>=0&&y<=1) ans=min(ans,f(x,y));
    }
    else
    {
        if(C.x==D.x&&C.y==D.y)
        {
            x=(sqrt(c3*c3*c7*c7+c3*c3*c4*c4-c1*c1)*(c1*c4*c9-c1*c6*c7)+((c1*c1-c3*c3*c4*c4)*c7-c3*c3*c7*c7*c7)*c9-c3*c3*c4*c6*c7*c7+(c1*c1*c4-c3*c3*c4*c4*c4)*c6)/(c3*c3*c7*c7*c7*c7+(2*c3*c3*c4*c4-c1*c1)*c7*c7+c3*c3*c4*c4*c4*c4-c1*c1*c4*c4);
            ans=min(ans,f(x,y));
            x=-(sqrt(c3*c3*c7*c7+c3*c3*c4*c4-c1*c1)*(c1*c4*c9-c1*c6*c7)+(c3*c3*c7*c7*c7+(c3*c3*c4*c4-c1*c1)*c7)*c9+c3*c3*c4*c6*c7*c7+(c3*c3*c4*c4*c4-c1*c1*c4)*c6)/(c3*c3*c7*c7*c7*c7+(2*c3*c3*c4*c4-c1*c1)*c7*c7+c3*c3*c4*c4*c4*c4-c1*c1*c4*c4);
            ans=min(ans,f(x,y));
        }
        else if(A.x==B.x&&A.y==B.y)
        {
            y=(sqrt(c3*c3*c8*c8+c3*c3*c5*c5-c2*c2)*(c2*c5*c9-c2*c6*c8)+((c2*c2-c3*c3*c5*c5)*c8-c3*c3*c8*c8*c8)*c9-c3*c3*c5*c6*c8*c8+(c2*c2*c5-c3*c3*c5*c5*c5)*c6)/(c3*c3*c8*c8*c8*c8+(2*c3*c3*c5*c5-c2*c2)*c8*c8+c3*c3*c5*c5*c5*c5-c2*c2*c5*c5);
            ans=min(ans,f(x,y));
            y=-(sqrt(c3*c3*c8*c8+c3*c3*c5*c5-c2*c2)*(c2*c5*c9-c2*c6*c8)+(c3*c3*c8*c8*c8+(c3*c3*c5*c5-c2*c2)*c8)*c9+c3*c3*c5*c6*c8*c8+(c3*c3*c5*c5*c5-c2*c2*c5)*c6)/(c3*c3*c8*c8*c8*c8+(2*c3*c3*c5*c5-c2*c2)*c8*c8+c3*c3*c5*c5*c5*c5-c2*c2*c5*c5);
            ans=min(ans,f(x,y));
        }
        else if(C.x==D.x&&C.y==D.y&&A.x==B.x&&A.y==B.y) ans=min(ans,dis(A,D));
    }
    ans=Min(f(0,0),f(0,1),f(1,0),f(1,1),ans);
    printf("%.2Lf\n",ans);   
    return 0;
}



特判——「平行」

若只想到这里,你还是只能获得60pts。。。

3

还有最后一种情况:即是 ABCD 不为 0AB//CD 也会使 c4c8c5c7=0「因为(xBxA)(yDyC)=(xDxC)(yByA)

回到一开始的式子:

t=f(x,y)=c1x+c2y+c3(c4x+c5y+c6)2+(c7x+c8y+c9)2

似乎很难下手了……




或许我们可以换一种思路「乱搞」:充分利用几何知识

两条直线平行,我们可以直接考虑以下几种方案:

  1. AD
  2. CAB 作垂线,若与 AB 有交点 M,则:AMCD
  3. DAB 作垂线,若与 AB 有交点 M,则:AMD
  4. BCD 作垂线,若与 CD 有交点 N,则:ABND
  5. ACD 作垂线,若与 CD 有交点 N,则:AND

还有可能两条线段在同一条直线上,需要特判一下

对这几种方案分别取最小值,得到正确答案的概率就大了许多


code:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<climits>
#define Min(a,b,c,d,e) (min(a,min(b,min(c,min(d,e)))))

using namespace std;

long double P,Q,R,ans=INT_MAX;
long double c1,c2,c3,c4,c5,c6,c7,c8,c9;
long double x,y;

struct node{
    long double x,y;
}A,B,C,D,M,N,t;

inline long double dis(node a,node b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

inline long double f(long double x,long double y)
{
    return c1*x+c2*y+c3*sqrt((c4*x+c5*y+c6)*(c4*x+c5*y+c6)+(c7*x+c8*y+c9)*(c7*x+c8*y+c9));
}

inline bool eq_direction(node A1,node B1,node A2,node B2)//判断共线向量A1B1、A2B2是否同向
{
    if(B1.x-A1.x&&B2.x-A2.x) return (B1.x-A1.x)*(B2.x-A2.x)>0;
    else return (B1.y-A1.y)*(B2.y-A2.y)>0;
}

inline node foot_point(node P,node A,node B)//过P点作直线AB的垂线,返回垂足
{
    node res;
    //计算AB的解析式
    long double a=B.y-A.y,b=A.x-B.x,c=-a*A.x-b*A.y;
    res.x=(b*b*P.x-a*b*P.y-a*c)/(a*a+b*b);
    res.y=(a*a*P.y-a*b*P.x-b*c)/(a*a+b*b);
    return res;
}

inline bool on_line(node P,node A,node B)//判断在直线AB上的点P是否在线段AB上,只需判断P是否在A、B围成的矩形中即可
{
    return min(A.x,B.x)<=P.x&&P.x<=max(A.x,B.x)&&min(A.y,B.y)<=P.y&&P.y<=max(A.y,B.y);
}

signed main()
{
    scanf("%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf",&A.x,&A.y,&B.x,&B.y,&C.x,&C.y,&D.x,&D.y,&P,&Q,&R);
    c1=dis(A,B)/P,c2=dis(C,D)/Q,c3=1/R;
    c4=B.x-A.x,c5=D.x-C.x,c6=A.x-D.x,c7=B.y-A.y,c8=D.y-C.y,c9=A.y-D.y;
    if(c4*c8-c5*c7)
    {
        x=(c5*c9-c6*c8)/(c4*c8-c5*c7),y=(c6*c7-c4*c9)/(c4*c8-c5*c7);
        if(x>=0&&x<=1&&y>=0&&y<=1) ans=min(ans,f(x,y));
    }
    else
    {
        if(C.x==D.x&&C.y==D.y)
        {
            x=(sqrt(c3*c3*c7*c7+c3*c3*c4*c4-c1*c1)*(c1*c4*c9-c1*c6*c7)+((c1*c1-c3*c3*c4*c4)*c7-c3*c3*c7*c7*c7)*c9-c3*c3*c4*c6*c7*c7+(c1*c1*c4-c3*c3*c4*c4*c4)*c6)/(c3*c3*c7*c7*c7*c7+(2*c3*c3*c4*c4-c1*c1)*c7*c7+c3*c3*c4*c4*c4*c4-c1*c1*c4*c4);
            ans=min(ans,f(x,y));
            x=-(sqrt(c3*c3*c7*c7+c3*c3*c4*c4-c1*c1)*(c1*c4*c9-c1*c6*c7)+(c3*c3*c7*c7*c7+(c3*c3*c4*c4-c1*c1)*c7)*c9+c3*c3*c4*c6*c7*c7+(c3*c3*c4*c4*c4-c1*c1*c4)*c6)/(c3*c3*c7*c7*c7*c7+(2*c3*c3*c4*c4-c1*c1)*c7*c7+c3*c3*c4*c4*c4*c4-c1*c1*c4*c4);
            ans=min(ans,f(x,y));
        }
        else if(A.x==B.x&&A.y==B.y)
        {
            y=(sqrt(c3*c3*c8*c8+c3*c3*c5*c5-c2*c2)*(c2*c5*c9-c2*c6*c8)+((c2*c2-c3*c3*c5*c5)*c8-c3*c3*c8*c8*c8)*c9-c3*c3*c5*c6*c8*c8+(c2*c2*c5-c3*c3*c5*c5*c5)*c6)/(c3*c3*c8*c8*c8*c8+(2*c3*c3*c5*c5-c2*c2)*c8*c8+c3*c3*c5*c5*c5*c5-c2*c2*c5*c5);
            ans=min(ans,f(x,y));
            y=-(sqrt(c3*c3*c8*c8+c3*c3*c5*c5-c2*c2)*(c2*c5*c9-c2*c6*c8)+(c3*c3*c8*c8*c8+(c3*c3*c5*c5-c2*c2)*c8)*c9+c3*c3*c5*c6*c8*c8+(c3*c3*c5*c5*c5-c2*c2*c5)*c6)/(c3*c3*c8*c8*c8*c8+(2*c3*c3*c5*c5-c2*c2)*c8*c8+c3*c3*c5*c5*c5*c5-c2*c2*c5*c5);
            ans=min(ans,f(x,y));
        }
        //else if(C.x==D.x&&C.y==D.y&&A.x==B.x&&A.y==B.y) ans=min(ans,dis(A,D));
        else
        {
            ans=min(ans,dis(A,D)/R);
            if(c4*(C.y-B.y)==c7*(C.x-B.x))//已知AB//CD,若AB//BC,则A、B、C、D四点共线
            {
                ans=min(ans,dis(A,B)/P+dis(B,C)/R+dis(C,D)/Q);//ABCD
                ans=min(ans,dis(A,B)/P+dis(B,D)/R);//ABD
                ans=min(ans,dis(A,C)/R+dis(C,D)/Q);//ACD
                if(!eq_direction(A,C,B,C)&&!eq_direction(A,D,B,D))
                {
                    if(eq_direction(A,C,C,D)) ans=min(ans,dis(A,C)/P+dis(C,D)/max(P,Q));//ACDB
                    else ans=min(ans,dis(A,D)/P);//ADCB
                }
                if(!eq_direction(A,C,A,D)&&!eq_direction(B,C,B,D))
                {
                    if(eq_direction(C,A,A,B)) ans=min(ans,dis(A,B)/max(P,Q)+dis(B,D)/Q);//CABD
                    else ans=min(ans,dis(A,D)/Q);//CBAD
                }
            }
            else//不共线
            {
                t=foot_point(C,A,B);
                if(on_line(t,A,B)) ans=min(ans,dis(A,t)/P+dis(t,C)/R+dis(C,D)/Q);//AMCD
                t=foot_point(D,A,B);
                if(on_line(t,A,B)) ans=min(ans,dis(A,t)/P+dis(t,D)/R);//AMD
                t=foot_point(B,C,D);
                if(on_line(t,C,D)) ans=min(ans,dis(A,B)/P+dis(B,t)/R+dis(t,D)/Q);//ABND
                t=foot_point(A,C,D);
                if(on_line(t,C,D)) ans=min(ans,dis(A,t)/R+dis(t,D)/Q);//AND
            }
        }
    }
    ans=Min(f(0,0),f(0,1),f(1,0),f(1,1),ans);
    printf("%.2Lf\n",ans);   
    return 0;
}



瓶颈

emmm...

仍是60pts...

乱搞终究不是正道。。。

不难发现,O(1)做法的瓶颈卡在了AB//CD使得求导后分母为0的情况

若有大佬可以通过调整参数、用更好的方法求最值的方式来绕过这一特殊情况,请不吝赐教




梯度下降法

O(1):我死了QAQ……

再次回到起点:

t=f(x,y)=c1x+c2y+c3(c4x+c5y+c6)2+(c7x+c8y+c9)2

O(1) 复杂度求解多元函数最值的方法有很多:粒子群算法、模拟退火、三分套三分、牛顿迭代法……

在此介绍梯度下降法


梯度

了解多元微积分的各位大佬们都知道,梯度是一个向量,指向多元函数的值变化最快的方向,大小即为变化率。

「不了解的可以看看 3Blue1Brown 的作者 Grant Sanderson 讲的多元微积分课程,在b站上已有熟肉【链接】」

百度百科对梯度的定义:

设二元函数z=f(x,y) 在平面区域D上具有一阶连续偏导数,则对于每一个点P(x,y)都可定出一个向量

{fx,fy}=fx(x,y)i^+fy(x,y)j^

函数「也不是不对,但称为向量更好理解」就称为函数z=f(x,y)在点P(x,y)的梯度,记作gradf(x,y)f(x,y)

于是有:

gradf(x,y)=f(x,y)={fx,fy}=fx(x,y)i^+fy(x,y)j^

其中

=xi^+yj^

称为(二维的)向量微分算子Nabla算子,且有

f=fxi^+fyj^


梯度下降法的主要思想

再来介绍梯度下降法。

正如我们先前对 f(x,y) 求偏导,令偏导值为 0,解出所有驻点的操作,实际上就是试图直接令梯度值为 0

但是也正如我们遇到的问题,有时直接求解比较困难,我们不妨通过迭代,让梯度值逐渐下降,即逐渐接近极值点。

例如,对于函数 f(x)=x2,求导得 f(x)=2x

4

不妨设 x0=1,则从点 (1,1) 开始,计算其梯度值:f(1)=2>0

梯度值 >0,说明在该点沿使 x 增大的方向「即 x 轴正半轴方向」,f(x) 函数值会增大,增大的速率为 2

容易发现,在 (3,9) 处,f(3)=6,增大的速率为 6,观察图像也可得知,沿 x 轴正半轴方向,f(x) 的图像变陡,上升速率变快。

而在 (0,0) 处,f(0)=0,函数图像在这一点的切线斜率是平的,增大的速率无限接近 0,此时梯度值为 0(0,0) 是函数 f(x) 的一个驻点,同时 f(x) 也在此取到最小值。

因此,我们有如下策略:

梯度值 >0,向左移动一点;梯度值 <0,向右移动一点。

即每次让 x 朝与梯度值符号相反的方向移动,使梯度值逐渐下降,最终趋于 0

用数学语言来描述,即:

xk+1=xkf(xk)

其中 xk 为第 k 次迭代时点的横坐标,xk+1 为第 k+1 次迭代移动到的点的横坐标,x0 表示初始横坐标。

f(x) 表示在 x 处的梯度值,f(x)=dfdx

xkf(xk) 表示向与梯度值符号相反的方向移动,符合之前的策略


优化——「学习率」

但这样就有一个问题:

例如 f(x)=x2,x0=10,则 f(x)=2x

x1=x02x0=10x2=x12x1=10x3=x22x2=10...

可以看到,虽然横坐标一直在变化,但一直在 1010 之间振荡,函数值始终没有降到最低点。

为此,我们需要一个参数控制移动的距离,这个参数被称作学习率,用 η 表示:

xk+1=xkηf(xk)

显然,在刚刚的计算中,η=1

η>1 时,函数值不仅不会降到最低点,甚至会越来越大:

e.g.η=1.05

x1=x02x0=11x2=x12x1=12.1x3=x22x2=13.31...

而若将 η 调低到一个较小值,如 0.02

x1=x02x0=9.6x2=x12x1=9.2x3=x22x2=8.8...

降低的速度太慢,容易超时。

η=0.2 时,迭代 10 次左右即可降入谷底:

x1=x02x0=6||f(x0)||=12x2=x12x1=3.6||f(x1)||=7.2x3=x22x2=2.16||f(x2)||=4.32...

可以看到,右边的梯度值在每次迭代后都会下降,故称为梯度下降法「Gradient Descent」

只要选择合适的学习率,梯度就可以下降到任意小:

limkf(xk)=minf(x)

可以用泰勒公式进行严格证明,此处不再赘述(逃

因此可以通过梯度值的大小作为终止条件「也可以直接用迭代次数控制精度」


对于二元函数,同样可以用梯度下降法求解极值:

f(xk+1,yk+1)=f(xk,yk)ηf(xk,yk)

e.g.f(x,y)=x2+2y2,(x0,y0)=(3.5,3.5),η=0.1,则f(x,y)=(2x,4y)

(x1,y1)=(x0,y0)ηf(x0,y0)=(2.8,2.1)(x2,y2)=(x1,y1)ηf(x1,y1)=(2.24,1.26)...


代码——「梯度下降法」

code:

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

inline long double f(long double x)
{
    return 3*x*x*x*x-x*x*x+2*x*x-9*x+5*sqrt((x+3)*(x+3)+(5*x+6)*(5*x+6))-25;
}

inline long double numerical_diff(long double x)//数值微分法估计一阶导数
{
    long double dx=1e-6;
    return (f(x+dx)-f(x-dx))/(dx*2);
}

inline long double gradient_descent(long double x,long double eta)//梯度下降法
{
    long double eps=1e-6;
    //int cnt=0;
    while(abs(numerical_diff(x))>eps)
    {
        x-=eta*numerical_diff(x);
        //cnt++;
    }
    //cout<<cnt<<endl;
    return f(x);
}

signed main()
{
    cout<<gradient_descent(0,0.02)<<endl;
    return 0;
}

e.g.f(x)=3x4x3+2x29x+5(x+3)2+(5x+6)225

5

经过9次迭代达到目标精度。


优化——动量梯度下降法「MGD」

需要指出的是,梯度值为 0 的驻点不一定是函数的极值点,如 f(x)=x3x=0 处梯度值为 0,但并不是函数的极值点:

6

「实际上,(0,0)f(x)拐点,此处函数的凹凸性发生改变」

同时,对于非单峰函数来说,梯度下降法的结果易受到初始值的影响,也就是陷入局部最优解

现实生活中,一个小球从高处落下,大概率会越过比较低的坎继续下降。

我们同样可以引入惯性来优化:

用梯度模拟受力,使之不直接控制移动距离,而是给小球一个速度 v;同时可以引入阻力,也就是速度衰减率 β 使小球减速。

于是,整个过程就像一个有动量的小球在空间中来回滚动,故称动量梯度下降法「Gradient Descent with Momentum,简称MGD」

根据动量定理

FΔt=mΔv

移项,得:

Δv=ΔtmF

直接令 η=Δtm,则:

Δv=ηF

但在实际编程中,不必那么精细地刻画阻力,直接用一个速度衰减率 β 代替即可:

vk+1=βvkηf(xk)

code:

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

inline long double f(long double x)
{
    return 3*x*x*x*x-x*x*x+2*x*x-9*x+5*sqrt((x+3)*(x+3)+(5*x+6)*(5*x+6))-25;
}

inline long double numerical_diff(long double x)
{
    long double dx=1e-6;
    return (f(x+dx)-f(x-dx))/(dx*2);
}

inline long double mgd(long double x,long double eta,long double beta)//动量梯度下降法
{
    long double v=0,eps=1e-6;
    int cnt=0;
    while(abs(numerical_diff(x))>eps)
    {
        v=beta*v-eta*numerical_diff(x);
        x+=v;
        cnt++;
    }
    cout<<cnt<<endl;
    return f(x);
}

signed main()
{
    cout<<mgd(0,0.03,0.05)<<endl;
    return 0;
}

迭代次数:13

「多峰函数的优化效果较明显」


优化——自适应梯度算法「AdaGrad」

对比一下两种算法的真实移动距离 Δxi

梯度下降法 动量梯度下降法
Δxi ηfxi ηvi

动量梯度下降法只优化了 fxi 的部分。

很自然的想到,是否可以优化学习率 η

答案是肯定的,这种算法称为自适应梯度算法「AdaGrad【Adapt+Gradient】」,基本思想是开始时快速移动接近目标,然后减速提高精度「梯度对其产生的动力效果逐渐减弱」。实现时记录每次梯度下降的梯度平方和 h,将 η 除以 h 进行调整「为防止分母出现 0,往往加上一个较小的常量」

code:

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

inline long double f(long double x)
{
    return 3*x*x*x*x-x*x*x+2*x*x-9*x+5*sqrt((x+3)*(x+3)+(5*x+6)*(5*x+6))-25;
}

inline long double numerical_diff(long double x)
{
    long double dx=1e-6;
    return (f(x+dx)-f(x-dx))/(dx*2);
}

inline long double AdaGrad(long double x,long double eta,long double beta)//自适应梯度算法
{
    long double v=0,h=0,grad=numerical_diff(x),eps=1e-6;
  	//int cnt=0;
    while(abs(grad)>eps)
    {
        h+=grad*grad;
        v=beta*v-eta*grad/(sqrt(h)+eps);
        x+=v;
      	grad=numerical_diff(x);
        //cnt++;
    }
    //cout<<cnt<<endl;
    return f(x);
}

signed main()
{
    cout<<AdaGrad(0,0.5,0.05)<<endl;
    return 0;
}

迭代次数:12

「多峰函数的优化效果较明显」


优化——自适应动量算法「Adam」

自适应梯度算法的缺陷:若初始点离极值点较远,可能还没到极值点,小球就跑不动了「相应的有 RMSProp 优化算法,通过把 h 乘以一个衰减率 β2 来逐步遗忘之前的梯度,专业一点说是“指数移动平均”,与动量梯度下降法有异曲同工之妙,此处不作详细介绍」

我们也可以将动量梯度下降法「MGD」与自适应梯度算法「AdaGrad」融合在一起,得到自适应动量算法「Adaptive Momentum Estimation,简称 Adam」

在梯度下降过程中,Adam 算法用梯度模拟小球受力改变速度 v,同时会增大小球的质量而改变小球移动的难易程度,二者的作用最终影响移动距离。

code:

#include<iostream>
#include<cstdio>
#include<cmath>

using namespace std;

inline long double f(long double x)
{
    return 3*x*x*x*x-x*x*x+2*x*x-9*x+5*sqrt((x+3)*(x+3)+(5*x+6)*(5*x+6))-25;
}

inline long double numerical_diff(long double x)
{
    long double dx=1e-6;
    return (f(x+dx)-f(x-dx))/(dx*2);
}

inline long double Adam(long double x,long double eta,long double beta1,long double beta2)
{
    long double v=0,m=0,grad=numerical_diff(x),t,t1=beta1,t2=beta2,eps=1e-6;
    //int cnt=0;
    while(abs(grad)>eps)
    {
        t=eta*sqrt(1-t2)/(1-t1);
        v+=(1-beta1)*(grad-v);
        m+=(1-beta2)*(grad*grad-m);
        x-=t*v/(sqrt(m)+eps);
        t1*=beta1,t2*=beta2;
        grad=numerical_diff(x);
        //cnt++;
    }
    //cout<<cnt<<endl;
    return f(x);
}

signed main()
{
    cout<<Adam(0,0.5,0.6,0.9999)<<endl;
    return 0;
}

迭代次数:49


综合来看,AdaGrad 的算法似乎更具优势,但毕竟各个算法特点不同,适用的函数也不同,用哪种算法还是应根据实际应用而定。


二维写法

code:

struct node{
    long double x,y;
};

inline long double f(long double x,long double y)
{
    return ...;
}

inline long double part_x(long double x,long double y)
{
    long double dx=1e-6;
    return (f(x+dx,y)-f(x-dx,y))/(dx*2);
}

inline long double part_y(long double x,long double y)
{
    long double dy=1e-6;
    return (f(x,y+dy)-f(x,y-dy))/(dy*2);
}

inline node gd(long double x,long double y,long double eta,long double beta)
{
    long double eps=1e-6;
    node res;
    int cnt=0,T=100;
    while(T--&&abs(part_x(x,y))+abs(part_y(x,y))>eps)
    {
        x-=eta*part_x(x,y);
        y-=eta*part_y(x,y);
        //cout<<x<<' '<<y<<' '<<f(x,y)<<' '<<++cnt<<endl;
    }
    res.x=x,res.y=y;
    return res;
}

inline node mgd(long double x,long double y,long double eta,long double beta)
{
    long double vx=0,vy=0,eps=1e-6;
    node res;
    int cnt=0,T=100;
    while(T--&&abs(part_x(x,y))+abs(part_y(x,y))>eps)
    {
        vx=beta*vx-eta*part_x(x,y);
        vy=beta*vy-eta*part_y(x,y);
        x+=vx,y+=vy;
        //cout<<x<<' '<<y<<' '<<f(x,y)<<' '<<++cnt<<endl;
    }
    res.x=x,res.y=y;
    return res;
}

inline node AdaGrad(long double x,long double y,long double eta,long double beta)
{
    long double vx=0,vy=0,hx=0,hy=0,gradx=part_x(x,y),grady=part_y(x,y),grad=abs(gradx)+abs(grady),eps=1e-6;
    node res;
    int cnt=0,T=100;
    while(T--&&grad>1e-5)
    {
        hx+=gradx*gradx;
        hy+=grady*grady;
        vx=beta*vx-eta*gradx/(sqrt(hx)+eps);
        vy=beta*vy-eta*grady/(sqrt(hy)+eps);
        x+=vx,y+=vy;
        gradx=part_x(x,y);
        grady=part_y(x,y);
        grad=abs(gradx)+abs(grady);
        //cout<<x<<' '<<y<<' '<<f(x,y)<<' '<<++cnt<<endl;
    }
    res.x=x,res.y=y;
    return res;
}

inline node Adam(long double x,long double y,long double eta,long double beta1,long double beta2)
{
    long double vx=0,vy=0,mx=0,my=0,gradx=part_x(x,y),grady=part_y(x,y),grad=abs(gradx)+abs(grady),t,t1=beta1,t2=beta2,eps=1e-7;
    node res;
    int cnt=0,T=100;
    while(T--&&grad>1e-5)
    {
        t=eta*sqrt(1-t2)/(1-t1);
        vx+=(1-beta1)*(gradx-vx);
        vy+=(1-beta1)*(grady-vy);
        mx+=(1-beta2)*(gradx*gradx-mx);
        my+=(1-beta2)*(grady*grady-my);
        x-=t*vx/(sqrt(mx)+eps);
        y-=t*vy/(sqrt(my)+eps);
        t1*=beta1,t2*=beta2;
        gradx=part_x(x,y);
        grady=part_y(x,y);
        grad=abs(gradx)+abs(grady);
        //cout<<x<<' '<<y<<' '<<f(x,y)<<' '<<++cnt<<endl;
    }
    res.x=x,res.y=y;
    return res;
}

本题代码

回到此题,直接用 Adam 交了一发,AC~「可能运气较好……

7

code:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<climits>
#include<cstdlib>
#include<ctime>
#define Min(a,b,c,d,e) (min(a,min(b,min(c,min(d,e)))))

using namespace std;

long double P,Q,R,ans=INT_MAX;
long double c1,c2,c3,c4,c5,c6,c7,c8,c9;
long double x,y;

struct node{
    long double x,y;
}A,B,C,D,M,N,t;

inline long double dis(node a,node b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

inline long double f(long double x,long double y)
{
    return c1*x+c2*y+c3*sqrt((c4*x+c5*y+c6)*(c4*x+c5*y+c6)+(c7*x+c8*y+c9)*(c7*x+c8*y+c9));
}

inline long double part_x(long double x,long double y)
{
    long double dx=1e-6;
    return (f(x+dx,y)-f(x-dx,y))/(dx*2);
}

inline long double part_y(long double x,long double y)
{
    long double dy=1e-6;
    return (f(x,y+dy)-f(x,y-dy))/(dy*2);
}

inline node Adam(long double x,long double y,long double eta,long double beta1,long double beta2)
{
    long double vx=0,vy=0,mx=0,my=0,gradx=part_x(x,y),grady=part_y(x,y),grad=abs(gradx)+abs(grady),t,t1=beta1,t2=beta2,eps=1e-6;
    node res;
    int T=20;
    while(T--&&grad>1e-4)
    {
        t=eta*sqrt(1-t2)/(1-t1);
        vx+=(1-beta1)*(gradx-vx);
        vy+=(1-beta1)*(grady-vy);
        mx+=(1-beta2)*(gradx*gradx-mx);
        my+=(1-beta2)*(grady*grady-my);
        x-=t*vx/(sqrt(mx)+eps);
        y-=t*vy/(sqrt(my)+eps);
        t1*=beta1,t2*=beta2;
        gradx=part_x(x,y);
        grady=part_y(x,y);
        grad=abs(gradx)+abs(grady);
    }
    res.x=x,res.y=y;
    return res;
}

inline long double Rand()
{
    return abs(rand()*rand()*rand()%(10000000)/(long double)(10000000));
}

inline void gradient()
{
    for(int T=1;T<=200000;T++)
    {
        t=Adam(Rand(),Rand(),Rand(),Rand(),Rand());
        if(t.x>=0&&t.x<=1&&t.y>=0&&t.y<=1) ans=min(ans,f(t.x,t.y));
    }
}

signed main()
{
    srand(time(0));
    scanf("%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf",&A.x,&A.y,&B.x,&B.y,&C.x,&C.y,&D.x,&D.y,&P,&Q,&R);
    c1=dis(A,B)/P,c2=dis(C,D)/Q,c3=1/R;
    c4=B.x-A.x,c5=D.x-C.x,c6=A.x-D.x,c7=B.y-A.y,c8=D.y-C.y,c9=A.y-D.y;
    gradient();
    ans=Min(f(0,0),f(0,1),f(1,0),f(1,1),ans);
    printf("%.2Lf\n",ans);
    return 0;
}



三分套三分法

突然发现三分套三分跑的贼快,于是也打算简单实现一下。

模板就不讲了,直接贴一下之前的博客:「二分法&三分法模板」

上代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<climits>
#define Min(a,b,c,d,e) (min(a,min(b,min(c,min(d,e)))))

using namespace std;

long double P,Q,R,ans=INT_MAX,eps=1e-6;
long double c1,c2,c3,c4,c5,c6,c7,c8,c9;
long double x,y;

struct node{
    long double x,y;
}A,B,C,D,M,N,t;

inline long double dis(node a,node b)
{
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}

inline long double f(long double x,long double y)
{
    return c1*x+c2*y+c3*sqrt((c4*x+c5*y+c6)*(c4*x+c5*y+c6)+(c7*x+c8*y+c9)*(c7*x+c8*y+c9));
}

inline long double g(long double x)
{
    long double l=0,r=1,mid;
    while(r-l>eps)
    {
        mid=(l+r)/2;
        if(f(x,mid-eps)<f(x,mid+eps)) r=mid;
        else l=mid;
    }
    return f(x,mid);
}

signed main()
{
    scanf("%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf%Lf",&A.x,&A.y,&B.x,&B.y,&C.x,&C.y,&D.x,&D.y,&P,&Q,&R);
    c1=dis(A,B)/P,c2=dis(C,D)/Q,c3=1/R;
    c4=B.x-A.x,c5=D.x-C.x,c6=A.x-D.x,c7=B.y-A.y,c8=D.y-C.y,c9=A.y-D.y;
    long double l=0,r=1,mid;
    while(r-l>eps)
    {
        mid=(l+r)/2;
        if(g(mid-eps)<g(mid+eps)) r=mid;
        else l=mid;
    }
    printf("%.2Lf\n",g(mid));
    return 0;
}

Thanksforreading.
————THE——END————
posted @   凌云_void  阅读(234)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示