SOS DP(子集DP)

感谢

文本中所选题目均来自于这里,感兴趣可以看看其它题,在这里特表感谢。

介绍

S O S ( S u m O v e r S u b s e t s ) D P SOS (Sum Over Subsets) DP SOS(SumOverSubsets)DP,即子集 D P DP DP,属于状压 D P DP DP的一种类型,可用于解决:

  1. 选择任意多个不重复的箱子,使每种物体至少选一个的方案数。
  2. n n n 个数,需要在 1 1 1 ~ K K K 1 < K ≤ n 1<K≤n 1<Kn)的区间内选择 2 2 2个数,使这 2 2 2个数的二进制与为 0 0 0

但事实上, S O S D P SOS DP SOSDP能解决的问题不止这些,仔细观察这些问题,也许你会发现它们都有一个共同点——信息传递的纽带是集合间的包含关系

但事实上,在没有分析这些问题前,是很难发现这一点的,因此我会在具体分析本题和 S O S D P SOS DP SOSDP的原理前,详细分析这三种问题。

补充

子数、原数

若一个二进制数 a a a 的每一位都 另一个二进制数 b b b a 、 b a、b ab 可以相等),则称 a a a b b b 的子数, b b b a a a 的原数。

一些简单结论

  1. 若集合 A 、 B 、 C A、B、C ABC 满足 A ⊆ B , B ⊆ C A ⊆ B,B ⊆ C ABBC ,则有 A ⊆ C A ⊆ C AC
  2. 在二进制下,若 A A A B B B 的子数,则有 A ≤ B A ≤ B AB

分析

问题一

[COCI2011-2012#6] KOŠARE

该问题的题面为:

n n n 个集合和 m m m 个元素,第 i i i 集合中有 k k ki 个元素: a a ai1 a a ai2 a a aik ,你需要求取出 X X X 1 ≤ X ≤ n 1 ≤ X ≤ n 1Xn )个集合且这些集合合并为一个大集合后包含 m m m 个元素的方案数。
定义两个方案不同当且仅当至少存在一个集合,在其中一个方案中被选择,在另一个方案中未被选择。

这种题目最暴力的做法当然是暴力枚举所有可能然后判断是否合法。

如果你学过状压 D P DP DP,也许会灵光一闪,将 m m m 个元素的选择转化为二进制数组 n u m num num (选和不选)以此来表示每个集合,然后试图用状压DP去解决这一问题,接着就被卡在了状态转移方程不好写。

当你把每个集合的状态压缩为整数之后,每个集合都拥有了自己的二进制编号,而答案集合从第 0 0 0 位到第 m − 1 m-1 m1 位上的数字均为 1 1 1

根据题意分析,就可以发现多个集合合并后的集合的二进制,其实就是多个集合对应的二进制的或和,合并操作其实就是二进制或!

再随便举个例子,比如 0110 0110 0110 要得到答案数 1111 1111 1111,则必须要和形如 1 X X 1 1XX1 1XX1 X X X 表示 0 0 0 1 1 1 )的二进制数进行或操作,而 0110 0110 0110 取反后的数是 1001 1001 1001,是符合条件的最小数,故用该数的取反数是一定可以的。

同时, 1011 、 1101 、 1111 1011、1101、1111 101111011111 也是可以的,而 1000 1000 1000 则不行。观察这四个数和 1001 1001 1001 的关系,就可以发现:取反数 ∈ ∈ 符合条件的二进制数的子数集

该结论的证明可以感性证明一下:

根据子数的定义可得,对于每一位,一定有原数 子数,而二进制的或操作从本质上来看,其实就是对两个二进制数的每一位取最大,故而若子数符合条件,原数一定符合条件。

又取反数是最小的符合条件的数,一定有子数 原数,故一定有取反数 ∈ ∈ 符合条件的二进制数的子数集,得证。

分析到这里之后,你就会发现状态转移方程还是很难写出来,想要求这个方案数好像只能用容斥原理,但是不好写,因为想要求二进制数 x ∈ x ∈ x 或和的子数集的方案数很难。

这个时候若是你不信邪,硬要去思考一下如果要用容斥怎么写,也许就会发现为什么很难求解:

某个二进制数的某一位为 1 1 1 ,则或和的这一位一定为 1 1 1 ,但或和的某一位为 1 1 1 无法推出每个二进制数的这一位都为 1 1 1 ,即子或和可以确定总或和,而总或和无法确定子或和

而在这个问题中,信息是由总传递给子的,故而用或和很难实现。

但是,与和则恰恰相反,它的信息是由总与和传递给子与和的,正好符合本题的信息传递特性,故而我们可以从与和的角度思考问题,举个例子:

1011 ∣ 1100 = 1111 1011 | 1100 = 1111 1011∣1100=1111 ,符合条件。

将三个数取反、把或改成与,就会很惊奇的发现:

0100 0100 0100 & 0011 = 0000 0011 = 0000 0011=0000等式仍成立

于是这种问题又可以换个思路了:

把所有数取反,求多个二进制数的与和为 0 0 0 的方案数,而这可以利用容斥原理来解决。

设函数 f ( x ) f(x) f(x) 表示 x = x = x= 多个集合与和的方案数, g ( x ) g(x) g(x) 表示子数集包括 x x x 的数的数量,则有:

f ( x ) = 2 g ( x ) − 1 f(x) = 2^{g(x)}-1 f(x)=2g(x)1
g ( x ) = ∑ i = 1 n [ n u m i g(x)=\sum_{i=1}^{n}[num_i g(x)=i=1n[numi & x = = x ] ⋅ g ( i ) x==x] \cdot g(i) x==x]g(i)

g ( x ) g(x) g(x) 的转移方程正是 S O S D P SOS DP SOSDP所优化的对象,具体怎么做下文会有具体说明。

问题二

Compatible Numbers

问题二与问题一相比最大的不同就是只选择两个数来对答案产生贡献,且选择的数有一定的限制。

这个问题其实就是求区间问题,所以一个最朴素的观察就是最终答案一定是区间所有答案中的最优答案。而问题在于,如何获取答案呢?

为了方便获取答案,我们把该题的答案设为所有答案中最大的那个。

感性发现一下,可以得出该结论:如果二进制数 a a a & b = 0 b=0 b=0,则必有 b b b a a a 的相反数的子数。

何以见得?

首先,设二进制数 a a a 的取反数为 b b b ,则必有 a a a & b = 0 b=0 b=0
思考一下二进制与运算的本质,可以发现二进制与运算其实就是取每一位的最小值。
若二进制数 a a a & c = 0 c=0 c=0,则两数的每一位的最小值一定为 0 0 0,而 b b b 是 a 中为 0 0 0 的数均为 1 1 1 的满足 a a a& b = 0 b=0 b=0的数,即 a 、 c a、c ac中的每一位一定是 ( 0 , 1 ) (0,1) (0,1) 的模式,而 a 、 c a、c ac 中每一位则有可能是 ( 0 , 0 ) (0,0) (0,0) 的模式,又由于 a a a 不变,故而 1 1 1 的变化一定出现在 b 、 c b、c bc 上,即每一位上一定有 b ≥ c b≥c bc ,由此可知 b ≥ c b≥c bc c c c b b b 的子数,得证。

于是我们设 f ( x ) f(x) f(x) 表示子数集包含 x x x 的数,则有:

对于所有 i ≥ 0 i≥0 i0 ,当 2 i 2^i 2i & x = 2 i x=2^i x=2i 时, f ( x ) = max ⁡ ( f ( x ) , f ( x − 2 i ) ) f(x) = \max(f(x),f(x-2^i)) f(x)=max(f(x),f(x2i))

对于所有 0 ≤ i ≤ K 0≤i≤K 0iK a n s = max ⁡ ( a n s , f ( i ) ) ans=\max(ans,f(i)) ans=max(ans,f(i))

实现

根据状态转移方程可以发现,最暴力的方法是O( 4 n 4^n 4n) ,只需要枚举所有集合以及每个集合的子集即可,但是这太暴力的,肯定不优,考虑如何优化。

根据上述问题的分析,我们可以发现,答案信息具有传递性,不是从原数传递到子数就是从子数传递到原数。根据这个传递性,我们可以往如何减少每次传递答案信息时所使用的子数个数这一方向上思考。

根据集合的性质可以发现,包含关系具有传递性,而事实上子数和原数之间也是存在这一关系的,类比集合即可。

根据这一性质可以发现,我们在统计某一个数的答案时,只需要把与它只相差 1 1 1 位的子数的完整答案统计过来即可,代码如下:

	for(int i=0;i<(1<<n);i++){
		f[i][-1]=A[i];//A数组保存了输入时就已经统计的答案信息
		for(int j=0;j<n;j++){
			if(i&(1<<j)){
				//此处写转移方程
			}
			else{
				dp[i][j]=dp[i][j-1];
			}
		}
		f[i]=dp[i][n-1];
	}

发现在统计答案时, d p [ i ] [ j ] dp[i][j] dp[i][j] 只受 d p [ x ] [ j − 1 ] dp[x][j-1] dp[x][j1]影响,故而可以从这里入手优化代码,类似于背包 D P DP DP的优化,优化后的代码如下:

	for(int i=0;i<(1<<n);i++){
		f[i]=A[i];
	}
	for(int i=0;i<n;i++){
		for(int j=0;j<(1<<n);j++){
			//此处写转移方程
		}
	}

总结

S O S D P SOS DP SOSDP 的核心在于答案信息的传递只沿着子数和原数间的包含关系,关于这一关系的运用可以成为列出状态转移方程的一大利器,而答案间的包含关系也是非常重要的考虑因素。

### 子集问题与动态规划算法实现 #### 动态规划的核心思想 动态规划是一种通过分解问题为更小的子问题来求解复杂问题的方法。这种方法特别适用于那些具有重叠子问题和最优子结构性质的问题[^1]。在处理子集和问题时,动态规划可以通过构建一张二维表格 `dp` 来存储中间状态的结果。 #### 子集和问题描述 给定一个正整数集合 \( S = \{x_1, x_2, ..., x_n\} \) 和目标值 \( c \),我们需要找到是否存在 \( S \) 的某个子集,其元素之和等于 \( c \)[^3]。此问题通常被称为子集和问题 (Subset Sum Problem)。 #### 动态规划的状态定义 设 `dp[i][j]` 表示前 \( i \) 个元素能否构成和为 \( j \)子集。初始条件为当 \( j=0 \) 时,任何数量的元素都可以组成和为零的情况(即空集)。因此有: \[ dp[0][0] = True \] 其他情况下,默认初始化为 False: \[ dp[i][j] = False \text{(for all } i,j>0)\] 转移方程如下: - 如果当前元素 \( x_i \leq j \), 则可以选择加入或者不加入该元素: \[ dp[i][j] = dp[i-1][j] \lor dp[i-1][j-x_i] \] - 否则只能选择不加入该元素: \[ dp[i][j] = dp[i-1][j] \] 最终答案就是查看 `dp[n][c]` 是否为真[^5]。 以下是基于上述逻辑的一种Python实现方式: ```python def subset_sum_dp(nums, target): n = len(nums) # 创建 DP 数组并初始化 dp = [[False]*(target+1) for _ in range(n+1)] # 当目标值为 0 时,总是可以由空集满足 for i in range(n+1): dp[i][0] = True # 填充 DP 表格 for i in range(1, n+1): for j in range(1, target+1): if nums[i-1] <= j: dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]] else: dp[i][j] = dp[i-1][j] return dp[n][target] # 测试数据 numbers = [3, 34, 4, 12, 5, 2] goal = 9 print(subset_sum_dp(numbers, goal)) # 输出应为True 或者 False ``` 以上代码实现了使用动态规划解决子集和问题的过程[^2]。 #### 性能分析 相比于简单的递归方法,这种动态规划解决方案的时间复杂度降低到了 O(nc),其中 n 是输入列表长度,而 c 是目标数值大小。尽管如此,在某些极端条件下仍可能存在效率瓶颈,比如非常大的目标值或过长的序列[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值