要使计算机能完成人们预定的工作,首先必须为如何完成预定的
工作设计一个算法,然后再根据算法编写程序。计算机程序要对
问题的每个对象和处理规则给出正确详尽的描述,其中程序的数
据结构和变量用来描述问题的对象,程序结构、函数和语句用来
描述问题的算法。算法数据结构是程序的两个重要方面。
算法是问题求解过程的精确描述,一个算法由有限条可完全机械
地执行的、有确定结果的指令组成。指令正确地描述了要完成的
任务和它们被执行的顺序。计算机按算法指令所描述的顺序执行
算法的指令能在有限的步骤内终止,或终止于给出问题的解,或
终止于指出问题对此输入数据无解。
通常求解一个问题可能会有多种算法可供选择,选择的主要标准
是算法的正确性和可靠性,简单性和易理解性。其次是算法所需
要的存储空间少和执行更快等。
算法设计是一件非常困难的工作,经常采用的算法设计技术主要
有迭代法、穷举搜索法、递推法、贪婪法、回溯法、分治法、动
态规划法等等。另外,为了更简洁的形式设计和藐视算法,在算
法设计时又常常采用递归技术,用递归描述算法。
一、迭代法
迭代法是用于求方程或方程组近似根的一种常用的算法设计方
法。设方程为 f(x)=0,用某种数学方法导出等价的形式 x=g(x),
然后按以下步骤执行:
(1) 选一个方程的近似根,赋给变量 x0;
(2) 将 x0 的值保存于变量 x1,然后计算 g(x1),并将结果存
于变量 x0;
(3) 当 x0 与 x1 的差的绝对值还小于指定的精度要求时,重复
步骤(2)的计算。
若方程有根,并且用上述方法计算出来的近似根序列收敛,则按
上述方法求得的 x0 就认为是方程的根。上述算法用 C 程序的形
式表示为:
【算法】迭代法求方程的根
{ x0=初始近似根;
do {
x1=x0;
x0=g(x1); /*按特定的方程计算新的近似根*/
} while ( fabs(x0-x1)>Epsilon);
printf(“方程的近似根是%f\n”,x0);
}
迭代算法也常用于求方程组的根,令
X=(x0,x1,…,xn-1)
设方程组为:
xi=gi(X) (I=0,1,…,n-1)
则求方程组根的迭代算法可描述如下:
【算法】迭代法求方程组的根
{ for (i=0;i x=初始近似根;
do {
for (i=0;i y=x;
for (i=0;i x=gi(X);
for (delta=0.0,i=0;i if (fabs(y-x)>delta) delta=fabs(y-x);
} while (delta>Epsilon);
for (i=0;i printf(“变量 x[%d]的近似根是 %f”,I,x);
printf(“\n”);
}
具体使用迭代法求根时应注意以下两种可能发生的情况:
(1) 如果方程无解,算法求出的近似根序列就不会收敛,迭代
过程会变成死循环,因此在使用迭代算法前应先考察方程是否有
解,并在程序中对迭代的次数给予限制;
(2) 方程虽然有解,但迭代公式选择不当,或迭代的初始近似
根选择不合理,也会导致迭代失败。
二、穷举搜索法
穷举搜索法是对可能是解的众多候选解按某种顺序进行逐一枚
举和检验,并从众找出那些符合要求的候选解作为问题的解。
【问题】 将 A、B、C、D、E、F 这六个变量排成如图所示的
三角形,这六个变量分别取[1,6]上的整数,且均不相同。求使
三角形三条边上的变量之和相等的全部解。如图就是一个解。
程序引入变量 a、b、c、d、e、f,并让它们分别顺序取 1 至 6
的证书,在它们互不相同的条件下,测试由它们排成的如图所示
的三角形三条边上的变量之和是否相等,如相等即为一种满足要
求的排列,把它们输出。当这些变量取尽所有的组合后,程序就
可得到全部可能的解。细节见下面的程序。
【程序 1】
# include
void main()
{ int a,b,c,d,e,f;
for (a=1;a<=6;a++)
for (b=1;b<=6;b++) {
if (b==a) continue;
for (c=1;c<=6;c++) {
if (c==a)||(c==b) continue;
for (d=1;d<=6;d++) {
if (d==a)||(d==b)||(d==c) continue;
for (e=1;e<=6;e++) {
if (e==a)||(e==b)||(e==c)||(e==d) continue;
f=21-(a+b+c+d+e);
if ((a+b+c==c+d+e))&&(a+b+c==e+f+a)) {
printf(“%6d,a);
printf(“%4d%4d”,b,f);
printf(“%2d%4d%4d”,c,d,e);
scanf(“%*c”);
}
}
}
}
}
}
按穷举法编写的程序通常不能适应变化的情况。如问题改成有 9
个变量排成三角形,每条边有 4 个变量的情况,程序的循环重数
就要相应改变。
对一组数穷尽所有排列,还有更直接的方法。将一个排列看作一
个长整数,则所有排列对应着一组整数。将这组整数按从小到大
的顺序排列排成一个整数,从对应最小的整数开始。按数列的递
增顺序逐一列举每个排列对应的每个整数,这能更有效地完成排
列的穷举。从一个排列找出对应数列的下一个排列可在当前排列
的基础上作部分调整来实现。倘若当前排列为 1,2,4,6,5,
3,并令其对应的长整数为 124653。要寻找比长整数 124653 更
大的排列,可从该排列的最后一个数字顺序向前逐位考察,当发
现排列中的某个数字比它前一个数字大时,如本例中的 6 比它的
前一位数字 4 大,这说明还有对应更大整数的排列。但为了顺序
从小到大列举出所有的排列,不能立即调整得太大,如本例中将
数字 6 与数字 4 交换得到的排列 126453 就不是排列 124653 的下
一个排列。为了得到排列 124653 的下一个排列,应从已经考察
过的那部分数字中选出比数字大,但又是它们中最小的那一个数
字,比如数字 5,与数字 4 交换。该数字也是从后向前考察过程
中第一个比 4 大的数字。5 与 4 交换后,得到排列 125643。在前
面数字 1,2,5 固定的情况下,还应选择对应最小整数的那个排
列,为此还需将后面那部分数字的排列顺序颠倒,如将数字 6,
4,3 的排列顺序颠倒,得到排列 1,2,5,3,4,6,这才是排
列 1,2,4,6,5,3 的下一个排列。按以上想法编写的程序如
下。
【程序 2】
# include
# define SIDE_N 3
# define LENGTH 3
# define VARIABLES 6
int A,B,C,D,E,F;
int *pt[]={&A,&B,&C,&D,&E,&F};
int
*side[SIDE_N][LENGTH]={&A,&B,&C,&C,&D,&E,&E,&F,&
A};
int side_total[SIDE_N];
main{}
{ int i,j,t,equal;
for (j=0;j
while(1)
*pt[j]=j+1;
{ for (i=0;i
{ for (t=j=0;j
t+=*side[i][j];
side_total[i]=t;
}
for (equal=1,i=0;equal&&i
if (side_total[i]!=side_total[i+1]
equal=0;
if (equal)
{ for (i=1;i
printf(“%4d”,*pt[i]);
printf(“\n”);
scanf(“%*c”);
}
for (j=VARIABLES-1;j>0;j--)
if (*pt[j]>*pt[j-1]) break;
if (j==0) break;
for (i=VARIABLES-1;i>=j;i--)
if (*pt[i]>*pt[i-1]) break;
t=*pt[j-1];* pt[j-1] =* pt[i]; *pt[i]=t;
for (i=VARIABLES-1;i>j;i--,j++)
{ t=*pt[j]; *pt[j] =* pt[i]; *pt[i]=t; }
}
}
从上述问题解决的方法中,最重要的因素就是确定某种方法来确
定所有的候选解。下面再用一个示例来加以说明。
【问题】 背包问题
问题描述:有不同价值、不同重量的物品 n 件,求从这 n 件物
品中选取一部分物品的选择方案,使选中物品的总重量不超过指
定的限制重量,但选中物品的价值之和最大。
设 n 个物品的重量和价值分别存储于数组 w[ ]和 v[ ]中,限制重
量为 tw。考虑一个 n 元组(x0,x1,…,xn-1),其中 xi=0 表
示第 i 个物品没有选取,而 xi=1 则表示第 i 个物品被选取。显然
这个 n 元组等价于一个选择方案。用枚举法解决背包问题,需要
枚举所有的选取方案,而根据上述方法,我们只要枚举所有的 n
元组,就可以得到问题的解。
显然,每个分量取值为 0 或 1 的 n 元组的个数共为 2n 个。而每
个 n 元组其实对应了一个长度为 n 的二进制数,且这些二进制
数的取值范围为 0~2n-1。因此,如果把 0~2n-1 分别转化为相
应的二进制数,则可以得到我们所需要的 2n 个 n 元组。
【算法】
maxv=0;
for (i=0;i<2n;i++)