题目链接:https://www.luogu.org/problem/P5515
题目背景
注: 该背景部分改编自 disangan233 中考前买计算器的真实事件。
博丽 灵梦 (Hakurei Reimu) 在成功抢回八云 紫 (Yakumo Yukari) 用隙间偷走的香火钱后,她和依神 紫苑 (Yorigami Shion) 去香霖堂买东西啦!
灵梦想买一个计算器来计算神社的香火钱,但是因为香霖堂的东西太贵了,她选择使用河童重工网络 (Kawashiro Nitori's Network,KNN) 网购一个 Casio 计算器。
但出人意料的是,灵梦使用 KNN 买回来的 Casio 是个假货,最多只能显示整数部分(即向下取整)。
灵梦很苦恼,因为这个计算器可能会导致一些特别大的误差。所以灵梦想让拥有外界的式神(指电脑)的你帮她解决一个问题。
题目描述
灵梦得到了3个实数 n ,a ,b ( 4≤n≤5,5≤a,b≤10 ) ,她成功地计算了 ,得到了一个只显示整数部分的结果。
灵梦想知道,若存在一个实数 n′(n′≥0),使得 的结果在计算器上与
的结果显示出来完全一致时,n′ 的变化范围,即 n′ 的最大值与最小值之差。
如果你不知道如何计算 ,请使用
cmath
库的pow()
函数,pow(n,k)
的结果即为 的结果。
为了提高本题的难度,灵梦给你设置了 T 组询问。而为了在某种程度上减少你的输入和输出量,我们采用以下的代码来生成询问(代码来自河童重工):
namespace Mker
{
// Powered By Kawashiro_Nitori
// Made In Gensokyo, Nihon
#define uint unsigned int
uint sd;int op;
inline void init() {scanf("%u %d", &sd, &op);}
inline uint uint_rand()
{
sd ^= sd << 13;
sd ^= sd >> 7;
sd ^= sd << 11;
return sd;
}
inline double get_n()
{
double x = (double) (uint_rand() % 100000) / 100000;
return x + 4;
}
inline double get_k()
{
double x = (double) (uint_rand() % 100000) / 100000;
return (x + 1) * 5;
}
inline void read(double &n,double &a, double &b)
{
n = get_n(); a = get_k();
if (op) b = a;
else b = get_k();
}
}
在调用 Mker::init()
函数之后,你第 i 次调用 Mker::read(n,a,b)
函数后得到的便是第 i 次询问的 ,
和
。
为了减少你的输出量,令第 i 次询问的答案为 ,你只需要输出
。如果你的答案与标准答案的绝对误差在
以内,你的答案则被视为是正确答案。
本题数据的生成采用时间复杂度远远劣于普通算法的高 (da) 精 (bao) 度 (li) 算法来保证精度,本题数据保证单次询问的误差小于 ,所以本题的SPJ范围对于正解来说是完全足够的。
为了让你更好地做题,这里给出了关于 op 的说明:
- 当 op=1 时,有 a=b,否则无特殊限定。
输入格式
输入共一行,包含 3 个正整数 T,seed,op,含义见题目描述。
输出格式
输出共一行,输出题目描述中要求输出的答案。
先看一眼题目。
题目大意是:给出,求
,使得
,且不存在
或
,使得
成立。
换句话说,假设,
,那么
。
Solution 1 二分
发现这个是个单调函数!所以直接二分分别求出
就可以了!
#include<cstdio>
#include<cmath>
using namespace std;
namespace Mker
{
// Powered By Kawashiro_Nitori
// Made In Gensokyo, Nihon
#define uint unsigned int
uint sd;int op;
inline void init() {scanf("%u %d", &sd, &op);}
inline uint uint_rand()
{
sd ^= sd << 13;
sd ^= sd >> 7;
sd ^= sd << 11;
return sd;
}
inline double get_n()
{
double x = (double) (uint_rand() % 100000) / 100000;
return x + 4;
}
inline double get_k()
{
double x = (double) (uint_rand() % 100000) / 100000;
return (x + 1) * 5;
}
inline void read(double &n,double &a, double &b)
{
n = get_n(); a = get_k();
if (op) b = a;
else b = get_k();
}
}
int main()
{
double ans=0;
int t;
scanf("%d",&t);
Mker::init();
for (int i=1;i<=t;i++)
{
double n,a,b;
Mker::read(n,a,b);
int tot=floor(pow(n,a)+pow(n,b));
double r=20;
double l=n;
while (l<r-0.000000001)
{
double mid=(l+r)/2;
if (tot<floor(pow(mid,a)+pow(mid,b))) r=mid; else l=mid;
}
double m=l;
l=0;
r=n;
while (l<r-0.000000001)
{
double mid=(l+r)/2;
if (tot>floor(pow(mid,a)+pow(mid,b))) l=mid; else r=mid;
}
ans+=m-l;
}
printf("%0.10lf",ans);
return 0;
}
然后发现只有。
Solution 2 不用迭代的牛顿迭代法
分析一下二分的时间复杂度,假设精度为,那么显然时间复杂度为
,会超时。
考虑牛顿迭代,因为,那么只要用牛顿迭代求出
和
的解,分别代入
即可。
由于我们可以目力观测出必定分别在
左右很小的范围内,所以只要把估值设为
,进行一次迭代即可,而不需要多次迭代。
#include<cstdio>
#include<cmath>
using namespace std;
int t;
double n,a,b;
namespace Mker
{
// Powered By Kawashiro_Nitori
// Made In Gensokyo, Nihon
#define uint unsigned int
uint sd;int op;
inline void init() {scanf("%u %d", &sd, &op);}
inline uint uint_rand()
{
sd ^= sd << 13;
sd ^= sd >> 7;
sd ^= sd << 11;
return sd;
}
inline double get_n()
{
double x = (double) (uint_rand() % 100000) / 100000;
return x + 4;
}
inline double get_k()
{
double x = (double) (uint_rand() % 100000) / 100000;
return (x + 1) * 5;
}
inline void read(double &n,double &a, double &b)
{
n = get_n(); a = get_k();
if (op) b = a;
else b = get_k();
}
}
int main()
{
scanf("%d",&t);
Mker::init();
double ans=0;
for (int i=1;i<=t;i++)
{
Mker::read(n,a,b);
int k=pow(n,a)+pow(n,b);
double r=n;
r-=(pow(r,a)+pow(r,b)-double(k+1))/(a*pow(r,a-1)+b*pow(r,b-1));
ans+=r;
r=n;
r-=(pow(r,a)+pow(r,b)-double(k))/(a*pow(r,a-1)+b*pow(r,b-1));
ans-=r;
}
printf("%.3lf",ans);
return 0;
}
按理说,单次询问复杂度为,那么用牛顿迭代总时间复杂度为
,应该是可以通过的。
但实际上,这只能得,原因是
函数比较慢,使得常数较大。
通过特殊的卡常技巧可以通过此题,当然也可以进一步推导来寻求更简单的方法。
Solution 3 用牛顿迭代推出结论
既然只用迭代一次,那么我们可以直接得到的公式:
那么单次询问答案就为。
把所有答案加起来即可,时间复杂度为,可以得到全部分数。
#include<cstdio>
#include<cmath>
using namespace std;
int t;
double n,a,b;
namespace Mker
{
// Powered By Kawashiro_Nitori
// Made In Gensokyo, Nihon
#define uint unsigned int
uint sd;int op;
inline void init() {scanf("%u %d", &sd, &op);}
inline uint uint_rand()
{
sd ^= sd << 13;
sd ^= sd >> 7;
sd ^= sd << 11;
return sd;
}
inline double get_n()
{
double x = (double) (uint_rand() % 100000) / 100000;
return x + 4;
}
inline double get_k()
{
double x = (double) (uint_rand() % 100000) / 100000;
return (x + 1) * 5;
}
inline void read(double &n,double &a, double &b)
{
n = get_n(); a = get_k();
if (op) b = a;
else b = get_k();
}
}
int main()
{
scanf("%d",&t);
Mker::init();
double ans=0;
for (int i=1;i<=t;i++)
{
Mker::read(n,a,b);
ans+=1/(a*pow(n,a-1)+b*pow(n,b-1));
}
printf("%.3lf",ans);
return 0;
}
(你问我为什么贴了这么多代码?没错,上面就是我的心(ti)路(jiao)历程)
Solution 4 数学推导
这是在题解里看到的,非常有意思。
在Solution 2中,我们已经知道与
十分接近,因此我们可以把
的图像画出来,把
段看成直线。
为什么可以看成直线呢?
如果你是高中生,应该在数学课上学定积分时学过“化曲为直”的思想,当时为了方便计算“曲边梯形”的面积,我们在很小的一个区间内把曲线看成直线从而转换为普通的梯形求其面积。
当然上述“化曲为直”的依据是区间无限小,在这里,我们的区间只能说是很小,但并不是无限小。
那么在本题中,“化曲为直”依据是什么呢?
精度。
题目的精度要求不高,因此可以粗略地把曲线看成直线。(当然即使这样,这种方法还是比较冒险,我个人不建议用)
那么我们可以发现在段,构成了一个直角三角形,其中
.
那么由三角函数知识,在图中的直角三角形内,。
时间复杂度为;实际上推出结论与Solution 4一致。