算法竞赛入门经典 紫书 第四章

一点小问题

关于判断素数的几点

//该函数有严重缺点:
//不能用于n==1和n较大的情况
//在n接近int的最大值时:
//若i=46340时,i*i=2147395600
//若i=46341时,i*i=2147488281超过了int的最大值,溢出变成了负数,就会继续进行下去

int is_prime(int n)
{
	for (int i = 2; i * i <= n; i++)
	{
		if (n % i == 0)
			return 0;
	}
	return 1;
}
//编写函数的时候,应该尽量保证该函数能对任何合法参数得到正确的结果
//改进后
//优点:避免了每次重复计算sqrt(n)
//通过四舍五入避免了浮点误差

int is_prime(int n)
{
	if (n <= 1)
		return 0;
	int m = floor(sqrt(n) + 0.5);
	for (int i = 2; i<=m; i++)
	{
		if (n % i == 0)
			return 0;
	}
	return 1;
}

谨慎地使用全局变量

调用栈

调用栈描述的是函数之间的调用关系
由多个栈帧组成,每个栈帧对应着一个未运行完的函数
栈帧中保存了该函数的返回地址和局部变量,因而不仅能在执行完毕后找到正确的返回地址,还能自然地保证了不同函数间的局部变量互不相干——因为不同函数之间对应不同的栈帧

指针易翻

这有一段错误的程序

void swap(int* a, int* b)
{
	int* t;
	*t = *a;
	*a = *b;
	*b = *t;
}

这个程序错误的原因是:
指针 t 在使用之前必须赋值
t在赋值之前是不确定的。如果这个不确定的值所代表的内存单元恰好是能写入的,那么这段程序将正常工作;但如果不是,而是只读的,那么程序就会可能崩溃

数组作参数易翻

这有一段错误的程序:

int sum(int a[])
{
	int ans = 0;
	for (int i = 0; i < sizeof(a); i++)
	{
		ans += a[i];
	}
	return ans;
}

错误的原因:
sizeof(a)是无法得到数组的大小的
因为,数组作为参数传递给函数时,实际上只有数组的首地址作为指针传递给了函数。
在函数定义中的int a[]等价于int *a。
在只有地址信息的情况下,是无法知道数组里有多少个元素的
正确的做法:
加一个参数,即数组的元素个数

一般的,若p是指针,k是正整数,那么p+k就是指针p后面的第k个元素,p-k是p前面的第k个元素,而如果p1和p2是类型相同的指针,则p2-p1是从p1到p2的元素个数

那么我们对计算数组和的函数可以用两种写法:
1.

int sum(int* begin, int* end)
{
	int n = end - begin;
	int ans = 0;
	for (int i = 0; i < n; i++)
	{
		ans += begin[i];
	}
	return ans;
}
int sum(int* begin, int* end)
{
	int* p = begin;
	int ans = 0;
	for (int* p = begin; p != end; p++)
	{
		ans += *p;
	}
	return ans;
}

上述两种写法都是左闭右开
第二种写法更为普遍
把数组作为指针传递给函数时,数组内容是可以修改的。
如果要写一个“返回数组“的函数,可以加一个数组参数,然后在函数中修改这个数组的内容

递归

在C语言的函数中,调用自己和调用其他函数并没有任何本质区别,都是建立新栈帧,传递参数并修改当前代码行。
在函数体执行完毕后,删除栈帧,处理返回值并修改当前代码行。

段错误

编译后产生的可执行文件里都保存着与操作系统相关的内容。
段。是指二进制文件内的区域,所有某种特定类型的信息被保存在里面。

在可执行文件里,正文段用于存储指令
数据段用于存储已初始化的全局变量
BSS段用于存储未赋值的全局变量所需的空间

调用栈不存储在可执行文件中,而是在运行时创建
调用栈所在段称为堆栈段
堆栈段也有自己的大小,不能被越界访问,否则就会出现段错误

栈溢出,那么就是每次递归调用都会往调用栈里增加一个栈帧,久而久之,就越界了。

在运行时,程序会动态创建一个堆栈段,里面存放着调用栈,因此保存着函数的调用关系和局部变量

局部变量也是存放在堆栈段内的。
栈溢出不一定是递归调用太多,也可能是局部变量太大
因此,我们一般把较大的数组存放在main函数外

例题子

例题4-1 古老的密码 UVa1339

//qsort函数的声明
void qsort(void* base, size_t num, size_t size, int (*comparator)(const void*, const void*))
//qsort函数实现的是快速排序算法
//参数说明:
//待排序的数组起始地址
//元素个数
//元素的大小
//一个指向函数的指针,该函数必须具有以下行书
int cmp(const void *,const void *) {...}
//这个函数中,const void *的意思是:
//指向常数的万能的指针,它可以通过强制类型转化变成任意类型的指针
//例如,如果排序的对象是个整型数组,那么:
int cmp(const void* a, const void* b)
{
	return *(int*)a - *(int*)b;
}
//一般的,需要先把参数a和参数b转化为真实的类型,然后让cmp函数当ab时分别返回负数,0,和正数即可。

qsort在算法竞赛中不经常使用
经常使用sort函数
这里是为了告诉“将一个函数作为参数传递给另外一个函数”是很有用的

例题4-2 刽子手游戏 UVa489
我们先来考虑,程序设计的方式
一般有两种:
自顶向下和自底向上
算法竞赛中一般是自顶向下
即:
先写伪代码,然后转化为实际的代码
先写主程序,包括对函数的调用,在实现函数本身。

这个题没有什么难点,只是说注意一点就可以
代码的注释我都不想写了
太简单了

#include
#include
#define maxn 100
int left, chance;
char s[maxn], s2[maxn];
int win, lose;

void guess(char ch)
{
	int bad = 1;
	for (int i = 0; i < strlen(s); i++)
	{
		if (s[i] == ch)
		{
			left--;
			s[i] = ' ';	//字符串序列中有当前猜的字母,则对这个清为空格
			bad = 0;
		}
	}
	if (bad)
		--chance;
	if (!chance)
		lose = 1;
	if (!left)
		win = 1;
}

int main()
{
	int rnd;
	while (scanf("%d%s%s", &rnd, s, s2) == 3 && rnd != -1)
	{
		printf("Round %d\n", rnd);
		win = lose = 0;
		left = strlen(s);
		chance = 7;
		for (int i = 0; i < strlen(s2); i++)
		{
			guess(s2[i]);
			if (win || lose)
				break;
		}
		if (win)
			printf("You win.\n");
		else if (lose)
			printf("You lose.\n");
		else
			printf("You chickened out.\n");
	}
	return 0;
}

例题4-3 救济金发放 UVa133
这个需要注意的地方就是
循环的处理方式
这个算作是约瑟夫环的进阶版

int go(int p, int d, int t)
{
	while (t--)
	{
		do 
		{
			p = (p + d + n - 1) % n + 1; 	//向前、后走
		}while (a[p] == 0);	//跳过为0项
	}
	return p;
}

例题4-4 信息解码 UVa213
书上代码:

#include
#include
int readchar()
{
	for (;;)
	{
		int ch = getchar();
		if (ch != '\n' && ch != '\r')
			return ch;		//一直读到换行符为止
	}
}

//readint主要用于读取01序列
int readint(int c)	//用于读取c位二进制字符,并转换为十进制数
{
	int v = 0;
	while (c--)
	{
		v = v * 2 + readchar() - '0';
	}
	return v;
}

int code[8][1 << 8];
int readcodes()	//用于读取编码,并形成编码序列
{
	memset(code, 0, sizeof(code));	//对编码序列清零
	code[1][0] = readchar();	//读取编码头的第一个字符
	for (int len = 2; len < 7; len++)	//len表示 编码长度,len=2,即表示下面进行对00,01,10的编码对应
	{
		for (int i = 0; i < (1 << len) - 1; i++)	//例如,当len=2时,那么会有三个字符输入,即(1<
		{
			int ch = getchar();	//读字符
			if (ch == EOF)	//程序结束,整个程序的结束
				return 0;
			if (ch == '\n' || ch == '\r')	//编码头的读取结束
				return 1;
			code[len][i] = ch;	//将字符存入所对应的位置。例如01,就存储长度为2的,第2个位置;10在存入长度为2的,第3个位置
		}
	}
	return 1;
}

int main()
{
	while (readcodes())	//读取编码头,形成编码序列
	{
		for (;;)
		{
			int len = readint(3);	//读取三位二进制数,以形成编码长度
			if (len == 0)	//标志结束,即000
				break;
			for (;;)
			{
				int v = readint(len);
				if (v == (1 << len) - 1)	//标志读到了全1序列,则直接退出。进行下一个长度的读取
					break;
				putchar(code[len][v]);
			}
		}
		putchar('\n');
	}
	return 0;
}

习题

习题4-1 UVa1589
习题4-2 UVa201
习题4-3 UVa220
习题4-4 UVa253
习题4-5 UVa1590
习题4-6 UVa508
习题4-7 UVa509
习题4-8 UVa12108
习题4-9 UVa1591
习题4-10 UVa815
习题4-11 UVa1588
习题4-12 UVa11809

你可能感兴趣的:(算法竞赛入门经典,书籍的知识)