更快的哈密顿路径/哈密顿回路算法
对于哈密顿问题,朴素的做法通常是 \(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;
}