算法综合实验报告学号: 1004111107 姓名:黄琼莹一、实验内容:分别用动态规划、贪心及分支限界法实现对TSP问题(无向图)的求解,并至少用两个测试用例对所完成的代码进行正确性及效率关系上的验证。
二、程序设计的基本思想、原理和算法描述:(包括程序的数据结构、函数组成、输入/输出设计、符号名说明等)1、动态规划法(1)数据结构:利用二进制来表示集合,则集合S可由一个十进制数x相对应,此x所对应的二进制数为y,如果y的第k位为1,则表示k存在集合S中。
例如:集合S={0,1}(其子集合为{}{0}{1}{01}),我们用二进制数11(所对应十进制数为3)表示S,11中右手边第1个数为1表示0在集合S中,右手边第二个数为1表示1在集合S中,其他位为0表示其它数字不在集合S中;同理,集合S={0,2}(其子集合为{}{0}{2}{02}可用二进制数101(所对应十进制数为5)表示(右手边第1个数为1表示0在集合S中,右手边第二个数为0表示1不在集合S中,右手边第3个数为1表示2在集合S中,则说明0,2在集合中,1不在集合中。
(2)函数组成getmin():获得该数组的最小值;getJ():根据2进制j和j中1的个数找下一个jgetnextj():返回下一个j的十进制数(3)输入/输出设计本题通过键盘进行输入,通过屏幕进行输出由于题目的输入要求是:第一行输入一个整数n(2<=n<=10),接下来的n行,每行输入n-1个整数,表示i与除了自己之外的所有点之间的距离,按点的编号从小到大顺序输入可以设计两个for循环来实现数据的输入,外层for循环实现一行一行地输入,内层for循环实现某一行中数据的输入53 1 5 83 6 7 91 6 4 25 7 4 38 9 2 3(4)符号名说明N:节点数,即城市的数目matr[20][20]:存邻接矩阵d[20][40000]={0}:存动态填表数据min:花费的最小值,即答案jlist[20]:存放j的二进制数组V[20]:标记节点是不是被访问过tmpres[20]:存放结果的数组(5)算法描述假设从顶点i出发,令d(i,V’)表示从顶点i出发经过V’中各个顶点一次且仅一次,最后回到出发点i的最短路径的长度,开始时,V’=V-{i},于是,旅行商问题的动态规划函数为:d(i,V’) = min{c ik + d(k,V’-{k})} (k∈V’) 1)d(k,{}) = c ki (k ≠ i) 2)简单来说,就是用递归表达:从出发点0到1号点,假设1是第一个,则剩下的路程就是从1经过剩下的点最后回到0点的最短路径. 所以当V’为空的时候, d(k,{}) = c ki (k ≠ i), 找的是最后一个点到0点的距离.递归求解1之后,再继续求V’之中剩下的点,最后找出min.如果按照这个思想直接做,对于每一个i都要递归剩下的V中所有的点,所以这样的时间复杂度就近似于N!,其中有很多重复的工作.可以从小的集合到大的集合算,并存入一个二维数组,这样当加入一个节点时,就可以用到之前的结果,如四个点的情况:邻接矩阵:node 0 1 2 30 5 3 21 5 7 92 3 7 123 2 9 12动态填表:表中元素代表第i个节点经过V集合中的点最后到0点的最短值.如果有多个值,取其中最小的一个.i\Vj 0 1 2 3 1,2(取min) 1,3(取min) 2,3(取min) 1,2,3(取min)0 c[0][i]+d[i][v’]=211 5 10 11 {c[1][2]+d[2][{3}]=21,c[1][3]+d[3][{2}]=24}2 3 12 14 {c[2][1]+d[1][{3}]=18,c[2][3]+d[3][{1}]=26}3 2 14 15 {c[3][1]+d[1][{2}]=19,c[3][2]+d[2][{1}]=24}这样一共循环(2^(N-1)-1)*(N-1)次,就把时间复杂度缩小到O(N*2N )的级别.核心伪代码如下:{for (i =1;i<n;i++) //初始化第0列d[i][0]=c[i][0];for( j=1;j<2^(N-1)-1;j++)for(i=1 ; i<n ;i++){if(子集Vj中不包含i){对Vj中的每个元素k,计算d[i][Vj] = min{c[i][k] + d[k][{Vj-k}] | 每一个k∈Vj};}}对V[2^(n-1)-1]中的每个元素k,计算:d[0][2^(n-1)-1] = min{c[0][k] + d[k][2^(n-1)-2]};输出最短路径:d[0][2^(n-1)-1];}2、贪心法(1)数据结构数组a[][]存放邻接矩阵,取到邻接点的最小权值然后直接跳到所对应节点。
(2)函数组成无(3)输入/输出设计5 43 1 5 8 3 6 73 6 7 9 5 2 31 6 42 6 4 25 7 4 3 3 7 58 9 2 3(4)符号名说明a[][]:存放邻接矩阵vis[]:标记已经过的节点sum :最小花费(5)算法描述贪心法的方法是在每个节点中找到与其他节点的最小距离,并且有顺序下面把城市和到目的地之间的关系用一个矩阵表示。
1 2 3 4 51 max 3 4 6 22 3 max 4 6 53 3 2 max4 44 2 3 1 max 65 46 5 2 max由于使用贪婪算法,每次都选择当前最短的目的地,所以应该会这么走:1-5-4-3-2-1 花费值是2+2+1+2+3=10。
可以明显看出选择一个城市出发,时间复杂性为O(n^2)。
3、分支限界法(1)数据结构利用最小堆取得最小数。
用到了堆的数据结构。
结构体数据结构。
(2)函数组成InitMiniHeap():初始化堆put():入堆RemoveMiniHeap():出堆Traveler():解决TSP问题(3)输入/输出设计5 43 1 5 8 3 6 73 6 7 9 5 2 31 6 42 6 4 25 7 4 3 3 7 58 9 2 3(4)符号名说明MAX_CITY_NUMBER :城市最大数目MAX_COST :两个城市之间费用的最大值City_Graph[MAX_CITY_NUMBER][MAX_CITY_NUMBER]:表示城市间边权重的数组City_Size:表示实际输入的城市数目Best_Cost:最小费用Best_Cost_Path[MAX_CITY_NUMBER]:最小费用时的路径struct MiniHeap:堆(5)算法描述①算法开始时创建一个最小堆,用于表示活结点优先队列②堆中每个结点的子树费用的下界lcost值是优先队列的优先级。
③接着算法计算出图中每个顶点的最小费用出边并用minout记录。
④如果所给的有向图中某个顶点没有出边,则该图不可能有回路,算法即告结束。
⑤如果每个顶点都有出边,则根据计算出的minout作算法初始化。
算法的while循环体完成对排列树内部结点的扩展。
对于当前扩展结点,算法分2种情况进行处理:①首先考虑s=n-2的情形,此时当前扩展结点是排列树中某个叶结点的父结点。
如果该叶结点相应一条可行回路且费用小于当前最小费用,则将该叶结点插入到优先队列中,否则舍去该叶结点。
②当s<n-2时,算法依次产生当前扩展结点的所有儿子结点。
由于当前扩展结点所相应的路径是x[0:s],其可行儿子结点是从剩余顶点x[s+1:n-1]中选取的顶点x[i],且(x[s],x[i])是所给有向图G中的一条边。
对于当前扩展结点的每一个可行儿子结点,计算出其前缀(x[0:s],x[i])的费用cc和相应的下界lcost。
当lcost<bestc时,将这个可行儿子结点插入到活结点优先队列中。
算法中while循环的终止条件是排列树的一个叶结点成为当前扩展结点。
当s=n-1时,已找到的回路前缀是x[0:n-1],它已包含图G的所有n个顶点。
因此,当s=n-1时,相应的扩展结点表示一个叶结点。
此时该叶结点所相应的回路的费用等于cc和lcost的值。
剩余的活结点的lcost值不小于已找到的回路的费用。
它们都不可能导致费用更小的回路。
因此已找到叶结点所相应的回路是一个最小费用旅行售货员回路,算法可结束。
算法结束时返回找到的最小费用,相应的最优解由数组v给出。
三、源程序及注释:1、动态规划法#include <iostream>#include <math.h>#include <stdio.h>#include <ctime>#include <algorithm>#define max 0x7ffffffusing namespace std;int N;//节点数int matr[20][20];//存邻接矩阵int d[20][40000]={0};//存动态填表数据int getmin(int *sum)//返回该数组中最小非零值{int i = 0;int min = -1,k;for(;i<N;i++){if((min < 0 && sum[i]>0) || (sum[i]>0 && sum[i]<min)){min = sum[i];k = i;}}return min;}void getJ(int jlist[], int c, int n)//根据2进制j和j中1的个数找下一个j{int i = n-1,j;int tmp = 1 , result = 0;while(!jlist[i])i--;//最高位的1的位臵j = i-1;while(jlist[j])j--;//找最高位1之下的最高0if(!jlist[n-1])//若最高位不为1{//将为1的最高一位向上移一位jlist[i]=0;jlist[i+1]=1;}else if(n-1-j==c)//若最高位为1,且j为当前1的个数所能组成的最大的j{for(i=0;i<n;i++)jlist[i]=0;for(i=0;i<c+1;i++)jlist[i]=1;}else//最高位为1,且在当前数目1下还能变更大{//将和最高位之间有隔0的最高的1上移一位,并把这个1之后的所有1 拉到紧邻它后面int k;k=n-1-j;//最高位一共有多少相邻的1while(!jlist[j])j--;//找和最高位隔着0的最高1for(i=0;j+i<n;i++)jlist[j+i]=0;for(i=0;i<=k;i++)jlist[j+i+1]=1;}}int getnextj( int j ){int nextj = 0;//下一个jint c=0;//计数j的二进制有几个1int jlist[20]={0};//存放j的二进制数组int i=0;int tmp = 1;while(j){if(j%2){c++;jlist[i++]=1;}else{jlist[i++]=0;}j/=2;}getJ(jlist,c,N-1);//处理jlistfor(i=0;i<N-1;i++)//将jlist中的2进制转换成int返回{if(jlist[i])nextj += tmp;tmp *= 2;}return nextj;}int main(){int i,j;int min;scanf("%d",&N);//读入点数for(i = 0; i < N; i++)//读入邻接矩阵{for(j = 0;j < N; j++){if(i!=j)scanf("%d",&matr[i][j]);}matr[i][i] = max ;}int V[20]={0};for(i = 0; i < N; i++)V[i]=1;//将所有点设臵为未经过点V[0]=0;//以第0个点为出发点//2^n次方解法for (i =1;i<N;i++) //初始化第0列d[i][0]=matr[i][0];for(j=1;j<pow(2,N-1)-1;j=getnextj(j))for(i=1; i<N ;i++){if(!(j & ( 1<<(i-1) )))//如果集合j中不包含i{//对V[j]中的每个元素k,计算d[i][j] = min{matr[i][k] + d[k][{j-k}]};int jlist[20]={0}; //存放j的二进制数组int tmpres[20]={0};int c=0,k=j,l;while(k){if(k%2){jlist[c++]=1;}else{jlist[c++]=0;}k/=2;}c=0;for(l=0;l<N;l++){if(jlist[l]){tmpres[c++]=matr[i][l+1] + d[l+1][j-(1<<l)];}}d[i][j] = getmin(tmpres);}}int tmpres[20]={0};j = pow(2,N-1)-1;for(i=1;i<N;i++){tmpres[i]=matr[0][i] + d[i][ j - (1<<(i-1) )];}min = getmin(tmpres);//对V[2^(n-1)-1]中的每个元素k,计算:// d[0][2^(n-1)-1] = min{matr[0][k] + d[k][2^(n-1)-2]};//输出最短路径:d[0][2^(n-1)-1];printf("%d\n",min);getchar();return 0;}2、贪心法#include <iostream>#include <stdio.h>#include <cmath>#include <string.h>#define max 10000000using namespace std ;int a[20][20] ;int vis[20] ;int main(){int n , j , i ;memset(vis , 0 , sizeof(vis)) ;scanf("%d" , &n) ;for(i = 0 ; i < n ; i ++){for(j = 0 ; j < n ; j ++){if(i != j){scanf("%d" , &a[i][j]) ;}}a[i][i] = max ;}int sum = 0 , count = 0;int cur , k = 0 ;i = 0 ;vis[0] = 1 ;while(count!=n){cur = max ;for(j = 0 ; j < n ; j ++)//找到邻接点最小的权值,并返回与其相连的节点 {if(a[i][j] < cur && vis[j] == 0 && i != j){cur = a[i][j] ;k = j ;}}sum += a[i][k] ;i = k ;vis[k] = 1 ;count ++ ;if(count == n)//当经过四个节点时,返回第一个节点时将第一个节点标记为0 {vis[0] = 0 ;}}printf("%d\n" , sum) ;return 0 ;}3、分支限界法#include <stdio.h>#include <iostream>using namespace std;#define MAX_CITY_NUMBER 10 //城市最大数目#define MAX_COST 10000000 //两个城市之间费用的最大值int City_Graph[MAX_CITY_NUMBER][MAX_CITY_NUMBER];//表示城市间边权重的数组int City_Size; //表示实际输入的城市数目int Best_Cost; //最小费用int Best_Cost_Path[MAX_CITY_NUMBER];//最小费用时的路径typedef struct Node{int lcost; //优先级int cc; //当前费用int rcost; //剩余所有结点的最小出边费用的和int s; //当前结点的深度,也就是它在解数组中的索引位置int x[MAX_CITY_NUMBER]; //当前结点对应的路径struct Node* pNext; //指向下一个结点}Node;//---------------------定义堆和相关对操作--------------------------------typedef struct MiniHeap{Node* pHead; //堆的头}MiniHeap;//初始化void InitMiniHeap(MiniHeap* pMiniHeap){pMiniHeap->pHead = new Node;pMiniHeap->pHead->pNext = NULL;}//入堆void put(MiniHeap* pMiniHeap,Node node){Node* next;Node* pre;Node* pinnode = new Node; //将传进来的结点信息copy一份保存//这样在函数外部对node的修改就不会影响到堆了 pinnode->cc = ;pinnode->lcost = node.lcost;pinnode->pNext = node.pNext;pinnode->rcost = node.rcost;pinnode->s = node.s;pinnode->pNext = NULL;for(int k=0;k<City_Size;k++){pinnode->x[k] = node.x[k];}pre = pMiniHeap->pHead;next = pMiniHeap->pHead->pNext;if(next == NULL){pMiniHeap->pHead->pNext = pinnode;}else{while(next != NULL){if((next->lcost) > (pinnode->lcost)){ //发现一个优先级大的,则置于其前面pinnode->pNext = pre->pNext;pre->pNext = pinnode;break; //跳出}pre = next;next = next->pNext;}pre->pNext = pinnode; //放在末尾}}//出堆Node* RemoveMiniHeap(MiniHeap* pMiniHeap){Node* pnode = NULL;if(pMiniHeap->pHead->pNext != NULL){pnode = pMiniHeap->pHead->pNext;pMiniHeap->pHead->pNext = pMiniHeap->pHead->pNext->pNext;}return pnode;}//---------------------分支限界法找最优解-------------------------------- void Traveler(){int i,j;int temp_x[MAX_CITY_NUMBER];Node* pNode = NULL;int miniSum; //所有结点最小出边的费用和int miniOut[MAX_CITY_NUMBER];//保存每个结点的最小出边的索引MiniHeap* heap = new MiniHeap; //分配堆InitMiniHeap(heap); //初始化堆miniSum = 0;for (i=0;i<City_Size;i++){miniOut[i] = MAX_COST; //初始化时每一个结点都不可达for(j=0;j<City_Size;j++){if (City_Graph[i][j]>0 && City_Graph[i][j]<miniOut[i]){//从i到j可达,且更小miniOut[i] = City_Graph[i][j];}}if (miniOut[i] == MAX_COST){// i 城市没有出边Best_Cost = -1;return ;}miniSum += miniOut[i];}for(i=0;i<City_Size;i++){ //初始化的最优路径就是把所有结点依次走一遍Best_Cost_Path[i] = i;}Best_Cost = MAX_COST; //初始化的最优费用是一个很大的数pNode = new Node; //初始化第一个结点并入堆pNode->lcost = 0; //当前结点的优先权为0 也就是最优pNode->cc = 0; //当前费用为0(还没有开始旅行)pNode->rcost = miniSum; //剩余所有结点的最小出边费用和就是初始化的miniSumpNode->s = 0; //层次为0pNode->pNext = NULL;for(int k=0;k<City_Size;k++){pNode->x[k] = Best_Cost_Path[k]; //第一个结点所保存的路径也就是初始化的路径}put(heap,*pNode); //入堆while(pNode != NULL && (pNode->s) < City_Size-1){//堆不空不是叶子for(int k=0;k<City_Size;k++){Best_Cost_Path[k] = pNode->x[k] ; //将最优路径置换为当前结点本身所保存的 }// pNode 结点保存的路径中的含有这条路径上所有结点的索引// x路径中保存的这一层结点的编号就是x[City_Size-2]// 下一层结点的编号就是x[City_Size-1]if ((pNode->s) == City_Size-2){ //是叶子的父亲int edge1 = City_Graph[(pNode->x)[City_Size-2]][(pNode->x)[City_Size-1]];int edge2 = City_Graph[(pNode->x)[City_Size-1]][(pNode->x)[0]];if(edge1 >= 0 && edge2 >= 0 && (pNode->cc+edge1+edge2) < Best_Cost){//edge1 -1 表示不可达//叶子可达起点费用更低Best_Cost = pNode->cc + edge1+edge2;pNode->cc = Best_Cost;pNode->lcost = Best_Cost; //优先权为Best_CostpNode->s++; //到达叶子层 }}else{ //内部结点for (i=pNode->s;i<City_Size;i++){ //从当前层到叶子层if(City_Graph[pNode->x[pNode->s]][pNode->x[i]] >= 0){ //可达//pNode的层数就是它在最优路径中的位置int temp_cc = pNode->cc+City_Graph[pNode->x[pNode->s]][pNode->x[i]];int temp_rcost = pNode->rcost-miniOut[pNode->x[pNode->s]];//下一个结点的剩余最小出边费用和 //等于当前结点的rcost减去当前这个结点的最小出边费用if (temp_cc+temp_rcost<Best_Cost){ //下一个结点的最小出边费用和小于当前的最优解,说明可能存在更优解for (j=0;j<City_Size;j++){ //完全copy路径,以便下面修改 temp_x[j]=Best_Cost_Path[j];}temp_x[pNode->x[pNode->s+1]] = Best_Cost_Path[i];//将当前结点的编号放入路径的深度为s+1的地方temp_x[i] = Best_Cost_Path[pNode->s+1]; //??????????????//将原路//径中的深度为s+1的结点编号放入当前路径的//相当于将原路径中的的深度为i的结点与深度W为s+1的结点交换Node* pNextNode = new Node;pNextNode->cc = temp_cc;pNextNode->lcost = temp_cc+temp_rcost;pNextNode->rcost = temp_rcost;pNextNode->s = pNode->s+1;pNextNode->pNext = NULL;for(int k=0;k<City_Size;k++){pNextNode->x[k] = temp_x[k];}put(heap,*pNextNode);delete pNextNode;}}}}pNode = RemoveMiniHeap(heap);}}int main(){int i,j;scanf("%d",&City_Size);for(i=0;i<City_Size;i++){for(j=0;j<City_Size;j++){if(i!=j){scanf("%d",&City_Graph[i][j]);}City_Graph[i][i] = MAX_COST ;}}Traveler();printf("%d\n",Best_Cost);return 0;}四、运行输出结果:(贴出程序运行完成时的屏幕截图或者输出文件的内容)1、动态规划法2、贪心法3、分支限界法五、调试和运行程序过程中产生的问题及采取的措施:1、动态规划问题:对集合的控制有错误,不能正确的表示集合。