状态压缩DP入门(算入了个门吧)

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;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值