状态压缩动态规划—(附luogu官方题单解法
状态压缩,即将状态通过二进制存储,以将一个复杂的状态仅仅用一个int数字表示。
在动态规划中,有着很多状态的转化,面对一些不是那么复杂的题目,我们很容易使用一维数组二维数组或三维数组的下标来存储状态,但是如果状态很复杂有着三维甚至三维以上,再仅仅使用数组下表存储就有些不合适了,下面是luogu状态压缩及动态规划官方题单
由简到难列出了几道状态压缩动态规划题的解法及思路
一、P1433
这道题一开始看是没什么思路的,有点复杂,对着别人题解做的。
思路:
首先看数据范围。1=<n<=15,
为了求解这个问题,不妨将所有奶酪状态分为两种,已经被吃和没有被吃,用1和0来分别,那么对于每一种状态我们都能够使用一个15位(二进制)的数字来表示,数字的每一位是否为1依次对应1~15块奶酪是否被吃,那么我们所要求的就是所有奶酪被吃掉时(数字的所有位都为1)移动距离最短,那我们仅仅使用一个一维数组足够存储所有的状态吗?
依据题意,可以知道到吃奶酪是有顺序的,因此我们不妨再加一维来存储第一个(或最后一个)吃的奶酪的编号依次可以得到状态转移方程
//f[第一个奶酪下标][当前状态]
//f[i][j] 从i出发,到状态j的最短距离
//i,j为奶酪编号(0~n-1)
//s 状态 包含i、j s-(1<<i)不包含i的状态
f[i][s] = min(f[i][s],dis[i][j] + f[j][s-(1<<i)]);
以上状态方程可以理解为从i出发,到达s中状态的距离求解。
我们可以遍历s中的每一个奶酪j,由于i到j再到s,取最小值,即完成状态的转移。
不妨捋一捋,遍历所有的状态,对于每个状态,求从满足条件的奶酪(包含在状态中的奶酪)出发到达状态的最短距离。
最后是两种写法(差不多其实)
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<string.h>
//luogu p1433 dp + 状态压缩
double dis(double a[],double b[]){
return sqrt((a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]));
}
double min(double a,double b ){
return a>b?b:a;
}
double all[20][2],state[20][66000],dist[20][20],ans;
int main(){
int n,i,j,k;
all[0][0]=0,all[0][1]=0;
scanf("%d",&n);
for(i=1;i<=n;i++){
scanf("%lf%lf",&all[i][0],&all[i][1]);
}
for(i=0;i<=n;i++){
for(j=0;j<=n;j++){
dist[i][j]=dis(all[i],all[j]);
}
}
memset(state,127,sizeof(state));
for(i=1;i<=(1<<(n))-1;i++){//所有状态
for(j=1;j<=n;j++){//该状态所有可能的出发点
if(((1<<j-1))==i){//状态中仅仅包含该点
state[j][i] = 0;
continue;
}
if(((1<<j-1)&i)==0){// 状态中不包含该点
continue;
}
for(k=1;k<=n;k++){
if(j==k||(i&(1<<k-1))==0)continue;
else{
state[j][i] = min(state[j][i],dist[j][k]+state[k][i-(1<<j-1)]);
}
}
}
}
ans = 1e20;
for(i=1;i<=n;i++){
ans = min(ans,dist[0][i]+state[i][(1<<n)-1]);
// printf("%.2f\n",state[i][(1<<n)-1]);
}
if(ans>1e10)ans =0;
printf("%.2f",ans);
}
//
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<string.h>
double dis(double a[],double b[]){
return sqrt((a[0]-b[0])*(a[0]-b[0])+(a[1]-b[1])*(a[1]-b[1]));
}
double min(double a,double b ){
return a>b?b:a;
}
double all[20][2],state[20][66000],dist[20][20],ans;
int main(){
int n,i,j,k;
all[0][0]=0,all[0][1]=0;
scanf("%d",&n);
for(i=1;i<=n;i++){
scanf("%lf%lf",&all[i][0],&all[i][1]);
}
for(i=0;i<=n;i++){
for(j=0;j<=n;j++){
dist[i][j]=dis(all[i],all[j]);
}
}
memset(state,127,sizeof(state));
for(i=1;i<=(1<<(n+1))-1;i++){
for(j=0;j<=n;j++){
if((1<<j)==i){
state[j][i] = 0;
continue;
}
if(((1<<j)&i)==0){
continue;
}
for(k=0;k<=n;k++){
if(j==k||(i&(1<<k))==0)continue;
state[j][i] = min(state[j][i],dist[j][k]+state[k][i-(1<<j)]);
}
}
}
printf("%.2f",state[0][(1<<(n+1))-1]);
}
二、P1441
这道题有点跟上一道题难度差不多,不过多了点背包的应用,还采用了深度搜索,算是比较综合的一道题
首先当然是求出所有状态,然后就是对每个状态求最多测量种类
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int m,n;
int tf[30]={0}, all[30],ans =0,ret = 0,f[2100],count=0;
void dp(){//对状态求解
int i,j,top= 0;
ans =0;
memset(f,0,sizeof(f));
f[0] = 1;
++count;
for(i=0;i<n;++i){
if(tf[i])continue;
for(j = top;j>=0;j--){//背包
if(f[j]&&!f[j+all[i]]){
f[j+all[i]] = 1;
top = j+all[i]>top?j+all[i]:top;
++ans;
}
}
}
ret = ans>ret?ans:ret;
}
void dfs(int cur,int rem){//深度搜索,查找出可能的状态
if(rem>m||cur>n)return;
if(rem==m&&cur==n){
dp();return;}
dfs(cur+1,rem);
tf[cur] = 1;
dfs(cur+1,rem+1);
tf[cur] = 0;
}
int main(){
int i,j,k;
scanf("%d%d",&n,&m);
for(i=0;i<n;i++){
scanf("%d",&all[i]);
}
dfs(0,0);
printf("%d",ret);
}
三、P3694
与P1433那题类似,也与顺序有关,这里的顺序是每个队伍的顺序。由于每个队伍的人数是确定的,因此,确定了队伍的顺序就可求出相对应的出队数目
思路:遍历所有状态,对于每个状态,遍历这个状态中每一队为尾的出列情况
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define maxm 25
#define maxn 100005
int count[maxm]={0},pos[maxm][maxn]={0};
int ans[(1<<20)][2]={0};
int min(int a,int b){
return a>b?b:a;
}
int mov_cnt(int sta,int type ){//包括
int ret = 0,cnt = count[type];
if(cnt ==0)return ret;
if(sta==0)ret =cnt- pos[type][sta+cnt-1];
else {
ret = pos[type][sta+cnt-1]-pos[type][sta-1];
ret = cnt -ret;
}
return ret;
}
int main(){
int n,m,i,type,j;
scanf("%d%d",&n,&m);
for(i=0;i<n;++i){//pos 从0开始
scanf("%d",&type);
++count[type];
for(j=1;j<=m;++j){
pos[j][i] = count[j];
}
}
for(i = 1;i<(1<<m);++i){
ans[i][0] = maxn;
for(j = 1;j<=m;j++){
if((i&(1<<(j-1)))==0)continue;
if(i==(1<<(j-1))){
ans[i][0] = mov_cnt(0,j),ans[i][1] = count[j];
break;
}else{
ans[i][0] =min(ans[i][0],ans[i-(1<<(j-1))][0]+mov_cnt(ans[i-(1<<(j-1))][1],j));
ans[i][1] = ans[i-(1<<(j-1))][1] + count[j];
}
}
}
printf("%d",ans[(1<<m)-1][0]);
}
四、P1896 P2704 P1879
P1896
P2704
P1879
三体都是在二维平面上,给出一些限制条件,然后要求求出某个值。
对于这些题,我们可以把每一行的每一格子是否放入作为状态,在从上到下遍历所有。
一般来说某一行的选择除了本身有关外,还与上一行有关,这些都与题目的限制条件有关
规则不同无非导致了限制条件的位运算不同。
值得一提的是 P2704 中某一行的选择还与前两行有关,但是这也没有增大太多难度,无非是再多开一维数组,将两层的状态都存起来。
还有就是滚动数组的使用,不难发现,某一行的选择与很可能仅仅与前一两行有关,因此数组可以循环使用以减小开销(MLE警告)上代码
//p1896
#include<stdio.h>
#include<stdlib.h>
#define maxk 64
#define maxn 9
long long int f[maxn][(1<<maxn)+1][maxk+1]={0},tr[maxn]={0};//三维分别对应 行、行状态、已放入数目
int n,k;
int count_one(int a){
int ret=0,i;
for(i=0;i<=maxn;i++){
ret +=(a%2);
a = a>>1;
}
return ret;
}
int main(){
int i,j,l,cnt,m ;
long long int ans=0;
scanf("%d%d",&n,&k);
for(i=0;i<n;i++){//遍历行
for(j=0;j<(1<<n);j++){//遍历该行所有状态
if((((j<<1)&j)!=0)||(((j>>1)&j)!=0)){
continue;
}
cnt = count_one(j);
if(cnt>k)continue;
if(i==0){
f[i][j][cnt] = 1;
continue;
}
for(l = 0;l<(1<<n);l++){// 遍历上一行所有状态
if((l&j)!=0||((l<<1)&j)!=0||((l>>1)&j)!=0)continue;
else {
for(m = 0;m+cnt<=k;m++){
f[i][j][m+cnt] +=f[i-1][l][m];
}
}
}
}
}
for(j = 0 ;j<(1<<n);j++){
// printf("%d\n",f[i][j][k]);
ans+=f[n-1][j][k];
}
printf("%lld",ans);
}
//p2704
#include<stdio.h>
#include<stdlib.h>
#define maxn 105
#define maxm 10
char s[maxm]={0};
int n ,m,map[maxn]={0},sum[(1<<maxm)+1];
int state[3][(1<<maxm)][(1<<maxm)+1]={0};// 多出的一维来存储上一行状态
int count_one(int a){
int ret=0,i;
for(i=0;i<=maxn;i++){
ret +=(a%2);
a = a>>1;
}
return ret;
}
int max(int a,int b){
return a>b?a:b;
}
int main(){
scanf("%d%d",&n,&m);
int i ,j,k,cnt,ret = 0,l;
for(i =0;i<(1<<m);i++){
sum[i] = count_one(i);
}
for(i = 0 ;i<n;i++){
scanf("%s",s);
for(j = m-1 ;j>=0;j--){
map[i] = (map[i]<<1);
if(s[j]=='H'){
map[i] += 1;
}
}
}
for(i = 0 ;i<n;i++){
for(j = 0 ;j<(1<<m);j++){
state[i%3][j][1<<maxm] = 0;
cnt = sum[j];
if(((j<<1)&(j))!=0||((j<<2)&(j))!=0||((j>>1)&(j))!=0||((j>>2)&(j))!=0||(j&map[i])!=0){
state[i%3][j][1<<maxm] = -1;
continue;
}
if(i == 0 ){
state[i%3][j][0] +=cnt;
ret = max(ret,cnt);
continue;
}
for(k = 0;k<(1<<m);k++){
state[i%3][j][k] = 0;
if(state[(i-1)%3][k][1<<maxm]==-1||(k&j)!=0)continue;
if(i>1){
for(l = 0 ;l<(1<<m);l++){
if((l&j)!=0||(l&k)!=0||state[(i-2)%3][l][1<<maxm]==-1)continue;
state[i%3][j][k] = max(state[i%3][j][k],state[(i-1)%3][k][l]+cnt);
}
}
else if( i ==1){
state[i%3][j][k] = max(state[i%3][j][k],state[(i-1)%3][k][0]+cnt);
}
ret = max(ret,state[i%3][j][k]);
}
}
}
printf("%d",ret);
}
#include<stdio.h>
#include<stdlib.h>
#define maxm 12
#define maxn 12
int n,m,map[maxn],sum[(1<<maxm)+1],row[maxm];
long long int count[2][(1<<maxm)+1],ret = 0;
int count_one(int a){
int i,ret = 0;
for(i = 0;i<=maxm+1;i++){
ret += a%2;
a/=2;
}
return ret;
}
int main(){
int i,j,k;
scanf("%d%d",&n,&m);
for(i = 0 ;i<n;i++){
map[i] = 0;
for(j = 0;j<m;j++){
scanf("%d",&row[j]);
}
for(j = m-1;j>=0;j--){
map[i]=(map[i]<<1)+(1-row[j]);
}
}
//for(i =0;i<(1<<m);i++){
// sum[i] = count_one(i);
//}
for(i = 0 ;i<n;i++){
for(j =0;j<(1<<m);j++){
count[i%2][j] = 0;
if(((j<<1)&j)!=0||((j>>1)&j)!=0||(j&map[i]))continue;
if(i==0){
count[i%2][j] =1;
continue;
}
for(k = 0 ;k<(1<<m);k++){
if(((k<<1)&k)!=0||((k>>1)&k)!=0||(k&map[i-1])||(k&j)!=0)continue;
count[i%2][j] += count[(i-1)%2][k];
}
count[i%2][j]%=100000000;
}
}
for(j = 0;j<(1<<m);j++){
ret += count[(n-1)%2][j];
ret %=100000000;
}
printf("%lld",ret);
}
五、P3092
预处理+二分(避免tle)
计算每个状态能购买的最多数目。
#include<stdio.h>
#include<stdlib.h>
#define maxk 16
#define maxn 1000005
int worth[maxk],cost[maxn],k,n,countone[(1<<maxk)+1],sum[maxn] = {0};
int state[(1<<maxk)+1] = {0};//
long long int retu = 0;
int max(int a,int b){
return a>b?a:b;
}
int count_one(int a){
int i,ret =0;
for(i = 0 ;i<maxk+1;i++){
ret +=a%2;
a/=2;
}
return ret;
}
long long int calculate_left(int a){
int i;
long long int ret = 0;
for(i = 0;i<k;i++){
if((a&(1<<i))==0){
ret +=worth[i];
}
}
return ret;
}
int find_largestpos(int s,int w){
int start = s,end = n,mid ;
while(start<=end){
mid = (start+end)/2;
if(sum[mid]>w){
end = mid -1;
continue;
}else if(sum[mid]<w){
start = mid+1;
continue;
}else {
return mid;
}
}
return end;
}
int pos[(1<<maxk)+1];
int main(){
int i,j,l,tmp,m;
long long int cc;
scanf("%d%d",&k,&n);
for(i = 0;i<maxk;i++){
pos[(1<<i)] = i;
}
for(i = 0 ;i<k;i++){
scanf("%d",&worth[i]);
}
for(i =1;i<=n;i++){
scanf("%d",&cost[i]);
sum[i] = sum[i-1]+cost[i];
}
for(i = 1 ;i<(1<<k);i++){
countone[i] = count_one(i);
if(countone[i]==1){
state[i] = find_largestpos(1,worth[pos[i]]);
if(state[i]==n){
retu = max(retu,calculate_left(i));
}
continue;
}
for(m= 0;m<k;m++){
if(((1<<m)&i)==0)continue;
j = ((1<<m)^i);
if(state[j]==n){
state[i] =n;
break;
}
tmp = state[j];
state[i] = max(state[i],find_largestpos(tmp+1,sum[tmp]+worth[pos[i-j]]));
if(state[i]==n){
retu = max(retu,calculate_left(i));
}
}
}
if(state[(1<<k)-1]<n)printf("-1");
else{
printf("%lld",retu);
}
}
总结
花了三四天时间总算肝完了这个题单的,挺有趣的其实。总而言之,状态压缩可以说是dp的一种实现形式,能够处理更复杂的情况,难点在于如何存储状态以及状态转移方程的建立。