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

状压 dp --- 数据范围小

hello,大家好,我们又见面了!今天是2025年9月6日,我们继续开启 状压 dp 的学习。

一:简介

状压 dp 常用于解决的第三种问题就是当数据范围很小但是暴搜的话会超时。

这类问题相对于前两类问题比较灵活,但是也是有明显的特征:属于 n 很小,但是暴搜的话即使加上优化和剪枝也会超时,因此,我们可以往状压 dp 的方向去思考。

接下来的两大题目都会讲到之前没有了解过的新东西,既然大家点进来了,就不要跳过了,我相信大家看完后一定会有所收获。

二:经典题目

题目一:糖果

题目链接:糖果

在这道题目中,会讲述一种之前我们很少用到的推导状态转移方程的方式,希望大家一定不要跳过

【题目描述】

【算法原理】

解法一:状态压缩 + 最短路 + BFS(最优解)(这个解法前面已经讲过了,就不再赘述了)

解法二:状压 dp

对于这类数据范围很小的问题,我们首先应该想到的解法就是暴力搜索。但是这道题目暴搜策略不好想而且时间复杂度会非常高,因此我们考虑使用状压 dp 来解决这道问题。

1.状态表示:(经验 + 题目要求)

f[x]表示:凑成糖果的口味状态为 x 时,最少需要多少包糖果。

2.最终结果:

如果这样定义状态表示的话,根据题目要求,最终结果显然是 f[2 ^ m - 1];

3.状态转移方程:(考虑最近的一步来分析问题)

我们考虑最近的一步,看一看有哪些状态可以通过再购买一包糖果后更新到 x 上。

这样更新状态的话要保证 x 之前的状态全部已经计算出来了。但是我们发现,前面的状态并不好确定。对于一包糖果而言,可以找到很多前驱状态。

因此,我们换一种思维方式,使用 f[x] 去更新后继节点(类似于拓扑排序),这样的话,状态转移方程就很好推导了,直接将某一包的糖果状态按位或进去就行了。

f[x | a[i]] = min(f[x | a[i]],f[x] + 1); 

4.初始化:

所有格子初始化为正无穷,f[0] = 0 即可。(最初的状态 0 去更新后面的状态)

5.填表顺序:

保证 f[x] 更新完毕之后然后才能去更新后继结点,因此我们只需保证从大到小枚举状态即可。

最后解释一个问题:为什么这道题可以使用状压 dp 而《关灯问题II》这道题目却不能使用呢?

因为本题状态是不会回退的,《关灯问题II》这道题目状态有可能回退,图中会出现环路,存在环路时不能使用动态规划的!!!

【代码实现】

#include <iostream>
#include <cstring>using namespace std;const int N = 110, M = 21;int n, m, k;
int a[N];int f[1 << M];int main()
{cin >> n >> m >> k;for(int i = 0; i < n; i++)for(int j = 0; j < k; j++){int x; cin >> x; x--;a[i] |= (1 << x);}memset(f, 0x3f, sizeof f);f[0] = 0;// s 一定要从 0 开始,因为我们要用 0 去更新后面的值 for(int s = 0; s < (1 << m); s++)for(int i = 0; i < n; i++)f[s | a[i]] = min(f[s | a[i]], f[s] + 1);if(f[(1 << m) - 1] == 0x3f3f3f3f) cout << -1 << endl;else cout << f[(1 << m) - 1] << endl; return 0;
}

题目二:PRZ

题目链接:PRZ

【题目描述】

解释:一号和二号为一组,过桥时间为 24,三号自己一组,过桥时间为 18,所有人过桥的总时间最优为 24 + 18 = 42。

【前置知识】(很重要)

如何枚举一个状态的子状态:

类比我们之前学过的枚举子集:

枚举一个状态的子状态:

枚举方式:

for(int x = 0; x < (1 << n); x++)for(int y = x; ; y = x & (y - 1)){// y 就是 x 这个状态的一个子状态if(!y) break;}

解释:

【算法原理】

本题数据范围很小,但是暴搜的话很难找到一种剪枝策略,因为这道题要考虑的因素比较多。

我们尝试使用 状压 dp 的方式来解决一下这道问题:

1.状态表示(经验 + 题目要求):

f[x] 表示:已经过桥人的状态为 x 时,所花费的最少时间。

2.最终结果:

根据状态表示以及题目要求,显然最终结果为 f[(1 << n) - 1]。

3.状态转移方程(根据最近的一步分情况讨论):

假设当前过桥状态 x 为 1,2,4,6,7 已过桥:

根据最后一组过桥的人分情况讨论:

我们枚举他们这些人中所有的子集 y:

当W[x ^ y] <= w 时(保证剩下的那些人能一次过桥),f[x] = min(f[x],f[y] + T[x ^ y]);

4.初始化:

因为我们要求的是最小值,因此所有格子的值先初始化为正无穷,f[0] = 0;

5.填表顺序:

枚举子集就可以了~~~。

【代码实现】

#include <iostream>
#include <cstring>using namespace std;const int N = 18;int m, n;
int t[N], w[N];int T[1 << N], W[1 << N], f[1 << N];int main()
{cin >> m >> n;for(int i = 0; i < n; i++) cin >> t[i] >> w[i];// 预处理出每一个状态的最大时间和总重for(int s = 0; s < (1 << n); s++)for(int i = 0; i < n; i++){if((s >> i) & 1){T[s] = max(T[s], t[i]);W[s] += w[i];}} memset(f, 0x3f, sizeof f);f[0] = 0;for(int x = 0; x < (1 << n); x++)for(int y = x; ; y = x & (y - 1)){if(W[x ^ y] <= m) f[x] = min(f[x], f[y] + T[x ^ y]);if(!y) break;}cout << f[(1 << n) - 1] << endl;return 0;
}

好的,今天的分享就先到这里了,大家再见。

http://www.xdnf.cn/news/20238.html

相关文章:

  • 雪球科技Java开发工程师笔试题
  • happen-before原则
  • WSL Ubuntu Docker 代理自动配置教程
  • LeetCode 139. 单词拆分 - 动态规划解法详解
  • 【软考架构】第二章 计算机系统基础知识:计算机网络
  • 主数据系统是否对于企业是必需的?
  • 最大似然估计:损失函数的底层数学原理
  • 基本数据类型和包装类的区别?
  • 2025年大数据专业人士认证发展路径分析
  • MySQL运维补充
  • 【目录-判断】鸿蒙HarmonyOS开发者基础
  • 敏捷scrum管理实战经验总结
  • 贪心算法应用:化工反应器调度问题详解
  • 【LLIE专题】SIED:看穿0.0001lux的极致黑暗
  • NPU边缘推理识物系统
  • 懒加载的概念
  • 新能源风口正劲,“充电第一股”能链智电为何掉队?
  • 操作系统启动过程详解
  • Coze源码分析-资源库-删除插件-前端源码-核心组件实现
  • 03-生产问题-慢SQL-20250926
  • 机器人控制器开发(导航算法——导航栈关联坐标系)
  • 创客匠人:什么是“好的创始人IP”
  • 2025年本体论:公理与规则的挑战与趋势
  • CentOS系统停服,系统迁移Ubuntu LTS
  • 【CSS,DaisyUI】自定义选取内容的颜色主题
  • Android开发——初步了解AndroidManifest.xml
  • 零基础入门深度学习:从理论到实战,GitHub+开源资源全指南(2025最新版)
  • C++ 条件变量 通知 cv.notify_all() 先释放锁再通知
  • [光学原理与应用-428]:非线性光学 - 为什么要改变光的波长/频率,获得特点波长/频率的光?
  • RocketMQ如何处理消息堆积