综述:
很多常见问题的算法都有一个多项式上限的时间复杂度,我们称这种问题为P问题。但现实生活中,很多问题我们怀疑其并没有多项式时间复杂度的解,这样的问题称为NP问题。这类问题,虽然在其潜在解空间进行搜索,可以得到正确结果,但其花费的时间难以接受。而采用状态压缩,进行动态规划往往能较好的解决一些NP问题。
状态压缩的核心是将状态用整数的二进制表示,状态转移则表示为二进制串之间的按位与,按位或,按位取反,按位异或四种运算的组合,对应的c++操作分别为A&b,A|B,~A,A^B。
所选的五道题分别为:
1.第一个问题为棋盘问题,该问题是动态规划状态压缩中最经典的问题,每行每列只能有一个棋子。
2.第二个问题为棋盘问题增加条件的变体,一个格子的上下左右都不能有物体。
3.第三个问题则在前两道题的基础上更进一步,为用1*2矩形填充n*m矩形矩形问题
4.第四个问题则是状态压缩与背包问题的结合
5.第五个问题为经典的TSP,旅行家问题,哈密尔顿回路与Floyd算法结合
问题1:
题目链接:http://poj.org/problem?id=1321
题目大意:
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C,N<=8,K<=N。
思路:
存在最优子结构等动态规划的要素,所以可以用状态压缩DP解决。
由于n最大取8,所以当前摆放状态可以由一个8位二进制整数表示,位为1表示该列已经有棋子,为0表示该列还没有棋子。
dp[i][j]表示在i行状态为j的可能数,对应的状态转移有两种:
1.当前行不加棋子,就是dp[i][j]+=dp[i-1][j];2.当前行加一个棋子,就是dp[i][newst]+=dp[i-1][j],而且加棋子的位置不能是空白的。
最后将最后一行棋子数等于k的状态的方案数求和即可。
算法步骤:
1.计算每种状态对应已放棋子数
2.记录不能放棋子的格子
3.从第一行到最后一行进行状态转移
4.得到最终的摆放方案数
算法复杂度:
O(2^N*N*N)
源代码:
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
int N,K,count;
bool check[10][10];
int num[256];
int dp[9][256];
int main()
{
for(int i=1;i<256;i++) //计算每个数值二进制对应状态中1的个数
{
int tmp=i;
while(tmp)
{
if(tmp&1) //最后一位为1的时候为真
num[i]++;
tmp>>=1; //每次右移1位
}
}
while(cin>>N>>K&&N!=-1&&K!=-1)
{
char str[20];
for(int i=1;i<=N;i++)
{
string s;
cin>>s;
for(int j=0;j<s.size();j++)
{
if(s[j]=='#')
check[i][j+1]=true;
else
check[i][j+1]=false;
}
}
int status=1<<N;
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=1;i<=N;i++)
{
for(int j=0;j<status;j++) if(num[j]<=K)
{
dp[i][j]+=dp[i-1][j]; //状态[i][j]可以由状态[i-1][j]在i行不放任何棋子得到
for(int l=1;l<=N;l++)
if(check[i][l] && (j&(1<<(l-1)))==0) //如果第i行从右往左数第l列为棋盘,且没有摆放棋子,则状态[i][j]可以由状态[i][(j|(1<<(l-1)))]在i行l列摆放棋子得到
{
dp[i][(j|(1<<(l-1)))]+=dp[i-1][j];
}
}
}
int ans=0;
for(int i=0;i<status;i++) if(num[i]==K) //对所有满足摆放了K个棋子的状态求和
ans+=dp[N][i];
cout<<ans<<endl;
}
return 0;
}
问题2:
题目链接:http://poj.org/problem?id=3254
题目大意:
一个有n*m格子的土地,有些地方可以种草,有些地方不能种草。边相邻的两个格子不允许同时种植,问总共有多少种种植方法
思路:
存在最优子结构等动态规划的要素,所以可以用状态压缩DP解决。
因为n最大为12,m最大为12,所以每行状态由二进制长度为12的数表示,2^12为4095。位为1表示种草。
由于存在初始条件,所以应该把输入的字符串表示为二进制数。
状态转移为:dp[i][state] += dp[i-1][pre_state],满足两个条件:
1.status和pre_status不相邻
2.status和pre_status是存在的
最后将最后一行满足条件的值相加得答案
算法步骤:
1.确定哪些值的二进制表达没有两个相邻的1,存入ant中,其中k表示这样的数有多少个
2.处理初始输入数据,存入row中,1表示该位不能种菜,从右往左
3.从第一行到最后一行进行状态转移
4.得到最后的种植方案
算法复杂度:
O(2^N*N*M)
源代码:
#include <iostream>
#include <cstring>
using namespace std;
const int MOD = 100000000;
int dp[15][5000],ant[5000],n,m,k,row[15];
bool check(int x)
{
if(x&(x<<1))return false;//判断某个状态,是否有两个相邻的1
return true;
}
int main()
{
while(cin>>n>>m)
{
memset(map,0,sizeof(map));
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
int tmp;
cin>>tmp;
if(tmp==0)map[i]=row[i]|(1<<j);//把第i行原始状态取反后放入map[i],1表示不可以种,从右往左
}
k=0;
memset(ant,0,sizeof(ant));
for(int i=0;i<(1<<m);i++)
{
if(check(i))
ant[k++]=i; //记录满足条件行数,k记录一共有多少个二进制数串满足没有两个相邻1
}
for(int i=0;i<k;i++)//初始化第一行状态
{
if(!(ant[i]&row[0]))
dp[0][i]=1;
}
for(int i=1;i<n;i++)
{
for(int j=0;j<k;j++)
{
if(row[i-1]&ant[j])//如果i-1行和第j个可取值有相同值的位则不满足要求,不可以种的地方种了
continue;
for(int p=0;p<k;p++)
{
if((row[i]&ant[p])||(ant[p]&ant[j]))//如果i-1行和第j个可取值有相同值的位,或者第p种可能取值和第j种可能取值有相同的值则不满足
continue;
dp[i][p]=(dp[i][p]+dp[i-1][j])%MOD;
}
}
}
int ans=0;
for(int i=0;i<k;i++)
ans=(ans+dp[n-1][i])%MOD;
cout<<ans<<endl;
}
return 0;
}
问题3:
题目链接:http://poj.org/problem?id=2411
题目大意:
在一个给定h*w的矩形区域,用2*1的小矩形填充,求解有多少种填充方案
思路:
存在最优子结构等动态规划的要素,所以可以用状态压缩DP解决。
由于h,w都小于11,所以每行状态由二进制长度为12的数表示。
第i行状态只和第i-1行状态有关。如果i-1行的出发状态某处没放,必然在第i行的该处放一个竖的矩形,所以对第i-1行取反则得到第i行放竖矩形的状态。然后搜索一道在第i行放横矩形的所有可能,并且把这些状态加上第i-1行出发状态的方法数。
最后一行1<<w-1的值即为方案数。
算法步骤:
1.从第一行开始,判断该行可以放竖矩形的状态
2.对每种状态进行深搜,判断能有多少种摆放横矩形的方法
3.把这些状态累加上i-1的出发状态的方法数,进行状态转移
4.得到最后的摆放方案
算法复杂度:
源代码:
#include <iostream>
#include <cstring>
using namespace std;
int h,w;
long long add;
long long dp[2][1<<12];
void dfs(int i,int s,int cur) //搜索在第i行,在s状态下,放横着的方块的所有可能,cur表示最后有效位
{
if(cur==w)
{
dp[i][s]+=add;
return;
}
dfs(i,s,cur+1);
if(cur<w-1&&!(s&1<<cur)&&!(s&1<<(cur+1)))
dfs(i,s|1<<cur|1<<(cur+1),cur+2);
}
int main()
{
while(cin>>h>>w&&h!=0&&w!=0)
{
int n=(1<<w)-1;
add=1;
memset(dp,0,sizeof(dp));
dfs(0,0,0);
for(int i=1;i<h;i++)
{
memset(dp[i%2],0,sizeof(dp[1])); //节省空间,每两行为一组算
for(int j=0;j<=n;j++)
if(dp[(i-1)%2][j]) //搜索扫一道在i行放横着的方块的所有可能,并且把这些状态累加上i-1的出发状态的方法数
{
add=dp[(i-1)%2][j];
dfs(i%2,~j&n,0);
}
}
cout<<dp[(h-1)%2][n]<<endl;
}
return 0;
}
问题4:
题目链接:http://poj.org/problem?id=2923
题目大意:
将家具从老地方搬到新地方,有两辆车,每辆车的载重为C1,C2,共有n件家具,每件重量为wi,两辆车必须同时行驶,求解最少的来回次数。
思路:
存在最优子结构等动态规划的要素,所以可以用状态压缩DP解决。
有n件东西,所以总状态量为1<<n,1表示该物品还没有被运走。
首先,判断这些状态中的物品能否一次就运走。然后把这些状态看做一个物体,共有tol个这样的物体,每个物体的重量为state[i],价值为1。
然后对这tol个物品种没有交集,即不存在同一位为1的物品进行0-1背包,dp[i]表示状态i需要运的最少次数,求包含n个物品的最少价值就是dp[1<<n-1]。
状态转移方程:
dp[j|k] = min(dp[j|k],dp[k]+1) (k为state[i,1<=j<=(1<<n)-1])
算法步骤:
1.判断所有状态中,哪些状态可以一次运走。
2.将所有可以一次运走的状态看做一个物体,共有tol个物品
3.对这tol个物品没交集的进行0-1背包,进行状态转移
4.最少次数为dp[1<<n-1]
算法复杂度:
O(2^N*N)
源代码:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int INF=1e9;
int state[1030];
int tol;
int dp[1030];
int n,C1,C2;
int cost[110];
bool vis[1030];
bool judge(int x) //判断x状态能否一次就运走
{
int sum=0;
memset(vis,false,sizeof(vis));
vis[0]=true;
for(int i=0;i<n;i++)
{
if((1<<i)&x)
{
sum+=cost[i];
for(int j=C1;j>=cost[i];j--)
if(vis[j-cost[i]])
vis[j]=true;
}
}
if(sum>C1+C2)return false;
for(int i=0;i<=C1;i++)
if(vis[i]&&sum-i<=C2)
return true;
return false;
}
int main()
{
int T;
int Case=0;
cin>>T;
while(T--)
{
Case++;
cin>>n>>C1>>C2;
for(int i=0;i<n;i++)
{
cin>>cost[i];
}
for(int i=0;i<(1<<n);i++)
{
dp[i]=INF;
}
dp[0]=0;
tol=0;
for(int i=1;i<(1<<n);i++)
if(judge(i))
state[tol++]=i; //tol为一次可运完的组合数
for(int i=0;i<tol;i++) //用这tol个物品种没有交集的物品进行0-1背包,
for(int j=(1<<n)-1;j>=0;j--)
{
if(dp[j]==INF)
continue;
if((j&state[i])==0)
{
dp[j|state[i]]=min(dp[j|state[i]],dp[j]+1);
}
}
cout<<Case<<dp[(1<<n)-1]<<endl;
}
return 0;
}
问题5:
题目链接:http://poj.org/problem?id=3229
题目大意:
有n个城市,规定有m个城市必须去,从起点出发,最后回到起点,在规定的时间内,求能经过的做大城市数
思路:
存在最优子结构等动态规划的要素,所以可以用状态压缩DP解决。
由于n<=15,所以用dp[i][j]表示在第i个点处于第j状态所用的总时间。
首先通过运算将可以直达的两点间的时间算出来,其次用floyd算法计算出任意两点间的能花费的最少时间。
接着进行状态转移dp[i][j]=min(dp[i][j],dp[k][j-(1<<i)]+map[k][i]+a[i]),k为第i位为0的状态,dp[i][j]表示在第i个点,处于第j状态所花时间。
对每次跟新的状态,判断其所旅行的城市数是否最多且是否满足必须去的城市都去了。
得到能旅行的最多城市数。
算法步骤:
1.计算出必须到达城市对应的状态量
2.根据floyd算法,算出任意两点间互达的最短时间
3.对每个城市进行初始化
4.进行状态转移
5.每次状态转移后判断该状态是否满足达到必须城市,并且所到城市数最大
6.输出答案
算法复杂度:
O(2^N*N*N)
源代码:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const double INF=1e9;
int N,M,K,u,v,len,kind;
double a[16];
double map[16][16];
double dp[16][1<<16];
void init()
{
int i,j;
for(i=0;i<N;i++)
{
for(j=0;j<N;j++)
{
map[i][j]=INF;
}
for(j=0;j<(1<<N);j++)
dp[i][j]=INF;
map[i][i]=0;
}
}
void floyd() //floyd算法,计算任意两点间旅行的最短时间
{
for(int k=0;k<N;k++)
{
for(int i=0;i<N;i++)
{
if(i!=k||map[i][k]<INF)
{
for(int j=0;j<N;j++)
{
if(i!=j||map[k][j]<INF)
map[i][j]=min(map[i][j],map[i][k]+map[k][j]);
}
}
}
}
}
int main()
{
while(cin>>N>>M>>K&&(N||M||K))
{
double time=12.0*K;
int res=0,ans=-1;
int cnt;
init();
for(int i=1;i<=M;i++) //res表示必须要去城市的状态,1为必须去,0为可不去
{
int j;
cin>>j;
res+=1<<(j-1);
}
for(int i=0;i<N;i++)
{
cin>>a[i];
}
while(cin>>u>>v>>len>>kind&&(u||v||len||kind))
{
u--;
v--;
double hour;
if(kind==1)
hour=1.0*len/120.0;
else if(kind==0)
hour=1.0*len/80.0;
if(map[u][v]>hour) //记录两点间可以直达的时间
{
map[u][v]=hour;
map[v][u]=hour;
}
}
floyd();
for(int i=1; i<N; i++) //在第i个城市处于j状态所用时间
dp[i][1<<i]=map[0][i]+a[i];
for(int j=0;j<(1<<N);j++)
{
for(int i=0;i<N;i++)
{
if(j&(1<<i)&&j!=(1<<i))
{
for(int k=0;k<N;k++)
{
if(j&(1<<k)&&i!=k&&j!=(1<<k))
{
dp[i][j]=min(dp[i][j],dp[k][j-(1<<i)]+map[k][i]+a[i]);
}
}
if(((j&res)==res)&&dp[i][j]+map[i][0]<=time) //判断是新算出来状态是否满足必须去的城市且是新算出来的城市数多还是之前的城市数多
{
int tmp=j;
cnt=0;
while(tmp)
{
if(tmp%2)cnt++;
tmp>>=1;
}
ans=max(ans,cnt);
}
}
}
}
if(ans>=0)
cout<<ans<<endl;
else
cout<<"No Solution"<<endl;
}
return 0;
}