更快的哈密顿路径/哈密顿回路算法

对于哈密顿问题,朴素的做法通常是 \(O(n!)\) 或者 \(O(2^n n^3)\) 的简单状压。接下来我们对哈密顿问题的状压做法进行若干优化,做到空间 \(O(2^n)\),时间 \(O(2^n n)\)

首先朴素状压设 \(f_{S,u,v}\) 表示是否存在一条 \(u\)\(v\) 的经过的点集为 \(S\) 的路径。

01 状态显然非常浪费,可以拿类似 bitset 的技巧优化一下。我们将 \(v\) 这一维压成一个数,状态变成 \(f_{S,u}\),对于点 \(v\) 的邻域也压成一个数 \(g_v\),那么转移的判断条件变成了 \(g_v\ \text{bitand}\ f_{S,u}\neq 0\)

时间优化到了 \(O(2^n n^2)\),空间到了 \(O(2^n n)\)

接下来有两种优化路线:

Route 1

给出这道题,此题瓶颈在于判断每一个点的集合是否是一个回路。

回路的特点是起点任意,所以我们钦定集合中最小的那个点永远是起点,扔掉了 \(u\) 这一维,接下来按照上述方法优化转移即可。

输出方案需要一定的技巧,如果直接记录每个状态的前驱空间复杂度会多一个 \(n\),这样常数巨大。

正确的做法是用 \(\text{bitand}\)\(\text{lowbit}\) 操作每次找出一个当前点的合法前驱。

原题代码:

#include <bits/stdc++.h>
#define fi first
#define se second
using namespace std; 
typedef long long ll;
typedef pair<int,int> pii;
template<typename T=int>
T read(){
	char c=getchar();T x=0;
	while(c<48||c>57) c=getchar();
	do x=(x<<1)+(x<<3)+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
int n,m;
int g[20];
int f[1<<20];
bool cir[1<<20];
int res[20];
int lis[20],rk;
void output(int s,int x){
	puts("Yes");
	for(int i=0;i<n;++i){
		if(s>>i&1) continue;
		res[i]=__builtin_ctz(g[i]&s);
	}
	while(s^=(1<<x)){
		lis[rk++]=x;
		x=__builtin_ctz(f[s]&g[x]);
	}
	lis[rk++]=x;
	for(int i=1;i<rk;++i) res[lis[i-1]]=lis[i];
	res[lis[rk-1]]=lis[0];
	for(int i=0;i<n;++i) printf("%d ",res[i]+1);
	putchar('\n');
}
int main(){
	n=read();m=read();
	for(int i=0;i<m;++i){
		int u=read()-1,v=read()-1;
		g[u]|=(1<<v);
		g[v]|=(1<<u);
	}
	for(int i=0;i<n;++i) f[1<<i]=1<<i;
	for(int s=1;s<(1<<n);++s){
		int lb=__builtin_ctz(s);
		for(int i=lb+1;i<n;++i){
			if(s>>i&1) continue;
			if(f[s]&g[i]) f[s|(1<<i)]|=(1<<i);
		}
		if(f[s]&g[lb]){
			bool fl=1;
			for(int i=0;i<n;++i){
				if(s>>i&1) continue;
				if(g[i]&s) continue;
				fl=0;break;
			}
			if(fl){output(s,__builtin_ctz(f[s]&g[lb]));return 0;}
		}
	}
	puts("No");
	return 0;
}

哈密顿回路核心代码:

#include <cstdio>
using namespace std;
int n;
int f[1<<24],g[24];
int lis[24],rk;
char str[30];
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;++i){
		scanf("%s",str);
		for(int j=0;j<n;++j) g[i]|=(str[j]^48)<<j;
	}
	for(int i=0;i<n;++i) f[1<<i]=1<<i;
	for(int s=1;s<(1<<n);++s){
		int lb=__builtin_ctz(s);
		for(int i=lb+1;i<n;++i){
			if(s>>i&1) continue;
			if(f[s]&g[i]) f[s|(1<<i)]|=(1<<i);
		}
	}
	if(f[(1<<n)-1]&g[0]){
		puts("Yes");
		int s=(1<<n)-1;
		int x=__builtin_ctz(f[s]&g[0]);
		while(s^=(1<<x)){
			printf("%d ",x);
			x=__builtin_ctz(f[s]&g[x]);
		}
		printf("%d ",x);
		putchar('\n');
	}
	else puts("No");
	return 0;
}

上述方法不好做哈密顿路径。下面的方法更具有普遍性。

Route 2

给出这道题

我们发现对于每个起点跑一遍 \(O(2^n n)\)\(\text{DP}\) 很浪费。我们只对一个节点 \(0\) 跑出以它为起点的路径信息 \(f_S\)

对于一条哈密顿回路,必须有 \(u\in f_{mask_u},v\in f_{mask_v}\),并且 \(mask_u \cup mask_v=\{0,1,\dots ,n-1\},mask_u \cap mask_v=\{0\}\)

于是枚举 \(mask_u\) 就可以利用位运算统计出所有点对间是否存在哈密顿路径,自然也判断出了哈密顿回路。

原题代码:

#include <cstdio>
using namespace std;
int n;
int f[1<<24],g[24];
int lis[24],rk;
char str[30];
int res[24];
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;++i){
		scanf("%s",str);
		for(int j=0;j<n;++j) g[i]|=(str[j]^48)<<j;
	}
	f[1]=1;
	for(int s=1;s<(1<<n);s+=2){
		if(!f[s]) continue;
		for(int i=1;i<n;++i){
			if(s>>i&1) continue;
			if(f[s]&g[i]) f[s|(1<<i)]|=(1<<i);
		}
	}
	for(int s=1;s<(1<<n);s+=2){
		if(!f[s]) continue;
		for(int u=0;u<n;++u)
			if(f[s]>>u&1) res[u]|=f[((1<<n)-1)^s^1];
	}
	for(int u=0;u<n;++u){
		for(int v=0;v<n;++v)
			if(res[u]>>v&1) putchar('1');
			else putchar('0');
		putchar('\n');
	}
	return 0;
}
posted @ 2023-03-13 22:41  yyyyxh  阅读(555)  评论(0编辑  收藏  举报