哈密顿路径状压DP算法C++实现

一、问题        

        求解经过所有点的最短路径(哈密顿路径)或者求经过所有点最短路径后再回到开始的地方(哈密顿回路)例如旅行商问题(TSP)。

       首先我们假设 每个点与点之间的权值(路的长度)已经求出 记为 w[i][j].
(对于这个部分有些时候需要进行处理,自己去把权值求出来)

我们把每一个点的状态压缩到应该二进制的位 1代表走这个点,0代表没有走这个点。

对于某一个状态,我们可以得到它可以由哪一个状态转移过来。
(设有7个点)例如: 当前我们得到了一个状态 1000001 表示第1个点和第7个点走过,
而它的上一个状态可以为0000001,1000000推出。
0000001 -> 1000001 说明此时是走了 7-> 1 这条边。

我们设dp[i][j] 表示当前第i个状态且正在访问第j个节点的最小花费。
那么上述结果为dp[1000001][1] = min(dp[0000001][7]+w[7][1],dp[1000001][1]);
可以得到主要步骤:

1 : 枚举一个状态从 1-(1<<n)。
2 : 找到上一个走的点。
3 : 枚举上一个状态
4 : 更新当前状态的最小值
5: 最后输出一个dp[(1<<n)-1][…]或者枚举这个状态的所有点输出最小值或者再加上w[…][1]的权值计算哈密顿回路 (看具体情况)。

1):Dirac定理(充分条件)

  设一个无向图中有N个顶点,若所有顶点的度数大于等于N/2,则哈密顿回路一定存在.(N/2指的是⌈N/2⌉,向上取整)

2):基本的必要条件

  设图G=<V, E>是哈密顿图,则对于v的任意一个非空子集S,若以|S|表示S中元素的数目,G-S表示G中删除了S中的点以及这些点所关联的边后得到的子图,则W(G-S)<=|S|成立.其中W(G-S)是G-S中联通分支数.

3):竞赛图(哈密顿通路)

  N(N>=2)阶竞赛图一点存在哈密顿通路.

算法:

在Dirac定理的前提下构造哈密顿回路

过程:

 1:任意找两个相邻的节点S和T,在其基础上扩展出一条尽量长的没有重复结点的路径.即如果S与结点v相邻,而且v不在路径S -> T上,则可以把该路径变成v -> S -> T,然后v成为新的S.从S和T分别向两头扩展,直到无法继续扩展为止,即所有与S或T相邻的节点都在路径S -> T上.

  2:若S与T相邻,则路径S -> T形成了一个回路.

  3:若S与T不相邻,可以构造出来一个回路.设路径S -> T上有k+2个节点,依次为S, v1, v2, ..., vk, T.可以证明存在节点vi(i属于[1, k]),满足vi与T相邻,且vi+1与S相邻.找到这个节点vi,把原路径变成S -> vi -> T -> vi+1 ,即形成了一个回路.

  4:到此为止,已经构造出来了一个没有重复节点的的回路,如果其长度为N,则哈密顿回路就找到了.如果回路的长度小于N,由于整个图是连通的,所以在该回路上,一定存在一点与回路之外的点相邻.那么从该点处把回路断开,就变回了一条路径,同时还可以将与之相邻的点加入路径.再按照步骤1的方法尽量扩展路径,则一定有新的节点被加进来.接着回到路径2.

证明:

  跟据鸽巢定理,既然与S和T相邻的点都在路径上,它们分布的范围只有v1,v2---,vk这k个点,k<=N-2,跟据哈密顿回路的第一个判断条件,d(S)+d(T)>=N,那么v1,v2,---vk这k个点中一定有至少2个点同时与S和T相连,根据鸽巢定理,肯定存在一个与S相邻的点vi和一个与T相邻的点vj,满足j=i+1

伪代码:

  设s为哈密顿回路的起始点,t为哈密顿回路中终点s之前的点.ans[]为最终的哈密顿回路.倒置的意思指的是将数组对应的区间中数字的排列顺序反向.

  1:初始化,令s = 1,t为s的任意一个邻接点.

  2:如果ans[]中元素的个数小于n,则从t开始向外扩展,如果有可扩展点v,放入ans[]的尾部,并且t=v,并继续扩展,如无法扩展进入步骤3.

  3:将当前得到的ans[]倒置,s和t互换,从t开始向外扩展,如果有可扩展点v,放入ans[]尾部,并且t=v,并继续扩展.如无法扩展进入步骤4.

  4:如果当前s和t相邻,进入步骤5.否则,遍历ans[],寻找点ans[i],使得ans[i]与t相连并且ans[i +1]与s相连,将从ans[i + 1]到t部分的ans[]倒置,t=ans[i +1],进如步骤5.

  5:如果当前ans[]中元素的个数等于n,算法结束,ans[]中保存了哈密顿回路(可看情况是否加入点s).否则,如果s与t连通,但是ans[]中的元素的个数小于n,则遍历ans[],寻找点ans[i],使得ans[i]与ans[]外的一点(j)相连,则令s=ans[i - 1],t = j,将ans[]中s到ans[i - 1]部分的ans[]倒置,将ans[]中的ans[i]到t的部分倒置,将点j加入到ans[]的尾部,转步骤2.

时间复杂度:

  如果说每次到步骤5算一轮的话,那么由于每一轮当中至少有一个节点被加入到路径S -> T中,所以总的轮数肯定不超过n轮,所以时间复杂度为O(n^2).空间上由于边数非常多,所以采用邻接矩阵来存储比较适合。

二、代码实现

 
const int maxN = 100;
 
inline void reverse(int arv[maxN + 7], int s, int t){//将数组anv从下标s到t的部分的顺序反向
    int temp;
    while(s  < t){
        temp = arv[s];
        arv[s] = arv[t];
        arv[t] = temp;
        s++;
        t--;
    }
}
 
void Hamilton(int ans[maxN + 7], bool map[maxN + 7][maxN + 7], int n){
    int s = 1, t;//初始化取s为1号点
    int ansi = 2;
    int i, j;
    int w;
    int temp;
    bool visit[maxN + 7] = {false};
    for(i = 1; i <= n; i++) if(map[s][i]) break;
    t = i;//取任意邻接与s的点为t
    visit[s] = visit[t] = true;
    ans[0] = s;
    ans[1] = t;
    while(true){
        while(true){//从t向外扩展
            for(i = 1; i <= n; i++){
                if(map[t][i] && !visit[i]){
                    ans[ansi++] = i;
                    visit[i] = true;
                    t = i;
                    break;
                }
            }
            if(i > n) break;
        }
        w = ansi - 1;//将当前得到的序列倒置,s和t互换,从t继续扩展,相当于在原来的序列上从s向外扩展
        i = 0;
        reverse(ans, i, w);
        temp = s;
        s = t;
        t = temp;
        while(true){//从新的t继续向外扩展,相当于在原来的序列上从s向外扩展
            for(i = 1; i <= n; i++){
                if(map[t][i] && !visit[i]){
                    ans[ansi++] = i;
                    visit[i] = true;
                    t = i;
                    break;
                }
            }
            if(i > n) break;    
        }
        if(!map[s][t]){//如果s和t不相邻,进行调整
            for(i = 1; i < ansi - 2; i++)//取序列中的一点i,使得ans[i]与t相连,并且ans[i+1]与s相连
                if(map[ans[i]][t] && map[s][ans[i + 1]])break;
            w = ansi - 1;
            i++;
            t = ans[i];
            reverse(ans, i, w);//将从ans[i +1]到t部分的ans[]倒置
        }//此时s和t相连
        if(ansi == n) return;//如果当前序列包含n个元素,算法结束
        for(j = 1; j <= n; j++){//当前序列中元素的个数小于n,寻找点ans[i],使得ans[i]与ans[]外的一个点相连
            if(visit[j]) continue;
            for(i = 1; i < ansi - 2; i++)if(map[ans[i]][j])break;
                if(map[ans[i]][j]) break;
        }
        s = ans[i - 1];
        t = j;//将新找到的点j赋给t
        reverse(ans, 0, i - 1);//将ans[]中s到ans[i-1]的部分倒置
        reverse(ans, i, ansi - 1);//将ans[]中ans[i]到t的部分倒置
        ans[ansi++] = j;//将点j加入到ans[]尾部
        visit[j] = true;
    }
}

        在整个构造过程中,如果说每次到步骤5算做一轮的话,那么每一轮当中至少有一个节点被加入到路径S->T中,所以总的轮数肯定不超过n轮,实际上,不难看出该算法的复杂度是O(n^2),因此总共拓展了n轮路径,每步拓展最多枚举所有的节点。

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大王算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值