读书笔记 - 《算法竞赛入门经典》
由于我事实上达到了CF1900分的水平,只记录一些我觉得有新意的东西。
第1章 程序设计入门
1、 双精度浮点数类型(double类型)的scanf输入用"%lf"
,而printf输出用"%f"
,某些编译器可能允许printf输出用"%lf"
,但这是不规范的(据说在C++中是都可以使用"%lf"
但是某次比赛中好像得到了错误的结果)。
2、 使用printf输出"%04d"
,表示输出一个右对齐的,宽度至少4个字符的,位数不足则在前面补0的十进制数。假如不加0,就表示在前面补空格。当然假如有题目要求这种奇怪的东西,建议手写一个输出函数来控制。
3、 注意区分逻辑或(logical OR)||
与按位或(bitwise OR)|
的区别。尤其要检查在判断语句中有没有打错。在打开-Wall开关后编译器应该会进行警告,一定要消除这种警告。除此之外,还要注意类似^
这样的运算的优先级是否满足本意,以及不要写l<=x<=r
这样的表达式。
第2章 循环结构程序设计
1、 我觉得P19有一点错误。i++
的含义不可能和i=i+1
相同,而是和++i
相同,实验结果表明确实是这样,赋值表达式的值确实等于左值。
2、 貌似可以在Code::Blocks里面使用Watches功能。查了一下资料,大概的步骤如下:在Settings->Compiler->ToolChain executables中记住编译器的路径,然后在Settings->Debugger中选择左侧的Default,设置一个正确的路径,比如编译器路径是C:\Program Files (x86)\CodeBlocks\MinGW
那么Debugger的路径就是C:\Program Files (x86)\CodeBlocks\MinGW\bin\gdb32.exe
,根据编译器的不同好像有选择GDB和CDB的区别。然后新建一个Project,选择Console Application,创建完之后看不到任何源代码,但是按F9编译运行之后会出现一个Hello World!,然后在工程文件夹下找到main.cpp,把这个拖进来,最后把Debug->Debugging windows->Watches窗口打开,拖动到喜欢的位置固定下来。然后就可以用F5打断点,F8开始运行/运行到下一个断点。假如从文件读入,注意是把in文件放在main.cpp的同目录下。
3、 P23好像也有些问题,for和while应该不能这样简单等价,需要考虑变量的作用域。不过这是本入门书,而不是钻这些牛角尖的。
4、 突然发现了消除Code::Blocks中用"%lld"
输入输出会被编译器提示的问题了,原来是推荐使用"%I64d"
。
5、 使用(double)clock() / CLOCKS_PER_SEC
获得程序的运行时间,尤其适合用于搜索中的卡时。
6、 注意观察阶乘非常有可能是某些数的倍数,因为阶乘包含了太多因数,利用这个性质可以得到一些奇奇怪怪的规律,比如25!末尾有6个0。
7、 scanf函数有返回值,返回的是成功输入的变量的个数。
8、 在Windows下用Ctrl+Z输入EOF,在Linux下用Ctrl+D输入EOF。不过一般大家都用文件读入。
9、 非静态的局部变量一般是未经初始化的。
10、 在不能使用重定向语句进行读写文件的比赛,使用下面的方式替代:
FILE *fin, *fout;
fin = fopen("data.in", "rb");
fout = fopen("data.out", "wb");
int n;
fscanf(fin, "%d", &n);
fprintf(fout, "%d", n);
fclose(fin);
fclose(fout);
重定向除了有可能被禁止之外,还有一点就是和标准输入输出冲突。fopen的方式还可以在任意位置关闭,重复读写文件,等等。
第3章 数组和字符串
1、 输入字符串时,不要在scanf中加入&
符号,虽然有时候的运行结果是对的,但是未必任何时候都是如此,例如:
char s[105];
scanf("%s", s+1);
scanf("%s", &s+1);
就是不同的。后者经常会导致程序卡死。其实应该很好理解,s
是一个字符串的首地址,也就是字符数组的首地址,它+1就是偏移一个字符的长度。而&s
是一个字符数组的首地址的地址,它+1应该偏移的是一个指针的长度。
2、 注意scanf("%c", &c);
是会输入空白符的,需要输入奇怪的东西的时候还是使用字符串输入最保险。
3、 char * strchr(char *, char)
用于在字符串中查找单个字符,返回值为首次出现的地址或者NULL。
4、 使用sprintf的时候要小心越界。
5、 每条语句中,不要多次出现++``````+=
等修改变量的操作符。不要省这种行数。
6、 使用fgetc(fin)
可以从已经fopen的文件fin中读取一个字符,返回值是这个字符或者EOF。假如是标准输入,可以使用getchar()
,它等价于fgetc(stdin)
。
7、 下面的例子:
scanf("%d", &n);
c = getchar()
假如输入"123 ",那么字符c就是这个' ',意思是scanf会保持第一个非法字符仍在流中。
8、 注意不同的操作系统中换行符的不同:
Windows: "\r\n"
Linux: "\n"
MaxOS: "\r"
使用这个东西构造快速读入时,要考虑操作系统的问题。
9、 用fgets(buf, maxlen, fin)
从已经fopen的文件fin中读入完整的一行,包括这个'\n',注意有时候文件未必会以'\n'结尾,有时会直接遇到EOF。当什么都没有读到时,fgets会返回NULL。也如同前面所说,可以用stdin直接替代fin。
10、 字符用八进制或者十六进制表示,例如'\o',这里o是一个八进制数,或者用'\xh',这里h是一个十六进制数。注意不是用十进制表示的(好像大一C++的期考错了这道题)。
11、 用"%o"
,"%x"
,"%X"
输入输出八进制或者十六进制数。当然大写X就是代表十六进制数用大写输出。不过输入的时候是不区分大小写的。
12、 移位运算符的优先级非常低,比加减法还要低。事实上位运算的优先级都非常低,尤其小心被拿来当成“叉积”符号的^
。
13、 用"%u"
输入输出无符号(32位)整数。假如输入输出64位整数,应该作和"%d"
相似的变换。
14、 sizeof不是一个函数,它在编译阶段就会计算出值。
15、 假如是自己弄出来的字符串,一定要保证以'\0'结尾,简单的方法是直接memset一次。
第4章 函数和递归
1、 一个声明有返回值,但是没有显示返回的函数,有时候会导致程序运行出奇奇怪怪的东西,这个取决于编译器。幸运的是-Wall编译开关可以警告这种错误。
2、 ++
(自增)运算符的优先级高于*
(访问地址)运算符,所以*p++
实际上就是*(p++)
。
3、 P71提到,一个未经初始化(或者赋值为NULL等“错误”初始化)的指针,千万不能使用*
(访问地址)运算符来访问它,尤其是对它赋值,极有可能导致程序运行错误甚至崩溃。必须要赋值一个合理的地址,或者用new
运算符申请一个新的地址才可以访问。
4、 函数定义中,指明的参数若为数组类型,事实上就是一个指针类型。传进来的也只是一个指针,并不再是数组的首地址。
5、 编译后产生的可执行文件保存的内容:Linux用ELF格式,DOS用COFF格式,Windows用PE格式(由COFF格式扩充而来)。用Linux或者Windows下的size程序可以得到可执行文件中各个段的大小。例如text段、data段、bss段,正文段用于存储指令,数据段用于存储已经初始化的全局变量,BSS段存储未赋值的全局变量所需的空间。调用栈存储在stack段(堆栈段),它不在可执行文件之中,而是在运行时创建,当段被越界访问是,发生段错误,即Segment Fault。而调用栈被栈帧塞满发生的越界,叫做栈溢出,即Stack Overflow。局部变量都是放在堆栈段的,栈溢出不一定是递归调用太多,也有可能是局部变量太大。只要总大小超过了范围,都是栈溢出。
6、 编码时,自顶向下构造和自底向上构造各有优势,可以看《On Lisp》。测试的时候,当然是自底向上测试。
7、 可以自己编写一个跨行读入的函数,注意要同时处理'\r'和'\n'。
8、 main函数也是一个普通的函数,甚至可以递归调用。
9、 常用的头文件:
cstdio printf/scanf, fprintf/fscanf, sprintf/sscanf
fopen, fclose, freopen
getchar, fgets
cmath sin, cos, pow, ...
cstring strlen, strcat, ...
memset, memcpy, ...
cctype isalpha, isdigit, toupper, ...
ctime clock
cstdlib rand
第5章 C++与STL入门
1、 关闭流与stdio的同步:ios::sync_with_stdio(false)
,貌似还可以cin.tie(nullptr)
,在取消流同步之后就不能再使用对标准输入输出进行操作的C语言的函数了,否则可能会得到一些由于同步错误得到的奇奇怪怪的结果。在使用cout输出时,用"\n"代替endl可以进一步提高速度。
2、 bool类型是C++新增的,C语言中没有bool类型。
3、 得到一行的更简单的方法:
string line;
while(getline(cin, line)) {
stringstream sin(line);
while(sin >> x)
;
}
注意string很慢,stringstream更慢,假如对效率有要求,还是应该使用自定义的输入函数(再提一次要注意处理'\r')。
4、 在C++中不再需要用typedef的方式定义struct。而且C++中的struct还可以有方法。
5、 用template<typename T>
来使用模板。有的模板是不需要显示声明类型使用的,貌似是函数。但是使用模板来创建的struct就需要用尖括号搞出模板的类型。(可以用这个方法写计算几何的模板?但是还是分成两份代码最方便)。
6、 STL的set居然有set_union和set_intersection两个操作,使用方法如下:
set<int> s1, s2, S;
s1.insert(1);
s1.insert(2);
s1.insert(3);
s2.insert(2);
s2.insert(3);
s2.insert(4);
S.clear();
set_union(s1.begin(), s1.end(), s2.begin(), s2.end(), inserter(S, S.begin()));
puts("S=");
for(auto &v : S)
printf("%d ", v);
puts("");
S.clear();
set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(), inserter(S, S.begin()));
puts("S=");
for(auto &v : S)
printf("%d ", v);
puts("");
注意要先clear。
7、 优先队列的格式是:priority_queue<int, vector<int>, cmp>
,其中cmp是一个struct,且需要重载()
运算符。
8、 例题5-7 丑数,这道题的处理方式:从优先队列中取出最小值,然后插入由这个最小值衍生出的新值,这个思路貌似在某道找第k短路的题目中用过。
9、 cstdlib中的rand生成[0, RAND_MAX]内均匀分布的随机整数,假如要生成更大的整数,不应该用rand()rand(),很显然这样会使得中间部分的数出现的概率显著增大,应该使用rand()RAND_MAX+rand()。小心溢出。
10、 根据当前时间设置一个随机数种子,常用srand(time(nullptr))
,注意包含对应的头文件。其中time函数返回的都是当前的时间,也就是UTC时间1970年1月1日0:00以来的秒数,假如程序执行得很快,很可能使用的也是同一个随机数序列。time函数传入的假如不是空指针,则会把时间写在这个time_t指针对应的内存中。不经过srand设置的随机数序列,默认为srand(1)
。注意不要在每次使用rand之前都设置一次srand,否则极有可能结果非常不均匀。
11、 使用函数处理vector等类型时,尽可能传引用以避免不必要的复制。提醒我的高精度整数模板中好像确实有不必要的复制。
12、 assert不是一个函数,而是一个宏。
13、 lrj的高精度整数貌似还配有除法,可以偷过来用,在他的代码仓库里。
14、 可以用<
运算符构造出其他比较运算符的重载,但是==
和!=
会进行两次比较,假如对性能有要求,应该再手动重载一个==
运算符。
15、 涉及字符串的完全匹配问题,应该先给字符串存到set里映射一个整数,在接下来的使用中进行整数的==
判断会非常快。