状态压缩模型
#写在前面
状态压缩分为两大类
- 棋盘式(基于连通性)
- 集合
#递归实现指数型枚举
https://www.acwing.com/problem/content/94/
状态压缩就是用一个二进制数来表示某一位上的东西取还是不取
9-------1001
表示第一位取,第2、3位不取,第4位取
当我们从 1----0001 枚举到 15----1111
会发现所有的取法都被遍历到了
----c++版
#include<iostream>
using namespace std;
int n;
void dfs(int u, int state){
if(u==n){
for(int i=0;i<n;i++)//将这个状态,的二进制数,如010,按照1不用,用2,3不用打印出来
if(state>>i&1)//看看二进制第i位是不是1,是就打印出来
cout<<i+1<<' ';//由于我们从第0位开始看的,所以要加一
cout<<endl;
return;
}
dfs(u+1, state);//不使用第u位的数字(毕竟一开始默认是0)
dfs(u+1, state|1<<u);//将第u位设置为1,表示使用第u位的数字
}
int main(){
cin>>n;
dfs(0,0);
return 0;
}
#棋盘式
##蒙德里安的梦想
https://www.acwing.com/problem/content/293/
----c++版
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//经典状态压缩
//摆完所有横向的1x2纸块,纵向的纸块只有1种摆法,所以只考虑横向先
//f[i,j]集合:第i列,j存储上一列有哪些横着摆的纸挡住了下一列的空格。
// j是个二进制数,0表示没挡住,1表示挡住了
//属性:个数
//计算:f[i,j]+=f[i-1,k];
const int N=12, M=1<<N;
int n,m;
long long f[N][M];
bool st[M];
int main(){
while(cin>>n>>m, n||m){
memset(f, 0, sizeof f);
//对于每一列,其各种合法的状态集合都是一样的,
//合法指列里余下的空格可以摆放竖着的1x2纸片
//所以可以预处理出来,用st来存合法的状态
for(int i=0;i<1<<n;i++){//枚举 列 可能的所有状态
st[i]=true;
int cnt=0;
for(int j=0;j<n;j++)//枚举列的每一行
if(i>>j&1){//如果当前是1,看看连续的0是否有奇数个
if(cnt&1)st[i]=false;//奇数个0的状态是不合法的
cnt=0;
}else cnt++;
if(cnt&1)st[i]=false;
}
f[0][0]=1;//第0列只能竖着放,只有一种方案
for(int i=1;i<=m;i++)//枚举每一列
for(int j=0;j<1<<n;j++)//枚举列可能的所有状态
for(int k=0;k<1<<n;k++)//枚举上一列可能的所有状态
if((j&k)==0&&st[j|k])//j与k不能有冲突,且该状态合法
f[i][j]+=f[i-1][k];//转移
cout<<f[m][0]<<endl;
}
return 0;
}
##小国王
https://www.acwing.com/problem/content/1066/
----c++版
#include<iostream>
#include<algorithm>
using namespace std;
//当我们在模拟放国王的时候,我们会发现:
//一行一行放置时,每一行的状态只与上一行的状态有关
//上上一行没有影响了
//集合:f[i,j,s]:前i行已经放完了,并且当前已经放了j个棋子,且第i行状态是s的方案集合
//属性;数量
//计算:
#include<cstring>
#include<vector>
const int N=12,M=1<<10,K=110;
typedef long long ll;
int n,m;
vector<int> state;
int id[M], cnt[M];//cnt每个状态里1的个数
vector<int> head[M];//每一个状态可以转移到的其他状态
ll f[N][K][M];
bool check(int state){
for(int i=0;i<n;i++)
if((state>>i&1)&&(state>>i+1&1))//不能有相邻的1,就是一行的一个合法的状态
return false;
return true;
}
int count(int state){
int res=0;
for(int i=0;i<n;i++)res+=state>>i&1;
return res;
}
int main(){
cin>>n>>m;
//一般来说最好先算一下
for(int i=0;i<1<<n;i++)
if(check(i)){
state.push_back(i);
id[i]=state.size()-1;//id存合法状态i的下标是多少
cnt[i]=count(i);//cnt存状态i里有几个1
}
//预处理一下哪些状态可以相互转移
for(int i=0;i<state.size();i++)
for(int j=0;j<state.size();j++){
int a=state[i],b=state[j];
if((a&b)==0&& check(a|b))//国王不在同一列上,且两个状态的并不能有连续的1
head[i].push_back(j);
}
//dp
f[0][0][0]=1;
for(int i=1; i <= n+1 ;i++)//+1省去了最后的枚举,所以一开始开到了12
for(int j=0;j<=m;j++)
for(int a=0;a<state.size();a++)
for(int b: head[a]){//这里a、b是状态的下标
int c=cnt[state[a]];
if(j>=c){
f[i][j][a]+=f[i-1][j-c][b];
}
}
cout<<f[n+1][m][0]<<endl;
return 0;
}
##玉米田
https://www.acwing.com/problem/content/329/
----c++版
#include<iostream>
#include<algorithm>
using namespace std;
//也是只与上一行的状态有关
#include<vector>
const int N=14,M=1<<12,mod=1e8;
int n,m;
vector<int> state;
int g[N];
vector<int> head[M];
int f[N][M];
bool check(int state){
for(int i=0;i<m;i++)
if((state>>i&1)&&(state>>i+1&1))//不能有相邻的1
return false;
return true;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=0;j<m;j++){
int t;
cin>>t;//二进制存地图
g[i]+=!t<<j;//是0的地方设置成1,方便做与运算
}
//预处理状态
for(int i=0;i<1<<m;i++)
if(check(i))
state.push_back(i);
//预处理能够互相转移的状态
for(int i=0; i<state.size();i++)
for(int j=0;j<state.size();j++){
int a=state[i],b=state[j];
if((a&b)==0)
head[i].push_back(j);
}
f[0][0]=1;
for(int i=1;i<=n+1;i++)
for(int a=0;a<state.size();a++)
for(int b:head[a]){
if(g[i]&state[a])continue;
f[i][a] = (f[i][a]+f[i-1][b])%mod;
}
cout<<f[n+1][0]<<endl;
return 0;
}
##炮兵阵地
https://www.acwing.com/problem/content/294/
----c++版
#include<iostream>
#include<algorithm>
using namespace std;
//由于空间限制比较小,所以要用滚动数组
#include<cstring>
#include<vector>
const int N=11,M=1<<10;
int n,m;
int g[110];
vector<int> state;
int f[2][M][M];//不然要开一个亿的数组
int cnt[N];
bool check(int state){
for(int i=0;i<m;i++)
if((state>>i&1)&&(state>>i+1&1)|((state>>i+2&1)))
return false;
return true;
}
int count(int state){
int res=0;
for(int i=0;i<m;i++)res+=state>>i&1;
return res;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=0;j<m;j++){
char c;
cin>>c;
if(c=='H')g[i]+=1<<j;
}
for(int i=0;i<1<<m;i++)
if(check(i)){
state.push_back(i);
cnt[i]=count(i);
}
for(int i=1;i<=n+2;i++)
for(int j=0;j<state.size();j++)//第i-1行的状态
for(int k=0;k<state.size();k++)//第i行的状态
for(int u=0;u<state.size();u++){//倒数第二行的状态
int a=state[j], b=state[k], c=state[u];
if((a&b)|(b&c)|(a&c))continue;
if(g[i-1]&a|g[i]&b)continue;
f[i&1][j][k]=max(f[i&1][j][k], f[i-1&1][u][j]+cnt[b]); //注意与1,滚动数组
}
cout<<f[n+2&1][0][0]<<endl;
return 0;
}
#集合式
##最短hamilton路径
https://www.acwing.com/problem/content/93/
----c++版
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//集合:f[i,j]所有从0走到j,走过的所有点是i的所有路径
// i是个压缩状态,存一个二进制数,表示点是否走过
//属性:min
//计算:用倒数第二个点来分类, 保证从0到倒数第二个点的不含倒数第一个点的所有路线中取最短
const int N=20,M=1<<N;
int n;
int w[N][N];
int f[M][N];
int main(){
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(f, 0x3f, sizeof f);
f[1][0]=0;
for(int i=0;i<1<<n;i++)
for(int j=0;j<n;j++)
if(i>>j&1)//从0走到j,i中必须包含j
for(int k=0;k<n;k++)
if((i-(1<<j))>>k&1)//除去j后,i中还得有第k个点才能保证状态的转移
f[i][j]=min(f[i][j], f[i-(1<<j)][k]+w[k][j]);
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
##愤怒的小鸟
https://www.acwing.com/problem/content/526/
爆搜的方式
----c++版
#include<iostream>
#include<algorithm>
using namespace std;
//经典的重复覆盖问题
//只需要两个点,即可确定此抛物线
//预处理出所有抛物线,最多n^2条
//并预处理出每条抛物线能够覆盖哪些点
//
//给定很多抛物线,最少选几条能覆盖所有点
//重复覆盖模型
//dancing links是最优解
//这里用状态压缩
#include<cstring>
#define x first
#define y second
const int N=18,M=1<<18;
typedef pair<double, double> pdd;
int n,m;
pdd q[N];
int path[N][N];
int f[M];
//注意浮点数比较
#include<cmath>
const double esp=1e-8;
int cmp(double x, double y){
if(fabs(x-y)<esp)return 0;
if(x<y) return -1;
return 1;
}
int main(){
int T;
cin>>T;
while(T--){
cin>>n>>m;
for(int i=0;i<n;i++)cin>>q[i].x>>q[i].y;
//利用现有的所有点,找出所有穿过两个点的抛物线
memset(path, 0, sizeof path);//path表示穿过i号点,j号点的抛物线能够覆盖哪些点
for(int i=0;i<n;i++){
path[i][i]=1<<i;//肯定能覆盖i号点
for(int j=0;j<n;j++){
double x1=q[i].x, y1=q[i].y;
double x2=q[j].x, y2=q[j].y;
if(!cmp(x1,x2))continue;//两个点不能在同一列上
double a=(y1/x1-y2/x2)/(x1-x2);
double b=y1/x1-a*x1;
if(cmp(a,0)>=0)continue;
int state=0;
//哪些点会被当前抛物线覆盖
for(int k=0;k<n;k++){
double x=q[k].x, y=q[k].y;
if(!cmp(a*x*x+b*x, y))state+=1<<k;
}
path[i][j]=state;//state:穿过i号点和j号点的抛物线能覆盖的点的状态
}
}
memset(f, 0x3f, sizeof f);
f[0]=0;//0这个状态不需要抛物线
for(int i=0;i+1<1<<n;i++){//枚举所有状态 +1是因为当我们包含所有列就不用更新了
int x=0;//x找没有被覆盖掉的列(没有被覆盖的点)
for(int j=0;j<n;j++)
if(!(i>>j&1)){//如果二进制第i-j位上是0
x=j;
break;
}
//枚举所有能覆盖x的抛物线 dp
for(int j=0;j<n;j++)
f[i|path[x][j]]=min(f[i|path[x][j]], f[i]+1);
}
cout<<f[(1<<n)-1]<<endl;
}
return 0;
}