状压 DP 重温
状压DP
分两类。棋盘类(基于连通性类)和集合类
状压跟进制数相关。
棋盘式(基于连通性)
板题
转化一下,横竖摆放,只需要确定横着如何摆放,竖着就确定了。于是问题等价于求横着摆放的方案数。
考虑按列分类,对于当前列,我们规定每次横着放都是伸向下一列的,这样统一放置方法方便后续讨论。
我们需要知道上一列横着摆过来的长方形,才能确定该列的方案。
于是得出状态:f[i][j],前 i 列,且 i-1 列延伸到 i 列的方块的集合为 j
然后枚举集合 k 转移到 j ,前提是合法。如果判断合法?
1.该列和上一列不重合,即 k & j ==0
2.该列和上一列组成的横着放的方块(k 延伸过来的方块和 j 的摆放方案),空余的地方能摆下竖着放的方块。
也就是 连续0 出现的个数为偶数。即 k | j 连续 0 出现的个数为偶数。这一部分可以预处理。
这里取答案的时候有个技巧,很多题可以去试试。(偷个懒吧)
本来答案应该是 $\sum_{i=0}^{2^{n-1}} {ok_i \times f_{m, i}}$,即:
for (int i=0; i<(1<<n); i++) if (ok[i]) ans+=f[m][i];
可以在计算 f 的时候多往后算一层,再就是很直接的答案了,即 f[m+1][0]
#include <bits/stdc++.h>
using namespace std;
const int N=13, M=1<<N;
int n, m;
long long f[N][M];
bool ok[M];
int main()
{
while (scanf("%d%d", &n, &m), n||m)
{
memset(f, 0, sizeof f);
for (int i=0; i<(1<<n); i++)
{
ok[i]=1;
int cnt=0;
for (int j=0; j<n; j++)
{
if ((i>>j)&1)
{
if (cnt&1) ok[i]=0;
cnt=0;
}
else cnt++;
}
if (cnt&1) ok[i]=0;
}
f[1][0]=1;
for (int i=1; i<=m+1; i++)
{
for (int j=0; j<(1<<n); j++)
for (int k=0; k<(1<<n); k++)
if ((j&k)==0 && ok[j|k]) f[i][j]+=f[i-1][k];
}
printf("%lld\n", f[m+1][0]);
}
return 0;
}
例题
(考察优化状压 dp)P1896 [SCOI2005] 互不侵犯 - 洛谷
这次考虑按照行分类,然后只需要关注每一行怎么放国王就好,还需要记录一下放的国王数量。
即 f[i][j][k],表示前 i 行,放了 j 个国王,第 i 行摆放国王的集合方案。
然后从 i-1 行枚举一个方案 s2 转移到 i 行的方案 s 即可。
转移就显然了,f[i][j][s]+=f[i-1][j-count(s)][s2],其中 count(s) 是 s 中 1 的个数。可以预处理。
转移的前提是合法,怎么看合法?这里比较巧妙的用了位运算判合法。
首先同一列不能有国王,就是 s & s2 == 0,然后两行的相邻列不能有国王,就是 s | s2 不含相邻的两个 1。这一点也可以预处理。
还需要注意转移的时候不能越界,就是 j>=count(s) 才能转移。
这是朴素做法。当然最后取答案的时候也可以和上题一样偷懒(
然而在一本通上 TLE。
原因是在转移时枚举了太多冗余的状态了。有时候可以考虑优化转移的集合,避免枚举不合法的集合,已达到减小时间复杂度的目的。
所以所以可以预处理出来每行的合法集合,然后从每行的合法集合里面枚举。也就就是,同一行已经有相邻的国王,肯定不行。
记录详情 - 洛谷 | 计算机科学教育新生态。然后就过了。
(上题弱化版)327. 玉米田 - AcWing题库
跟上题几乎一样。就是井字形改为米字形。
少了放置数量限制,这个少记录一维状态就好。还有多了不能放置的格子限制。这个在枚举集合的时候顺便判断是否合法即可。
#include <bits/stdc++.h>
using namespace std;
const int N=14, M=1<<N, Mod=1e8;
int n, m, t, g[N], f[N][M];
bool ok[M];
vector<int> vec;
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=n; i++)
for (int j=0; j<m; j++)
{
scanf("%d", &t);
g[i]+=((t^1)<<j);
}
for (int i=0; i<(1<<m); i++)
{
ok[i]=1;
for (int j=1; j<m; j++)
if (((i>>j)&1) && ((i>>(j-1))&1))
{
ok[i]=0;
break;
}
if (ok[i]) vec.push_back(i);
}
f[0][0]=1;
for (int i=1; i<=n+1; i++)
for (int p=0; p<vec.size(); p++)
for (int q=0; q<vec.size(); q++)
{
int s=vec[p], s2=vec[q];
if ((s&s2)==0 && (s&g[i])==0 && (s2&g[i-1])==0) f[i][s]=(f[i][s]+f[i-1][s2])%Mod;
}
printf("%d", f[n+1][0]);
return 0;
}
(上题加强+扩展版) 292. 炮兵阵地 - AcWing题库
攻击范围两格,就考虑多记录一维状态,记录当前行的前一行的状态,然后正常转移即可。即 f[i][a][b] 表示前 i 行,第 i 行状态为 a,i-1 行状态为 b。
一个问题:实现攻击范围两格的时候,为什么不能改变转移,而是改变状态?
只改变转移,从前两行和前一行转移,会算重方案数,因为用状态把问题划分成子问题时,没有做到划分成不重复的子问题。
求放的数量,改变下记录的值即可。注意要滚动数组优化下。
其实还能再优化一点,就是只有当前状态不合法就没必要继续往下枚举其他状态了,优化后的代码:292. 炮兵阵地 - AcWing题库
优化了约1s左右,还是很快的。
总结
关于优化复杂度:
1.提前处理合法状态,在转移时只转移合法状态。但是要确保在转移过程中只会用到合法状态。
2.剪枝也可以优化,当前状态不合法的话就不用往下转移了,同样需要确保在转移过程中只会用到合法状态。
集合式
板题
考虑记录走的路径集合,和最后到哪个点,就行了。即 f[i][j],走了 i 这些点,最终走到 j 号点。其中 i 是个集合。
这也是一类需要知道最后决策点类型的 DP,因为只有知道最后一步的决策点,才能计算进行当前决策产生的代价。
那就容易转移了。枚举上一个走到的点 k 转移到 j 即可。
这里都以 0 为下标当做点的标号,之后转移会方便些。
#include <bits/stdc++.h>
using namespace std;
const int N=21, M=1<<N;
int n, a[N][N], f[M][N];
int main()
{
scanf("%d", &n);
for (int i=0; i<n; i++)
for (int j=0; j<n; j++) scanf("%d", &a[i][j]);
memset(f, 63, sizeof f);
f[1][0]=0;
for (int i=0; i<(1<<n); i++)
for (int j=0; j<n; j++)
if ((i>>j)&1)
for (int k=0; k<n; k++)
if (((i-(1<<j))>>k)&1) f[i][j]=min(f[i][j], f[i-(1<<j)][k]+a[k][j]);
printf("%d", f[(1<<n)-1][n-1]);
return 0;
}
转移的时候有个细节,就是 为什么 i 和 j 循环的位置不能互换的问题
因为在进行状态转移时,f[i][j] 要由 f[i^(1 << j)][k] 转移过来,所以一定要保证在计算 f[i][j] 之前一定已经计算过 f[i^(1 << j)][k] 了,这样才能保证答案递推的连续性。
在 i 是外层循环中,是按照最外层为 状态依次从小到大 的顺序进行对答案的计算
因为状态 i^(1 << j) 一定是小于状态 i 的,所以符合递推的连续性。
在 j 是外层循环中,在计算 f[i][j] 时一定用到 f[i^(1 << j)][k]
而当 k>j 的情况时,此时的 f[i^(1 << j)][k] 并没有被计算过,因为你最外层循环还没循环到k呢。
例如: f[101011][3] = f[100011][5] + w[5][3]
按 i 外层循环,一定可以保证 f[100011][5] 在f[101011][3] 之前被计算出来。
而按 j 外层循环,由于 5 > 3 ,f[100011][5] 根本就没有被计算过,所以不可行。
所以转移的时候,记得满足 dp 是个拓扑图的形式!
例题
(数学+重复覆盖问题)P2831 [NOIP 2016 提高组] 愤怒的小鸟 - 洛谷
有难度的。
记 f[i] 表示消灭的集合为 i 的猪,所用的小鸟的最小数量。
然后考虑转移,就是用一些抛物线把没消灭的猪消灭掉。假设消灭掉后为 new_i,那么转移就是 f[i]=min(f[i], f[new_i]+1)
所以预处理每条抛物线能消掉的猪。这里需要先用两个点确定抛物线,然后再暴力看能消灭那些点即可。
公式:
$$ {\begin{cases} y_1 = ax_1^2 + bx_1 \\\ y_2 = ax_2^2 + bx_2\end{cases}} \quad \Rightarrow \quad {\begin{cases} a = \dfrac{\dfrac{y_1}{x_1} - \dfrac{y_2}{x_2}}{x_1 - x_2}\\\ b = \dfrac{y_1}{x_1} - ax_1\end{cases}} $$
这其实是重复覆盖问题(可以用 Dancing Links 解决),不过这里用了状压优化。
复杂度:$O(T2^nn^2)$。
实际上不用 $n^2$ 枚举点,只用找到第一个二进制位表示 0 的点,就用它来更新就好。
因为最终全部猪都要消灭,早消晚消都是要消,不妨每次都消灭还未被消灭的猪。
这样用类似 lowbit 的转移优化,降为 $O(T2^nn)$。
(单点式转移)P2915 [USACO08NOV] Mixed Up Cows G(Code)
单点式转移(随便起的名)。也就是集合由枚举点转移。
需要知道之前用过的奶牛,所以需要记录奶牛集合。同时记录最后一个奶牛即可。
定 f[s][i] 表示选了奶牛集合为 s,且最后一只奶牛的编号是 i,的方案数。
一点细节:因为每次要知道上一次选的奶牛,于是初始化的时候就一定钦定选一个奶牛,而不是说一只奶牛都不选,这样不会有奇奇怪怪的错。
这里状压的时候,集合里 $1$ 的个数就是阶段,所以没啥必要把阶段记录在状态里。
(集合式转移)P5911 [POI 2004] PRZ(Code)
集合式转移(随便起的名)。也就是集合由枚举集合转移。
定 f[s] 表示过桥的人的集合为 s,的最小花费。
枚举集合转移就好。这里可以用 $O(3^n)$ 枚举子集的技巧,降低复杂度。
然后对于某个子集的代价,可以用 lowbit 预处理出来,且用 lowbit 可以从 $O(2^nn)$ 的复杂度降低至 $O(2^n)$。
原因就是集合最终是一定是全集,所以不妨钦定,每次转移都从还未加入集合的最后一个元素进行转移,就省去了额外枚举的复杂度,然后发现这一步其实就是 lowbit。
这里分析一下枚举的子集的原理和复杂度:
原理:
这里假设是枚举 $s$ 的子集 $s2$,称操作为 $s2$ 每次减一后再按位与上 $s$。
每次操作,如果 $s2$ 当前末位有一,减去一不会不产生退位。此时显然是 $s$ 的子集。
如果当前末位没有一,减去一会让 $s2$ 最后一个一退位,并让 $s2$ 这个 1 后的所有 0 变成 1,此时再按位与上 $s$,也显然是 $s$ 的子集。
容易发现这样一定不会重,也不会漏,保证了 $s2$ 每一次操作后都是 $s$ 的子集。
复杂度:
根据 $s2$ 每一次操作后都是 $s$ 的子集的原理,考虑 $s$ 的子集有多少个即可,因为子集个数就是复杂度。
对于 $s$ 按照 1 的个数分类。
假设 $s$ 有 $k$ 位,其中有 $cnt$ 位为 1,那么 $s$ 有 $2^{cnt}$ 个子集,然后这些 $1$ 的位置是可以随意安排的,有 $\dbinom{k}{cnt}$ 的方案数,乘起来就是当前 $s$ 的贡献。
所以可以得到以下式子,然后进行一些推导变成二项式定理就证完了:
复杂度
$=\sum_{cnt=1}^{k} \dbinom{k}{cnt} 2^{cnt}$
$=\sum_{cnt=1}^{k} \dbinom{k}{cnt} 2^{cnt} 1^{k-cnt}$
$=(2+1)^{k}$
$=3^k$
I. P5997 [PA 2014] Pakowanie - 洛谷(Code)
背包是不考虑物品顺序的,于是物品可以乱序放入背包,所以需要状压记录放了的物品。还需要知道用了几个背包,以及当前背包剩余的空间。
考虑贪心,容量大的背包肯定先用。于是有了朴素状态:$f_{i, s, j, v}$ 装了 $i$ 个物品的集合为 $s$,用了 $j$ 个背包,且第 $j$ 个背包剩余容量 $v$,是否合法。
$i$ 可以省去(就是 $s$ 里 $1$ 的个数),再把 $j$ 和 $v$ 从定义域拉到值域。为什么变化这两个状态?
1.因为尽可能想要定义域和决策相关,这样就方便转移。
2.值域同时有 2 个值,就考虑双关键字更新。但是前提是需要严格满足双关键字,恰巧这里符合。
这里就是 $j$ 为第一关键字,$v$ 为第二关键字,是严格满足的,可以用贪心理解,所以就是对的。
然后正常转移。
显然有 $O(nmt)$ 做法,$f_{i,j,k}=p$ 表示前 $i$ 首歌,用了 $j$ 个 CD,第 $j$ 个 CD 剩了 $k$ 的时间,所装的最多歌数 $p$。
很 easy。但是发现可以利用把定义域拉到值域的技巧,降低复杂度。
$f_{i, j}=[p, k]$ 前 $i$ 首歌,用了 $j$ 个 CD,装了 $p$ 首歌,第 $j$ 个 CD 剩下 $k$ 的时间。
$O(nm)$ 这样就做完了吗?错完了。这里并不是严格满足双关键字。因为我完全可以让歌曲数量减少,来换取 CD 剩下的时间,于是并不严格满足双关键字更新的条件。
那么稍微改改,$f_{i, j}=[p, k]$ 前 $i$ 首歌,装了 $j$ 首歌,用了 $p$ 个 CD,第 $p$ 个 CD 剩下 $k$ 的时间。
此时就是严格满足双关键字的条件了,然后正常转移。
- III. P10973 Coins(Code)
显然可以多重背包 $O(nm)$ 无任何思维含量的碾过去,这里讨论一种更加智慧的方法。
考虑正常记录状态,$f_{i, v}=0/1$:前 $i$ 个物品,能否拼出体积为 $v$。
此时转移是 $O(k)$ 的,发现对着转移优化就是多重背包的套路,但如果我换一种优化方式呢?
我可以直接把转移拉到状态里,就可以让转移降低复杂度,但是状态也会增加复杂度了,然后后续再对状态进行优化。
具体的,有 $f_{i, j, v}=0/1$:前 $i$ 个物品,第 $i$ 个物品用了 $j$ 次,能否拼出体积为 $v$。
此时状态是 $O(nmk)$ 的,转移是 $O(1)$ 的,我们可以先把 $i$ 滚动掉,再把定义域拉到值域中。
变成:$f_v=[0/1, j]$:前 $i$ 个物品,能否拼出体积为 $v$,且第 $i$ 个物品用了最少的 $j$ 次。
然后恰好这两个值符合双关键字的性质,所以就是正确的。也可以贪心的去理解。
一点细节:在计算当前物品 $i$ 时,需要把当前物品的 $j$ 这一值全部清空,根据状态定义就可以知道,$j$ 是第 $i$ 的物品的最少使用次数,此时就是用了 $0$ 次。
- IIII.总结
小结一下,这三题都是在 DP 的值域里记录两个值,然后然后双关键字转移,以此优化复杂度。
但是需要注意,必须严格满足双关键字的条件才能使用。在双关键字更新的时候可以用贪心的角度理解正确性。
有 $n$ 扇门,每扇门开启需要红色钥匙 $ri$ 把,绿色钥匙 $gi$ 把,开启后可以获得红色钥匙 $dri$ 把,绿色钥匙 $dgi$ 把,白色钥匙 $dwi$ 把。
一开始你拥有一定数量的各种颜色的钥匙。
每扇门最多开一次,白色钥匙是万能钥匙,可以充当任何颜色的钥匙。
你的目标是,最大化你手中的钥匙总数,注意你不需要把门全部开完。输入格式:
共 7 行,$r$,$g$,$dri$,$dgi$,$dwi$ 共 6 行,初始 $sr$,$sg$,$sw$ 共 1 行。
$1\le n \le12$
$r,g,b,dri,dgi,dwi,sr,sg,sw \le 10$
```
in:
3
1 2 3
0 4 9
0 0 10
0 8 9
1 0 8
3 1 2out:
8in2:
12
9 7 3 4 0 8 4 7 10 7 9 10
2 3 9 3 8 6 4 6 9 2 0 0
3 6 3 0 5 2 2 1 3 0 4 0
0 0 1 3 2 3 4 1 5 6 5 4
0 2 0 2 4 1 0 5 3 1 6 0
3 3 4
out2:
17
注意到开门可以是乱序的,可以考虑状压。
然后一个朴素的状态:$f_{i, r, g}=w$:开了门的状态为 $i$,手中有 $r$ 红钥匙,$g$ 把绿钥匙,能获得的最多 $w$ 白钥匙。
注意到开门顺序部会影响钥匙总量,所以发现状态记录了一个已知可以推出的量,那么就可以删去。
所以有 $f_{i, r}=w$:开了门的状态为 $i$,手中有 $r$ 红钥匙,$tot_i-r-w$ 绿钥匙,能获得的最多 $w$ 白钥匙。
#include <bits/stdc++.h>
using namespace std;
const int N=15, M=5005, INF=1e9;
int n, nr[N], ng[N], dr[N], dg[N], dw[N], sr, sg, sw, f[M][150], tot[M], p[M], ans=0;
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; i++) scanf("%d", &nr[i]);
for (int i=1; i<=n; i++) scanf("%d", &ng[i]);
for (int i=1; i<=n; i++) scanf("%d", &dr[i]);
for (int i=1; i<=n; i++) scanf("%d", &dg[i]);
for (int i=1; i<=n; i++) scanf("%d", &dw[i]);
scanf("%d%d%d", &sr, &sg, &sw);
tot[0]=sr+sg+sw;
for (int i=1; i<=n; i++) p[1<<i-1]=i;
for (int i=1; i<(1<<n); i++)
{
int lb=(i&(-i));
tot[i]=tot[i^lb]-nr[p[lb]]-ng[p[lb]]+dr[p[lb]]+dg[p[lb]]+dw[p[lb]];
}
for (int i=0; i<(1<<n); i++) for (int r=0; r<=130; r++) f[i][r]=-INF;
f[0][sr]=sw;
for (int i=0; i<(1<<n); i++)
{
for (int r=0; r<=130; r++)
{
int w=f[i][r], g=tot[i]-r-w;
if (w<0 || g<0) continue;
ans=max(ans, tot[i]);
for (int j=1; j<=n; j++)
{
if ((i>>j-1)&1) continue;
int r2=r-nr[j], g2=g-ng[j], w2=w;
if (r2<0) w2+=r2, r2=0;
if (g2<0) w2+=g2, g2=0;
if (w2>=0) f[i|(1<<j-1)][r2+dr[j]]=max(f[i|(1<<j-1)][r2+dr[j]], w2+dw[j]);
}
}
}
printf("%d", ans);
return 0;
}
求能构造出长度为 $n$ 的序列 $a_1, a_2, \dots, a_n$ 的数量,满足以下两个条件:
1. 每个元素 $a_i$ 是 $[0, L]$ 中的整数;
2. 存在一个子集(可空),其元素和为 $k$。
$1 \le L \le 10^9$,$1\le n,k\le20$
很牛的题啊。第一想法:记 $f_{i, j}$:前 $i$ 个数拼出和为 $j$ 的方案数。然后正常转移。是错的。
会重复计数,因为对于一个序列可能有多个子集和为 $j$,此时贡献应该为 $1$,而非多次。
那如何解决重复计数?考虑某种状态唯一对应某种情况,就可以避免重复计数。
记 $f_{i, s}$:前 $i$ 个数能拼出的所有可能的和,为 $s$ 这个的集合的方案数,用一个集合唯一对应这个序列,这样就不会重复计数了。
转移有一点细节,考虑 $a_i$ 的取值范围就好,然后就做完了。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=22, M=3e6, Mod=1e9+7;
int G, n, k, L, tot=0;
ll f[2][M], ans=0;
int main()
{
scanf("%d", &G);
while (G--)
{
ans=0;
scanf("%d%d%d", &n, &k, &L);
tot=(1<<k+1)-1;
memset(f, 0, sizeof f);
f[0][0]=1;
for (int i=0; i<n; i++)
{
memset(f[(i+1)&1], 0, sizeof f[(i+1)&1]);
for (int j=0; j<=tot; j++)
{
if (!f[i&1][j]) continue;
for (int p=0; p<=min(k, L); p++)
{
int s=(j|((j<<p)&tot)|(1<<p));
f[(i+1)&1][s]=(f[(i+1)&1][s]+f[i&1][j])%Mod;
}
if (L>=k) f[(i+1)&1][j]=(f[(i+1)&1][j]+1ll*(L-k)*f[i&1][j])%Mod;
}
}
for (int i=0; i<=tot; i++) if ((i>>k)&1) ans=(ans+f[n&1][i])%Mod;
printf("%lld", ans);
}
return 0;
}
给定整数 $N$ 和 $K$,设集合 $S = \{1, 2, \ldots, 2N\}$。将 $S$ 划分为两个子集 $A$ 和 $B$,满足以下条件:
1. 每个元素恰好属于 $A$ 或 $B$ 之一
2. $|A| = |B| = N$($|·|$ 表示集合大小)
3. 设 $A[i]$ 为 $A$ 的第 $i$ 小元素,$B[i]$ 为 $B$ 的第 $i$ 小元素($1 \leq i \leq N$),则 $|A[i] - B[i]| \leq K$。求满足条件的划分方式数 $X$,对 $1{,}000{,}000{,}007$ 取模。
$1\le N \le 50$,$1\le K \le 10$
这是这么多题中最牛逼的题。
因为是集合,不考虑顺序,直接按从小到大的顺序填数到 A 和 B 中即可。
然后需要一定的观察力,和手玩能力。
假设 N=4,K=2,当前填的是:
12??
????
那么 3 一定不能填到 A 的末位,不然会超出限制。
更一般的,可以发现 A 和 B 中较长序列多出来的每个数,都限制着当前数应该填在哪。
不妨对这个信息状压,定 $f_{x,y,j}$ 表示序列 A 放在到了第 $x$ 个,序列 B 放到了第 $y$ 个,AB 中较长序列多出来的每个数与当前数之差压成 $j$ 时的方案数。
如果 $j$ 的二进制数从低往高的第 $t$ 位是 1,表示存在差为 $t$。由于 $t \le k$, 所以 $j$ 最大值为 $2^k-1$。
再进一步发现,我们放新的数只与两序列尚未匹配的数有关。
假设 N=4,K=2,当前填的是:
12??
3???
那么当前数 4 与第一列已经没关系。
这样的话,我们要存的状态必定是其中一列为空的,即我们要存的是:
2??
???
此时考虑转移,就会方便很多。
再观察一下,只需要知道当前放的数是什么,并不需要知道 A 放了多少数,B 放了多少数。
因为我们只关心 AB 中较长序列多出来的那些数(由上面的发现可知),恰好这些数与当前数之差已经状压了,于是 A 放了多少数,B 放了多少数,其实是没用的。
然后就可以化简状态:$f_{i,j}$ 表示放完前 $i$ 个数字,AB 中较长序列多出来的每个数与当前数之差压为 $j$ 时的方案数。
考虑转移。
考虑第 $i$ 个数放在较长序列中,或者放在较短序列中。
1.放在较长序列中,需要看差值最大的数是否小于等于 $K$。如果合法,就能转移。
此时新的数与旧差值全部加一,也就是旧差值左移一位,然后因为是放到了较长序列,就会多了差值 1。就有:$f_{i+1, j<<1|1}+=f_{i, j}$。
2.放在较短序列中,就先去掉最大差值 $h$,然后再更新差值,就有:$f_{i+1, (j^(1<<h))<<1}+=f_{i, j}$。
特别的,如果 AB 等长,也就是 $j=0$,此时新的数随便放 AB 都行,也就有:$f_{i+1, 1}=f{i, 0} \times 2$。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=105, K=2000, Mod=1e9+7;
int n, k;
ll f[N][K];
void upd(ll &to, ll from)
{
to=(to+from)%Mod;
}
int main()
{
scanf("%d%d", &n, &k);
f[0][0]=1;
for (int i=0; i<=2*n; i++)
{
upd(f[i+1][1], f[i][0]*2);
for (int j=1; j<(1<<k); j++)
{
int h=0; // 0<=h<=k-1
for (h=k-1; h>=0; h--) if ((j>>h)&1) break;
if (h<k-1) upd(f[i+1][j<<1|1], f[i][j]);
upd(f[i+1][(j^(1<<h))<<1], f[i][j]);
}
}
printf("%lld", f[2*n][0]);
return 0;
}
总结
1.状压中,1 的个数就是阶段
2.3^n 枚举子集的技巧
3.预处理思想(lowbit 来预处理更新)
4.合理的状态表示(蒙德里安的梦想那题,需要钦定当前放横方块的方式,这样才能不重复计数)
5.把状态(也称定义域。太大的话就这样)拉到值域里去,有多个值域,按多关键字转移就行
6.对于无序的,或者转移需要的不仅仅是一个信息的,就可以状压。

浙公网安备 33010602011771号