1、什么是状态压缩DP
状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1;当然如果有三种状态用三进制来表示也未尝不可。
状态压缩DP:顾名思义,就是用状态压缩实现DP
2、经典问题
状态压缩最经典的问题应该就是旅行商问题了
(1)问题描述:旅行推销员问题(英语:Travelling salesman problem, TSP)是这样一个问题:给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。(它是组合优化中的一个NP难问题,在运筹学和理论计算机科学中非常重要。)
(2)如何运用状态压缩
比如一共有5个点:1 2 3 4 5。现在要表示已经走过的地方和没走过的地方,我们可以用0,1来表示。其中0表示没到过,1表示到达过
那么对应的状态有
1 2 3 4 5
0 0 0 0 0(刚要开始走,还没有到达的地方)
0 0 0 0 1(已经到过第五个点)
0 0 0 1 0(已经到过第四个点)
0 0 0 1 1(已经到过第四,五个点)
……
我们发现以上的状态可以用二进制数表示,二进制数就是由0,1组成的。并且2^5可以涵盖所有的情况,从00000到11111;
dp[i][s]表示走到i这个点,已经经过的地方为s,此时所走过的最短路。
(3)状态转移
举个栗子,当s为11011的时候,s可以是由11001来,也可以是从11010来。那么我们可以运用位运算:
for(int j=1;j<=n;j++)//是从j走到i的
{
if(i==j||(s&(1<<(j-1)))==0)//没有j这个元素或者从i到i
{
continue;
}
dp[i][s]=min(dp[i][s],dp[j][s-(1<<(i-1))]+dis(i,j));//dis(i,j)为i到j的距离
}
实例与完整代码:
题目描述:
房间里放着 nn 块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在 (0,0)(0,0) 点处。
#include<iostream>
#include<math.h>
#include<algorithm>
#include<cstring>
#include<iomanip>
using namespace std;
double x[20];
double y[20];
int n;
double dp[20][35000];//dp[i][s]表示走到i这个点,已经到达过的集合为s
double dis(int i,int j)
{
return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>x[i];
cin>>y[i];
}
x[0]=y[0]=0;
memset(dp,127,sizeof(dp));//设置全部dp为很大的数
for(int s=1;s<(1<<n);s++)
{
for(int i=1;i<=n;i++)//s中新加入的元素可能是i
{
if((s&(1<<(i-1)))==0)//s中没有这个元素。
{
continue;
}
if(s==(1<<(i-1)))//s就只有这个元素。也就是选择起点。
{
dp[i][s]=0;
}
for(int j=1;j<=n;j++)//是从j走到i的
{
if(i==j||(s&(1<<(j-1)))==0)//没有j这个元素或者从i到i
{
continue;
}
dp[i][s]=min(dp[i][s],dp[j][s-(1<<(i-1))]+dis(i,j));
}
}
}
double ans=1e9;
for(int i=1;i<=n;i++)
{
double s=dp[i][(1<<n)-1]+dis(i,0);
ans=min(ans,s);
}
cout<<fixed<<setprecision(2)<<ans<<endl;
}
3、更多例子:
郊区春游
今天春天铁子的班上组织了一场春游,在铁子的城市里有n个郊区和m条无向道路,第i条道路连接郊区Ai和Bi,路费是Ci。经过铁子和顺溜的提议,他们决定去其中的R个郊区玩耍(不考虑玩耍的顺序),但是由于他们的班费紧张,所以需要找到一条旅游路线使得他们的花费最少,假设他们制定的旅游路线为V1, V2 ,V3 … VR,那么他们的总花费为从V1到V2的花费加上V2到V3的花费依次类推,注意从铁子班上到V1的花费和从VR到铁子班上的花费是不需要考虑的,因为这两段花费由学校报销而且我们也不打算告诉你铁子学校的位置。
解题思路:
floyd+tsp。先用floyd求出每个点之间相互最短距离,然后转化,最后用tsp。dis,edge,dp的初始化。(一开始因为用了memset(dis,127,sizeof(dis)),但是这样在后面的dis相加越界导致值变为负数出错)
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
int dp[205][(1<<15)+10];
int vis[205];//要去这个郊区玩
int dis[205][205];//从i到j最短路
int edge[25][25];
int main()
{
int n,m,r;//n郊区m条边去r个郊区玩耍
cin>>n>>m>>r;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
dis[i][j]=1e9;
}
}
for(int i=1;i<=r;i++)
{
for(int j=1;j<=r;j++)
{
edge[i][j]=1e9;
}
}
memset(dp,127,sizeof(dp));
for(int i=1;i<=r;i++)
{
cin>>vis[i];//要去玩的郊区
}
for(int i=0;i<m;i++)
{
int u,v,w;
cin>>u>>v>>w;
dis[u][v]=dis[v][u]=w;
}
//floyd
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(dis[i][j]>dis[i][k]+dis[k][j])
{dis[i][j]=dis[i][k]+dis[k][j];}
}
//把点转化为1234
for(int i=1;i<=r;i++)
for(int j=1;j<=r;j++)
edge[i][j]=dis[vis[i]][vis[j]];
//tsp
for(int s=1;s<(1<<r);s++)
{
for(int i=1;i<=r;i++)//集合为s的时候刚好走到i
{
if((s&(1<<(i-1)))==0) continue;//没有i这个点
if(s==(1<<(i-1))) {dp[i][s]=0;continue;}//起点
for(int j=1;j<=r;j++)//从j走到i
{
if(dp[i][s]>dp[j][s-(1<<(i-1))]+edge[i][j])
dp[i][s]=dp[j][s-(1<<(i-1))]+edge[i][j];//直接减去,意思就是消除i这个点,表示没有到过这里,然后可以从j到i
}
}
}
//不计班级到起点也不用从终点回班级
int ans=1e9;
for(int i=1;i<=r;i++)
{
ans=min(dp[i][(1<<r)-1],ans);
}
cout<<ans<<endl;
}
锁
题目描述 :
106号房间共有n名居民, 他们每人有一个重要度。房间的门上可以装若干把锁。假设共有k把锁,命名为1到k。每把锁有一种对应的钥匙,也用1到k表示。钥匙可以复制并发给任意多个居民。每个106房间的居民持有若干钥匙,也就是1到k的一个子集。如果几名居民的钥匙的并集是1到k,即他们拥有全部锁的对应钥匙,他们都在场时就能打开房门。新的陆战协定规定,一组居民都在场时能打开房门当且仅当他们的重要度加起来至少为m。问至少需要给106号房间装多少把锁。即,求最小的k,使得可以适当地给居民们每人若干钥匙(即一个1到k的子集),使得任意重要度之和小于m的居民集合持有的钥匙的并集不是1到k,而任意重要度之和大于等于m的居民集合持有的钥匙的并集是1到k。
答案是这样的居民子集个数q:重要度的和不足s,但加入任何一个新居民都将导致重要度的和大于等于s.
#include<iostream>
using namespace std;
long long a[25];
int main()
{
long long n,m;
cin>>n>>m;
long long ans=0;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
for(int i=0;i<(1<<n);i++)
{
long long sum=0;
for(int j=0;j<n;j++)
{
if((i&(1<<j)))
{
sum+=a[j];
}
}
int flag=1;
if(sum>=m) {continue;}
for(int j=0;j<n;j++)
{
if((i&(1<<j))==0)
{
if(sum+a[j]<m)
{
flag=0;
break;
}
}
}
ans+=flag;
}
cout<<ans<<endl;
}