状压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 1d1000, 1 ≤ T ≤ 15 1\leq T\leq15 1T15

样例输入输出:
输入:
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{dpST+costT}TS,T=,weightTW

得到这个方程,然后直接子集枚举,并计算 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 1n19,0m)。 接下来 m m m行,每行两个整数 a , b a,b a,b( 1 ≤ a , b ≤ n , a ≠ b 1\leq a,b\leq n,a\neq b 1a,bn,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=2ansm

特别说明的一点是为什么我们会以 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
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值