当前位置:首页 > > 充电吧
[导读]首先说明一下,此博文来自我在CSDN上看到的一篇哈密顿回路(有向图中)的位运算算法,出自GDTZX大神之手,(侵删),虽然刚从校园毕业,但脑子已经完全僵住了,花了许久才看懂了这个算法。 哈密顿回路,

首先说明一下,此博文来自我在CSDN上看到的一篇哈密顿回路(有向图中)的位运算算法,出自GDTZX大神之手,(侵删),虽然刚从校园毕业,但脑子已经完全僵住了,花了许久才看懂了这个算法。

哈密顿回路,具体到本题之中即从某一个点开始经过所有的点一次后再回到该点的不同路径数。对于这个不同需要注意两点:

如果我们将路径经过的点按顺序写下,比如当n=3时,若存在123和231。此时,我们认为这两条路径是同一条哈密顿回路。而123和213则是不同的哈密顿回路。

若两个点之间有多条边,经过不同的边的路径仍然看作同一条哈密顿回路。不同哈密顿回路只和经过的点有关。因此对于多条边的情况我们可以将其合并为一条边来考虑。

对于哈密顿回路,一个简单的想法就是枚举所有可能的路径,判定这个路径是否存在。即时间复杂度为O(n!)。而题目给定的数据范围为:n <= 12,所以最大可能的枚举次数为12! = 479,001,600。

极限的数据不到5亿,所以我们可以考虑使用暴力来枚举所有的哈密顿回路。直接采用DFS枚举我们的每一步,最后判定是否走回到起点。

伪代码如下:

DFS(int nowVertex, bool visitedVertex, int path, int length)
    visitedVertex[ nowVertex ] = True;
    If (all Vertex is visited) Then
        Count = Count + 1
    Else 
        For (u is next vertex of nowVertex)
            If (not visitedVertex[ u ]) Then
                path[ length ] = u
                DFS(u, visitedVertex, path, length + 1)
            End If
        End For    
    End If
    visitedVertex[ nowVertex ] = False

由于哈密顿回路始终会经过每一个点,所以我们只以1为起点就一定可以找出所有的哈密顿回路。

那么这样是否能够解决这道题目呢?我只能说不一定能够解决。

虽然伪代码相同,但是根据实现的方法会有不同的运行效率,在某些实现方法下就能够通过所有的数据点,在某些实现方法下就会超过时限。

这里我们介绍一种利用位运算的实现,能够使得整个程序的效率提高很多倍。

首先来看看代码:

const int MAXN = 14;

int edge[ MAXN ];
int p[1 << MAXN];
int cnt;

void dfs(int nowVertex, int unused) {
    if (!unused) {
        cnt += (edge[nowVertex] & 1);
        return ;
    }
    int rest = unused & edge[ nowVertex ];
    while (rest) {
        int tp = rest & (-rest);
        dfs(p[ tp ], unused - tp);
        rest -= tp;
    }
    return ;
}

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    for (int i = 0; i < n; ++i)
        p[ 1 << i ] = i + 1;
    while (m--) {
        int u, v;
        scanf("%d %d", &u, &v);
        edge[u] |= 1 << (v - 1);
    }
    dfs(1, (1 << n) - 2);
    printf("%dn", cnt);
    return 0;
}

我们一个一个来解释每一个变量的含义:

edge[i]表示点i的next节点情况,我们用二进制表示edge[i],比如当edge[i]=01011时:

+---+---+---+---+---+
| 5 | 4 | 3 | 2 | 1 | 右起第j位
+---+---+---+---+---+
| 0 | 1 | 0 | 1 | 1 | 二进制位的值
+---+---+---+---+---+

从右起第j位,若为1,则表示存在i到j的边;若为0,则表示不存在i到j的边。所以edge[i]=01011就表示节点i可以到达节点1,2,4。

p[i]是为了方便查找点的编号。在edge[i]中若存在01000,我们可以根据01000=8, 而p[8]=4来快速的通过二进制位来定位节点编号。

所以有初始化:

for (int i = 0; i < n; ++i)
    p[ 1 << i ] = i + 1;

而通过节点编号来找到二进制位,则可以直接使用1 << (i - 1)实现。

我们在读入数据时,通过位运算边可以记录下所有点之间的连边情况:

while (m--) {
    int u, v;
    scanf("%d %d", &u, &v);
    edge[u] |= 1 << (v - 1);    // 记录u有后继节点v
}

unused该二进制数表示我们尚未访问的节点集合,同样的右起第i位表示节点i,比如unused = 01100

+---+---+---+---+---+
| 5 | 4 | 3 | 2 | 1 | 右起第i位
+---+---+---+---+---+
| 0 | 1 | 1 | 0 | 0 | 二进制位的值
+---+---+---+---+---+

表示我们现在深度优先搜索已经经过了节点1,2,5,而节点3,4还尚未经过。

由于我们是以节点1作为起点,初始化的unused也就要去掉最右边的1,所以代码为dfs(1, (1 << n) - 2)

接下来我们逐行解释dfs函数:

if (!unused) {
    cnt += (edge[nowVertex] & 1);
    return ;
}

当我们所有的节点都经过一次之后,unused中一定不存在1,因此有!unused = true。但是此时并不一定找到了哈密顿回路,我们还需要判断当前节点是否能回到起点,也就是节点1。若nowVertex可以到达节点1,则edge[nowVertex]最右边1位一定是1,那么就一定有edge[nowVertex] & 1 = 1

int rest = unused & edge[ nowVertex ];

rest表示当前节点还可以走到的未访问节点。由于&运算的性质,只有当unusededge[ nowVertex ]对应二进制位同时为1时,rest对应的二进制位才会为1。其含义就是该节点尚未访问,且节点nowVertex可以到达该节点。

while (rest) {
    int tp = rest & (-rest);
    dfs(p[ tp ], unused - tp);
    rest -= tp;
}

该循环的作用是枚举每一个可以到达的点。

int tp = rest & (-rest);

这里利用了一个性质,即p & -p等于取出p的最右边的一个1。举个例子p=10110:

   +---+---+---+---+---+
p  | 1 | 0 | 1 | 1 | 0 | 
   +---+---+---+---+---+
-p | 0 | 1 | 0 | 1 | 0 | 
   +---+---+---+---+---+
&  | 0 | 0 | 0 | 1 | 0 | 
   +---+---+---+---+---+

我们不断的利用这个性质从rest里面取出可以使用的二进制位,进行dfs(p[ tp ], unused - tp);。同时再枚举完一个节点后,将其从rest中删除,即rest -= tp;

最后我们再使用printf("%dn", cnt);来输出我们找到的方案数。


在上面DFS的基础上,我们还可以进一步优化。

递归的过程中,unused很有可能会出现重复的情况,比如说从1->3->2->4和从1->2->3->4,对于dfs(4, unused)来说,此时的unused值都是相同的。因此我们可以采用记忆化搜索的方式进一步降低复杂度。

定义数组f[n][unused]表示当前节点为n,且节点访问情况为unused时的方案数量。

那么有:

f[n][unused] = sigma(f[p][unused + (1 << (n - 1))] | (unused & (1 << (n - 1)) == 0) and (p != n) and (edge[p] & (1 << (n - 1)) != 0))

这对应的是原dfs函数中下面这段代码的逆过程。

while (rest) {
    int tp = rest & (-rest);
    dfs(p[ tp ], unused - tp);
    rest -= tp;
}

三个条件分别为:

(unused & (1 << (n - 1)) == 0)表示当前状态中右起第n位为0(p != n)表示前驱结点不等于n(edge[p] & (1 << (n - 1)) != 0)表示节点p能够到达节点n

在计算f[n][unused]的过程中,假设unused的二进制表示中有i个1,则我们需要事先计算出所有i+1个1的状态才能够保证unused + (1 << (p - 1))是正确的结果。

因此我们在枚举过程中,需要按照状态中1的个数来枚举。

其伪代码:

For numberOfOnes = n-1 .. 0
    For (the number of ones of unused equals numberOfOnes)
        For nowVertex = 1 .. n
            For prevVertex = 1 .. n
                If (unused & (1 << (nowVertex - 1)) == 0) and (prevVertex != nowVertex) and (edge[ prevVertex ] & (1 << (nowVertex - 1)) != 0) Then
                    f[ nowVertex ][ unused ] = f[ nowVertex ][ unused ] + f[ prevVertex ][unused + (1 << (nowVertex - 1))]
                End If
            End For
        End For
    End For
End For

对于f[n][ unused ]数组,其初始条件为f[1][(1 << n) - 2] = 1

最后需要将所有的f[n][0]中能够到达节点1的节点n累加起来,即可得到所有的方案数。该算法的理论时间复杂度为O(2^n*n^2)

结果分析

该题目一共只有7名选手通过,大部分提交过该题的选手也都有一定的部分分值。

本题直接采用搜索就能够通过,拉开差距的主要原因是对于伪代码实现方式的不同,而导致通过的测试点数量不同。

另外还有一个易错点是走完所有节点之后一定要判断是否可以回到起点。




本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭