状压DP 学习笔记
概述
在某些题目中,状态里需要添加很多 bool 类型的状态,比如表示一些元素是否在集合中。当这种状态过多,可以将其压缩为一个二进制数。
比如 \(101001\) 表示 \(1,3,6\) 为真,\(2,4,5\) 为假。
例题
状压 DP 题一个主要特征是数据范围很小,如本题中 \(4\leq n\leq 16\)。
可以设 \(f_{i,j}\) 表示队列末尾为第 \(i\) 头,状态为 \(j\)。\(j\) 表示每头牛是否在队列中,压成一个二进制数。
转移有三重循环:原状态,末尾的奶牛,即将入队的奶牛。如果末尾的奶牛在队伍中,即将入队的奶牛不在队伍中且差超过 \(K\),则转移。
初始时只有一头牛的状态初始化为 \(1\)。枚举所有末尾且所有奶牛在队列中的情况,和为答案。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,k,a[21];
long long f[21][65541],ans;
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)f[i][1<<(i-1)]=1;
for(int s=0;s<(1<<n);s++)for(int j=1;j<=n;j++)if(s&(1<<(j-1)))for(int i=1;i<=n;i++)if(!(s&(1<<(i-1)))&&abs(a[j]-a[i])>k)f[i][s|(1<<(i-1))]+=f[j][s];
for(int i=1;i<=n;i++)ans+=f[i][(1<<n)-1];
return cout<<ans<<'\n',0;
}
子集 DP
在状压 DP 中,我们常常需要对子集求和,也就是求 \(sum_i=\sum_{j\operatorname{and}i=i}a_j\)。
直接枚举每个数的子集是 \(O(3^n)\) 的。
此时我们运用高位前缀和的技巧:把每一位看作一维,每一维只有 \(0,1\),原来的下标中的每一位对应一维的下标。此时 \(sum\) 就是 \(a\) 的高维前缀和。复杂度优化为 \(O(n2^n)\)。
for(int i=0;i<n;i++)for(int j=0;j<(1<<n);j++)if(j>>i&1)a[j]+=a[j^(1<<i)];
假如对超集求和,其实就是高维后缀和。
for(int i=0;i<n;i++)for(int j=(i<<n)-1;j>=0;j--)if(j>>i&1)a[j^(1<<i)]+=a[j];
位运算技巧
取 \(a\) 的第 \(b\) 位:(a>>b)&1
将 \(a\) 的第 \(b\) 位设为 \(0\):a&=~(1<<b)
将 \(a\) 的第 \(b\) 位设为 \(1\):a|=(1<<b)
取反 \(a\) 的第 \(b\) 位:a^=(1<<b)
枚举 \(a\) 的子集:for(int j=a;;j=(j-1)&a)
其中对每个数枚举子集是 \(O(3^n)\) 的。感性理解,枚举 \(A\) 的子集 \(B\),再枚举 \(B\) 的子集 \(C\),可以把每个数分三类:属于 \(C\) 的,属于 \(B\) 但不属于 \(C\) 的,不属于 \(B\) 的。
[[动态规划]]

浙公网安备 33010602011771号