8/1多校赛1003血虐我 非得学会状压DP不可
感谢博主无私分享
状态压缩鼻祖: n皇后问题
描述:
传统做法是对每一行 i,暴力查找每一列 j 。 主要浪费时间在于检查是否有皇后会攻击,又花费o(n)。因此一共是o(n^3)。
改进算法引入状态压缩,依旧是每行i , 每列j 跑不掉的 o(n^2) ,不过判断上面可以用位运算花费o(1),实现复杂度降低
那么位运算是如何用于判断某个位置上是否收到攻击的呢? 举个例子吧
第1行放置的情况 1 0 0 0
第2 0 0 1 0
结合前两行,第3行在这些位置上在竖直方向上已经不能再放了( 约束条件 ) 1 0 1 0。按照这个思想,我们再算出主对角线上的约束条件,副对角线上的约束条件,通过总约束条件的约束,剩下的点就是可以放皇后的合法位置, 我们依次dfs这些合法位置就行。
所以现在问题就在于如何计算这些约束条件,这里有篇很好的文章(再次感谢博主无私分享,上面是我看完自己的理解,可以移步去这个链接了)下面记录转载过来的位运算笔记用自己的话理解
在状态压缩中,通常用 ( 1 << N ) - 1 来表示最大状态MAXST(全部都是1 ,11111111),用 A | B 来合并A和B的状态位,用 A & ~B 来清除A状态中所有的B中的状态位。
一、DFS函数参数Col,MainDiag,ContDiag的表示意义:
上面讲的,前 i行上的总列约束条件、主对角线约束、副对角线约束
二、Col == ( 1 << N ) - 1 的表示意义:
前i行把所有列上的总约束条件占满了 ,那么就是全摆上1了啊,也就是说找到了一种方案
三、emptycol = ( ( 1 << N ) - 1 ) & ~( Col | MainDiag | ContDiag ) 的表示意义:
111111 & (~ 00010) 则得到 1111101 , 相当于扣掉了总约束条件,剩下的合法序列
四、curcol = ( emptycol ) & ( ( ~emptycol ) + 1 ) 的表示意义:
也就是 lowbit = x & (-x) ,补码等于反码+1 , 比如说x等于 110 则 -x : 010 , 因此lowbit = 010,得到最右边的合法位置。 也就是用于遍历合法序列
五、 emptycol &= ~curcol 的表示意义:
类似于三,当前选中了curcol, 那么合法序列就扣掉当前位置
六、DFS递归的函数参数 Col | curcol,( MainDiag | curcol ) >> 1,( ContDiag | curcol ) << 1 的表示意义:
第一个参数在上面已经讲过了,现在讲第二个参数, 我们还是分析第1行对第2行的影响。
例如: 1 -> 0 1 0 0 0
第二行怎么找到自己主对角线上的 约束呢 ,其实只要把 第一行右移一位,然后照着第一个参数的方法累加,得到的就是对第二行的约束。 因为 (x,y) 的主对角线约束是 (x-1, y-1)。
第三个参数也就类似
以上都是本焫鸡通过自己能理解的方式理解的,要是有什么不对的地方请指出
代码:
// 位运算
#include<stdio.h>
#include <iostream>
using namespace std;
int n,high,ans;
// col 是前i-1行摆放的皇后 联合起来对 第i行的约束
// fir 前i-1行 摆放的皇后,在主对角线方向上,对i行的约束
// sec 是前i-1行摆放的皇后,在负对角线上对第i行的约束
void dfs(int col,int fir,int sec)
{
if(col == high)
{
ans++;
return;
}
int cur = high & (~(col|fir|sec)); //11111的情况 剔除掉 三座大山联合约束的情况 ,得到的序列, 存在某位为1 表示该位可以放
while(cur)
{
int lowbit = cur &(-cur); //得到最后一个位置
dfs(col|lowbit, (fir|lowbit)>>1 , (sec|lowbit)<<1);
cur = cur & (~lowbit); // 最后一个1 标记为1 。 配合while循环。相当于把所以可能性遍历了一遍
}
}
int main()
{
while(~scanf("%d",&n)&&n)
{
ans=0;
high = (1<<n)-1;
dfs(0,0,0);
printf("%d\n",ans);
}
return 0;
}
带权类n皇后问题
求到当前所用的列是第几位,然后带回去计算值,记忆话搜索剪枝
代码:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 100;
int mp[maxn][maxn], dp[maxn][maxn];
int n,ans, high;
int dfs(int col,int tmp, int i)
{
if(dp[col][i] != -1) return dp[col][i];
if(col == high)
{
return dp[col][i] = tmp;
}
int last = high& (~col);
int ans2 = 0;
while(last)
{
int lowbit = last & (-last); // 得到最低位的1 的位置
int cnt = log2(lowbit)+1;
// cout<<cnt<<endl;
ans2 = max(tmp,dfs(col|lowbit, tmp + mp[i][n-cnt+1], i+1));
last = last & (~lowbit);
}
return dp[col][i] = ans2;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
memset(dp,-1,sizeof(dp));
ans = 0;
scanf("%d",&n);
high = (1<<n)-1;
for(int i=1; i<=n; i++)
{
for(int j=1; j<=n; j++)
{
scanf("%d",&mp[i][j]);
}
}
printf("%d\n",dfs(0,0,1));
}
return 0;
}
lightoj 1011 - Marriage Ceremonies
题意:
有N个怪兽,给出H[ ]数组,代表每个怪兽的HP值,给出 mp[ n ][ p ],表示第n个怪兽死了之后,获得它的装备可以对 第p 个怪兽造成的伤害( 每攻击一下 ), 另外,我们还有一把小手枪 每攻击一次 造成 1 伤害,问的是至少攻击几次能把所有怪兽杀死?
思路:
如果用状态压缩保存怪兽存亡状态,那么数组只需要开 1<<16,我们考虑的是杀小怪兽的顺序,和使用哪把武器( 这里无限使用 ),因此我们暴力杀怪兽的顺序,n ! 级别, 每杀一只怪兽都使用效果最好的武器( 贪心 ),因此搜索的结构就是(搜索第一步一定要确立好结构,之后再填上细节,剪枝)
int dfs(int state)
{
for(怪兽:) //选出还活着的
{
for( 武器: ) // 贪心找出效果最好的
{
cos = min ;
}
DP[state] = max(DP[state], dfs(杀完小怪兽的state) + cos);
}
}
main{
dfs(0);
}
我们可以用1 表示怪兽已经死了,这里使用的不是之前的位运算遍历, 因为状态的0 我们也需要考虑, 那么如果仅仅接受状态,而不接收n , 例如 state = 1 , 那么我们无法知道还活着的还有几只,也就是1前面有几个
代码:
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000;
const int inf = 0x3f3f3f3f;
int mp[maxn][maxn], dp[maxn], h[maxn];
char st[maxn];
int n,ans, high;
int dfs(int state)
{
if(dp[state] != inf)
return dp[state];
if(state == high)
return dp[state] = 0;
for(int i=1; i<=n; i++) // 选择杀哪个怪物
{
if( !(state& (1<< (i-1) ) ) ) // 找出活的
{
// cout<<"I am alive"<<endl;
int cnt = inf;
for(int j=1; j<=n; j++) // 用哪个枪
{
if(state & (1<< (j-1) ) && mp[j][i] != 0) // 死了的为1 , 那么& 真, 就是能用这把枪
{
// cout<<"you are my gun"<<endl;
int cos = h[i] / mp[j][i] + ((h[i] % mp[j][i])? 1:0) ;
cnt = min(cnt, cos);
}
}
if(cnt == inf)
{
cnt = h[i];
}
dp[state] = min(dp[state], dfs(state | ( (1<<i-1) )) + cnt);
}
}
return dp[state] ;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
// memset(dp,-1,sizeof(dp));
ans = 0;
scanf("%d",&n);
for(int i=1; i<=n; i++)
scanf("%d",&h[i]);
high = (1<<n) -1;
for(int i=0; i<= high ; i++)
dp[i] = inf;
for(int i=1; i<=n; i++)
{
scanf("%s",st);
for(int j=0; j<n; j++)
{
mp[i][j+1] = st[j]-'0';
}
}
printf("%d\n",dfs(0));
}
return 0;
}
Poj - 3254
题目大意:农夫有一块地,被划分为m行n列大小相等的格子,其中一些格子是可以放牧的(用1标记),农夫可以在这些格子里放牛,其他格子则不能放牛(用0标记),并且要求不可以使相邻格子都有牛。现在输入数据给出这块地的大小及可否放牧的情况,求该农夫有多少种放牧方案可以选择(注意:任何格子都不放也是一种选择,不要忘记考虑!
Sample Input
2 3
1 1 1
0 1 0
Sample Output
9
分析
本题中,假设第i行允许 j 种放牛方式(即j种状态) ,那么对于每一种状态 k (k (= [1, j]),把上一行 i-1 上满足和 k 不冲突的(题目要求上下不能同时为1 )状态累加,相当于每个k对应一系列 i-1 行满足的状态 , 遍历 k 累加就行,直接上代码读起来容易些。
该类型题目由于状态很小,所以每次暴力查找状态。
代码:
#include <cstdio>
#include <cstring>
using namespace std;
#define mod 100000000
const int maxn = 10050;
int M,N,top = 0;
//top表示每行最多的状态数
int state[600],num[110];
//state存放每行所有的可行状态(即没有相邻的状态
//
int dp[20][600];
//dp[i][j]:对于前i行数据,每行有前j种可能状态时的解
int cur[20];
//cur[i]表示的是第i行整行的情况
inline bool ok(int x) //判断状态x是否可行
{
if(x&x<<1)
return false;//若存在相邻两个格子都为1,则该状态不可行
return true;
}
void init() //遍历所有可能的状态
{
top = 0;
int total = 1 << N; //遍历状态的上界
for(int i = 0; i < total; ++i)
{
if(ok(i))
state[++top] = i;
}
}
inline bool fit(int x,int k) //判断状态x 与第k行的实际状态的逆是否有‘重合’
{
if(x&cur[k])
return false; //若有重合,(即x不符合要求)
return true; //若没有,则可行
}
int main()
{
while(scanf("%d%d",&M,&N)!= EOF)
{
init(); // 1 - 1<<N 的所以可行状态保存在state[1-top]中
memset(dp,0,sizeof(dp));
for(int i = 1; i <= M; ++i)
{
cur[i] = 0; // cur[i] 代表第i行草地的情况 1001 还是 0100等等
int num;
for(int j = 1; j <= N; ++j) //输入时就要按位来存储,cur[i]表示的是第i行整行的情况,每次改变该数字的二进制表示的一位
{
scanf("%d",&num); //表示第i行第j列的情况(0或1)
if(num == 0) //若该格为0
cur[i] +=(1<<(N-j)); //则将该位置为1(注意要以相反方式存储,即1表示不可放牧
// 假设输入 1010 那么cur[i]为 1010 , 第一个读入的 左移 4-1 次
}
}
for(int i = 1; i <= top; i++)
{
if(fit(state[i],1)) //判断所有可能状态与第一行的实际状态的逆是否有重合
{
dp[1][i] = 1; //若第1行的状态与第i种可行状态吻合,则dp[1][i]记为1
}
// 例如第一行本应该是 111 那么转换成了 000 , 则状态101 跟第一行fit为1 ,则dp[1][state - 101] = 1
}
/*
状态转移过程中,dp[i][k] =Sigma dp[i-1][j] (j为符合条件的所有状态)
dp[i][k] = Sigma dp[i-1][j] (j为满足条件的所有状态)
*/
for(int i = 2; i <= M; ++i) //i索引第2行到第M行
{
for(int k = 1; k <= top; ++k) //该循环针对所有可能的状态,找出一组与第i行相符的state[k]
{
if(!fit(state[k],i))
continue; //判断是否符合第i行实际情况
for(int j = 1; j <= top ; ++j) //找到state[k]后,再找一组与第i-1行符合,且与第i行(state[])不冲突的状态state[j]
{
if(!fit(state[j],i-1))
continue; //判断是否符合第i-1行实际情况
if(state[k]&state[j])
continue; //判断是否与第i行冲突
dp[i][k] = (dp[i][k] +dp[i-1][j])%mod; //若以上皆可通过,则将'j'累加到‘k'上
// dp[i][k] = dp[i][k] + d[i-1][j] 第i行的状态 加上i-1行可用的状态。
// 比如说 i行有两种状态 010 000 那么i就要考虑这两种情况, 当选择第一个010 时, i-1 有四种情况,也就是j有四种
}
}
}
int ans = 0;
for(int i = 1; i <= top; ++i) //累加最后一行所有可能状态的值,即得最终结果!!!泥马写注释累死我了终于写完了!
{
ans = (ans + dp[M][i])%mod;
}
printf("%d\n",ans);
}
}
棋盘放子
有一个N*M(N<=5,M<=1000)的棋盘,现在有1*2及2*1的小木块无数个,要盖满整个棋盘,有多少种方式?答案只需要mod1,000,000,007即可。
例如:对于一个2*2的棋盘,有两种方法,一种是使用2个1*2的,一种是使用2个2*1的。
由于行数n很小,那么我们每一列的状态最多只有 1<<5 种,做法就是针对每一列,搜索出所有合法状态。
当前列仅仅影响下一列,比如说第一列放了1*2 ,那么第二列该位置就不能再放。
状态转移方程 dp[ j+1 ][ nex ] += dp[ j ][ state ] , nex是由当前state限制下能构成的合法状态
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<string>
#include<vector>
#include<stack>
#include<bitset>
#include<cstdlib>
#include<cmath>
#include<set>
#include<list>
#include<deque>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int INF = 0x3f3f3f3f;
const int maxn = 100;
const int mod = 1e9 + 7;
int T, n, m;
ll dp[11][(1 << 11) + 5];
void dfs(int i,int j,int state, int nex)
// 第几行,第几列,当前状态,对下一行的影响
{
if(i == n) // 到达第n+1 行的时候,当前列已经得到一种状态 state,且产生对下一行允许的 nex
{
dp[j+1][nex] += dp[j][state];
return;
}
if( (1<<(i) &state) > 0) // 当前行 被占用了,那么就跳到下一行
dfs(i+1,j,state,nex);
else
{
dfs(i+1,j,state, nex|(1<<i));//在当前行摆上 1*2,则下一列被占用一个
if(i+1<n && (((1<<(i+1)) & state) == 0) ) // 在当前列摆上2*1
{
dfs(i+2,j,state,nex);
}
}
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
if(m == 0 && n == 0) break;
memset(dp,0,sizeof(dp));
dp[1][0] = 1;
for(int j=1; j<=m; j++)
{
// 第j列
for(int k=0; k<(1<<n); k++)
// 各个状态
{
if(dp[j][k])//dp[j][k]!=0 表示第j行 k状态下,有方案可以放置木板,
dfs(0,j,k,0);
}
}
printf("%lld\n",dp[m+1][0]);
}// 要求铺满,那么m+1 不能有露出来的}
return 0;
}