状态压缩dp

蒙德里安的梦想

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

拿到这个题目,我们要优先知道这道题的解决关键————只横放的有效方案数就是总方案数。

所以关键就是获取包含横放的方案数

此题里,一共有两种放置的方式,横放和竖放,我们可以用什么来表示这两种状态呢?

1和0 ,1就代表该位置横放,0就代表该位置竖放

定义的dp含义

dp[i][j] 表示在i-1列放置好的前提下,i-1列放置的状态延伸到i列来的,状态为j的总方案数。

状态j是什么?

j在数组中,可以看作一个二进制的数,此二进制的数代表了某一列中,每一行的状态,但是实际上是由十进制来表达,只是抽象成为二进制数去表达状态。

但需要注意的是,此处的“1”,的意思是该位置开始横放,向右延伸,其余不横放的话,都是“0”。

因此,在图中的1001 对应的j应该就是 9,讨论的也是对于9,只不过思考的时候要换为二进制来思考,代码中具象化为十进制。

状态转移方程

我们已经知道了dp[i][j]处的j状态,其实是由i-1处的k状态来获得的,但因为j会对应不同的k子集,所以dp[i][j]+=\sum dp[i-1][k]      

但需要注意的是 ,因为“1”代表横放,会延伸到下一列,所以当i-1列的状态为k时,与i列的状态j不能有交集,也就是同一行中,相邻列不能同时为1。

例如在这两个图中

图1: 第一列为1100,第二列为0011,这样不会有交集是合法的,倘若第二列为1111,

那么前两行的蓝色部分会和绿色部分有交集,因此 (k&j)==0 是保证k状态成功转移到j状态的条件之一。

图2:对于合并,由于前后两个合并起来了,那么往第三列传递的状态应该为前两列的状态合并 ,便为 (k|j),前面的状态会影响到后面的状态,所以需要判断合并后的列的空余位置是否可以放入竖放的(也就是是否有连续的0).

此题的重点就是判断有多少个横放的方案,对于竖放,只需要判断该列在横放后,剩余的位置的连续的0是否为偶数即可。 所以就是(k|j)的判断

预处理

预处理有两个

1:首先就是判断不同状态j下的,每一列中是否有连续的偶数个0,这对于判断合并后的列是否合法有很大的作用

2. 对于每一列中的判断后,需要获取j状态下,合法的k状态,这不仅需要合并和后的(k|j) 合法,还要不能有相同的1,也就是 (j&k)==0 因此需要对此进行书写

代码

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 12, M = 1 << N;
long long f[N][M];//此处是动态规划的表
int n, m;
vector<int> state[M];//定义是行,因为M代表的是状态 (2^N -1),此处获取的是j状态对应的前一个合法的k状态
bool st[M];//此处也是对于每一列的是否合法进行判断
int main()
{
	while (cin >> n >> m, n || m) {
		//预处理,对于每一列的状态的判断,判断剩余的位置是否是连续的偶数个0
		for (int i = 0; i < 1 << n; i++) {   //处理每一列的状态的连续的0的个数
			int cnt = 0;
			bool is_vaild = true;//默认此处合法
			for (int j = 0; j < n; j++)//进行位运算,对于i的连续的0的个数进行判断
			{
				if (i >> j & 1) {   //此处为1的话,那么需要判断前面0的个数
					if (cnt & 1) {  //为1的话,那么就说明此处的连续的0的个数是奇数,不合法
						is_vaild = false; break;
					}
					cnt = 0;//重置,判断后续的连续的0的个数
				}
				else cnt++;  //不为0的话,那么说明还是连续的0
			}
			if (cnt & 1) is_vaild = false;//此处就是判断后面的连续的0是否是奇数
			st[i] = is_vaild;//i状态的赋值,表明此处是否合法
		}
		//预处理2,就是获取j状态前一个k状态是否合法,就是可以到达j状态的k状态。
		for (int j = 0; j < 1 << n; j++) {
			state[j].clear();
			for (int k = 0; k < 1 << n; k++) {
				if ((k & j) == 0 && st[j | k]) {
					state[j].push_back(k);//表示k状态可以到达j状态
				}
			}
		}
		//dp状态转移的核心
		memset(f, 0, sizeof(f));
		f[0][0] = 1;//此处要赋值为1,其实代表的是,什么都不放置的话,也是一种方案
		for (int i = 1; i <= m; i++) {   //i代表的是列,,所以赋值为m
			for (int j = 0; j < 1 << n; j++) {
				for (auto k : state[j]) //遍历可以到达j状态的合法的k状态
					f[i][j] += f[i - 1][k];
			}
		}
		cout << f[m][0] << endl;  //为什么是f[m][0] 因为f[i][j]代表的是前i-1列已经摆好,到达i列的状态为j
		//所以f[m][0] 其实就是前0到m-1列已经摆好,然后到达m列的没有溢出,也就是没有横放的,其实代表前m-1列已经都摆放满了,所以就是全部的方案数
	}
	return 0;
}

小国王

1064. 小国王 - AcWing题库

无法相互攻击,其实就是以一个点开始,相邻的八个格子内都不能有其他的棋子放。

当前元素的上、下、左、右、左上、右上、左下、右下八个方向

但是因为想到的是动态规划,所以应该怎么规划???

首先要知道,dp其实就是将一类集合的情况可以用另一类的集合表达出来。

因此,若想表达第i行的情况,那么就需要获得第i-1行的情况


所以相邻两行的状态怎么才算合法呢??一行的情况怎么才算合法呢??

首先讨论一行的情况,若合法的话,那么相邻的棋子一定不能放在一起,因为若有两个棋子放在相邻的话,那么肯定不合法,因此就是一个状态,不可以有两个相邻的1.


两行的情况怎么算合法呢?? 因为两行的状态会传递给i+1行,所以还需要对于 第i行和第i-1行的情况合并后进行判断。  


状态转移方程

假如第i行的状态为a,第i-1行的状态为b,那么该怎么表达

f[i][a]+=f[i-1][b] ? 

但是题目里还有一个重要的点就是,要放入k个棋子

所以应该定义的是 f[i][j][k] :在已经放入到第i行的棋子的时候,已经放入了j个棋子,第i行状态为k的方案数


因此还需要获得每一行合法状态的1的个数


所以就是状态转移方程就是 f[i][j][a]+= f[i-1][j-c][b] 

c就是a状态的1的个数,因为j表示的是已经放入了j个棋子,那么a状态放入了count(a)个棋子

那么对于b状态的值,就是已经放入了 j-count(a) 个棋子


动态规划的问题,不可能准确的直到每一个地方的值,只能是由一类的集合来获得的值

要是想获得b状态的对应的放入的值,那么其实就违背了动态规划的思想了。

代码

#include <bits/stdc++.h>
using namespace std;
//f[i][j][z] 表示前i行已经放入了j个k,第i行状态为z的最大方案总数,倘若第i-1行的状态为b,第i行,也就是a状态所拥有的1个数为count(a)
//假设第i行的状态为a, i-1行的状态为b
//则第i-1行的状态转移可以写为 f[i-1][j-count(a)][b]
//所以就是 f[i][j][a]+=f[i-1][j-count(a)][b];
//那么相邻两行,若符合要求的话,a,b状态需要怎么样??
//假设第i行的状态为a, i-1行的状态为b
typedef long long ll;
const int N=12,M=1<<N,K=121;  
ll f[N][K][M];
vector<int> state;//保存有效的状态,直接放入,之后用下标读取
int cnt[M];//保存每个状态对应的1的个数
vector<int> head[M];//保存一个状态对应的合法的状态
int n,k;

bool isvaild(int u){  //一个状态不可以有相邻的1,否则就是false
    for(int i=0;i<n;i++){ 
        if((u>>i&1)&&(u>>i+1&1)) return false;
    }
    return true;
}
int count(int u){
    int ret=0;
    for(int i=0;i<n;i++) ret+=u>>i&1;
    return ret;
}
int main(){
    cin>>n>>k;
    for(int i=0;i<1<<n;i++){ //判断全部的状态的合法性
        if(isvaild(i)) {
            state.push_back(i);//放入这个状态
            cnt[i]=count(i);//顺便统计一下这个状态的1的个数
        }
    }
    //预处理完后,判断上一个状态为i,下一个状态为j的时候的合法
    //但前提是两个状态都得是合法的,所以要在state中取
    for(int i=0;i<state.size();i++){
        for(int j=0;j<state.size();j++){
            int a=state[i],b=state[j];//选取两个合法的状态
            if((a&b)==0&&isvaild(a|b))  //两个的或运算后,也没有相邻的1,所以就符合
            {
               // head[a].push_back(b);//放入这个数据   状态处理
                head[i].push_back(j);//放入这个数据   ,下标处理
                //放入的是下标,因为state中是用下标读取元素的,下标其实也是对应着元素。
                //此处就是状态的下标代替了状态进行了运算???
                //那如果用状态进行运算呢???
            }
        }
    }
    //预处理
    f[0][0][0]=1;
    //接下来的就是对于dp的处理
    for(int i=1;i<=n+1;i++){
        for(int j=0;j<=k;j++){
            for(int a=0;a<state.size();a++) //遍历合法状态的下标,然后获取状态
            {
                /*int ao=state[a];//获取a这个状态n,那么此时就是用状态来进行计算*/
                
                for(int b:head[a]){   //遍历a状态对应的合法的b状态
                    int c=cnt[state[a]]; //获取a状态的元素
                    if(j-c>=0) f[i][j][a]+=f[i-1][j-c][b];
                }
            }
        }
    }
    cout<<f[n+1][k][0]<<endl;  //直接用第n+1行什么都不放为最后的值
    return 0;
}

玉米田

327. 玉米田 - AcWing题库

和小国王那道题不同,这道玉米田的题目主要是对于十字形的田地不可以进行种植。

那么继续采用状态规划的步骤


1.同一行中,哪些状态是合法的呢?? ————没有两个连续的1在一起,所以需要判断一个状态是否合法,合法的话,就放入state————为下一步的相邻两行的状态判断做准备。


2.接着就是需要判断两行相邻状态的是否合法

也就是若第i行的状态合法,那么就需要获取第i-1行中,与第i行状态结合后共同合法的状态。

假设第i行的状态为a,i-1行的状态为b,那么 两行共同合法的条件应该是,同一列没有相同的1,因此就是 a&b==0  ,成立的话, 那就对应a的状态放入b


3.逐行进行判断,也就是对于每一行的状态进行讨论。先获得第i行要放入的状态a,然后将状态a与该行的情况进行判断,也就是与空地的位置进行判断,如果同为1的话,那么就不能放入,表示冲突,否则的话,第i行的状态a的方案数 += 第i-1行状态b的方案数。

状态b由a对应的合法的数组head得到


所以步骤为

1. 根据题目,判断状态由行确定还是由列确定

2.判断每一行/列合法的条件,并且将合法的状态放入数组中

3.根据题目,判断多行/多列共同需要满足的条件是什么,也就是对于a状态与b状态合并后的状态需要满足的要求 。那么此时就把满足的b状态放入a状态对应的数组里(所以此处是一个二维数组)

4.DP环节,模板就是

for(int i=1;i<=n+1;i++){  //获取遍历每一行,但是会到n+1,这个后面说
    for(int a:state){  //在合法状态里获得要放入第i行的状态
    {
        if()  //判断放入第i行的状态与本来的这一行的状态是否有冲突(就是a状态中填充的位置,在给定的数据里是否可以放入)
        //若可以放入的话,那么就遍历a状态对应的合法的b状态,也就是i-1行的状态
        for(int b:head[a])
            f[i][a] = f[i][a]+f[i-1][b];
    }
}

5.输出值是什么?

因为f[i][j]表示的是前i-1行放置好了,第i行的状态为j的方案数

所以假如前i行已经放好了,所以应该输出 f[i+1][---]

列输出什么?? 因为状态dp都为一个 状态类的模型,所以i+1行就什么都没放,在宏观定义上是什么都可以不放的,所以就是 f[n+1][0]

6.初始化

初始化千万不能忘,所以就是什么都不放的时候,也能为一个方案数,f[0][0]=1

#include <bits/stdc++.h>
using namespace std;
const int N=14,M=1<<12;
const int mod=1e8;
int f[N][M];//表示的是读取到第i行,状态为j的方案数
//返回值应该是 f[n+1][0]  此时就是前面的都放完了
vector<int> state;
vector<int> head[M];//放入状态对应的合法的状态
int g[N];//放入每一行中土地的位置
int cnt[M];
int n,m;
bool isvaild(int u){
    for(int i=0;i<m;i++){
        if((u>>i)&1&&(u>>i+1)&1) return false; 
    }
    return true;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        for(int j=0;j<m;j++) //对于空地的位置置1
        {
            int t;cin>>t;
            g[i] += !t*(1<<j);
        }
    }
    //合法状态的放入
    for(int i=0;i<1<<m;i++){   //以行为状态,获取合法状态
        if(isvaild(i)){
            state.push_back(i);
        }
    }
    //判断相邻两个状态
    for(int a:state){//第i行的状态
        for(int b:state){//第i-1行的状态
            if(!(a&b)){
                head[a].push_back(b);
            }
        }
    }
    //初始化
    f[0][0]=1;//什么都不放就是1
    //DP环节
    for(int i=1;i<=n+1;i++){
        for(int a:state){  //获取第i行的状态a
            if(!(g[i]&a)) //合法的话,即将放入的状态和该行状态不能同时为1
            for(int b:head[a]) //寻找i-1行中,与a状态对应的合法的状态b
            {
                f[i][a]=(f[i][a]+f[i-1][b])%mod;
            }   
            
        }
    }
    cout<<f[n+1][0]<<endl;
    return 0;
}

炮兵阵地

292. 炮兵阵地 - AcWing题库

分析如上

#include <bits/stdc++.h>
using namespace std;
const int N=110,M=1<<10;
int f[2][M][M];  //四维压缩成三维
int g[M];
vector<int> state;//获取合法的状态的数组
int cnt[M];
vector<int> head[M];
int n,m;//定义行和列
//首先写每一行的合法状态判断
//由于统计的是炮兵的数量,所以跟每个状态的1的个数有关,所以需要统计1的个数
bool isvaild(int u){  //左边两个位置不可以有1
     for (int i = 0; i < m; i ++ )
        if ((u >> i & 1) && ((u >> i + 1 & 1) || (u >> i + 2 & 1)))
            return false;
    return true;
}

int count(int u){
    int res=0;
    for(int i=0;i<m;i++) res+=u>>i&1;  //m为列
    return res;
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        for(int j=0;j<m;j++){  //获取每一列的状态,高地处不能放
            char ch;cin>>ch;
            g[i]+= (ch=='H')*(1<<j); //高地处置为1
        }
    }
    //获取合法的状态
    for(int i=0;i<1<<m;i++){
        if(isvaild(i)){
            state.push_back(i);  //放入合法的状态
            cnt[i]=count(i);     //获取合法状态的1的个数,用数组存储
        }
    }
    //对于合法的两个状态的初始化
    for(int a:state){
        for(int b:state){
            if(!(a&b)) head[a].push_back(b);
        }
    }
    //DP
    for(int i=0;i<n+2;i++){
        for(int a:state){
            if(!(a&g[i]))
            for(int b:head[a])
                for(int c:head[b]){
                    if(!(a&c))
                    f[i&1][a][b]=max(f[i&1][a][b],f[i-1&1][b][c]+cnt[a]);
                }
        }
    }
    cout<<f[n+1&1][0][0]<<endl;
    return 0;
}

此处由于N=100,所以范围很大,所以需要一个滚动数组来实现,滚动数组实现就需要&1即可


待定更新,感觉已经摸清了入门的门道

获得合法的状态,以及合法状态之间的关系,传递过去...

此文章只是一个本人的对于题目的心路历程和笔记

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值