蒙德里安的梦想
拿到这个题目,我们要优先知道这道题的解决关键————只横放的有效方案数就是总方案数。
所以关键就是获取包含横放的方案数
此题里,一共有两种放置的方式,横放和竖放,我们可以用什么来表示这两种状态呢?
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子集,所以
+=
![]()
但需要注意的是 ,因为“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;
}
小国王
无法相互攻击,其实就是以一个点开始,相邻的八个格子内都不能有其他的棋子放。
当前元素的上、下、左、右、左上、右上、左下、右下八个方向
但是因为想到的是动态规划,所以应该怎么规划???
首先要知道,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;
}
玉米田
和小国王那道题不同,这道玉米田的题目主要是对于十字形的田地不可以进行种植。
那么继续采用状态规划的步骤
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;
}
炮兵阵地
分析如上
#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即可
待定更新,感觉已经摸清了入门的门道
获得合法的状态,以及合法状态之间的关系,传递过去...
此文章只是一个本人的对于题目的心路历程和笔记