状态压缩动态规划

帅哥CN是帅哥 / 2024-02-26 / 原文

集合与二进制


 

第1题     集合基本概念

1、集合与元素

集合:由一个或多个确定的元素所构成的整体,是指具有某种特定性质的具体的或抽象的对象汇总而成的集体。

元素:构成集合的这些对象则称为该集合的元素

例如,全中国人的集合,它的元素就是每一个中国人。

例如,{1,3,5}是一个集合,3是该集合的元素。

2、空集

有一类特殊的集合,它不包含任何元素,称之为空集,记为∅。

 

3、全集

一般的,如果一个集合含有我们所研究问题中涉及的所有元素,那么就称这个集合为全集,通常记作U。

4、集合中元素的特性

(1)确定性

给定一个集合,任给一个元素,该元素或者属于或者不属于该集合,二者必居其一,不允许有模棱两可的情况出现。

(2)互异性

一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次。有时需要对同一元素出现多次的情形进行刻画,可以使用多重集,其中的元素允许出现多次。

(3)无序性

一个集合中,每个元素的地位都是相同的,元素之间是无序的。集合上可以定义序关系,定义了序关系后,元素之间就可以按照序关系排序。但就集合本身的特性而言,元素之间没有必然的序。

5、元素与集合的关系

(1)属于

如果元素a在集合A中,就说a属于A,记作a∈A。例如:3∈{1,3,5}。

(2)不属于

如果元素a不在集合A中,就说a不属于A,记作a∉A。例如:2∉{1,3,5}。

 

6、集合间的基本关系

(1)子集与真子集

设S,T是两个集合,如果S的所有元素都属于T ,即x∈S ⇒x∈T,则称S是T的子集,记为 S⊆T(读作S含于T)。

显然,对任何集合S,都有S⊆S,∅⊆S。 其中,符号⊆读作包含于,表示该符号左边的集合中的元素全部是该符号右边集合的元素。

如果S是T的一个子集,即S⊆T ,但在T中存在一个元素x不属于S ,则称S是T的一个真子集。

空集∅是任意一个非空集合的真子集,空集是任何一个集合的子集。

(2)交集

交集由属于A且属于B的相同元素组成的集合,记作A∩B(或B∩A),读作“A交B”(或“B交A”),即A∩B={x|x∈A,且x∈B}。

例如:A={1,3,5},B={2,3,6},则A∩B = {3}。

(3)并集

并集由所有属于集合A或属于集合B的元素所组成的集合,记作A∪B(或B∪A),读作“A并B”(或“B并A”),即A∪B={x|x∈A,或x∈B}。

例如:A={1,3,5},B={2,3,6},则A∪B = {1,2,3,5,6}。

(4) 补集

补集又可分为相对补集和绝对补集。

相对补集定义:由属于A而不属于B的元素组成的集合,称为B关于A的相对补集,记作A-B或A\B,即A-B={x|x∈A,且x∉B}。

 

填空题

1、若A={3,2,1}, 则集合A的不同的子集有:Φ,{1},{2},,{1,2},{1,3},{2,3},{1,2,3} 。

2、若A={10,9,8,7,6,5,4,3,2,1},  则集合A的有个不同的子集。

3、若A={1,2,3,4},B={2,4,6,1}, 则A∩B =  【说明:为了方便测评,元素从小到大,不出现空格】       

4、若A={1,2,3,4},B={2,4,6,1}, 则AUB =  【说明:为了方便测评,元素从小到大,不出现空格】

5、若A={4,3,2,1,0}, B={3,1},  则A-B=  【说明:为了方便测评,元素从小到大,不出现空格】

6、有A,B,C三个集合,若A⊆B,且B⊆C,则A⊆C。 【说明:填对或错】

7、若C=A∩B, D=A∪B, 则C⊆D。 【说明:填对或错】

8、若A⊆B, C=B-A, 则C⊆B。【说明:填对或错】

 

答案

1.{3}   2.1024     3,{1,2,4}    4.{1,2,3,4,6}    5.{0,2,4}    6.对   7.对   8.对

 

 

 

 

 

 

 

第2题     二进制位运算

1、按位与运算符(&)

参加运算的两个数,按二进制位进行“与”运算。

运算规则:只有两个数的二进制同时为1,结果才为1,否则为0。(负数按补码形式参加按位与运算)

即 0 & 0= 0 ,0 & 1= 0,1 & 0= 0, 1 & 1= 1。

例:3 &5  即 00000011 & 00000101 = 00000001 ,所以 3 & 5的值为1。

2、按位或运算符(|)

参加运算的两个数,按二进制位进行“或”运算。

运算规则:参加运算的两个数只要两个数中的一个为1,结果就为1。

即  0 | 0= 0 ,  1 | 0= 1  , 0 | 1= 1  ,  1 | 1= 1 。

例:2 | 4 即 00000010 | 00000100 = 00000110 ,所以2 | 4的值为 6 。

3、异或运算符(^)

参加运算的两个数,按二进制位进行“异或”运算。

运算规则:参加运算的两个数,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。

即 0 ^ 0=0  , 0 ^ 1= 1  , 1 ^ 0= 1  , 1 ^ 1= 0 。

例: 2 ^ 4 即 00000010 ^ 00000100 =00000110 ,所以 2 ^ 4 的值为6 。

4、左移( << )

将6左移2位:6<<2

0000 0110  十进制6

0001 1000  左移2位后,低位补0,换算成十进制为24

可以发现把一个数a左移1位相当于a=a*2,把一个数b左移3位相当于b=b*2^3=b*8。

5、右移( >> )

将6右移2位:6>>2

0000 0110  十进制6

0000 0001  右移2位后,高位补0,换算成十进制为1

 

可以发现把一个数a右移1位相当于a=a/2,把一个数b右移3位相当于b=b/2^3=b/8。

6、非( ~ )  非是一元操作符

操作数的每一位1变为0,0变为1

将6非运算:~6

0000 0110

1111 1001     换算成十进制为-7

 

填空题:

13 & 6 = 

5 | 6  = 

7 ^ 6  = 

1<<3  = 

1<<4  = 

10<<2 = 

15>>2 = 

 

答案 4 7 1 8 16 40 3

 

 

 

第3题     集合的二进制表示(程序填空)

 

我们可以使用一个01串A来表示一个集合。对于数x(x≥0),用A[x]=0表示它不在该集合中,用A[x]=1表示它在该集合中。

将01串A看作是一个二进制数,我们把它转换为十进制,就可以使用一个十进制整数来表示一个实际使用二进制方式表示的集合。

这样,我们可以使用位运算方便地处理集合的操作。

现在考虑总共有5个数构成的全集A={5,4,3,2,1},请观察它们的一一对应关系:

集合

01二进制数

十进制数

{5,4,3,2,1}

11111

31

{5,4,3,1}

11101

29

{4,2,1}

01011

11

有n个学生,学号1至n,学号构成的集合A={n,n-1,n-2,...3,2,1}。

由于最近流感比较严重,有一些学生请假了,于是老师开始点名。

最后只有k个学生回答报到,第i个回答报到的学生的学号是s[i]。

显然,根据上面题目的描述,可以把n个学生的考勤表看成01二进制数串,

缺席的学生对应的学号的二进制位设置为0,报到的学生对应的二进制位设置位1。

请输出考勤表二进制数串所对应的十进制数。

 

 

输入格式

 

第一行,两个整数:n和k。1<=k<=n<=10。

第二行,k个整数,第i个整数是s[i],1<=s[i]<=n,且s[i]互不相同。

 

输出格式

 

一个整数。

 

输入/输出例子1

输入:

5 3

4 1 2

 

输出:

11

 

样例解释

 

有3个学生,学号是1,2,4的学生到了,说明学号是5和3的学生缺席了,

因此考勤表的01二进制串是: 01011, 这个二进制串对应的十进制数是11 。

学号是4的学生对应的二进制数是:01000

学号是1的学生对应的二进制数是:00001

学号是2的学生对应的二进制数是:00010

我们只需要把二进制数(01000) + (00001) + (00010) = 8 + 1 +  2  = 11即可。

 

#include<bits/stdc++.h>
using namespace std;

int n, k;
int main()
{
    cin>>n>>k;
    int ans = 0; 
    for(int i = 0; i < k; i++){
        int si;
        cin>>si;
        int bi = 1<<(  ); //学号是si的学生报到 
        ans = ans + bi;
    } 
    cout<<ans<<endl; 
    return 0;
}

 答案 si-1

 

 

 

 

 

 

 

第4题     集合的交和二进制的与(程序填空)

 

两个集合A和B的交集,即为两个集合公有元素的集合。

这等价于两个原始集合对应的二进制整数的某一位同时为1,则所求集合该位为1。

用位运算描述,集合交集实质上就是二进制与运算,即A ∩ B = A & B。

现在考虑总共有5个数构成的全集:{5,4,3,2,1},请观察对应关系

集合A和B

集合A∩B对应的二进制数

集合A∩B对应的十进制数

A={5,3,2,1},B={4,3,1}

A∩B=10111&01101=00101

A∩B=23&13=5

A={4,3,2,1}, B={4,2}

A∩B=1111&1010=1010

A∩B=15&10 = 10

有n个学生,学号1至n,他们的学号构成集合U={n,n-1,n-2,...2,1}。

有s个学生喜欢打篮球,第i个喜欢打篮球的学生的学号是x[i]。

有t个学生喜欢踢足球,第i个喜欢踢足球的学生的学号是y[i]。

把n个学生看成一个01二进制串,如果第i个学生同时喜欢打蓝球和踢足球,

那么学号i对应的二进制位设置位1,否则设置为0。

输出该01二进制串所对应的十进制数。

输入格式

 

第一行,三个整数:n,s,t。1<=n<=10, 1<=s<=n, 1<=t<=n。

第二行,s个整数,第i个整数是x[i]。

第三行,t个整数,第i个整数是y[i]。

 

输出格式

 

一个整数。

 

输入/输出例子1

输入:

6  3 4

5 2 4 

2 5 1 6

 

 

输出:

18

 

样例解释

 

篮球对应的二进制串:011010,因为第2,4,5,共三学生喜欢打篮球,所以对应的位设置1,其余为0。

足球对应的二进制串:110011,因为第1,2,5,6,共四学生喜欢踢足球,所以对应的位设置1,其余为0。

同时喜欢打篮球和踢足球的学生的集合对应的二进制数 = 011010 & 110011 = 010010,

而二进制数010010对应的十进制数是18。

 

篮球A和足球B

A∩B对应的二进制数

A∩B对应的十进制数

A={5,2,4}, B={2,5,1,6}

A∩B=011010&110011=010010

A∩B=26&51=18

 

 

↑↓?
1
#include<bits/stdc++.h>
using namespace std;

int n, s, t;
int main()
{
    cin>>n>>s>>t;
    int sum1 = 0; //喜欢篮球的学生的学号构成的集合A,所对应的二进制数,的十进制数。
    int sum2 = 0; //喜欢足球的学生的学号构成的集合B,所对应的二进制数,的十进制数。
    for(int i = 0; i < s; i++){
        int a;
        cin>>a;
        sum1 += (1<<(a-1));
    } 
    for(int i = 0; i < t; i++){
        int b;
        cin>>b;
        sum2 += (1<<(b-1));
    }     
    int ans =   ;
    cout<<ans<<endl; 
    return 0;
}

  答案  sum1&sum2

 

 

 

 

 

 

 

第5题     集合的并和二进制的或(程序填空)

两个集合A和B的并集,即为两个集合所有元素的集合。

这等价于两个原始集合对应的二进制整数的某一位只要有一个为1,则所求集合该位为1。

用位运算描述,并集运算实质上就是或运算,即A∪B=A|B。

现在考虑总共有5个数构成的全集:{5,4,3,2,1},请观察对应关系

 

集合A和B 集合A∪B对应的二进制数 集合A∪B对应的十进制数
A={5,3,1}, B={4,3,1} A∪B = 10101 | 01101 = 11101 A∪B = 21 | 13 =29

 

有n个学生,学号1至n,他们的学号构成集合U={n,n-1,n-2,...2,1}。

有s个学生喜欢打篮球,第i个喜欢打篮球的学生的学号是x[i]。

有t个学生喜欢踢足球,第i个喜欢踢足球的学生的学号是y[i]。

把n个学生看成一个01二进制串,如果第i个学生喜欢打蓝球或者踢足球,

那么学号i对应的二进制位设置位1,否则设置为0。

输出该01二进制串所对应的十进制数。

输入格式

 

第一行,三个整数:n,s,t。1<=n<=10, 1<=s<=n, 1<=t<=n。

第二行,s个整数,第i个整数是x[i]。

第三行,t个整数,第i个整数是y[i]。

 

输出格式

 

一个整数。

 

输入/输出例子1

输入:

6  3 4

5 2 4 

2 5 1 6

 

 

输出:

59

 

样例解释

 

篮球对应的二进制串:011010,因为第2,4,5,共三学生喜欢打篮球,所以对应的位设置1,其余为0。

足球对应的二进制串:110011,因为第1,2,5,6,共四学生喜欢踢足球,所以对应的位设置1,其余为0。

喜欢打篮球或者踢足球的学生的集合对应的二进制数 = 011010 | 110011 = 111011,

而二进制数111011对应的十进制数是59。

 

篮球A和足球B A∪B对应的二进制数 A∪B对应的十进制数

A={5,2,4}, B={2,5,1,6}

A ∪ B = 011010 | 110011 = 111011

 A ∪ B = 26 | 51 = 59

 

#include<bits/stdc++.h>
using namespace std;

int n, s, t;
int main()
{
    cin>>n>>s>>t;
    int sum1 = 0; //喜欢篮球的学生的学号构成的集合A,所对应的二进制数,的十进制数。
    int sum2 = 0; //喜欢足球的学生的学号构成的集合B,所对应的二进制数,的十进制数。
    for(int i = 0; i < s; i++){
        int a;
        cin>>a;
        sum1 += (1<<(a-1));
    } 
    for(int i = 0; i < t; i++){
        int b;
        cin>>b;
        sum2 += (  );
    }     
    int ans =   ;
    cout<<ans<<endl; 
    return 0;
}

  答案

第1空 1<<(b-1)
第2空 sum1|sum2

 

 

 

 

 

 

第6题     判断包含关系(程序填空)

如果集合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}。

喜欢踢足球的学生,他们的学号对应的二进制数写成十进制数之后是a。

喜欢打篮球的学生,他们的学号对应的二进制数写成十进制数之后是b。

若喜欢踢足球的学生是喜欢打篮球的学生的子集,输出yes,否则输出no

 

输入格式

 

第一行,3个整数: n,a,b。  1<=n<=10,   0<=a,b<1024。

 

 

输出格式

 

yes或no

 

输入/输出例子1

输入:

5 10 14

 

输出:

yes

 

样例解释

 

 

答案

 (a&b)==a

 

 

 

 

 

 

 

第7题     判断一个元素是否属于集合(程序填空)

设U是全集,共有n个元素,用i来表示第i个数,从右往左排列好,即U= {n, n-1, ......, 2, 1}。

设B是U的一个子集。 如何判断第i个元素是否属于集合B?

判断步骤如下:

1、求出第i个元素对应的二进制数,不妨设为C,则C = 1<<(i-1);

2、 判断B & C 的结果。如果大于0则说明第i个元素属于集合B,否则不属于B。

现在考虑总共有5个元素构成的全集U={5,4,3,2,1},请填写下面的一一对应关系

集合B 问题 集合B对应的二进制数 集合B对应的十进制数 问题对应的二进制数 问题对应的十进制数 判断结果
B={5,3,2,1} 第3个元素是否在集合B内 10111 23 1<<(3-1)=00100 4

23&4=10111&00100=00100>0

第3个元素在集合B内

B={5,3,2,1} 第4个元素是否在集合B内 10111 23 1<<(4-1)=01000 8

23&8=10111&01000=00000=0

第4个元素不在集合B内

综上所述,问第i个元素是否在集合B内,在C++中,只需判断:

  int  C = 1<<(i-1);
  if ( (B&C) > 0 )  集合B包括第i个元素;
  else   集和B不包含第i个元素。

 

现在有n个学生,他们的学号构成全集U={n,n-1,...2,1}。

喜欢打篮球的学生,他们的学号对应的二进制数写成十进制数之后是b。

给出b,请从小到大的次序,输出喜欢打篮球的学生的学号。

 

题目分析:

可以枚举法,for  i =1  to  n, 判断学生i是否喜欢打篮球,

记第i个学生对应的二进制数写成十进制数是c, 则c=1<<(i-1), 所以

只需要判断 if( (c&b) == c)即可。

输入格式

 

一行,两个整数:n和b。1<=n<=10,   1<=b<1024。

 

输出格式

 

一行,若干个正整数。

 

输入/输出例子1

输入:

5 13

 

输出:

1 2 3 5

 

样例解释

 

 

#include<bits/stdc++.h>
using namespace std;

int n, b;
int main()
{
    cin>>n>>b;
    for(int i = 1; i <= n; i++){
        int c = (1<<(i-1));
        if(    == c) cout<<i<<" "; 
    }
    return 0;
}

  答案  (c&b)

 

 

 

第8题     全集补集差集(程序填空)

全集

若要研究的问题总共有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个学生,他们的学号构成全集U={n,n-1,...2,1},设全集U所对应的二进制数写成十进制数是Y。

喜欢打篮球的学生,他们的学号对应的二进制数写成十进制数之后是A。

喜欢踢足球的学生,他们的学号对应的二进制数写成十进制数之后是B。

已知B是A的子集。

若设喜欢打篮球但不喜欢踢足球的学生,他们的学号对应的二进制数写成十进制数之后是C。

读入n,A,B,输出Y和C。

 

分析题目

容易得知Y = (1<<n) - 1

由于B是A的子集,所以

C = A ^ B 是正确的,

或者C = A - B 也是正确的。

输入格式

 

一行,三个整数n,A,B。1<=n<=10,  1<=B<=A<1024。

 

输出格式

 

输出Y和C

 

输入/输出例子1

输入:

5 23 3

 

输出:

31 20

 

样例解释

 

 

 

#include<bits/stdc++.h>
using namespace std;

int n, A, B;
int main()
{
    cin>>n>>A>>B;
    int Y =   ;
    int C =  ;
    cout<<Y<<" "<<C<<endl;
    return 0;
}

  答案

位置我的作答
第1空 (1<<n)-1
第2空 (A^B)

 

 

 

 

 

例题1分析


 

 

第1题     原子弹(程序填空)

最近,火星研究人员发现了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数组的下标希望是整型。

 

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

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

状态对应的集合 状态对应的二进制 状态对应的十进制 状态的f函数值
{1} 001 1 f[1] = 0
{2} 010 2 f[2] = 0
{2,1} 011 3 f[3] = 3
{3} 100 4 f[4] =0
{3,1} 101 5 f[5] = 7
{3,2} 110 6 f[6] = 6
{3,2,1} 111 7 f[7] = 11

可以发现,上面的计算顺序是按照十进制数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]

{

        f[i] = ?  //十进制数i,展开后是二进制数, 代表一个集合。

}

cout<<f[s]<<endl;

 

进一步具体地:

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个原子弹。

      

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

                     都能变成子问题,维护好f[i]的值。

}

cout<<f[s]<<endl;

 

 

更进一步具体地:

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 )

 

输入格式

 

有多组数据。

每组数据下的第一行是整数N(2 <= N <= 10),这意味着有N个原子:A1到AN。
然后下面有N行,每行有N个整数。
在第i行中的第j个整数表示当i和j碰撞之后产生的能量,并且碰撞之后j会消失。
所有整数都是正数,且不大于10000。
输入以n=0结尾。
输入数据不超过500个。

 

 

输出格式

 

输出N个原子碰撞之后产生的最大总能量。

 

输入/输出例子1

输入:


0 4
1 0

0 20 1 
12 0 1 
1 10 0 
0

 

输出:


22

 

样例解释

 
 
#include <bits/stdc++.h>
using namespace std;
const int maxS = 1<<10;
int N, f[maxS+5], M[12][12], A[12];

void dp()
{
    int   s =   ;
    for(int i=1;   i<=s;  i++)  //按照十进制数从1至s的顺序来计算f[i]
    {
        int tot = 0;  //临时变量,表示十进制数i所表示的集合,总共包含tot个原子弹。
        for(int j=N; j>=1;  j--)  //判断十进制数i所表示的集合是否包含第j个原子弹
        {
            int  C = 1<<(j-1);
            if(   )   A[++tot] = j;  //说明十进制数i所表示的集合包含第j个原子弹
        }
        for(int u = 1;  u < tot;  u++)
            for(int  v = u + 1;  v <= tot;  v++)
            {
                int  X = A[u];
                int  Y = A[v];
                int  Z = 1<<(Y-1);
                f[i] = max(f[i],  M[X][Y] + f[i^Z]);
                Z = 1<<(X-1);
                f[i] = max(f[i],    );
            }
    }
    cout<<f[s]<<endl;
}

int main()
{
    while(cin>>N)
    {
        if(N==0) break;
        for(int i=1; i<=N; i++)
            for(int j=1; j<=N; j++)
                cin>>M[i][j];
        memset(f,0,sizeof(f));
        dp();
    }
    return 0;
}

  答案

位置我的作答
第1空 (1<<N)-1
第2空 (i&C)==C
第3空 M[Y][X]+f[i-Z]

 

 

例题一


 

第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

 

 

样例解释

 

//f[i][j]表示前i本书和书包集合是j的匹配方案数。

读入数据是用vector<int> a[N]; //a[i]保存的是与第i本书兼容的书包

枚举第i本书与哪个书包匹配,即可转移。

注意为了减少不必要的枚举,f[i][j]是有限制的,即集合j的二进制的1的个数要等于i,不然f[i][j]肯定等于0

以下是参考程序:

 

#include <bits/stdc++.h>
#define N 22
#define mod 1000000007 
using namespace std; 
//define换const表达也行 例 const N=22; 

int n, t[N][1<<N], p=0, k[1<<N]; //1<<N=pow(2,22) 
vector<int> a[N]; 
int main()
{
	cin>>n;
	for (int i=1; i<=n; i++)
		for (int j=1; j<=n; j++)
		{
			int v;
			cin>>v;
			if (v==1) a[i].push_back(j); //第i本书和第j个书包可以匹配
		}
	
	t[0][0]=1; //边界条件 一般为1 
	p=(1<<n)-1; //书/书包的全集 
	
	//防止超时做出的优化 
	for (int s=1; s<=p; s++) //s:书包集和 
	{
		for (int i=1; i<=n; i++) //枚举第i个书包是否在s集和里 
		{
			int e=(1<<(i-1)); //例:i=4  e=1000 即第4位有书包 所以为1 
			if ((e&s)==e) //第i个书包在集和s内 
				k[s]++; //k:书包集和 记录书包个数 
		}
	}
	
	for (int i=1; i<=n; i++) //前i本书 
	{
		for (int w=1; w<=p; w++) //w:枚举书包集和 
		{
			if (i!=k[w]) continue; //书个数和书包个数不一样 结果肯定为0 省时间直接跳 
			//第i本书和那个书包匹配?  枚举a[i][j] 
			for (int j=0; j<a[i].size(); j++) //j:枚举书包 
			{
				//尝试书包e和第i本书匹配 
				int e=a[i][j], e1=(1<<(e-1));
				if ((e1&w)==e1) //可以匹配 
				{
					t[i][w]+=t[i-1][w-e1]; //拿了一本书 所以i-1  拿了一个书包 所以w-e1 
					t[i][w]%=mod; //防止溢出 边算边模 
				}
			}
		}
	}
	
	cout<<t[n][p];  
	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

 

 

样例解释

 

//记f[i]表示把集合i内的兔子分组最多能赚的钱,初始化为-oo,因为a数组有负数

//转移:考虑枚举i的一个子集j,把j集合的兔子分在一组,得到子问题f[i-j]

 

注意枚举子集要用3^n的方法,不然时间复杂度是4^n会超时。

 

以下是参考程序:

 

#include <bits/stdc++.h>
#define X 1000000000000LL
using namespace std;

int n, a[20][20];
long long val[1<<17], tot, f[1<<17];
int main() 
{
	cin>>n;
	for (int i=1; i<=n; i++)
		for (int j=1; j<=n; j++)
			cin>>a[i][j];
	
	tot=(1<<n)-1;
	for (int i=1; i<=tot; i++)
	{
		for (int j=1; j<n; j++)
		{
			int x=1<<(j-1);
			if ((x&i)!=x) continue;
			for (int k=j+1; k<=n; k++)
			{
				int y=1<<(k-1);
				if ((y&i)!=y) continue;
				val[i]+=a[j][k];
			}
		}
	}
	
	for (int i=1; i<=tot; i++)
		f[i]=-X;
	
	for (int i=1; i<=tot; i++)
		for (int j=i; j>0; j=((j-1)&i))
			f[i]=max(f[i], val[j]+f[i^j]);
	
	cout<<f[tot];
    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

 

 

样例解释

无‘

 

//记f[i][j]表示前i把钥匙,要把j集合所有的锁打开,需要最少费用。

//转移:考虑买或不买第i把钥匙即可转移

 

 

#include <bits/stdc++.h>
#define X 100000000
using namespace std;

int n, m, p[1010], k;
int key[1010], f[1010][1<<12], tot=0;
int main() 
{
	cin>>n>>m;
	for (int i=1; i<=m; i++)
	{
		int c;
		cin>>p[i]>>k;
		for (int j=1; j<=k; j++)
		{
			cin>>c;
			key[i]+=(1<<(c-1));
		}
	}
	tot=(1<<n)-1;
	for (int i=0; i<=m; i++)
		for (int j=1; j<=tot; j++)
			f[i][j]=X;
	
	for (int i=1; i<=m; i++)
		for (int s=1; s<=tot; s++)
		{
			f[i][s]=f[i-1][s];
			int t=((s&key[i])^s);
			f[i][s]=min(f[i-1][s], f[i-1][t]+p[i]);
		}
	
	if (f[m][tot]>=X) cout<<-1;
	else cout<<f[m][tot];
    return 0;
}

  

 

 

第4题     旅行商 查看测评数据信息

有三维立体空间里,有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号城市,问最少的旅行距离。

输入格式

 

第一行,一个整数n。2<=n<=17。

接下来有n行,第i行有三个整数:x[i],y[i],z[i]。-1e6<=x[i],y[i],z[i]<=1e6。

所有的城市坐标不会重叠。

 

输出格式

 

一个整数。

 

输入/输出例子1

输入:

2

0 0 0

1 2 3

 

 

输出:

9

 

 

输入/输出例子2

输入:

3

0 0 0

1 1 1

-1 -1 -1

 

 

输出:

10

 

 

输入/输出例子3

输入:

17

14142 13562 373095

-17320 508075 68877

223606 -79774 9979

-24494 -89742 783178

26457 513110 -64591

-282842 7124 -74619

31622 -77660 -168379

-33166 -24790 -3554

346410 16151 37755

-36055 51275 463989

37416 -573867 73941

-3872 -983346 207417

412310 56256 -17661

-42426 40687 -119285

43588 -989435 -40674

-447213 -59549 -99579

45825 7569 45584

 

 

输出:

6519344

 

 

样例解释

经过分析,根据三角形不等式,同一个城市经过多次不优。

记f[i][j]表示i城市是j集合中的一个城市,要从i城市遍历完j集合的每一个城市,然后返回1号城市的最少距离。

转移:令x=j-(1<<(i-1)),令k是x集合的某个城市,用f[x][k] +dis[i][k]更新f[i][j],注意边界,当集合只有一个元素i时:f[i][2^i-1] = dis[i][1]

 

 

 

 

 

 

第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

 

 

样例解释

 

记cnt[i]表示整数i的二进制有多少个1,记f[i]表示把i集合的所有学生放在队伍的最左边的cnt[i]个位置,有多少种合法的方案,最终本题答案就是f[(1<<n)-1]。如何转移呢?

1、首先判断f[i]是否合法,若把i集合放在最左边的cnt[i]个位置,是否满足结束点在cnt[i]的所有限制?即题目限制条件中的x参数,f[i]要满足所有x=cnt[i]的限制条件,否则f[i] = 0;

2、若第一点满足,那么考虑第cnt[i]个位置到底放哪个学生,设k是集合i的某个学生,若把第k个学生放到第cnt[i]个位置,那么子问题就是f[i-(1<<(k-1)),把子问题的答案累加到f[i]即可。

 

时间复杂度分析:状态数是2^n, 转移是O(n),但判断所有状态f[i]是否合法的时间复杂总共是多少?

因为假设n=18,我们发现当所有的m个限制的x[i]都是等于9时(贪心,如果x[i]不是9,需要检查的状态数更少),此时需要检查的次数是最多的,共有C(18,9)个状态需要检查,每个状态检查m次,每次消耗9次判断,

因此需要判断C(18,9) * m * 9次。

 

 

 

 

 

 

 

第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

 

 

样例解释

 

转移:只需要考虑b[x]和a数组集合i里面的哪个匹配即可。

image.png

image.png

image.png

image.png

 

 

#include <iostream>

#include <vector>

#include <algorithm>

#define inf 1e18

using namespace std;

typedef long long ll;


ll N, X, Y;

ll A[20], B[20];

ll dp[1<<18];


int f(int S, int x)

{

  int ret = 0;

  for(int p = 1; p <= N; p++){

    if((S & (1<<(p-1))) == 0 && p < x) ret++;

  }

  return ret;

}


int main(void)

{

  cin >> N >> X >> Y;

  for(int i = 1; i <= N; i++) cin >> A[i];

  for(int i = 1; i <= N; i++) cin >> B[i];


  dp[0] = 0;

  for(int S = 1; S < (1<<N); S++) dp[S] = inf;


  for(int S = 0; S < (1<<N); S++){

    int sizeS = 0;

    for(int i = 1; i <= N; i++) if(S & (1<<(i-1))) sizeS++;

    for(int x = 1; x <= N; x++){

      if(S & (1<<(x-1))) continue;

      dp[S|(1<<(x-1))] = min(dp[S|(1<<(x-1))], dp[S] + abs(A[x] - B[sizeS+1]) * X + f(S, x) * Y);

    }

  }

  cout << dp[(1<<N)-1] << endl;


  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

 

 

样例解释

 

 

记f[i][j]表示有字符串集合j,i是j里面的一个字符串,当前先手面对集合j,若当前先手选择字符串i,当前先手是否必赢。

1、若j只含一个字符串,f[i][j]=true;

2、若当前先手选择i字符串后,对手下一轮根本无法接龙,显然f[i][j]=true

3、若当前先手选择i字符串后,令x = j - (1<<(i-1)),设k是x集合里面的某个字符串;若存在f[x][k]=true,那么f[i][j]=false; 若所有的f[x][k]都是false, 则f[i][j]是true

最终答案的判断:

若存一个某个i,使得f[i][(1<<n)-1]是true,则输出First, 否则输出Second

image.png

image.png

以下是参考程序: