P1737 [NOI2016] 旷野大计算 题解
littlez_meow · · 题解
我的一次伟大的尝试。
有一说一,这道题和游戏《A=B》好像。
题目指路
前言
让我们看看有哪些可用操作吧!
对三个扣分节点不予理睬,除了正常的输入输出、变量和常量加法、变量取反、乘或除以
既然题面都给了我们
【性质 1】
证明:当
证毕
【性质 2】
证明:
证毕
【性质 3】
证明:
证毕
事实上,函数
最后碎碎念一下,题目里的输入有个小陷阱,只有
下面,进入正题。
task 1
概况
输入:
输出:
难度:受精卵
分析
两行输入,一行加法,一行左移,一行取反,一行输出
代码
共
printf("I\n");
printf("I\n");
printf("+ 1 2\n");
printf("< 3 1\n");
printf("- 4\n");
printf("O 5\n");
task 2
概况
输入:
输出:
难度:满月
分析
一看这个式子,很像
这辈子不可能用乘法的,但怎么求
所以
一行输入,一行左移,一行加法,一行取反,一行
代码
共
printf("I\n");
printf("< 1 4\n");
printf("+ 1 2\n");
printf("- 3\n");
printf("S 4\n");
printf("O 5\n");
task 3
概况
输入:
输出:
难度:幼升小
分析
突然就难起来了。
虽然那个比较函数很诱人,但用一个四分没了。
回想上一题,那个函数
……吗?
你看它的数据范围,整数部分一千位,小数部分九十位,谁没事会想写高精度呢?就连 spj 都只精确到小数点后九位。
再想想实数乘法法则,任何一个数乘以一个很大的正数,绝对值变得很大,符号不变。这个很大的整数很容易通过左移得到。
换句话说,我们要构造一个函数
-
\lim\limits_{x\to \infty} f_3(x)=1 -
\lim\limits_{x\to -\infty} f_3(x)=-1 -
f_3(0)=0
数感好的同学已经发现,
接下来就要决定那个非常非常大的正数取
列出不等式
其中,
借助某些工具,解得
当然,考试的时候可没有工具解。可以拿 python 跑二分。不过更好的方案是带一个较大的数进
综上,
顺带一提,
一行输入,一行左移,一行
代码
共
printf("I\n");
printf("< 1 100\n");
printf("S 2\n");
printf("< 3 1\n");
printf("C 4 -1\n");
printf("O 5\n");
task 4
概况
输入:
输出:
难度:小学奥数
分析
上面那一问的结果乘以
让我们试图模仿一下。
直接求肯定不现实,我们考虑把它写成
也就是说,我们要求
回头看看
至于如何缩小,我们可以使用
接下来,我们就需要把正负分开。上一问中,我们使用了
我们需要使用偏移,使结果对于自变量正负显著不同。
这个偏移肯定要用到
你看那个系数
这个偏移要在大于
加在
再看导数拟合的一次函数,
在
当
我们现在还拥有一个正负敏感的变量——
由于
范围懒得算了,想算自己算吧,我就取
代码
共
printf("I\n");
printf("C 1 0.00000000000000000001\n");
printf("< 2 250\n");
printf("S 3\n");
printf("< 4 250\n");
printf("> 1 248\n");
printf("+ 6 5\n");
printf("S 7\n");
printf("C 8 -0.5\n");
printf("< 9 251\n");
printf("- 10\n");
printf("+ 11 5\n");
printf("+ 12 1\n");
printf("O 13\n");
task 5
概况
输入:
输出:把
难度:小升初
分析
众所周知,小升初试题难度比小学奥数简单。
很简单,每个数按权值位移最后相加就完了。
但是,请不要为了循环美观去乘
输入三十二行,左移三十一行,相加三十一行,输出一行。
代码
共
对了,下文所有代码中 F(i,a,b) R(i,a,b) 分别指 for(int i=a;i<=b;++i) for(int i=a;i>=b;--i)。
F(i,1,32) printf("I\n");
F(i,1,31) printf("< %d %d\n",i,32-i);
printf("+ 63 32\n");
F(i,1,30) printf("+ %d %d\n",63+i,32+i);
printf("O 94\n");
task 6
概况
输入:
输出:
难度:中考
分析
难度又加回来了,但也没 task 4 难。
二进制下的数
然后,我们让
问题就回到了比较大小。看看第三个点,不正是比较大小的问题吗?把
再看每次用
然后是和 task 4 一样严峻的问题,如果相等减出
等等,你看循环里那么多偏移,为什么偏偏要专门在开头时浪费一行偏呢?我们每次偏移少偏一点,同样可以解决问题。因此,把偏移少偏个
最后,因为是正整数,最低位不用比,直接就是。
三十一次比较,每次六行,输入一行,左右偏移各一行,输出一行
解决!
代码
共
printf("I\n");
printf("< 1 50\n");
for(int i(3),j(31);j;--j,i+=6){
char str[1001];
sprintf(str,"%Lf",pow((long double)2.0,50+j)-112590);
printf("C %d -%s\n",i-1,str);
printf("S %d\n",i);
printf("O %d\n",i+1);
printf("- %d\n",i+2);
printf("< %d %d\n",i+3,50+j);
printf("+ %d %d\n",i+4,i-1);
}
printf("> 188 50\n");
printf("O 189\n");
task 7
概况
输入:
输出:
难度:高中
分析
task 5 和 task 6 的综合运用!
首先用 task 6 的代码转二进制,异或后再用 task 5 的代码转回来。
也就是说,目前首要目标为解决函数
我相信你们老师或教练都讲过,异或是二进制下的不进位加法,不信你看:
自然而然想到加法。
但是,加法在
结合 task 4 的经验,我们给
让我们记这个数为
你看那个
再仔细看看,
就是用
解决!……了吗?
回到我刚才说的,异或是二进制下的不进位加法。这说明我们把二进制变回十进制时,可以优化。
但上面那种方法足以得到满分,这个优化留给读者思考。(其实是懒)
代码
共
printf("I\n");
printf("I\n");
printf("< 1 50\n");
for(int i(4),j(31);j;--j,i+=5){
char str[1001];
sprintf(str,"%Lf",pow((long double)2.0,50+j)-112590);
printf("C %d -%s\n",i-1,str);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,50+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("> 158 50\n");
printf("< 2 50\n");
for(int i(161),j(31);j;--j,i+=5){
char str[1001];
sprintf(str,"%Lf",pow((long double)2.0,50+j)-112590);
printf("C %d -%s\n",i-1,str);
printf("S %d\n",i);
printf("- %d\n",i+1);
printf("< %d %d\n",i+2,50+j);
printf("+ %d %d\n",i+3,i-1);
}
printf("> 315 50\n");
for(int i(5),j(1);j<=31;++j,i+=5) printf("+ %d %d\n",i,i+157);
printf("+ 159 316\n");
for(int i(317),j(349);i<=348;j+=6,++i){
printf("C %d -1.5\n",i);
printf("< %d 50\n",j);
printf("S %d\n",j+1);
printf("< %d 1\n",j+2);
printf("- %d\n",j+3);
printf("+ %d %d\n",j+4,i);
}
for(int i(355),j(31);j>=1;--j,i+=6) printf("< %d %d\n",i,j);
for(int i(540),j(1);j<=31;++j,++i) printf("+ %d %d\n",i,i+31);
printf("O 602\n");
task 8
概况
输入:
输出:
难度:高中竞赛
分析
哎呀,终于不是二进制了。
看看输出,是一个关于
结合 task 4 肯定又要用到导数。我们要求出
先来求个导,有
这玩意是个连续函数,且最大值为
设这个解为
我们肯定可以继续用先缩小再放大的策略,即
那么最后就是求解
眼睛看上去,
至于求解,八仙过海各显神通,checker、牛顿迭代、二分、计算器均可。
我用的计算器。化简式子得到
再用 checker 求得
至于
代码
共
printf("I\n");
printf("> 1 250\n");
printf("C 2 2.063437068895560546727281172620\n");
printf("S 3\n");
printf("C 4 -0.887298334620741688517926539978236773937633025540819832675154107295416657242528255923059519\n");
printf("< 5 250\n");
printf("O 6\n");
task 9
概况
输入:
输出:
难度:高考
分析
其实也没那么难,综合性强而已。
排序无论如何都绕不开一个操作:比较并交换。
至于交换,由于每个节点只能被赋值一次,后面只能调用。因此,我们要用一个尽量不用中间变量的方法。
你看那个异或,
结合大小关系,我们可以得出下面这段交换代码:
h=a0+b0;
a1=h-min(a0,b0);
b1=h-a1;
也就是说,问题转化为实现
这似乎不是很好实现。让我们改写一下上面那段代码:
h=a0+b0;
a1=a0-min(a0-b0,0);
b1=h-a1;
为什么要改写成这种
那这不就解决了吗?
题目给的操作不适合复杂的排序。让我们寻找原始的排序:冒泡排序。
最后模拟就完了。注意,常用的冒泡代码是倒着的,需要倒序输出。同样,因为节点的变化难以预测,可以用数组来记录节点号。
当然,你要用其他排序我也支持,但我不想再去思考其他怎么写,就用冒泡了。(甚至我觉得冒泡都不好写,看着题解调的。感谢 @da32s1da 题解的代码。)
代码
共
short line[17]={};
F(i,1,16) printf("I\n"),line[i]=i;
for(int i(1),j(16);i<=15;++i) for(int k(i+1);k<=16;++k,j+=19){
printf("- %d\n",line[k]);
printf("+ %d %d\n",line[i],line[k]);
printf("+ %d %d\n",line[i],j+1);
printf("C %d 0.00000000000000000001\n",j+3);
printf("< %d 250\n",j+4);
printf("S %d\n",j+5);
printf("< %d 250\n",j+6);
printf("> %d 249\n",j+3);
printf("+ %d %d\n",j+7,j+8);
printf("S %d\n",j+9);
printf("< %d 1\n",j+10);
printf("- %d\n",j+6);
printf("+ %d %d\n",j+11,j+12);
printf("C %d -1\n",j+13);
printf("< %d 250\n",j+14);
printf("- %d\n",j+15);
printf("+ %d %d\n",line[i],j+16),line[i]=j+17;
printf("- %d\n",line[i]);
printf("+ %d %d\n",j+2,j+18),line[k]=j+19;
}
R(i,16,1) printf("O %d\n",line[i]);
task 10
概况
输入:
输出:
难度:大学
分析
大学比高考要更摆烂些。
好像绕不开乘法了呢,又不能用乘法节点,怎么办?
回到乘法在小学中的定义,乘法是连加的简记。
一个一个加肯定不行,
在历年来的 NOI 中,考察过这样的一个知识点——龟速乘。它适用于两数相乘取模但会溢出的情况。思想与快速幂类似,二进制拆分,按权相乘,最后相加。
例如:
不过这里一千位,肯定不会溢出,因此取模放在最后进行,先着重处理乘法。
在二进制拆分后,根据二进制该位是
写成分段函数,有
这玩意是不是除了范围又很像 所以说小学奥数是世界的尽头。
那么就剩取模了。取模可以用减法实现,但一个个减又太浪费了。这时就可以再用二进制。