状压 DP 重温

状压DP


分两类。棋盘类(基于连通性类)和集合类

状压跟进制数相关。

 

 

棋盘式(基于连通性)


板题


291. 蒙德里安的梦想 - AcWing题库

转化一下,横竖摆放,只需要确定横着如何摆放,竖着就确定了。于是问题等价于求横着摆放的方案数。

考虑按列分类,对于当前列,我们规定每次横着放都是伸向下一列的,这样统一放置方法方便后续讨论。

我们需要知道上一列横着摆过来的长方形,才能确定该列的方案。

于是得出状态: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题库

 

其实还能再优化一点,就是只有当前状态不合法就没必要继续往下枚举其他状态了,优化后的代码:292. 炮兵阵地 - AcWing题库

优化了约1s左右,还是很快的。

 

总结


关于优化复杂度:

1.提前处理合法状态,在转移时只转移合法状态。但是要确保在转移过程中只会用到合法状态。

2.剪枝也可以优化,当前状态不合法的话就不用往下转移了,同样需要确保在转移过程中只会用到合法状态。

 

集合式


板题


91. 最短Hamilton路径 - AcWing题库

考虑记录走的路径集合,和最后到哪个点,就行了。即 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$ 的时间。 

此时就是严格满足双关键字的条件了,然后正常转移。

显然可以多重背包 $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 的值域里记录两个值,然后然后双关键字转移,以此优化复杂度。

但是需要注意,必须严格满足双关键字的条件才能使用。在双关键字更新的时候可以用贪心的角度理解正确性。

 

 

KeyDungeonDiv1

有 $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 2

out:
8

in2:
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;
}

  

 

Our happy ending

求能构造出长度为 $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;
}

  

AlienAndSetDiv2

给定整数 $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.对于无序的,或者转移需要的不仅仅是一个信息的,就可以状压。

 

posted @ 2025-02-23 10:40  cn是大帅哥886  阅读(13)  评论(0)    收藏  举报