当前位置: 首页 > news >正文

[笔记]状压 DP

状压 DP 是动态规划的一种,通过将状态集合压缩成一个整数以实现状态转移的目的。

其中状态集合由若干个独立状态组成。很多情况下,每种独立状态都只有 \(2\) 种取值,也即二元状态。这种情况下,我么经常用 \(n\) 位二进制数来表示这 \(n\) 个二元状态组成的集合。

P1896 [SCOI2005] 互不侵犯

\(f[i][j][l]\) 为前 \(i\) 行,第 \(i\) 行的状态为 \(j\),且已经放置 \(l\) 个国王的方案数。

这里的 \(j\) 就是状态集合压缩而成的整数。比如当前行的填写情况是(图片来源 OI Wiki):

img

不妨令棋盘左端为二进制最低位,那么该状态对应的 \(j=100101_{(2)}=37\)

显然每一种放置方法都能唯一地表示为这样一个整数。

考虑 \(i-1\) 行的状态 \(x\) 转移到状态 \(j\) 需要满足的条件。

  • \(x\) 不存在行内冲突。
  • \(j\) 不存在行内冲突。
  • \(x,j\) 两行之间不存在冲突。

前两个条件,状态 \(t\) 不存在行内冲突,当且仅当 (t&(t<<1))==0。我们可以预处理出所有这样的状态,然后改变 \(f\) 的含义,用 \(x,j\) 直接表示状态的编号

第三个条件,其实就是要同时满足:

  • (x&j)==0
  • (x&(j<<1))==0
  • ((x<<1)&j)==0

对于每个满足条件的 \(x\),我们再枚举 \(l\) 来转移就可以了。

时间复杂度似乎是 \(O(n\times 2^n\times 2^n\times k)\)

实际上不难发现,剔除行内冲突的状态后,剩余的状态个数,即为长度为 \(n\)\(01\) 串中,任两个 \(1\) 不相邻的方案数。

这个方案数是 \(\text{Fib}(n+2)\),即斐波那契数列的第 \(n+2\) 项。理解起来很容易,我们令 \(g[i]\) 为填写到第 \(i\) 位的方案数,则有 \(g[i]=g[i-1]+g[i-2]\)(对应着第 \(i\) 位填 \(0/1\)),与斐波那契数列的递推式一致。

所以更准确的时间复杂度为 \(O(n\times \text{Fib}^2(n)\times k)=O(n^3\times\text{Fib}^2(n))\),其中 \(\text{Fib}(n)\) 也可以替换为同阶的 \(\varphi ^n\approx 1.618^n\)

点击查看代码 - R232968409
#include<bits/stdc++.h>
#define int long long
#define pc(x) __builtin_popcount(x)//x 在二进制表示下有多少个 1
using namespace std;
const int N=9;
int n,k,sta[1<<N],cnt[1<<N],idx,f[N][1<<N][N*N],ans;
inline bool valid(int x,int y){if(x&y) return 0;if(x&(y<<1)) return 0;if((x<<1)&y) return 0;return 1;
}
signed main(){cin>>n>>k;for(int i=0,lim=(1<<n)-1;i<=lim;i++) if(!(i&(i<<1))){sta[idx]=i,f[0][idx][cnt[idx++]=pc(i)]=1;}for(int i=1;i<n;i++){for(int j=0;j<idx;j++){//当前行的状态for(int x=0;x<idx;x++){//上一行的状态if(valid(sta[j],sta[x])^1) continue;for(int l=cnt[j];l<=k;l++){f[i][j][l]+=f[i-1][x][l-cnt[j]];}}}}for(int i=0;i<idx;i++) ans+=f[n-1][i][k];cout<<ans<<"\n";return 0;
}

P1879 [USACO06NOV] Corn Fields G

上题弱化版,一是不需要限制放多少棋子,二是互相攻击的条件从八连通变成四连通。

时间复杂度 \(O(m\times\text{Fib}^2(n))\)

下面给出代码。

点击查看代码 - R232967816
#include<bits/stdc++.h>
#define pc(x) __builtin_popcount(x)
using namespace std;
const int M=12,N=12,P=1e8;
int m,n,idx,a[M],f[M][1<<N],sta[1<<N];
long long ans;
signed main(){ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>m>>n;for(int i=0;i<m;i++)for(int j=0,x;j<n;j++)cin>>x,a[i]|=((x^1)<<j);for(int i=0,lim=(1<<n);i!=lim;i++){if(i&(i>>1)) continue;sta[idx]=i;if(!(i&a[0])) f[0][idx]=1;idx++;}for(int i=1;i<m;i++){for(int j=0;j<idx;j++){if(sta[j]&a[i]) continue;for(int k=0;k<idx;k++){if(i>1&&(sta[k]&a[i-1])) continue;if(sta[j]&sta[k]) continue;(f[i][j]+=f[i-1][k])%=P;}}}for(int i=0;i<idx;i++) ans+=f[m-1][i];cout<<ans%P<<"\n";return 0;
}

P2704 [NOI2001] 炮兵阵地

与 P1856 类似,只是判定范围有所不同。

这道题我们需要记录前 \(2\) 行的状态,即用 \(f[i][j][k]\) 表示前 \(i\) 行,第 \(i\) 行状态为 \(j\),第 \(i-1\) 行状态为 \(k\) 的最大数量。

\(f[i][j][k]\) 可以由 \(f[i-1][k][l]\) 转移而来,我们仅需枚举这样的 \(l\),判断 \(j,k,l\) 三个连续行的状态是否合法,若合法则转移。

为了提升效率,我们仍然将行内冲突的状态剔除掉。

时间复杂度不好衡量,如果令剔除后的状态数是 \(k\) 的话(经测试 \(k\) 的上界是 \(60\)),时间复杂度应为 \(O(nm+2^m+k^3n)\)

点击查看代码 - R232968551
#include<bits/stdc++.h>
#define pc(x) __builtin_popcount(x)
using namespace std;
const int N=100,M=10;
int n,m,lim,idx,a[N],f[N][1<<M][1<<M],sta[1<<M],cnt[1<<M],ans;
string s;
signed main(){ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>n>>m;for(int i=0;i<n;i++){cin>>s;for(int j=0;j<m;j++) a[i]|=((s[j]=='H')<<j);}for(int i=0,lim=(1<<m);i<lim;i++){if((i&(i<<1))||(i&(i<<2))) continue;sta[idx]=i,cnt[idx]=pc(i);if(!(i&a[0])) f[0][idx][0]=pc(i);idx++;}for(int i=1;i<n;i++){for(int j=0;j<idx;j++){//第 i 行的状态if(sta[j]&a[i]) continue;for(int k=0;k<idx;k++){//第 i-1 行的状态if(sta[k]&a[i-1]) continue;if(sta[j]&sta[k]) continue;for(int l=0;l<idx;l++){//第 i-2 行的状态if(i>1&&(sta[l]&a[i-2])) continue;if(sta[j]&sta[l]) continue;if(sta[k]&sta[l]) continue;f[i][j][k]=max(f[i][j][k],f[i-1][k][l]+cnt[j]);}}}}for(int i=0;i<idx;i++){for(int j=0;j<idx;j++){ans=max(ans,f[n-1][i][j]);}}cout<<ans<<"\n";return 0;
}

P8756 [蓝桥杯 2021 省 AB2] 国际象棋

与 P2704 类似地,需要记录 \(i,i-1\) 共两行的状态。

又与 P1856 类似,需要额外开一维限制“已经放了 \(x\) 个棋子”。

所以 DP 数组一共 \(4\) 维,具体见代码。

因为同一行无论怎么放内部都是合法的,所以无法缩减状态规模。时间复杂度为 \(O(m\times 2^{3n})\)

点击查看代码 - R232960233
#include<bits/stdc++.h>
#define pc(x) __builtin_popcount(x)
using namespace std;
const int N=6,M=100,K=21,P=1e9+7;
int n,m,lim,kk,f[M][1<<N][1<<N][K];
long long ans;
signed main(){ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>n>>m>>kk;lim=(1<<n);for(int i=0;i<lim;i++) f[0][i][0][pc(i)]=1;for(int i=1;i<m;i++){for(int j=0;j<lim;j++){for(int k=0;k<lim;k++){if(((j>>2)&k)|((k>>2)&j)) continue;for(int l=0;l<lim;l++){if(((l>>2)&k)|((k>>2)&l)) continue;if(((l>>1)&j)|((j>>1)&l)) continue;for(int o=pc(j);o<=kk;o++){(f[i][j][k][o]+=f[i-1][k][l][o-pc(j)])%=P;}}}}}for(int i=0;i<lim;i++){for(int j=0;j<lim;j++){ans+=f[m-1][i][j][kk];}}cout<<ans%P<<"\n";return 0;
}

P5911 [POI 2004] PRZ

\(S\) 为原集合的一个子集,\(f[S]\)\(S\) 过桥所花的最短时间,\(w[S],t[S]\) 分别表示 \(S\) 内元素的重量之和,最长时间。

则有转移:

\[f[S]=\min\limits_{T\subseteq S,w[\complement_S T]\le W} f[T]+t[\complement_S T] \]

每一个子集同样可以表示为一个 \(n\) 位二进制数,第 \(i\) 位为 \(1\) 表示第 \(i\) 个元素被选入子集。

然后就可以按式子进行递推了。

\(T\subseteq S\)T&S==T。不过遍历 \(S\) 已经是 \(O(2^n)\) 的,如果遍历 \(T\) 仍然是 \(O(2^n)\) 的话,总时间将达到 \(O(2^{2n})\),不可接受。

实际上,我们可以做到直接枚举 \(S\) 的每一个子集,仅需像下面这样写:

for(S=0;S<(1<<n);S++){for(T=S;T;T=(T-1)&S){//...}
}

正确性不难理解,主要对时间复杂度 \(O(3^n)\) 进行证明。

\(\bf{Proof:}\)
对于一个 \(S\),若它的二进制表示有 \(k\)\(1\),则它的子集数量为 \(2^k\)。而有 \(k\)\(1\) 的集合 \(S\)\(C_n^k\) 种,于是所有子集的总数是:

\[\sum\limits_{k=0}^n C_n^k\times 2^k \]

根据二项式定理,这是 \((1+2)^n\) 的展开形式,所以子集总数是 \(3^n\)

另外注意枚举 \(S\) 的顺序,\(T\) 转移到 \(S\) 时必须保证 \(f[T]\) 已经被计算好,从小到大遍历 \(S\) 就能做到这一点。

时间复杂度 \(O(3^n)\)

点击查看代码 - R232902835
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=16;
int W,n,lim,f[1<<N],tt[1<<N],ww[1<<N];
signed main(){ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>W>>n;lim=(1<<n);for(int i=0,t,w;i<n;i++){cin>>t>>w;for(int j=0;j<lim;j++){if((j>>i)&1) tt[j]=max(tt[j],t),ww[j]+=w;}}memset(f,0x3f,sizeof f);for(int i=0;i<lim;i++){//Sif(ww[i]<=W) f[i]=tt[i];for(int j=i;j;j=(j-1)&i){//Tif(ww[i^j]<=W){//i^j 即 T 在 S 中的补集f[i]=min(f[i],f[j]+tt[i^j]);}}}cout<<f[lim-1]<<"\n";return 0;
}

更新中。

http://www.sczhlp.com/news/35054/

相关文章:

  • C++面试周刊(3):面试不慌,这样回答指针与引用,青铜秒变王者
  • 2 两数相加
  • 免费网站java源码大全不下载拼多多运营
  • 专业做室内设计的网站有哪些免费crm网站不用下载的软件
  • 网站建设个人简历企业seo培训
  • 徐州市中宇建设工程有限公司网站在线看crm系统
  • 诸暨网站建设torrent种子搜索引擎
  • 如何在百度网站收录提交入口大数据营销案例
  • 响应式网站的制作网站制作网络广告网站
  • 如何做vip微信电影网站百度网址大全在哪里找
  • 石家庄平台公司网站排名优化教程
  • 怎么做网赚网站网络营销和网络推广
  • 2025 贵阳代码源集训 复盘
  • 网站建设提案重庆seo整站优化系统
  • 买网站的域名杭州seo排名优化外包
  • 杭州知名网站制作公司销售网站排名
  • 做seo用什么网站系统百度快速排名提升
  • 河源网站建设 科技谷歌广告上海有限公司
  • php 企业网站开发教程引擎搜索网站
  • 数码网站建设维护荆州网站seo
  • 北京市住房与城乡建设部网站p站关键词排名
  • 做网站是先做后台还是前端搜索热门关键词
  • 做bt网站安全不seo优化需要多少钱
  • 尉氏做网站google play下载安装
  • 云效拉取GitHub流水线源超时
  • 贝叶斯神经网络在碳封存优化中的突破应用
  • 视频网站做短视频培训计划方案模板
  • 深圳专业网站建设制作价格公司网站制作需要多少钱
  • 自己怎么做公司网站百度站长工具seo查询
  • 做门户网站需要什么条件北京网站优化对策