题目
质数、素数、合数
- 质数(prime number)又称素数,有无限个。一个大于1的自然数,除了1和它本身外,不能被其他自然数整除,换句话说就是该数除了1和它本身以外不再有其他的因数;否则称为合数
- 根据算术基本定理,每一个比1大的整数,要么本身是一个质数,要么可以写成一系列质数的乘积;而且如果不考虑这些质数在乘积中的顺序,那么写出来的形式是唯一的。最小的质数是
2
- 合数,数学用语,英文名为Composite number,指自然数中除了能被1和本身整除外,还能被其他的数整除(不包括0)的数。与之相对的是质数(因数只有1和它本身,如
2,3,5,7,11,13
等等,也称素数),而1既不属于质数也不属于合数。最小的合数是4 - 素数就是质数
关于动态规划(dp)
相似性
可以用动态规划解决的题目,通常具有一定的相似性,这些相似性包括但不限于:
- 通常该问题是一个求最优解的问题
- 该问题一定具有最优子结构
- 一般具有重叠子问题的性质
- 可以抽象为一个状态,不同的状态之间可以转移
概念讲解:
- 最优子结构:如果一个问题的最优解中包含了子问题的最优解,这个性质叫最优子结构(局部最优 -> 全局最优);求解动态规划问题的重点就是要描述这个问题的最优子结构;
- 重叠子问题:就是字面上的意思,进一步解释就是在求解的过程中,碰到的子问题可能已经出现过了
- 状态:一个问题可以抽象为一个状态,如何定义状态在动态规划中特别重要(因为定义状态就相当于描述了这个问题的最优子结构);
状态定义
所有的dp是解决多阶段决策最优化问题的一种思想方法
请注意多阶段这三个字:
如何定义状态是解决动态规划最重要的一步;
状态的定义也就决定了阶段的划分;状态表示了求解问题的某个阶段;
在背包问题中,通过物品的件数 i i i和背包的容量 j j j来定义状态或者说是划分阶段;
动态规划一个重要的特性就是无后效性。无后效性就是指对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的发展,而只能通过当前的这个状态。换句话说影响当前阶段状态只可能是前一阶段的状态;
可以看出如何定义状态是至关重要的,因为状态决定了阶段的划分,阶段的划分保证了无后效性。
状压dp
简介
状压dp是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的
为了达到最优子结构和无后效性的效果,必须要定义好状态。但是有时候状态维度特别多,但是每个状态的决策又很少,这样开多维数组很可能会浪费,并且可能会爆空间。这时候考虑用状态压缩来做,比如每个状态的决策只有两个,但是状态的维度很多。
举例
下面用01背包来举例:
有 n n n件物品和一个容量为 v v v的背包。放入第 i i i件物品的占的空间为 C i C_i Ci,得到的价值是 W i W_i Wi;求解每种放法的背包价值;(这不是一个典型的动态规划问题,但是用动态规划的思想有助于讲解状压dp);
- 定义状态
因为要求每一种放法的背包价值,所以状态应该是这
n
n
n件物品的放与不放的情况。最容易想到的是开个
n
n
n维数组,第
i
i
i个维度的下标如果是
1
1
1的话代表放第
i
i
i件物品,
0
0
0的话代表不放第
i
i
i件物品;但是这样很容易造成空间浪费;仔细观察就会发现,每件物品有放与不放两种选择;假设有5件物品的时候,用1和0代表放和不放,如果这5件物品都不放的话,那就是00000
;如果这5件物品都放的话,那就是11111
;
因此可以用二进制表示所有物品的放与不放的情况;二进制用十进制表示的话就只有 一个维度了。而且这一个维度能表示所有物品放与不放的情况,这个过程就叫做状态压缩; 00000 − 11111 00000 - 11111 00000−11111可以代表所有的情况,转化为十进制就是 0 ( 1 < < 5 − 1 ) 0~(1<<5 - 1) 0 (1<<5−1);
- 状态转移
放的状态只能从不放的状态转移过来,所以dp[10000]
只能从dp[00000] + W[1]
转移过来;dp[11000]
可以从dp[01000] + W[1]
或者dp[10000] + W[2]
转移过来
- 按一个方向求出该问题的解
该问题并不是一个典型的动态规划问题,要求的解并不是求一个最优值
import java.util.Scanner;
/**
* 有 n 件物品和一个容量为 v 的背包。放入第 i 件物品的占的空间为 C_i ,得到的价值是 W_i ;求解每种放法的背包价值
* 样例:
* 4
* 3 6
* 2 5
* 3 8
* 4 9
* 占用空间 价值
*/
public class KnapsackStateCompressionDP {
public static void main(String[] args) {
int INF = 1 << 15;
Scanner sc = new Scanner(System.in);
// 物品数量
int n = sc.nextInt();
// 价值
int[] W = new int[n];
// 空间
int[] C = new int[n];
for (int i = 0; i < n; i++) {
C[i] = sc.nextInt();
W[i] = sc.nextInt();
}
int[] dp = new int[INF +10];
int[] dp1 = new int[INF + 10];
for (int i = 0; i < (1 << n); i++) {
for (int j = 0; j < n; j++) {
// i 放法中 没放 第 j 件物品
if ((i & (1 << j)) == 0) {
int temp = i | (1 << j);
// temp 放法的价值
dp[temp] = dp[i] + W[j];
// temp 放法占用的空间
dp1[temp] = dp1[i] + C[j];
}
}
}
for (int i = 0; i < (1 << n); i++) {
// 方案 i
myPrint(i);
System.out.print("\t");
// 方案 i 的价值
System.out.print(dp[i]);
System.out.print("\t");
// 方案 i 的占用空间
System.out.print(dp1[i]);
System.out.println();
}
}
public static void myPrint(int num) {
int k = 0;
StringBuilder sb = new StringBuilder();
if (num == 0) {
sb.append("0");
}
for (; (1 << k) <= num; k++) {
if ((num & (1 << k)) != 0) {
sb.append("1");
} else {
sb.append("0");
}
}
System.out.print(sb.reverse().toString());
}
}
状压dp的特点一般是规模比较小,n一般小于15。而且一般只有两种决策
状态压缩基础
-
元素c插入集合A:
A |= (1 << c)
-
A删除c:
A &= ~(1 << c)
-
A置空:
A=0
-
并集:
A | B
-
交集:
A & B
-
判断A是否是B的子集:
(A & B) == A
-
全集:
(1 << n) - 1
-
补集:
((1 << n) - 1) ^ A
-
获取最低位的1:
x & -x
-
最低位的1变为0:
n = n & (n - 1)
-
判断是否是2的幂,也就是去除最低位的1之后为0:
A & (A - 1) == 0
-
获取最高位的1
int v = x & -x; while (v != x) { x -= v; v = x & -x; } return v;
-
枚举A的子集
for (int sub = A & (A - 1); sub != A; sub = (sub - 1) & A) { }
本题思路
题目规定数组中的元素不超过 30
,因此可以将 [1, 30]
中的整数分成如下三类:
-
1
:对于任意一个好子集而言,添加任意数目的 1,得到的新子集仍然是好子集; -
2,3,5,6,7,10,11,13,14,15,17,19,21,22,23,26,29,30
:这些数均不包含平方因子,因此每个数在好子集中至多出现一次; -
4,8,9,12,16,18,20,24,25,27,28
:这些数包含平方因子,因此一定不能在好子集中出现
可以通过硬编码的方式把 [1, 30]
中的整数按照上述分类,也可以先预处理出所有 [1, 30]
中质数 2,3,5,7,11,13,17,19,23,29
,再通过试除的方式动态分类。
由于每个质因数只能出现一次,并且 [1, 30]
中一共有 10
个质数,因此可以用一个长度为 10
的二进制数 mask
表示这些质因数的使用情况,其中 mask
的第 i
位为 1
当且仅当第 i
个质数已经被使用过(状态压缩)。
状态定义
定义f[i][mask]
表示当只选择 [2,i]
范围内的数,并且选择的数的质因数使用情况为 mask
时的方案数。
状态转移
如果 i
本身包含平方因子,那么无法选择 i
,相当于在 [2, i-1]
范围内选择,状态转移方程为:
f
[
i
]
[
m
a
s
k
]
=
f
[
i
−
1
]
[
m
a
s
k
]
f[i][mask]=f[i-1][mask]
f[i][mask]=f[i−1][mask]
如果 i
本身不包含平方因子,记其包含的质因子的二进制表示为 subset
(同样可以通过试除的方法得到),那么状态转移方程为:
f
[
i
]
[
m
a
s
k
]
=
f
[
i
−
1
]
[
m
a
s
k
]
+
f
[
i
−
1
]
[
m
a
s
k
∧
s
u
b
s
e
t
]
×
f
r
e
q
[
i
]
f[i][mask]=f[i-1][mask]+f[i-1][mask^{\,\wedge}subset] \times freq[i]
f[i][mask]=f[i−1][mask]+f[i−1][mask∧subset]×freq[i]
其中:
-
freq[i]
表示数组nums
中i
出现的次数; -
mask^subset
表示从二进制表示mask
中去除所有在subset
中出现的1
,使用按位异或运算实现。这里需要保证subset
是mask
的子集,可以使用按位与运算来判断
动态规划的边界条件为:
f
[
1
]
[
0
]
=
2
f
r
e
q
[
1
]
f[1][0]=2^{freq[1]}
f[1][0]=2freq[1]
按一个方向求出该问题的解
最终的答案即为所有 f[30][..]
中除了 f[30][0]
以外的项的总和。
class Solution {
static final int[] PRIMES = {2,3,5,7,11,13,17,19,23,29};
static final int NUM_MAX = 30;
static final int MOD = 1000000007;
static final int MASK = 1 << PRIMES.length;
public int numberOfGoodSubsets(int[] nums) {
int[] freq = new int[NUM_MAX + 1];
for (int num : nums) {
freq[num]++;
}
int[][] f = new int[NUM_MAX + 1][MASK];
// 边界条件
f[1][0] = 1;
for (int i = 0; i < freq[1]; i++) {
f[1][0] = f[1][0] * 2 % MOD;
}
for (int i = 2; i <= NUM_MAX; i++) {
for (int s = 0; s < MASK; s++) {
f[i][s] = f[i - 1][s];
}
// nums 中不包括 i 这个数
if (freq[i] == 0) {
continue ;
}
// 检查 i 的每个质因数是否均不超过 1 个
int subset = 0, x = i;
boolean check = true;
for (int j = 0; j < PRIMES.length; j++) {
int prime = PRIMES[j];
if (x % (prime * prime) == 0) {
check = false;
break;
}
// i 包含的质因子的二进制表示为 subset
if (x % prime == 0) {
subset |= (1 << j);
}
}
if (!check) {
continue ;
}
// 动态规划
// check = true 表示数字 i 可以放入集合中,且只能放入一个
for (int mask = 0; mask < MASK; mask++) {
if ((mask & subset) == subset) {
f[i][mask] = (int) ((f[i - 1][mask] + ((long)f[i-1][mask ^ subset]) * freq[i]) % MOD); }
}
}
int ans = 0;
for (int i = 1; i < MASK; i++) {
ans = (ans + f[30][i]) % MOD;
}
return ans;
}
}
空间优化
注意到 f[i][mask]
只会从 f[i−1][..]
转移而来,并且 f[i−1][..]
中的下标总是小于 mask
,因此可以使用类似 0−1 背包的空间优化方法,在遍历 mask
时从
2
10
−
1
2^{10}-1
210−1 到 1 逆序遍历,这样就只需要使用一个长度为
2
10
2^{10}
210 的一维数组做状态转移了
class Solution {
static final int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static final int NUM_MAX = 30;
static final int MOD = 1000000007;
static final int MASK = 1 << PRIMES.length;
public int numberOfGoodSubsets(int[] nums) {
int[] freq = new int[NUM_MAX + 1];
for (int num : nums) {
++freq[num];
}
int[] f = new int[MASK];
f[0] = 1;
for (int i = 0; i < freq[1]; ++i) {
f[0] = f[0] * 2 % MOD;
}
for (int i = 2; i <= NUM_MAX; ++i) {
if (freq[i] == 0) {
continue;
}
// 检查 i 的每个质因数是否均不超过 1 个
int subset = 0, x = i;
boolean check = true;
for (int j = 0; j < PRIMES.length; ++j) {
int prime = PRIMES[j];
if (x % (prime * prime) == 0) {
check = false;
break;
}
if (x % prime == 0) {
subset |= (1 << j);
}
}
if (!check) {
continue;
}
// 动态规划
for (int mask = MASK - 1; mask > 0; --mask) {
if ((mask & subset) == subset) {
f[mask] = (int) ((f[mask] + ((long) f[mask ^ subset]) * freq[i]) % MOD);
}
}
}
int ans = 0;
for (int mask = 1; mask < MASK; ++mask) {
ans = (ans + f[mask]) % MOD;
}
return ans;
}
}
-
时间复杂度: O ( n + C × 2 π ( C ) ) O(n + C \times 2^{\pi(C)}) O(n+C×2π(C))。其中 n n n 是数组 n u m s nums nums 的长度, C C C 是 n u m s nums nums 元素的最大值,在本题中
C=30
, π ( x ) \pi(x) π(x) 表示 ≤ x \leq x ≤x 的质数的个数。- 一共需要考虑 O ( C ) O(C) O(C) 个数,每个数需要 O ( 2 π ( C ) ) O(2^{\pi(C)}) O(2π(C)) 的时间计算动态规划;
- 在初始时还需要遍历一遍所有的数,时间复杂度为 O ( n ) O(n) O(n)。
-
空间复杂度: O ( 2 π ( C ) ) O(2^{\pi(C)}) O(2π(C)),即为动态规划需要使用的空间。