书名:信息学竞赛宝典 基础算法
ISBN:978-7-115-59659-8
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 张新华 胡向荣 葛 阳
责任编辑 赵祥妮
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e59659”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
本书的核心是信息学竞赛中经常用到的9种基础算法,包括模拟算法、递归算法、枚举算法、递推算法、分治算法、贪心算法、排序算法、高精度算法和搜索算法。本书直接以各类竞赛真题入手,内容讲解上由浅入深,设计合理:对于引入新知识点的题目,书中会提供该题目的完整参考代码,但随着读者对此知识点理解的逐步加深,后续的同类型题目将逐步向仅提供算法思路、提供伪代码和无任何提示的方式转变;对于一些思维跨度较大的题目,本书会给出一定的提示;此外,本书还安排了相关习题。
本书中的每一章都分为普及组和提高组两部分。普及组涉及的内容对应NOIP普及组难度,读者可初步掌握每种算法的思想和用法;提高组涉及的内容对应NOIP提高组难度,读者可复习和提高已讲解过的算法内容。
本书既适合作为学习了C++语言和算法入门知识的读者的进阶教材,也适合作为有一定编程基础的读者学习算法的独立用书。
主任: 刘建祥 江玉军
副主任: 宋建陵 梁靖韵
成员: 严开明 张新华
谢春玫 葛 阳
徐景全 黎旭明
袁颖华 伍婉秋
黎伟枝 黄钰彬
钟腾浩 刘路定
热则古丽
随着计算机逐步深入人类生活的各个方面,利用计算机及其程序设计来分析、解决问题的算法在计算机科学领域乃至于整个科学界的作用日益明显。相应地,各类以算法为主的编程竞赛也层出不穷:在国内,有全国青少年信息学奥林匹克联赛(National Olympiad in Informatics in Provinces,NOIP),该联赛与全国中学生生物学联赛、全国中学生物理竞赛、全国高中数学联赛、全国高中学生化学竞赛,并称为国内影响力最大的“五大奥赛”;在国际上,有面向中学生的国际信息学奥林匹克竞赛(International Olympiad in Informatics,IOI),面向亚太地区在校中学生的信息学科竞赛,即亚洲与太平洋地区信息学奥林匹克(Asia-Pacific Informatics Olympiad,APIO)以及由国际计算机学会(Association for Computing Machinery,ACM)主办的面向大学生的国际大学生程序设计竞赛(International Collegiate Programming Contest, ICPC)等。
各类编程竞赛的参赛选手不仅要具有深厚的计算机算法功底、快速并准确编程的能力以及创造性的思维,还要有团队合作精神和抗压能力,因此编程竞赛在高校、IT公司和其他领域中获得越来越多的认同与重视。编程竞赛的优胜者更是Microsoft、Google、百度、Meta(原Facebook)等全球知名IT公司争相高薪招募的对象。因此,除了参加各类编程竞赛的选手外,很多不参加此类竞赛的研究工作者和IT行业的从业人士,也都希望能进行这方面的专业训练,并从中得到一定的收获。
经常有人说:“我不学算法也照样可以通过编程开发软件。”那么,为什么我们还要学习算法呢?
首先,算法(algorithm)一词源于算术(algorism),具体地说,算法是指由已知推求未知的运算过程。后来,人们把它推广为一般过程,即把完成某一工作的方法和步骤称为算法。一个程序要完成一个任务,多会涉及算法的实现,算法的优劣直接决定了程序的优劣。因此,算法是程序的“灵魂”。学好了算法,就能够设计出更加优异的软件,以非常有效的方式实现复杂的功能。例如,要设计一个具有较强人工智能的人机对弈棋类游戏,程序员没有深厚的算法功底是很难实现的。
其次,算法是对事物本质的数学抽象,是初等数学、高等数学、线性代数、计算几何、离散数学、概率论、数理统计和计算方法等知识的具体运用。真正懂计算机的人(不是“编程匠”)通常都在数学上有相当高的造诣,他们既能用科学家的严谨思维来求证,也能用工程师的务实手段来解决问题——这种思维和手段的最佳演绎之一就是“算法”。学习算法,能锻炼我们的思维,使思维变得更清晰、更有逻辑,更有深度和广度。学习算法更是培养逻辑推理能力的非常好的方法之一。因此,学习算法,其意义不仅在于算法本身,更重要的是,对我们日后的学习、生活和思维方式也会产生深远的影响。
最后,学习算法很有意思、很有趣味。所谓“技术做到极致就是艺术”,当一个人真正沉浸到算法研究中时,他既会感受到精妙绝伦的算法的艺术之美,也会为它“巧夺天工”的构思而深深震撼,并从中体会到一种不可言喻的美感和愉悦。虽然算法的那份“优雅”与“精巧”很吸引人,但也令很多人望而生畏。事实证明,对很多人来说,学习算法的确是一件非常有难度的事情。
为了提高读者的学习效率,本书直接以各类竞赛真题入手,以精练而准确的语言,全面细致地介绍了编程竞赛中经常用到的各类基础算法;为了帮助读者更深刻地理解和掌握算法的思想内涵,本书还通过精挑细选,由浅入深地安排了相关习题。考虑到读者的接受水平,一般引入新知识点的题目时,书中会提供该题目的完整参考代码以供读者参考,但随着读者对知识点的理解逐步加深,后续的同类型题目将逐步向仅提供算法思路、提供伪代码或无任何提示的方式转变。此外,对于一些思维跨度较大的题目,本书会酌情给予读者一定的提示。
本书每章分为普及组和提高组两部分。通常,普及组所涉及的内容对应NOIP普及组难度,提高组所涉及的内容对应NOIP提高组难度。一个合理的学习安排是将本书的内容分为两个阶段学习,即第一个阶段学习各章的普及组内容,初步掌握每种算法的思想和用法,第二个阶段学习各章的提高组内容,作为之前学习的算法内容的复习和提高。
为了帮助读者通过本书更好地掌握算法知识,本书提供了丰富的配套资源,包括源码、题目讲解视频、PPT。读者可通过以下方式获取配套资源。
● 源码和PPT的下载地址为https://box.ptpress.com.cn/y/59659
● 题目讲解视频可在线观看。
方法一:在异步社区网站搜索本书书名,进入本书页面,单击【在线课程】,可在线观看讲解视频。
方式二:“内容市场”微信小程序或App中搜索本书书名,即可在线观看视频。
本书可作为NOIP复赛的教材和ICPC的参考与学习用书,也可作为计算机专业学生、IT工程师、科研人员、算法爱好者的参考和学习用书。
本书既可以作为学习完《编程竞赛宝典:C++语言和算法入门》的读者继续学习的教材,也可以作为有一定编程基础的读者学习算法的独立用书。
感谢全国各省市中学、大学的信息学奥赛指导老师,他们给本书提了许多真诚而有益的建议,并对编者在写书过程中遇到的一些困惑和问题给予了热心的解答。
在本书编写过程中,编者使用了NOIP的部分原题、在线评测网站的部分题目,并参考和收集了其他创作者发表在互联网、杂志等媒体上的相关资料,无法一一列举,在此一并表示衷心感谢。
感谢卷积文化传媒(北京)有限公司CEO高博先生和他的同事。
由于编者水平所限,书中难免存在不妥之处,欢迎各位同人或读者赐正。读者如果在阅读中发现任何问题,请发送电子邮件到hiapollo@sohu.com。也希望读者对本书提出建设性意见,以便修订再版时改进。
本书对应的题库网址为www.magicoj.com,题库正在不断完善中。
希望本书的出版,能够给学有余力的中学生、计算机专业的大学生、程序算法爱好者以及IT从业者提供学习算法有价值的参考。
广州市第六中学强基计划基地教材编委会
2023年1月
模拟算法即程序完整地按题目所描述的方式运行,最终得到答案。模拟算法通常对算法设计要求不高,但是要求编程者选择最合适的数据结构进行模拟,一般代码量略大。
laura
每个人都准备了一些钱用于给他的朋友们送礼物,他们把准备的钱平分后购买礼物给各自的朋友们,所有人送礼物的钱都是整数,而且尽可能多准备,没能花出的钱由送礼物者自己保留。
请你统计每个人因此而产生的最终盈亏数。
第1行为一个整数n(2≤n≤10),表示总人数。
随后用n行描述每个人的名字,假设没有人的名字会长于14个字符。
随后描述每个人送礼物的情况:每个人对应的第1行是他的名字,第2行有两个数字,第1个数字为他准备的钱的金额(范围为0~2000),第2个数字是他要送礼物的朋友数m,随后m行为他要送礼物的朋友的名字。
输出n行,即按输入顺序输出每个人的名字和他因互送礼物而产生的盈亏数,名字与数字之间以空格间隔。
5
dave
laura
owen
vick
amr
dave
200 3
owen
vick
owen
500 1
dave
amr
150 2
vick
owen
laura
0 2
amr
vick
vick
0 0
dave 302
laura 66
owen -359
vick 141
amr -150
按题目的要求模拟计算即可。保存每个人的名字和钱的金额可以使用结构体或者标准模板库(Standard Template Library,STL)里的map。计算时注意除数为0时的处理。
参考代码如下。
1 //互送礼物
2 #include <bits/stdc++.h>
3 using namespace std;
4
5 map<string,int>ans; //使用map,名字为数组下标(索引)
6
7 int main()
8 {
9 int n,money;
10 cin>>n;
11 string name[15],s,Friend; //friend为关键字,故Friend 中的F大写
12 for(int i=1; i<=n; i++)
13 cin>>name[i];
14 for(int i=1,num; i<=n; i++)
15 {
16 cin>>s>>money>>num;
17 for(int j=1; j<=num; j++)
18 {
19 cin>>Friend;
20 ans[Friend]+=money/num; //该朋友收到的礼物对应的钱的金额
21 }
22 if(num!=0) //必须先确保除数不为0
23 ans[s]=ans[s]-money+money%num; //计算本人盈亏数
24 }
25 for(int i=1; i<=n; i++)
26 cout<<name[i]<<' '<<ans[name[i]]<<endl;
27 return 0;
28 }
有一条从上到下垂直于地面的线段,长为L,可用坐标从上向下标记为1,2,…,L,无数的“幽灵粒子”在该线段上的初始坐标均为整数且各不相同。“幽灵粒子”的初始移动方向只有两个,即向上移动或者向下移动,“幽灵粒子”在任何时候的移动速度均为1。
多个“幽灵粒子”同向移动时,坐标可以重叠(要不怎么叫“幽灵粒子”呢?),但异向面对面碰到时,两个“幽灵粒子”均会改变方向反向移动,改变方向不需要时间。
当“幽灵粒子”移到坐标0或L+1的位置时就会消失,求所有“幽灵粒子”消失所需要的最短时间和最长时间。
第1行为一个整数N(1≤N≤5000),表示“幽灵粒子”的数量。
第2行为一个整数L(N≤L≤10000),表示线段的长度。
第3行为N个整数,表示“幽灵粒子”的初始坐标。
两个整数,表示“幽灵粒子”消失所需要的最短时间和最长时间。
3
5
1 2 3
3 5
无数小球沿着每个平台左右滚动并下落。1,2,3,4,5,这5个平台如图1.1所示。
图1.1
小球从平台5向左滚动会落到平台4上,向右滚动会落到平台1上;小球从平台4向左滚动会落到平台2,向右滚动会落到平台1;小球从平台3向左滚动会落到平台2,向右滚动会落到平台1;小球从平台2向左滚动会落到地面(以0表示),向右滚动会落到平台1……
已知平台不会两两重叠,也不会有两个平台的边缘碰在一起。试输出所有平台上的小球左右滚动后落到的平台的序号(序号由输入顺序决定,第1个输入的平台序号为1)。
第1行为一个整数N(1≤N≤1000),表示平台数量。
接下来N行中,每行有3个整数H、L、R (0≤H、L和R≤50000),分别代表平台的高度、左端点坐标和右端点坐标。
输出共N行,每行两个数,分别为从平台左端点和右端点落下后到达的平台的序号。
5
1 0 5
2 0 2
3 1 2
4 1 3
5 2 3
0 0
0 1
2 1
2 1
4 1
如果在输入的字符串中含有类似于“d-h”或者“4-8”的子串,我们就把它当作一种简写。输出时,用连续递增的字母或数字串替代其中的“-”,即将上面的两个子串分别输出为“defgh”和“45678”。在本题中,我们通过增加一些参数,使字符串的展开更为灵活。约定如下。
(1)遇到下面的情况需要进行字符串的展开:在输入的字符串中,出现了“-”,“-”两边同为小写字母或同为数字,且按照ASCII的顺序,“-”右边的字符严格大于左边的字符。
(2)参数p1:当p1=1时,对于字母子串,填充小写字母;p1=2时,对于字母子串,填充大写字母(这两种情况下数字子串的填充方式相同);p1=3时,不论是字母子串还是数字子串,都用与要填充的字母或数字个数相同的星号“*”来填充。
(3)参数p2:填充字符的重复个数。p2=k表示同一个字符要连续填充k次。例如当p2=3时,子串“d-h”应扩展为“deeefffgggh”。“-”两边的字符不变。
(4)参数p3:确定是否改为逆序。p3=1表示维持原来顺序,p3=2表示采用逆序输出,注意这时候仍然不包括“-”两边的字符。例如当p1=1、p2=2、p3=2时,子串“d-h”应扩展为“dggffeeh”。
(5)如果“-”右边的字符恰好是左边字符的后继,删除中间的“-”,例如“d-e”应输出为“de”,“3-4”应输出为“34”。如果“-”右边的字符按照ASCII的顺序小于或等于左边的字符,输出时,保留中间的“-”,例如“d-d”应输出为“d-d”,“3-1”应输出为“3-1”。
第1行为用空格分隔的3个正整数,依次表示参数p1、p2、p3。
第2行为字符串,仅由数字、小写字母和“-”组成,行首和行末均无空格。
只有一行,为展开后的字符串。
1 2 1
abcs-w1234-9s-4zz
abcsttuuvvw1234556677889s-4zz
2 3 2
a-d-d
aCCCBBBd-d
3 4 2
di-jkstra2-6
dijkstra2************6
40%的数据满足:字符串长度不超过5。
100%的数据满足:1≤p1≤3,1≤p2≤8,1≤p3≤2,字符串长度不超过100。
虽然这是一道简单的模拟题,但是细节要考虑周全,例如“-”可能出现在字符串的第一个或最后一个,也可能连续出现。
此外,对于字符c来说,判断它是否为小写英文字母可以用islower(c),判断它是否为大写英文字母可以用isupper(c),判断它是否为数字可以用isdigit(c),将之转为小写字母可以用tolower(c),将之转为大写字母可以用toupper(c)。
对一个由n个整数构成的序列有以下两种操作。
操作1为“1 x y”,表示所有a[kx](k为正整数,kx≤n)的值都加上y(|y|≤1000000)。
操作2为“2 i”,表示输出a[i](i≤n,操作数不超过10000条)的值。
第1行为两个整数n和m(n≤1000000,m≤100000),表示有n个数,m条操作。
第2行为n个数(这些数的绝对值小于或等于1000000)。
随后m行为m条操作。
输出若干行,每行对应完成一次操作2后输出的值。
5 4
6 9 9 8 1
2 4
1 2 5
1 3 1
2 4
8
13
因为数据规模过大,在执行操作1时,如果将所有a[kx] 逐个加上y,显然会超时,所以需要考虑更优的算法。
假设n×n台计算机组成了一个n×n的矩阵,初始时有的计算机感染了病毒,以后每隔一小时其会使邻近的未安装杀毒软件的计算机染上病毒,试计算在m小时后感染病毒的计算机数。
第1行为一个整数n(n≤100)。
接下来n行,每行n个字符。其中“*”表示初始时未感染病毒的计算机,“#”表示该计算机已安装杀毒软件,“@”表示初始时已感染病毒的计算机。
最后一行是一个整数m(m≤100),表示小时数。
一个整数,即第m小时后感染病毒的计算机数。
5
****#
*#*@*
*#@**
#****
*****
4
16
设“C”表示猫,“M”表示老鼠,“*”表示障碍,“.”表示空地。猫和老鼠在10×10的矩阵中,例如:
*...*.....
......*...
...*...*..
..........
...*.C....
*.....*...
...*......
..M......*
...*.*....
.*.*......
初始时猫和老鼠都面向北方(矩阵方向为上北下南、左西右东),它们每秒走一格,如果它们在同一格中,那么猫就抓住老鼠了(“对穿”是不算的)。猫和老鼠的移动方式相同:平时沿直线走,下一步如果会碰到障碍物或者出界,就用1秒的时间右转90°。
试计算猫抓住老鼠需要多少秒。
第1行为一个整数N(N≤10),表示有N组测试数据。
每组测试数据为10行10列,格式如题目所描述。
如果100步内无解,输出-1,否则输出猫抓住老鼠的时间。
1
*...*.....
......*...
...*...*..
..........
...*.C....
*.....*...
...*......
..M......*
...*.*....
.*.*......
49
设(x,y)为老鼠的坐标,(X,Y)为猫的坐标,0,1,2,3表示猫/老鼠移动的4个方向,每次按题目描述的移动方式更改老鼠和猫的坐标,直至两者坐标重合或步数超过100步为止。
参考代码如下。
1 //猫和老鼠
2 #include <bits/stdc++.h>
3 using namespace std;
4
5 int main()
6 {
7 int N,x,y,X,Y; //(x,y)为老鼠的坐标,(X,Y)为猫的坐标
8 cin>>N;
9 for (int k = 0; k < N; k++)
10 {
11 int m=0,c=0,count=0; //m为猫的方向,c为老鼠的方向
12 string Map[10]; //储存地图
13 for (int j = 0; j < 10; j++)
14 cin>>Map[j]; //一次读一行
15 for (int i = 0; i < 10; i++)
16 for (int j = 0; j < 10; j++)
17 if (Map[i][j] == 'C') //获取猫所在的位置并标记
18 {
19 X = i;
20 Y = j;
21 }
22 else if (Map[i][j] == 'M') //获取老鼠所在的位置并标记
23 {
24 x = i;
25 y = j;
26 }
27 while (count < 100 && (X!=x || Y!=y)) //未到100步或未抓到则继续
28 {
29 if (m==0 && x-1>=0 && Map[x-1][y]!='*')//模拟老鼠的移动
30 x--;
31 else if (m==1 && y+1<10 && Map[x][y+1]!='*')
32 y++;
33 else if (m==2 && x+1<10 && Map[x+1][y]!='*')
34 x++;
35 else if (m==3 && y-1>=0 && Map[x][y-1]!='*')
36 y--;
37 else
38 m=(++m)%4; //改变方向
39 if (c==0 && X-1>=0 && Map[X-1][Y]!='*') //模拟猫的移动
40 X--;
41 else if (c==1 && Y+1<10 && Map[X][Y+1]!='*')
42 Y++;
43 else if (c==2 && X+1<10 && Map[X+1][Y]!='*')
44 X++;
45 else if (c==3 && Y-1>=0 && Map[X][Y-1]!='*')
46 Y--;
47 else
48 c=(++c)%4; //改变方向
49 ++count;
50 }
51 printf("%d\n",(X == x && Y == y)?count:-1);
52 }
53 return 0;
54 }
一张8×8的棋盘,棋盘上有一些棋子和一个玩偶,操作方式有4种,描述如下。
(1)move n:n是非负整数,表示玩偶按目前所在方向前进n步,如果即将走出棋盘,则停止;如果面前有棋子,则将其向前推一步,棋子可以被推出棋盘。
(2)turn left:向左转90°。
(3)turn right:向右转90°。
(4)turn back:向后转。
已知玩偶的初始位置和方向,求经过一系列操作后的棋盘状态。
输入前8行,每行8个字符,表示棋盘初始状态。其中“.”表示该格为空,字母表示棋子,不同字母表示不同的棋子。玩偶所在位置用“∧”“<”“>”“∨”这4个符号中的1个表示,分别表示上、左、右、下4个方向。
接下来有若干行,每行表示一个操作,最后一行以“#”结束。操作数不超过1000个。
输出8行,每行8个字符,表示经过一系列操作后棋盘和玩偶的状态。
......bA
.....^..
........
........
........
........
........
........
move 2
turn right
move 1
#
......>b
........
........
........
........
........
........
........
农夫有N头奶牛,可由于产奶太少,他决定以后把每天产奶最少的奶牛卖给肉铺老板,但如果当天不止一头奶牛产奶最少,便“放过”它们。设奶牛产奶是周期性的,问最后有多少头奶牛幸存。
第1行为一个整数T(1≤T≤500),表示有T组测试数据。
每组数据的第1行为一个整数N(N≤1000),表示奶牛总数。
随后N行表示每头奶牛的产奶周期(不超过10天)以及每天的产奶量(产奶量不超过250)。
输出幸存的奶牛数(可能全被卖)及最后一头奶牛是在哪一天被卖的。
1
4
4 7 1 2 9
1 2
2 7 1
1 2
2 6 (2指最后剩下2头奶牛,6指最后一头奶牛是在第6天被卖的。)
普通的模拟算法效率很低,可以试着优化。
由于每头奶牛的产奶周期不会超过10天,因此几头奶牛具有同样的产奶周期的概率很大。而具有同样产奶周期的奶牛的“命运”是有紧密关联的,即任意一天有奶牛被卖,假设被卖的是这几头奶牛中的一头,那么它肯定是其中产奶量最少的一头。因此,可以将具有相同“命运”的奶牛们作为一个整体来维护(前期可用STL中的multiset容器实现,后期可以用效率更高的手写堆排序实现),每次将其中产奶量的最小值和其他整体进行比较,当奶牛被卖后重新维护该整体,即可大大减少计算量。
本题中,我们将用符号c
表示对c向下取整,例如:
3.0
=
3.1
=
3.9
=3。
“蛐蛐国”最近蚯蚓成灾了!“蛐蛐国王”只好去请“神刀手”来帮他们消灭蚯蚓。
蛐蛐国里现在共有n(n为正整数)只蚯蚓。每只蚯蚓都有一定的长度,我们设第i只蚯蚓的长度为ai ( i=1,2,…,n),并保证所有蚯蚓的长度都是非负整数(可能存在长度为0的蚯蚓)。
每一秒,神刀手都会在所有的蚯蚓中准确地找到最长的那一只(如有多只则任选一只),然后将其切成两半。神刀手切开蚯蚓的位置由常数p(p是满足0<p<1的有理数)决定,设这只蚯蚓长度为x,神刀手会将其切成两只长度分别为 px
和x-
px
的蚯蚓。特殊地,如果这两个数的其中一个为0,则这只长度为0的蚯蚓也会被保留。此外,除了刚刚产生的两只新蚯蚓,其余蚯蚓的长度都会增加q(q是一个非负整数)。
蛐蛐国王知道这样不是长久之计,因为蚯蚓不仅会越来越多,还会越来越长。蛐蛐国王决定求助一位有着“洪荒之力”的神秘人物,但是救兵还需要m(m为非负整数)秒才能到来……蛐蛐国王希望知道:
m秒内,每一秒被切断的蚯蚓被切断前的长度(有m个数);
m秒后,所有蚯蚓的长度(有n+m个数)。
第1行包含6个整数n,m,q,u,v,t,其中:n、m、q的意义见例题讲解;u、v、t均为正整数;你需要自己计算p=u/v(保证0<u<v)的值;t是输出参数,其含义将会在输出格式中解释。
第2行包含n个非负整数,为 a1,a2,…,an,即初始时n只蚯蚓的长度。
同一行中相邻的两个数之间用一个空格分隔。
保证1≤n≤105,0≤m≤7×106,0<u<v≤109,0≤q≤200,1≤t≤71,0≤ai≤108。
第1行输出m/t
个整数,按时间顺序,依次输出第t秒、第2t秒、第3t秒……被切断蚯蚓(在被切断前)的长度。
第2行输出(n+m)/t
个整数,输出m秒后所有蚯蚓的长度:需要按从大到小的顺序,依次输出第t秒、第2t秒、第3t秒……时蚯蚓的长度。
同一行中相邻的两个数之间用一个空格分隔。即使某一行没有任何数需要输出,也应输出一行空行。
请阅读输入样例来更好地理解这个格式。
3 7 1 1 3 1
3 3 2
3 4 4 4 5 5 6
6 6 6 5 5 4 4 3 2 2
在神刀手到来前:3只蚯蚓的长度为3、3、2,p为1/3。
1秒后:一只长度为3的蚯蚓被切成了两只长度分别为1和2的蚯蚓,其余蚯蚓的长度增加了1。最终4只蚯蚓的长度分别为(1、2)、4、3。括号表示这个位置刚刚有一只蚯蚓被切断。
2秒后:一只长度为4的蚯蚓被切成了1和3。5只蚯蚓的长度分别为2,3,(1、3),4。
3秒后:一只长度为4的蚯蚓被切断。6只蚯蚓的长度分别为3,4,2,4,(1、3)。
4秒后:一只长度为4的蚯蚓被切断。7只蚯蚓的长度分别为4,(1、3),3,5,2,4。
5秒后:一只长度为5的蚯蚓被切断。8只蚯蚓的长度分别为5,2,4,4,(1、4),3,5。
6秒后:一只长度为5的蚯蚓被切断。9只蚯蚓的长度分别为(1、4), 3,5,5,2,5,4,6。
7秒后:一只长度为6的蚯蚓被切断。10只蚯蚓的长度分别为2,5,4,6,6,3,6,5,(2、4)。
所以,7秒内被切断的蚯蚓的长度依次为3,4, 4,4,5,5,6。7秒后,所有蚯蚓长度从大到小排序为6,6,6,5,5,4,4,3,2,2。
3 7 1 1 3 2
3 3 2
4 4 5
6 5 4 3 2
这个样例中只有t=2与上个数据不同。只需在每行都改为每两个数输出一个数即可。
虽然第1行最后有一个6没有被输出,但是第2行仍然要重新从第2个数再开始输出。
3 7 1 1 3 9
3 3 2
2
这个数据中只有t=9与上个数据不同。注意:第1行没有数要输出,但也要输出一行空行。
一种简单的方法是将每一只蚯蚓的长度都放入一个优先队列(大根堆)中,每次从堆顶(队首)取出最长的那只蚯蚓切成两段,再将新产生的两只蚯蚓直接放回优先队列即可,因为优先队列的特性之一是自动排序。神刀手操作m次后,逐个输出队首(堆顶)的数即可。这种使用STL中的优先队列存储所有的蚯蚓,模拟神刀手的操作过程可以处理大部分测试数据。
要想通过全部测试数据,需要对操作过程进行优化,即神刀手每次操作后,其余蚯蚓的增长没有必要实时更新,可以定义一个变量sum统计其余蚯蚓的总增长量。操作过程中,如果切断的蚯蚓入队,给它减去sum减去q的长度;如果队首的蚯蚓出队,给它加上sum的长度后再处理即可。
参考代码如下。
1 //蚯蚓 —— 使用优先队列模拟可处理大部分数据
2 #include <bits/stdc++.h>
3 using namespace std;
4
5 int main()
6 {
7 priority_queue<int> earthworm; //优先队列默认由大到小排列蚯蚓长度
8 int n,m,t,q,u,v,sum=0; //sum用于保存累计增加的q值
9 cin>>n>>m>>q>>u>>v>>t;
10 double p=double(u)/v;
11 for(int i=0,temp; i<n; ++i)
12 {
13 cin>>temp;
14 earthworm.push(temp); //入队列
15 }
16 for(int i=1; i<=m; i++)
17 {
18 int Big=earthworm.top()+sum; //队首为最大值,出队时还原回真实值
19 earthworm.pop(); //删除队首的蚯蚓
20 if(!(i%t)) //输出第i*t秒切的蚯蚓
21 cout<<Big<<" ";
22 int cut=floor(p*double(Big)); //用floor函数,以防因编译器不同而出现误差
23 earthworm.push(cut-sum-q); //被切割的蚯蚓无须加q,所以先减去
24 earthworm.push(Big-cut-sum-q);
25 sum+=q; //累计增长量
26 }
27 cout<<'\n';
28 for(int i=1; i<=n+m; ++i)
29 {
30 if(!(i%t))
31 cout<<earthworm.top()+sum<<' ';
32 earthworm.pop(); //逐个删除队首的蚯蚓
33 }
34 cout<<'\n';
35 return 0;
36 }
因为每一次操作都要找出最大的一个数,如果用排序的方法会超时,所以考虑采用3个由大到小排列的队列来完成(使用STL里的优先队列会超时,需要自己编写代码)。第1个队列p[0]保存的是已经由大到小排好序的n只蚯蚓的长度,p[2]、p[3]分别保存每一次切割后的前半段和后半段长度,每次取3个队列中队首最大的元素进行切割后存入p[2]和p[3]中,请思考p[2]、p[3]需要由大到小排序吗?
试根据以上分析,完成满分代码(指完成的代码可通过全部测试数据)。
小球钟是一种通过不断在轨道上移动小球来度量时间的设备。每分钟,一个转动臂将一个小球从小球队列的底部移走,让它上升到钟的顶部,并将它安置在一个表示1分钟、5分钟、15分钟和小时的轨道上。它可以显示从1:00到24:59(这正是奇怪之处)内的时间,若有3个球在1分钟轨道,1个球在5分钟轨道,2个球在15分钟轨道及15个球在小时轨道上,就表示时间为15:38。
当小球通过钟的机械装置被移动后,它们就会改变其初始次序。仔细研究它们次序的改变,可以发现相同的次序会不断出现。由于小球的初始次序迟早会重复出现,因此这段时间的长短是可以被度量的,这完全取决于所提供的小球的总数。
每分钟,最近最少被使用的那个小球从位于球钟底部的小球队列被移走,并将上升到显示1分钟的轨道上,这里可以放置4个小球。当第5个小球滚入该轨道,它们的重量(重量为质量的俗称)使得轨道倾斜,原先在轨道上的4个小球按照与它们原先滚入轨道的次序相反的次序加入钟底部的小球队列。引起倾斜的第5个小球滚入显示5分钟的轨道。该轨道可以放置2个球。当第3个小球滚入该轨道,它们的重量使得轨道倾斜,原先的2个小球同样以相反的次序加入钟底部的小球队列。而第3个小球滚入了显示15分钟的轨道。这里可以放置3个小球。当第4个小球滚入该轨道,它们的重量使得轨道倾斜,原先在轨道上的3个小球按照与它们原先滚入轨道的次序相反的次序加入钟底部的小球队列,而这第4个小球滚入了显示小时的轨道。该轨道可以放置23个球,但这里有一个外加的、固定的(不能被移动的)小球,这样小时的值域就变为1~24。从15分钟轨道滚入的第24个小球将使小时轨道倾斜,这23个球同样以相反的次序加入钟底部的小球队列,然后第24个小球同样加入钟底部的小球队列。
输入小球时钟序列。每个时钟都按照前面描述的那样运作。所有时钟的区别仅在于它们在时钟启动时刻小球初始个数的不同。在输入的每行上给出一个时钟的小球数,它并不包括那个在小时轨道上的固定的小球。合法的数据为33~250。0表示输入的结束。
输出中的每一行只有一个数,表示对应的输入情形中给出的小球数量的时钟在经过多少天的运行后可以回到它的初始小球序列。
33
55
0
22
50
可以通过模拟出每个小球回到原来位置上所需的天数,然后求它们的最小公倍数的方法来解决这个问题,但这样速度仍然很慢。改进方法是先模拟小球钟最先24小时的运行情况,得到24小时后的钟底部的新小球队列。设初始时,钟底部的小球编号依次是1,2,3,…,n。24小时后,钟底部的小球编号依次是p1, p2, p3,…, pn。则可以建立这样的置换:
1 2 3 … n
p1 p2 p3 … pn
注意到小球钟的运作规则保证了上述置换是不变的,就可以计算出小球钟运行48小时后、72小时后……钟底部的小球队列情况,直至队列情况重新是1,2,3,…,n。这样,在求得以上置换的基础上,我们可以求每一个小球回到原位置的周期,然后求它们的最小公倍数即可。
现举例说明每一个小球(如1号小球)回到原位置的周期是怎么计算的。
如图1.2所示,假设初始队列为1 2 3 4,则24小时后的队列为4 1 2 3。可以看出4号位置上的4号小球跑到了1号位置上,3号位置上的3号小球跑到了4号位置上。显然再过24小时,4号位置上的3号小球会跑到1号位置上,而3号位置上的2号小球会跑到4号位置上。
图1.2
再过24小时,4号位置上的2号小球跑到1号位置,而4号位置将被1号小球占据,因为第1个24小时后,1号位置上的1号小球跑到了2号位置上。
再过24小时,4号位置上的1号小球跑到了初始的1号位置上,1号小球的周期计算完毕。
参考代码如下。
1 //小球钟
2 #include <bits/stdc++.h>
3 using namespace std;
4
5 const int Limit[4] = {5,3,4,24};//定义每种轨道容纳的小球数
6 int Line[4][25]; //4种轨道,即1分钟、5分钟、15分钟、小时轨道
7 int solved[300]; //保存计算好的结果
8 deque<int> Q; //队列
9
10 int GCD(int m, int n) //求最大公约数
11 {
12 return n==0?m:GCD(n,m%n);
13 }
14
15 long long GetDay(int n)
16 {
17 int j,k;
18 long long ans = 1;
19 for (int i = 0; i < n; ++i) //枚举每个小球
20 {
21 for (j = Q[i], k = 1; j != i; j = Q[j], ++k);//计算每个小球的周期k
22 ans=ans*k/GCD(ans, k);//求此小球与之前所有小球的周期的最小公倍数
23 }
24 return ans;
25 }
26
27 int Solve(int n)
28 {
29 Q.clear(); //清空队列
30 for (int i = 0; i < n; ++i) //初始化队列
31 Q.push_back(i);
32 while(1)
33 {
34 Line[0][++Line[0][0]]=Q.front(); //Line[i][0]记录第i种轨道已有的球数
35 Q.pop_front();
36 for (int i = 0; i < 3; ++i) //枚举1分钟、5分钟、15分钟的轨道
37 if (Line[i][0] == Limit[i]) //若本轨道达到了容纳极限
38 {
39 Line[i+1][++Line[i+1][0]]=Line[i][Line[i][0]--];//最后一个球滚入下一轨道
40 while (Line[i][0] != 0)
41 Q.push_back(Line[i][Line[i][0]--]);//剩余的球依次逆序入队列
42 }
43 if (Line[3][0] == Limit[3]) //若24小时到了
44 {
45 int o = Line[3][0]--; //先记录本球的编号
46 while (Line[3][0] != 0) //其他球依次入队列
47 Q.push_back(Line[3][Line[3][0]--]);
48 Q.push_back(Line[3][0]); //最后一个球滚入队列
49 break;
50 }
51 }
52 return GetDay(n);
53 }
54
55 int main()
56 {
57 int n;
58 while (cin >> n, n != 0)
59 if (solved[n] != 0) //如果之前已计算过,直接输出结果
60 cout<<solved[n]<<'\n';
61 else
62 cout<<(solved[n]=Solve(n))<<'\n'; //记录计算结果并输出
63 return 0;
64 }
有一块面积为m×n的矩形区域,上面有m×n个边长为1的格子,每个格子上堆了一些同样大小的积木(积木的长、宽、高都是1),我们定义每块积木为如下样式,并且不会进行任何旋转,只会严格以图1.3所示的形式摆放。
图1.3
积木的每个顶点用1个 “+”表示,长用3个“-”表示,宽用1个“/”表示,高用2个“|”表示。字符“+”“-”“/”“|”的ASCII值分别为43,45,47,124。字符“.”(ASCII值为46)需要作为背景输出,即立体图里的空白部分需要用“.”代替。立体图的画法如图1.4所示。
图1.4
在立体图中,定义位于(m,1)的格子(即第m行第1列的格子)上面自底向上的第1块积木(即最下面的一块积木)的左下角顶点为整张图最左下角的点。
输入两个整数m和n,表示有m×n个格子(1≤m,n≤50)。
接下来的m行,是一个m×n的“矩阵”,每行有n个用空格分隔的整数,其中第i行第j列上的整数表示第i行第j列的格子上的积木数(1≤每个格子上的积木数≤100)。
输出满足题目要求的立体图,是一个K行L列的字符矩阵,其中K和L表示最少需要K行L列才能按规定输出立体图。
3 4
2 2 1 2
2 2 1 1
3 2 1 2
......+---+---+...+---+
..+---+ / /|.. / /|
. / / |-+---+ |.+---+ |
+---+ | / / | +-| | +
| | +---+ |/+---+ |/|
| | / / | +/ /|-+|
+---+---+ |/+---+ | / |+
| | | +-| | + |/.
| | |/ | | -| +..
+---+---+--+---+ | /...
| | | | | +....
| | | | | /.....
+---+---+---+---+......
定义一个全局字符数组Map[1001][1001]用于保存立体图并最后输出,全局字符数组的所有元素值会自动初始化为NULL(即0);再定义一个字符数组Model[6][8]保存一个积木模板。
参考代码如下。
1 char Map[1001][1001],Model[6][8]=
2 {
3 " +---+",
4 " / /|",
5 "+---+ |",
6 "| | +",
7 "| |/ ",
8 "+---+ "
9 };
这样在生成立体图时,就可以按题目要求,将积木模板逐个复制到全局字符数组Map[]的相应位置了。另外,方便起见,积木模板左上角的空白处和右下角的空白处无须赋值,这样全局字符数组Map[]中没有被赋值的位置的元素值仍为NULL,输出时直接以“.”代替即可。
那么积木的高度怎么计算呢?由图1.5(a)可以看出,我们可以将积木分成上面和下面两部分,上面部分为3行,下面部分为3行,则积木高度为3×积木块数+3,计算积木宽度同理。
图1.5
由图1.5(b)可以看出,同一层积木,行与行的高度差为2,所以可以计算出整个立体图输出的高度值,也可以计算出每个立方体在全局字符数组Map[]中的对应位置后复制积木模板。复制模板时从积木模板的左下角开始,所有积木的复制顺序应该是从后向前、从下向上、从左向右依次覆盖。这样上面的积木把下面的积木覆盖,右边的积木把左边的积木覆盖,前面的积木把后面的积木覆盖,就处理好了视觉遮挡的问题。
小明正在学习一种新的编程语言A++,刚学会循环语句的他写了好多程序并给出了他自己算出的时间复杂度,请你编程判断小明对他的每个程序给出的时间复杂度是否正确。
A++语言的循环结构如下:
F i x y
循环体
E
其中“F i x y”表示新建变量 i(变量i不可与未被销毁的变量重名)并初始化为x,然后判断 i和y的大小关系,若i小于或等于y则进入循环,否则不进入。每次循环结束后i都会被修改成i+1,一旦i大于y终止循环。
x和y可以是正整数(x和y的大小关系不定)或变量n。n是一个表示数据规模的变量,在时间复杂度计算中需保留该变量而不能将其视为常数,该数远大于100。
“E”表示循环体结束。循环体结束时,这个循环体新建的变量也被销毁。
注:方便起见,在描述时间复杂度时,使用大写英文字母“O”表示通常意义下“Θ”的概念。
第1行为一个正整数t,表示有t(t≤10)个程序需要计算时间复杂度。每个程序我们只需抽取其中的“F i x y”和“E”即可计算时间复杂度。注意:循环结构允许嵌套。
接下来每个程序的第1行包含一个正整数L(L≤100)和一个字符串,L代表程序行数,字符串表示这个程序的时间复杂度。O(1)表示常数复杂度,O(n^w)表示时间复杂度为nw,其中w是一个小于100的正整数(输入不包含引号),保证输入的时间复杂度只有O(1)和O(n^w) 两种类型。
接下来 L行代表程序中循环结构中的“F i x y”或者“E”。程序行若以“F”开头,表示进入一个循环,之后有空格分隔的3个字符(串)“i x y”,其中i是一个小写字母(保证不为n),表示新建的变量名,x和y可能是正整数或变量n,已知若x和y为正整数则一定小于100。
程序行若以“E”开头,则表示循环体结束。
输出t行,对应输入的t个程序,每行输出“Yes”或“No”或者“ERR”(输出不包含引号)。若程序实际时间复杂度与输入给出的时间复杂度一致则输出“Yes”,否则输出“No”。若程序有语法错误(其中,语法错误只有 “F”和“E”不匹配,以及新建的变量与已经存在但未被销毁的变量重复两种),则输出“ERR”。
注意:即使在程序不会执行的循环体中出现了语法错误也会编译错误,要输出“ERR”。
8
2 O(1)
F i 1 1
E
2 O(n^1)
F x 1 n
E
1 O(1)
F x 1 n
4 O(n^2)
F x 5 n
F y 10 n
E
E
4 O(n^2)
F x 9 n
E
F y 2 n
E
4 O(n^1)
F x 9 n
F y n 4
E
E
4 O(1)
F y n 4
F x 9 n
E
E
4 O(n^2)
F x 1 n
F x 1 10
E
E
Yes
Yes
ERR
Yes
No
Yes
Yes
ERR
第1个程序 i从1到1是常数复杂度。
第2个程序x从1到n是n的一次幂复杂度。
第3个程序有一个“F”开启循环却没有“E”结束,语法错误。
第4个程序二重循环,n的平方复杂度。
第5个程序两个一重循环,n的一次幂复杂度。
第6个程序第1重循环正常,但第2重循环开始即终止(因为n远大于100,100大于4)。
第7个程序第1重循环无法进入,故为常数复杂度。
第8个程序第2重循环中的变量x与第1重循环中的变量重复,出现上述第2种语法错误,输出“ERR”。
判断“F”和“E”是否匹配及循环结构的嵌套可以使用堆栈来实现,可以采取边读入程序行边处理的在线处理方式,也可以采取读取完全部程序行后再处理的离线处理方式。如果程序行以“F”开头就存入结构体数组并入栈(此时如果该程序行中的循环变量与还在栈中未销毁的循环变量重名,则输出“ERR”并退出,如果程序行以“E”开头就出栈。出栈时如果堆栈为空或者栈首不是以“F”开头的程序行就输出“ERR”。
结构体结构如下。
1 struct Code
2 {
3 char F,i; //操作符、循环变量
4 int x,y; //变量初始值和结束值
5 } code[200];
计算时间复杂度也在堆栈中实现,计算程序行“F i x y”中的y与x的差值,如果y-x的值是一个极大值(即循环n次)则“贡献”n的时间复杂度。注意:如果y-x的值是负数,它及它后面所嵌套的循环均无法贡献时间复杂度。
拱猪游戏的计分方法如下。
用S、H、D及C分别代表黑桃、红心、方块及梅花,并以数字1~13来代表A、2……Q、K等牌点,例如H1为红心A,S13为黑桃K。
牌局结束时,各玩家持有的有关计分的牌(计分牌)仅有S12(猪)、所有红心牌、D11(羊)及C10(加倍)等16张牌,其他牌均弃置不计。若未持有这16张牌中的任一张则以0分计算。
若持有C10的玩家只有该牌而没有任何其他牌则得+50分,若除了C10还有其他计分牌,则将其他计分牌所得分数加倍计算。
若红心牌不在同一玩家手中,则H1~H13这13张牌均以负分计,其数值依次为-50,-2,-3,-4,-5,-6,-7,-8,-9,-10,-20,-30和-40,而且S12与D11分别以-100及+100分计算。
若红心牌H1~H13均在同一玩家手中,有下列情形。
所有红心牌以+200分计算。
若S12、D11皆在持有所有红心牌的玩家手中,则此玩家得+500分。
而C10还是以前面所述原则计算之。
例1:若各玩家持有计分牌如下。
A:D11 H8 H9
B:C10 H1 H2 H4 H6 H7
C:H10 H11 H12
D:S12 H3 H5 H13
则各玩家得分依序为 +83、-138、-60及-148。
例2:若各玩家持有计分牌如下(D未持有任何计分牌)。
A:H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 H11 H12 H13
B:S12 C10
C:D11
则各玩家之得分依序为 +200、-200、+100 及 0。
例3:A持有所有16张计分牌,得 +1000分;其余3个玩家均得0分。
输入多组测试数据,每组测试数据有4行,每一行第1个数为该玩家持有的计分牌总数,而后列出其持有的所有计分牌,牌数与各计分牌均以一个以上的空格分隔,读到持牌数为0表示输入结束。
每一行输出一组测试数据对应的结果,依次输出各玩家所得分数。
4 S12 H3 H5 H13
3 D11 H8 H9
6 C10 H1 H2 H4 H6 H7
3 H10 H11 H12
13 H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 H11 H12 H13
2 S12 C10
1 D11
0
0
0
0
0
-148 +83 -138 -60
+200 -200 +1000
可将15张牌的分值预先存到s[17]数组中,s[1]~s[13]表示H1~H13的分值,s[14]和s[15]分别表示S12和D11的分值。将4个人的牌读入MAP[5][17]数组,例如MAP[1][16]=1表示第1个玩家有一张C10的牌。
计分时,需按所有红心牌全在同一玩家手中和不在同一玩家手中两种情况来讨论、模拟。
梭哈是一种二人扑克牌游戏,每位玩家手里有5张牌,要比较每位玩家手里牌型的大小以确定赢家,牌型最大的玩家赢得牌局。
所有5张牌的组合,按以下顺序,由大至小排行分为不同牌型。
(1)同花顺(Straight Flush):同一花色,顺序的牌。例:Q♦ J♦ 10♦
9♦
8♦
。
(2)四条(Four of a Kind):有4张同一点数的牌。例:10♣
10♦
10♥
10♠
9♥
。
(3)满堂红(Full House):3张同一点数的牌,加一对其他点数的牌。例:8♣
8♦
8♠
K♥
K♠
。
(4)同花(Flush):5张同一花色的牌。例:A♠
K♠
10♠
9♠
8♠
。
(5)顺子(Straight):5张顺连的牌。例:K♦
Q♥
J♠
10♦
9♦
。
(6)三条(Three of a Kind):有3张同一点数的牌。例:J♣
J♥
J♠
K♦
9♠
。
(7)两对(Two Pairs):两张相同点数的牌,加另外两张相同点数的牌。例:A♣
A♦
8♥
8♠
Q♠
。
(8)一对(One Pair):两张相同点数的牌。例:9♥
9♠
A♣
J♠
8♥
。
(9)无对(Zilch):不能排成以上组合的牌,以点数决定大小。例:A♦
Q♦
J♠
9♣
8♣
。
若牌型一样,则通过点数和花色决定胜负(点数优先)。
点数的顺序(从大至小)为 A>K>Q>J>10>9>8>7>6>5>4>3>2。(注:当5张牌是5 4 3 2 A的时候,A可以看作最小的牌,此时的牌型仍然为顺子,是顺子里面最小的。)
花色的顺序(从大至小)为 黑桃(♠
)>红心(♥
)>梅花(♣
)>方块(♦
)。
举例说明:
(1)Q♦
J♦
10♦
9♦
8♦
>8♣
8♥
8♠
K♥
K♠
(前者牌型为同花顺,比后者大);
(2)9♣
9♦
9♠
Q♥
Q♠
>8♣
8♦
8♠
K♥
K♠
(两者牌型均为满堂红,比较牌型中3张同一点数的牌9比8大);
(3)A♣
A♦
8♥
8♠
Q♠
>A♠
A♥
7♥
7♠
K♠
(两者牌型均为两对,且最大的对子相同,此时比较次大的对子,8比7大);
(4)A♠
Q♠
J♥
9♥
8♥
>A♦
Q♦
J♠
9♣
8♣
(两者牌型均为无对,所有数码均相同,此时比较最大牌的花色,A♠
>A♦
);
(5)4♠
4♥
A♦
Q♦
5♦
>4♣
4♦
A♠
Q♠
5♠
(两者牌型均为一对,所有数码均相同,此时对4为牌型里最大的部分,因此比较4♠
>4♣
)。
输入多组数据,每组数据用一个空行分隔,数据组数不超过2000。
每组数据都是共10行。
前5行每行用两个整数描述玩家A手上的牌:第1个数表示牌的数码(1表示 A,13表示 K,12表示 Q,11表示J),第2个数表示牌的花色(1表示黑桃,2 表示红心,3表示梅花,4表示方块)。
后5行每行用两个整数描述玩家B手上的牌:第1个数表示牌的数码(1表示A,13表示K,12表示Q,11表示J),第2个数表示牌的花色(1表示黑桃,2表示红心,3表示梅花,4表示方块)。
保证两位玩家手里没有同一张牌。
对于每组输入数据,如果玩家 A 的牌大,输出“Player A win!”,否则输出“Player B win!”。
12 4
11 4
10 4
9 4
8 4
8 1
8 2
8 3
10 1
10 2
Player A win!
30%的数据保证两位玩家的牌型都一样。
30%的数据保证两位玩家的牌型都不一样。
余下40%的数据为各种可能情况。
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e59659”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。