状压dp
引入
相信大家一定学过回溯。
在回溯的时候,我们有时候会遇到同一个状态需要访问多次的困境。
同时这些状态的计算又需要耗费我们大量的时间,显然,我们不想承担这样的后果。
于是,在遇到一个状态可以拆成子状态的情况下,我们引入状压dp
在需要以一些数据的状态作为一维来dp时,可以用状压dp。
简介
状压dp与常规dp不同的地方,在于它以二进制的方式在一维的空间里存下一些数据的状态。
举个例子,有1~n的数,我想知道当前哪些数用过了,我就用二进制来表示,这里用1表示用过。
如:1~5中,2和3用过,二进制就以01100来表示。
我们将其转换为十进制存进一维的状态里。
当然,我们并不需要让这个表示状态的数在两个进制里转来转去,这太麻烦了。
我们直接使用位运算。
(以下 位 均是从左到右表示)
位运算常用的基本操作
- 判断当前位是否为真
if((1<<(n-i))&s==(1<<(n-i))) 或 if((1<<(n-i))|s==s))
- 将当前位设置为真
s=s|(1<<(n-i));
- 将当前位设置为假
int is=1<<(n-i);
if(s&is==is) s=s^is;
例题
第1题 O - Matching
有n本不同的书,编号1至n。
有n个不同的书包,编号1至n。
冬令营开始了,有n个学生,每个学生将会获得1个书包和1本书。
给出二维数组a[1...n][1...n],如果a[i][j]=1表示第i本书和第j个书包是兼容的,
若a[i][j]=0表示第i本书和第j个书包是不兼容的。
每个学生收到的1个书包和1本书必须是兼容的。
n本书和n个书包之间,有多少种不同的匹配方案。
输入格式
第一行,一个整数n. 1<=n<=21。
接下来是n行n列的二维数组a。
输出格式
一个整数,答案模1000000007。
输入/输出例子1
输入:
3
0 1 1
1 0 1
1 1 1
输出:
3
输入/输出例子2
输入:
4
0 1 0 0
0 0 0 1
1 0 0 0
0 0 1 0
输出:
1
输入/输出例子3
输入:
1
0
输出:
0
参考程序:
#include<bits/stdc++.h>
using namespace std;
const int maxn=21;
const long long mod=1e9+7;
int n,is[maxn+5][maxn+5],U;
long long dp[maxn+5][(1<<maxn)+5];
bool vis[maxn+5][(1<<maxn)+5];
long long f(int now,int B) {
if(!B) return 1;
if(vis[now][B]) return dp[now][B];
vis[now][B]=true;
for(int i=1;i<=n;i++) {
int b=1<<(n-i);
if(((B&b)!=b)||(!is[now][i])) continue;
int nb=B^b;
dp[now][B]=(dp[now][B]+f(now-1,nb))%mod;
}
return dp[now][B];
}
int main() {
cin>>n;
U=(1<<n)-1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>is[i][j];
cout<<f(n,U);
return 0;
}
第2题 U - Grouping
有n只兔子,编号1至n。
给出二维数组a[1...n][1...n]其中a[i][j]表示第i只兔子和第j只兔子的兼容度,
数据保证a[i][i]=0, a[i][j] = a[j][i]。
现在你需要把这n只兔子分成若干组,使得每只兔子仅属于一个组。
当分组结束后,对于1<=i<j<=n,你将会获得a[i][j]元钱,前提是第i只兔子和第j只兔子分在了同一组。
应该如何分组,才能使得最终赚的钱最多。
输入格式
第一行,一个整数n。 1<=n<=16。
接下来是二维数组a, 其中 -1e9 <= a[i][j] <= 1e9。
输出格式
一个整数,最多能赚的钱。
输入/输出例子1
输入:
3
0 10 20
10 0 -100
20 -100 0
输出:
20
输入/输出例子2
输入:
2
0 -10
-10 0
输出:
0
输入/输出例子3
输入:
4
0 1000000000 1000000000 1000000000
1000000000 0 1000000000 1000000000
1000000000 1000000000 0 -1
1000000000 1000000000 -1 0
输出:
4999999999
参考程序
#include<bits/stdc++.h>
using namespace std;
const int maxn=16;
int n,a[maxn+5][maxn+5],U;
long long dp[(1<<maxn)+5],sum[(1<<maxn)+5];
bool vis[(1<<maxn)+5];
long long f(int B) {
if(!B) return 0;
if(vis[B]) return dp[B];
vis[B]=true;
dp[B]=0;
for(int i=B;i;i=B&(i-1))
dp[B]=max(dp[B],sum[i]+f(B^i));
return dp[B];
}
int main() {
cin>>n;
U=(1<<n)-1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>a[i][j];
for(int i=1;i<=U;i++)
for(int j=1;j<=n;j++)
if((i|(1<<(n-j)))==i)
for(int k=1;k<j;k++)
if((i|(1<<(n-k)))==i) sum[i]+=a[j][k];
cout<<f(U);
return 0;
}
第3题 开锁
有n把锁,编号1至n。有m把钥匙,第i把钥匙的价格是p[i],第i把钥匙可以开k[i]把锁,
分别可以开第c[i][1],c[i][2],...第c[k[i]]把锁。
问你如果购买钥匙,用最少的费用把n把锁全部打开。
如果无论无何也不能把n把锁全部打开,输出-1。
输入格式
第一行,两个整数n和m。1<=n<=12, 1<=m<=1000。
接下来是描述m把钥匙的信息,第i把钥匙有两行:
第1行,两个整数: p[i]和k[i]。1<=p[i]<=100000, 1<=k[i]<=n。
第2行,k[i]个整数,依次表似乎第i把钥匙所能打开的锁的编号,从小到大给出编号。
输出格式
一个整数。
输入/输出例子1
输入:
2 3
10 1
1
15 1
2
30 2
1 2
输出:
25
输入/输出例子2
输入:
12 1
100000 1
2
输出:
-1
输入/输出例子3
输入:
4 6
67786 3
1 3 4
3497 1
2
44908 3
2 3 4
2156 3
2 3 4
26230 1
2
86918 1
3
输出:
69942
#include<bits/stdc++.h>
using namespace std;
const int maxn=12,maxm=1000;
int n,m,cost[(1<<maxn)+5],U;
long long dp[(1<<maxn)+5];
bool vis[(1<<maxn)+5],al[(1<<maxn)+5];
long long f(int B) {
if(!B) return 0;
if(vis[B]) return dp[B];
vis[B]=true;
dp[B]=1e10;
for(int i=B;i;i=B&(i-1))
if(al[i]) dp[B]=min(dp[B],f(B^i)+cost[i]);
return dp[B];
}
int main() {
memset(cost,0x3f3f3f,sizeof(cost));
cin>>n>>m;
U=(1<<n)-1;
for(int i=1;i<=m;i++) {
int p,k;
cin>>p>>k;
int b=0;
while(k--) {
int a;
cin>>a;
b|=1<<(n-a);
}
for(int j=b;j;j=b&(j-1))
cost[j]=min(cost[j],p),al[j]=true;
}
if(f(U)<1e10) cout<<f(U);
else cout<<-1;
return 0;
}
第5题 不同排列
有n个学生,学号1至n。你现在需要把这n个学生从左往右排成一行形成队伍,要满足如下所有m个条件:
第i个条件的格式是x[i],y[i],z[i],表示队伍的前x[i]学生当中,学号小于y[i]的学生不能超过z[i]人。
求满足上面所有m个条件的队伍有多少种不同的方案。
输入格式
第一行,n和m。 2<=n<=18, 0<=m<=100。、
接下来m行,第i行是x[i],y[i],z[i]。1<=x[i],y[i]<n,0<=z[i]<n。
输出格式
一个整数。
输入/输出例子1
输入:
3 1
2 2 1
输出:
4
输入/输出例子2
输入:
5 2
3 3 2
4 4 3
输出:
90
输入/输出例子3
输入:
18 0
输出:
6402373705728000
参考程序
#include<bits/stdc++.h>
using namespace std;
const int maxn=18,maxm=100;
int n,m,cnt[(1<<maxn)+5],U;
long long dp[(1<<maxn)+5];
bool vis[(1<<maxn)+5];
struct node {
int y,z;
};
vector<node>limit[maxn+5];
vector<int>d[(1<<maxn)+5];
bool check(int s,int len) {
for(int i=0;i<limit[len].size();i++) {
int y=limit[len][i].y,z=limit[len][i].z,sum=0;
for(int j=0;j<len;j++)
if(d[s][j]<=y) sum++;
if(sum>z) return 0;
}
return 1;
}
long long f(int B) {
if(!B) return 1;
if(vis[B]) return dp[B];
vis[B]=true;
dp[B]=0;
if(!check(B,cnt[B])) return 0;
for(int i=1;i<=n;i++) {
int is=1<<(n-i);
if((B&is)!=is) continue;
dp[B]=dp[B]+f(B^is);
}
return dp[B];
}
int main() {
cin>>n>>m;
U=(1<<n)-1;
for(int i=1;i<=m;i++) {
int x,y,z;
cin>>x>>y>>z;
limit[x].push_back({y,z});
}
for(int i=1;i<=U;i++) {
for(int j=1;j<=n;j++) {
int is=1<<(n-j);
if((i&is)!=is) continue;
d[i].push_back(j);
}
cnt[i]=d[i].size();
}
cout<<f(U);
return 0;
}
第6题 序列
有两个序列:a[1...n]和b[1...n]。你需要把序列a变成序列b。
每次你可以对序列a进行如下两种操作之一:
1、选择一个下标i(1<=i<=n),你可以让a[i]减少1或者让a[i]增加1,本次费用为X。
2、选择一个下标i(1<=i<n),你可以交换a[i]和a[i+1]的值,本次费用为Y。
你可以进行任意多次上述操作,求最少的费用使得a变成b一样。
输入格式
第一行,三个整数:n,X,Y。2<=n<=18, 1<=X<=1e8, 1<=y<=1e16。
第二行,n个整数,第i个整数是a[i], 1<=a[i]<=1e8。
第三行,n个整数,第i个整数是b[i], 1<=b[i]<=1e8。
输出格式
一个整数。
输入/输出例子1
输入:
4 3 5
4 2 5 2
6 4 2 1
输出:
16
输入/输出例子2
输入:
5 12345 6789
1 2 3 4 5
1 2 3 4 5
输出:
0
参考程序
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=18;
int n,X,Y,a[maxn+5],b[maxn+5],U;
long long dp[(1<<maxn)+5];
bool vis[(1<<maxn)+5];
long long f(int now,int B) {
if(!B) return 0;
if(vis[B]) return dp[B];
vis[B]=true;
int cnt=0;
dp[B]=1e18;
for(int i=1;i<=n;i++) {
int is=1<<(n-i);
if((B&is)!=is) {
cnt++;
continue;
}
long long cost=abs(b[now]-a[i])*X+abs(now-i+cnt)*Y;
dp[B]=min(dp[B],f(now-1,B^is)+cost);
}
return dp[B];
}
signed main() {
cin>>n>>X>>Y;
U=(1<<n)-1;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
cin>>b[i];
cout<<f(n,U);
return 0;
}
第7题 接龙
有n个字符串,第i个字符串是S[i]。每个字符串都是由不超过10个小写英文字母构成。
现在A和B两人要玩字符串接龙游戏,A是先手。
每次当前玩家从剩下的字符串当中选中一个拿出来(不妨假设选中的是S[j]),
接龙到前面的字符串(不妨假设上一个被选出来的字符串是s[i]),
那么S[j]的首字母必须等于S[i]的末尾字母,这样才算接龙接得上。
如果轮到当前玩家了,而当前玩家却发现从剩下得字符串当中找不到能接得上龙得字符串,那么游戏结束,当前玩家输。
假设A和B都用最优策略玩游戏。
如果A能胜利输出"First",如果B能勝利輸出"Second"。
输入格式
第一行,一個整數n。 1<=n<=16。
接下來有n行,第i行是字符串S[i]。
输出格式
如果A能胜利输出"First",如果B能勝利輸出"Second"。
输入/输出例子1
输入:
6
enum
float
if
modint
takahashi
template
输出:
First
输入/输出例子2
输入:
10
catch
chokudai
class
continue
copy
exec
havoc
intrinsic
static
yucatec
输出:
Second
参考程序
#include<bits/stdc++.h>
using namespace std;
const int maxn=16;
int n,U;
string s[maxn+5];
bool dp[maxn+5][(1<<maxn)+5][2],vis[maxn+5][(1<<maxn)+5][2];
bool ans=0;
bool f(int now,int B,int q) {
if(!B) return true;
if(vis[now][B][q]) return dp[now][B][q];
vis[now][B][q]=true;
dp[now][B][q]=false;
bool Q=false;
for(int i=1;i<=n;i++) {
int is=1<<(n-i);
if((B&is)!=is||s[now][s[now].size()-1]!=s[i][0]) continue;
Q=true;
dp[now][B][q]|=!f(i,B^is,!q);
}
if(!Q) dp[now][B][q]=true;
return dp[now][B][q];
}
int main() {
cin>>n;
U=(1<<n)-1;
for(int i=1;i<=n;i++)
cin>>s[i];
for(int i=1;i<=n;i++)
ans|=f(i,U^(1<<(n-i)),1);
if(ans) cout<<"First";
else cout<<"Second";
return 0;
}
重点例题:铺地砖
有高为h,宽为w的二维表格,现在要用\({1*2}\) 或 \({2*1}\)的地砖将其铺满,有多少种不同方案?
输入格式
两个整数数字:高度h和大型矩形的宽度w。 1 < = h,w < = 16。
输出格式
一个整数,答案模1000000007。
输入/输出例子1
输入:
2 4
输出:
5
参考程序
#include<bits/stdc++.h>
using namespace std;
const int maxn=16;
const long long mod=1000000007;
int n,m,U;
long long dp[maxn+5][maxn+5][(1<<maxn)+5];//这里状压记录的是会影响当前行的数据,即当前行前面和上一行后面的数据
bool vis[maxn+5][maxn+5][(1<<maxn)+5];
bool in(int i,int j) {
return ((1<<(m-i))|j)==j;
}
int set0(int i,int s) {
int x=1<<(m-i);
if(in(i,s)) return s^x;
else return s;
}
int set1(int i,int s) {
int x=1<<(m-i);
return s|x;
}
long long f(int x,int y,int s) {
if(x>n) return 1;
if(y>m) return f(x+1,1,s);
if(vis[x][y][s]) return dp[x][y][s];
vis[x][y][s]=true;
if(!in(y,s)) {
if(x<n) dp[x][y][s]=f(x,y+1,set1(y,s));
if(y<m&&!in(y+1,s)) dp[x][y][s]=(dp[x][y][s]+f(x,y+1,set1(y+1,s)))%mod;
}
else dp[x][y][s]=f(x,y+1,set0(y,s));
return dp[x][y][s];
}
int main() {
cin>>n>>m;
if((n*m)%2==1) {
cout<<0;
return 0;
}
U=(1<<m)-1;
cout<<f(1,1,0);
return 0;
}