三分查找模板和例题
通过左右三分定出两个点,判断两点的高低,然后舍掉三分之一的距离,寻找凸函数的极值点。
- 如果
lmid
和rmid
在最大(小)值的同一侧: 那么由于单调性,一定是二者中较大(小)的那个离最值近一些,较远的那个点对应的区间不可能包含最值,所以可以舍弃。 - 如果在两侧: 由于最值在二者中间,我们舍弃两侧的一个区间后,也不会影响最值,所以可以舍弃。
例题1:P3382 【模板】三分法
题意:三分模板题,求n次函数在[l,r]的极值点,保留5位小数,保证答案存在。
(这道题的题解很有趣,什么鬼都出来了,明明是三分模板,偏偏有很多大佬要凑热闹,粒子群算法、求导+二分、模拟退火算法、黄金分割法、梯度下降、四分等,留下了不学无术的泪水)
#include<stdio.h> #include<iostream> #include<algorithm> #include<cstring> #include<math.h> #include<string> #include<map> #include<queue> #include<stack> #include<set> #include<ctime> #define ll long long #define inf 0x3f3f3f3f const double pi=3.1415926; using namespace std; double a[15]; int n; double l,r; double cal(double x){///计算n次函数在x点的值 double res=0; for(int i=n;i>=0;i--) res+=a[i]*pow(x, i); return res; } int main() { scanf("%d%lf%lf",&n,&l,&r); for(int i=n;i>=0;i--) scanf("%lf",&a[i]); double eps=0.0000001;///减小误差 while(fabs(r-l)>=eps){ double m=(r-l)/3;///三分距离 double lm=l+m;///左三分点 double rm=r-m;///右三分点 if(cal(lm)<cal(rm)) l=lm; else r=rm; } printf("%.5f\n",l); return 0; }
另外,不知道为什么Java的代码全部超时,代码复制得一模一样,请指教。
import java.util.*; public class Main{ static double[] a=new double[15]; static int n; static double eps=0.0000001; public static void main(String[] args) { Scanner scan=new Scanner(System.in); n=scan.nextInt(); double l=scan.nextDouble(); double r=scan.nextDouble(); for(int i=n;i>=0;i--) a[i]=scan.nextDouble(); double ans=0; while(Math.abs(r-l)>eps) { double m=(r-l)/3; double lm=l+m;; double rm=r-m;; if(cal(lm)<=cal(rm)) l=lm; else r=rm; } System.out.printf("%.5f\n",l); } public static double cal(double x) { double res=0; for(int i=n;i>=0;i--) res+=a[i]*Math.pow(x, i); return res; } }
例题2:牛客练习赛59C装备合成
题意:有x件材料a和y件材料b,2件材料a和3件材料b可以做一件装备A,4件材料a和1件材料b可以做一件装备B,求最多能做多少装备。1<=a,b<=1e9
思路:假设装备A做了a件,装备B则是min( (x-2a)/4,y-3a ),答案ans=a+min( (x-2a)/4,y-3a )。大佬们说:不难看出是凸函数,然后三分查找。
赛后补题的,题解说是三分那就是三分,大佬说打表就能看出是三分,可能是题做多了就有经验吧,代码附带打表函数可以像大佬们一样测试。
偶然发现的坑:三分三分,像我一样的初学者设置while条件就喜欢设成r-l<=3,刚好可以被3除,皆大欢喜。但是对于整数,要考虑取整问题,如果这样设while条件,在if判断中加一个等号和不加等号会导致结果不一样,可以修改代码测试一下,详看代码。为了保证准确性,最终出来的l和r最好是一段稍微大一点的区间,再去遍历求极值,稳!
import java.util.*; public class Main{//牛客练习59C static int x,y; public static void main(String []args) { Scanner scan=new Scanner(System.in); /* 大佬们说的打表,还真是这么回事 dabiao(300, 300); dabiao(60,10); */ int t=scan.nextInt(); while(t!=0) { t--; x=scan.nextInt(); y=scan.nextInt(); int l=0,r=Math.min(x/2, y/3); while(r-l>=9) {//这里的差值不要用3,用稍微大一点的区间 int m=(r-l)/3; int lm=l+m; int rm=r-m; //System.out.printf("lm=%d cal(lm)=%d rm=%d cal(rm)=%d\n",lm,cal(lm),rm,cal(rm)); if(cal(lm)<cal(rm))//把while设成r-l>=3后加不加等号第四组数据会出错。 l=lm; else r=rm; } //System.out.printf("l=%d r=%d\n",l,r); int ans=0; for(int i=l;i<=r;i++) //上面只是锁定了一个区间,没能找到最值,左右区间不超过3,再遍历一下 ans=Math.max(ans, cal(i)); System.out.println(ans); } } public static int cal(int a) { int res=a+Math.min( (x-2*a)/4, y-3*a); return res; } public static void dabiao(int x,int y) {//大佬说的打表 //从做0件A装备到全部做A装备 System.out.printf("打表x=%d y=%d\n",x,y); for(int i=0;i<Math.min(x/2, y/3);i++) { int b=Math.min( (x-2*i)/4, y-3*i); System.out.printf("a=%d b=%d sum=%d\n",i,b,i+b); } System.out.println(); } }