前言

状态压缩是一种\(dp\)里的暴力,但是非常优秀,状态的转移,方程的转移和定义都是状压\(dp\)的难点,本人在次总结状压dp的几个题型和例题,便于自己以后理解分析状态和定义方式

状态压缩动态规划,就是我们俗称的状压\(dp\),是利用计算机二进制的性质来描述状态的一种\(dp\)方式。

很多棋盘问题都运用到了状压,同时,状压也很经常和BFS及\(dp\)连用。

状压\(dp\)其实就是将状态压缩成2进制来保存 其特征就是看起来有点像搜索,每个格子的状态只有\(1\)或\(0\) ,是另一类非常典型的动态规划

NO.1 Corn Fields G

题目描述

农场主\(John\)新买了一块长方形的新牧场,这块牧场被划分成\(M\)行\(N\)列\((1 \le M \le 12; 1 \le N \le 12)\),每一格都是一块正方形的土地。\(John\)打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是\(John\)不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

\(John\)想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

输入格式

第一行:两个整数\(M\)和\(N\),用空格隔开。

第\(2\)到第\(M+1\)行:每行包含\(N\)个用空格隔开的整数,描述了每块土地的状态。第\(i+1\)行描述了第\(i\)行的土地,所有整数均为\(0\)或\(1\),是\(1\)的话,表示这块土地足够肥沃,\(0\)则表示这块土地不适合种草。

输出格式

一个整数,即牧场分配总方案数除以\(100,000,000\)的余数。

输入输出样例

输入 #1

2 3

1 1 1

0 1 0

输出 #1

9

分析

首先看到范围,\(M\)和\(N\)的范围都极小,所以根据状压\(dp\)通性,显然要通过每行或者每一列的状态来进行转移。这个题比较灵性(恶心)的就是行为\(M\),颠覆认知……因为每个奶牛不能挨在一起,并且不种植也是一种方案,所以合法的状态肯定是要初始化一下的,即:

  1. g[i]=(!(i<<1)&i&&!(i&(i>>1)))

这道题就是按每一行来进行状态转移,定义\(f[i][j]\)为第\(i\)行状态为\(j\)时的方案数,所以要从上一行转移而来,那么设上一行的状态为\(k\),所以就可以根据这个列出状态转移方程:

\[f[i][j] = f[i][j]+f[i-1][k]
\]

最终的答案就是把最后一行每一个状态的答案加起来即可

具体的初始化见代码注释

代码

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int maxn = 13;
  4. const int mod = 100000000;
  5. int n,m;
  6. int jl[maxn][maxn];
  7. int f[maxn][1<<maxn];
  8. int ste[maxn],g[1<<maxn];
  9. int main(){
  10. cin>>m>>n;
  11. for(int i=1;i<=m;++i){
  12. for(int j=1;j<=n;++j){
  13. cin>>jl[i][j];//记录矩阵
  14. }
  15. }
  16. for(int i=1;i<=m;++i){
  17. for(int j=1;j<=n;++j){
  18. ste[i] = (ste[i]<<1)+jl[i][j];//用二进制记录每一行的状态(有草的地方)
  19. }
  20. }
  21. f[0][0] = 1;
  22. int ms = 1<<n;//总状态
  23. for (int i = 0; i <ms; i++)
  24. g[i] = ((i&(i<<1))==0) && ((i&(i>>1))==0);//初始化出所有状态中的合法状态
  25. for(int i=1;i<=m;++i){//枚举每一行
  26. for(int j=0;j<ms;j++){//当前行状态
  27. if(g[j] && (j & ste[i]) == j){//该状态合法且当前放牛都是在有草的地方
  28. for(int k=0;k<ms;k++){//上一行状态
  29. if((k&j) == 0){//上一行与这一行不能有相同的状态,也就是牛不能相邻
  30. f[i][j] = (f[i][j]+f[i-1][k])%mod;
  31. }
  32. }
  33. }
  34. }
  35. }
  36. int ans = 0;
  37. for(int i=0;i<ms;++i){//累加每一个状态的方案
  38. ans =(ans+f[m][i])%mod;
  39. }
  40. cout<<ans%mod<<endl;//记得每一次都要取模
  41. return 0;
  42. }

NO.2 No Change G

题目

约翰到商场购物,他的钱包里有\(K(1 \le K \le 16)\)个硬币,面值的范围是\(1..100,000,000\)。

约翰想按顺序买 \(N\)个物品\((1 \le N \le 100,000)\),第\(i\)个物品需要花费\(c_i\)块钱,\((1 \le c_i \le 10,000)\)。

在依次进行的购买\(N\)个物品的过程中,约翰可以随时停下来付款,每次付款只用一个硬币,支付购买的内容是从上一次支付后开始到现在的这些所有物品(前提是该硬币足以支付这些物品的费用)。不幸的是,商场的收银机坏了,如果约翰支付的硬币面值大于所需的费用,他不会得到任何找零。

请计算出在购买完\(N\)个物品后,约翰最多剩下多少钱。如果无法完成购买,输出\(-1\)

输入格式

  • Line \(1\): Two integers, \(K\) and \(N\).

  • Lines \(2..1+K\): Each line contains the amount of money of one of FJ's coins.

  • Lines \(2+K..1+N+K\): These \(N\) lines contain the costs of FJ's intended purchases.

输出格式

  • Line 1: The maximum amount of money FJ can end up with, or -1 if FJ cannot complete all of his purchases.

输入输出样例

输入 #1

3 6

12

15

10

6

3

3

2

3

7

输出 #1

12

说明/提示

FJ has \(3\) coins of values \(12, 15\), and \(10\). He must make purchases in sequence of value \(6, 3, 3, 2, 3\), and \(7\).

FJ spends his 10-unit coin on the first two purchases, then the 15-unit coin on the remaining purchases. This leaves him with the 12-unit coin.

分析

看到友好又有标志性的硬币数的范围,当然要从硬币使用作为状态来进行状压啦!所以我们的状态就定义为\(f[i]\)表示使用硬币状态为\(i\)的时候买的最大物品数,看题意大概就是买东西需要按顺序,所以我们维护一个前缀和来优化时间效率,并且记录一下所有硬币的价值总和,方便最后判断。与上一个题相似的是,也要把使用每个硬币的状态提前初始化一下:

\[g[i] = g[i-1]<<1;
\]

表示的是只用了第\(i\)个硬币的状态。并且\(g[1]=1\)然后就可以愉快的状态转移了!状态转移就是用了状态为\(i的\)硬币时买的物品,与当前用了第\(j\)个硬币买的物品去最大值。状态转移方程比较特殊,所以一会在代码中解释。

状态转移完了就需要统计答案了,每次当前状态能买的最大物品为\(n\)时就开始统计。枚举硬币,如果当前状态用了该硬币,那么就让计数\(cnt\)加上当前硬币的价值,最后\(ans\)取最大的剩余价值。需要注意的是最后剩余可以为\(0\),所以\(ans\)应该初始化为\(-1\),如果小于\(0\),那么就是不可能的情况。

代码

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int maxn = 1e6+10;
  4. const int mm = 19;
  5. int f[1<<mm];
  6. int n,k;
  7. int sum[maxn];
  8. int g[maxn];
  9. int tot,cnt,ans = -1;
  10. int c[mm],a[maxn];
  11. int main(){
  12. cin>>k>>n;
  13. for(int i=1;i<=k;++i){//输入硬币数并计算总价值
  14. cin>>c[i];
  15. tot += c[i];
  16. }
  17. for(int i=1;i<=n;++i){//输入每个物品价值并求出前缀和
  18. cin>>a[i];
  19. sum[i] = sum[i-1]+a[i];
  20. }
  21. g[1] = 1;
  22. for(int i=2;i<=k;++i){//初始化每个硬币使用的状态
  23. g[i] = g[i-1]<<1;
  24. }
  25. int ms = (1<<k)-1;
  26. for(int i=0;i<=ms;++i){//枚举所有状态
  27. for(int j=1;j<=k;++j){//枚举所有硬币
  28. if(i & g[j]){//当前状态用了该硬币就继续运行
  29. int te = f[i^g[j]];//te为没用该硬币时能卖的商品数。
  30. te = upper_bound(sum+1,sum+n+1,sum[te]+c[j])-sum;//二分求使用了第j个硬币后大于的第一个商品前缀和,就可以统计出能卖多少商品。
  31. f[i] = max(f[i],te - 1);//此时te-1为买了的商品数,取max更新
  32. }
  33. }
  34. }
  35. for(int i=0;i<=ms;++i){//再次枚举状态
  36. if(f[i] == n){//当前状态能够买完n个物品就继续进行
  37. cnt = 0;
  38. for(int j=1;j<=k;++j){
  39. if((i & g[j])){//当前状态用了第j个硬币就加上它的价值
  40. cnt += c[j];
  41. }
  42. }
  43. ans = max(ans,tot-cnt);//求出最大的剩余价值
  44. }
  45. }
  46. if(ans<0)cout<<-1<<"\n";//小于0就是不可能买完
  47. else cout<<ans<<endl;
  48. }

NO.3 吃奶酪

题目描述

房间里放着 \(n\) 块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在 \((0,0)\) 点处。

输入格式

第一行有一个整数,表示奶酪的数量 \(n\)。

第 \(2\) 到第 \((n+1)\) 行,每行两个实数,第 \((i+1)\) 行的实数分别表示第 \(i\) 块奶酪的横纵坐标 \(x_i, y_i\)​。

输出格式

输出一行一个实数,表示要跑的最少距离,保留 \(2\) 位小数。

输入输出样例

输入 #1

4

1 1

1 -1

-1 1

-1 -1

输出 #1

7.41

说明/提示

数据规模与约定

对于全部的测试点,保证 \(1\le n\le 15\),\(|x_i|,|y_i| \le 200\),小数点后最多有 \(3\) 位数字。

提示

对于两个点 \((x_1,y_1)\),\((x_2, y_2)\),两点之间的距离公式为 \(\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}\)​。

分析

再一次看到友好的奶酪数据范围,显然要从吃奶酪的状态入手转移,因为需要计算距离,所以我们需要把每个点之间的距离全部求出来,并且状态数组需要一维来存储上一次的位置,所以我们定义\(f[i][j]\)为吃奶酪状态\(i\)时位置为\(j\)的走的距离,也就是吃了奶酪\(j\),从上一个状态没吃\(j\)转移来,位置为\(k\)。所以状态转移方程就是:

\[dp[i][j] = min(dp[i][j],dp[i -(1<<(j-1))][k] + jl[k][j]);
\]

\(jl\)就是距离……

而我们需要预处理一些东西,每个位置之间距离是一个,原点与每个位置之间距离也要预处理,然后就是只吃了一个奶酪的走的长度预处理,然后就可以愉快的状态转移了。最后不要忘了把每个位置吃完奶酪的走的距离都扫一边。

代码

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define db double
  4. const int maxn = 16;
  5. double dp[1<<maxn][maxn];
  6. int n;
  7. db jl[maxn][maxn];
  8. db jlx[maxn],jly[maxn];
  9. db pow(db x){//求乘方
  10. return x*x;
  11. }
  12. db len(db xx,db yy,db x2,db y2){//求距离
  13. return sqrt(pow(xx-x2)+pow(yy-y2));
  14. }
  15. int main(){
  16. cin>>n;
  17. for(int i=1;i<=n;++i){
  18. cin>>jlx[i]>>jly[i];
  19. jl[0][i] = jl[i][0] = len(0,0,jlx[i],jly[i]);//原点到每个点的距离
  20. }
  21. for(int i=1;i<=n;++i){//每个位置之间的距离
  22. for(int j=1;j<=n;++j){
  23. jl[i][j] = len(jlx[i],jly[i],jlx[j],jly[j]);
  24. }
  25. }
  26. for(int i=1;i<(1<<n);++i){//初始化极大值
  27. for(int j=1;j<=n;++j){
  28. dp[i][j] = 999999.0;
  29. }
  30. }
  31. for(int i=1;i<=n;++i){//只吃了一个奶酪的距离
  32. dp[1<<(i-1)][i] = jl[0][i];
  33. }
  34. dp[0][0] = 0.0;
  35. for(int i=0;i<(1<<n);++i){//枚举状态
  36. for(int j=1;j<=n;++j){
  37. if((i & (1<<(j-1))) == 0)continue; //没吃第j个就continue
  38. for(int k=1;k<=n;++k){
  39. if(k == j)continue;//上个位置和这个位置相同无意义,continue
  40. if((i & (1<<(k-1))) == 0)continue;//没吃第k个也没意义,continue
  41. dp[i][j] = min(dp[i][j],dp[i - (1<<(j-1))][k] + jl[k][j]);//状态转移
  42. }
  43. }
  44. }
  45. db ans = 99999999.0;//一定是浮点数
  46. for(int i=1;i<=n;++i){//从头到尾扫一边
  47. ans = min(ans,dp[(1<<n)-1][i]);
  48. }
  49. printf("%.2lf",ans);
  50. }

NO.4 中国象棋

题目描述

这次小可可想解决的难题和中国象棋有关,在一个\(N\)行\(M\)列的棋盘上,让你放若干个炮(可以是\(0\)个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!

输入格式

一行包含两个整数\(N\),\(M\),之间由一个空格隔开。

输出格式

总共的方案数,由于该值可能很大,只需给出方案数模\(9999973\)的结果。

输入输出样例

输入 #1

1 3

输出 #1

7

说明/提示

样例说明

除了\(3\)个格子里都塞满了炮以外,其它方案都是可行的,所以一共有\(2\times 2\times 2-1=7\)种方案。

数据范围

\(100\%\)的数据中\(N\)和\(M\)均不超过\(100\)

\(50\%\)的数据中\(N\)和\(M\)至少有一个数不超过\(8\)

\(30\%\)的数据中\(N\)和\(M\)均不超过\(6\)

分析

其实这个题像极了线性dp,但是思想跟状压有一丢丢相似

想必大家都知道象棋规则吧,不知道的可以去问问度娘,按照象棋的规则,一行或者一列里最多有两个炮,所以就可以根据这个来转移了。

而且每一行也最多放两个,所以就可以一一枚举进行转移,方程过多,在这里就不列举出来了,具体见代码注释吧。

\(f[i][j][k]\)表示第\(i\)行

代码

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int maxn = 101;
  4. const int Mod = 9999973;
  5. int n,m;
  6. long long f[maxn][maxn][maxn];
  7. int Pow(int num){
  8. return num*(num-1)/2;
  9. }
  10. int main(){
  11. ios::sync_with_stdio(false);//cin/cout优化,加不加无所谓……
  12. cin.tie(0);
  13. cin>>n>>m;
  14. f[0][0][0] = 1;
  15. for(int i=0;i<n;++i){//枚举行
  16. for(int j=0;j<=m;++j){//枚举有一个棋子的列数
  17. for(int k=0;j+k<=m;++k){//两个棋子的列数
  18. if(f[i][j][k]){//上一次状态不为0才转移
  19. f[i+1][j][k]=(f[i+1][j][k]+f[i][j][k])%Mod;//不放棋子
  20. if(m-k-j>=1)f[i+1][j+1][k]=(f[i+1][j+1][k]+f[i][j][k]*(m-k-j))%Mod;//在没放棋子的列放一个
  21. if(j>=1)f[i+1][j-1][k+1]=(f[i+1][j-1][k+1]+f[i][j][k]*j)%Mod;//在放了一个棋子的列放一个
  22. if(m-k-j>=2)f[i+1][j+2][k]=(f[i+1][j+2][k]+f[i][j][k]*Pow(m-k-j))%Mod;//在没放棋子的列分别放两个
  23. if(m-k-j>=1 && j>=1)f[i+1][j][k+1]=(f[i+1][j][k+1]+f[i][j][k]*(m-k-j)*j)%Mod;//放了一个棋子和没放棋子的列各放一个
  24. if(j>=2)f[i+1][j-2][k+2]=(f[i+1][j-2][k+2]+f[i][j][k]*Pow(j))%Mod;//在放了一个棋子的列各放一个
  25. }
  26. }
  27. }
  28. }
  29. long long ans= 0;
  30. for(int i=0;i<=m;++i){
  31. for(int j=0;i+j<=m;++j){//统计答案
  32. ans=(ans+f[n][i][j])%Mod;
  33. }
  34. }
  35. cout<<ans<<"\n";
  36. }/*最后搞个总结,每次从上个状态转移而来,如果放了一个棋子,一定要乘以剩下能放棋子列的个数,放两个当然是要乘以组合数了,这样才能求出方案数*/

状压dp大总结1 [洛谷]的更多相关文章

  1. 【题解】洛谷P2704 [NOI2001] 炮兵阵地(状压DP)

    洛谷P2704:https://www.luogu.org/problemnew/show/P2704 思路 这道题一开始以为是什么基于状压的高端算法 没想到只是一道加了一行状态判断的状压DP而已 与 ...

  2. 洛谷 P4484 - [BJWC2018]最长上升子序列(状压 dp+打表)

    洛谷题面传送门 首先看到 LIS 我们可以想到它的 \(\infty\) 种求法(bushi),但是对于此题而言,既然题目出这样一个数据范围,硬要暴搜过去也不太现实,因此我们需想到用某种奇奇怪怪的方式 ...

  3. 洛谷P3959 宝藏(NOIP2017)(状压DP,子集DP)

    洛谷题目传送门 Dalao的题解多数是什么模拟退火.DFS剪枝.\(O(3^nn^2)\)的状压DP之类.蒟蒻尝试着把状压改进了一下使复杂度降到\(O(3^nn)\). 考虑到每条边的贡献跟它所在的层 ...

  4. 【题解】洛谷P3959 [NOIP2017TG] 宝藏(状压DP+DFS)

    洛谷P3959:https://www.luogu.org/problemnew/show/P3959 前言 NOIP2017时还很弱(现在也很弱 看出来是DP 但是并不会状压DP 现在看来思路并不复 ...

  5. 【题解】洛谷P1896 [SCOI2005] 互不侵犯(状压DP)

    洛谷P1896:https://www.luogu.org/problemnew/show/P1896 前言 这是一道状压DP的经典题 原来已经做过了 但是快要NOIP 复习一波 关于一些位运算的知识 ...

  6. 【BZOJ2595_洛谷4294】[WC2008]游览计划(斯坦纳树_状压DP)

    上个月写的题qwq--突然想写篇博客 题目: 洛谷4294 分析: 斯坦纳树模板题. 简单来说,斯坦纳树问题就是给定一张有边权(或点权)的无向图,要求选若干条边使图中一些选定的点连通(可以经过其他点) ...

  7. 洛谷P1171 售货员的难题【状压DP】

    题目描述 某乡有n个村庄(1 输入格式: 村庄数n和各村之间的路程(均是整数). 输出格式: 最短的路程. 输入样例: 3 0 2 1 1 0 2 2 1 0 输出样例 3 说明 输入解释 3 {村庄 ...

  8. 洛谷 P1278 单词游戏 【状压dp】

    题目描述 Io和Ao在玩一个单词游戏. 他们轮流说出一个仅包含元音字母的单词,并且后一个单词的第一个字母必须与前一个单词的最后一个字母一致. 游戏可以从任何一个单词开始. 任何单词禁止说两遍,游戏中只 ...

  9. 最短路+状压DP【洛谷P3489】 [POI2009]WIE-Hexer

    P3489 [POI2009]WIE-Hexer 大陆上有n个村庄,m条双向道路,p种怪物,k个铁匠,每个铁匠会居住在一个村庄里,你到了那个村庄后可以让他给你打造剑,每个铁匠打造的剑都可以对付一些特定 ...

随机推荐

  1. MySQL死锁及解决方案

    一.MySQL锁类型 1. MySQL常用存储引擎的锁机制 MyISAM和MEMORY采用表级锁(table-level locking) BDB采用页面锁(page-level locking)或表 ...

  2. 玩转计划任务命令:schtasks

    管理计划任务SCHTASKS /parameter [arguments] 描述:    允许管理员创建.删除.查询.更改.运行和中止本地或远程系统上的计划任务. 参数列表:    /Create   ...

  3. Java基础(八)

    一.Java集合框架 Java集合类库也将接口与实现分离. 队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数. 队列通常有两种实现方式:一种是使用循环数组:另一 ...

  4. nginx功能介绍和基本安装

    一.简介 nginx是一款自由的.开源的.高性能的HTTP服务器和反向代理服务器:同时也是一个IMAP.POP3.SMTP代理服务器:nginx可以作为一个HTTP服务器进行网站的发布处理,另外ngi ...

  5. MATLAB实例:聚类网络连接图

    MATLAB实例:聚类网络连接图 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 本文给出一个简单实例,先生成2维高斯数据,得到数据之后,用模糊C均值( ...

  6. 小程序-图片/文件本地缓存,减少CDN流量消耗

    写在前面 小程序网络图片读取: 在读取OSS图片CDN分发时流量大量消耗,导致资金费用增加. 网络图片比较大时,图片加载缓慢. 为了尽量减少上面两个问题,所以对已读的图片进行缓存处理,减少多次访问不必 ...

  7. javascript 面向对象学习(二)——原型与继承

    什么是原型? 首先我们创建一个简单的空对象,再把它打印出来 var example = {} console.log(example) 结果如下: { __proto__: { constructor ...

  8. CentOS安装部署Mysql 5.7

    1,如果没有安装wget,先安装yum -y install wget 2,下载MySQL官方的 Yum Repositorywget http://repo.mysql.com/mysql57-co ...

  9. MySQL的分页存储过程

    -- 创建分页存储过程-- 1 判断存在即删除DROP PROCEDURE IF EXISTS popp;-- 2 创建万能分页CREATE  PROCEDURE popp(_fls VARCHAR( ...

  10. Day7-微信小程序实战-交友小程序首页UI

    一般都是直接用微信提供的组件来进行布局的 在小程序中最好少用id,尽量用class 轮播图就是直接用swiper 直接在微信开发者文档里面->组件->swiper->示例代码 < ...