状压dp与普通dp不同的方面即为它把之前的各种状态用一个数字表示,通过这个数字二进制下各位是0还是1来表示这个状态下的各种情况(是否有放棋子之类的),然后通过枚举数字即可枚举之前的各种情况进行dp
举个用二进制表示状态的例子:有一个大小为n*n的农田,我们可以在任意处种田,现在来描述一下某一行的某种状态:
设n = 9,对于每一行
有二进制数 100011011(九位),每一位表示该农田是否被占用,1表示用了,0表示没用,这样一种状态就被我们表示出来了:见下表则 1+2+8+16+256即283代表了这一行的情况
所以本质上状压dp是一个非常暴力的算法,对于规模为n的题目它的时间复杂度是2^n次,n大的时候是会t的,所以一些题目可以依照题意,排除不合法的方案,使一行的总方案数大大减少从而减少枚举
位运算的技巧
1.判断一个数字x二进制下第i位是不是等于1。(最低第1位)
方法:if(((1<<(i−1))&x)>0) 将1左移i-1位,相当于制造了一个只有第i位 上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0, 说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x=x|(1<<(i−1)) 证明方法与1类似。
3.将一个数字x二进制下第i位更改成0。
方法:x=x&~(1<<(i−1)) ~号代表按位全部取反
4.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x−1)
板子
https://www.luogu.com.cn/problem/P1896
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=160;
int f[11][maxn][maxn];
int num[maxn],s[maxn];
int n,k,cnt;
void init(){ //预处理一下没有其他行限制下每一行的可能情况有多少种
cnt=0;
for(int i=0;i<(1<<n);i++){
if(i&(i<<1)){ // 代表左右有相邻国王
continue;
}
int sum=0;
for(int j=0;j<n;j++){ //枚举一下i这个情况下哪些地方是国王
if(i&(1<<j)){
sum++;
}
}
s[++cnt]=i; //s[cnt]代表了第cnt种情况下的状态
num[cnt]=sum;
}
// cout<<"cnt "<<cnt<<"\n";
}
void solve(){
cin>>n>>k;
init();
f[0][1][0]=1; //代表第0行在num[1]即放了0个国王的情况有1种
for(int i=1;i<=n;i++){ //枚举行
for(int j=1;j<=cnt;j++){ //枚举这一行有多少种情况
for(int l=0;l<=k;l++){ //枚举算上这一行的国王总数
if(l>=num[j]){ //算上这一行放的国王总数起码得大于等于这一行自己就有的国王个数
for(int t=1;t<=cnt;t++){ //枚举上一行的情况
//1.不能跟上一行有列重合 2.不能刚好差一行
if(!(s[t]&s[j])&&!(s[t]&(s[j]<<1))&&!(s[t]&(s[j]>>1))){
f[i][j][l]+=f[i-1][t][l-num[j]];
}
}
}
}
}
}
int ans=0;
for(int i=1;i<=cnt;i++){
ans+=f[n][i][k];
}
cout<<ans<<"\n";
}
signed main(){
int t;
t=1;
while(t--){
solve();
}
}
#include<bits/stdc++.h>
#define il inline
#define ll long long
#define RE register
#define For(i,a,b) for(RE int (i)=(a);(i)<=(b);(i)++)
#define Bor(i,a,b) for(RE int (i)=(b);(i)>=(a);(i)--)
using namespace std;
const int M=11;
int n,m,cnt,f[2][1<<M][1<<M],mp[105],w[1<<M];
bool sta[1<<M];
int main(){
ios::sync_with_stdio(0);
cin>>n>>m; char c; int lim=(1<<m)-1;
int num=0;
For(i,0,lim) {
if((i<<1)&i||(i<<2)&i) continue;
if((i>>1)&i||(i>>2)&i) continue;
For(j,0,m-1) if(i&(1<<j)) w[i]++;
sta[i]=1;
num++;
}
For(i,1,n) For(j,1,m) cin>>c,mp[i]+=(c=='P'?(1<<j-1):0);
For(i,1,n) {
For(j,0,lim) if(sta[j]&&((j&mp[i-1])==j)){
For(k,0,lim)
if(sta[k]&&(i==1||(k&mp[i-2])==k)&&(!(j&k)))
For(p,0,lim) if(sta[p]&&(!(p&k))&&(!(p&j))&&(p&mp[i])==p)
f[cnt^1][j][p]=max(f[cnt^1][j][p],f[cnt][k][j]+w[p]);
}
cnt^=1;
}
int ans=0;
For(i,0,lim) if(sta[i]&&(i&mp[n-1])==i)
For(j,0,lim) if(sta[j]&&(j&mp[n])==j&&!(i&j)){
//cout<<f[cnt][i][j]<<"\n";
ans=max(ans,f[cnt][i][j]);
}
cout<<ans;
return 0;
}
填方格模板:
前面f[i][j]代表到了前面i列在第i列放置情况为j,即这些位置横着放1*2方格的时候的可行方案个数,可以加上f[i-1][k]的要求是没有连续的空着的格子为奇数的情况并且i-1列和第i列不能在同一行放1*2的格子
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
const int maxn=1<<12;
int f[15][maxn];
int st[maxn];
vector<int>state[maxn];
void solve(){
int limit=1<<n;
int cnt=0;
for(int i=0;i<limit;i++){
int cnt=0;
int check=1;
for(int j=0;j<n;j++){
if(i&(1<<j)){
if(cnt&1){
check=0;
break;
}
else{
cnt=0;
}
}
else{
cnt++;
}
}
if(cnt&1){
check=0;
}
if(check==1){
cnt++;
}
//cout<<"cnt ";
//cout<<cnt<<"\n";
st[i]=check;
}
for(int i=0;i<limit;i++){
state[i].clear();
for(int j=0;j<limit;j++){
if(!(i&j)&&st[i|j]){
state[i].push_back(j);
}
}
}
memset(f,0,sizeof(f));
f[0][0]=1;
for(int i=1;i<=m;i++){
for(int j=0;j<limit;j++){
for(int k:state[j]){
f[i][j]+=f[i-1][k];
}
}
}
cout<<f[m][0]<<"\n";
}
signed main(){
while(cin>>n>>m){
if(!n&&!m){
return 0;
}
else{
solve();
}
}
}
f[i][j]代表从0开始走过了i这些点目前在j点时的最小花费
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=20,M=1<<N;
int f[M][N],w[N][N];//w表示的是无权图
int main()
{
int n;
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;//因为零是起点,所以f[1][0]=0;
for(int i=0;i<1<<n;i++)//i表示所有的情况
for(int j=0;j<n;j++)//j表示走到哪一个点
if(i>>j&1)
for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
if(i>>k&1)
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);//更新最短距离
cout<<f[(1<<n)-1][n-1]<<endl;//表示所有点都走过了,且终点是n-1的最短距离
//位运算的优先级低于'+'-'所以有必要的情况下要打括号
return 0;
}。
2022杭电多校 Boss Rush
题目描述:
给定n个技能和怪兽的血量,每个技能有一个冷却时间ti,发动该技能后在冷却时间内不能用任何技能,有一个伤害时间leni,发动该技能后的1-leni时间内,每秒会造成damage[i]的伤害,每个技能只能用一次,求打败怪兽需要的最少时间,其中n<=20
思路:
如果技能的发动顺序确定,那么这道题可以很容易的解决,但是要确定发动顺序的话为A(20,20),显然不能直接确定发动顺序,观察到n很小,如果用状压dp,考虑在x时间内,dp[i]为x时间内选用i这些技能所能造成的最大伤害,那么枚举没有选择的技能j,因为时间x已知,则dp[i+(1<<j)]也可以容易的求出来,并且这样枚举状态的同时也枚举了所有的发动顺序,二分x即可求出答案,时间复杂度为O(n*2^n*log(最大时间))
#include<bits/stdc++.h>
using namespace std;
const int INF=1e9+7;
long long dp[1<<18];
int qianlen[20];
int a[20];
int sumt[1<<18]; //当前状态的冷却时间总和
long long pre[18][100001];
long long h;
int n;
bool check(int mid){
for(int bit=1;bit<(1<<n);bit++){
dp[bit]=0;
}
for(int bit=0;bit<(1<<n);bit++){
if(dp[bit]>=h){
return true;
}
for(int j=0;j<n;j++){
if(!((bit>>j)&1)){ //没有这一位
int shenyu=mid-sumt[bit]+1;
shenyu=max(0,shenyu);
dp[bit+(1<<j)]=max(dp[bit|(1<<j)],dp[bit]+pre[j][min(qianlen[j],shenyu)]);
if(dp[bit+(1<<j)]>=h) return true;
}
}
}
return false;
}
void solve(){
scanf("%d %lld",&n,&h);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
int len;
scanf("%d",&len);
qianlen[i]=len;
for(int j=1;j<=len;j++){
scanf("%lld",&pre[i][j]);
pre[i][j]+=pre[i][j-1];
}
}
for(int bit=1;bit<(1<<n);bit++){
for(int i=0;i<n;i++){
if((bit>>i)&1) {
sumt[bit]=sumt[bit^(1<<i)]+a[i];
break;
}
}
}
int l=0,r=INF;
int ans=INF;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid)){
ans=min(ans,mid);
r=mid-1;
}
else l=mid+1;
}
if(ans==INF) printf("-1\n");
else printf("%d\n",ans);
return ;
}
int main(){
int t;
cin>>t;
while(t--){
solve();
}
}