我们知道,用DP解决一个问题的时候很重要的一环就是状态的表示,一般来说,一个数组即可保存状态。但是有这样的一些题目,它们具有DP问题的特性,但是状态中所包含的信息过多,如果要用数组来保存状态的话需要四维以上的数组。于是,我们就需要通过状态压缩来保存状态,而使用状态压缩来保存状态的DP就叫做状态压缩DP。
状态压缩DP的特点:
状态中的某一维会比较小,一般不会超过15,多了的话状态数会急剧上升而无法压缩,一般来说需要状态压缩的也就是这一维。
状态压缩DP的常见优化:
预处理是最常见的优化,尤其是在棋盘类问题上,比如说例题1,如果我们想进一步提高效率,我们还可以预处理出状态之间是否可以转移而不用在每一次转移中判断。
例如:经典的哈密尔顿回路问题。
状态压缩动态规划(简称状压dp)是另一类非常典型的动态规划,通常使用在NP问题的小规模求解中,虽然是指数级别的复杂度,但速度比搜索快,其思想非常值得借鉴。
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’^’符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
1.判断一个数字x二进制下第i位是不是等于1。
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
2596 售货员的难题
题目描述 Description
某乡有n个村庄(1<n<=15),有一个售货员,他要到各个村庄去售货,各村庄之间的路程s(0<s<1000)是已知的,且A村到B村与B村到A村的路大多不同。为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为1,他不知道选择什么样的路线才能使所走的路程最短。请你帮他选择一条最短的路。
输入描述 Input Description
村庄数n和各村之间的路程(均是整数)
输出描述 Output Description
最短的路程
样例输入1 Sample Input
3
0 2 1
1 0 2
2 1 0
样例输出1 Sample Output
3
样例输入2 Sample Input
4
0 1 2 4
1 0 1 3
2 1 0 2
4 3 2 0
样例输出2 Sample Output
状态压缩DP的特点:
状态中的某一维会比较小,一般不会超过15,多了的话状态数会急剧上升而无法压缩,一般来说需要状态压缩的也就是这一维。
状态压缩DP的常见优化:
预处理是最常见的优化,尤其是在棋盘类问题上,比如说例题1,如果我们想进一步提高效率,我们还可以预处理出状态之间是否可以转移而不用在每一次转移中判断。
例如:经典的哈密尔顿回路问题。
状态压缩动态规划(简称状压dp)是另一类非常典型的动态规划,通常使用在NP问题的小规模求解中,虽然是指数级别的复杂度,但速度比搜索快,其思想非常值得借鉴。
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’^’符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
1.判断一个数字x二进制下第i位是不是等于1。
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
2596 售货员的难题
题目描述 Description
某乡有n个村庄(1<n<=15),有一个售货员,他要到各个村庄去售货,各村庄之间的路程s(0<s<1000)是已知的,且A村到B村与B村到A村的路大多不同。为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为1,他不知道选择什么样的路线才能使所走的路程最短。请你帮他选择一条最短的路。
输入描述 Input Description
村庄数n和各村之间的路程(均是整数)
输出描述 Output Description
最短的路程
样例输入1 Sample Input
3
0 2 1
1 0 2
2 1 0
样例输出1 Sample Output
3
样例输入2 Sample Input
4
0 1 2 4
1 0 1 3
2 1 0 2
4 3 2 0
样例输出2 Sample Output
8
题目分析:
从当前节点出发,可以去往任意一个没有到过的结点。我们将所有节点用一个二进数表示,(bn-1,....b1,b0)bi=0代表i+1结点没走过,bi=1代表走过。
设目前被到达的结点产生的状态i,最后叨叨的节点为j,最短路表示为f[j][i];显然从商店出发,第一次到达的节点i,那么f[i][2^(i-1)]=map[0][i].,
阶段i:我们用i表示当前的状态,枚举i,(0-2^n-1)
状态j:i状态中最后到达的结点。
决策:枚举状态i可以到达的状态i+2^(k-1),并找出记录这个状态的最优解。 f[k][i|(1<<(k-1))] =min(f[k][i|(1<<(k-1))],f[j][i]+map[j][k]);
最后回到枚举统计从遍历的最后一个节点返回商店。找到最优值。
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
using namespace std;
const int maxn=20;
int map[maxn][maxn],num,n,ans,nums;
int f[maxn][1<<18];
void dp(){
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++)f[i][1<<(i-1)]=map[0][i];
nums=(1<<n)-1;
for(int i=0;i<=nums;i++)//枚举所有可能的状态;
for(int j=1;j<=n;j++)
if(i&(1<<(j-1))) //枚举当前状态下到了第j个结点
for(int k=1;k<=n;k++) {//从j之k,找出到k的最优值。
if((i&(1<<(k-1)))==0) //如果第k结点还到,那么到达k,并更新其值。
f[k][i|(1<<(k-1))] =min(f[k][i|(1<<(k-1))],f[j][i]+map[j][k]);
}
ans=1<<20;
for(int i=1;i<=n;i++)
ans=min(ans,f[i][nums]+map[i][0]);
cout<<ans<<endl;
}
int main(){
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
cin>>n;
n--;
for(int i=0;i<=n;i++)
for(int j=0;j<=n;j++)
cin>>map[i][j];
dp();
return 0;
}