状压DP学习笔记
状压DP学习笔记
泛黄的春联还残留在墙上
依稀可见几个字岁岁平安
在我没回去过的老家米缸
爷爷用楷书写一个满
黄金葛爬满了雕花的门窗
夕阳斜斜映在斑驳的砖墙
铺着榉木板的屋内还弥漫
姥姥当年酿的豆瓣酱
我对着黑白照片开始想象
爸和妈 当年的模样
说着一口吴侬软语的姑娘
缓缓走过外滩
消失的旧时光 一九四三
在回忆的路上 时间变好慢
老街坊 小弄堂
是属于那年代白墙黑瓦的淡淡的忧伤
消失的旧时光 一九四三
回头看的片段 有一些风霜
老唱盘 旧皮箱
装满了明信片的铁盒里
藏着一片玫瑰花瓣
——周杰伦《上海一九四三》
持续点亮知识树,放的是最近新听的歌,挺隽永。但 ,难评。
不得不说是个很有用的知识点,可以用于骗分,很好的加强了我 DP
和位运算的能力。学起来痛苦,但是用处还不小。
但是这个算法的提示性比较明显,因为和状态相关的,一般范围都极小,大约是 这样的量级。
其实只是一种思想,但是在这之前,你要会用数来表示集合(这里的集合默认不可重)。
做法是这样的,比方说有一个集合 ,因为每个数字只出现一次,所以我们把他编入二进制编码 中,也就是对应位标记为 ,然后把这个二进制数转为 进制,也就是 。
这样的话可以使得无论这个不可重集长什么样,表示出来的数都不会重复。这就是他能够使用的本质原因,并且选择二进制的话因为他好写位运算。
再往深一点想,如果这个集合里面有数出现两次的话,那就不能用二进制了,因为这样会导致数重复,比如说 和 就是个例子。所以如果一个集合里面最多出现了 个相同的数,就应该使用 进制。
但是拿到一个数,怎么方便的处理属于他的集合呢?位运算就体现它的强大之处了。
- 空集:
0
。 - 仅有一个数 :
1<<i
。 - 集合 :
(1<<n)-1
,也就是 。 - 判断 中是否有 :
(S>>i)&1
,就是滚掉后 位,因为是从 开始标号的,最后判断末尾是不是 。 - 在 中加入 :
S|(1<<i)
。 - 在 中删除 :
S&~(1<<i)
。 - 遍历 的子集:
for(int i=0;i<(1<<n);i++)
,重点,原因是把这些子集看做在二进制 里面标号,有 种情况,并且每个互不相同,且一定小于 ,那就只能是 了。 - 枚举某个二进制编码
sub
的子集,不妨反过来,考虑在消掉一个数以后,但是又要把它后面的数不回来,这样才能不漏,综合这两步就是
int sup=sub;
do{
// Slove sub
sub=(sub-1)⊃
}while(sub!=sup)//end in -1
会到这里就差不多了,接下来用一道例题讲解:
首先这道题,如果直接考虑设 为当前点是否放置国王(放了多少个为 )的方案数的话,是会有后效性的,你可以考虑从当前点多往外面扩几层,然后就发现理论上的方程 有误,因为外面还有点能取到 且对当前无影响,发现状态数就要持续增长了。
并且我们发现他还受右边的影响,所以我们不妨激进一些,设 表示前 行的,当前行每个点是否放置国王, 表示放了多少个。就是从范围小入手设状态,设取了一个集合 就是状压的核心设计状态思路。
这样在转移的时候只用枚举所当前行的所有状态,并且直接从上一行的可能所有状态转移过来就行了。这样落实了所有的情况,杜绝了后效性,很理想。时间是,就是状态数乘上单次转移复杂度。暴力枚举所有可能情况就是状压的核心转移思路。
问题是,对于不同的 都要写一个 dp
,那就显得很冗杂了,所以这时候我们不妨把取的国王的位置用一个集合表示,然后再按照上面的方法转换成数,和转换成了 表示前 行, 行所选集合表示的数(也可以理解成选的位置标记为 最终代表的数)用了 个国王的方案数。
转移上面讲过,做完了。
看到数据范围 很小,考虑状压。
既然是状压,就考虑选了一个集合 会怎么样,不难发现,这里可以考虑选择走过的点当作集合,那么把走过的点对应的位置标记成 ,就能把它抽象成一个数了。
发现只有 还不够,设 表示集合 ,走到的最后一个点是 的最小路程,这里的起点是 。
转移的话就直接考虑枚举还没有走过的点(也就是集合内标记为 的点),对他们进行更新,发现我们需要求出 到 的最短距离。
题目中限制了单次路程不能超过 ,转换成两个点的距离不能超过 ,可以每飞两个点就加一次油。找到这些边(因为能使用的边只有这些),然后跑最短路,就可以知道在限制下两个点的最短路。
答案就是枚举最后一个点的可能,加上他到第一个点的最短距离就是答案。
你要知道,double
初始化不要用 memset
。赋值出来极其小。
数据范围很小,直接考虑状压或者暴搜,这里考虑状压。
那就需要考虑我们选了一个集合 ,很自然能够想到把每个矩形是/否涂过色标记为 ,从而衍生为一个集合。
转移的话也很简单,在枚举所有集合 的时候,直接枚举可以使用的颜色,大力模拟出来使用了当前画笔后的集合情况为 ,方程就是:
思路看起来很清晰,但是实际操作起来难度比较大,写到吐。
开始的时候应该先排序,按照左上角的横坐标来排,这样才能保证在从上往下覆盖的时候不会漏掉矩形。
在判断矩形的时候,考虑直接用一个数组标记矩形靠上的那条长上的所有点的覆盖情况,注意,当前矩形靠上的长和另一个矩形靠下的长在同一直线,但是交集为 的时候,题目不认为这属于“覆盖”的范畴,那么这个点就不能被标记了。
当然你也不能用一个二维数组去搞,因为很多点都是重合的,这样会漏掉信息。
同时可以发现,按照我们这样的设法,这题的状态数其实不会很多,跑起来飞快。
- 特别注意数组清空问题,不要忘记清空数组,在几行代码上面想想有没有在解决几个不同问题的时候(比如 )用了一个数组,但没有清空?
- 调试
DP
的时候,一个比较愚蠢但是有效的方法就是你考虑把样例的转移过程你自己先模拟一遍,(一来检验思路有没有错),二者套回去代码里面进行一个检查。
P1879 [USACO06NOV] Corn Fields G
和例题是同一个类型的题,还是考虑选了某一行的某些点来种植,表示成一个集合即可。
注意模数,以及在初始化的时候手动计算了第一行的答案,但是考虑如果只有一行的情况?
一遍过样例,挺好。
P2915 [USACO08NOV] Mixed Up Cows G
在序列上进行一个状压,从选的集合来看,就是设 表示选了 集合,末尾是 的方案数。
转移的话直接枚举下一个合法的点即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?