【学习笔记】状压DP

状态压缩DP

对于一个集合,他一有\(2^n\)个子集,而状态压缩就是枚举这些子集,每一个状态就是一个由\(01\)构成的集合,如果为\(0\)就表示不选当前的元素,否则就表示选。因为状态压缩将每一个状态压缩成了一个用二进制表示的数,所以不光可以节省空间,还可以节省时间。
因为是枚举子集,所以时间复杂度为\(O(2^n)\),一般使用的标志就是\(n\le20\).

关灯问题

这是一道经典的\(bfs\)加状态压缩的题目!!

思路

题目要求最多按多少次可以将所有的灯关闭。我们可以将题目给出的条件转化为一个图。每个节点所表示的是当前所有灯的开关情况,但是我们发现这样每个节点的信息要用一个一维数组表示,不好写代码,所以我们可以用状态压缩将每次的开关灯状态转化为一个二进制表示,接着跑一遍\(bfs\)
对于每种状态,枚举接下来按的按钮,生成新一个的状态,如果当前状态没有被访问过,就加进队列,并记录步数,因为是\(bfs\),所以此步数一定是到达当前状态的最小步数。

位运算

题目中涉及到第\(i\)个开关对\(j\)盏灯的影响,设当前状态为\(v\)
1.如果\(a[i][j]=1\)并且第\(j\)盏灯是开着的

\[v=v\oplus(1<<(j-1)) \]

2.如果\(a[i][j]=0\),不用管
3.如果\(a[i][j]=0\)并且第\(j\)盏灯是关着的

\[v=v\oplus(1<<(j-1)) \]

\(code\)

#include<bits/stdc++.h>
using namespace std;
int n,m;
int a[110][11],d[1<<11];
queue<int>q;
bool vis[1<<11];
void bfs(){
	q.push((1<<n)-1);
	vis[(1<<n)-1]=1;
	d[(1<<n)-1]=0;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		for(int i=1;i<=m;i++){
			int v=u;
			for(int j=1;j<=n;j++){
				if(a[i][j]&&(v&(1<<(j-1))))v=(v^(1<<(j-1)));
				else if(a[i][j]==-1&&(!(v&(1<<(j-1)))))v=(v^(1<<(j-1)));
			}
			if(!vis[v]){
				vis[v]=1;
				q.push(v);
				d[v]=d[u]+1;
				if(v==0)return;
			}
			
		}
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			cin>>a[i][j];
		}
	}
	bfs();
	if(d[0])cout<<d[0];
	else cout<<-1;
	return 0;
}

炮兵阵地


这是一道状压DP的典题!!

题目大意

每一个炮兵部队都有一定的攻击范围,题目要求在地图上最多能安排多少个炮兵部队,而且要满足以下几个条件:
1.各个部队之间不能互相伤害
2.部队不能安排在山坡上

思路梳理

读入

状压DP就是用一串\(0,1\)来表示当前状态,读入时要将每一行的字符转化为二进制,每次读入一个字符,如果为\(H\)那么二进制串的当前位是\(1\),表示山坡,否则是\(0\)表示平地。

for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			char x;
			cin>>x;
			a[i]<<=1;
			if(x=='H')a[i]+=1;
		}
}

预处理

为了方便统计每种状态下能安排的炮兵部队的个数,即\(1\)的个数,所以需要预处理,每个\(01\)串中\(1\)的个数

int getsum(int S){
	int tot=0;
	while(S){
		if(S&1)++tot;
		S>>=1;
	}
	return tot;
}

动态规划方程

因为一个炮兵部队的攻击范围涉及当前行状态\(s\),上一行状态\(l\),上上行状态\(fl\),当前行数\(i\),显然设一个四位数组\(f[s][l][fl][i]\),空间复杂度为\(O(2^{30}*100)\),显然会炸空间,所以简化设一个三维数组,\(f[s][l][i]\)表示当前状态为\(s\),上一状态为\(l\),当前行数为\(i\)经过反复推敲,动态规划方程

\[f[s][l][i]=max(f[s][l][i],f[l][fl][i-1]+sum[s]) \]

但是发现此时的空间复杂度\(O(2^{20}*100)\),约为\(10^8\),但是见过世面的我们发现动态规划方程只与前三维有关,所以开滚动数组好啦!

条件限制

1.部队只能安排在山坡上,则

\[a[i]\&S!=0 \]

因为如果结果为\(1\),则表示,当前地形为山坡,并且还安排了炮兵部队
2.部队间的左右距离必须大于\(2\)

\[s\&(s<<1)!=0,S\&(s<<2)!=0 \]

如果为\(1\),就表示存在一位\(i\),右边的\(i+1\)\(i+2\),与他都为1,或者左边的\(i-1\)\(i-2\),与他都为1
3.部队间的前后距离必须大于\(2\)

\[s\&fl!=0\&\&s\&l!=0 \]

理解类似于1

\(code\)

注意!!!状态的最大值为\(111...1\)\(2^m-1\)所以代码中要写 $$<2^m$$

#include <bits/stdc++.h>
using namespace std;
int n,m,a[110];
int sum[1<<10],f[1<<10][1<<10][5];
int ans=0;
int getsum(int S){
	int tot=0;
	while(S){
		if(S&1)++tot;
		S>>=1;
	}
	return tot;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			char x;
			cin>>x;
			a[i]<<=1;
			if(x=='H')a[i]+=1;
		}
	}
	for(int i=0;i<(1<<m);i++){
		sum[i]=getsum(i);
	}
	for(int S=0;S<(1<<m);S++){
		if(!(S&a[1]||(S&(S<<1))||(S&(S<<2)))){
			f[1][S][1]=sum[S];
		}
	}
	for(int l=0;l<(1<<m);l++){
		for(int s=0;s<(1<<m);s++){
			if(!(l&s||(l&a[1])||(s&a[2])||(l&(l<<1))||(l&(l<<2))||(s&(s<<1))||(s&(s<<2))))
				f[l][s][2]=sum[s]+sum[l];
		}
	}
	for(int i=3;i<=n;i++){
		for(int fl=0;fl<(1<<m);fl++){
			if((fl&a[i-2])||(fl&(fl<<1))||(fl&(fl<<2)))continue;
			for(int l=0;l<(1<<m);l++){
				if(l&a[i-1]||(l&fl)||(l&(l<<1))||(l&(l<<2)))continue;
				for(int s=0;s<(1<<m);s++){
					if(s&l||(s&fl)||(s&a[i])||(s&(s<<1))||(s&(s<<2)))continue;
						f[l][s][i%3]=max(f[l][s][i%3],f[fl][l][(i-1)%3]+sum[s]);
				}
			}
		}
	}
	for(int l=0;l<(1<<m);l++){
		for(int s=0;s<(1<<m);s++)
			ans=max(ans,f[l][s][n%3]);
	}
	cout<<ans;
	return 0;
} 

吃奶酪

这也是一类状态压缩DP题目

思路

根据题意,必须经过每个节点有且只有一次,显然走过的路程是一条链,而不是一棵树,所以没法用最小生成树做!!
接下来考虑状态,每次吃一块奶酪,对总路程的贡献是与他上一次吃的奶酪所连边的长度,所以显然我们需要一维数组来记录他这一次吃的是哪块奶酪,同时我们还需要记录已经有哪些节点已经在这条链上,也就是哪些奶酪已经被吃过了,于是状态就出来了.\(f[i][j]\)表示当前吃的是第\(i\)块奶酪,已经吃的奶酪为\(j\)(这里用到了状态压缩)。

状态转移方程

转移方程是非常显然的,既然我们已经知道当前吃的是\(i\)块奶酪,吃过的奶酪为\(k\),那么我们就只用枚举上一次吃的\(j\)块奶酪,在加上\(i,j\)之间的边长\(d[i][j]\)就好了.

\[f[i][k]=min(f[i][k],f[j][k^(1<<(i-1))]+d[j][i]) \]

\(code\)

注意!!!! 在转移时因为\(i,j\)并不具有单调性,而\(k\)因为是将第\(i\)位从1变成\(0\),所以具有单调性,因此要将\(k\)放到循环的最外层

 #include<bits/stdc++.h>
 #define maxn 16
 using namespace std;
 struct node{
    double x,y;
 }a[maxn];
 int n;
 double f[maxn][1<<16],d[maxn][maxn];
 double calc(int x,int y){
    double dx=a[x].x-a[y].x;
    double dy=a[x].y-a[y].y;
    return sqrt(dx*dx+dy*dy);
 }
 int main(){
    cin>>n;
    a[0].x=a[0].y=0;
    for(int i=1;i<=n;i++){
        cin>>a[i].x>>a[i].y;
        d[0][i]=calc(0,i);
        d[i][0]=d[0][i];
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            d[i][j]=calc(i,j);
        }
    }
    memset(f,127,sizeof(f));
    double ans=f[0][0];
    for(int i=1;i<=n;i++){
        f[i][1<<(i-1)]=d[0][i];
    }
    for(int i=1;i<(1<<n);i++){
        for(int j=1;j<=n;j++){
            if(!(i&(1<<(j-1))))continue;
            for(int k=1;k<=n;k++){
                if(k==j)continue;
                if(!(i&(1<<(k-1))))continue;
                f[j][i]=min(f[j][i],f[k][i-(1<<(j-1))]+d[k][j]);
                //cout<<f[j][i]<<endl;
            }
        }
    }
    // for(int i=1;i<=n;i++){
    //     for(int j=1;j<(1<<n);j++){
    //         cerr<<f[i][j]<<endl;
    //     }
    // }
    for(int i=1;i<=n;i++){
        ans=min(ans,f[i][(1<<n)-1]);
    }
    printf("%.2lf",ans);
    return 0;
 }
posted @ 2024-09-12 19:25  SDFZ_GuoSN  阅读(28)  评论(0编辑  收藏  举报