状压DP 学习笔记

概述

在某些题目中,状态里需要添加很多 bool 类型的状态,比如表示一些元素是否在集合中。当这种状态过多,可以将其压缩为一个二进制数。

比如 \(101001\) 表示 \(1,3,6\) 为真,\(2,4,5\) 为假。

例题

P2915

状压 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\) 的。

[[动态规划]]

posted @ 2024-03-01 09:21  lgh_2009  阅读(7)  评论(0)    收藏  举报