状压DP练习详解
例题1:[BZOJ1072][SCOI2007]排列perm
题目描述:
给一个数字串s和正整数d, 统计s有多少种不同的排列能被d整除(可以有前导0)。例如123434有90种排列能
被2整除,其中末位为2的有30种,末位为4的有60种。
数据范围:
100 % 100\% 100%的数据满足: s s s的长度不超过 10 10 10, 1 ≤ d ≤ 1000 1\leq d\leq 1000 1≤d≤1000, 1 ≤ T ≤ 15 1\leq T\leq15 1≤T≤15
样例输入输出:
输入:
7
000 1
001 1
1234567890 1
123434 2
1234 7
12345 17
12345678 29
输出:
1
3
3628800
90
3
6
1398
思路:
看到数据范围这么小,考虑状态压缩
d
p
dp
dp
设
d
p
[
i
]
[
j
]
dp[\ i\ ][\ j\ ]
dp[ i ][ j ]
i
i
i是一个二进制位表示选了
s
s
s 中的某些位, 这些位为
1
1
1 的数字和
∑
s
[
i
]
≡
0
(
m
o
d
d
)
\sum s[\ i\ ]\equiv 0(\mod d)
∑s[ i ]≡0(modd)时的方案数
转移方程就是枚举未选择的数字:
d p [ i ∣ ( 1 < < k ) ] [ ( j × 10 + s [ k ] − 48 ) % d ] + = d p [ i ] [ j ] dp[\ i|(1<<k)\ ][\ (j\times 10+s[k]-48)\% d\ ]+=dp[\ i\ ][\ j\ ] dp[ i∣(1<<k) ][ (j×10+s[k]−48)%d ]+=dp[ i ][ j ]
#include<bits/stdc++.h>
#define int long long
#define mem(a) memset(a,0,sizeof(a))
#define set(a,b) memset(a,b,sizeof(a))
#define ls p<<1
#define rs p<<1|1
#define pb(x) push_back(x)
#define rand RAND
#define LOCAL
using namespace std;
template<class Typ> Typ &Rd(Typ &x){char ch=getchar(),sgn=0; x=0;for(;ch<'0'||ch>'9';ch=getchar()) sgn|=ch=='-';for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+(ch^48);return sgn&&(x=-x),x;}
template<class Typ> void Wt(Typ x){if(x<0) putchar('-'),x=-x;if(x>9) Wt(x/10);putchar(x%10^48);}
const int inf=0x3f3f3f3f3f;
const int maxn=200005;
int seed = 19243;
unsigned rand(){return seed=(seed*48271ll)%2147483647;}
int T;
int d;
string s;
int dp[(1<<10)+5][1005];
signed main(){
Rd(T);
while(T--){
cin>>s>>d;
int n=s.size();
mem(dp);
sort(s.begin(),s.end());
dp[0][0]=1;
for(int t=0;t<(1<<n);t++){
for(int i=0;i<=d-1;i++){
if(!dp[t][i])continue;
for(int j=0;j<n;j++){
if((1<<j)&t)continue;
if(j&&s[j]==s[j-1]&&(t&(1<<(j-1)))==0)continue;
dp[t|(1<<j)][(i*10+s[j]-'0')%d]+=dp[t][i];
}
}
}
Wt(dp[(1<<n)-1][0]),putchar('\n');
}
return 0;
}
/*
#ifdef LOCAL
freopen(,,stdin);
freopen(,,stdout);
#endif
*/
例题2:CF580D Kefa and Dishes
题目描述
kefa进入了一家餐厅,这家餐厅中有n个菜(0<n<=18),kefa对第i个菜的满意度为ai(0<=ai<=109),并且对于这n个菜有k个规则,如果kefa在吃完第xi个菜之后吃了第yi个菜(保证xi、yi不相等),那么会额外获得ci(0<=ci<=109)的满意度。kefa要吃m道任意的菜(0<m<=n),但是他希望自己吃菜的顺序得到的满意度最大,请你帮帮kefa吧!
输入第一行是三个数:n,m,k
第二行是n个数,第i个数表示kefa对第i道菜的满意度为ai
第三行到第k+2行每行有3个数:xi,yi,ci,表示如果kefa在吃完第xi道菜之后立刻吃了第yi道菜,则会额外获得ci的满意度
样例输入输出
输入:
4 3 2
1 2 3 4
2 1 5
3 4 2
输出:
12
思路:
处理不同菜肴的加成作用,只要在状态中记录下最后一个菜肴是一种即可。
定义 d p [ S ] [ i ] dp[S][i] dp[S][i] 表示状态为 S S S,最后吃的一道菜肴是 i i i 的最大快乐值。
枚举上一次吃的菜肴 j j j, 可以得到转移方程:
d p [ S ] [ i ] = m a x ( d p [ S ⊕ ( 1 < < i ) ] [ j ] + a [ i ] + c ( j , i ) ) c ( j , i ) 表示先吃 j 再吃 i 提供的快乐值。 dp[\ S\ ][\ i\ ] = max(dp[\ S \oplus (1 << i)\ ][\ j\ ] + a[\ i\ ] + c(j, i) ) \\c(j,i) 表示先吃j 再吃 i 提供的快乐值。 dp[ S ][ i ]=max(dp[ S⊕(1<<i) ][ j ]+a[ i ]+c(j,i))c(j,i)表示先吃j再吃i提供的快乐值。
时间复杂度 Θ ( N 2 × 2 N ) \Theta (N^2\times 2^N) Θ(N2×2N)
#include<bits/stdc++.h>
#define int long long
#define mem(a) memset(a,0,sizeof(a))
#define set(a,b) memset(a,b,sizeof(a))
#define ls p<<1
#define rs p<<1|1
#define pb(x) push_back(x)
#define rand RAND
#define LOCAL
using namespace std;
template<class Typ> Typ &Rd(Typ &x){char ch=getchar(),sgn=0; x=0;for(;ch<'0'||ch>'9';ch=getchar()) sgn|=ch=='-';for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+(ch^48);return sgn&&(x=-x),x;}
template<class Typ> void Wt(Typ x){if(x<0) putchar('-'),x=-x;if(x>9) Wt(x/10);putchar(x%10^48);}
const int inf=0x3f3f3f3f3f;
const int maxn=20;
int seed = 19243;
unsigned rand(){return seed=(seed*48271ll)%2147483647;}
int dp[(1<<maxn)][maxn],A[maxn],C[maxn][maxn],n,m,k;
signed main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++)cin>>A[i];
while(k--){
int x,y,c;
cin>>x>>y>>c;
C[x][y]=c;
}
int ans=0;
for(int S=0;S<(1<<n);S++){
int t=S,cnt=0;
while(t){
if(t&1)cnt++;
t>>=1;
}
if(cnt>m)continue;
for(int i=1;i<=n;i++){
if(!(S&(1<<(i-1))))continue;
for(int j=1;j<=n;j++){
if(!(S&(1<<(j-1))))continue;
dp[S][i]=max(dp[S][i],dp[S^(1<<(i-1))][j]+A[i]+C[j][i]);
}
ans=max(ans,dp[S][i]);
}
}
cout<<ans<<endl;
return 0;
}
/*
#ifdef LOCAL
freopen(,,stdin);
freopen(,,stdout);
#endif
*/
例题3:CF377C Captains Mode
题目描述:
有两个队在游戏Dota 2里选择英雄。有 n n n个英雄,每个英雄的力量为 s i s_i si。两个队伍一共要操作 m m m次。操作有两种:
操作’p’会选择一个英雄,并且获得该英雄的力量。选择这个英雄后,这个英雄就不能被任意队伍再次选择。
操作’b’会禁用一个英雄,禁用这个英雄后,这个英雄就不能被任意队伍再次选择。
操作时队伍可以选择跳过一次操作。
如果跳过’p’操作,一个随机的英雄将会被加入队伍,并且获得该英雄的力量。
如果跳过’b’操作,没有英雄将被禁用。
求第一个队伍的英雄力量之和减去第二个队伍的英雄力量之和的最大值。
样例 #1
样例输入 #1
2
2 1
2
p 1
p 2
样例输出 #1
1
样例 #2
样例输入 #2
6
6 4 5 4 5 5
4
b 2
p 1
b 1
p 2
样例输出 #2
0
样例 #3
样例输入 #3
4
1 2 3 4
4
p 2
b 2
p 1
b 1
样例输出 #3
-2
思路:
实际上只有力量值最大的前 m m m张牌才会被选择。考虑状压 d p dp dp,需要记录哪些英雄被禁止或选择了。 d p [ i ] [ S ] dp[\ i\ ][\ S\ ] dp[ i ][ S ]表示前 i i i次操作,被禁掉或选择的英雄卡牌为集合为 S S S^时,对应的力量差值。 转移时枚举禁止的卡片可以做到^ 0 ( m 2 × 2 m ) 0(m^2\times 2^m) 0(m2×2m) 实际_上可以优化,对于 1 1 1号操作,肯定不会不选,且一定是选当前力量值最大的卡牌。 2 2 2号操作禁掉最差的卡牌与不禁卡牌效果等价。 这样第一维的阶段性就可以在状态 S S S中体现,可以省去。所以时间复杂度是 O ( m × 2 m ) O(m\times 2^m) O(m×2m)。
#include<bits/stdc++.h>
#define int long long
#define mem(a) memset(a,0,sizeof(a))
#define set(a,b) memset(a,b,sizeof(a))
#define ls p<<1
#define rs p<<1|1
#define pb(x) push_back(x)
#define rand RAND
#define LOCAL
using namespace std;
template<class Typ> Typ &Rd(Typ &x){char ch=getchar(),sgn=0; x=0;for(;ch<'0'||ch>'9';ch=getchar()) sgn|=ch=='-';for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+(ch^48);return sgn&&(x=-x),x;}
template<class Typ> void Wt(Typ x){if(x<0) putchar('-'),x=-x;if(x>9) Wt(x/10);putchar(x%10^48);}
const int inf=0x3f3f3f3f3f;
const int maxn=21;
const int maxm=1100005;
int seed = 19243;
unsigned rand(){return seed=(seed*48271ll)%2147483647;}
int n,m;
bool en;
int s[105];
int op[maxn],id[maxn];
int dp[maxn][maxm];
int lg[maxm];
bool st;
signed main(){
// printf("%.5f\n",(&st-&en)/1024.0/1024.0);
cin>>n;
for(int i=1;i<=n;i++)cin>>s[i];
cin>>m;
for(int i=1;i<=m;i++){
char c;
cin>>c>>id[i];
op[i]=(c=='p');
}
sort(s+1,s+n+1,greater<int>());
for(int i=2;i<(1<<m);i++)lg[i]=lg[i>>1]+1;
for(int j=1;j<(1<<m);j++){
int t=__builtin_popcount(j);
int i=m-t+1;
if(id[i]==1)dp[i][j]=-inf;
else dp[i][j]=inf;
if(!op[i]){
for(int k=1;k<=m;k++){
if(j&(1<<(k-1))){
if(id[i]==1)dp[i][j]=max(dp[i][j],dp[i+1][j^(1<<(k-1))]);
else dp[i][j]=min(dp[i][j],dp[i+1][j^(1<<(k-1))]);
}
}
}
else{
int x=lg[j&-j];
if(id[i]==1)dp[i][j]=dp[i+1][j^(1<<x)]+s[x+1];
else dp[i][j]=dp[i+1][j^(1<<x)]-s[x+1];
}
}
cout<<dp[1][(1<<m)-1]<<endl;
return 0;
}
/*
#ifdef LOCAL
freopen(,,stdin);
freopen(,,stdout);
#endif
*/
例题4:[BZOJ2073][POI2004]PRZ
题目描述:
一只队伍在爬山时碰到了雪崩,他们在逃跑时遇到了一座桥,他们要尽快的过桥. 桥已经很旧了, 所以它不能承受太重的东西. 任何时候队伍在桥上的人都不能超过一定的限制. 所以这只队伍过桥时只能分批过,当一组全部过去时,下一组才能接着过. 队伍里每个人过桥都需要特定的时间,当一批队员过桥时时间应该算走得最慢的那一个,每个人也有特定的重量,我们想知道如何分批过桥能使总时间最少.
输入格式:
第一行两个数: w – 桥能承受的最大重量(100 <= w <= 400) 和 n – 队员总数(1 <= n <= 16). 接下来n 行每行两个数分别表示: t – 该队员过桥所需时间(1 <= t <= 50) 和 w – 该队员的重量(10 <= w <= 100).
输出格式:
输出一个数表示最少的过桥时间.
样例:
输入:
100 3
24 60
10 40
18 50
输出:
42
思路:
状压dp板子题
根据题意可以设计出 d p S dp_S dpS表示已经过桥的人的 b i t m a s k bitmask bitmask为 S S S的最小代价
可得转移方程:
d p S = m i n { d p S ⊕ T + c o s t T } T ⊆ S , T ≠ ∅ , w e i g h t T ≤ W dp_S=min\{dp_{S\oplus T}+cost_T\} T\subseteq S,T\neq \emptyset,weight_T\leq W dpS=min{dpS⊕T+costT}T⊆S,T=∅,weightT≤W
得到这个方程,然后直接子集枚举,并计算 c o s t cost cost和 w e i g h t weight weight是 Θ ( N × 3 N ) \Theta (N\times 3^N) Θ(N×3N) 肯定 T L E TLE TLE
但是我们可以 Θ ( N × 2 N ) \Theta (N\times 2^N) Θ(N×2N) 预处理出对于每个每个状态 T T T时对应的 c o s t cost cost和 w e i g h t weight weight
然后 Θ ( 3 N ) \Theta(3^N) Θ(3N)枚举子集
#include<bits/stdc++.h>
#define int long long
#define mem(a) memset(a,0,sizeof(a))
#define set(a,b) memset(a,b,sizeof(a))
#define ls p<<1
#define rs p<<1|1
#define pb(x) push_back(x)
#define rand RAND
#define LOCAL
using namespace std;
template<class Typ> Typ &Rd(Typ &x){char ch=getchar(),sgn=0; x=0;for(;ch<'0'||ch>'9';ch=getchar()) sgn|=ch=='-';for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+(ch^48);return sgn&&(x=-x),x;}
template<class Typ> void Wt(Typ x){if(x<0) putchar('-'),x=-x;if(x>9) Wt(x/10);putchar(x%10^48);}
const int inf=0x3f3f3f3f;
const int maxn=25;
const int maxm=(1<<17)+5;
int seed = 19243;
unsigned rand(){return seed=(seed*48271ll)%2147483647;}
int m,n;
int c[maxn],w[maxn];
int C[maxm],W[maxm];
int dp[maxm];
signed main(){
Rd(m),Rd(n);
for(int i=1;i<=n;i++)Rd(c[i]),Rd(w[i]);
for(int s=0;s<(1<<n);s++){
W[s]=C[s]=0;
for(int i=1;i<=n;i++)if(s&(1<<(i-1)))W[s]+=w[i],C[s]=max(C[s],c[i]);
}
// for(int s=0;s<(1<<n);s++)cout<<W[s]<<" ";
// cout<<endl;
// for(int s=0;s<(1<<n);s++)cout<<C[s]<<" ";
// cout<<endl;
set(dp,inf);
dp[0]=0;
for(int s=0;s<(1<<n);s++){
// cout<<s<<":"<<endl;
for(int t=s;t;t=(t-1)&s){
// cout<<t<<" ";
if(W[t]<=m)dp[s]=min(dp[s],dp[s^t]+C[t]);
}
// cout<<endl;
}
cout<<dp[(1<<n)-1]<<endl;
return 0;
}
/*
#ifdef LOCAL
freopen(,,stdin);
freopen(,,stdout);
#endif
*/
例题5:CF11D A Simple Task
题目描述:
求简单无向图的环数
输入格式:
第 1 1 1行两个整数 n , m n,m n,m( 1 ≤ n ≤ 19 , 0 ≤ m 1\leq n\leq 19,0\leq m 1≤n≤19,0≤m)。 接下来 m m m行,每行两个整数 a , b a,b a,b( 1 ≤ a , b ≤ n , a ≠ b 1\leq a,b\leq n,a\neq b 1≤a,b≤n,a=b)示节点 a a a和 b b b存在连通的边。
输出格式:
输出一个整数表示环的总数。
样例输入输出:
输入:
4 6
1 2
1 3
1 4
2 3
2 4
3 4
输出:
7
思路:
一眼想到状压 d p dp dp, d p i , j dp_{i,j} dpi,j表示 m m m条边是否走过的 b i t m a s k bitmask bitmask为 i i i的时候, lowbit ( i ) \operatorname{lowbit}(i) lowbit(i)为起点, j j j为终点构成了多少个简单环
但是你会发现, 如果这样设计的话, 由于是无向图的缘故, 会出现一条路径出现两次、一个点和两条边构成一个非法环, 所以答案 a n s = a n s − m 2 ans=\frac{ans-m}{2} ans=2ans−m
特别说明的一点是为什么我们会以 lowbit ( i ) \operatorname{lowbit}(i) lowbit(i)作为起点: 由于圆排列的性质,同一个状态可能会多次出现, 所以我们通过指定 lowbit ( i ) \operatorname{lowbit}(i) lowbit(i)作为起点可以消除这种重复性
#include<bits/stdc++.h>
#define int long long
#define mem(a) memset(a,0,sizeof(a))
#define set(a,b) memset(a,b,sizeof(a))
#define ls p<<1
#define rs p<<1|1
#define pb(x) push_back(x)
#define rand RAND
#define LOCAL
using namespace std;
template<class Typ> Typ &Rd(Typ &x){char ch=getchar(),sgn=0; x=0;for(;ch<'0'||ch>'9';ch=getchar()) sgn|=ch=='-';for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+(ch^48);return sgn&&(x=-x),x;}
template<class Typ> void Wt(Typ x){if(x<0) putchar('-'),x=-x;if(x>9) Wt(x/10);putchar(x%10^48);}
const int inf=0x3f3f3f3f3f;
const int maxn=20;
int seed = 19243;
unsigned rand(){return seed=(seed*48271ll)%2147483647;}
int n,m;
vector<int> G[maxn];
int dp[(1<<maxn)][maxn];
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
G[u].pb(v);
G[v].pb(u);
}
for(int i=1;i<=n;i++)dp[1<<(i-1)][i]=1;//将所有以i为起点,i为终点的dp赋为1
int ans=0;
for(int i=1;i<(1<<n);i++){
for(int j=1;j<=n;j++){
if(!(i&(1<<(j-1))))continue;
for(int v:G[j]){
if((i&-i)>(1<<(v-1)))continue;//因为我们已经钦定lowbit(i)为起点,所以不能有比lowbit(i)还小的点加入进来
if(i&(1<<(v-1))){//如果这个节点已经被访问过,且起点和终点一样
if((i&-i)==(1<<(v-1)))ans+=dp[i][j];//计算答案
}
else{
dp[i|(1<<(v-1))][v]+=dp[i][j];//如果没有被访问,就走这个点
}
}
}
}
cout<<(ans-m)/2<<endl;
return 0;
}
/*
#ifdef LOCAL
freopen(,,stdin);
freopen(,,stdout);
#endif
*/