状态压缩,如名,将一些在程序中难于表示但却易于表示(?!不要急,往下看)的状态通过数组进行存储,往往与DP配合食用
常用状压方法:二进制法
对于大部分状压题,通常只有1和0两种状态,所以可以将
f
数组表示为:
其中 kn 为1或0
如果如此做法,在时间复杂度和空间复杂度上与状压后的DP没有太大区别
至于代码复杂度嘛…
所以才说这种状态“难于 表示 却 易于 表示”
于是我们引入了状压,在状压之后,只需要一个数组就可以存储按照之前方式多个数组存储的状态信息,具体实现则是通过:
根据二进制,将多个数组压缩到一个数组的不同位上,形成形如“10101001”的数组,但却用十进制数组存储,既不占用额外空间,又不跑额外时间,比如原来的 f[1][0][1][1] 首先被转成 f[1011] ,最终成为 f[11] (20+21+23=11)
状压的好处
特别是对于二进制法构造的状压,对于判断某元素是否存在只需要 & 一遍就可以找到对应位,在状态转移过程对存储数组的加减中也可以用 | 、 ^ 来代替原本的 + 、- ,以取得更快的运行速度
状压的题型特征也非常明显,由于数组大小限制,导致状压类的题目数据非常小,通常一个二进制状压中 n 的大小不超过18,因此通过细致观察数据范围也可以推测题目是否可能为状压题目,也可以通过题意自己分析找出规律
由于状压DP书写方法较多,不同的压缩方式及对应转移方程都可能得到不同的时间复杂度及空间复杂度,这成为是否A题的关键,需要多练习,找感觉,并认真思考
某些例题
T1 单词游戏(soj125)
题目描述
Io和Ao在玩一个单词游戏。
他们轮流说出一个仅包含元音字母的单词,并且后一个单词的第一个字母必须与前一单词的最后一个字母一致。游戏可以从任何一个单词开始。任何单词禁止说两遍,游戏中只能使用给定词典中含有的单词。游戏的复杂度定义为游戏中所使用的单词长度总和。
编写程序,求出使用一本给定的词典来玩这个游戏所能达到的游戏最大可能复杂度。
输入格式
输入文件的第一行,表示一个自然数 N(1≤N≤16),N 表示一本字典中包含的单词数量,以下的每一行包含字典中的一个单词,每一个单词是由字母 A、E、I、O 和 U 组成的一个字符串,每个单词的长度将小于等于 100,所有的单词是不一样的。
输出格式
输出文件仅有一行,表示该游戏的最大可能复杂度。
样例数据 1
输入
5
IOO
IUUO
AI
OIOOI
AOOI
输出
16
备注
【样例说明】
length=16
解析:
感觉是非常直白的状压DP+01背包,也是很经典的入门题,不需要怎么多说
想了想还是贴个码吧
#include<bits/stdc++.h>
#define ll long long
#define m(a) memset(a,0,sizeof(a))
using namespace std;
int read(){
int x=0,f=1;char c;
for(c=getchar();!isdigit(c);c=getchar())if(c=='-')f=-1;
for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+c-'0';
return x*f;
}
int n,maxx=0;
char s[1005];
int len[205],h[205],t[205];
int f[200005],ed[200005];
int main()
{
m(f);
n=read();
for(int i=0;i<n;i++){
scanf("%s",s);
len[i]=strlen(s);
h[i]=s[0]-'A';t[i]=s[len[i]-1]-'A';
f[1<<i]=len[i];ed[1<<i]=t[i];
maxx=max(maxx,f[1<<i]);
}
for(int i=1;i<(1<<16);i++){
if(!f[i]) continue;
for(int j=0;j<n;j++){
if((1<<j)&i)continue;
if(h[j]!=ed[i])continue;
if(len[j]+f[i]>f[i|(1<<j)]){
f[i|(1<<j)]=len[j]+f[i];
ed[i|(1<<j)]=t[j];
maxx=max(maxx,f[i|(1<<j)]);
}
}
}
cout<<maxx;
}
T2 牧场的安排(poj3254)
题目描述
Farmer John 新买了一块长方形的牧场,这块牧场被划分成 N 行 M 列 (1<=M<=12;1<=N<=12) ,每一格都是一块正方形的土地。
FJ 打算在牧场上的某几格土地里种上美味的草,供他的奶牛们享用。遗憾的是,有些土地相当的贫瘠,不能用来放牧。并且,奶牛们喜欢独占一块草地的感觉,于是 FJ 不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。当然,FJ 还没有决定在哪些土地上种草。
作为一个好奇的农场主,FJ 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择。当然,把新的牧场荒废,不在任何土地上种草,也算一种方案。请你帮 FJ 算一下这个总方案数。
输入格式
第 1 行: 两个正整数 N 和 M ,用空格隔开
第 2..N+1 行: 每行包含 M 个用空格隔开的整数,描述了每块土地的状态。输入的第 i+1 行描述了第 i 行的土地。所有整数均为 0 或 1 ,是 1 的话,表示这块土地足够肥沃,0 则表示这块地上不适合种草。
输出格式
输出一个整数,即牧场分配总方案数除以 100,000,000 的余数。
样例数据 1
输入
2 3
1 1 1
0 1 0
输出
9
备注
【样例说明】
土地情况如下:
1 1 1
0 1 0
按下图把肥沃的各块土地编号:
1 2 3
0 4 0
只开辟一块草地的话,有 4 种方案:选 1、2、3、4 中的任一块。
开辟两块草地的话,有 3 种方案:13、14 以及 34。
选三块草地只有一种方案:1、3、4。
再加把牧场荒废的那一种,总方案数为 4+3+1+1=9 种。
【数据范围】
对于 50% 的数据,满足1≤N≤5;1≤M≤6
对于 100% 的数据,满足1≤N≤12;1≤M≤12。
解析
也是一道经典状压题,开个
f[i][k]
表示当前位于第
i
列,采用对于每行的第
#include<bits/stdc++.h>
using namespace std;
int read()
{
int x=0,f=1;char c;
for(c=getchar();!isdigit(c);c=getchar())if(c=='-')f=-1;
for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+c-'0';
return x*f;
}
#define mod 100000000
int n,m,x;
long long ans=0;
int tot=1;
long long f[20][10005];int plan[10005];
int ld[20];
bool fit(int x,int i){
if(x&i){
return 0;
}
return 1;
}
void predo(int x,int ff){
if(x>=0)plan[++tot]=ff;
for(int i=x+2;i<=m;i++){
predo(i,ff+(1<<i));
}
}
int main(){
memset(plan,0,sizeof(plan));
memset(f,0,sizeof(f));
n=read();m=read();
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
x=read();x^=1;
ld[i]+=(x<<j);
}
// cout<<ld[i]<<endl;
}
predo(-1,0);
for(int i=1;i<=tot;i++){
if(fit(ld[1],plan[i])){
f[1][i]=1;
}
}
for(int i=2;i<=n;i++){
for(int j=1;j<=tot;j++){
for(int k=1;k<=tot;k++){
if(f[i-1][j]){
if(fit(ld[i],plan[k])){
if(fit(plan[j],plan[k])){
f[i][k]=(f[i][k]+f[i-1][j])%mod;
}
}
}
}
}
}
// for(int i=1;i<=tot;i++)cout<<plan[i]<<" ";cout<<'\n';
for(int i=1;i<=tot;i++){
ans=(ans+f[n][i])%mod;
}
cout<<ans;
}