状压dp-其一

关于二进制

判断包含关系

如果集合A是集合B的子集,那么集合A的每一个元素都属于集合B。

那么,集合A与集合B的交集即为集合A,而它们的并集即为集合B。

用位运算描述,若(A&B)==A,则A∈B; 若(A|B) ==B,则A∈B。

在C++中,只需判断:

if( (A&B)==A ) 则集合A是集合B的子集

else 则集合A不是集合B的子集

或者在C++中判断:

if( (A|B)==B ) 则集合A是集合B的子集

else 则集合A不是集合B的子集

考虑总共有5个元素构成的全集U={5,4,3,2,1},他们表示5个学生的学号。

若知道喜欢踢足球的学生的学号对应的二进制数写成十进制数后是10,

也知道喜欢打篮球的学生的学号对应的二进制数写成十进制数后是14,

问喜欢踢足球的学生是不是喜欢打篮球的学生的子集?

根据上面的判断方法,设a=10, b=14,

if( (a&b) == a) 则喜欢踢足球的是喜欢打篮球的子集

else 则喜欢踢足球的不是喜欢打篮球的子集

全集

若要研究的问题总共有n个元素。那么全集U为{n,n-1,......2,1},全集对应的二进制数是11…1 (共n个1)。

这样一个集合的二进制对应的十进制数是多少呢? 容易得知,是, 则 (1<<n) - 1。

补集

若集合A是集合U的子集,那么A在U中的补集为U中不属于A的元素的集合。

由于A中的元素必定存在于U中,这等价于不同时存在于A和U但至少存在于其中一个集合的元素的集合,

也就二进制异或运算的结果。用位运算描述,A在U中的补集 = A^U。

差集

集合A与集合B的差集,即为属于集合A但不属于集合B的元素的集合。

也就是说,我们从集合A中去掉同时属于A和B的元素,得到的就是A与B的差集,即它们的交集在A中的补集。

用位运算描述,A-B=A^(A&B)。

例题一 原子弹(程序填空)

最近,火星研究人员发现了N个强大的原子。他们互相都不一样。

这些原子具有一些性质。当这两个原子碰撞时,其中一个原子会消失,产生大量的能量。
研究人员知道每两个原子在碰撞时的能释放的能量。
你要写一个程序,让它们碰撞之后产生最多的总能量。

【输入格式】

第一行是整数N(2 <= N <= 10),这意味着有N个原子:A1到AN。

然后下面有N行,每行有N个整数。
在第i行中的第j个整数表示当i和j碰撞之后产生的能量,并且碰撞之后j会消失。
所有整数都是正数,且不大于10000。

【分析题目】

举例分析问题 ,N=3, M数组如下

0 3 7

2 0 6

5 2 0

考虑用动态规划。

用f{3,2,1}表示原问题,则状态是一个集合。

如何找子问题?

取集合里面的两个原子弹出来碰撞, 这样会有一个原子弹消失,那么原子弹的集合就会少一个元素,从而变成了子问题。

(1)那原子弹1碰撞原子弹2,得到能量3,原子弹2消失,变成子问题: f{3,1},  记 A = 3 + f{3,1}

(2)那原子弹1碰撞原子弹3,得到能量7,原子弹3消失,变成子问题: f{2,1},  记 B = 7 + f{2,1}

(3)那原子弹2碰撞原子弹1,得到能量2,原子弹1消失,变成子问题: f{3,2},  记 C = 2 + f{3,2}

(4)那原子弹2碰撞原子弹3,得到能量6,原子弹3消失,变成子问题: f{2,1},  记 D = 6 + f{2,1}

(5)那原子弹3碰撞原子弹1,得到能量5,原子弹1消失,变成子问题: f{3,2},  记 E =  5 + f{3,2}

(6)那原子弹3碰撞原子弹2,得到能量2,原子弹2消失,变成子问题: f{3,1},  记 F =  2 + f{3,1}

原问题f{3,2,1} = max( A, B, C, D, E, F), 所以只有能求出ABCDEF,就能求出原问题。

下面分析如何A,其他BCDEF同理求。

A = 3 + f{3,1},只要求出子问题f{3,1}就能求出A。

如何求f{3,1}? 分两种情况:

(1)原子弹1碰撞原子弹3,得到能量7,原子弹3消失,变成子问题:f{1},易知f{1} = 0

(2)原子弹3碰撞原子弹1,得到能量5,原子弹1消失,变成子问题:f{3},易知f{3} = 0

所以 A = 3 + f{3,1} = max(7+0, 5+0) = 3 + 7 = 10。
同理可以求出:

B= 7 + f{2,1} = 7 + 3 = 10

C = 2 + f{3,2} = 2 + 6 = 8

D = 6 + f{2,1} = 6 + 3 = 9

E =  5 + f{3,2} = 5 + 6 = 11

F =  2 + f{3,1} = 2 + 7 = 9

所以原问题f{3,2,1} = max(A,B,C,D,E,F) = max(10,8,9,11,9) = 11

通过上面的分析可以发现, 子问题集合的元素个数比原问题集合的元素个数少。

所以我们计算顺序是: 先算集合元素个数少的,再算集合元素个数多的。

集合元素的个数L       集合对应的f值
L=1	
                       f{1} = 0

                       f{2} = 0

                       f{3} = 0

L=2	
                       f{2,1} = 3

                       f{3,1} = 7

                       f{3,2} = 6  

L=3	                     f{3,2,1} = 11

这种计算顺序(即动态规划的阶段)虽然正确性没问题,但是比较麻烦。

因为状态是一个集合,表示起来比较麻烦, 例如 f{3,1},在C++如何表示?

f[{3,1}]?这样肯定不行(除非用f是map类型), f数组的下标希望是整型。

接下来,我们换一种计算顺序,不再是从集合元素个数从少算到多。

用新的计算顺序来计算上面的样例:

可以发现,上面的计算顺序是按照十进制数1至7所对应的集合的来计算。

这样编程就容易了:

for(int i=1; i<=7; i++) f[i] = max( ...... );

但是有一个问题,在计算f[i]时,会不会出现后效性?(动态规划要满足无后效性)

for(int i=1;  i<=7;  i++) 

{

       f[i] = ?; 

       因为i是从小到大的计算顺序,所以在计算f[i]时,必须保证f[i]的值只和f[0],f[1],f[2],...f[i-1]有关,

       f[i]的值不能和f[i+1],f[i+2],......有关。

       即f[i]无后效性。

}  

幸运的是,f[i]的计算过程确实是无后效性。

下面看看计算f[7]时,是如何计算的。

首先,十进制数7表示的二进制数是111,所以111表示的集合是{3,2,1}。

求f[7]实际就是求f{3,2,1},根据前面的分析可以知道:

取集合里面的两个原子弹出来碰撞, 这样会有一个原子弹消失,那么原子弹的集合就会少一个元素,从而变成了子问题。

(1)那原子弹1碰撞原子弹2,得到能量3,原子弹2消失,变成子问题: f{3,1} =  f[5],  记 A = 3 + f[5]

(2)那原子弹1碰撞原子弹3,得到能量7,原子弹3消失,变成子问题: f{2,1}  = f[3],  记 B = 7 + f[3]

(3)那原子弹2碰撞原子弹1,得到能量2,原子弹1消失,变成子问题: f{3,2} =  f[6],  记 C = 2 + f[6]

(4)那原子弹2碰撞原子弹3,得到能量6,原子弹3消失,变成子问题: f{2,1} =  f[3],  记 D = 6 + f[3]

(5)那原子弹3碰撞原子弹1,得到能量5,原子弹1消失,变成子问题: f{3,2} =  f[6] ,  记 E =  5 + f[6]

(6)那原子弹3碰撞原子弹2,得到能量2,原子弹2消失,变成子问题: f{3,1}  = f[5] ,  记 F =  2 + f[5]

所以f[7]只与子问题f[3]、f[5]、f[6]有关。

因为 for(int i=1;  i<=7;  i++)   f[i] = max( ...... );  所以在计算f[7]时,f[3]、f[5]、f[6]肯定已经计算出来了。

其实很容易知道,如果f[j]是f[i]的子问题,那么j一定小于i。证明?

其实很简单,因为j是i去掉1个原子弹之后得到的集合,所以j对应的二进制数一定小于i对应的二进制数,

那么j对应的十进制数也一定小于i对应的十进制数。

所以本题的程序框架是:


int   s = (1<<N) - 1;   //即s是一个十进制数,s代表全集,包含N个原子弹,s={N,N-1,......,2,1}, 

//    集合、 二进制数、 十进制数, 三者是一一对应的关系!

for(int i=1;   i<=s;  i++)  //按照十进制数从1至s的顺序来计算f[i]

{

       第一步:把十进制数i对应的集合,所包含的原子弹分离出来并保存到临时数组A。

                    例如: i = 22,那么A[] = {5,3,2},则十进制数22所代表的集合包含了第2,3,5个原子弹。

                    可以通过枚举第j个原子弹是否属于十进制数i所表示的集合,从而产生A数组。

                     int tot = 0;  //临时变量,表示十进制数i所表示的集合,总共包含tot个原子弹。

                     for(int j=N; j>=1;  j--)  //判断十进制数i所表示的集合是否包含第j个原子弹

                     {

                                    int  C = 1<<(j-1);

                                    if(  (i&C) > 0)   A[++tot] = j;  //说明十进制数i所表示的集合包含第j个原子弹

                     }

       第二步: 从A数组里面任意取出两个原子弹(不妨假设是X和Y),分别用X碰撞Y,用Y碰撞X,都能变成子问题。

                      for(int u = 1;  u < tot;  u++)

                          for(int  v = u + 1;  v <= tot;  v++)

                          {

                                    int  X = A[u];

                                    int  Y = A[v];

                                    (1)用X碰撞Y,得到能量M[X][Y], 子问题是什么?

                                             因为碰撞后第Y个原子弹消失,那么子问题就是从十进制数i代表的集合里面去掉第Y个原子弹,如何表示子问题的状态呢?

                                             第Y个原子弹对应的二进制数是:  1 << (Y-1) , 不妨记为 Z = 1<<(Y-1);

                                             根据前面所学的集合与二进制的关系,子问题的状态为: i - Z =  i ^ Z,  即i异或Z

                                              所以  f[i] = max(f[i],  M[X][Y] + f[i^Z]);

                                    

                                     (2)用Y碰撞X,得到能量M[Y][X], 子问题是什么?

                                             因为碰撞后第X个原子弹消失,那么子问题就是从十进制数i代表的集合里面去掉第X个原子弹,如何表示子问题的状态呢?

                                             第X个原子弹对应的二进制数是:  1 << (X-1) , 不妨记为 Z = 1<<(X-1);

                                             根据前面所学的集合与二进制的关系,子问题的状态为: i - Z =  i ^ Z,  即i异或Z

                                             所以  f[i] = max(f[i],  M[Y][X] +  f[i^Z];

                          } 

}

cout<<f[s]<<endl;

时间复杂度: O(2^n * n * n )

例题二 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个书包之间,有多少种不同的匹配方案。

1<=n<=21

做法

我们记f[i][j]为现在到第i个人,还剩下二进制集合j的书(0为可以选,1为已经被选),答案即为f[n][(1<<n)-1]

时间复杂度O(2^n * n)

实际上我们还可以发现我们可以去掉第i个人的那一维(

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=24,p=1e9+7;
int n,x,a[maxn],f[maxn][(1<<21)+10],d[(1<<21)+10];
int check(int num){
	int cnt=0;
	while(num!=0){
		if(num&1)cnt++;
		num=num/2;
	}
	return cnt;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			cin>>x;
			if(x==1)a[i]+=(1<<(j-1));
		}
	}
	for(int i=1;i<(1<<n);i++)d[i]=check(i);
	f[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<(1<<n);j++){
			if(d[j]!=i)continue;
			for(int k=1;k<=n;k++){
				int Num=(1<<(k-1));
				if((j&Num) && (a[i]&Num))f[i][j]=(f[i][j]+f[i-1][j^(j&Num)])%p;
			}
		}
	}
	cout<<f[n][(1<<n)-1];
	
	
	return 0;
}

例题三 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只兔子分在了同一组。

应该如何分组,才能使得最终赚的钱最多

1<=n<=16

做法

与上一题类似的,我们记f[i]为二进制集合i(1为已选,0为未选)的分配方案的最大值,对于一个状态,我们枚举他的子集,使子集内的分在一组

时间复杂度为O(3^n)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=16;
int n,a[maxn+5][maxn+5],f[(1<<maxn)+10],s[maxn+5],d[(1<<maxn)+10];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>a[i][j];
	f[0]=0;
	for(int i=1;i<(1<<n);i++){
		int len=0;
		for(int j=1;j<=n;j++)
			if(i&(1<<(j-1)))
				s[++len]=j;
		
		for(int j=1;j<=len;j++)
			for(int k=j+1;k<=len;k++)
				d[i]+=a[s[j]][s[k]];
		
//		cout<<i<<" "<<d[i]<<endl;
		for(int j=i;j!=0;j=(j-1)&i)
			f[i]=max(f[i],f[i^j]+d[j]);
		
	}
	cout<<f[(1<<n)-1];
	
	
	
	return 0;
}

例题四 开锁

题目大意

有n把锁,编号1至n。有m把钥匙,第i把钥匙的价格是p[i],第i把钥匙可以开k[i]把锁,

分别可以开第c[i][1],c[i][2],...第c[k[i]]把锁。

问你如果购买钥匙,用最少的费用把n把锁全部打开。

如果无论无何也不能把n把锁全部打开,输出-1。

1<=n<=12

做法

显然

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=12;
int n,m,f[(1<<maxn)+10],p,N,x;
int main(){
	cin>>n>>m;
	memset(f,0x3f,sizeof f);
	f[0]=0;
	for(int i=1;i<=m;i++){
		cin>>p>>N;
		int Num=0;
		for(int j=1;j<=N;j++){
			cin>>x;
			Num+=(1<<(x-1));
		}
		for(int j=0;j<(1<<n);j++){
			f[j|Num]=min(f[j|Num],f[j]+p);
		}
	}
	if(f[(1<<n)-1]==1061109567)cout<<-1;
	else cout<<f[(1<<n)-1];
	return 0;
}

例题五 旅行商

题目大意

有三维立体空间里,有n个城市,第i个城市的坐标是(x[i],y[i],z[i])。

从第i个城市到第j个城市的距离dis[i][j] = abs(x[j]-x[i]) + abs(y[j]-y[i]) + max(0,z[j]-z[i]),其中abs是求绝对值。

你需要从1号城市出发,遍历每一个城市至少一次,最后回到1号城市,问最少的旅行距离。
2<=n<=17

做法

我们记f[i][j]为二进制集合为i(1为已去,0为未去),最后到j的最短距离

时间复杂度为O(2^n * n^2)

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=17;
int n,f[(1<<maxn)+10][maxn+5];
int d[maxn+5][maxn+5];
struct node{
	int x,y,z;
}a[maxn+5];

int main(){
	cin>>n;
	memset(f,0x3f,sizeof f);
	for(int i=1;i<=n;i++){
		cin>>a[i].x>>a[i].y>>a[i].z;
	}
	f[1][1]=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			if(i==j)continue;
			d[i][j]=abs(a[j].x-a[i].x)+abs(a[j].y-a[i].y)+max(0,a[j].z-a[i].z);
		}
	}
	for(int i=1;i<(1<<n);i++){
		for(int j=1;j<=n;j++){
			if(!(i&(1<<(j-1))))continue;
			for(int k=1;k<=n;k++){
				if(!(i&(1<<(k-1)))||k==j)continue;
				f[i][j]=min(f[i^(1<<(j-1))][k]+d[k][j],f[i][j]);
			}
		}
	}
	int minn=1e9;
	for(int i=2;i<=n;i++){
		minn=min(minn,f[(1<<n)-1][i]+d[i][1]);
	}
	cout<<minn;
	return 0;
}

例题六 不同排列

题目大意

有n个学生,学号1至n。你现在需要把这n个学生从左往右排成一行形成队伍,要满足如下所有m个条件:

第i个条件的格式是x[i],y[i],z[i],表示队伍的前x[i]学生当中,学号小于y[i]的学生不能超过z[i]人。

求满足上面所有m个条件的队伍有多少种不同的方案。

2<=n<=18

做法

我们记f[i]为二进制集合为i(1为已用,0反之)的方案数

若不符合条件,则直接令之为0

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=18;
int n,m,s[maxn+5],f[(1<<maxn)+5],b[maxn+5];
struct node{
	int x,y,z;
}a[105];
bool cmp(node x,node y){
	return x.x>y.x;
};
string chang(int num){
	string s(n,'0');
	int i=1;
	while(num!=0){
		s[n-i]=char(num%2+'0');
		num=num/2;
		i++;
	}
	return s;
}
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		cin>>a[i].x>>a[i].y>>a[i].z;
	}
	sort(a+1,a+m+1,cmp);
	f[0]=1;
	for(int i=1;i<(1<<n);i++){
		int cnt=0;
		s[0]=0;
		for(int j=1;j<=n;j++){
			s[j]=s[j-1];
			if(i&(1<<(j-1)))cnt++,s[j]++;
		}
		bool flag=true;
		for(int j=1;j<=m&&a[j].x>=cnt;j++){
			if(s[a[j].y]>a[j].z){
				flag=false;
				break;
			}
		}
		if(!flag){
			f[i]=0;
			continue;
		}
		f[i]=0;
		for(int j=1;j<=n;j++){
			if(i&(1<<(j-1)))f[i]+=f[i^(1<<(j-1))];
		}
	}
	cout<<f[(1<<n)-1]<<endl;
	return 0;
}

例题七 序列

题目大意

有两个序列: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一样。

2<=n<=18

做法

从后往前考虑,我们记f[i][j]为匹配到第i个数,还有集合j(0为可用,1不可用)可以选(这里的第一维似乎也可以省略),我们从集合j中选一个数使之与i匹配

时间复杂度为O(2^n * n)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=18;
int n,X,Y,f[maxn+5][(1<<maxn)+10],a[maxn+5],b[maxn+5],d[(1<<maxn)+18];
int check(int num,bool flag){
	int ans=0;
	string s(n,'0');
	int i=-1;
	while(num!=0){
		ans+=num%2;
		if(flag)s[++i]=char(num%2+'0');
		num/=2;
	}
	if(flag){
		cout<<s<<endl;
		return -1;
	}
	return ans;
}
signed main(){
	cin>>n>>X>>Y;
	memset(f,0x3f,sizeof f);
	f[n+1][0]=0;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)cin>>b[i];
	for(int i=1;i<(1<<n);i++)d[i]=check(i,false);
	for(int i=n;i>=1;i--){
		for(int j=1;j<(1<<n);j++){
			if(d[j]!=n-i+1)continue;
			int cnt=1;
			for(int k=1;k<=n;k++){
				if(j&(1<<(k-1))){
					f[i][j]=min(f[i][j],f[i+1][j^(1<<(k-1))]+abs(cnt-i)*Y+abs(b[i]-a[k])*X);
				}
				else cnt++;
			}
//			cout<<i<<" "<<check(j,true)<<" "<<f[i][j]<<endl;
		}	
	}
	cout<<f[1][(1<<n)-1];
	return 0;
}

例题九 接龙

题目大意

有n个字符串,第i个字符串是S[i]。每个字符串都是由不超过10个小写英文字母构成。

现在A和B两人要玩字符串接龙游戏,A是先手。

每次当前玩家从剩下的字符串当中选中一个拿出来(不妨假设选中的是S[j]),

接龙到前面的字符串(不妨假设上一个被选出来的字符串是s[i]),

那么S[j]的首字母必须等于S[i]的末尾字母,这样才算接龙接得上。

如果轮到当前玩家了,而当前玩家却发现从剩下得字符串当中找不到能接得上龙得字符串,那么游戏结束,当前玩家输。

假设A和B都用最优策略玩游戏。

如果A能胜利输出"First",如果B能勝利輸出"Second"。

1<=n<=16

做法

显而易见,不写了(

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=16;
int n;
struct node{
	int fir,en;
}a[maxn+10];
string s;
int f[30][(1<<maxn)+10];
string change(int num){
	string s(n,'0');
	int i=-1;
	while(num!=0){
		s[++i]=num%2+'0';
		num=num/2;
	}
	return s;
}
int check(bool whi,int num,int last){
	
	if(f[last][num]!=-1)return f[last][num];
	
	bool flag=false;
	for(int i=1;i<=n;i++)
		if(!(num&(1<<(i-1)))&&(last==0||last==a[i].fir))flag=flag|( whi^(check(!whi,num^(1<<(i-1)),a[i].en)));
	
	if(flag)f[last][num]=(1^whi);
	else f[last][num]=(0^whi);
//	cout<<whi<<" "<<change(num)<<" "<<char(last-1+'a')<<" ";cout<<(whi^f[last][num])<<endl;
	return f[last][num];
}
int main(){
	memset(f,-1,sizeof f);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>s;
		a[i].fir=s[0]-'a'+1;
		a[i].en=s[s.size()-1]-'a'+1;
	}
	if(check(false,0,0)==1)cout<<"First\n";
	else cout<<"Second\n";
	
	return 0;
}
posted @ 2023-05-22 19:45  Ayaka_T  阅读(98)  评论(0)    收藏  举报