状压 DP 做题记录
状压 DP
状压是一种把集合映射到数字的方法,通常是二进制状压,每一位的 01 表示集合是否含有这一项。状压可以大幅度减少与集合有关的 DP 写法上的复杂性,而且用上位运算相关的技巧还可以优化复杂度,是一种常见的方法。
一般的状压是往一个集合中加入或删除一个元素进行转移,也有枚举子集、超集进行转移,这一类有可以用高维前后缀和优化,还有些可以用 FWT、子集卷积优化。
状态的设计肯定是基于题目中实际数据范围小的量,然后判断转移是添加、删除元素还是枚举子集一类。
一种很优雅的枚举子集方法:
for(int T=S;T;T=(T-1)&S){
f[T]->f[S]
}
CF11D A Simple Task
题意:求简单无向图的环数。
思路:设 \(f[S][i]\) 表示走过了 \(S\) 中的点,出发点是编号最小的点,终点是 \(i\) 的方案数,转移有 \(f[S][j]\rightarrow f[S\cup\{k\}][k]\),这样每条边会被算两次,最后除掉就可以了。
trick:对于状态,可以钦定编号最小的点是关键点,减少状态量。
P5912 [POI2004] JAS
题意:求最浅的点分树深度。
思路:相当于找深度最浅的点分树。设 \(d_i\) 表示 \(i\) 号点是倒数第几次被找到的,那么就要求如果有两个点满足 \(d_i=d_j\),那么就 \(i,j\) 的路径上一定存在一个点 \(k\)满足 \(d_k>d_i\),就相当于是 \(i,j\) 在点分树上的祖先,这样就可以从叶子开始往上不断给节点赋值。由于点分树的性质,深度是 \(O(\log n)\) 的,于是可以做到 \(O(n\log n)\)。但是因为深度只有 \(O(\log n)\),因此可以用状压,这样可以用位运算做到 \(O(n)\)。
[APC001F] XOR Tree
题意:给一棵有 \(N\) 个节点的树,节点编号从 \(0\) 到 \(N-1\),
树边编号从 \(1\) 到 \(N-1\)。第 \(i\) 条边连接节点 \(x_i\) 和 \(y_i\),其权值为 \(a_i\)。
你可以对树执行任意次操作,每次操作选取一条链和一个非负整数 \(x\),将链上的边的权值与 \(x\) 异或成为该边的新权值。
问最少需要多少次操作,使得所有边的权值都为 \(0\)。
思路:首先边权不好处理,于是试着转成点权,即每个点的点权是与它相连的所有边权的异或和,这样每次操作一条链就是操作两个端点,于是原问题就转化成了有一些数,每次可以选择两个数异或上一个数,求使得所有数是0的最小操作次数。我们把相等的数两两配对,这样最后只会剩下不到16个数。
接着就可以直接状压,因为只有所有数异或和为 0 才能消成 0,所以对于一个状态 \(S\) 可以枚举一个异或和为 0 的子集 \(T\),然后直接转移。
trick:将链操作通过异或差分为对单点的操作,然后通过减少状态量来使得可以支持状压。
P6499 [COCI2016-2017#2] Burza
思路:好题!
一开始没细想 既定的策略,然后一直以为贪心是对的,后来才发现问题。不过好在是想到了复杂度的来源: \(k<\sqrt{n}\) 才有用,证明类似长剖,然后复杂度就是 \(O(2^{\sqrt{n}})\)。
正解:首先,每次我们肯定是选择一个深度为 \(i\) 的点,目标是不让另一个让走到深度为 \(k\) 的节点,然后就相当于是每一层选一个点覆盖整棵树。
因为 \(k\) 很小,而且我们可以先不管每次选择的层数顺序,只需最终每个层数都有选点即可,于是考虑状压,我们求出 dfs 序,然后设 \(f[i][S]\) 表示用 \(S\) 集合内的深度的点能否把 dfs 序在 \(1\sim i\) 的点覆盖住,选择一个点会覆盖住 \([L_x,R_x]\) 内的点,那么转移就有 \(f[i][S]\rightarrow f[R_x+1][S|dep[x]]\)。
trick:可以通过钦定与顺序无关以更方便状压。
P9333 [JOISC 2023 Day2] Council
题意:在 JOI 市议会中,有 \(N\) 名议员,编号从 \(1\) 到 \(N\)。议会将召开会议,议员们将对 \(M\) 项提案进行表决,编号为 \(1\) 到 \(M\)。如果 \(A_{i,j}=1\),则议员 \(i (1≤i≤N)\) 将对提案 \(j (1≤j≤M)\) 表决肯定票。如果 \(A_{i,j}=0\),则议员 \(i\) 将对提案 \(j\) 表决否定票。
JOI 市议会的程序如下所示。
-
选定主席,主席将在除了主席以外的其他 \(N−1\) 名议员中选择副主席。
-
将对 \(M\) 项提案进行表决。除了主席和副主席以外的其他 \(N−2\) 名议员,每人对每个提案均投票支持或反对。如果大多数议员(即肯定票大于等于 \(⌊\dfrac{N}{2}⌋\))投票赞成,则议会将批准该提案。其中 \(⌊x⌋\) 表示不超过 \(x\) 的最大整数。
市长 K 希望议会尽可能地批准更多的提案。市长 K 收集了议员的信息并知道每个议员在每个提案上的表决结果。你需要求出在给定议员投票信息的情况下,计算每个议员作为主席时议会可以批准的提案数量的最大可能值。
思路:一开始把题意理解错了,想了一些跟状压相关的东西,但是没想出来,结果才发现题目看错了,就懒得想了。
首先,确定了一个人后对答案有影响的只有那些 \(cnt=\left\lfloor\frac{n}{2}\right\rfloor\) 的位,记为 \(S\) ,那么我们就是要找到一个 \(j\ne i\) 满足 \(\text{popcount}(a_j\&S)\) 最小。
我们发现,\(a_j\) 是 \(a_j\& S\) 的超集,且 \(a_j\& S\) 是 \(S\) 的子集,就不难往高维前/后缀和方面去想。发现最小值不好维护,于是维护最大值,即把 \(a_i\) 取反后求最大值。设 \(g1_S,g2_S\) 表示是 \(S\) 的超集的两个 \(i\),然后再对于每个 \(S\),用一遍高维前缀和求出所有 \(\text{popcount}(a_j\&S)\) 的最大、次大值,然后就可以了。
trick:min 和 max 的灵活转化。
P9131 [USACO23FEB] Problem Setting P
题意:有 \(n\) 道题,\(m\) 个人,每个人对每道题的评价是 0/1,求选出若干题满足排列后对于每个人来说评价为 1 的题一定在评价为 0 的题后。
思路:好像有一车做法,甚至 \(O(3^m)\) 都可过。
有一个比较简单的做法是分成前后两部分,设 \(dp[i][j]\) 表示所有满足 \(x\) 的前 10 位是 \(i\),后 10 位是 \(j\) 的子集的 \(cnt[x]\) 之和,复杂度是 \(O(6^{\frac{m}{2}})\)。
也可以类似 CDQ 分治一样先做左儿子,然后算左儿子对右儿子的贡献,然后算右儿子。
trick:折半处理可以有效降低复杂度。
[AGC016F] Games on DAG
题意:给定一个 \(n\) 个点 \(m\) 条边的DAG,对于每条边 \((u,v)\) 都满足 \(u<v\),\(1,2\) 号点各一个石头,每次可以沿 DAG 上的边移动一颗石头,不能移动则输,求所有 \(2^{m}\)个边的子集中,只保留这个子集先手必胜的方案个数。
思路:依旧不会 SG 函数。
两颗石子是互不相干的,于是可以分开考虑。假设求出了两颗石子的 SG 函数 \(SG_1,SG_2\),先手获胜的条件就是 \(SG_1\oplus SG_2=1\),也就是用总的答案减去 \(SG_1=SG_2\) 的答案。
分析一下 SG 序列的性质。如果有一个 \(SG_i=x\) 的点,那么对于每个 \(y<x\) ,\(i\) 会向至少一个 \(SG_j=y\) 的 \(j\) 连边,这些点相互之间不能连边。
再找特殊情况: \(x=0\)。此时剩下的所有点都得和这些点有边,对于剩下的边,把所有的 \(SG\) 减 1 后还是同样的情况。
于是考虑状态压缩。设 \(f[S]\) 表示考虑 \(S\) 的导出子图时,满足 \(SG_1=SG_2\) 的方案数。转移时,枚举 \(S\) 的子集 \(T\),表示只有 \(T\) 中的 \(SG\ne 0\),讨论一下每个点的出边是否有限制,可以做到 \(O(3^nn)\)。
P4363 [九省联考 2018] 一双木棋 chess
思路:轮廓线 DP 模版题。
发现每个状态都是从上到下每一行选了的数都是一段前缀,而且长度不增,于是考虑维护轮廓线。
如图,从左侧开始,如果上面被选了就向右走,加上 0,否则就往上走,加上 1,下图就是 1011010010:
首先求出当前是谁移动,然后枚举转移,就是从哪一行拓展一个,就可以了。
trick:轮廓线可以被描述成一个 01 串,可以基于此来状压。
P3160 [CQOI2012]局部极小值
题意:往 \(n\times m\) 的棋盘上填入 \(1\sim n\times m\),每个数只能填一次。告诉所有是与其八联通的格子里最小的数的位置,求填数的方案数。
思路:因为所有格子里的数互不相同,所以在 \(4\times7\) 的棋盘上最多有 8 个这样的位置(即为局部极小值),这个数据范围提醒我们可以状压。又因为我们要填入数字,我们设DP状态为 \(f[i][S]\) 表示已经填了 \(1\sim i-1\),当前要填\(i\),填完 \(i\) 后局部最小值的状态为 \(S\),转移的时候我们分两种情况。一种是当前的数填到某给局部最小值的位置,这时直接枚举那个地方还没填数即可。第二种是填到一个不是最小值的位置,因为我们是从小到大填数的,所以局部最小值一定得在周围的数填入之前填入,这样就很方便预处理出每种状态下有多少点可以被填。但我们发现题目中说的是“所有”,这就意味这我们随便填会把一些不是局部最小值的位置变成局部最小值,这是只需大力容斥就行了(反正最多也只能填8个数),用一个不多的方案减去多1个的方案,加上多两个的……就行了。
trick:状压很适合和容斥一起处理。
CF79D Password
题意:有长度为 n 的 01 串和 l 个长度,每次可以将某个长度的子区间的取反,初始全是 0,求把给定的 k 个位置变成 1 的最小步数。
思路:把状压和最短路结合起来的题目好像很罕见
看到 \(k\leqslant 10\) 就想到是状压,但是区间取反不好维护,我们考虑差分,即每个数异或上前一个数,那区间取反可以变成两个单点取反。因为只有 \(k\) 个点最后为1,所以也最多只会有 \(2k\) 个数最后差分数组为 1。
设 \(f[S]\) 为把 01 串变成状态 \(S\) 的最小步数,因此 \(f[S]+cost(i,j)\rightarrow f[S|(1<<i)|(1<<j)]\) 其中 \(cost(i,j)\) 表示只把 \(i,j\) 取反要花费的最小代价。
我们考虑每进行一次操作是把相隔为 \(a_i\) 的两个位置取反,而我们取反 \(l,r\) 再取反 \(r,l'\),就相当与对 \(l,l'\) 进行了取反,然后我们发现这很像一个最短路问题,每个点向 \(x+a_i\) 与 \(x-a_i\) 连距离为 1 的边,再用 \(bfs\) 求一遍最短路即可求出 \(cost(i,j)\)。
总复杂度 \(O(nm+2^{2k})\)。
trick:区间操作差分成单点操作。
P3349 [ZJOI2016]小星星
题意:给定有 \(n\) 个点的数和无向图,求有多少种标号方案使得树上每一条边都在无向图中存在。
思路:考虑到求编号,我们设 \(f[i][j][S]\) 表示 \(i\) 子树内,\(i\) 标号为 \(j\),且使用了 \(S\) 中的标号的方案数,但转移时要枚举子集,复杂度是 \(O(n^3\times3^n)\) 的,过不去。
我们考虑优化。首先很容易想到把标号集 \(S\) 的限制去掉,但这样会出现重复编号,那容斥不就解决了吗?具体来说,对于每个 \(S\in[0,2^n-1]\) 求出每个点的编号都在 \(S\) 内的方案数,然后用 \(\left|S\right|=n\) 的方案数减掉 \(\left|S\right|=n-1\) 的,再加上 \(\left|S\right|=n-2\) 的 \(\cdots\) 然后这题就做完了。复杂度 \(O(n^3\times2^n)\)。
trick:遇到带标号的限制,可以容斥掉这个限制。
P7519 [省选联考 2021 A/B 卷] 滚榜
题意:给出 \(a\) 数组以及 \(b\) 数组的总和,求有多少种标号方案,使得按 \(b_i\) 从小到大依次加入 \(b_i\) 后新加入的 \(a_i+b_i\) 是当前最大的。
思路:学到了一个很新的科技
我们考虑DP的状态应该与那些有关,首先是已经加入了的位置有哪些,这个是 \(O(2^n)\) 的,当前加入的数的 \(b_i\) 以及总和 \(sum\),这样的DP固然带 \(2^n\times m^2\),显然不行。
我们考虑怎么去掉一维。我们可以发现,一旦确定了顺序,我们就可以贪心地使每次的 \(b\) 尽量小,这也启示我们可以试图去掉当前枚举的 \(b_i\) 这一维,只需保证 \(b_i\) 不降,即 \(b_i\) 的差分数组非负。但是有一个很严重的问题是 \(b\) 是有后效性的,于是我们可以用“费用提前计算”的 \(trick\),把当前选的数对后面的数的影响算出来并减掉,这样就可以避免有后效性了。
复杂度 \(O(2^nn^2m)\)。
trick:费用提前计算。
CF1322B Present
题意:求两两加和的异或和。
思路:因为是求异或和,可以按位考虑。对于第 \(k\) 位,有影响的就是前 \(k\) 位,于是我们取出前 \(k\) 位,那么和在 \([2^{k},2^{k+1}-1],[3\times 2^{k},2^{k+2}-1]\) 两个区间的才会有贡献,可以直接双指针计算,复杂度 \(O(n\log n\log w)\)。
CF1550E Stringforces
题意:- 设 \(s\) 是一个由前 \(k\) 个小写字母构成的字符串,\(v\) 是前 \(k\) 个小写字母中的某一个。定义 \(\mathrm{MaxLen}(s,v)\) 表示 \(s\) 所有仅由字母 \(v\) 构成的连续子串的最长长度。
- 定义 \(s\) 的价值为所有 \(\mathrm{MaxLen}(S,v)\) 的最小值,其中 \(v\) 取遍前 \(k\) 个小写字母。
- 现在给定一个长度为 \(n\) 的字符串 \(s\),\(s\) 中字母要么是前 \(k\) 个小写字母中的某一个,要么是问号。你需要将 \(s\) 中的每一个问号替换成前 \(k\) 个小写字母中的一个,并最大化 \(s\) 的价值。方便起见,你只需要输出这个最大的价值即可。
思路:因为是最小值最大,于是想到二分答案。
因为字符集很小,可以考虑状压。那么状态就是如果要使 \(S\) 中的字符合法,最短需要多长的字符串。提前预处理从一个位置开始最前的可以形成连续段的位置是哪儿就可以 \(O(1)\) 转移了。
trick:要达到整体合法,可以把状态设为用什么样的集合可以覆盖多长的前缀/需要多长的前缀。
P2150 [NOI2015] 寿司晚宴
题意:有 \(2\sim n\) 的集合,求有多少种方法选出两个子集使得任意一对不在同一个集合里的数都互质。
思路:朴素的想法是状压用了的质数,但是 500 以内的质数太多了,无法状压。
发现一个数至多只有 1 个大于 22 的质因子,而小于 22 的质因子只有 8 个,可以状压,那么我们就可以单独记录大于 22 的质因子。
设 \(f[S_1][S_2]\) 表示质因子的集合为 \(S_1,S_2\) 时的答案,转移时对于一段大质因子相同的数,我们设两个辅助数组 \(g_1,g_2\),分别表示这一段由 哪一个人选择的答案,转移有 \(g_1[i][S_1|k][S_2]\leftarrow g_2[i-1][S_1][S_2](k\&S_2=0)\),与背包很像,可以用滚动数组优化,然后有 \(f[S_1][S_2]=g_1[S_1][S_2]+g_2[S_1][S_2]-f[S_1][S_2]\),因为两个人都不选的方法会被多算。
复杂度 \(O(n2^{16})\)。
trick:减少枚举量来状压。
P6846 [CEOI2019] Amusement Park
题意:给一张图,求所有把边反向使得图是 DAG 的方案反向的边数和。
思路:首先,可以发现一张图如果是 DAG,那么把所有边反向后仍是 DAG,于是如果翻转 \(a\) 条边时 DAG,翻转 \(m-a\) 条边也是 DAG,平均下来就是 \(\dfrac{m}{2}\),于是我们要求的就可以转成翻转若干边使得是 DAG 的方案数。
设 \(f[S]\) 表示 \(S\) 中的点是 DAG 的方案数,转移时枚举 \(S\) 的子集 \(T\),那么要求就是 \(T\) 是独立集,再容斥一下即可。
复杂度 \(O(3^n+n^22^n)\)。
trick:对于点集要求是 DAG 转移时的条件:转移的子集是独立集。
[AGC012E] Camel and Oases
题意:给定 \(n\) 个绿洲,第 \(i\) 个绿洲的坐标为 \(x_i\) ,保证 \(-10^{9}\le x_1<x_2...<x_n\le 10^9\)
现在有一个人在沙漠中进行旅行,他初始的背包的水容积为 \(V\) 升,同时他初始拥有 \(V\) 升水 ,每次到达一个绿洲时,他拥有的水的量将自动重置为容积上限(可以使用多次)。他现在可以选择两个操作来进行旅行:
\(1.\) 走路,行走距离为 \(d\) 时,需要消耗 \(d\) 升水。清注意,任意时刻你拥有的水的数量不能为负数。
\(2.\) 跳跃,令 \(v\) 为你当前拥有的水量,若 \(v>0\),则你可以跳跃至任意一个绿洲,然后重置容积上界和所拥有的水量为 \(v/2\) (向下取整)。
对于每一个 \(i\) 满足 \(1\le i\le n\) ,你需要求解,当你在第 \(i\) 个绿洲作为起点时,你能否依次遍历其他所有绿洲。
思路:首先可以发现,我们使用跳跃的次数最多只有 \(O(\log V)\) 次,我们考虑看成一个有 \(\log V\) 层的容器,每层有若干个连续段,这些连续段时可以在不跳跃的前提下互相到达的。现在问题变成了每一层选择一个线段,要求最终可以覆盖全集。
然后考虑状压。设 \(exl[S]\) 表示层数集合为 S 最多可以覆盖多长的前缀,\(exr[S]\) 表示层数集合为 S 最多可以覆盖多长的后缀记 \(L[d][i]\) 表示第 d 层 i 所在连续段的左端点,\(R[d][i]\) 同理,转移有:
然后我们枚举第一层的连续段,枚举所有层数集合,判断是否可以覆盖全集。但是如果第一层连续段很多呢?我们发现,如果多于 \(\log V\) 那么没有点是合法的,因此复杂度是对的。
trick:同样是把覆盖全集转成覆盖前后缀。
CF1463F Max Correct Set
题意:规定一组正整数 \(S\)。当且仅当满足以下条件时该组正整数成立:
-
\(S \subseteq \{1,2,...,n\}\)
-
如果 \(a \in s\) 并且 \(b \in s\),那么 \(|a - b| \not ={x}\) 并且 \(|a - b| \not ={y}\)
对于给定的数值 \(n,x,y\),你需要找到成立数组的最大长度。
思路:首先注意到 \(x,y\) 很小,考虑把范围降到与 \(x,y\) 同阶。我们考虑最终最优解肯定是把一个结构重复很多次,这个结构的长度就是 \(x+y\)。容易证明,这样选择循环节是合法的。
考虑状压。我们设 \(f[i][S]\) 表示当前到第 \(i\) 个,前 \(max(x,y)\) 位的状态是 S 的最大长度,转移是 \(O(1)\) 的。
P3290 [SCOI2016] 围棋
题意:对于一个模板,只要棋盘中有某个窗口与其完全匹配,我们称这个模板是被激活的,否则称这个模板没有被激活。
考虑一个 \(n\times m\) 的棋盘,每个位置只能是黑子、白子或无子三种情况,换句话说,这样的棋盘共有 \(3^{n\times m}\) 种。此外,我们会给出 \(q\) 个 \(2\times c\) 的模板。
我们希望知道,对于每个模板,有多少种棋盘可以激活它。强调:模板一定是两行的。
思路:首先容斥一步,用所有情况减去一次都不匹配的情况。
然后考虑轮廓线状压 DP。我们枚举当前行匹配到了模版第二行的哪里,轮廓线存上一行每个位置是否和模版第一行匹配,于是状态就是 \(f[d][S][i][j]\) 表示当前是第 \(d\) 行,轮廓线的状态是 S,第一行匹配到了 \(i\) 第二行匹配到了 \(j\) 的方案数,预处理出来 kmp 数组就可以转移了。
trick:对于匹配的问题,通常用 kmp。