SCNU ACM 2016新生赛决赛 解题报告
新生初赛题目、解题思路、参考代码一览
A. 拒绝虐狗
Problem Description
CZJ 去排队打饭的时候看到前面有几对情侣秀恩爱,作为单身狗的 CZJ 表示很难受。
现在给出一个字符串代表 CZJ 前面的队列。你可以帮他一把把那些情侣分开吗?
Input
第一行是一个\(T\),代表数据的组数\(T\)(\(T\leq 100\))。
每组一个只包含大写字母的字符串\(p\)(\(1\leq len_{p}\leq 1000\)),表示 CZJ 前面的队列,保证不会有连续三个及以上相同的字母序列
Output
将队列中相同且相相邻的字母用“1”隔开并输出。
Sample Input
2
AABB
ABBA
Sample Output
A1AB1B
AB1BA
解题思路
- 签到题,由于保证不会出现3个及以上的连续相同字符,所以每次判断一下上一个字符就好;
- 直接在线处理,时间复杂度\(O(len_{p})\)。
参考代码
#include <stdio.h>
int main() {
int T;
// 读入数据组数,并忽略本行回车
scanf("%d%*c", &T);
while (T--) {
// 每次先初始化上一次读入的字符为EOF
int readChar, lastChar = EOF;
// 不断读入一个字符,直到回车
while (readChar = getchar(), readChar != '\n') {
// 如果读入的字符和上次读入字符相同,则先输出'1'分开
if (readChar == lastChar) putchar('1');
// 经过前面的处理后,把本次读入的字符输出
putchar(readChar);
// 重置上次读入的字符
lastChar = readChar;
}
// 单个数据结束,输出换行
putchar('\n');
}
return 0;
}
比赛提交代码中发现的问题
- 有选手直接提交了exe、txt文件而不是源码文件,导致编译错误;
- 部分选手对于循环理解有偏差,
for(i=0; i<=n; i++)
是执行n+1遍的,而数据只有n组,导致一直卡输入直至超时; - 部分选手误认为要读入全部数据后才能输出;实际上可以计算完一个情形后再读入下一个情形计算;
- 接上一点,其中又有部分选手因此开了像
char a[105][1600];
的数组。在内存限制范围内开数组没问题,但是不能在main()等函数中开这么大的数组;详见C/C++堆栈空间分配; - 接上一点,其中又有部分选手开了大小刚好为1000的数组;实际上是对字符串的存储理解有偏差,字符串需要额外存储一个结束符'\0',因此导致越界访问内存;
- 有选手使用了C++的string类,但可惜基本功不扎实,存在错误使用;
- 有选手一开始提交的代码中出现了“Please input ...”这样的输出,导致WA;
- 有选手把==写成了=(比如
if(a==1)
成了if(a=1)
),导致循环不结束直至TLE。
### B. Zyj 消极比赛
Problem Description
ACM 比赛剩下最后 10 分钟,其他人都已经收拾好东西准备走人,Zyj 才从睡梦中醒来。
Zyj 可以看到所有其他人的过题情况,他希望得到的名次在 a 到 b 之间,问有几种可选择的方案?
(假设其他人的提交时间即使加上罚时也早于 Zyj 的提交,毕竟剩下 10 分钟了都)
Input
第一行为 T (0 < T < 100),代表有 T 组数据。
每组数据中第一行为两个数字 m n a b,由空格隔开,代表这次比赛有 m 道题 (由于字母数量的限制,0 < m < 27)、
n 个其他人、Zyj 希望得到的名次 x 满足 a < x < b ( −1 < n, a, b < 1000)。
下一行为 Zyj 解决每道题需要的秒数 \(0\leq t_i < 999\)。
下面 n 行为每个人每道题的通过情况,1 为已通过,0 为未通过。
Output
对每个样例,输出 Case #k: ans,其中 k 为样例编号,ans 为方案数量。
Sample Input
1
4 1 0 2
300 300 300 300
0 0 0 1
Sample Output
Case #1: 6
Hint
样例解释:
Zyj 必须做出至少 2 题,又因为每个题的解题时间为 300 秒即 5 分钟,Zyj 也只能做最多 2 题,
所以 Zyj 做两题有 \(C_4^2 = 6\) 种方案。
解题思路
- 初赛这题是求组合数。但是你有没有想过为什么能用求组合数的方法来做呢?
- 还记得吗?初赛这道题没有对Zyj做每题作出限制。也正因为如此,Zyj无论做哪一题都是等价的,所以对于无序集合,我们可以求组合数。
- 但是这里更接近实际情况,Zyj做每一题的时间都是不一样的。有可能这道题做了,剩下的题就没时间做了。由于剩下的时间有限,这是个求满足特定条件的子集的问题。
- 能不能用枚举的方法呢?
- 选一道题,根据剩下的时间再选一道题,重复下去,总能把所有情况列举出来。
- 但是你想啊,一个大小为\(m\)的集合,它的非空子集有 $2^m-1 $个;
- 一个不小心你可能需要 \(O(2^m)\) 到甚至 \(O(m!)\) (姿势不对时)的复杂度把所有可行子集都枚举出来。
- 枚举太暴力了,我们可以记忆化搜索。
- 我们定义一个数组 dp[i][j][t] (\(j\leq i\))来表示在总共有 \(i\) 道题,Zyj睡醒后剩下 \(t\) 时间的情况下,Zyj做 \(j\) 题的方案数。
- 显然,对于任意 \(1\leq i\leq 26\), \(0\leq t\leq 600\) (10分钟=600秒), dp[i][0][t]=1 恒成立,均可算作1种情况。
- 设 c[i] 代表Zyj做第 \(i\) 题所需的时间,显然, dp[i][i][t] = dp[i-1][i-1][t-c[i]];
- 而Zyj做 \(0<j<i\) 题时(dp[i][j][t]),不做第 \(i\) 题的方案数为 dp[i-1][j][t],做第 \(i\) 题并在前面的题选做 \(j-1\) 题的方案数为 dp[i-1][j-1][t-c[i]]。
- 综上,我们可以得出 dp[i][j][t] = { dp[i-1][j][t] + dp[i-1][j-1][t-c[i]], if \(j\leq i\) }, { 0, if j > i }, { 1, if j = 0 }。
- 时间复杂度 \(O(300m^2)\);空间复杂度 \(O(600m^2)\),用滚动数组优化到 \(O(600m)=15600\)。
参考代码
#include <stdio.h>
#include <string.h>
const int MaxTime = 600;
int m, n, a, b;
int dp[27][601];// dp[j][t]代表Zyj在t时间内做j题
int solveCount[27];// solveCount[i] 记录其他人做i题的人数
void init() {
memset(dp, 0x0, sizeof(dp));
for (int iterTime = 0; iterTime <= MaxTime; ++iterTime)
dp[0][iterTime] = 1;
memset(solveCount, 0x0, sizeof(solveCount));
}
void prepare() {
int costTime;// 读入Zyj做每一题所需的时间
for (int iterProb = 1; iterProb <= m; ++iterProb) {
scanf("%d", &costTime);
// j=0 的情况不需要处理,已经初始化好了
// 由于使用了滚动数组,从大到小更新
for (int iterSolv = iterProb; iterSolv > 0; --iterSolv)
// 直接从Zyj解该题所需时间考虑,剩下时间小于该时间的话Zyj也解不出来
for (int iterTime = costTime; iterTime <= MaxTime; ++iterTime)
dp[iterSolv][iterTime] += dp[iterSolv - 1][iterTime - costTime];
}
}
void readSolves() {
// 读入n个人的做题情况
for (int iterPerson = 0; iterPerson < n; ++iterPerson) {
int acc = 0, solvedTag;// acc用于累加每个人做题总数,solvedTag表示是否解题
for (int iterProb = 0; iterProb < m; ++iterProb) {
scanf("%d", &solvedTag);
acc += solvedTag;
}
// 累加总共做出acc道题的人数
++solveCount[acc];
}
}
void read() {
scanf("%d%d%d%d", &m, &n, &a, &b);
prepare();
readSolves();
}
int work() {
// rank代表Zyj做i题以上能拿的名次
// Zyj有拿第一的可能,但也一定是同题数的最后一名
int rank = 1, result = 0;
// 从全做m题开始遍历到只做1题
for (int iterSolv = m; iterSolv > 0; --iterSolv) {
rank += solveCount[iterSolv];
if (a < rank && rank < b)
result += dp[iterSolv][MaxTime];
}
// 若做0题,则与其他做0题的人并列排名
if (a < rank && rank < b)
result += dp[0][MaxTime];
return result;
}
int main() {
int T;
scanf("%d", &T);
for (int cse = 1; cse <= T; cse++) {
init();
read();
printf("Case #%d: %d\n", cse, work());
}
return 0;
}
### C. 星界游神 llm
Problem Description
L2m 向星界游神学艺,星界游神教会他一种技能后大笑“后继有人”后溘然长逝。于是 L2m 成了新的星界游神。
L2m 比较强,可以收集红蓝绿三种调和之音。但是原本的星界游神比较弱,教给 L2m 的技能要消耗同等数量的调和之音才能发出。
Zyj 就想知道,L2m 最多能发多少次技能,好在 L2m 技能用光后打败他。
Input
第一行只有一个正整数 T (\(T\leq 1000\)),代表了 T 个情形。
接下来的 T 行,分别是三个整数 \(0<R,B,G<10^9\),代表大佬 L2m 所拥有的红、蓝、绿三种调和之音。
Output
请你对于每种情况输出大佬能发出多少次技能。
Sample Input
1
1 3 2
Sample Output
1
解题思路
- 这是2015年广东省赛原题,我改了下题面作为签到题放上来啦。
- 因为需要消耗同等数量的调和之音,所以三个变量之间存在制约,释放技能次数取决于三者的最小值。
- 求最小值应该很好求吧,1. if-else; 2. 条件表达式
(a<b)?(a<c?a:c):(b<c?b:c)
; 3. 使用min函数std::min(a,b)
; 4. 自己写个min函数int min(int a,int b){return a<b?a:b;}
- 时间复杂度:O(1)
#include <stdio.h>
#include <algorithm>
using std::min;
int main() {
int T, R, B, G;
scanf("%d", &T);
while (~scanf("%d%d%d", &R, &B, &G)) {
printf("%d\n", min(min(R, B), G));
}
return 0;
}
比赛提交代码中发现的问题
- 交错代码了..把别的题目的代码交上来了..;
- 又提交了exe、txt上来..;
- 恶意提交exe,当我想打开源码看交了什么鬼时,程序卡了好久..;
- 提交的代码中含有
system("pause");
,导致编译错误or超时(大概是用VC6.0写的?我建议选手们除了写作业和考试外不要使用VC6.0); - 对R,G,B三个数字做奇怪的加权or除法,没看懂为什么要这么做;
- 想将计算结果保存后再一次性输出..结果保存的还不对;还有输出漏了一个
for(i=1;i<T;i++)
;呃..再强调一遍,我们是可以计算完一个情形后立刻输出的; - 输出的时候忘记换行。你想啊,比如你要分别输出1和2,结果你输出了12,能一样吗;
- 有选手把所有情形的答案全部加起来了才输出,啊,咋回事啊,是我的题面阅读性差嘛QAQ
### D. Oyk 剪纸
Problem Description
Oyk 又和 Zlm 剪纸了。因为以往的剪纸都是一个方向的,他们决定换个方式。
把一张矩形纸分割为 a*b 的网格,每次可以剪去 n 个格子以内组成的矩形。
Oyk 希望能剪到最后一个格子,那么他应该先剪还是后剪?
如 n=5 时,下列方案中 1,2,5 为可行方案,3 不是(因为它不是个矩形),4 也不是(因为由 6>5 个格子组成)
注意,剪完之后如果纸断开了,可以任选一部分剪,如同它们没有断开一样,只是不可能同时剪到其中的多个部分;剪出来的部分就扔了,不再动。
Input
输入有多组(少于1000组)数据,处理到文件尾。每行有三个数字,a,b和 n(均为小于10000的正整数),由空格隔开。
Output
对每个样例。输出 1 如果应该先走,否则输出 2。
Sample Input
1 3 1
1 3 3
Sample Output
1
1
Hint
1 3 1 中,不管剪哪一个格子,总会剩下两个,Zlm 再剪一个剩下一个,Oyk 能剪到最后一个。
1 3 3 中,Oyk 可以一次把整个纸剪了,Oyk 还是能剪到最后一个。
解题思路
- 找规律题,但我不会。于是又让Oyk教了我一次剪纸....
- Oyk:推荐先思考的经典问题:
- 一个圆桌上放硬币,先放满(剩余空间不能再容纳)为胜,先手怎么放才能获胜;
- 变种:一个直径d圆桌放直径p圆盘,先放满为负,问先手能不能赢;
- 一维剪纸:一条长纸带划为n格;一条环形纸带划为n格;
- 首先观察任意大小的矩形网格纸,知道无论先手剪去哪部分,后手总能根据对称性剪去一样的部分;如果先手能够先挖掉矩形纸的中间一块,而使得剩下的纸仍然具有中心对称的性质,则获得了后手必胜局势(注意不连续的纸不能同时剪,并必须剪小矩形)。
- 由于对称性的存在,我们构造最小不同例子,分别是边长为\(1\times 1\)、\(1\times 2\)、\(2\times 2\)的矩形纸:
- 先验证它们是最小不同例:
- 对于\(1\times 1\)的情况,先手总是胜。在边的两侧同时添加一行或一列不影响对称性,在边的单侧添加会由于对称性而转变为另两种情况;
- 对于\(1\times 2\)的情况,先手只能全部拿完才获胜。在边的两侧同时添加一行或一列不影响对称性,在边的单侧添加会由于对称性而转变为另两种情况;
- 对于\(2\times 2\)的情况,先手只能全部拿完才获胜。在边的两侧同时添加一行或一列不影响对称性,在边的单侧添加会由于对称性而转变为另两种情况;
- 不失一般性,考察\(2\times 1\)的情况,发现旋转后与\(1\times 2\)的情况等价,不需另外考虑。
- 对于三个不同情况的等价类,边长的奇偶性相同,因此得出三种先手必胜策略:同为奇取1,同为偶取4,一奇一偶取2;
- 显然,如果需要取的最少格子数不大于N的话,先手无法占先取得对称局势,而后手总能在第一局补齐获取对称局势所需的格子,从而获得胜利。
- 先验证它们是最小不同例:
- 总结:根据两边长奇偶性考察N的大小。
- 时间复杂度:O(1)
参考代码
#include <stdio.h>
int main() {
int a, b, n;
while (~scanf("%d%d%d", &a, &b, &n))
puts(4 >> (a % 2 + b % 2) > n ? "2" : "1");
return 0;
}
### E. Zyj 穿越到古代
Problem Description
Zyj 又消极比赛了,这次他被 SCNU 的大佬们追杀穿越到了古代。他成了古希腊的一个将军,但是他的军队被 Hyr 带领 SCNU 的大佬们围困在山顶。眼看突破无望,但是 Zyj 军队里的将士又不肯投降,就想以死殉职。但是 Zyj 就不愿意了。他想:宝宝心里苦啊,不就是比赛的时候睡个觉嘛。于是他想出一个方法让自己逃脱制裁。
将军队里面剩下的所有人从 1 到 N 开始编号,每次数 M 个人,从 1 开始数。被数到的那个人以死殉职。然后下一个人继续从 1 开始数。Zyj 要逃脱就只能成为最后一个殉职的人,这样他就能乖乖投降了。请你告诉 Zyj 要选编号是多少才能逃脱制裁。
比如有 3 个人,编号 1,2,3。每次数 2 个,第一个殉职:2。剩下 1,3。第 2 个殉职:1。只剩下3。于是 zyj 一开始选择编号 3 可以逃脱。
Input
有多组数据,数据不超过 100 组,输入到文件尾结束。每组数据共一行两个数,第一个数 N 代表人的总数,第二个数 M 代表每次数多少个人(\(0\leq N\leq 1000\),\(0\leq M\leq 1000000000\))。
Output
输出一行,一个整数代表 zyj 一开始要选哪个编号才能活着。
Sample Input
5 3
4 6
Sample Output
4
3
解题思路
- 不是我出的题,只是我造的数据。数据如下:
500 999999999
500 999999797
500 999999792
500 643242600
500 123456789
491 999999999
499 999999797
503 999999792
509 643242600
521 123456789
500 1
523 1
- 可能大家都在课本上学过循环数数的方法,但这是很慢的,复杂度高达\(O(NM)\),妥妥的TLE。
- 当然,如果你不死心,一定要模拟循环数数,那也可以的,因为只有\(N\)个人,当\(M>N\)时,数到最后一个人只会再从第一个人开始数起,不可能数出第\(M\)个人来,所以循环数数的长度应该是\(M\%N\)。时间复杂度\(O(\sum^{N}_{i=2}M\%i)=o(N^2)\)。
- 我认为我的题是必须良心的,所以放过了\(O(N^2)\)的算法,因此用一个数组标记每个人,每次你算出那个人(数组元素的下标)之后,直接把数组后面的元素往前移就相当于删掉那个人了,也就是重新标号。无论是int数组前移,还是结构体数组重标号,还是vector删除元素,都是\(O(N^2)\)的时间复杂度。
- 可惜场上没有多少同学想到要\(M\%N\),几乎都直接循环\(M\)的长度。我真的不知道原因在哪里,是C语言课上的太水了吗?请你们在自己的电脑上测试一下,你的电脑跑\(10^9\)长度的循环需要多长时间。
- 正解:
- 假设当前数号到\(num\),好的他已经出局了,原来的第\(num+1\)个人成了新的第\(num\)个人;
- 要从新的第\(num\)个人开始往后数出第\(M\)个人出局,而总人数只有\(N\)个人,所以下一次会数号到\(num'=(num+M)\%N\),第\(num'\)个人出局,类推。
- 因此得到一个递推公式:\(R_i=(R_{i-1}+M)\%N\),当\(i=N\)时,总人数只剩下一个人,\(R_N\)就是Zyj想要的编号。
- 时间复杂度:\(O(N)\)
参考代码
#include <stdio.h>
int main() {
int n, m, i, s = 0;
while (~scanf("%d%d", &n, &m)) {
s = 0;
for (i = 2; i <= n; i++) {
s = (s + m) % i;
}
printf("%d\n", s + 1);
}
return 0;
}
比赛提交代码中发现的问题
- 跑了长度为\(M\)的循环,不再多说;
- 以为数据还会输入一个整数\(T\),但是这里是要读到EOF的;
- 有同学用了链表..但是链表不能做到随机查询的话,还是模拟循环数数而已;
### F. 贪吃蛇
Problem Description
贪吃蛇是一个很有名的游戏。小蛇每吃掉一个食物,就会长长一格;撞到障碍物或自己,就会结束游戏。
这里把贪吃蛇的规则略作修改:
- 蛇所在世界的长为 W,高为 H,坐标为左上角 (1,1) 至右下角 (W,H)。
- 刚开始时,蛇长度为 1,位于 (1,1) 处,初始方向向东。
- 每秒可以按下 W 表示向上,A 表示向左,S 表示向下,D 表示向右,或不按按钮,来试图对蛇进行转向,但只有试图的转向与当前方向垂直时,转向有效。
- 每秒在尝试转向后,向新方向移动 1 格(如果转向成功)。如果吃到食物,长度增加 1,否则长度不变。
- 世界是环形的,即从右边离开边界会从左边重新进入,以此类推
- 撞到自己,蛇长度恢复为 1,方向、位置不变。
Input
有多组样例,处理到文件结束。每组样例第一行为三个数字 W,H,N(0<W,H<500,0<N<100000)
下面 N 行,每行:第一个字符为 WASDNwasdn 之一,字母表示那一秒的按键,N 表示那一秒不按键;大写表示吃到食物,小写表示没有。后面紧接一个空格,然后一个 64 位无符号整数\(a_i\)。
Output
对每个样例,输出一行,其值为 \(\sum_{t=1}^{N}a_t \sum_{x=1}^{W}\sum_{y=1}^{H}3^{yW+x}c_{txy} mod 2^{64}\),其中 \(c_{txy}\) 表示第 t 秒 (x,y) 处,有蛇为 1,没有蛇为 0
Sample Input
3 2 8
a 0
s 0
D 0
N 0
n 0
w 1
s 0
n 1
Sample Output
15552
样例解释:
输出为 \(1(3^{1\times 3 + 2} + 3^{2\times 3 + 1} + 3^{2\times 3 + 2}) + 1\times 3^{1\times 3 + 2} mod 2^{64} = 9234\)
解题思路
- 模拟题。直接模拟就好,没什么特别的技巧。
- 时间复杂度:O(玄学)
参考代码(出题人的代码)
#include <stdio.h>
#include <stdlib.h>
#include <queue>
int z[500][500], q;
unsigned long long vu[300000]={1};
inline void setp(int x, int y) {
z[x][y]=q;
}
inline void clrp(int x, int y) {
z[x][y]=0;
}
inline bool seep(int x, int y) {
return z[x][y]==q;
}
int main() {
int w,h,n;
for (n=1; n<300000; n++) {
vu[n] = vu[n-1] * 3;
}
while (~scanf("%d%d%d",&w,&h,&n)) {
char c[2];
unsigned long long val=0, ali, map=vu[w+1];
q++;
int x=1, y=1, d='d';
std::queue<int> L;
L.push(w+1);
while (n--) {
scanf("%s%llu", &c, &ali);
c[1]=c[0]<'a';
if(c[1]) c[0]+=32;
switch (c[0]) {
case 'd': case 'a':
if(d=='w' || d=='s') d=c[0]; break;
case 'w': case 's':
if(d=='a' || d=='d') d=c[0]; break;
}
switch (d) {
case 'a':
x--;
if(x==0) x=w;
break;
case 's':
y++;
if(y>h) y=1;
break;
case 'd':
x++;
if(x>w) x=1;
break;
case 'w':
y--;
if(y==0) y=h;
break;
}
if (c[1]) {
//got new
} else {
int t=L.front()-1;
L.pop();
int y=t/w, x=t%w+1;
clrp(x,y);
map-=vu[w*y+x];
}
if (seep(x,y)) {
map=0;
q++;
std::queue<int>t;
std::swap(L,t);
}
L.push(y*w+x);
setp(x,y);
map+=vu[w*y+x];
val+=ali*map;
}
printf ("%llu\n", val);
}
}
### G. equation
Problem Description
给出一个各项系数均非负的多项式\(f(x)\),是否存在\(x>0\)使得\(f(x)=1\)?
Input
每行一个样例,有多个数字(均小于25000,最多精确到6位小数)分别表示各项系数。具体地说,第\(i\)个数字表示\(f\)的\((i-1)\)次方项系数。没有列出的数字系数为0。
Output
若有解则输出解,若有多个解则从小到大输出,精确到四位小数,若无解输出空行。
Sample Input
0.5 1
Sample Output
0.5000
Hint
该输入表示\(f(x)=1x+0.5=1\)解为\(x=0.5\)
解题思路
- 二分法。
- 当各项系数非负,且\(x\geq 0\)时,\(f(x)\geq 0\)恒成立且\(f(x)\)在\([0, +\infty)\)上单调递增。
- 如果\(\exists x>0, f(x)-1=0\),则\(f(0)-1<0\),即\(f(0)<1\);
- 考察增长最慢的一个有解函数\(f(x)=1\times 10^{-6}x=0.000001x\)(所给数据只精确到6位小数),也存在\(f(10^6)=1\),取最近的素数\(f(10^6+3)>1\);
- 综上,我们可以在\((0, 10^6+3)\)区间上二分出满足\(f(x)=1\)的答案。二分精度为\(10^{-6}\)。
- 当常数项大于或等于1,则\(\forall C\geq 1, \forall x>0, f(x)=g(x)+C>1\)恒成立,此时无解。
- 当\(f(10^6+3)<1\),说明出现比\(1\times 10^{-6}\)更小的系数,即系数全为0,此时无解。
- 时间复杂度:设\(f(x)\)最多有N项,最坏\(O(N log_2((10^6+3)\times 1/10^{-6}))=O(40N)\)
参考代码(出题人的代码)
#include <stdio.h>
#include <stdlib.h>
double g[1000007];
int n=0;
double f(double x) {
double ans=0, leg=1;
for (int i=0; i<n; i++) {
ans+=g[i]*leg;
leg*=x;
}
return ans;
}
int main() {
char c;
while (~scanf("%lf%c",g+n++,&c)) {
if(c=='\n') {
if (g[0]>=1 || f(1000009)<1) puts("");
else {
double L=0, R=1000009;
for (; R-L>1e-6; ) {
double M=(L+R)*.5;
if (f(M)<1) L=M;
else R=M;
}
printf("%.4f\n", L);
}
n=0;
}
}
}
01/22/17更新:牛顿迭代法
由于逆天出题人二分时没有考虑精度误差,导致部分数据与实际事实不符。这里给出牛顿迭代解
注:
- 已与Mathematica对比正确;
- deriv()是近似求导;
- 实际上我更倾向于迭代若干次,比如100次,而不是迭代到指定精度。
#include <stdio.h>
#include <math.h>
#include <utility>
const int MAXM = 1000003;
const double Eps = 1e-7;
int count = 0;
double coeff[1000007];
std::pair<double, double> calc(double x) {
double func = .0, deriv = .0, p = 1.0;
for (int i = 0; i < count; ++i) {
double item = coeff[i] * p;
func += item;
deriv += i * item;
p *= x;
}
return std::make_pair(func, deriv);
}
double func(double x) {
double res = .0, p = 1.0;
for (int i = 0; i < count; ++i) {
res += coeff[i] * p;
p *= x;
}
return res;
}
double deriv(double x) {
return (func(x + Eps / 2.) - func(x - Eps / 2.)) / Eps;
}
inline int test() {
return coeff[0] < 1 && func(MAXM) > 1;
}
void work2() {
if (test()) {
double x0 = .0, x = 1.0;
while (fabs(x - x0) > Eps) {
x0 = x;
//x = x - (func(x) - 1) / deriv(x);
std::pair<double, double> newton = calc(x);
x = x - (newton.first - 1) / newton.second;
}
if (x > 0) printf("%.4f", x);
}
putchar('\n');
}
int main() {
/* freopen("new_in.txt", "r+", stdin);
freopen("new_out2.txt", "w+", stdout);*/
char c;
while (~scanf("%lf%c", &coeff[count++], &c)) {
if (c != '\n') continue;
work2();
count = 0;
}
return 0;
}
### H. 续时间
Problem Description
Czj快毕业了,但他还想留在学校里,于是找到了SCNU最强大的魔法师Wwj买时间。
Wwj觉得他太蠢了,想要为难他一下,就用了分身术boom的一下变成了好多个Wwj。
Ccr经过一番侦查,发现\(N\)个Wwj的法力不一,第\(i\)个Wwj只能给Czj延长\(T_i\)的在校时间,但收费取决于心情,是心情的\(P_i\)倍,而当前Wwj的心情是\(D_i\)。
Hyr经过一番侦查,发现魔法值不一定要完全用完,比如当前Wwj收费\(C\),Czj可以拿出\(C * a\%\)的财宝,来换取对应只有\(T_i * a\%\)的时间。
即使Czj家财万贯,天天请Lht奶茶喝,也经不住如此高昂的花费。所以他只带总共价值为M的财宝,去找Wwj的魔法分身换时间。
Lzp想知道Czj最多能延迟多长的在校时间。
Input
第一行为一个正整数\(T<50\),代表有\(T\)种情况。
第二行输入每种情况的\(N\)和\(M\),均是正整数,\(N\leq 1000\),\(M\leq 10000\)。
接下来\(N\)行,每行有三个正整数,分别是\(T_i\)、\(P_i\)、\(D_i\),均不大于\(100\)。
Output
对于每种情况,输出Czj最大能延长的时间,由题意知可能存在小数,请保留4位小数。
Sample Input
1
3 10
10 3 2
5 2 2
5 3 2
Sample Output
15
解题思路
- 我出的题都是良心题。这题是用贪心法做。我知道新生想在短时间内想出来还是挺难的,定位是中等题(什么叫难题?你看上面那两个..正常人会做的?)
- 本来我的题面是有强调 a% 的,但是我写LaTeX公式时没有对百分号 % 做转义处理,导致pdf上没显示出来,影响部分同学没看懂题,十分抱歉。
- 正解:
- 因为财富是一定的,所以隐含了要花最少的代价,买到最多的商品。想通了这点,你就知道应该要去计算性价比(或单价)(单价与性价比互为倒数);
- 但这里的价格计算方式不一样,因为最终的花费是\(P_i\times D_i\),所以性价比是\(\frac{T_i}{P_i\times D_i}\);
- 对所有种类商品的性价比进行排序,优先买性价比最高(单价最低)的。最后由于可以买部分数量商品而不需要全买(即允许购买 a%),把剩余的钱数跟性价比一乘,就是还能买的 a% 数量的商品。
- 贪心策略证明:
- 如果在一个可行的购买方案中,存在性价比更高的商品有剩余(没购买),那么购买该性价比更高的商品,而不够买原方案中相应总价的性价比最低商品,总能获得更多数量的商品。
- 因此所有购买性价比最高商品的局部最优解的总和是全局最优解。
参考代码(这里采用结构体排序,并用了C++语法)
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
void generate() {
FILE *outFile = freopen("in.txt", "w+", stdout);
int T = rand() % 10 + 11;
fprintf(outFile, "%d\n", T);
while (T--) {
int N = rand() % 1000 + 1;
int M = rand() % 10000 + 1;
fprintf(outFile, "%d %d\n", N, M);
while (N--) {
int Ti = rand() % 100 + 1;
int Pi = rand() % 100 + 1;
int Di = rand() % 100 + 1;
fprintf(outFile, "%d %d %d\n", Ti, Pi, Di);
}
}
}
struct Magic {
int T, P, D, cost;
double ratio;
bool operator<(const Magic &c) const {
return fabs(ratio - c.ratio) > 1e-7 && ratio < c.ratio;
}
} magic[1010];
int main() {/*
generate();
freopen("in.txt", "r+", stdin);
freopen("out.txt", "w+", stdout);*/
int T, N, M;
scanf("%d", &T);
while (T--) {
scanf("%d%d\n", &N, &M);
for (size_t i = 0; i < N; ++i) {
int Ti, Pi, Di;
scanf("%d%d%d\n", &magic[i].T, &magic[i].P, &magic[i].D);
magic[i].cost = magic[i].P * magic[i].D;
magic[i].ratio = (double) magic[i].T / magic[i].cost;
}
std::sort(magic, magic + N);
double res = 0.;
for (size_t i = N - 1; i >= 0; --i)
if (M > magic[i].cost) {
M -= magic[i].cost;
res += magic[i].T;
} else {
res += M * magic[i].ratio;
M = 0;
break;
}
printf("%.4f\n", res);
}
return 0;
}
出题及解题总结
我校ACM吃枣药丸
建议、意见、吐槽
欢迎在下方评论区提出问题,12月内我都会回复and更新。
本文基于知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议发布,欢迎引用、转载或演绎,但是必须保留本文的署名BlackStorm以及本文链接http://www.cnblogs.com/BlackStorm/p/SCNUCPC_2016_For_Freshman_Final_Solution.html,且未经许可不能用于商业目的。如有疑问或授权协商请与我联系。