题目大意:只允许使用加、取反(添负号)、偏移(加减一个常数)、左右移位(乘或除以 22 的非负整数次幂)和神奇的 �(�)S(x) 函数来进行编程,造一台计算机,构造一个计算网络,完成 1010 项指定的计算任务。要求计算次数尽可能少。
其中 �(�)S(x) 函数为 11+�−�1+e−x1 ,是一个非线性的函数。
�(�)S(x) 函数全名是 SigmoidSigmoid 函数,又称 “�S 型生长曲线” 。
�=�(�)y=S(x) 的图像:
还有三个功能强大但是使用了会扣分的计算节点:比较节点、取两数最大值节点、乘法节点。
是一道提交答案题。
题目补充。
checker下载。
解法:构造答案
具体解法在各个 tasktask 中介绍。
本题主要就是 1010 个计算网络的构造。
核心思想就是:利用好 �(�)S(x) 的选择性。用 �(�)S(x) 来实现比较大小、取 00 取 11 ,制造分段函数,是这道题的关键所在。
必要的地方加一些小优化,以减少计算节点数。
本题解对解题过程中所用到的所有参数进行了我自认为比较详细地分析,希望这些分析能帮到阅读本题解的你。
task1task1
要求
输入:�,�a,b 。
输入限制:∣�∣,∣�∣⩽109∣a∣,∣b∣⩽109 ,小数部分不超过 99 位。
输出:−2�−2�−2a−2b 。
满分行数:66 行。
解决方法
送分点。
一般来说,我们都会想到提取公因数 −2−2 来减少运算次数。
一般来说,我们也都会想到用左移 11 位来计算乘 22 这个操作。
输出代码
if(n==1) { // 6行。
printf("I\n");
printf("I\n");
printf("+ 1 2\n");
printf("< 3 1\n");
printf("- 4\n");
printf("O 5");
}
task2task2
要求
输入:�a 。
输入限制:∣�∣⩽109∣a∣⩽109 ,小数部分不超过 99 位。
输出:11+�17�1+e17a1 。
满分行数:66 行。
解决方法
也是送分点。
观察输出的这个表达式的样子,显然这个点是要用 �(�)S(x) 函数的。
然后,注意到 17�=16�+�=24�+�17a=16a+a=24a+a ,所以我们可以用位移和加法来解决乘法了。
最后还得小心,�(�)=11+�−�S(x)=1+e−x1 ,还得将 17�17a 取反后使用 �S 函数。
输出代码
if(n==2) { // 6行。
printf("I\n");
printf("< 1 4\n");
printf("+ 1 2\n");
printf("- 3\n");
printf("S 4\n");
printf("O 5");
}
task3task3
要求
输入:�a 。
输入限制:∣�∣⩽109∣a∣⩽109 ,小数部分不超过 99 位。
输出:sign(�)={−1,�<0 0,�=0 1,�>0sign(a)=⎩⎪⎪⎨⎪⎪⎧−1,a<0 0,a=0 1,a>0 。
满分行数:66 行。
解决方法
不是送分了。开始有难度了。
没有除法,所以 �∣�∣∣a∣a 泡汤了。况且求 ∣�∣∣a∣ 是下一个 tasktask 的任务,所以按道理说应该并不容易求。
使用比较语句的话,直接与 00 做比较,输出比较的值即可。可得 66 分。
可以用 �×2−1000a×2−1000 来造 00 。之后就可以用比较节点比较 �a 和 00 的大小。
但是如何使用基本操作来搞出答案呢?
我们是该想想怎么用 �S 函数了。
回顾题目
其实,我们在读题面过程中,会发现计算过程中数据精度特别高:小数点后 9090 位,整数部分绝对值不超过 101000101000。
而最终
Special Judge
判定结果时,只要与标准答案的结果相差小于 10−910−9 即可。
为什么给如此大的精度?尤其是整数部分,为何那么大?小数部分似乎也精确得过分了,这又是为什么?
而且,为什么 �S 函数一定要用 11+�−�1+e−x1 ?它有什么特殊之处吗?
观察发现,除了第二个测试点需要直接使用了 �(�)S(x) 来求出 11+�17�1+e17a1 精确值之外,其余九个测试点表面上似乎都没有涉及到 �(�)S(x) 的使用。
出题人如果只是为了送分的话,为什么还要搞这么一个函数出来呢?这个函数真的没有其他作用了吗?
仔细想想,我们察觉到,好像确实没有哪个测试点需要使用 �(�)S(x) 的精确值了。所以,�(�)S(x) 的价值,难道是体现在其近似值上面了吗?
种种不同寻常的地方提醒着我们,这道题必然要涉及到极限的问题,即我们要通过某些操作,爆掉 9090 位小数的精度,以此模拟取极限的过程。而 �S 函数的特殊之处就在于,它在正负无穷大处都有极限,分别为 11 和 00 ,而 �=�(�)y=S(x) 图像上有唯一一个横纵坐标都是有理数的点 (0,12)(0,21) ,这也是 �(�)S(x) 的性质。进一步的, �(�)+�(−�)=1S(x)+S(−x)=1 ,同样是一条性质。
这就启示我们结合 lim�→+∞�(�)=1x→+∞limS(x)=1 , lim�→−∞�(�)=0x→−∞limS(x)=0 ,和 �(0)=12S(0)=21 这三条性质,来完成题目。
�(�)S(x) 的两个极限: 11 和 00 ,恰好对应了“真”和“假”两种状态。这个性质,某种意义上而言,是实现“比较”操作的方法,能够用来判别正负。
所以 �S 函数与极限结合,应该是这道题考察的重点之一。
怎么利用 �S 函数呢?
既然要考虑极限,我们很容易想到,将 �S 函数乘 22 ,再减去 11,所得恰好是个在 +∞,−∞+∞,−∞ 处极限分别为 +1,−1+1,−1 的函数(即 21+�−�−11+e−x2−1)。而且在 �=0x=0 处,取值也恰好为 00 。
这么看来,我们应该构造对了。
那么如何取极限?
简单,左移个百来位就可以变得很大了,只要 �−�e−x 爆了 9090 位小数的精度,就能认为 �(�)S(x) 与 ±1±1 精确相等了。
大概多少位比较合适呢?
-
+∞+∞ 处的极限:
由 11+�−�<1−5×10−911+e−x1<1−5×10−91 ,
得 �−�<11−5×10−91−1e−x<1−5×10−911−1,
因为,当 ∣�∣<1∣x∣<1 时,根据等比数列求和公式和极限理论,有 11−�=1+�+�2+�3+⋯=∑�=0∞��>1+�1−x1=1+x+x2+x3+⋯=i=0∑∞xi>1+x 。这是因为 �2x2 必然是正数,而且比 �3x3 大,同理 �4x4 是正数,比 �5x5 大,以此类推,可知 11−�>1+�1−x1>1+x 成立。
所以 11−5×10−91−1>5×10−911−5×10−911−1>5×10−91 ,
那么,只需要 �−�<5×10−91e−x<5×10−91 即可。
得 −�<ln(5×10−91)=ln5−91ln10−x<ln(5×10−91)=ln5−91ln10 ,
于是 �>91ln10−ln5≈207.926x>91ln10−ln5≈207.926 ,
而 �a 最小是 10−910−9 ,只需解 2�×10−9>2082k×10−9>208 ,解得 �k 即可。
那么,�>log2(208×109)≈37.598k>log2(208×109)≈37.598 。
所以 �⩾38k⩾38 。
-
−∞−∞ 处的极限:
由 21+�−�<5×10−911+e−x2<5×10−91 ,
得 �−�>4×1090−1e−x>4×1090−1 , −�>ln(4×1090−1)−x>ln(4×1090−1) ,只需 −�>ln(4×1090)−x>ln(4×1090) ,
于是 �<−90ln10−ln4≈−208.619x<−90ln10−ln4≈−208.619 , �>log2(212×109)≈37.625k>log2(212×109)≈37.625 。
所以,左移 3838 位即可。
-
为什么不用偏移来制造无穷大呢?
因为偏移无法保持正负。
-
位移有上限,需解方程 2�×109<1010002k×109<101000 ,也就是 �<991log210≈3292.031k<991log210≈3292.031。所以 �=38k=38 没问题的。
输出代码
if(n==3) { // 6行。
printf("I\n");
printf("< 1 38\n");
printf("S 2\n");
printf("< 3 1\n");
printf("C 4 -1\n");
printf("O 5");
}
task4task4
要求
输入:�a 。
输入限制:∣�∣⩽109∣a∣⩽109 ,小数部分不超过 99 位。
输出:∣�∣∣a∣ ,即 �a 的绝对值。
满分行数:1414 行。
解决方法
��.�PS.a 乘 ����(�)sign(a) 是一种解决方法,可得 66 分。
我的思路和第三个点的差不多。
首先我想到,�=∣�∣y=∣x∣ 的图像关于 �y 轴对称,是个偶函数,而 �(�)+�(−�)=1S(x)+S(−x)=1 告诉我们,�(�)S(x) 图像关于 (0,12)(0,21) 对称。所以,怎么用 �(�)S(x) 搞出一个偶函数来?
�(�)S(x) 在正负无穷处函数变化率都趋于零,这启示我们使用导数。
经过验算,我发现,�′(�)=�−�(1+�−�)2S′(x)=(1+e−x)2e−x 是个偶函数,且 lim�→∞�′(�)=0x→∞limS′(x)=0 , �(0)=14S(0)=41 。
虽然这个导数同 ∣�∣∣x∣ 一样,是个偶函数;但是 ∣�∣∣x∣ 在 �x 趋于无穷时是无穷,而 �′(�)S′(x) 在 �x 趋于无穷时却趋于 00 。更糟的是,在 �=0x=0 的两侧,∣�∣∣x∣ 的表达式可写成不一样的形式,即 ∣�∣={ �,�>0 0,�=0−�,�<0∣x∣=⎩⎪⎪⎨⎪⎪⎧ x,x>0 0,x=0−x,x<0 ,而 �′(�)S′(x) 不能。
所以这样看来, �′(�)S′(x) 并不能很好的模拟 ∣�∣∣x∣ 函数。即使能够用 �′(�)S′(x) 构造,怕也免不了一些乘法或者比较的操作。
虽然用 �′(�)S′(x) 直接构造 ∣�∣∣x∣ 的思路失败了,但引入 �′(�)S′(x) 来辅助解题确实是一个良好的尝试。
况且,难道仅凭这一次尝试,就能断言 �′(�)S′(x) 毫无作用了吗?
事实上导数真的很有用。
我们用不了 �′(�)S′(x) 的全部,我们只考虑 �′(�)S′(x) 在一点处的取值。
在解决第三个点时,我们使用了 �(�)S(x) 的两个极限,那么做这个点时,不妨再考虑考虑,能不能使用 �(0)=12S(0)=21 这个性质。
由 �′(0)=14S′(0)=41 ,我们很容易写出 �(�)S(x) 在 �=0x=0 处的切线:�=14�+12y=41x+21 。
由切线的意义可知,�=�(�)y=S(x) 在 �=0x=0 附近与 �=14�+12y=41x+21 拟合得很好,
这样,我们就得到了一个关于 �x 的一次函数,且这个函数减去 1221 再乘 44 就能得到 �x 。
我们实际上得到的是一个从 �(�)S(x) 得到 �x 的办法,这个办法很重要。
其重要性在于,我们可以将获得 �x 的操作拆解为 �→�(�)→�x→S(x)→x 两个步骤,从而向这两个步骤中添加进一些我们想要的条件。
简言之,这个办法给了我们操作的空间,使得我们有了构造分段函数的可能。更本质的,使得我们有了构造条件语句的可能。
所以在这个测试点中,我们要从 �(�+∞)S(+∞x) 着手构造 ∣�∣∣x∣ (�(�+∞)S(+∞x) 这种写法不规范,但有助于理解)。
现在,再来考虑,如何将 �x 的正负区分开来。我们只有找到区分正负的办法,才可能构造出函数来模拟正负有别的 ∣�∣∣x∣ 函数。
区分正负的办法上面已经提到了,就是 �(�)S(x) 在正负无穷大处的两个极限,分别为 0,10,1 。
那么 �(−�)S(−x) 的极限就分别为 1,01,0 。
我们要想办法让这两个截然不同的极限对我们构造的函数产生影响,使得我们构造的函数在 �x 正负符号不同时具有显著的差别。
设这个影响为 �t ,把这个影响加到 −�+∞+∞−x 上去(这就是对我们构造的函数产生影响的操作过程)。现在考虑 �(−�+∞+�)S(+∞−x+t) 。
我们需要让这个影响在 �>0x>0 时为无穷小(即:几乎没什么影响),在 �<0x<0 时足够大,大到能显著影响 �(−�+∞+�)S(+∞−x+t) 的值。
所谓显著影响 �(−�+∞+�)S(+∞−x+t) 的值,无非就是把 �(−�+∞)S(+∞−x) 的值从 1221 附近变成 11 ,就是说 �(−�+∞+�)→1S(+∞−x+t)→1 。而减去 1221 再乘 44 后,这个影响作用的结果就变成了“使 00 变成 22 ”。
这说明 �t 应该是个由 11 搞出来的无穷大。比方说 1×2641×264 (这里未必就是 6464 这个数,还可以更大,而且这里的 11 不是确切的数字 11 ,它只是表示一个趋近于 11 但永远不等于 11 的变量)。
我们把这个大小为 22 的影响稍微调整一下,通过位移调整到和原来的影响 �t 大小相当的地步。这样,我们就可以通过减去 �t 来消除多余的影响,来构造出一个在 �<0x<0 时取 00 ,�>0x>0 时取 �x 的函数。
即 {0,�<0�,�>0{0,x<0x,x>0 。
这下就好办了。让这个函数乘 22 ,再减去 �x ,就得到 ∣�∣∣x∣ 啦!
上面的思考太抽象了,我们来重新整理一下思路:
-
首先,计算 �=�(−�×2�)×2�={0 ,�>02�,�<0t=S(−x×2q)×2p={0 ,x>02p,x<0 ,
��.PS. 这是 �=�(�×241)×2178y=S(x×241)×2178 的图像:
这里就要求 �(−�×2�)S(−x×2q) 能够与 00 或 11 近似相等,
-
于是令 �=−10−9x=−10−9 ,
只需有 11+�−2�10−9>1−10−901+e−2q10−91>1−10−90 ,
利用上面的近似公式,可知 1−�−2�10−9<11+�−2�10−91−e−2q10−9<1+e−2q10−91 ,
只需要 1−�−2�10−9>1−10−901−e−2q10−9>1−10−90 ,
即 2�10−9>90ln102q10−9>90ln10 ,
即 �>log2(9×1010ln10)≈37.59q>log2(9×1010ln10)≈37.59 ,
所以 �⩾38q⩾38 。
-
再令 �=10−9x=10−9 ,
只需有 11+�2�10−9<10−901+e2q10−91<10−90 ,
即 �2�10−9>1090−1e2q10−9>1090−1 ,
只需 �2�10−9>1090e2q10−9>1090 ,
即 2�10−9>90ln102q10−9>90ln10 ,与上种情况同解,
所以 �⩾38q⩾38 。
这样,在跳蚤看来,�(−�×2�)S(−x×2q) 与 00 或 11 是精确相等的。
��.PS. 这是 �=�(�×241)y=S(x×241) 的图像:
-
-
然后,计算 �(−�2�+�)={14−�2�+12,�>01 ,�<0S(2k−x+t)=⎩⎪⎪⎨⎪⎪⎧412k−x+21,x>01 ,x<0 ,
声明:为了方便书写,以下所有表达式 �x 均表示取反后的 �x ,但表达式中表示条件的 �x 仍然为未取反的 �x 。
声明:为了方便书写,以下所有表达式 �x 均表示取反后的 �x ,但表达式中表示条件的 �x 仍然为未取反的 �x 。
声明:为了方便书写,以下所有表达式 �x 均表示取反后的 �x ,但表达式中表示条件的 �x 仍然为未取反的 �x 。
-
这里要求 �>0x>0 时, (14�2�+12)−�(�2�)<10−90(412kx+21)−S(2kx)<10−90 ,不妨取 �=109x=109 ,即 1094×2�+12−11+�−2−�109<10−904×2k109+21−1+e−2−k1091<10−90
为了方便书写,设 1092�=�2k109=t ,
则上式为 �4+12−11+�−�<10−904t+21−1+e−t1<10−90
即 �4−1−�−�2(1+�−�)<10−904t−2(1+e−t)1−e−t<10−90 ,
即 �2−��−1��+1<2×10−902t−et+1et−1<2×10−90 ,
因为 �>0t>0 时, ��−1��+1>���+1et+1et−1>et+1t ,
这里用到了不等式 ��>�+1et>t+1 ,该不等式可用导数的相关知识来证明。
所以只需 �2−���+1<2×10−902t−et+1t<2×10−90 ,
即 ���−12(��+1)<2×10−90t2(et+1)et−1<2×10−90 ,
因为 �>0t>0 时, 1(��+1)<12(et+1)1<21 ,
所以只需 �(��−1)4<2×10−904t(et−1)<2×10−90 ,即 �(��−1)<8×10−90t(et−1)<8×10−90 ,
还是利用 ��>�+1et>t+1 ,欲使上式成立,至少需要满足 (��−1)2<8×10−90(et−1)2<8×10−90 ,
解得 �<ln(22×10−45+1)t<ln(22×10−45+1) ,
(其实只是个必要条件)
把 �=1092�t=2k109 代入,得 1092�<ln(22×10−45+1)2k109<ln(22×10−45+1) ,
即 2�>109ln(22×10−45+1)2k>ln(22×10−45+1)109 ,
即 �>9log210−log2(ln(22×10−45+1))≈177.884k>9log210−log2(ln(22×10−45+1))≈177.884 ,
所以 �⩾178k⩾178 。
当然 �k 不能取太大,否则会造成 �2�2kx 的精度下降,只需 10−9>2�×10−9010−9>2k×10−90 ,解得 �⩽269k⩽269 。
而且,我所采用的放缩方式,其放缩幅度还是很大的,所以 178178 的下界很松。但上界 269269 是严格的。
-
这里还要求 �<0x<0 时, 11+�−2�>1−10−901+e−2p1>1−10−90 ,
只需 1−�−2�>1−10−901−e−2p>1−10−90 ,
即 �>log2(90ln10)≈7.695p>log2(90ln10)≈7.695 ,
至于 �2�2kx ,它很小,只要 2�2p 够大,它对 2�2p 的影响就不足以改变 �(�2�+�)S(2kx+t) 的值。
所以 �⩾8p⩾8 。
-
其实没必要要求计算所得的数在 9090 位小数内精确相等,只要保证计算结果的 99 位小数是正确的,就能解决这个问题。
而我保证了 9090 位小数范围内精确相等,也是为了避免之后左移操作造成极大的误差。所以也不无道理。
不过我的推理过程中有一些放缩幅度是较大的,并不能恰好取到阈值。而且我分析的时候并没有考虑小数点 9090 位后是四舍五入的。所以我的取值基本上能做到功过相抵。当然你想要考虑四舍五入也可以,这个的证明就要靠读者自己发挥啦,可以参考上面的过程。
我写这么一大篇数学推理就是为了告诉读者这些参数该如何取。每个参数都是有理论依据的,而不是随便取一些足够大或足够小的数就行。即便随便取参数会更快,但我更推荐从数学上得到验证。
-
接着,减去二分之一 �(�2�+�)−12={14�2�,�>012 ,�<0S(2kx+t)−21=⎩⎪⎪⎨⎪⎪⎧412kx,x>021 ,x<0 ,
-
再然后,乘 2�+22k+2 ,得 (�(�2�+�)−12)×2�+2={� ,�>02�+1,�<0(S(2kx+t)−21)×2k+2={x ,x>02k+1,x<0 ,
-
接着,减去 �t 的影响,得 (�(�2�+�)−12)×2�+2−�={� ,�>02�+1−�,�<0(S(2kx+t)−21)×2k+2−t={x ,x>02k+1−t,x<0
我们要求 2�+1−�=02k+1−t=0 ,而 �=2�,(�<0)t=2p,(x<0) ,所以得到等式 �+1=�k+1=p 。
所以,(�(�2�+�)−12)×2�+2−�={�,�>00,�<0(S(2kx+t)−21)×2k+2−t={x,x>00,x<0 ,
取 �=41,�=178,�=180q=41,k=178,p=180 ,得到的图像大致为(这里 �=�+2p=k+2 的原因见下文分析 ):
-
乘 22 再减去 �x ,得:
((�(�2�+�)−12)×2�+2−�)×2−�={� ,�>0−�,�<0((S(2kx+t)−21)×2k+2−t)×2−x={x ,x>0−x,x<0 。
-
减少计算节点,把 22 乘到里面去,得:
(�(�2�+�)−12)×2�+3−�×2−�={� ,�>0−�,�<0(S(2kx+t)−21)×2k+3−t×2−x={x ,x>0−x,x<0
-
不妨把 �p 再取大一点,使 �t 不用乘 22 ,
所以取 �=�+2p=k+2 。式子就变成了:
(�(�2�+�)−12)×2�+3−�−�={� ,�>0−�,�<0(S(2kx+t)−21)×2k+3−t−x={x ,x>0−x,x<0
得到 ∣�∣∣x∣ 啦。
这里面还有一个小问题,那就是 �t 在负数和零处的取值是不同的,具体而言 �=�(−�×2�)×2�≈{2� ,�<02�−1,�=00 ,�>0t=S(−x×2q)×2p≈⎩⎪⎪⎨⎪⎪⎧2p ,x<02p−1,x=00 ,x>0 ,这样的话,在 �=0x=0 处计算得到 (�(�2�+�)−12)×2�+3(S(2kx+t)−21)×2k+3 后,消除 �t 的影响时,需要减去 2�2t 才行。
但若是特判零的话,势必要增加很多节点,相当麻烦。
我们想到 ∣�∣={� ,�⩾0−�,�<0∣x∣={x ,x⩾0−x,x<0 ,所以我们能不能把 �=0x=0 和 �>0x>0 合并到一起运算呢(或者和 �<0x<0 合并到一起运算)?
可以的。
那么,怎么合并呢?
由于 �x 小数部分不超过 99 位,所以给 �x 加上一个很小的常数 �=10−10ε=10−10 即可。这样,当 �≠0x=0 时, �x 原本的符号不改变;�=0x=0 时,�x 变成了正数 10−1010−10。
相应的我们需要调整一下参数,
此时需要满足 �>log2(9×1011ln10)≈40.914q>log2(9×1011ln10)≈40.914 ,
所以 �⩾41q⩾41 。
�,�k,p 不用改变。
��.PS.
241.000=2,199,023,255,552241.000=2,199,023,255,552 ,
240.914≈2,071,768,581,099.5056016240.914≈2,071,768,581,099.5056016 。
差距还是蛮大的。
我们不妨取 �=41,�=178,�=180q=41,k=178,p=180 。
这样一写,发现至少要 1515 行,究其原因,是我们需要对 �,�x,t 各做一次取反。所以考虑不对 �,�x,t 取反,而对 (�(�2�+�)−12)×2�+3(S(2kx+t)−21)×2k+3 取反的写法。
声明到此结束。
令 �=�(�+�2�)×2�={2�,�⩾00 ,�<0t=S(2qx+ε)×2p={2p,x⩾00 ,x<0 ,我们容易得到,答案就是:
−(�(�2�+�)−12)×2�+3+�+�={� ,�⩾0−�,�<0−(S(2kx+t)−21)×2k+3+t+x={x ,x⩾0−x,x<0 ,
这样就只需要取一次反,恰好 1414 行。
输出代码
66 分代码,使用乘法节点:
if(n==4) { // 6分代码。
printf("I\n");
printf("< 1 38\n");
printf("S 2\n");
printf("< 3 1\n");
printf("C 4 -1\n");
printf("* 1 5\n");
printf("O 6");
}
99 分代码,1515 行:
if(n==4) { // 15行。
printf("I\n");
printf("C 1 0.0000000001\n");
printf("- 2\n");
printf("< 3 41\n");
printf("S 4\n");
printf("< 5 180\n");
printf("> 1 178\n");
printf("+ 7 6\n");
printf("S 8\n");
printf("C 9 -0.5\n");
printf("< 10 181\n");
printf("- 6\n");
printf("+ 12 11\n");
printf("+ 13 3\n");
printf("O 14");
}
以及满分代码,1414 行:
if(n==4) { // 14行。
printf("I\n");
printf("C 1 0.0000000001\n");
printf("< 2 41\n");
printf("S 3\n");
printf("< 4 180\n");
printf("> 1 178\n");
printf("+ 6 5\n");
printf("S 7\n");
printf("C 8 -0.5\n");
printf("< 9 181\n");
printf("- 10\n");
printf("+ 11 5\n");
printf("+ 12 1\n");
printf("O 13");
}
task5task5
要求
输入:�1,�2,⋯ ,�32a1,a2,⋯,a32
输入限制:�1,�2,⋯ ,�32∈{0,1}a1,a2,⋯,a32∈{0,1} 。
输出:把 �1,�2,⋯ ,�32a1,a2,⋯,a32 从左到右看成一个二进制整数,高位在左,低位在右,输出该整数的值。
满分行数:9595 行。
解决方法
终于,
熬过上题后,
再一次遇到送分点。
按照位数加权再相加即可。
只要弄对行数,就没有任何问题。
显然没有更优解。因为没有能合并的项。
秦九韶算法可减少乘法次数,但现在是做位移。
输出代码
if(n==5) { // 95行。
for(int i=1;i<=32;++i) printf("I\n");
for(int i=1;i<=31;++i) printf("< %d %d\n",i,32-i);
printf("+ 63 32\n");
for(int i=64;i<=93;++i) printf("+ %d %d\n",i,i-31);
printf("O 94");
}
task6task6
要求
输入:�a 。
输入限制:0⩽∣�∣<2320⩽∣a∣<232 ,�a 为整数。
输出:输出 3232 个整数,从高位到低位输出 �a 的二进制表示(不足 3232 位的在高位补 00)。
满分行数:190190 行。
解决方法
输出的是 �a 的二进制表示,肯定只包含 00 和 11 。
可以想到,这题又要用 �(�)S(x) 函数来搞出 00 和 11 。
用 �(�)S(x) 获得 00 和 11 要求自变量 �x 有正负两种取值,所以我们构造这个正负的取值。
考虑 �a 的 22 进制的最高位,即第 3131 位,很显然,若这一位为 11 ,则 �⩾231a⩾231 ;若这一位为 00 ,则 �<231a<231 ,于是我们减去 231231 就能搞出正负不同的取值了。
同样的,我们需要加上 10−1010−10 来保证 �(�)S(x) 不会恰好取到 1221 。
由此我们发现,�(�)S(x) 函数能够用来比较变量和某个常数之间的大小关系。
我们至少需要将减去 231231 后的数左移 4141 位以爆掉精度。这是 task4task4 中计算过的。
所以我们计算最高位时就输出 �((�−231)×241)S((a−231)×241) ,即 �(�×241−272)S(a×241−272) 。
我们得到 00 或 11 后,把它左移恰当位数后再从原数中减去,得到 �′a′ ,就可以继续对下一位搞出正负两种取值了。第二高位答案就是 �((�′−230)×241)=�(�′×241−271)S((a′−230)×241)=S(a′×241−271) 。
诶?还是乘 4141 哎!
于是我们发觉,可以一开始就将 �a 左移 4141 位,以减少计算节点数。
但这样搞就需要我们计算出 242242 到 272272 这 3131 个幂次方了。计算这些幂次方可以用高精(三个 ���� ����long long 拼起来就足够了),可以用 pow
函数,也可以用计算器来手动模拟压位高精。
当然最靠谱的方法就是用题目提供的 checker
来计算。写好计算节点,输入 11 ,让 checker
自己跑出来 242242 到 272272 。省时省力。
总之是可以预处理的。
总体思路大概就是这样了。
然后发现,我们可以把“加上 10−1010−10” 这个操作放到一开始,这样就不必每次循环都多使用一个计算节点了。
计算一下节点的总数,循环里至少需要 “�,�,�,−,<,+C,S,O,−,<,+” 六个节点,把最后一位拿出来直接处理的话,就只需要 3131 次循环。
满分行数是 190190 ,恰好是 31×6+431×6+4 ,所以每次循环用 66 个节点应该确实是正确的。
但我们实际操作一下就会发现,按照以上所说的做法,需要写 191191 行,能拿 99 分。
说明有一行可以被优化掉。
仔细想想,发现我们可以把“加上 10−1010−10” 这个操作放到 �C 操作里,每次减去的不要刚好是 22 的幂次,而是 22 的幂次减去 10−10×241≈219.902≈22010−10×241≈219.902≈220 ,这样就能减少最开始的偏移节点,从而将计算节点数减少到 190190 。
为什么是 22 的幂次减去 220220 而不是加上 220220 呢?
因为减去 22 的幂次后等于零意味着这一位本来是 11 ,所以我们应该让这一位经过 220220 调整完后是个正数。所以应该是 22 的幂次减去 220220 。
于是我们把偏移的参数调整一下,通通减去 220220 即可。
现在是 190190 行。
输出代码
99 分代码,191191 行:
if(n==6) { // 191行。
char s[32][25]={ "\0",
"4398046511104", // 2^42
"8796093022208", // 2^43
"17592186044416", // 2^44
"35184372088832", // 2^45
"70368744177664", // 2^46
"140737488355328", // 2^47
"281474976710656", // 2^48
"562949953421312", // 2^49
"1125899906842624", // 2^50
"2251799813685248", // 2^51
"4503599627370469", // 2^52
"9007199254740992", // 2^53
"18014398509481984", // 2^54
"36028797018963968", // 2^55
"72057594037927936", // 2^56
"144115188075855872", // 2^57
"288230376151711744", // 2^58
"576460752303423488", // 2^59
"1152921504606846976", // 2^60
"2305843009213693952", // 2^61
"4611686018427387904", // 2^62
"9223372036854775808", // 2^63
"18446744073709551616", // 2^64
"36893488147419103232", // 2^65
"73786976294838206464", // 2^66
"147573952589676412928", // 2^67
"295147905179352825856", // 2^68
"590295810358705651712", // 2^69
"1180591620717411303424", // 2^70
"2361183241434822606848", // 2^71
"4722366482869645213696", // 2^72
};
printf("I\n");
printf("C 1 0.0000000001\n");
printf("< 2 41\n");
for(int i=4,j=31;j>=1;--j,i+=6) {
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("O %d\n",i+1);
printf("- %d\n",i+2);
printf("< %d %d\n",i+3,41+j);
printf("+ %d %d\n",i+4,i-1);
}
printf("> 189 41\n");
printf("O 190");
}
满分,190190 行:
if(n==6) { // 190行。
char s[32][25]={ "\0",
"4398046510884", // 2^42-220
"8796093021988", // 2^43-220
"17592186044196", // 2^44-220
"35184372088612", // 2^45-220
"70368744177444", // 2^46-220
"140737488355108", // 2^47-220
"281474976710436", // 2^48-220
"562949953421092", // 2^49-220
"1125899906842404", // 2^50-220
"2251799813685028", // 2^51-220
"4503599627370249", // 2^52-220
"9007199254740772", // 2^53-220
"18014398509481764", // 2^54-220
"36028797018963748", // 2^55-220
"72057594037927716", // 2^56-220
"144115188075855652", // 2^57-220
"288230376151711524", // 2^58-220
"576460752303423268", // 2^59-220
"1152921504606846756", // 2^60-220
"2305843009213693732", // 2^61-220
"4611686018427387684", // 2^62-220
"9223372036854775588", // 2^63-220
"18446744073709551396", // 2^64-220
"36893488147419103012", // 2^65-220
"73786976294838206244", // 2^66-220
"147573952589676412708", // 2^67-220
"295147905179352825636", // 2^68-220
"590295810358705651492", // 2^69-220
"1180591620717411303204", // 2^70-220
"2361183241434822606628", // 2^71-220
"4722366482869645213476", // 2^72-220
};
printf("I\n");
printf("< 1 41\n");
for(int i=3,j=31;j>=1;--j,i+=6) {
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("O %d\n",i+1);
printf("- %d\n",i+2);
printf("< %d %d\n",i+3,41+j);
printf("+ %d %d\n",i+4,i-1);
}
printf("> 188 41\n");
printf("O 189");
}
task7task7
要求
输入:�,�a,b 。
输入限制:0⩽�,�<2320⩽a,b<232 , �,�a,b 均为整数。
输出:�,�a,b 按位异或后的结果。
满分行数:605605 行。
解决方法
因为是按位异或,所以必然需要把每一位都先取出来。这就用到 task6task6 的代码了。
而按位异或完得到的 3232 位二进制数还需要转化成十进制数才能输出,所以这又用到 task5task5 的代码了。
这就已经用掉大约 2×5×31+62=3722×5×31+62=372 个计算节点,满分是 605605 个计算节点,所以我们大约需要用 77 个计算节点来完成每一位上的异或操作。
异或还是涉及到 00 和 11 的运算,我们当然离不开 �(�)S(x) 函数啦。
-
自然是要先来研究一下异或操作,看看能不能找到规律。
我们知道:
0⊕0=00⊕0=0 ,
0⊕1=10⊕1=1 ,
1⊕0=11⊕0=1 ,
1⊕1=01⊕1=0 ,
-
考虑做差。
做个差的话, 1⊕1=01⊕1=0 和 0⊕0=00⊕0=0 都能正确计算,但 0⊕1=1⊕0=10⊕1=1⊕0=1 可能会算出 1,−11,−1 两个值。
怎么把 −1−1 搞成 11 呢?
取绝对值就好啦。
取绝对值需要 1212 个节点,每一位都这样搞,写得再优美也大概需要 700+700+ 个计算节点。能拿到 88 分。
做差死了吗?
还没,还能抢救一下。
换个思路。我们换个思路,看看还能怎么把 −1−1 搞成 11 。
设 �t 为做差得到的值,即 �={−1,0⊕1 0,0⊕0 �� 1⊕1 1,1⊕0t=⎩⎪⎪⎨⎪⎪⎧−1,0⊕1 0,0⊕0 or 1⊕1 1,1⊕0 ,
首先,怎么把 −1−1 和 0,10,1 区分开呢?
简单,加个 0.50.5 再左移 4141 位,再放到 �(�)S(x) 里算一下,就得到 �={0,0⊕11,0⊕0 �� 1⊕11,1⊕0u=⎩⎪⎪⎨⎪⎪⎧0,0⊕11,0⊕0 or 1⊕11,1⊕0 ,
怎么用 �u 和 �t 搞出异或呢?
我们发现 (1−�)×2+�={1,0⊕10,0⊕0 �� 1⊕11,1⊕0(1−u)×2+t=⎩⎪⎪⎨⎪⎪⎧1,0⊕10,0⊕0 or 1⊕11,1⊕0 。
得到异或啦。
但这个方法运算过程较麻烦,我们试试将 �t 减去 0.50.5 再左移 4141 位,放到 �(�)S(x) 里算一下,看看能不能得到更简单的式子。
可以得到 �′={0,0⊕10,0⊕0 �� 1⊕11,1⊕0u′=⎩⎪⎪⎨⎪⎪⎧0,0⊕10,0⊕0 or 1⊕11,1⊕0 ,
进一步的, 2�′−�={1,0⊕10,0⊕0 �� 1⊕11,1⊕02u′−t=⎩⎪⎪⎨⎪⎪⎧1,0⊕10,0⊕0 or 1⊕11,1⊕0 ,
它显然比上一种方式用的节点少。
于是我们不由得想到,能不能用加法类似实现异或呢?
减法毕竟是“取反+加法”的,计算节点个数严格大于加法,所以如果加法能实现这个操作,就没必要做差了。
幸运的是,加法确实可以。
于是做差真的死了。
-
考虑做和。
做和的话,0⊕1=1⊕0=10⊕1=1⊕0=1 都能正确计算,但是 0+0=0,1+1=20+0=0,1+1=2 ,又产生了分歧。
怎么处理分歧呢?
-
首先有一个思路:
考虑到我们的 �(�)S(x) 函数现在能比较变量和一个常数的大小关系了,
所以想办法找到一些能用的大小关系,使得 00 和 22 在这些大小关系上表现一致,而 11 却不一致。这样就能区分出 11 和 0,20,2 了。
设 �={0,0⊕01,1⊕0 �� 0⊕12,1⊕1t=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕12,1⊕1 ,
我们找两个常数:0.5,1.50.5,1.5 ,00 比他俩都小,22 比他俩都大,11 在他俩中间。
分别减去 0.50.5 和 1.51.5 ,再左移 4141 位,再放到 �(�)S(x) 里算一下,我们看看结果:
设 �1=�((�−0.5)×241)={�(−0.5×241),0⊕0�( 0.5×241),1⊕0 �� 0⊕1�( 1.5×241),1⊕1f1=S((t−0.5)×241)=⎩⎪⎪⎨⎪⎪⎧S(−0.5×241),0⊕0S( 0.5×241),1⊕0 or 0⊕1S( 1.5×241),1⊕1 ,
再设 �2=�((�−1.5)×241)={�(−1.5×241),0⊕0�(−0.5×241),1⊕0 �� 0⊕1�( 1.5×241),1⊕1f2=S((t−1.5)×241)=⎩⎪⎪⎨⎪⎪⎧S(−1.5×241),0⊕0S(−0.5×241),1⊕0 or 0⊕1S( 1.5×241),1⊕1 ,
于是 �1={0,0⊕01,1⊕0 �� 0⊕11,1⊕1f1=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕11,1⊕1 , �2={0,0⊕00,1⊕0 �� 0⊕11,1⊕1f2=⎩⎪⎪⎨⎪⎪⎧0,0⊕00,1⊕0 or 0⊕11,1⊕1 ,
于是 �1−�2={0,0⊕01,1⊕0 �� 0⊕10,1⊕1f1−f2=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕10,1⊕1 ,
这样我们就做出异或了。
我开开心心地一写,发现 663663 行,能拿 99 分。
663663 和 605605 应该相差 22 个计算节点(指 3232 位每一位计算时相差 22 个),这意味着是我们的算法出了问题。当前算法即便优化应该也不至于优化掉两个计算节点。
换种思路,按照刚刚减法的那个套路来。
还是 �={0,0⊕01,1⊕0 �� 0⊕12,1⊕1t=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕12,1⊕1 ,我们想办法把 22 和 0,10,1 区分开,
方法就是减去 1.51.5 再左移 4141 位,再放到 �(�)S(x) 里算一下,得到 �={0,0⊕00,1⊕0 �� 0⊕11,1⊕1u=⎩⎪⎪⎨⎪⎪⎧0,0⊕00,1⊕0 or 0⊕11,1⊕1 ,
观察发现, �−2�={0,0⊕01,1⊕0 �� 0⊕10,1⊕1t−2u=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕10,1⊕1 ;
一样的,若将 �t 减去 0.50.5 再左移、再 �(�)S(x) ,得到 �′={0,0⊕01,1⊕0 �� 0⊕11,1⊕1u′=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕11,1⊕1 ,
用 2�′−�={0,0⊕01,1⊕0 �� 0⊕10,1⊕12u′−t=⎩⎪⎪⎨⎪⎪⎧0,0⊕01,1⊕0 or 0⊕10,1⊕1 ,也能用同样多的计算节点得到异或的结果。
这种方法比上述 663663 行的方法至少少用一个位移节点、一个 �S 节点,所以应该是正解了。
实测 603603 行,可过。
-
-
考虑优化。
注意到最终输出的结果是个十进制数,而不是二进制数。我们将 �,�a,b 转化为二进制、做完异或后又转回十进制的过程,是否可以进一步优化呢?
我们知道,其实异或这个操作就是加法的底层实现。
不信你看,就单独的一位二进制位而言,加法有如下规律:
-
0+0=00+0=0 ,
-
1+0=11+0=1 ,
-
0+1=10+1=1 ,
-
1+1=01+1=0 ,
可以看出,就单独的一位而言,加法和异或会得到相同的结果。
但不同的是,在加法中 1+11+1 会产生进位信号(可以用“与”操作判断是否进位),这个进位信号会被传递给更高一位,从而影响更高一位的值。
所以,如果我们把这个进位信号对更高位的影响消去,就可以把加法操作变成异或操作了。
换句话说,在二进制下考虑,异或就是不进位的加法。
那我们怎么消除进位的影响呢?
先把 �,�a,b 加到一起,
然后还是把每一位都取出来,按位相加。在按位相加的过程中判断进位。
如果某一位上加起来结果等于 22 ,则意味着这一位会向高位进位,而等于 00 或 11 则意味着不会进位。
于是,我们用上文提到的“减去 1.51.5 再左移 4141 位,再放到 �(�)S(x) 里算一下”来将“进位”与“不进位”区分开来,得到 �={0,0⊕00,1⊕0 �� 0⊕11,1⊕1u=⎩⎪⎪⎨⎪⎪⎧0,0⊕00,1⊕0 or 0⊕11,1⊕1 ,即 �={0, no carry1, carryu={0, no carry1, carry ,
若将二进制位从低位到高位编号为 00 到 3131 ,则易知第 �i 位产生的进位会给第 �+1i+1 位加上 11,所以我们可以通过从 �+�a+b 中减去 1×2�+11×2i+1 来消去这个进位的影响。
第 �i 位如果不产生进位,那么减去 0×2�+10×2i+1 也不会有什么问题。
于是我们可以对上述 603603 行代码进行进一步优化了。至少能省去将二进制转化为十进制所用的若干节点。
于是我写了一下,542542 行。
将二进制下最低位单独处理,可以再节省 33 个节点。具体而言:
- 取出 �,�a,b 二进制位时,不将最低位右移 4141 位还原,而是将其直接减去 1.5×2411.5×241 ,再放到 �(�)S(x) 里面进行计算。
于是得到了 539539 行的计算机。
-
输出代码
99 分,663663 行:
if(n==7) { // 663行。
char s[32][25]={ "\0",
"4398046510884", // 2^42-220
"8796093021988", // 2^43-220
"17592186044196", // 2^44-220
"35184372088612", // 2^45-220
"70368744177444", // 2^46-220
"140737488355108", // 2^47-220
"281474976710436", // 2^48-220
"562949953421092", // 2^49-220
"1125899906842404", // 2^50-220
"2251799813685028", // 2^51-220
"4503599627370249", // 2^52-220
"9007199254740772", // 2^53-220
"18014398509481764", // 2^54-220
"36028797018963748", // 2^55-220
"72057594037927716", // 2^56-220
"144115188075855652", // 2^57-220
"288230376151711524", // 2^58-220
"576460752303423268", // 2^59-220
"1152921504606846756", // 2^60-220
"2305843009213693732", // 2^61-220
"4611686018427387684", // 2^62-220
"9223372036854775588", // 2^63-220
"18446744073709551396", // 2^64-220
"36893488147419103012", // 2^65-220
"73786976294838206244", // 2^66-220
"147573952589676412708", // 2^67-220
"295147905179352825636", // 2^68-220
"590295810358705651492", // 2^69-220
"1180591620717411303204", // 2^70-220
"2361183241434822606628", // 2^71-220
"4722366482869645213476", // 2^72-220
};
char s15[]="3298534883328",s05[]="1099511627776";
printf("I\n");
printf("I\n");
printf("< 1 41\n");
printf("< 2 41\n");
for(int i=5,j=31;j>=1;--j,i+=19) {
printf("C %d -%s\n",i-2,s[j]); // i
printf("C %d -%s\n",i-1,s[j]); // i+1
printf("S %d\n",i); // i+2
printf("S %d\n",i+1); // i+3
printf("+ %d %d\n",i+2,i+3); // i+4
printf("C %d -0.5\n",i+4); // i+5
printf("C %d -1.5\n",i+4); // i+6
printf("< %d 41\n",i+5); // i+7
printf("< %d 41\n",i+6); // i+8
printf("S %d\n",i+7); // i+9
printf("S %d\n",i+8); // i+10
printf("- %d\n",i+10); // i+11
printf("+ %d %d\n",i+11,i+9); // i+12
printf("- %d\n",i+2); // i+13
printf("- %d\n",i+3); // i+14
printf("< %d %d\n",i+13,41+j); // i+15
printf("< %d %d\n",i+14,41+j); // i+16
printf("+ %d %d\n",i+15,i-2); // i+17
printf("+ %d %d\n",i+16,i-1); // i+18
}
printf("+ 592 593\n"); // 594
printf("C 594 -%s\n",s05);
printf("C 594 -%s\n",s15);
printf("S 595\n");
printf("S 596\n");
printf("- 598\n");
printf("+ 599 597\n"); // 600
for(int i=17,j=31;j>=1;--j,i+=19) printf("< %d %d\n",i,j); // 601~631
for(int i=601,j=631;i<=630;++j,++i) printf("+ %d %d\n",i,j); // 632~661
printf("+ 661 600\n");
printf("O 662");
}
满分,603603 行:
if(n==7) { // 603行。
char s[32][25]={ "\0",
"4398046510884", // 2^42-220
"8796093021988", // 2^43-220
"17592186044196", // 2^44-220
"35184372088612", // 2^45-220
"70368744177444", // 2^46-220
"140737488355108", // 2^47-220
"281474976710436", // 2^48-220
"562949953421092", // 2^49-220
"1125899906842404", // 2^50-220
"2251799813685028", // 2^51-220
"4503599627370249", // 2^52-220
"9007199254740772", // 2^53-220
"18014398509481764", // 2^54-220
"36028797018963748", // 2^55-220
"72057594037927716", // 2^56-220
"144115188075855652", // 2^57-220
"288230376151711524", // 2^58-220
"576460752303423268", // 2^59-220
"1152921504606846756", // 2^60-220
"2305843009213693732", // 2^61-220
"4611686018427387684", // 2^62-220
"9223372036854775588", // 2^63-220
"18446744073709551396", // 2^64-220
"36893488147419103012", // 2^65-220
"73786976294838206244", // 2^66-220
"147573952589676412708", // 2^67-220
"295147905179352825636", // 2^68-220
"590295810358705651492", // 2^69-220
"1180591620717411303204", // 2^70-220
"2361183241434822606628", // 2^71-220
"4722366482869645213476", // 2^72-220
};
printf("I\n"); // 1。
printf("I\n"); // 2。
printf("< 1 41\n"); // 3。
for(int i=4,j=31;j>=1;--j,i+=5) { // 4~158。
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,41+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("> 158 41\n"); // 159。
printf("< 2 41\n"); // 160。
for(int i=161,j=31;j>=1;--j,i+=5) { // 161~315。
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,41+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("> 315 41\n"); // 316。
for(int i=5,j=1;j<=31;++j,i+=5) { // 317~347。
printf("+ %d %d\n",i,i+157);
}
printf("+ 159 316\n"); // 348。
for(int i=317,j=1;j<=32;++j,++i) { // 349~380。
printf("C %d -1.5\n",i);
}
for(int i=349,j=1;j<=32;++j,++i) { // 381~412。
printf("< %d 41\n",i);
}
for(int i=381,j=1;j<=32;++j,++i) { // 413~444。
printf("S %d\n",i);
}
for(int i=413,j=1;j<=32;++j,++i) { // 445~476。
printf("< %d 1\n",i);
}
for(int i=445,j=1;j<=32;++j,++i) { // 477~508。
printf("- %d\n",i);
}
for(int i=477,j=1;j<=32;++j,++i) { // 509~540。
printf("+ %d %d\n",i,i-160);
}
for(int i=509,j=31;j>=1;--j,++i) { // 541~571。
printf("< %d %d\n",i,j);
}
for(int i=540,j=1;j<=31;++j,++i) { // 572~602。
printf("+ %d %d\n",i,i+31);
}
printf("O 602\n");
}
满分,542542 行:
if(n==7) { // 542行。
char s[32][25]={ "\0",
"4398046510884", // 2^42-220
"8796093021988", // 2^43-220
"17592186044196", // 2^44-220
"35184372088612", // 2^45-220
"70368744177444", // 2^46-220
"140737488355108", // 2^47-220
"281474976710436", // 2^48-220
"562949953421092", // 2^49-220
"1125899906842404", // 2^50-220
"2251799813685028", // 2^51-220
"4503599627370249", // 2^52-220
"9007199254740772", // 2^53-220
"18014398509481764", // 2^54-220
"36028797018963748", // 2^55-220
"72057594037927716", // 2^56-220
"144115188075855652", // 2^57-220
"288230376151711524", // 2^58-220
"576460752303423268", // 2^59-220
"1152921504606846756", // 2^60-220
"2305843009213693732", // 2^61-220
"4611686018427387684", // 2^62-220
"9223372036854775588", // 2^63-220
"18446744073709551396", // 2^64-220
"36893488147419103012", // 2^65-220
"73786976294838206244", // 2^66-220
"147573952589676412708", // 2^67-220
"295147905179352825636", // 2^68-220
"590295810358705651492", // 2^69-220
"1180591620717411303204", // 2^70-220
"2361183241434822606628", // 2^71-220
"4722366482869645213476", // 2^72-220
};
printf("I\n"); // 1。
printf("I\n"); // 2。
printf("+ 1 2\n"); // 3。
printf("< 1 41\n"); // 4。
for(int i=5,j=31;j>=1;--j,i+=5) { // 5~159。
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,41+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("> 159 41\n"); // 160。
printf("< 2 41\n"); // 161。
for(int i=162,j=31;j>=1;--j,i+=5) { // 162~316。
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,41+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("> 316 41\n"); // 317。
for(int i=6,j=1;j<=31;++j,i+=5) { // 318~348。
printf("+ %d %d\n",i,i+157);
}
printf("+ 160 317\n"); // 349。
for(int i=318,j=1;j<=32;++j,++i) { // 350~381。
printf("C %d -1.5\n",i);
}
for(int i=350,j=1;j<=32;++j,++i) { // 382~413。
printf("< %d 41\n",i);
}
for(int i=382,j=1;j<=32;++j,++i) { // 414~445。
printf("S %d\n",i);
}
for(int i=414,j=32;j>=1;--j,++i) { // 446~477。
printf("< %d %d\n",i,j);
}
for(int i=446,j=1;j<=32;++j,++i) { // 478~509。
printf("- %d\n",i);
}
printf("+ 478 3\n"); // 510。
for(int i=479,j=1;j<=31;++j,++i) { // 511~541。
printf("+ %d %d\n",i,i+31);
}
printf("O 541");
}
满分,539539 行:
if(n==7) { // 539行。 // (a+b)-sum{(a[i]+b[i]==2)<<i+1} 能省掉二进制转十进制的节点。
char s[32][25]={ "\0",
"4398046510884", // 2^42-220
"8796093021988", // 2^43-220
"17592186044196", // 2^44-220
"35184372088612", // 2^45-220
"70368744177444", // 2^46-220
"140737488355108", // 2^47-220
"281474976710436", // 2^48-220
"562949953421092", // 2^49-220
"1125899906842404", // 2^50-220
"2251799813685028", // 2^51-220
"4503599627370249", // 2^52-220
"9007199254740772", // 2^53-220
"18014398509481764", // 2^54-220
"36028797018963748", // 2^55-220
"72057594037927716", // 2^56-220
"144115188075855652", // 2^57-220
"288230376151711524", // 2^58-220
"576460752303423268", // 2^59-220
"1152921504606846756", // 2^60-220
"2305843009213693732", // 2^61-220
"4611686018427387684", // 2^62-220
"9223372036854775588", // 2^63-220
"18446744073709551396", // 2^64-220
"36893488147419103012", // 2^65-220
"73786976294838206244", // 2^66-220
"147573952589676412708", // 2^67-220
"295147905179352825636", // 2^68-220
"590295810358705651492", // 2^69-220
"1180591620717411303204", // 2^70-220
"2361183241434822606628", // 2^71-220
"4722366482869645213476", // 2^72-220
};
char s15[]={"3298534883328"};
printf("I\n"); // 1。
printf("I\n"); // 2。
printf("+ 1 2\n"); // 3。
printf("< 1 41\n"); // 4。
for(int i=5,j=31;j>=1;--j,i+=5) { // 5~159。
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,41+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("< 2 41\n"); // 160。
for(int i=161,j=31;j>=1;--j,i+=5) { // 161~315。
printf("C %d -%s\n",i-1,s[j]);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,41+j);
printf("+ %d %d\n",i+3,i-1);
}
for(int i=6,j=1;j<=31;++j,i+=5) { // 316~346。
printf("+ %d %d\n",i,i+156);
}
printf("+ 159 315\n"); // 347。
for(int i=316,j=1;j<=31;++j,++i) { // 348~378。
printf("C %d -1.5\n",i);
}
for(int i=348,j=1;j<=31;++j,++i) { // 379~409。
printf("< %d 41\n",i);
}
printf("C 347 -%s\n",s15); // 410。
for(int i=379,j=1;j<=32;++j,++i) { // 411~442。
printf("S %d\n",i);
}
for(int i=411,j=32;j>=1;--j,++i) { // 443~474。
printf("< %d %d\n",i,j);
}
for(int i=443,j=1;j<=32;++j,++i) { // 475~505。
printf("- %d\n",i);
}
printf("+ 475 3\n"); // 506。
for(int i=476,j=1;j<=31;++j,++i) { // 507~538。
printf("+ %d %d\n",i,i+31);
}
printf("O 538");
}
task8task8
要求
输入:�a 。
输入限制:∣�∣⩽109∣a∣⩽109 ,小数部分不超过 99 位。
输出:�1010a 。
满分行数:77 行。
解决方法
与二进制无关了,所以用不到 �(�)S(x) 的选择性了。
通过 node8.ans
,我们知道,满分计算机是 77 个计算节点!
这说明我们需要用一些奇技淫巧了。
-
能不能二分答案呢?
因为乘 1010 的操作还是好做的,所以对 [0,�8][0,8a] 进行二分,检验二分值的 1010 倍是否等于 �a ,用 �(�)S(x) 函数来作比较,并完成二分区间的更新。
理论上可行,但二分至误差不超过 10−910−9 是真的困难,可以预见,我们的计算节点数会多到爆炸。还不如直接用乘法节点赚个实惠。
-
能不能用多次右移再相加来模拟乘 0.10.1 的操作呢?
也可以,但仍然不是正解。
0.1=2−4+2−5+2−8+2−9+2−12+2−13+2−16+2−17+2−20+⋯0.1=2−4+2−5+2−8+2−9+2−12+2−13+2−16+2−17+2−20+⋯
而 2−4+2−5+2−8+2−9+2−12+2−13+2−16+2−17+2−20=0.099999427795410156252−4+2−5+2−8+2−9+2−12+2−13+2−16+2−17+2−20=0.09999942779541015625 ,
只需要加足够多的项就能使得答案在精度范围内准确了。
据说能拿 77 到 88 分。
下面讲正解。
我们考虑如何正确使用 �(�)S(x) 函数来完成除以 1010 的操作。
回想我们完成 task4task4 时对 �(�)S(x) 导数的思考,以及我们使用 �(�)S(x) 在 �=0x=0 处的切线来获取一条近似直线的操作,
那么我们是否可以找到 �(�)S(x) 上一条斜率为 110101 的切线呢?
我们知道, �′(�)=�−�(1+�−�)2∈(0,14]S′(x)=(1+e−x)2e−x∈(0,41] ,且 �′(�)S′(x) 是 �R 上的连续函数、偶函数,
所以 ∃ �∈(0,+∞)∃ ξ∈(0,+∞) , 使得 �′(�)=110S′(ξ)=101 ,
我们可以写出 �(�)S(x) 在这点的切线方程,为 �=�′(�)(�−�)+�(�)y=S′(ξ)(x−ξ)+S(ξ) ,
即 �=110(�−�)+�(�)y=101(x−ξ)+S(ξ) 。
由切线的意义知,当 �x 与 �ξ 非常接近时,�(�)S(x) 近似等于 �′(�)(�−�)+�(�)=110(�−�)+�(�)S′(ξ)(x−ξ)+S(ξ)=101(x−ξ)+S(ξ) ,
所以我们将输入的 �a 右移 �k 位(造一个微小量),再加上 �ξ ,就构造出了一个与 �ξ 非常接近的值,并计算 �(�2�+�)S(2ka+ξ) 的值,
因为 �2�2ka 很小,所以 �(�2�+�)S(2ka+ξ) 近似等于 110(�2�+�−�)+�(�)=110�2�+�(�)101(2ka+ξ−ξ)+S(ξ)=1012ka+S(ξ) ,
于是 (�(�2�+�)−�(�))×2�(S(2ka+ξ)−S(ξ))×2k 近似等于 �1010a 。
好了,现在唯一的问题就是, �ξ 等于多少?
解方程 �′(�)=�−�(1+�−�)2=110S′(ξ)=(1+e−ξ)2e−ξ=101 ,
得: 10�−�=(1+�−�)210e−ξ=(1+e−ξ)2 ,
得: �−2�−8�−�+1=0e−2ξ−8e−ξ+1=0 ,
使用求根公式得: �−�=4±15e−ξ=4±15 ,
钦定 �>0ξ>0 ,所以 0<�−�<10<e−ξ<1 ,所以舍去 4+154+15 ,得:�−�=4−15e−ξ=4−15 ,
于是 �=−ln(4−15)=ln(4+15)ξ=−ln(4−15)=ln(4+15) ,
于是 �(�)=11+�−�=15−15=5+1510S(ξ)=1+e−ξ1=5−151=105+15 ,
好了,现在该如何计算出这两个玩意儿?
因为我们最后会乘个 2�2k ,所以这两个玩意儿的精度必须极其高才行。
那么先算 �k :
-
令 �=�2�x=2ka ,
解 110�+�(�)−�(�+�)<10−90101x+S(ξ)−S(x+ξ)<10−90 ,
即 110�+11+�−�−11+�−(�+�)<10−90101x+1+e−ξ1−1+e−(x+ξ)1<10−90 ,
即 110�−�−�−�−(�+�)(1+�−(�+�))(1+�−�)<10−90101x−(1+e−(x+ξ))(1+e−ξ)e−ξ−e−(x+ξ)<10−90 ,
即 110�−��−1(�(�+�)+1)(1+�−�)<10−90101x−(e(x+ξ)+1)(1+e−ξ)ex−1<10−90 ,
因为 �>0x>0 时,��−1(�(�+�)+1)(1+�−�)>�(��+�+1)(1+�−�)(e(x+ξ)+1)(1+e−ξ)ex−1>(ex+ξ+1)(1+e−ξ)x ,
所以只需 110�−�(�(�+�)+1)(1+�−�)<10−90101x−(e(x+ξ)+1)(1+e−ξ)x<10−90 ,
即 �(�(�+�)+1)(1+�−�)−1010(�(�+�)+1)(1+�−�)<10−90x10(e(x+ξ)+1)(1+e−ξ)(e(x+ξ)+1)(1+e−ξ)−10<10−90 ,
注意到 (��+1)(1+�−�)=4+15+4−15+2=10(eξ+1)(1+e−ξ)=4+15+4−15+2=10 ,
所以将上式中的 �(�+�)e(x+ξ) 强行拆开,得:�(�(�+�)+1)(1+�−�)−1010(�(�+�)+1)(1+�−�)=�(��−1)(��+1)10((��−1)(��+1)+10)<10−90x10(e(x+ξ)+1)(1+e−ξ)(e(x+ξ)+1)(1+e−ξ)−10=x10((ex−1)(eξ+1)+10)(ex−1)(eξ+1)<10−90
�>0x>0 时,仍然使用 ��>�+1>1ex>x+1>1 ,将分母中的 ��ex 放缩为 11 ,将分子中的 �x 放缩为 ��−1ex−1 ,
所以只需 (��−1)2(��+1)100<10−90100(ex−1)2(eξ+1)<10−90 ,
即 �<ln((5−15)×10−89+1)x<ln((5−15)×10−89+1) ,
把 �=�2�x=2ka 代入得:�2�<ln((5−15)×10−89+1)2ka<ln((5−15)×10−89+1)
取 �=109a=109 ,得 �>9log210−log2(ln((5−15)×10−89+1))≈177.637k>9log210−log2(ln((5−15)×10−89+1))≈177.637
故只需 �⩾178k⩾178 。
同样的,�k 不能太大,同 task4task4 ,需有 �⩽269k⩽269 。
同样的,178178 这个下界很松。实测取到 8686 都没问题。
-
再来计算:
�=−ln(4−15)=ln(4+15)ξ=−ln(4−15)=ln(4+15) ,
�(�)=11+�−�=15−15=5+1510S(ξ)=1+e−ξ1=5−151=105+15 ,
怎么算呢?
用电脑的计算器计算 �=ln(4+15)ξ=ln(4+15) ,然后把计算所得的数放到
checker
里面算出一个 �(�)S(ξ) 。不要忘了,你的
checker
是可以运行你所构造的计算机的。我用电脑的计算器算得 �=ξ=
2.0634370688955605467272811726201
,用
checker
算得 �(�)=S(ξ)=0.887298334620741688517926539978236773937633025540819832675154107295416657242528255923059519
,实测这两个参数可以过。
使用强大的计算器算得 �ξ 和 �(�)S(ξ) 小数点后 9090 位(括号里为第 9191 位)得:
2.063437068895560546727281172620131871456591449883392499836032692765902842847409911780353006(4)
0.887298334620741688517926539978239961083292170529159082658757376611348309193697903351928737(6)
��.PS. 可以看到,还是有一定的误差的。
这是 �=(�(�2178+�)−�(�))×2178y=(S(2178x+ξ)−S(ξ))×2178 的图像:
于是这个测试点可以过了。
输出代码
66 分,使用乘法节点:
if(n==8) { // 6 分代码,使用乘法节点。
printf("I\n");
printf("> 1 233\n"); // 造零。
printf("C 2 0.1\n"); // 造0.1。
printf("* 1 3\n");
printf("O 4");
}
满分,77 行:
if(n==8) { // 7 行。
printf("I\n");
//printf("C 2 2.063437068895560546727281172620131871456591449883392499836032692765902842847409911780353006\n");
printf("> 1 178\n");
printf("C 2 2.0634370688955605467272811726201\n"); // 计算器答案。
printf("S 3\n");
//printf("C 4 -0.887298334620741688517926539978239961083292170529159082658757376611348309193697903351928738\n");
printf("C 4 -0.887298334620741688517926539978236773937633025540819832675154107295416657242528255923059519\n");
printf("< 5 178\n");
printf("O 6");
}
task9task9
要求
输入:�1,⋯ ,�16a1,⋯,a16 。
输入限制:∣�1∣,⋯ ,∣�16∣⩽109∣a1∣,⋯,∣a16∣⩽109 ,小数部分不超过 99 位。
输出:输出 1616 个实数,表示 �1,⋯ ,�16a1,⋯,a16 从小到大排序后的结果。
满分行数:30003000 。
解决方法
计算节点太单一了,所以有些操作很难实现。
很多高效的排序方法都不能用了,只能使用冒泡排序、选择排序这些基本的算法。
一步一步来考虑。
-
不管是用什么排序,一个绕不开的操作就是交换两个数。
嗯,怎么交换两个数呢?
。a^=b^=a^=b
(异或需要 539539 行,可见在这道题里面位运算可并不快)
我们有加法和减法,所以用这个来实现交换。考虑到每个计算节点只能被赋值一次,我们写出如下代码:
u=a+b; p=u-b; q=u-p;
最终 �p 即为交换后的 �b ,�q 即为交换后的 �a 。
——为什么不直接
p=a;q=b;
来完成交换呢?——因为我们需要同时考虑不交换的情况,故我们的交换操做略微修改后应能够兼容不交换的情况。而直接使用赋值来进行交换会使得我们失去在交换中操作空间,也就没有修改的余地了。
现在考虑不交换的情况,首先我们需要把不交换的操作也写成上面那种形式,并且最好有很多地方与上文完全相同,便于我们构造计算网络。思索一番,我们得到:
u=a+b; p=u-a; q=u-p;
两种写法仅在
p=u-?
处有差别。交换则减 �b ,不交换则减 �a 。我们钦定,若 �>�a>b ,则交换;否则不交换。
于是,我们把交换和不交换两种写法合到一起,得到这样的代码:
u=a+b; p=u-min(a,b); q=u-p;
现在问题在于如何计算 min(�,�)min(a,b) 。
min(�,�)min(a,b) 计算的返回值是 �a 或者 �b ,而不是 00 或者 11 。
什么样的操作能返回自变量本身呢?
记不记得我在上文讲 task4task4 时曾说过:
我们实际上得到的是一个从 �(�)S(x) 得到 �x 的办法,这个办法很重要。
就在 task4task4 中,我们构造了若干以零为分界点的分段函数。其中一个分段函数(就是 task4task4 的答案)为:
−(�(�2�+�)−12)×2�+3+�+�={� ,�⩾0−�,�<0−(S(2kx+t)−21)×2k+3+t+x={x ,x⩾0−x,x<0 ,
往回退几步,有这样一个分段函数:
(�(�2�+�)−12)×2�+2−�={0,�⩾0�,�<0(S(2kx+t)−21)×2k+2−t={0,x⩾0x,x<0 ,
其中 �=�(�+�2�)×2�={2�,�⩾00 ,�<0t=S(2qx+ε)×2p={2p,x⩾00 ,x<0 ,
此时的参数为 �=41,�=178,�=179q=41,k=178,p=179 ,注意此处 �p 取值并非 task4task4 中最后 �p 的取值 180180 ,而且 �(�2�+�)−12S(2kx+t)−21 后面乘的并非 2�+32k+3 。
而容易发现, min(�,0)={0,�⩾0�,�<0min(x,0)={0,x⩾0x,x<0 ,所以我们有了一个可以和 00 取 minmin 的函数,即 min(�,0)=(�(�2�+�)−12)×2�+2−�min(x,0)=(S(2kx+t)−21)×2k+2−t 。
将
p=u-min(a,b)
改写为p=(u-b)-min(a-b,b-b)
,即p=a-min(a-b,0)
的形式。这个形式就可以用上面得到的 min(�,0)min(x,0) 来解决了。于是交换的操作完成了,还顺便解决了比大小操作。
-
解决了交换和比较大小的操作后,我们只需要再选择一种排序方式就能完成任务了。
冒泡排序当然可以(而且还很优美),在这里我试着写了一下 Bitonic SortBitonic Sort (双调排序)。
双调排序是一个非常适合于 GPUGPU 编程的排序算法,只需要比较器就可以完成全部的排序工作。
不过它只能对 22 的幂次个数据进行排序,好在此测试点恰好有 1616 个数据。
关于双调排序,此处就不赘述了;
我会尽快在我的博客 双调排序 中具体介绍它(该博客现在处于待填坑隐藏状态,什么时候它不再是隐藏文章了,就说明我填坑完毕了。能力有限,实在抱歉 ⋯⋯⋯⋯)。
双调排序体现在代码中的
for
循环中,不会双调排序的话,这几个for
看起来会很费解。。。
输出代码
满分,13921392 行:
if(n==9) { // 1392 行。
const int N=16; // 数据总数。
int pos[17],row=0,a,b; // 16个数据当前的位置,当前输出行数。两个临时节点。
for(int i=1;i<=N;++i) printf("I\n"),pos[i]=i,++row;
for(int i=1,i2=2;i2<=N;++i,i2<<=1) {
for(int j=1,j2=i2-1;j2>=1;++j,j2-=2) {
for(int k=j;k<=N;k+=i2) { // 每次交换 17 行。
a=k;b=k+j2;
++row; printf("+ %d %d\n",pos[a],pos[b]); // u=a+b;
++row; printf("- %d\n",pos[b]); // -b;
++row; printf("+ %d %d\n",pos[a],row-1); // x=a+-b;
++row; printf("C %d 0.0000000001\n",row-1); // x+eps;
++row; printf("< %d 41\n",row-1); // (x+eps)<<41;
++row; printf("S %d\n",row-1); // S((x+eps)<<41);
++row; printf("< %d 179\n",row-1); // t=S((x+eps)<<41)<<179;
++row; printf("> %d 178\n",row-5); // x>>178;
++row; printf("+ %d %d\n",row-1,row-2); // (x>>178)+t;
++row; printf("S %d\n",row-1); // S((x>>178)+t);
++row; printf("C %d -0.5\n",row-1); // S(...)-0.5;
++row; printf("< %d 180\n",row-1); // (S(...)-0.5)<<180;
++row; printf("- %d\n",row-1); // -(S(...)-0.5)<<180;
++row; printf("+ %d %d\n",row-7,row-1); // -min=t-(S(...)-0.5)<<180;
++row; printf("+ %d %d\n",pos[a],row-1); pos[b]=row; // p=a-min; b'=p;
++row; printf("- %d\n",row-1); // -p;
++row; printf("+ %d %d\n",row-16,row-1); pos[a]=row; // q=u+-p; a'=q;
}
}
for(int j=(i2>>2);j>=1;j>>=1) {
for(int k=1;k<=j;++k) {
for(int l=k;l<=N;l+=(j<<1)) {
a=l;b=l+j;
++row; printf("+ %d %d\n",pos[a],pos[b]); // u=a+b;
++row; printf("- %d\n",pos[b]); // -b;
++row; printf("+ %d %d\n",pos[a],row-1); // x=a+-b;
++row; printf("C %d 0.0000000001\n",row-1); // x+eps;
++row; printf("< %d 41\n",row-1); // (x+eps)<<41;
++row; printf("S %d\n",row-1); // S((x+eps)<<41);
++row; printf("< %d 179\n",row-1); // t=S((x+eps)<<41)<<179;
++row; printf("> %d 178\n",row-5); // x>>178;
++row; printf("+ %d %d\n",row-1,row-2); // (x>>178)+t;
++row; printf("S %d\n",row-1); // S((x>>178)+t);
++row; printf("C %d -0.5\n",row-1); // S(...)-0.5;
++row; printf("< %d 180\n",row-1); // (S(...)-0.5)<<180;
++row; printf("- %d\n",row-1); // -(S(...)-0.5)<<180;
++row; printf("+ %d %d\n",row-7,row-1); // -min=t-(S(...)-0.5)<<180;
++row; printf("+ %d %d\n",pos[a],row-1); pos[b]=row; // p=a-min; b'=p;
++row; printf("- %d\n",row-1); // -p;
++row; printf("+ %d %d\n",row-16,row-1); pos[a]=row; // q=u+-p; a'=q;
}
}
}
}
for(int i=1;i<=N;++i) printf("O %d\n",pos[i]);
}
task10task10
要求
输入:�,�,�a,b,m 。
输入限制:0⩽�,�<2320⩽a,b<232 , 1⩽�<2321⩽m<232 , �,�,�a,b,m 均为整数。
输出:�×�a×b 除以 �m 的余数。
满分行数:20002000 行。
解决方法
首先,这个式子没有化简的余地。
使用 �×�mod �=((�mod �)×(�mod �))mod �a×bmodm=((amodm)×(bmodm))modm 更不行,虽然每次取余所需的计算节点个数少了,但总的取余次数多了。
看来不管怎么搞,我们都避不开乘法操作。
那就先来讲讲怎么实现乘法。
-
一个比较容易想到的做法就是使用快速乘,将 �b 拆分为二进制,将 �a 按 �b 不同二进制位上的 00 或 11 相应地做位移后再加起来。
举个栗子,�=5=22+20b=5=22+20 ,则 �×�=�×22+�×20a×b=a×22+a×20 。
二进制拆分参见 task6task6 ,将 �a 对应做位移再相加参见 task5task5 ,有前面的铺垫,这个点的乘法应该怎么计算还是蛮容易想到的。
-
一个不太容易想到的做法是泰勒展开。
先普及一下泰勒公式:
如果函数 �(�)f(x) 在 �=�0x=x0 的某个邻域内有 �n 阶导数,且在 �=�0x=x0 处有 �+1n+1 阶导数,则对该邻域内的任意一点 �x 处,有
�(�)=�(�0)+�′(�0)(�−�0)+�′′(�0)2!(�−�0)2+⋯+�(�)(�0)�!(�−�0)�+�((�−�0)�)f(x)=f(x0)+f′(x0)(x−x0)+2!f′′(x0)(x−x0)2+⋯+n!f(n)(x0)(x−x0)n+o((x−x0)n) ,
�(�)(�0)f(n)(x0) 为 �(�)f(x) 在 �0x0 处的 �n 阶导数。
�((�−�0)�)o((x−x0)n) 表示一个函数,它的表达式我们写不出来,我们只知道它是 (�−�0)�(x−x0)n 的高阶无穷小。什么意思呢?就是当 �−�0x−x0 无限接近于 00 时,此时的 (�−�0)�(x−x0)n 是非常非常小的一个数,非常接近于 00 ,而 �((�−�0)�)o((x−x0)n) 是个比 (�−�0)�(x−x0)n 还要小很多的数,更接近于 00 ——尽管我们并不知道 �((�−�0)�)o((x−x0)n) 的具体表达式是什么。
不要和我较真说 �((�−�0)�)o((x−x0)n) 就等于 �(�)−(�(�0)+�′(�0)(�−�0)+�′′(�0)2!(�−�0)2+⋯+�(�)(�0)�!(�−�0)�)f(x)−(f(x0)+f′(x0)(x−x0)+2!f′′(x0)(x−x0)2+⋯+n!f(n)(x0)(x−x0)n) ,
这样写出来的 �((�−�0)�)o((x−x0)n) 没有实际意义,并不能清楚地表达 �((�−�0)�)o((x−x0)n) 的性质。
上面那个等式就是 �(�)f(x) 的 �n 阶泰勒公式,我们一般也将之称为“泰勒展开”。
为什么想到要使用泰勒公式?
核心思想还是想办法利用 �(�)S(x) 函数构造乘法。在我们只能做加加减减的情况下,怎么才能快速构造出 �×�a×b 这样的二次的表达式呢?
这就想到泰勒公式了,因为泰勒公式能将一个函数强行拆成若干多项式以及一个表达式未知但却很小的 �((�−�0)�)o((x−x0)n) 的和,所以能够搞出次数为二次的项。有了二次的项,我们的加加减减就能更方便地构造 �×�a×b 了(之所以认为加加减减不方便,是因为常数次的加加减减不会提高变量的次数,譬如 �a 乘任意一个(与 �a 无关的)常数都不会变成 �2a2)。
那么该怎么用泰勒公式呢?
先将 �(�)S(x) 用泰勒公式展开两阶看看。
根据求导的法则,我们得:
�′(�)=�−�(1+�−�)2=�(�)(1−�(�))S′(x)=(1+e−x)2e−x=S(x)(1−S(x)) ,
�′′(�)=�−�(�−�−1)(1+�−�)3S′′(x)=(1+e−x)3e−x(e−x−1) ,
则 �(�)S(x) 在某点 �0x0 处的 22 阶泰勒公式为:
�(�)=�(�0)+�′(�0)(�−�0)+�′′(�0)2!(�−�0)2+�((�−�0)2)S(x)=S(x0)+S′(x0)(x−x0)+2!S′′(x0)(x−x0)2+o((x−x0)2) ,
即 �(�)=11+�−�0+�−�0(1+�−�0)2(�−�0)+�−�0(�−�0−1)2(1+�−�0)3(�−�0)2+�((�−�0)2)S(x)=1+e−x01+(1+e−x0)2e−x0(x−x0)+2(1+e−x0)3e−x0(e−x0−1)(x−x0)2+o((x−x0)2) ,
换元,用 �+�0x+x0 代替 �x,得 �(�+�0)=11+�−�0+�−�0(1+�−�0)2�+�−�0(�−�0−1)2(1+�−�0)3�2+�(�2)S(x+x0)=1+e−x01+(1+e−x0)2e−x0x+2(1+e−x0)3e−x0(e−x0−1)x2+o(x2) ,
我们取一个比较好的 �0x0 ,使得上式变得优美一些。
比如说取 �0=−ln3x0=−ln3 ,
那么上式变成:�(�−ln3)=14+316�+364�2+�(�2)S(x−ln3)=41+163x+643x2+o(x2) ,
——为什么选 −ln3−ln3 ?
首先可以看到,分母全部变成 22 的倍数了,方便我们用位移来计算除法。
当然 −ln7−ln7 什么的也可以,但是写起代码来会麻烦一些。因为 3=1+23=1+2 而 7=1+2+47=1+2+4 ,会至少多一次位移和一次加法。
+ln3+ln3 也行,做法基本一致。
根据上文所说的关于 �()o() 函数的含义,我们知道,当 �x 非常小、非常接近于 00 时 , �(�2)o(x2) 也非常接近 00 ,且比 �2x2 更接近 00 。也就是说,�x 趋近于 00 时,�(�)S(x) 和 14+316�+364�241+163x+643x2 几乎相等。
则我们用 �(�)S(x) 减去 14+316�41+163x 再乘 643364 就可以近似得到 �2x2 。
按照这个操作,我们可以获得 �2,�2,(�+�)2a2,b2,(a+b)2 ,则根据 �×�=12((�+�)2−�2−�2)a×b=21((a+b)2−a2−b2) 就能构造出 �×�a×b 了。
好我们还需要解决除以 33 的问题。
由于 �′(�)⩽13S′(x)⩽31 ,所以我们改为除以 66 再乘 22 。
解方程 �′(�)=16S′(ξ)=61 得 �=ln(2+3),�(�)=3+36ξ=ln(2+3),S(ξ)=63+3 ,
使用电脑的计算器算得 �=ξ=
1.316957896924816708625046347308
,再用
checker
算得 �(�)=S(ξ)=0.788675134594812882254574390250983987152637213723810691381222453707336011601878062966598728
,但很遗憾,这样算得的 �ξ 和 �(�)S(ξ) 精度太低了,在后面的运算过程中小数点后 99 位以内会出现较大误差,所以使用
checker
的方法宣告 ��gg 。使用强大的计算器算得 �ξ 和 �(�)S(ξ) 分别如下:
1.316957896924816708625046347307968444026981971467516479768472256920460185416443976074219013(4)
0.788675134594812882254574390250978727823800875635063438009301163241988836151466672846857697(7)
我们只能用这两个数了。
类似 task8task8 ,我们用除以 2�2k 来造极小量,
这里不能直接用 �=178k=178 ,也不能随便设个差不多的数。就因为这一个参数没有设置准确,我调参调了一天,一直 ��WA 到自闭才找到问题所在。
实测这里的 �k 取值范围极其窄。所以务必认真对待以下分析。
因为 �′′′(�)=�−�(�−2�−4�−�+1)(1+�−�)4S′′′(x)=(1+e−x)4e−x(e−2x−4e−x+1) ,
所以 �(�)S(x) 在 �0x0 处的 33 阶泰勒公式为:
�(�)=�(�0)+�′(�0)(�−�0)+�′′(�0)2!(�−�0)2+�′′′(�0)3!(�−�0)3+�((�−�0)3)S(x)=S(x0)+S′(x0)(x−x0)+2!S′′(x0)(x−x0)2+3!S′′′(x0)(x−x0)3+o((x−x0)3)
=11+�−�0+�−�0(1+�−�0)2(�−�0)+�−�0(�−�0−1)2(1+�−�0)3(�−�0)2+�−�0(�−2�0−4�−�0+1)6(1+�−�0)4(�−�0)3+�((�−�0)3) =1+e−x01+(1+e−x0)2e−x0(x−x0)+2(1+e−x0)3e−x0(e−x0−1)(x−x0)2+6(1+e−x0)4e−x0(e−2x0−4e−x0+1)(x−x0)3+o((x−x0)3) ,
用 �+�0x+x0 替换 �x ,上式即为:
�(�+�0)=11+�−�0+�−�0(1+�−�0)2�+�−�0(�−�0−1)2(1+�−�0)3�2+�−�0(�−2�0−4�−�0+1)(1+�−�0)4�3+�(�3)S(x+x0)=1+e−x01+(1+e−x0)2e−x0x+2(1+e−x0)3e−x0(e−x0−1)x2+(1+e−x0)4e−x0(e−2x0−4e−x0+1)x3+o(x3) ,
取 �0=−ln3x0=−ln3 得 �(�)S(x) 在 −ln3−ln3 处的 33 阶泰勒公式为:
�(�−ln3)=14+316�+364�2−1256�3+�(�3)S(x−ln3)=41+163x+643x2−2561x3+o(x3) ,
其中 lim�→0�(�3)=0x→0limo(x3)=0 ,甚至 lim�→0�(�3)�3=0x→0limx3o(x3)=0 。
(第二个式子是 �(�3)o(x3) 的定义,在此不详述。第二个式子真正能说明 �(�3)o(x3) 比 �3x3 还小得多,因为 �(�3)o(x3) 除以了一个非常小的数 �3x3 之后,极限仍然是 00 )
取 �=x= 微小量 �2�2ka ,可取足够大的 �k 使 �x 非常接近 00 。现在来求 �k 的取值范围:
欲使 �(�−ln3)=14+316�+364�2S(x−ln3)=41+163x+643x2 近似相等,需要有 ∣−1256�3+�(�3)∣<5×10−91∣−2561x3+o(x3)∣<5×10−91 。
因为 �(�3)o(x3) 真的很小,我们将之忽略,所以只需 1256�3<5×10−912561x3<5×10−91 ,即 1256(�2�)3<5×10−912561(2ka)3<5×10−91 ,
�<232a<232 ,故只需 1256(2322�)3<5×10−912561(2k232)3<5×10−91 ,
整理得 �>893+30log210≈129.325k>389+30log210≈129.325 ,
所以 �>130k>130 。
另一方面,364(�2�)2643(2ka)2 的精度不能丢失,故要求 364(�2�)2>10−90643(2ka)2>10−90 ,
取 �=1a=1 ,只需 322�+6>10−9022k+63>10−90 ,
整理得 �<45log210+12log23−3≈147.279k<45log210+21log23−3≈147.279 ,
所以 �⩽147k⩽147 。
然而实测 �k 的取值范围为 [124,131][124,131] ,个人猜测可能有几点原因:
-
上述分析有 bugbug ,比如即便 1256(2322�)3<5×10−912561(2k232)3<5×10−91 成立,第 9191 位仍可能产生不该有的进位。再如保证了 364(�2�)2>10−90643(2ka)2>10−90 ,也不能保证其小数点后最后一位精度不会丢失。
-
ln3,�ln3,ξ 和 �(�)S(ξ) 本身只能保留 9090 位,在乘 22 的幂次时造成误差。
-
�(�)S(x) 函数含超越数 �e ,本身很容易产生误差。
最终我取了 �=130k=130 。
可以得出:16�=(�(�2130+�)−�(�))×213061x=(S(2130x+ξ)−S(ξ))×2130 ,
于是 643�=(�(�2130+�)−�(�))×2137364x=(S(2130x+ξ)−S(ξ))×2137 。
而 316�=116�+18�163x=161x+81x ,两次位移和一次加法可解决。
这样,我们就可以计算出 �2x2 了。
这是 �=(�(�2130+�)−�(�))×2130y=(S(2130x+ξ)−S(ξ))×2130 的图像:
梳理一下思路:
令 �1=�(�2130−ln3)−14−(116�2130+18�2130)t1=S(2130a−ln3)−41−(1612130a+812130a) ,则 (�2130)2=643�1=(�(�1+�)−�(�))×27(2130a)2=364t1=(S(t1+ξ)−S(ξ))×27 ;
这里大家可能会有个疑惑:为什么 �1t1 没有除以 21302130 就直接放到 �(�)S(x) 函数里计算了呢?
其实 �1=364(�2130)2+�((�2130)2)≈364(�2130)2t1=643(2130a)2+o((2130a)2)≈643(2130a)2 ,
可见 �1t1 仍然是微小量。所以不需要再除以 21302130 了。
(�2130)2,(�2130+�2130)2(2130b)2,(2130a+2130b)2 同理 。最后再乘上 22602260 就能还原回 �2,�2,(�+�)2a2,b2,(a+b)2 了。
为了减少计算节点,我们将 �(�1+�),�(�2+�),�(�3+�)S(t1+ξ),S(t2+ξ),S(t3+ξ) 都计算出来后,先按照 ((�+�)2−�2−�2)((a+b)2−a2−b2) 做差,即 �(�3+�)−�(�1+�)−�(�2+�)S(t3+ξ)−S(t1+ξ)−S(t2+ξ) ,再加上 �(�)S(ξ) ,最后乘上 22662266 ,就可以得到 �×�a×b 啦。
再加一些优化,计算节点数还可以变得更少。具体做法就留给读者自己思索咯。
可是 ln3ln3 怎么算?
这个我真的不知道该如何利用
checker
来计算了。。。使用强大的计算器算得:
ln3=ln3=
1.098612288668109691395245236922525704647490557822749451734694333637494293218608966873615754(8)
-
至此,我们用比快速乘更少的节点( 3535 个)计算出了 �×�a×b 。
下面讲如何完成取余操作。
-
取余操作,可以等效为若干次减法操作。
设 �×�=�×�+�a×b=p×m+r ,其中 0⩽�<�0⩽r<m ,
那么,一个显然的做法就是:
我们枚举这个 �p ,如果 �×�a×b 比当前枚举到的 �×�p×m 大,则继续往大枚举;否则往小枚举。
这样搞的计算节点数当然会多到爆炸啦。
由于 �×�<264a×b<264 ,
所以考虑直接将 �p 二进制拆分,即 �=�0×20+�1×21+⋯+�63×263p=p0×20+p1×21+⋯+p63×263 ,其中 ��∈{0,1},�∈[0,63]pi∈{0,1},i∈[0,63] 。
然后从高位到低位枚举 ��pi ,当前 ��ab 比 �×2�m×2i 大,则将 �×2�m×2i 从 ��ab 中减去。直到 �=0i=0 ,操作完成后即得 �r ,即余数。
为了减少计算节点数,我们先将 ��ab 取反,变成 −��−ab ,然后每次加上 �×2�m×2i ,最后把得到的 −�−r 取反即得 �r 。这样我们在计算 ��ab 时的方式也要相应的调整,变为 12(�2+�2−(�+�)2)21(a2+b2−(a+b)2) ,同时调整一些细节即可。
至于如何调整,留给读者自己思考完成吧。
那么具体如何比较大小呢?
考虑对于 �=�×2�M=m×2i ,如何正确执行 {−��+� ,−��+�⩽0−�� ,−��+�>0{−ab+M ,−ab+M⩽0−ab ,−ab+M>0 呢?
首先我们可以得到一个函数 �={0, −��+�<01, −��+�>0p={0, −ab+M<01, −ab+M>0 ,
这样的话, �=0p=0 就代表要让 −��−ab 加上 �M ,�=1p=1 代表不加上 �M 。
由于 −��+�−ab+M 一定是整数,且 −��+�=0−ab+M=0 时也应该执行加上 �M 的操作,所以我们将 −��−ab 提前减去 0.10.1 ,从而使得我们的 �p 能够处理 −��+�=0−ab+M=0 的情形。
则此时 �={0, −��+�⩽01, −��+�>0p={0, −ab+M⩽01, −ab+M>0 ,
我们想要的是 {−��+� ,−��+�⩽0−�� ,−��+�>0{−ab+M ,−ab+M⩽0−ab ,−ab+M>0 ,利用 �p 可以将之改写为 −��+(1−�)×�−ab+(1−p)×M ,
考虑到 1−�1−p 只有 0,10,1 两种取值,所以我们只需要构造 (1−�)�={� ,�=00 ,�=1(1−p)M={M ,p=00 ,p=1 ,
这个形式的函数我们在 task4task4 中讲过,以下再简述一番。
将 �M 拆为 (�(�2178)−0.5)×2180(S(2178M)−0.5)×2180 ,考虑在这个过程中加入 �p 的影响。
构造 �=�×2179={0 ,�=02179 ,�=1t=p×2179={0 ,p=02179 ,p=1 ,作为 �p 的影响。
将 �t 加入上述过程中,即 (�(�2178+�)−0.5)×2180={� ,�=02179 ,�=1(S(2178M+t)−0.5)×2180={M ,p=02179 ,p=1 ,
减去 �t ,消除多余影响,得 (�(�2178+�)−0.5)×2180−�={� ,�=00 ,�=1(S(2178M+t)−0.5)×2180−t={M ,p=00 ,p=1 ,
于是做完了。
共 807807 行,比官方题解的 819819 行略少。
开心。
输出代码
满分,807807 行:
if(n==10) { // 807 行。
const int W=130;
char ln3[]={"1.098612288668109691395245236922525704647490557822749451734694333637494293218608966873615755"};
// 1.316957896924816708625046347307968444026981971467516479768472256920460185416443976074219013
// -0.25
// =1.066957896924816708625046347307968444026981971467516479768472256920460185416443976074219013
char xi []={"1.066957896924816708625046347307968444026981971467516479768472256920460185416443976074219013"};
// 0.78867513459481288225457439025097872782380087563506343800930116324198883615146667 2846857698
// +0.00000000000000000000000000000000000000000000000000000000000000000000000000000000 0843375836 // 0.1>>266;
// =0.78867513459481288225457439025097872782380087563506343800930116324198883615146667 3690233534
char Sxi[]={"0.788675134594812882254574390250978727823800875635063438009301163241988836151466673690233534"};
printf("I\n");
printf("I\n");
printf("I\n");
printf("> 1 %d\n",W);
printf("> 2 %d\n",W); // 5。
printf("+ 4 5\n");
printf("C 4 -%s\n",ln3);
printf("C 5 -%s\n",ln3);
printf("C 6 -%s\n",ln3);
printf("S 7\n"); // 10。
printf("S 8\n");
printf("S 9\n");
printf("> 4 4\n");
printf("> 4 3\n");
printf("> 5 4\n"); // 15。
printf("> 5 3\n");
printf("+ 13 14\n");
printf("+ 15 16\n");
printf("- 17\n");
printf("- 18\n"); // 20。
printf("+ 19 20\n");
printf("+ 10 19\n");
printf("+ 11 20\n");
printf("+ 12 21\n");
printf("C 22 %s\n",xi); // 25。
printf("C 23 %s\n",xi);
printf("C 24 %s\n",xi);
printf("S 25\n");
printf("S 26\n");
printf("S 27\n"); // 30
printf("+ 28 29\n");
printf("- 30\n");
printf("+ 31 32\n");
printf("C 33 -%s\n",Sxi);
printf("< 34 %d\n",W*2+6+41); // 35。 // 提前左移 41 位,加上本来应该左移的 266 位。
for(int i=36,j=63;j>=0;--j,i+=12) {
printf("< 3 %d\n",41+j); // M=m<<i;
printf("+ %d %d\n",i,i-1); // -ab+M;
printf("S %d\n",i+1); // p=S((-ab+M)<<41);
printf("< %d 179\n",i+2); // t=p<<179;
printf("> %d 178\n",i); // M>>178;
printf("+ %d %d\n",i+4,i+3);
printf("S %d\n",i+5);
printf("C %d -0.5\n",i+6);
printf("< %d %d\n",i+7,180);
printf("- %d\n",i+3); // -t;
printf("+ %d %d\n",i+9,i+8);
printf("+ %d %d\n",i+10,i-1); // -ab+M or -ab+0;
}
printf("> 803 41\n");
printf("C 804 0.100000000064\n"); // 因为 0.1>>266 时产生了一些精度误差,所以在这个地方往回补一补精度。
printf("- 805\n");
printf("O 806");
}
结语
-
本篇题解作为旷野大计算的题解,思路清晰,解法自然,逻辑严谨,你可以利用这篇题解,为自己的解题之旅进行热身。
苯蒟蒻相信,这篇优美的题解,可以给拼搏于获奖的逐梦之路上的你,提供一个有力的援助。
不好意思走错片场了。 -
整体代码就不往上放了,这里是 评测记录。
-
利用好 �(�)S(x) 函数。该函数是本题的灵魂。
这话说着容易做起来难。极限是本题的经脉,使灵魂有了承载。二者结合,我认为是本题最重要的思想。利用连续函数构造分段函数,是二者结合的具体体现。
-
注意计算节点的优化,有些可以特殊处理的就拿出来单独处理。单独处理往往能减少计算节点个数。
-
我尽可能减少计算节点,并成功把 task7,task10task7,task10 的计算节点数压到了官方题解以下。
可把我牛逼坏了。 -
关于 task4,task8task4,task8 中将函数视作直线的误差分析,同样可使用 task10task10 中泰勒展开的方式来做。
-
定义结构体、使用函数来输出、重载运算符,从而将自己的代码让程序自动按执行顺序翻译成题目所给的计算节点的形式。这样的操作更有利于调试和编程,也方便理解,而且可移植性非常强。若按照我的写法,修改代码的时候,很多表示计算节点编号的参数都需要重新计算。输出调试的时候麻烦更甚!
虽然我这种最底层编写方式能带给我些许减少计算节点的可能,但就题论题,我减少掉的那几个计算节点其实无关紧要。
所以最好不要参照我的这种写法,参照
ljc1301
dalaodalao 的题解或者da32s1da
dalaodalao 所给出的官方题解都可以。官方题解的代码写法真的让在下甘拜下风,比我的不知高明了多少,太方便了!
-
双调排序积极填坑中。
-
本题真的是在造一台计算机。也可视作使用一种较低级语言进行编程。
-
本题的
checker
可以留下来,我觉得蛮有用的。 -
本题解 posted on 01:23:48posted on 01:23:48 !
很遗憾秒数没有卡准。
-
终于做完了这道题,总体来说还是很有成就感的。
为了写这篇题解,放弃期末复习,我也是很有胆魄了(捂脸)。
我已经尽可能减少错别字了,期望能带给读者最好的阅读体验。
这篇题解基本上真实地还原了我的思考历程,
当然省略了我看题解的过程,可以算是事无巨细,为的是能够带给前来查阅题解的人一些思路,也为了将尽可能严谨的分析贡献出来,更为了将我对这道题更本质的思考展现给大家,希望能给大家一些题目以外的启迪。诚挚感谢您的阅读,希望我的题解能帮到您。祝 AC 愉快。