当前位置: 首页 > news >正文

状压DP-子集枚举技巧

状压DP进阶-子集枚举技巧

    • 一、子集枚举的核心意义与问题
      • 1.1 为什么需要子集枚举?
      • 1.2 暴力枚举的局限性
    • 二、子集枚举的经典技巧
      • 2.1 技巧1:标准子集枚举(迭代法)
      • 2.2 技巧2:非空子集枚举
      • 2.3 技巧3:真子集枚举(排除自身)
      • 2.4 技巧4:按元素数量枚举子集
    • 三、经典案例:子集枚举在状压DP中的应用
      • 3.1 集合划分的最小代价
        • 问题描述
        • 状压DP设计
        • 核心部分代码实现
      • 3.2 子集贡献累加(SOS DP)
        • 问题描述
        • 高效解法:SOS DP(Sum Over Subsets)
    • 四、子集枚举优化与注意事项
      • 4.1 优化技巧
      • 4.2 注意事项
      • 4.3 总结

在状压DP中“子集枚举”是处理集合相关问题的核心操作,许多场景需要遍历某个状态的所有子集(如“从已选元素中拆分出一个子集进行转移”“枚举子集计算贡献”),直接暴力枚举会导致时间复杂度爆炸。

一、子集枚举的核心意义与问题

1.1 为什么需要子集枚举?

状压DP中,状态通常用二进制mask表示元素的选择情况。当问题涉及“将集合拆分为两个子集”“从集合中选择一个子集满足特定条件”时,需要枚举mask的所有子集。例如:

  • 集合划分问题:将mask表示的集合拆分为两个不相交子集aba | b = maska & b = 0),计算最优划分方案。
  • 贡献累加问题:对mask的所有子集sub,累加sub对应的价值到mask的状态中。

1.2 暴力枚举的局限性

maskk个1,则其子集数量为2^k。当k=20时,子集数量超过百万,暴力枚举(遍历所有可能的sub并检查sub是否为mask的子集)会导致时间复杂度达到O(2^n * 2^n) = O(4^n),对于n=20就是1e12级操作,完全不可行。

因此,必须掌握高效的子集枚举技巧,利用位运算特性减少无效遍历。

二、子集枚举的经典技巧

2.1 技巧1:标准子集枚举(迭代法)

核心思想:利用位运算submask = (submask - 1) & mask,从mask开始,每次生成比当前submask小的最大子集,直到submask为0。

原理

  • submask - 1会将submask的最后一个1变为0,并将其后的0变为1;
  • mask进行&运算,确保submask始终是mask的子集(仅保留mask中为1的位)。

代码示例

// 枚举mask的所有子集(包括空集和mask本身)
void enumerateSubsets(int mask) {for (int sub = mask; ; sub = (sub - 1) & mask) {// 处理子集subSystem.out.println(sub);if (sub == 0) break; // 终止条件}
}

示例mask = 6(二进制110

  • 第一次循环:sub = 110(6)
  • 第二次:sub = (110-1) & 110 = 101 & 110 = 100(4)
  • 第三次:sub = (100-1) & 110 = 011 & 110 = 010(2)
  • 第四次:sub = (010-1) & 110 = 001 & 110 = 000(0),循环结束。

时间复杂度O(2^k),其中kmask中1的个数(仅遍历有效子集)。

2.2 技巧2:非空子集枚举

场景:需排除空集的情况(如“至少选择一个元素的子集”)。

实现:在标准枚举基础上,当sub == 0时提前终止(不处理空集):

// 枚举mask的所有非空子集
void enumerateNonEmptySubsets(int mask) {for (int sub = mask; sub > 0; sub = (sub - 1) & mask) {// 处理非空子集subSystem.out.println(sub);}
}

示例mask=6时,输出6,4,2(跳过空集)。

2.3 技巧3:真子集枚举(排除自身)

场景:需排除mask本身的子集(如“拆分集合为两个非空子集”)。

实现:在标准枚举中,当sub == mask时跳过:

// 枚举mask的所有真子集(不包括mask本身)
void enumerateProperSubsets(int mask) {for (int sub = (mask - 1) & mask; ; sub = (sub - 1) & mask) {// 处理真子集subSystem.out.println(sub);if (sub == 0) break;}
}

原理:初始sub设为(mask-1) & mask,直接跳过mask本身。

2.4 技巧4:按元素数量枚举子集

场景:需按子集大小(1的个数)分组处理(如“先处理大小为k的子集,再处理k+1的”)。

实现:预处理每个mask中1的数量(bitCount),按数量分组存储:

List<Integer>[] subsetsBySize = new List[n+1]; // subsetsBySize[k]存储所有含k个1的mask
static {for (int i = 0; i <= n; i++) {subsetsBySize[i] = new ArrayList<>();}for (int mask = 0; mask < (1 << n); mask++) {int cnt = Integer.bitCount(mask);subsetsBySize[cnt].add(mask);}
}// 按元素数量从小到大枚举子集
for (int k = 1; k <= n; k++) {for (int mask : subsetsBySize[k]) {// 处理大小为k的mask// 再枚举其大小为t的子集(t < k)for (int sub = mask; sub > 0; sub = (sub - 1) & mask) {if (Integer.bitCount(sub) == t) {// 处理特定大小的子集}}}
}

三、经典案例:子集枚举在状压DP中的应用

3.1 集合划分的最小代价

问题描述

给定n个元素,每个子集S有代价cost[S]。将全集Umask = (1<<n)-1)划分为k个不相交非空子集S_1, S_2, ..., S_k,总代价为各子集代价之和,求最小总代价。

状压DP设计
  1. 状态定义dp[mask][t]表示将mask划分为t个子集的最小代价。
  2. 状态转移
    • 初始:dp[sub][1] = cost[sub](单个子集的代价)。
    • t > 1,枚举mask的非空真子集sub,则:
      dp[mask][t] = min(dp[mask][t], dp[sub][t-1] + dp[mask ^ sub][1])
      (将mask拆分为submask^sub,前者划分为t-1个,后者为1个)。
  3. 结果dp[full_mask][k]
核心部分代码实现
int n = 5;
int k = 2;
int fullMask = (1 << n) - 1;
int[] cost = new int[1 << n]; // 预处理每个子集的代价
int[][] dp = new int[1 << n][k + 1];
for (int[] row : dp) Arrays.fill(row, INF);// 初始化:t=1的情况
for (int sub = 1; sub <= fullMask; sub++) {dp[sub][1] = cost[sub];
}// 填充dp[mask][t]
for (int t = 2; t <= k; t++) {for (int mask = 1; mask <= fullMask; mask++) {// 枚举mask的非空真子集subfor (int sub = (mask - 1) & mask; sub > 0; sub = (sub - 1) & mask) {int rest = mask ^ sub; // 剩余部分if (dp[sub][t-1] != INF && dp[rest][1] != INF) {dp[mask][t] = Math.min(dp[mask][t], dp[sub][t-1] + dp[rest][1]);}}}
}System.out.println(dp[fullMask][k]);

关键:用sub = (mask-1) & mask枚举真子集,避免重复计算subrest(因sub < rest时可跳过,减少一半运算)。

3.2 子集贡献累加(SOS DP)

问题描述

给定数组a[mask],对每个mask,计算其所有子集suba[sub]之和,即sum[mask] = Σa[sub]submask的子集)。

高效解法:SOS DP(Sum Over Subsets)

传统方法:对每个mask枚举所有子集sub累加,时间O(4^n)
优化方法:利用二进制位的递进关系,按位更新sum[mask]

  1. 状态定义sum[mask]mask所有子集的a[sub]之和。
  2. 转移:对每个位i,若mask的第i位为1,则sum[mask] = sum[mask] + sum[mask ^ (1<<i)](包含第i位和不包含第i位的子集之和)。

代码实现

int n = 5;
int[] a = new int[1 << n]; // 原始数组
int[] sum = new int[1 << n];
// 初始化sum为a的副本
System.arraycopy(a, 0, sum, 0, 1 << n);// SOS DP:按位更新
for (int i = 0; i < n; i++) {for (int mask = 0; mask < (1 << n); mask++) {if ((mask & (1 << i)) != 0) { // 若mask包含第i位sum[mask] += sum[mask ^ (1 << i)];}}
}

原理:通过按位迭代,每个masksum值由其不包含第i位的子集mask ^ (1<<i)sum累加而来,时间复杂度降至O(n * 2^n)

四、子集枚举优化与注意事项

4.1 优化技巧

  1. 剪枝无效子集

    • 若子集sub不满足问题条件(如代价超过阈值),直接跳过。
    • 利用对称性:若submask^sub对称(如代价相同),可只处理一次。
  2. 预处理加速

    • 预计算每个mask的子集列表,避免重复枚举(适用于多次查询的场景)。
    • 预计算mask中1的位置,快速定位可拆分的元素。
  3. 位运算加速

    • Integer.bitCount(sub)快速获取子集大小,避免重复计算。
    • (sub & -sub)获取子集的最低位1,用于特定场景的拆分(如逐个元素移除)。

4.2 注意事项

  • 时间复杂度上限:即使优化后,子集枚举的时间复杂度仍与2^k相关,因此仅适用于n≤20的问题(2^20≈1e6,可接受)。
  • 空集与全集的处理:根据问题需求明确是否包含空集或全集,避免边界错误。
  • 位运算优先级:位运算优先级低于算术运算,需注意加括号(如(sub - 1) & mask而非sub - 1 & mask)。

4.3 总结

子集枚举是状压DP中处理集合拆分、贡献累加等问题的核心技巧,其效率直接决定算法可行性。本文介绍的四大枚举方法各有适用场景:

  • 标准迭代法(sub = (sub-1) & mask)是通用基础,适用于大多数子集枚举场景;
  • 非空/真子集枚举通过调整循环条件,减少无效状态处理;
  • 按大小枚举结合预处理,适合分阶段处理的问题;
  • SOS DP则是子集求和的专用优化方法,将复杂度从O(4^n)降至O(n*2^n)

掌握这些技巧的关键是:

  1. 理解位运算对二进制状态的操控逻辑;
  2. 根据问题特性选择合适的枚举方式;
  3. 结合预处理和剪枝进一步优化性能。

That’s all, thanks for reading~~
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

http://www.dtcms.com/a/319405.html

相关文章:

  • MySQL UNION 操作符详细说明
  • 机器视觉系统工业相机的成像原理及如何选型
  • 数据结构-哈希表(散列表)
  • 进程控制:进程的创建、终止、阻塞、唤醒、切换等生命周期管理操作
  • 基于深度学习的调制信号分类识别算法的研究生学习之旅
  • C语言sprintf、strcmp、strcpy、strcat函数详解:字符串操作的核心工具
  • Modbus转Profinet网关与西门子PLC的互联配置案例:用于永宏品牌变频器的控制实现
  • 一个基于 epoll 实现的多路复用 TCP 服务器程序,相比 select 和 poll 具有更高的效率
  • 并发编程(三)线程模型和通信
  • 【AI算法承载】海思3516DV500+IMX664方案一体机芯,开放AI算法部署二次开发
  • 蓝桥杯----数码管、按键、定时器与中断
  • PTrade详细介绍
  • 【遥感图像入门】遥感中的“景”是什么意思?
  • 深入理解 ReentrantLock和AQS底层源码
  • 专题:2025财务转型与AI赋能数字化报告|附30+份报告PDF汇总下载
  • 《深入解析缓存三大难题:穿透、雪崩、击穿及应对之道》
  • cv2.threshold cv2.morphologyEx
  • 宝塔面板配置Nacos集群
  • Plant Biotechnol J(IF=10.5)|DAP-seq助力揭示葡萄白粉病抗性机制
  • 什么是POE接口?通俗理解
  • Pytest项目_day07(pytest)
  • MySql MVCC的原理总结
  • S7-1200 串行通信介绍
  • 配送算法9 A GRASP algorithm for the Meal Delivery Routing Problem
  • React 中 useRef 使用方法
  • 设计模式 观察者模式
  • react-router/react-router-dom
  • 对话访谈|盘古信息×冠捷科技:全球制造标杆的智能化密码
  • 鸿蒙类型转化Json转map
  • 【实录】NestJS 中的 IoC