文章目录
- 简述
- [P7859 [COCI2015-2016#2] GEPPETTO](https://www.luogu.com.cn/problem/P7859)
- 蓝桥杯2021省赛A组E题 回路计数
- [P4802 [CCO 2015]路短最](https://www.luogu.com.cn/problem/P4802)
- [P1433 吃奶酪](https://www.luogu.com.cn/problem/P1433)
- [P5911 [POI2004]PRZ](https://www.luogu.com.cn/problem/P5911)
- [P2622 关灯问题II](https://www.luogu.com.cn/problem/P2622)
- [P1896 [SCOI2005]互不侵犯](https://www.luogu.com.cn/problem/P1896)
- [P1879 [USACO06NOV]Corn Fields G](https://www.luogu.com.cn/problem/P1879)
- 附:一只题单
简述
状态压缩的作用就是把 S S S编成数字,比如给 { 3 } \{3\} {3}编号 4 4 4,给 { 1 , 2 , 4 } \{1,2,4\} {1,2,4}编号 11 11 11等,便于存储集合信息。编码规则是 S = { x 1 , x 2 , … , x t } S=\{x_1,x_2,\dots,x_t\} S={x1,x2,…,xt}则编码为 ∑ i = 1 t 2 x i − 1 \sum_{i=1}^t2^{x_i-1} ∑i=1t2xi−1。可以证明这种编码不同的 S S S之间没有冲突(这是最基本的要求);且用编码极易还原 S S S;更进一步,按编码顺序访问 S S S,则转移一定是从编码小的 S 1 S_1 S1到编码大的 S 2 S_2 S2。
在编码和集合 S S S之间的转换用位运算更加高效。首先,位运算符的优先级低于加减(就如加减低于乘除一样)(因此优先级也低于逻辑运算符与(&&)或(||)但高于大于小于, w h y why why建议能加括号就加,不要干些不靠谱的活计 :):左移(<<),右移(>>),按位与(&),按位或(|),按位异或(^)。
取一个二进制数 n n n(其实就是普通的一个整数 n n n)的从右向左 k k k位的方法是(n>>k)&1,类比取十进制数的方法是(n/(10^k))%10一样。
状态压缩题目典型的特征就是数据范围很小,一般都在20左右。
P7859 [COCI2015-2016#2] GEPPETTO
直接枚举所有状态,判断该状态下包含的所有成员中是否任意两个都不冲突即可。
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long a[410],b[410],f[1<<20];
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
cin>>a[i]>>b[i];
int cnt=0;
for(int i=0;i<(1<<n);i++)
{
int flag=1;
for(int j=0;j<m;j++)
{
if((i>>(a[j]-1)&1)&&(i>>(b[j]-1)&1))
{
flag=0;
break;
}
}
if(flag) cnt++;
}
cout<<cnt;
return 0;
}
蓝桥杯2021省赛A组E题 回路计数
(有21个点,1-21,编号互质的两点之间可通行,现从编号为1的点出发,问有多少种哈密顿回路的走法)
定义状态 f ( S , x ) f(S,x) f(S,x):不重复经过点集 S S S,当前所在点为 x x x的方案数;
状态转移:对状态 f ( S , x ) f(S,x) f(S,x),对任意 y ∉ S y \notin S y∈/S且 x x x, y y y之间有连边的 y y y,可以转移到 f ( S ⋃ y , y ) f(S\bigcup y,y) f(S⋃y,y)。(换一句话说,前一个状态是达成后一个状态的策略之一);
#include<bits/stdc++.h>
using namespace std;
const int N=21,M=1<<N;
long long road[N][N],f[M][N];
int main()
{
for(int i=0;i<N;i++)
for(int j=0;j<N;j++)
if(__gcd(i+1,j+1)==1)
road[i][j]=1;
f[1][0]=1;//集合为{1},停留在点1(在数组中处理将其减一故第二维为0)
for(int i=0;i<(1<<N);i++)//集合状态
for(int j=0;j<N;j++)//走过当前集合且最后停留在点j
if(i>>j&1)//看j点有没有到(在不在编码i代表的集合S中),这里为真代表在
for(int k=0;k<N;k++)//要往点k走
if(!(i>>k&1))//k不在S中
if(road[j][k])
f[i|(1<<k)][k]+=f[i][j];
long long ans=0;
for(int i=0;i<N;i++)
ans+=f[(1<<N)-1][i];//全集对应的编码为(1<<N)-1,这里i全循环一遍是因为教学楼1和所有教学楼都连边
cout<<ans<<endl;
return 0;
}
P4802 [CCO 2015]路短最
#include<bits/stdc++.h>
using namespace std;
int n,m;
int mp[20][20];
int f[1<<18][20];
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int from,to,w;
scanf("%d%d%d",&from,&to,&w);
mp[from][to]=w;
}
memset(f,0x8f,sizeof(f));
f[1][0]=0;
for(int i=3;i<(1<<n);i+=2)//集合状态
for(int j=0;j<n;j++)//走过当前集合且最后停留在点j
if(i>>j&1)//看j点有没有到(在不在编码i代表的集合S中),这里为真代表在
for(int k=1;k<n;k++)//要往点k走
if(((i>>k)&1)&&mp[j][k])
f[i][k]=max(f[i][k],f[i-(1<<k)][j]+mp[j][k]);
int ans=0;
for(int i=(1<<(n-1))+1;i<(1<<n);i+=2)//老老实实加括号~~~我debug1h的教训QAQ
ans=max(ans,f[i][n-1]);
cout<<ans;
return 0;
}
P1433 吃奶酪
跟上上一题类似。
#include<bits/stdc++.h>
using namespace std;
int n;
double f[1<<15][16],x[17],y[17];
double dis(int a,int b)
{
return sqrt((x[a]-x[b])*(x[a]-x[b])+(y[a]-y[b])*(y[a]-y[b]));
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
cin>>x[i]>>y[i];
memset(f,127,sizeof(f));
for(int i=0;i<n;i++) f[1<<i][i]=0;
for(int i=0;i<1<<n;i++)
{
for(int j=0;j<n;j++)
{
if((i>>j)&1)
{
for(int k=0;k<n;k++)
{
if(!(i>>k&1||j==k))
f[i|(1<<k)][k]=min(f[i|(1<<k)][k],f[i][j]+dis(j,k));
}
}
}
}
double ans=0x3ffffff;
for(int i=0;i<n;i++)
{
f[(1<<n)-1][i]+=dis(i,17);
ans=min(ans,f[(1<<n)-1][i]);
}
printf("%.2lf",ans);
return 0;
}
P5911 [POI2004]PRZ
首先要知道怎么枚举一个集合的子集:
for (int x = S; x; x = (x-1)&S)
若S=1011,则x分别为:1011, 1010, 1001, 1000, 0011, 0010, 0001。
观察过程:
#include<bits/stdc++.h>
using namespace std;
#define inf 0x3ffffff
#define MAXN (1<<16)+1
int w, n, c[17], t[17];
int T[MAXN], C[MAXN], f[MAXN];
int main()
{
scanf("%d %d",&w,&n);
for (int i=0;i<n;i++)
scanf("%d %d",&t[i],&c[i]);
for (int i=1;i<(1<<n);i++)
{
for(int j=0;j<n;j++)
{
if((1<<j)&i)
{
C[i]+=c[j];
T[i]=max(T[i],t[j]);
}
}
f[i]=inf;
}
for(int i=1;i<(1<<n);i++)
{
cout<<"母集合:";
int t=i;
while(t)
{
cout<<t%2;
t/=2;
}
cout<<endl;
for (int j = i; j >= 0; j = i & (j - 1))
{
cout<<" 子集:" ;
int temp1=j;
int temp2=i^j;
while(temp1)
{
cout<<temp1%2;
temp1/=2;
}
cout<<endl;
cout<<" 补集:";
while(temp2)
{
cout<<temp2%2;
temp2/=2;
}
cout<<endl;
cout<<" f[i]:" <<f[i]<<' '<<"f[j]:"<<f[j]<<" + "<<"T[i^j]"<<T[i^j]<<endl;
if (C[i ^ j] <= w) f[i] = min(f[i], f[j] + T[i ^ j]);
if (j == 0) break;
}
cout<<"最后f[i]:" <<f[i]<<endl<<endl;
}
printf("%d\n",f[(1<<n)-1]);
return 0;
}
最终代码:
#include<bits/stdc++.h>
using namespace std;
#define inf 0x3ffffff
#define MAXN (1<<16)+1
int w, n, c[17], t[17];
int T[MAXN], C[MAXN], f[MAXN];
//C[i]:当i这些人为一队时的总重量;
//T[i]:i些人为一队时里面最慢的那个人要的时间
int main()
{
scanf("%d %d",&w,&n);
for(int i=0;i<n;i++)
scanf("%d %d",&t[i],&c[i]);
for(int i=1;i<(1<<n);i++)//枚举所有的状态
{
for(int j=0;j<n;j++)//枚举每一位
{
if((1<<j)&i)//该状态选中了枚举的该位
{
C[i]+=c[j];//更新i状态的重量
T[i]=max(T[i],t[j]);//更新i状态的时间(所有成员所耗的时间中最大的)
}
}
f[i]=inf;//求最小值,初始化为无穷大
}
for(int i=1;i<(1<<n);i++)//枚举所有状态
{
for(int j=i;j>=0;j=i&(j-1)) //假设i为母集,则j为i的一个子集
{
if(C[i^j]<=w) f[i]=min(f[i],f[j]+T[i^j]);
//i^j可以得到i中除了j以外的成员(补集),如果补集算出的重量小于限额(T[i^j]==f[i^j] ) ,可以尝试更新f[i]
//也可以用下面这个状态转移方程,道理类似
//f(C[j]<=w) f[i]=min(f[i],f[i^j]+T[j]);
if(j==0) break;//这句必不可少,少了会挂掉
}
}
printf("%d\n",f[(1<<n)-1]);//f[(1<<n)-1]满员状态
return 0;
P2622 关灯问题II
#include<bits/stdc++.h>
using namespace std;
int f[1<<10],n,m,tab[110][11];
//f[状态]:达到该状态的最小步数
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
for(int j=0;j<n;j++)
scanf("%d",&tab[i][j]);
memset(f,0x3f,sizeof(f));
f[(1<<n)-1]=0; //0步达到全开状态
for(int i=(1<<n)-1;i>=0;i--)
{
for(int k=1;k<=m;k++)//枚举在该状态下按下的按钮,算出会达到的状态
{
int temp=i;
for(int j=0;j<n;j++)
{
if(tab[k][j]==-1&&!(i&(1<<j))) temp^=(1<<j);
if(tab[k][j]==1&&(i&(1<<j))) temp^=(1<<j);
}
f[temp]=min(f[temp],f[i]+1);//状态转移
}
}
if(f[0]==0x3f3f3f3f) cout<<-1;
else cout<<f[0];
return 0;
}
P1896 [SCOI2005]互不侵犯
#include<bits/stdc++.h>
using namespace std;
int N,K;
long long f[1<<9][10][85];
//三个状态参数分别为:每行的状态;行数;已经用掉的国王数,f用于记录方案数
//不开long long WAWA两声
int state[1000],num[1000];
//state:储存预处理出的合法状态数
//num:记录每个合法状态中1的个数
int main()
{
cin>>N>>K;
int cnt=0;
for(int s=0;s<1<<N;s++)//枚举行内的所有状态
{
if((s>>1|s<<1)&s) continue;//判断国王们是否相邻,如果相邻,行内就会起冲突,不合法
state[cnt++]=s;
int cnt2=0;
for(int i=0;i<N;i++)
if(s>>i&1) cnt2++;
num[s]=cnt2;
}//预处理出一些可能的排序,并记录这些排序中1的个数,这样不用每行都枚举所有状态并记录每种状态中1的个数
//for(int i=0;i<cnt;i++)
// cout<<state[i]<<' '<<num[state[i]]<<endl;
f[0][0][0]=1;//初始的第0行(会被第1行调用 ):一个国王都不放的方案数只有一种
for(int line=1;line<=N;line++)//枚举第一行到第n行
{
for(int i=0;i<cnt;i++)//枚举当前行的状态
{
for(int j=0;j<cnt;j++)//枚举前一行的状态
{
if((state[j]<<1|state[j]|state[j]>>1)&state[i]) continue;
//前一行有国王的位置的左下方、正下方、右下方不能安置国王
for(int p=0;p<=K;p++)//枚举前一行国王的个数,希望找到对应f[state[j]][line-1][p]的方案数
f[state[i]][line][p+num[state[i]]]+=f[state[j]][line-1][p];
//在当前行新增了num[state[i]个国王,状态转移
}
}
}
long long ans=0;
for(int i=0;i<cnt;i++)
ans+=f[state[i]][N][K];//求出到第n行时所有状态下符合条件的方案总数
cout<<ans;
return 0;
}
P1879 [USACO06NOV]Corn Fields G
#include<bits/stdc++.h>
using namespace std;
int n,m,num[15],mp[13][1<<12];
// mp[行标][状态编号]=状态
const int mod=100000000;
long long f[1<<12][13];
//f[状态][行标]=方案数
int state[1<<12],cnt;
int main()
{
cin>>m>>n;
for(int s=0;s<1<<n;s++)
{
if((s<<1)&s) continue;
state[s]=1;
}//预处理出合法状态(1的位置不相邻)
for(int i=1;i<=m;i++)
{
int temp=0;
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
temp<<=1;
temp+=x;
}
for(int j=temp;j;j=(j-1)&temp)//枚举temp的子集
if(state[j]) mp[i][num[i]++]=j;
mp[i][num[i]]=0;
}//预处理出每行的合法状态,第i行的合法状态有num[i]个
f[0][0]=1;//第0行全为0,这是一种方案
for(int line=1;line<=m;line++)//枚举行标
{
for(int i=num[line];i>=0;i--)//枚举当前行状态
{
for(int k=num[line-1];k>=0;k--)//枚举前一行状态
{
if(!(mp[line][i]&mp[line-1][k]))//当前行状态和前一行妆台在所有位置都不冲突
{
//cout<<"mp[line][i]:"<<mp[line][i]<<" mp[line-1][k]:"<<mp[line-1][k]<<endl;
//cout<<"f[mp["<<line-1<<"]["<<k<<"]]["<<line-1<<"]:"<<f[mp[line-1][k]][line-1]<<endl;
f[mp[line][i]][line]+=f[mp[line-1][k]][line-1];
f[mp[line][i]][line]%=mod;
}
}
//cout<<"f[mp["<<line<<"]["<<i<<"]]["<<line<<"]:"<<f[mp[line][i]][line]<<endl;
}
}
long long ans=0;
for(int i=0;i<=num[m];i++)//求和最后一行的所有行状态对应的方案数
ans=(ans+f[mp[m][i]][m])%mod;
cout<<ans;
return 0;
}