算法竞赛入门经典
第1部分 语言篇
第1章 程序设计入门
1.3
1、变量交换的程序:
#include<stdio.h>
int main()
{
int a, b;
scanf("%d %d", &a,&b);
a^=b;
b^=a;
a^=b;
printf("%d %d\n", a, b);
}
第2章 循环结构程序设计
2.1
2、if(floor(m +0.5) == m)
函数floor(x)返回x的整数部分,那么为什么不直接比较floor(m)和m呢?原因在于:浮点数的运算(和函数)有可能存在误差——不是一定存在,但经常都会。
2.2
3、要计算只包含加法、减法和乘法的整数表达式除以正整数n的余数,可以在每步计算之后对n取余,结果不变。
4、计时函数clock()返回程序目前为止运行的时间(需要#include <time.h>)。这样,在程序结束之前调用它,便可获得整个程序的运行时间。这个时间除以常数CLOCKS_PER_SEC之后得到的值以“秒”为单位。
5、为了避免输入数据的时间影响测试结果,我们使用一种称为管道的小技巧:在Windows命令行下执行echo 20 | abc(Linux下输入echo 20 | ./abc),操作系统会自动帮你把20输入,其中abc是你的程序名。
2.3
6、scanf函数返回的是成功输入的变量个数。
7、在Windows下,输入完毕后先按Enter键,再按Ctrl+Z键,最后再按Enter键,即可结束输入。在Linux下,输入完毕后按Ctrl+D键即可结束输入。
8、freopen(“input.txt”,“r”, stdin); freopen(“output.txt”, “w”, stdout);
它将使得scanf从文件input.txt读入,printf写入文件output.txt。事实上,不只是scanf和printf,所有读键盘输入、写屏幕输出的函数都将改用文件。
2.4
9、要在C++程序中使用C语言头文件,请去掉扩展名.h,并在最前面加上小写字母c。例如,stdio.h在C++中的新名字是cstdio。
第3章 数组和字符串
3.1
10、比较大的数组应尽量声明在main函数外。
11、如果需要把数组a全部复制到数组b中,可以写得简单一些:memcpy(b,a, sizeof(a))。
3.2
12、用编译选项-Wall编译程序时,会给出很多(但不是所有)警告信息。
3.3
13、使用fgetc(fin)可以从打开的文件fin中读取一个字符。一般情况下应当在检查它不是EOF后再将其转换成char值。从标准输入读取一个字符可以用getchar(),它等价于fgetc(stdin)。
14、不同操作系统的回车换行符是不一致的。Windows是’\r’和’\n’两个字符,Linux是’\n’,而MacOS是’\r’。
3.4
15、C++字符串和C语言的字符数组是可以相互转换的:如果s是一个字符数组,那么string(s)就是相应的字符串;如果s是一个字符串,则s.c_str()就是相应的字符数组。需要注意的是,c_str()返回的内容是只读的。
第4章 函数和递归
4.1
16、为了使用方便,往往用”typedefstruct {域定义;} 类型名;”的方式定义一个新类型名。这样,就可以像原生数据类型一样使用这个自定义类型。
17、程序使用assert.h中的assert宏来限制非法的函数调用:当x>=0不成立时,程序将异常终止。
4.2
18、调用栈描述的是函数之间的调用关系。它由多个栈帧组成,每个栈帧对应着一个为运行完的函数。栈帧中保存了该函数的返回地址和局部变量,因而不仅能在执行完毕后找到正确的返回地址,还很自然地保证了不同函数间的局部变量互不相干。
19、每个变量都占有一定数目的字节(可用sizeof运算符获得),其中第一个字节的地址称为变量的地址。
4.3
20、在可执行文件中,正文段(TextSegment)储存指令,数据段(Data Segment)储存已初始化的全局变量,BSS段储存未赋值的全局变量所需的空间。
21、局部变量也是存放在堆栈段的。栈溢出不见得是递归调用太多,也可能是局部变量太大。只要总大小超过了允许的范围,就会产生栈溢出。
第2部分 算法篇
第5章 基础题目选解
5.2
22、为了方便起见,我们让f[0]保存结果的个位,f[1]是十位,f[2]是百位。。。
23、在C++中,并不需要typedef就可以直接用结构体名来定义,而且还提供“自动初始化”的功能。
24、注意函数定义后的const关键字,它表明“x.str()不会改变x”。
25、重新定义>>和<<运算符,这两个函数要定义在结构体bign的外边,不要写在里面。
5.4
26、你有一块椭圆的土地。你可以在边界上选n个点,并两两连接得到条线段。它们最多能把土地分成多少个部分?
分析:不难发现,最优方案不会让任何3条线段交于一点。根据欧拉公式:V – E + F = 2。其中,V是顶点数(即所有线段的端点数加上交点数),E是边数(即n段椭圆弧加上这些线段被切成的段数),F是面数(即土地块数家和桑椭圆外那个无穷大的面)。换句话说,只需求出V和E,答案就是E – V + 1。
不管是顶点还是边,计算都要枚举一条从固定点出发(所以,最后要乘以n)的所有对角线。假设该对角线的左边有i个点,右边有n – 2 – i个点,则左右两边的点两两搭配后在这条对角线上形成了i(n – 2 – i)个交点,得到了i(n – 2 – i) + 1条线段。注意,每个交点被重复计算了4次,而每条线段被重复计算了2次。下面是完整的公式:
n =1~6, 结果是1、2、4、8、16、31。
第6章 数据结构基础
6.2
27、当用scanf(“%d”)读取整数时,并没有读到它后面的回车换行符。这时,用%c会读到这个回车换行符,而只有%s才会跳过它,读取下一个非空白符组成的字符串。
28、如果不调用srand而直接使用rand(),相当于调用过一次srand(1),因此程序每次执行时,将得到同一套随机数。
6.3
29、小球下落
有一棵二叉树,最大深度为D,且所有叶子的深度都相同。所有结点从上到下从左到右编号为1,2,3,…, – 1。在结点1处放一个小球,它会往下落。每个内结点上都有一个开关,初始全部关闭,当每次有小球落到一个开关上时,它的状态都会改变。当小球到达一个内结点时,如果该结点上的开关关闭,则往左走,否则往右走,直到走到叶子结点。一些小球从结点1处依次开始下落,最后一个小球将会落到哪里呢。
分析:每个小球都会落在根结点上,因此前两个小球必然是一个在左子树,一个在右子树。一般地,只需看小球编号的奇偶性,就能知道它最终在哪棵子树中。对于那些落入根结点左子树的小球来说,只需知道该小球是第几个落在根的左子树里的的,就可以知道它下一步往左还是往右了。以此类推,直到小球落到叶子上。
如果使用编号I,则当I是奇数时,它是往左走的第(I+ 1)/2个小球;当I是偶数时,它是往右走的第I/2个小球。这样,可以直接模拟最后一个小球的路线:
while(scanf(“%d%d”,&D, &I) == 2)
{
int k = 1;
for(int I = 0; I < D – 1; i++)
if(I%2) { k = k*2; I = (I +1)/2; }
else { k = k*2 + 1; I /= 2;}
printf(“%d\n”, k);
}
30、二叉树重建
输入一棵二叉树的先序遍历和中序遍历序列,输出它的后序遍历序列。
分析:
先序遍历的第一个字符就是根,因此只需在中序遍历中找到它,就知道左右子树的先序和后序遍历了。
void build(int n, char *s1, char *s2, char *s)
{
if(n <= 0) return;
int p = strchr(s2, s1[0]) – s2;
build(p, s1+ 1, s2, s);
build(n – p – 1, s1 + p + 1, s2 + p +1, s + p);
s[n – 1] = s1[0];
}
它的作用是根据一个长度为n的先序序列s1和中序序列s2,构造一个长度为n的后序序列。
主程序:
while(scanf(“%s%s”,s1, s2) == 2)
{
int n = strlen(s1);
build(n, s1, s2, ans);
ans[n] = ‘\0’;
printf(“%s\n”, ans);
}
6.4
31、拓扑排序
int c[MAXN];
intt opo[MAXN], t;
bool dfs(int u)
{
c[u] = -1;
for(int v = 0; v < n; v++)if(G[u][v])
{
if(c[v] < 0) returnfalse; // 存在有向环,失败退出
else if(!c[v] &&!dfs(v)) return false;
}
c[u] = 1; topo[--t] = u;
return true;
}
bool toposort()
{
t = n;
memset(c, 0, sizeof(c));
for(int u = 0; u < n; u++)if(!c[u])
if(!dfs(u)) return false;
return true;
}
第7章 暴力求解法
7.3
给定一个集合,枚举它所有可能的子集。
32、增量构造法:
void print_subset(int n, int *A, int cur)
{
for(int i = 0; I < cur; i++)printf(“%d “, A[i]); // 打印当前集合
printf(“\n”);
int s = cur ? A[cur – 1] + 1 : 0; // 确定当前元素的最小可能值
for(int I = s; I < n; i++)
{
A[cur] = I;
print_subset(n, A, cur +1); // 递归构造子集
}
}
33、二进制法:
另外,还可以用二进制来表示{0,1,2,…,n – 1}的子集S,从右往左第i位(各位从0开始编号)表示元素i是否在集合S中。不难看出,A&B,A|B和A^B分别对应集合的交、并和对称差。异或运算最重要的性质就是“开关性”——异或两次以后相当于没有异或,即A^B^B=A。
7.5
34、回溯法是按照深度优先顺序遍历的,它的优点是空间很节省:只有递归栈中的结点需要保存。换句话说,回溯法的空间开销和访问到的最深结点的深度成正比。树不仅可以深度优先遍历,还可以宽度优先遍历。这样做的好处是:找到的第一个解一定是离根最近的解,但空间开销却大大增加(结点队列中的结点可能很多)。
35、树的BFS不需要判重,因为根本不会重复;但对于图来说,如果不判重,时间和空间都将产生极大的浪费。
第8章 高效算法设计
8.1
36、“分成元素个数尽量相等的两半”时分界点的计算。在数学上,分界点应当是x和y的平均数m = (x + y) / 2,我们却用的是x + (y – x) / 2。运算符“/”的“取整”是朝零方向的取整,而不是向下取整。
第3部分 竞赛篇
第9章 动态规划初步
9.1
37、可以用记忆化搜索的方法计算状态转移方程。当采用记忆化搜索时,不必实现确定各状态的计算顺序,但需要记录每个状态“是否已经计算过”。
38、为表项d[i]声明一个饮用ans。这样,任何对ans的读写实际上都是在对d[i]进行。当d[i]换成d[i][j][k][l][m][n]这样很长的名字时,该技巧的优势就会很明显。
9.3
39、0-1背包问题
有n种物品,每种只有一个。第i种物品的体积为,重量为。选一些物品装到一个容量为C的背包,使得背包内物品在总体积不超过C的前提下重量尽量大。
状态转移方程:
(1)用d(i, j)表示当前在第i层,背包剩余容量为j时接下来的最大重量和,则d(i,j) = max{d(i + 1, j), d(i + 1, j – V[i]) + W[i]},边界是i > n时d(i, j) = 0,j < 0时为负无穷。
(2)用f(i, j)表示“把前i个物品装到容量为j的背包中的最大总重量”,则f(i,j) = max{f(i – 1, j), f(i – 1, j – V[i]) + W[i]},边界是类似的。
第10章 数学概念与方法
10.1
40、辗转相除法
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a%b);
}
10.3
41、C语言不支持负数下标的解决方案:最简单的莫过于把所有下标全部加上1。如果觉得这样做破坏了程序的可读性,可以使用宏,像这样:
#define F(i, j) (f[(i)+ 1][(j) + 1])
第11章 图论模型与算法
11.1
42、表达式树:找到“最后计算”的运算符(它是整棵表达式树的根),然后递归处理。
const int maxn = 1000;
int lch[maxn], rch[maxn]; char op[maxn]; //每个结点的左右儿子编号和字符
int nc =0; // 结点数
int build_tree(char *s, int x, int y)
{
int i, c1 = -1, c2 = -1, p = 0;
int u;
if(y – x == 1) // 仅一个字符,建立单独结点
{
u = ++nc;
lch[u] = rch[u] = 0; op[u]= s[x];
return u;
}
for(i = x; i < y; i++)
{
switch(s[i])
{
case ‘(‘ : p++; break;
case ‘)’ : p--; break;
case ‘+’: case ‘-‘ : if(!p) c1 = i; break;
case ‘*‘: case ‘/’ : if(!p) c2 = i; break;
}
}
if(c1 < 0) c1 = c2; // 找不到括号外的加减号,就用乘除号
if(c1 < 0) return build_tree(s, x+ 1, y – 1); // 整个表达式被一对括号括起来
u = ++nc;
lch[u] = build_tree(s, x, c1);
rch[u] = build_tree(s, c1 + 1, y);
op[u] = s[c1];
return u;
}
注意,两个变量c1和c2分别记录“最右”出现的加减号和乘除号。如果括号外有加减号,它们肯定最后计算;但如果没有加减号,就需要考虑乘除号(if(c1 < 0) c1 = c2);如果全都没有,说明整个表达式外面都被一对括号括起来,把它去掉后递归调用。这样,就找到了最后计算的运算符s[c1],它的左子树是区间[x, c1],右区间是[c1 + 1, y]。
附录A
43、在Linux中,可以用time命令计时。例如运行time./abc会执行abc并输出它的运行时间。