二进制的很多应用离不开集合这个概念,我们都知道在计算机当中,所有数据都是以二进制的形式存储的。一般一个int整形是4个字节,也就是32位bit,我们通过这32位bit上0和1的组合可以表示多大21亿个不同的数。如果我们把这32位bit看成是一个集合,那么每一个数都应该对应集合的一种状态,并且每个数的状态都是不同的。
比如上图当中,我们列举了5个二进制位,我们把其中两个设置成了1,其余的设置成了0。我们通过计算,可以得到6这个数字,那么6也就代表了(00110)这个状态。数字和状态是一一对应的,因为每个整数转化成二进制都是唯一的。
也就是说一个整数可以转化成二进制数,它可以代表某个集合的一个状态,这两者一一对应。这一点非常重要,是后面一切推导的基础。
状态转移
整数的二进制表示可以代表一个二元集合的状态,既然是状态就可以转移。在此基础上,我们可以得出另一个非常重要的结论——我们可以用整数的加减表示状态之间的转移。
我们还用刚才的例子来举例,上面的图当中我们列举了5个二进制位,假设我们用这5个二进制位表示5个小球,这些小球的编号分别是0到4。这样一来,刚才的6可以认为表示拿取了1号和2号两个小球的状态。
如果这个时候我们又拿取了3号小球,那么集合的状态会发生变化,我们用一张图来表示:
上图当中粉丝的笔表示决策,比如我们拿取了3号球就是一个决策,在这个决策的影响下,集合的状态发生了转移。转移之后的集合代表的数是14,它是由之前的集合6加上转移带来的变化,也就是得到的。刚好就代表拿取3号球这个决策,这样我们就把整个过程串起来了。
总结一下,我们用二进制的0和1表示一个二元集合的状态。可以简单认为某个物品存在或者不存在的状态。由于二进制的0和1可以转化成一个int整数,也就是说我们用整数代表了一个集合的状态。这样一来,我们可以用整数的加减计算来代表集合状态的变化。
这也就是状态压缩的精髓,所谓的压缩,其实就是将一个集合压缩成了一个整数的意思,因为整数可以作为数组的下标,这样操作会方便我们的编码。
最短Hamilton路径问题(也就是大名鼎鼎的旅行商问题)
给定一张 n 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数nn。
接下来nn行每行nn个整数,其中第ii行第jj个整数表示点ii到jj的距离(记为a[i,j])。
对于任意的x,y,zx,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]>=a[x,z]。
输出格式
输出一个整数,表示最短Hamilton路径的长度。
数据范围
1≤n≤201≤n≤200≤a[i,j]≤1070≤a[i,j]≤107
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18
1 #include<iostream>
2 #include<cstring>
3 #include<algorithm>
4
5 using namespace std;
6
7 const int N=20,M=1<<N;
8 long long int dp[M][N];
9 int w[N][N];
10 int n;
11
12 int main()
13 {
14 cin>>n;
15
16 for(int i=0;i<n;i++)
17 for(int j=0;j<n;j++)
18 cin>>w[i][j];
19
20 memset(dp,0x3f,sizeof dp);
21 dp[1][0]=0;
22 for(int i=0;i<(1<<n);i++)
23 for(int j=0;j<n;j++)
24 if(i>>j&1)
25 for(int k=0;k<n;k++)
26 if(i-(1<<j)>>k&1)
27 dp[i][j]=min(dp[i][j],dp[i-(1<<j)][k]+w[k][j]);
28
29 cout<<dp[(1<<n)-1][n-1];
30
31 return 0;
32 }