算法竞赛入门经典读书笔记

算法竞赛入门经典

第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并输出它的运行时间。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值