TSP问题分析动态规划分支界限法蛮力法.docx
《TSP问题分析动态规划分支界限法蛮力法.docx》由会员分享,可在线阅读,更多相关《TSP问题分析动态规划分支界限法蛮力法.docx(23页珍藏版)》请在冰豆网上搜索。
TSP问题分析动态规划分支界限法蛮力法
算法综合实验报告
学号:
姓名:
李宏强
一、实验内容:
分别用动态规划、贪心及分支限界法实现对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不在集合中。
利用邻接矩阵表示任意两点之间的距离
例如:
mp[i][j]表示点i,j两点之间的距离。
(2)函数组成
输入函数in()
利用动态规划法算法实现的求解函数solve()
主函数main()
(3)输入/输出设计
本程序可以通过键盘进行输入、屏幕进行输出。
(根据实际程序情况,还可以选择随机产生输入数据、将输出数据输出到文件等其它方式)
这里采用随机产生输入数据,将数据输出在屏幕上的方式。
(4)符号名说明
n表示顶点个数。
mp[i][j]表示顶点i和顶点j之间的距离。
dp[i][j]表示顶点i经过集合S(用二进制表示的数为j)后回到起始点的最短路径和。
(5)算法描述
某一个点i不经过任意点回到起始点的最短路径和为mp[i][0](默认初始点为0)
dp[i][0]=mp[i][0];(1<=i点i经过集合S(二进制表示的数为j)的最短路径和为从点i经过集合S中的某一点k后再从该点出发,经过集合S-{k}的最小值。
dp[i][j]=min{mp[i][k]+dp[k][j-(1<<(k-1))};
2、贪心法
(1)数据结构
利用邻接矩阵表示任意两点之间的距离
例如:
mp[i][j]表示点i,j两点之间的距离。
(2)函数组成
输入函数in()
利用贪心法每次取最近距离的函数dfs(u,k,l),u表示当前在顶点u,k表示已经走过了k个点,l表示所经过的路径和。
主函数main()
(3)输入/输出设计
本程序可以通过键盘进行输入、屏幕进行输出。
(根据实际程序情况,还可以选择随机产生输入数据、将输出数据输出到文件等其它方式)
这里采用随机产生输入数据,将数据输出在屏幕上的方式。
(4)符号名说明
n表示顶点个数。
mp[i][j]表示顶点i和顶点j之间的距离。
inq[i]表示顶点i已经走过了。
(5)算法描述
如果当前在顶点u,则取与顶点u距离最近的点p。
dfs(u,k,l)=dfs(p,k+1,l+mp[u][p])
3、分支限界法
(1)数据结构
利用邻接矩阵表示任意两点之间的距离
例如:
mp[i][j]表示点i,j两点之间的距离。
结构体
structnode
{
intvisp[22];//标记哪些点走了
intst;//起点
inted;//终点
intk;//走过的点数
intsumv;//经过路径的距离
intlb;//目标函数的值
booloperator<(constnode&p)const
{
returnlb>;
}
};
优先级队列
priority_queueq;
(2)函数组成
in()
输入函数。
dfs(intu,intk,intl)
利用贪心法每次取最近距离的函数,u表示当前在顶点u,k表示已经走过了k个点,l表示所经过的路径和。
将贪心法的解作为上界的初始值。
get_lb(nodep)
求对应节点p的目标函数。
main()
主函数。
get_up()
求分支限界法的上界。
get_low()
求分支界限法的下界。
solve()
利用分支限界法求解函数
(3)输入/输出设计
本程序可以通过键盘进行输入、屏幕进行输出。
(根据实际程序情况,还可以选择随机产生输入数据、将输出数据输出到文件等其它方式)
这里采用随机产生输入数据,将数据输出在屏幕上的方式。
(4)符号名说明
n表示顶点个数。
mp[i][j]表示顶点i和顶点j之间的距离。
inq[i]表示顶点i已经走过了。
(5)算法描述
首先通过贪心法求解的值作为上界,把每个点最近的两条边之和的1/2作为下界。
分支限界法通过设定目标函数,每次从优先级队列中取目标函数的值最小的节点。
先判断是否已经经过了n-1个点,如果已经经过了n-1个点,那么可直接求出最短路径和,并与在队列里的其他节点的目标函数值比较,如果该路径和比其他所有在队列里的节点的目标函数值都小,那么改路径和就是问题的解。
否则,继续计算优先级队列里面的其他节点。
3、源程序及注释:
这里默认顶点的个数小于22。
1、动态规划法
#include
#include
#defineINF9999
usingnamespacestd;
intmp[22][22];
intn;
voidin()
{
scanf("%d",&n);
for(inti=0;i{
for(intj=0;j{
if(i==j)
{
mp[i][j]=INF;
continue;
}
scanf("%d",&mp[i][j]);
}
}
}
intdp[22][1<<22];
intsolve()
{
ints=(1<<(n-1));
dp[0][0]=0;
for(inti=1;i{
dp[i][0]=mp[i][0];
}
dp[0][(s-1)]=INF;
for(intj=1;j<(s-1);j++)//总共有n-1个点,但不能全部取
{
for(inti=1;i{
if((j&(1<<(i-1)))==0)//i不在集合中
{
intm=INF;
for(intk=1;k{
if((j&(1<<(k-1)))>0)//k在集合中
{
inttmp=dp[k][(j-(1<<(k-1)))]+mp[i][k];
if(m>tmp)
m=tmp;
}
}
dp[i][j]=m;
}
}
}
dp[0][s-1]=INF;
for(inti=1;i{
dp[0][s-1]=min(dp[0][s-1],mp[0][i]+dp[i][(s-1)-(1<<(i-1))]);
}
returndp[0][s-1];
}
intmain()
{
in();
printf("%d\n",solve());
return0;
}
2、贪心法
#include
#include
#defineINF9999
usingnamespacestd;
intn;
intmp[22][22];
intinq[22];
voidin()
{
scanf("%d",&n);
for(inti=1;i<=n;i++)
{
for(intj=1;j<=n;j++)
{
if(i==j)
{
mp[i][j]=INF;
continue;
}
scanf("%d",&mp[i][j]);
}
}
}
intdfs(intu,intk,intl)
{
if(k==n)returnl+mp[u][1];
intminlen=INF,p;
for(inti=1;i<=n;i++)
{
if(inq[i]==0&&minlen>mp[u][i])/*取与所有点的连边中最小的边*/
{
minlen=mp[u][i];
p=i;
}
}
inq[p]=1;
returndfs(p,k+1,l+minlen);
}
intmain()
{
in();
inq[1]=1;
printf("%d\n",dfs(1,1,0));
return0;
}
3、分支限界法
//分支限界法
#include
#include
#include
#include
#defineINF100000
usingnamespacestd;
/*n*n的一个矩阵*/
intn;
intmp[22][22];//最少3个点,最多15个点
/*输入距离矩阵*/
voidin()
{
scanf("%d",&n);
for(inti=1;i<=n;i++)
{
for(intj=1;j<=n;j++)
{
if(i==j)
{
mp[i][j]=INF;
continue;
}
scanf("%d",&mp[i][j]);
}
}
}
structnode
{
intvisp[22];//标记哪些点走了
intst;//起点
intst_p;//起点的邻接点
inted;//终点
inted_p;//终点的邻接点
intk;//走过的点数
intsumv;//经过路径的距离
intlb;//目标函数的值
booloperator<(constnode&p)const
{
returnlb>;
}
};
priority_queueq;
intlow,up;
intinq[22];
//确定上界
intdfs(intu,intk,intl)
{
if(k==n)returnl+mp[u][1];
intminlen=INF,p;
for(inti=1;i<=n;i++)
{
if(inq[i]==0&&minlen>mp[u][i])/*取与所有点的连边中最小的边*/
{
minlen=mp[u][i];
p=i;
}
}
inq[p]=1;
returndfs(p,k+1,l+minlen);
}
intget_lb(nodep)
{
intret=*2;//路径上的点的距离
intmin1=INF,min2=INF;//起点和终点连出来的边
for(inti=1;i<=n;i++)
{
if[i]==0&&min1>mp[i][])
{
min1=mp[i][];
}
}
ret+=min1;
for(inti=1;i<=n;i++)
{
if[i]==0&&min2>mp[][i])
{
min2=mp[][i];
}
}
ret+=min2;
for(inti=1;i<=n;i++)
{
if[i]==0)
{
min1=min2=INF;
for(intj=1;j<=n;j++)
{
if(min1>mp[i][j])
min1=mp[i][j];
}
for(intj=1;j<=n;j++)
{
if(min2>mp[j][i])
min2=mp[j][i];
}
ret+=min1+min2;
}
}
returnret%2==0?
(ret/2):
(ret/2+1);
}
voidget_up()
{
inq[1]=1;
up=dfs(1,1,0);
}
voidget_low()
{
low=0;
for(inti=1;i<=n;i++)
{
/*通过排序求两个最小值*/
intmin1=INF,min2=INF;
inttmpA[22];
for(intj=1;j<=n;j++)
{
tmpA[j]=mp[i][j];
}
sort(tmpA+1,tmpA+1+n);//对临时的数组进行排序
low+=tmpA[1];
}
}
intsolve()
{
/*贪心法确定上界*/
get_up();
/*取每行最小的边之和作为下界*/
get_low();
/*设置初始点,默认从1开始*/
nodestar;
=1;
=1;
=1;
for(inti=1;i<=n;i++)[i]=0;
[1]=1;
=0;
=low;
/*ret为问题的解*/
intret=INF;
(star);
while(!
())
{
nodetmp=();
();
if==n-1)
{
/*找最后一个没有走的点*/
intp;
for(inti=1;i<=n;i++)
{
if[i]==0)
{
p=i;
break;
}
}
intans=+mp[p][]+mp[][p];
nodejudge=();
/*如果当前的路径和比所有的目标函数值都小则跳出*/
if(ans<=
{
ret=min(ans,ret);
break;
}
/*否则继续求其他可能的路径和,并更新上界*/
else
{
up=min(up,ans);
ret=min(ret,ans);
continue;
}
}
/*当前点可以向下扩展的点入优先级队列*/
nodenext;
for(inti=1;i<=n;i++)
{
if[i]==0)
{
=;
/*更新路径和*/
=+mp[][i];
/*更新最后一个点*/
=i;
/*更新顶点数*/
=+1;
/*更新经过的顶点*/
for(intj=1;j<=n;j++)[j]=[j];
[i]=1;
/*求目标函数*/
=get_lb(next);
/*如果大于上界就不加入队列*/
if>up)continue;
(next);
}
}
}
returnret;
}
intmain()
{
in();
printf("%d\n",solve());
return0;
}
四、运行输出结果:
(贴出程序运行完成时的屏幕截图或者输出文件的内容)
这里采用相同的两组数据进行测试。
1、动态规划法
样例1:
样例2:
2、贪心法
样例1:
样例2:
贪心法只能求局部最优解,局部最优解不一定是全局最优解。
3、分支限界法
样例1:
样例2:
五、调试和运行程序过程中产生的问题及采取的措施:
1、动态规划法中输出错误,通过测试数据进行反复验证,并分块输出局部结果,从而发现问题并解决。
2、贪心法对第二组样例的解不正确,因为局部最优解不一定是全局最优解。
3、分支限界法对于测试样例输出随机值,solve()函数在每次返回的时候结果不一致。
通过反复观察代码,发现循环跳出的条件有问题,应该是当前的解小于或等于队列中的目标函数值才跳出。
六、对算法的程序的讨论、分析,改进设想,其它经验教训:
1、动态规划法算法时间复杂度为O(
),在oj上的测试时间如下:
(oj上的测试样例n最大值为15)
2、贪心法只能求局部最优解,不一定是全局最优解。
所以第二组样例的解不正确。
3、分支限界法的复杂度是根据数据的不同而不同,搜索的节点越少,复杂度越低,跟目标函数的选择有很大关系。
目标函数值的计算也会需要一定时间,比如此文章中的目标函数值求解的复杂度是O(
)。
在oj上的测试时间如下:
在设置节点的时候,用数组标记经过的顶点,visp[i]=1,则说明i点已经经过了,由于是静态分配空间,所以每次创建新的节点,都会增加空间。
所以可以考虑动态分配空间,把不用的节点的空间释放掉。
4、对于顶点少的TSP问题,还可以采用蛮力法。
时间复杂度为O(n!
),但实现起来比较简单,这里使用了stl中生成全全排列的函数next_permutation()。
在oj上的测试时间如下:
(测试时限为5000ms)
下面给出蛮力法的代码:
#include
#include
#include
usingnamespacestd;
intmp[22][22],n;
#defineINF99999
voidin()
{
scanf("%d",&n);
for(inti=1;i<=n;i++)
{
for(intj=1;j<=n;j++)
{
if(i==j)
{
mp[i][j]=INF;
continue;
}
scanf("%d",&mp[i][j]);
}
}
}
inta[22],b[22],c[22];
intsolve()
{
intret=99999;
for(inti=1;i<=n;i++)
{
a[i]=i;
b[i]=i;
c[i]=i;
}
do
{
inttmp[22];
for(inti=1;i<=n;i++)
{
tmp[i]=b[a[i]];
}
intsum=0;
for(inti=2;i<=n;i++)
{
sum+=mp[tmp[i-1]][tmp[i]];
}
sum+=mp[tmp[n]][tmp[1]];
if(sum{
ret=sum;
for(inti=1;i<=n;i++)c[i]=tmp[i];
}
}while(next_permutation(a+1,a+1+n));
returnret;
}
intmain()
{
in();
printf("%d\n",solve());
return0;
}