该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
“算法分析与设计”是一门面向设计的,处于计算机类相关学科核心地位的课程。无论是计算机系统、系统软件和解决计算机的各种应用课题都可归结为算法的设计。通过本课程的学习,学生将消化理论知识,加深对讲授内容的理解,尤其是一些算法的实现及其应用;并掌握计算机领域中许多常用的非数值计算的算法设计技术:递归算法、分治算法、贪心算法、动态规划算法、回溯算法、分支限界算法,增强独立编程和调试程序的能力;与此同时,读者将对算法的分析与设计有更深刻的认识,并掌握算法分析的方法。
上机实验一般应包括以下几个步骤。
(1)准备好上机所需的程序。手编程序应书写整齐,并经人工检查无误后才能上机。
(2)上机输入和调试自己所编写的程序。一人一组,独立上机调试。若上机时出现问题,最好独立解决。
(3)上机结束后,整理出实验报告。实验报告应包括:题目、程序清单、运行结果、对运行情况所做的分析等。
实验一 递归与分治算法
1.1 实验目的与要求
1. 进一步熟悉C/C++语言的集成开发环境;
2. 通过本实验加深对递归与分治策略的理解和运用。
1.2 实验课时
4学时(课内2学时+课外2学时)
1.3 实验原理
分治(Divide-and-Conquer)的思想:一个规模为n的复杂问题的求解,可以划分成若干个规模小于n的子问题,再将子问题的解合并成原问题的解。
需要注意的是,分治法使用递归的思想。划分后的每一个子问题与原问题的性质相同,可用相同的求解方法。最后,当子问题规模足够小时,可以直接求解,然后逆求原问题的解。
1.4 实验题目
1. 范例:汉诺塔(hanoi)问题
设有A、B、C共3根塔座,在塔座A上堆叠n个金盘,每个盘大小不同,只允许小盘在大盘之上,最底层的盘最大,如附图1所示。现在要求将A上的盘全都移到C上,在移的过程中要遵循以下原则:每次只能移动一个盘;圆盘可以插在A、B和C任一个塔座上;在任何时刻,大盘不能放在小盘的上面。附图1所示为初始状态。
hanoi问题递归求解思想:
我们把一个规模为n的hanoi问题:1到n号盘按照移动规则从A上借助B移到C上表示为H(A,B,C,n);原问题划分成如下三个子问题:
(1)将1到n-1号盘按照移动规则从A上借助C移到B上H(A,C,B,n-1);
(2)将n号盘从A上直接移到C上;
(3)将1到n-1号盘按照移动规则从B上借助A移到C上H(B,A,C,n-1);
经过三个子问题求解,原问题的也即求解完成。
hanoi问题递归求解代码:
void H(char A,charB,charC,int n)
{
if(n>0)
{
H(A,C,B,n-1);
printf("%d from %c to %c",n,A,C);
H(B,A,C,n-1);
}
}
2. 上机题目:格雷码构造问题
Gray码是一个长度为2n的序列。序列无相同元素,每个元素都是长度为n的串,相邻元素恰好只有一位不同。试设计一个算法对任意n构造相应的Gray码(分治、减治、变治皆可)。
对于给定的正整数n,格雷码为满足如下条件的一个编码序列。
(1)序列由2n个编码组成,每个编码都是长度为n的二进制位串。
(2)序列中无相同的编码。
(3)序列中位置相邻的两个编码恰有一位不同。
1.5 思考题
(1)递归的关键问题在哪里?
(2)递归与非递归之间如何实现程序的转换?
(3)分析二分查找和快速排序中使用的分治思想。
(4)分析二次取中法和锦标赛算法中的分治思想。
实验二 贪心算法
2.1 实验目的与要求
1. 理解贪心算法的基本思想;
2. 运用贪心算法解决实际问题,加深对贪心算法的理解和运用。
2.2 实验课时
4学时(课内2学时+课外2学时)
2.3 实验原理
贪心算法的思想:
(1)贪心算法(Greedy Approach)能得到问题的最优解,要证明我们所做的第一步选择一定包含着一个最优解,即存在一个最优解的第一步是从我们的贪心选择开始。
(2)在做出第一步贪心选择后,剩下的子问题应该是和原问题类似的规模较小的子问题,为此我们可以用数学归纳法来证明贪心选择能得到问题的最优解。
2.4 实验题目
1. 范例:单源最短路径
在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V到其余各点的最短路径。
按路径长度递增次序产生最短路径算法。
首先把V分成两组:
(1)S:已求出最短路径的顶点的集合;
(2)V-S=T:尚未确定最短路径的顶点集合。
然后将T中顶点按最短路径递增的次序加入到S中,需保证:
(1)从源点V到S中各顶点的最短路径长度都不大于从V到T中任何顶点的最短路径长度;
(2)每个顶点对应一个距离值。S中顶点:从V到此顶点的最短路径长度;T中顶点:从V到此顶点的只包括S中顶点作中间顶点的最短路径长度。
依据:可以证明V到T中顶点Vk的最短路径,或是从V到Vk的直接路径的权值;或是从V经S中顶点到Vk的路径权值之和。(反证法可证)
单源最短路径代码:
void Dijkstra(AdjMGraph G,int v0,int *dist, int *path)
{
int n=G.Vertices.size;
inti,j,k ,pre ,min ;
int *s=(int *)malloc(sizeof(int)*n);
for(i=0;i
{
s[i]=0;
dist[i]=G->edge[v0][i];
if(i!=v0&&dist[i]
path[i]=v0;
else
path[i]=-1;
}
s[v0]=1; //标记v0已从集合T中加入到S
for(i=1;i
{
min=MaxWeight;
for(j=0;j
if (s[j]==0&&dist[j]< min )
{
min=dist[j];
k=j;
}
if(min == MaxWeight)
return;
S[k]=1; //标记k已从集合T中加入到S
for (j=0;j
if (s[j]==0&&dist[j]>dist[k]+ G->edge[k][j])
{
dist[j]=dist[k]+G->edge[k][j];
path[j]=k;
}
}
}
2. 上机题目:最小延迟调度问题
给定等待服务的客户集合A={1,2,…,n},预计对客户i的服务时长为ti>0,T=(t1,t2,…,tn),客户i希望的服务完成时刻为di>0,D=(d1,d2,…,dn);一个调度f:A→N,f(i)为客户i的开始时刻。如果对客户i的服务在di之前结束,那么对客户i的服务没有延迟,即如果在di之后结束,那么这个服务就被延迟了,延迟的时间等于该服务的实际完成时刻f(i)+ti减去预期结束时刻di。一个调度f的最大延迟是所有客户延迟时长的最大值maxi∈A{f(i)+ti-di}。附图2所示是不同调度下的最大延迟。使用贪心策略找出一个调度使得最大延迟达到最小。
2.5 思考题
(1)哈夫曼编码问题的编程如何实现?
(2)使用贪心策略求解背包问题。
(3)分析普里姆算法和克鲁斯卡尔算法中的贪心策略。
(4)思考如何证明贪心策略的正确性。
(5)使用贪心策略求解多机调度问题。
实验三 动态规划算法
3.1 实验目的与要求
1. 理解动态规划算法的基本思想;
2. 运用动态规划算法解决实际问题,加深对贪心算法的理解和运用。
3.2 实验课时
4学时(课内2学时+课外2学时)
3.3 实验原理
动态规划(Dynamic Programming)算法思想:把待求解问题分解成若干个子问题,先求解子问题,然后由这些子问题的解得到原问题的解。动态规划求解过的子问题的结果会被保留下来,不像递归那样每个子问题的求解都要从头开始反复求解。动态规划求解问题的关键在于获得各个阶段子问题的递推关系式:
(1)分析原问题的最优解性质,刻画其结构特征;
(2)递归定义最优值;
(3)自底向上(由后向前)的方式计算最优值;
(4)根据计算最优值时得到的信息,构造一个最优解。
3.4 实验题目
1. 范例:多阶段决策问题——最短路径
如附图3所示从A0点要铺设一条管道到达A6点,中间必须经过5个中间站,第一站可以在A1、B1两地中任选一个站点,其他站点类似,连接两地间管道的距离(造价)用如附图3所示中连线的数字表示,求A0到A6间的最短造价路径。
代码清单:
#define m1 8
#define m 7
#define n1 17
#include
#include
main()
{
inti,j,k,fw;
int n[m1];
int c[m1][m1][m1];
int s[m1][m1];
int f[m1][m1];
int b[n1];
//clrscr();
for (i=0;i<=m;i++)
{
n[i]=0;
for(j=0;j<=m;j++)
{
s[i][j]=0;
f[i][j]=0;
for(k=0;k<=m;k++)
c[i][j][k]=0;
}
}
printf("\n输入每个阶段的结点数目:");
for(i=1;i<=m;i++)
scanf("%d",&n[i]);
printf("\n输入每阶段的结点的编号:");
for(i=1;i<=m;i++)
for(j=1;j<=n[i];j++)
scanf("%d",&s[i][j]);
printf("\n输入i阶段结点s[i,j]与i+1阶段中结点s[i+1,k]之间的权值:");
for(i=1;i<=m-1;i++)
for(j=1;j<=n[i];j++)
for(k=1;k<=n[i+1];k++)
scanf("%d",&c[i][j][k]);
printf("\n反推法计算第一步的最优值:");
for(j=1;j<=n[m-1];j++)
{
f[m-1][j]=c[m-1][j][1];
b[s[m-1][j]]=s[m][1];
}
printf("\n计算第二步递归定义的其他最优值:");
for(i=m-2;i>=1;i--)
for(j=1;j<=n[i];j++)
{
f[i][j]=c[i][j][1]+f[i+1][1];
b[s[i][j]]=s[i+1][1];
if(n[i+1]!=1)
for(k=2;k<=n[i+1];k++)
{
fw=c[i][j][k]+f[i+1][k];
if(f[i][j]>fw)
{
f[i][j]=fw;
b[s[i][j]]=s[i+1][k];
}
}
}
i=1;
printf("\n%4d",i);
while(b[i]!=n1-1)
{
printf("%4d",b[i]);
i=b[i];
}
printf("%4d\n",b[i]);
printf("\nthemininum cost is %d:",f[1][1]);
getch();
}
/*5 3 1 3 6 100 100 8 7 6 6 8 100 3 5 100 100 3 3 100 8 4 2 2 100 100 1 2 100 3 3 3 5
5 2 6 6 4 3*/
2. 上机题目:最大子段和问题
给定n个整数(可以为负数)组成的序列(a1,a2,…,an),使用动态规划思想求该序列的子段和的最大值。注:当所有整数均为负整数时,其最大子段和为0。
例如,对于六元组(-2, 11, -4, 13, -5, -2),其最大字段和为:a2+ a3+ a4= 20。
除了动态规划,该问题可以使用顺序求和+比较(蛮力法)和分治法求解,思考其求解过程。
3.5 思考题
(1)深刻理解动态规划与递归求解问题的区别是什么?
(2)动态规划思想解题的步骤是什么?
(3)动态规划思想和贪心算法在求解问题时的区别是什么?
(4)使用动态规划算法求解最长公共子序列(LCS)问题。
(5)使用动态规划算法求解最长最大字段和问题。
实验四 回溯算法
4.1 实验目的与要求
1. 通过回溯算法的示例程序理解回溯算法的基本思想;
2. 运用回溯算法解决实际问题,进一步加深对回溯算法的理解和运用。
4.2 实验课时
4学时(课内2学时+课外2学时)。
4.3 实验原理
回溯算法(Backtrack)的基本做法是搜索,或是一种组织得井井有条的、能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
回溯算法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解:如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
回溯算法的基本步骤:
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
常用剪枝函数:
(1)用约束函数在扩展结点处剪去不满足约束的子树;
(2)用限界函数剪去得不到最优解的子树。
4.4 实验题目
1. 范例:0-1背包问题
有n件物品和一个容量为c的背包。第i件物品的容量是w[i],价值是p[i]。求解将哪些物品装入背包可使价值总和最大。
代码清单:
#include
#define c 30
#define n 3
int bound(inti,intcw,intcp,int w[],int p[])
{
int cleft=c-cw;
int b=cp;
while(i<=n && w[i]<=cleft)
{
cleft-=w[i];
b+=p[i];
i++;
}
if(i<=n)
b+=p[i]/w[i]*cleft;
return b;
}
void back(inti,intcw,intcp,int *bestp,int w[],int p[],int x[])
{
if(i>n)
{
*bestp=cp;
return;
}
if(cw+w[i]<=c)
{
cw+=w[i];
cp+=p[i];
x[i]=1;
back(i+1,cw,cp,bestp,w,p,x);
cw-=w[i];
cp-=p[i];
}
if(bound(i+1,cw,cp,w,p)>*bestp)
{
x[i]=0;
back(i+1,cw,cp,bestp,w,p,x);
}
}
main()
{
intcw=0,cp=0,bestp=0,x[5];
int w[]={0,16,15,15},p[]={0,45,25,25};
inti;
clrscr();
back(1,cw,cp,&bestp,w,p,x);
printf(" %d\n",bestp);
for(i=1;i<=n;i++)
printf("%2d",x[i]);
getch();
}
2. 上机题目:排兵布阵问题
某游戏中,不同的兵种处于不同的地形上时,其攻击能力也一样,现有n个不同兵种的角色(1, 2, ..., n),需安排在某战区n个点上,角色i在j点上的攻击力为Aij,使用回溯法设计一个布阵方案,使总的攻击力最大。注:个人决定A矩阵的初始化工作。该问题求解算法的输入数据形如附图4所示。
4.5 思考题
(1)什么是启发式搜索问题?
(2)搜索算法的解空间树的如何定义?
(3)0-1背包问题的动态规划算法如何求解?
(4)n皇后问题使用回溯法如何求解?
(5)使用回溯法求解装载问题。
实验五 分支限界算法
5.1 实验目的与要求
1. 通过分支限界算法的示例程序进一步理解分支限界算法的基本思想;
2. 运用分支限界算法解决实际问题,进一步加深对分支限界算法的理解和运用。
5.2 实验课时
4学时(课内2学时+课外2学时)。
5.3 实验原理
分枝限界(Branch-and-Bound)算法是另一种系统地搜索解空间的方法,它与回溯算法的主要区别在于对E-结点的扩充方式。每个活结点有且仅有一次机会变成E-结点。当一个结点变为E-结点时,则生成从该结点移动一步即可到达的所有新结点。在生成的结点中,抛弃那些不可能导出(最优)可行解的结点,其余结点加入活结点表,然后从表中选择一个结点作为下一个E-结点。从活结点表中取出所选择的结点并进行扩充,直到找到解或活动表为空,扩充过程才结束。
有两种常用的方法可用来选择下一个E-结点(虽然也可能存在其他的方法):
(1)先进先出(F I F O)即从活结点表中取出结点的顺序与加入结点的顺序相同,因此活结点表的性质与队列相同。
(2)(优先队列)最小耗费(LC)或最大收益法在这种模式中,每个结点都有一个对应的耗费或收益。如果查找一个具有最小耗费的解,则活结点表可用最小堆来建立,下一个E-结点就是具有最小耗费的活结点;如果希望搜索一个具有最大收益的解,则可用最大堆来构造活结点表,下一个E-结点是具有最大收益的活结点。
5.4 实验题目
1. 范例:旅行商售货员(TSP)问题
某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一次,最后回到驻地的路线,使总的路程(或总旅费)最小。
实验提示:旅行商问题的解空间树是一个排列树。有两种实现的方法。第一种是只使用一个优先队列,队列中的每个元素中都包含到达根的路径。另一种是保留一个部分解空间树和一个优先队列,优先队列中的元素并不包含到达根的路径。以下为第一种方法。
由于我们要寻找的是最小耗费的旅行路径,因此可以使用最小耗费分支限界算法。在实现过程中,使用一个最小优先队列来记录活结点,队列中每个结点的类型为MinHeapNode。每个结点包括如下区域:x(从1到n的整数排列,其中x[0] = 1),s(一个整数,使得从排列树的根结点到当前结点的路径定义了旅行路径的前缀x[0:s],而剩余待访问的结点是x[s+ 1 : n- 1 ]),cc(旅行路径前缀,即解空间树中从根结点到当前节点的耗费),lcost(该节点子树中任意叶节点中的最小耗费),rcost(从顶点x[s: n- 1]出发的所有边的最小耗费之和)。当类型为MinHeapNode( T)的数据被转换成为类型T时,其结果即为lcost的值。分支限界算法的代码见程序。
程序首先生成一个容量为100的最小堆栈,用来表示活结点的最小优先队列。活结点按lcost值从最小堆中取出。接下来,计算有向图中从每个顶点出发的边中耗费最小的边所具有的耗费MinOut。如果某些顶点没有出边,则有向图中没有旅行路径,搜索终止。如果所有的顶点都有出边,则可以启动最小耗费分支限界搜索。根的孩子B作为第一个E-结点,在此结点上,所生成的旅行路径前缀只有一个顶点1,因此s=0, x[0]=1, x[1:n-1]是剩余的顶点(即顶点2,3,…,n)。旅行路径前缀1的开销为0,即cc= 0,并且,rcost=n&&i=1时MinOut。
在程序中,bestc给出了当前能找到的最少的耗费值。初始时,由于没有找到任何旅行路径,因此bestc的值被设为NoEdge。
旅行商问题的最小耗费分枝定界算法如下。
templateT AdjacencyWDigraph::BBTSP(int v[])
{ // 旅行商问题的最小耗费分枝定界算法
// 定义一个最多可容纳1000个活节点的最小堆栈
MinHeap>H(1000);
T *MinOut = new T [n+1];
// 计算MinOut= 离开顶点i的最小耗费边的耗费
T MinSum = 0; // 离开顶点i的最小耗费边的数目
for (inti = 1; i<= n; i++)
{
T Min = NoEdge;
for (int j = 1; j <= n; j++)
if (a[j] != NoEdge&& (a[j] < Min || Min == NoEdge))
Min = a[j];
if (Min == NoEdge)
return NoEdge; // 此路不通
MinOut = Min;
MinSum += Min;
}
// 把E-结点初始化为树根
MinHeapNode E;
E.x = new int [n];
for (i=0; i
E.x=i+1;
E.s=0; // 局部旅行路径为x [ 1 : 0 ]
E.cc=0; // 其耗费为0
E.rcost=MinSum;
T bestc=NoEdge; // 目前没有找到旅行路径
// 搜索排列树
while (E.s
{ // 不是叶子
if(E.s==n-2)
{ // 叶子节点的父结点
// 通过添加两条边来完成旅行
// 检查新的旅行路径是不是更好
if (a[E.x[n-2]][E.x[n-1]]!= NoEdge&& a[E.x[n-1]][1]!=NoEdge&& (E.cc +
a[E.x[n-2]][E.x[n-1]]+a[E.x[n-1]][1]
{ // 找到更优的旅行路径
bestc=E.cc + a[E.x[n-2]][E.x[n-1]]+a[E.x[n-1]][1];
E.cc=bestc;
E.lcost=bestc;
E.s++;
H.Insert( E ) ;
}
else
delete [] E.x;
}
else
{ // 产生孩子
for (int i=E.s+1; i
if (a[E.x[E.s]][E.x]!=NoEdge)
{ // 可行的孩子,限定了路径的耗费
T cc=E.cc+a[E.x[E.s]][E.x];
T rcost=E.rcost-MinOut[E.x[E.s]];
T b=cc+rcost; // 下限
if (b
{// 子树可能有更好的叶子
// 把根保存到最大堆栈中
MinHeapNode N;
N.x=new int [n];
for (int j=0; j
N.x[j]=E.x[j];
N.x[E.s+1]=E.x;
N.x=E.x[E.s+1];
N.cc=cc;
N.s=E.s+1;
N.lcost=b;
N.rcost=rcost;
H.Insert( N ) ;
}
} // 结束可行的孩子
delete [] E.x;
} // 对本结点的处理结束
try
{
H.DeleteMin(E);
} // 取下一个E-结点
catch(OutOfBounds)
{
break;
} // 没有未处理的结点
}
if (bestc==NoEdge)
return NoEdge; // 没有旅行路径将最优路径复制到v[1:n] 中
for (i=0; i
v[i+1]=E.x;
while (true)
{ // 释放最小堆中的所有结点
delete [] E.x;
try
{
H.DeleteMin(E);
}
catch (OutOfBounds)
{
break;
}
}
returnbestc;
}
2. 上机题目:运动员最佳配对问题
羽毛球队有男女运动员各n人。给定两个n×n矩阵P和Q;P[i][j]表示男运动员i和女运动员j配对组成混合双打的男运动员竞赛优势,Q[i][j]表示女运动员i和男运动员j配对组成混合双打的女运动员竞赛优势(注意:由于多种原因,P[i][j]未必等于Q[j][i]),男运动员i和女运动员j配对组成混合双打的男女运动员双方总体竞赛优势为P[i][j]* Q[j][i]。用分支限界法设计一个算法,计算男女运动员的最佳配对,即各组男女运动员双方总体竞赛优势的总和达到最大。
5.5 思考题
(1)批处理作业调度问题的分支限界算法如何求解?
(2)0-1背包问题的分支限界算法如何求解?