状态压缩(棋盘问题+hdu3254+poj1185+hud2553)

问题1:
在n*n(n≤20)的方格棋盘上放置 n 个车(可以攻击所在行、列),求使它们不能互相攻击的方案总数。
分析:
利用组合数学:答案为n!
状态压缩递推:
首先,每一行只能有一个车,只要一行行地放,每行只放一个,保证同一行之间不会有攻击。其次,每一列只能放一个车,只要能记录下哪一列有车,下次不再考虑这一列就可以了。用一个二进制数S来表示某一列是否已经放置了车。例如n=5,S=01101,就表示第一、三、四列(从低位开始)已经放置了车。f[S]表示在状态S下的方案数。
那么状态S是怎么来的呢?
因为是一行行放置的,所以到状态S的时候已经放置到了第三行。
三种情况:
①前两行在第3、 4 列放置了棋子(不考虑顺序,下同),第三行在第 1 列放置;
②前两行在第1、 4 列放置了棋子,第三行在第 3 列放置;
③前两行在第1、 3 列放置了棋子,第三行在第 4 列放置。
如下图所示,依次 为以上三种情况,(红,蓝),(红,绿)分别代表两种顺序。

这三种情况互不相交,且只可能有这三种情况,根据加法原理,应该等于这三种情况的和,写成递推式就是f[01101]= f[01100]+f[01001]+f[00101],这个式子相当于01101分别从右到左把有1的位置变为零,然后相加。
推广状态S,那么f[S] =Σf[S^(1<<(i-1))],其中i是枚举的状态S中每一个1的位置(从低位到高位)。然后依次去掉一个1。
边界条件:f[0] = 1。
代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int INF=(1<<20)-1;
int f[INF];
int main()
{
    int n;
    scanf("%d",&n);
    int i,S,j;
    f[0]=1;
    for(S=1;S<=(1<<n)-1;++S)
    {
        for(i=1;i<=n;++i)
        {
            if(( S & 1<<(i-1))>0)//状态S的第i列有棋子,
            {
                int s=S^1<<(i-1);//去掉第i列的1
                f[S]+=f[s];
            }
        }
    }
    printf("%d",f[(1<<n)-1]);
    return 0;
}



问题二
n在n*n(n≤20)的方格棋盘上放置 n 个车,某些格子不能放,求使它们不能互相攻击的方案总数。

解题思路
一开始想到的应该是直接加一个判断条件,但是考虑时间超限的问题,这里还是使用状态压缩。用一个二进制数的数组cur[i]来储存第 i 行某个位置是否能放置, 1表示能,0表示不能。对于一个状态S,设tmps= S & cur[i],那么tmps就是排除掉第 i 行所有不能放置的位置之后的可放位置,只要枚举状态tmps中的位置即可。(其实我觉得这里实际上就是多了一个控制条件,就跟多了一个 if 语句差不多,只不过这里多的是一个 & ) 然后要用S与之异或。去掉S中的1。
m表示有m个点不能放,这m个点分别是第 i 行第 j 列;

代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int INF=(1<<20)-1;
int f[INF];
int cur[21];//存第i行不能放车子的点,能放存1不能放存0
int n,m;//有m个点不能放棋子

/*  求行数的办法1
int find_line(int x)//一共有几个1,即在第几行
{
    int i=1,count=0;
    for(i=1;i<=n;++i)
        if(x & 1<<(i-1)) count ++;
    return count;
}
*/

//更好的求行数的办法
int find_line(int x)
{
    int count=0;
    while(x>0)
    {
        count++;
        x &= (x-1);
    }
    return count;
}

int main()
{
    int i,S,j,s;
    scanf("%d%d",&n,&m);
    int maxn=(1<<n)-1;
    for(i=0;i<=n;++i)
    {
        cur[i]=maxn;//初始值为全都能放。
    }


    while(m--)
    {
        scanf("%d%d",&i,&j);//第i行第j列的格子不能放;
        cur[i]-=1<<(j-1);//不能放就让他变成0
    }

    f[0]=1;//初值,全空时候有一种方案

    for(S=1;S<=(1<<n)-1;++S)
    {
        int line=find_line(S);//状态S有几个1,也就是在第几行
        int temps=S & cur[line];//能放棋子
        for(i=1;i<=n;++i)
        {
            if((temps & 1<<(i-1))>0)//并且第i列有1
            {
                s =S ^ 1<<(i-1);//去掉第i列的1
                    f[S]+=f[s];
            }
        }
    }
    printf("%d",f[(1<<n)-1]);
    return 0;
}


问题三

给出一个 n*m 的棋盘(n 、m≤ 80,n*m ≤ 80),要在棋盘上放 k(k ≤ 20)个棋子, 使得任意两个棋子不相邻。问有多少种方案。

解题思路
察题目给出的规模,n、m≤80,这个规模要想用 SC 是困难的,若同样用上例的状态表示方法(放则为 1,不放为0),2^80  无论在时间还是在空间上都无法承受。然而我们还看到 n*m≤80,这种给出数据规模的方法是不多见的,有什么玄机呢?能把状态数控制在可以承受的范围吗?稍微一思考,我们可以发现:9*9=81>80,即如果n,m 都大于等于 9,将不再满足n*m≤80 这一条件。所以,我们有n 或m 小于等于 8,而2^8 是可以承受的。我们假设 m≤n(否则交换,由对称性知结果不变)n  是行数 m  是列数,则每行的状态可以用 m 位的二进制数表示。但是本题和例 1 又有不同:例 1 每行每列都只能放置一个棋子,而本题却只限制每行每列的棋子不相邻。但是,上例中枚举当前行的放置方案的做法依然可行。我们用数组 s[1..num] 保存一行中所有的num 个放置方案,则s 数组可以在预处理过程中用DFS 求出,同时用c[i]保存第i 个状态中 1 的个数以避免重复计算。开始设计状态。如注释一所说,维数需要增加,原因在于并不是每一行只放一个棋子,也不是每一行都要求有棋子,原先的表示方法已经无法完整表达一个状态。我们用 f[i][j][k]表示第 i 行的状态为s[j]且前i 行已经放置了k 个棋子(2) 的方案数。沿用枚举当前行方案的做法,只要当前行的方案和上一行的方案不冲突即可,“微观”地讲,即s[snum[i]]和s[snum[i-1]]没有同为 1 的位,其中snum[x]表示第x 行的状态的编号。然而,虽然我们枚举了第 i 行的放置方案,但却不知道其上一行(i-1)的方案。为了解决这个问题,我们不得不连第i-1 的状态一起枚举,则可以写出递推式: f[i][s[j]][t] += f[i-1][s[k]][t-c[j]];  i,j,k都需要枚举。

代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int INF=(1<<20)-1;
int f[81][1<<9][21]={0};//第i行状态为s[j],前i行放k个棋子的方案数。
int s[1<<9];//各种状态
int c[1<<9];//每种状态对应得1的个数
int n,m,maxn=0,count1=0,num=0;//n行,m列

void dfs(int state,int p,int count1)//当前状态,位置,1的个数
{
    if(p>m)//列从右向左走完了(即每一列都有相应的方案了)
    {
        num++;
        s[num]=state;
        c[num]=count1;
        return ;//返回上一个位置执行下一种方案。
    }
    dfs(state,p+1,count1);//当前位置不放
    dfs(state+(1<<(p-1)),p+2,count1+1);//当前位置放,放了之后下一个位置就不能放了。
}

int main()
{
    int i,j,k,t;
    scanf("%d%d%d",&n,&m,&maxn);
    if(m>n) swap(n,m);//让列成为小的那个
    dfs(0,1,0);//全不放,从第一列开始,1的个数是0
    for(i=1;i<=num;++i)
        f[1][s[i]][c[i]]=1;//第一行使用状态s[i],放了count[i]个棋子。是一种方案

    for(i=2;i<=n;++i)//前2-n行
    {
        for(j=1;j<=num;++j)//当前行的状态
        {
            for(k=1;k<=num;++k)//上一行状态
            {
                if(!(s[j]&s[k]))//当前行与上一行不冲突
                {
                    for(t=0;t<=maxn;++t)//前i行放了几个棋子
                    {
                        if(t>=c[j])//到当前行为止放的棋子总数要不小于选择当前状态放会添加的棋子数。
                            f[i][s[j]][t] += f[i-1][s[k]][t-c[j]];
                    }
                }
            }
        }
    }
    long long ans=0;
    for(i=1;i<=num;++i)
        ans+=f[n][s[i]][maxn]; //前n行放k个棋子,第n行选择状态s[i]的方案数相加
    printf("%I64d",ans);
    return 0;
}


问题四
hdu - 3254 corn fields
题目大意是 输入一个m*n的矩阵可以放牛,其中有一些地方不能放牛,放牛的规则是牛与牛之间只要不相邻就可以,可以不放,问有多少种方案。1 ≤ M ≤ 12; 1 ≤ N ≤ 12。输出结果要对10000000取余。

解题思路
用状态压缩dp,其实这到题和上道题差不多而且感觉更好想一点。
还是预处理出一个数组s【】,存的是一行中所有能放的状态。一个allow【】数组存的是能不能放牛, 这里用1表示不能放牛0表示可以放牛。如果allow & s >0 说明有两个1是在同一列的 ,就是说第x列不允许放牛但是状态s放了牛,这样这个状态就是非法的。那么为什么allow数组不使用1表示能放0表示不能放牛,然后allow & s 结果>0表示合法呢?这里考虑到一个反例,比如s中的不放牛状态和allow去按位与得到的结果一定=0,按上述处理属于不合法,但实际上题目要求不放牛是合法状态。
用f[i][j]表示第 i 行 使用状态 s[j] 的方案数目。第 i 行使用的状态与 allow 有关也与上一行有关。枚举这两行的状态做累加。

代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int mod=100000000;
int f[14][1<<13];//第i行状态为s[j]时候的前i行方案数目
int s[1<<13];//每行能放的各种状态
int allow[1<<13];//1表示不能放,0表示能放
int c[1<<13];//每种状态对应得1的个数
int tot;

int n,m,maxn=0,count1=0,num=0;//n列,m行,num表示每一行最多有多少种放法。

//枚举每种状态的办法①
void dfs(int state,int p)//当前状态,位置
{
    if(p>n)//列从右向左走完了(即每一列都有相应的方案了)
    {
        num++;
        s[num]=state;
        return ;//返回上一个位置执行下一种方案。
    }
    dfs(state,p+1);当前位置不放
    dfs(state+(1<<(p-1)),p+2);//当前列放,放了之后下一列就不能放了。
}

//从30-42行表示枚举各种状态的办法②
bool ok(int x)
{
    if (x & (x<<1)) return false;
    return true;
}

//各种状态
void init()
{
    num = 0;
    for (int i=0; i<(1<<n); i++)
        if (ok(i)) s[++num] = i;
}


int main()
{
    scanf("%d%d",&m,&n);
    int i,j,k,t;
    //记录能不能放,1表示不能放,0表示能放,这里不能反过来。见上面解释
    for(i=1;i<=m;++i)//提供两种处理allow的办法
    {
        for(j=1;j<=n;++j)
        {
            scanf("%d",&t);//方法①
           // if(t==0)
           //     allow[i]+=1<<(n-j);

            //方法②
            t = (!t);
            allow[i] = allow[i]*2 + t;
        }
    }



 // dfs(0,1);//在不考虑坑的情况下,求出每一行能放的状态
    init();

    for(i=1;i<=num;++i)
        if((s[i] & allow[1])==0)
            f[1][s[i]]=1;  //只要合法,第一行用什么状态都是一种方案


    for(i=2;i<=m;++i)//第i行
    {
        for(j=1;j<=num;++j)//当前行的状态
        {
            if((s[j] & allow[i])==0)//这种状态可行。(坑与放牛的位置不重合)
            {
                for(k=1;k<=num;++k)//上一行的状态
                {
                    if((s[k] & allow[i-1])==0)//上一行适用这种状态
                    {
                        if((s[k] & s[j])==0)//上一行与这一行不冲突
                        {
                            f[i][s[j]]=(f[i][s[j]]+f[i-1][s[k]])%mod;

                        }
                    }
                }
            }
        }
    }
    int ans=0;

    for(i=1;i<=num;++i)
    {
        ans=(ans+f[m][s[i]])%mod;//答案要对100000000取余
       // ans+=f[m][s[i]]%mod;//错误。
    }
    printf("%d\n",ans);
    return 0;
}




问题五

解题思路
这道题和上道题其实差不多,只不过这次是求得最优方案,递推方程有点区别。再就是因为他控制的行比上个多,所以要多开一维存状态。
预处理三个数组,s 、allow、c。  s存的是每行的各种合法状态;c存的是状态s里有多少个1即安排多少个炮兵;allow存高地和平原,这里1表示高地不能放0表示平原;
f【i】【j】【k】表示第i行状态为s【j】 第i-1行状态为s【k】。那么f【i】【j】【k】= max{ f[i-1][k][t] + c[j] }。

代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;
int f[102][1<<10][1<<10];//第i行状态为s[j]时候的,第i-1行状态为s[k]
int s[1<<10];//每行能放的各种状态
int allow[1<<10];//0表示能放,1表示高地不能放
int c[1<<10];//每种状态对应得1的个数

int n,m,maxn=0,count1=0,num=0;//n行m列,num表示每一行最多有多少种放法。

//枚举每种状态的办法①(用dfs)
void dfs(int state,int p,int count1)//当前状态,位置,1的个数
{
    if(p>m)//列从右向左走完了(即每一列都有相应的方案了)
    {
        num++;
        s[num]=state;
        c[num]=count1;
        return ;//返回上一个位置执行下一种方案。
    }
    dfs(state,p+1,count1);当前位置不放
    dfs(state+(1<<(p-1)),p+3,count1+1);//当前列放,放了之后下一列就不能放了。
}


int Cnt(int s)//当前状态有多少个1
{
    int cnt = 0;
    while (s > 0)
    {
        cnt++;
        s &= (s-1);
    }
    return cnt;
}

bool ok(int x)
{
    if (x & (x<<1)) return false;
    if (x & (x<<2)) return false;
    return true;
}

//枚举各种状态的办法②
void init()
{
    num = 0;
    for (int i=0; i<(1<<m); i++)
        if (ok(i))
        {
            s[++num] = i;
            c[num] = Cnt(i);
        }
}

bool fit(int i,int j) //第i行状态j合不合适(有没有炮兵被安排在高地上)
{
    if(allow[i] & j) return false;
    return true;
}


int main()
{
    int i,j,k,t;
    char temp;
    scanf("%d%d",&n,&m);
    getchar();
    //处理平原高地。
    for(i=1;i<=n;++i)
    {
        for(j=1;j<=m;++j)
        {
            scanf("%c",&temp);
            if(temp=='H')
            {
                allow[i]+=1<<(m-j);
            }
        }
        getchar();
    }

    init();
  //  dfs(0,1,0);
//处理第一行
    for(i=1;i<=num;++i)
    {
        if(fit(1,s[i]))
            f[1][s[i]][0]=c[i];
    }

    for(i=2;i<=n;++i)//枚举行
    {
        for(j=1;j<=num;++j)//当前行状态
        {
            if(fit(i,s[j]))//当前行没炮兵被安排在高地上
            {
                for(k=1;k<=num;++k)//上一行状态
                {
                    if(fit(i-1,s[k]) && !(s[k]&s[j]) )//上一行没炮兵被安排在高地上&&上一行和当前行不冲突
                    {
                        for(t=1;t<=num;++t)//第i-2行的状态
                        {
                            if(fit(i-2,s[t]) && (!(s[t]&s[k])) && (!(s[t]&s[j])) )//i-2行没炮兵被安排在高地上&&第i-2行和i-1行、第i行都不冲突
                            {
                                f[i][s[j]][s[k]]=max(f[i][s[j]][s[k]],f[i-1][s[k]][s[t]]+c[j]);
                            }
                        }
                    }
                }
            }
        }
    }
    int ans=0;
    for(i=1;i<=num;++i)
    {
        for(j=1;j<=num;++j)
        {
             ans=max(ans,f[n][s[i]][s[j]]);
        }
    }
    printf("%d",ans);
    return 0;
}




问题六
在 n*n(n ≤ 10)的棋盘上放 k 个国王(可攻击相邻的 8 个格子),求使它们无法 互相攻击的方案数

解题思路

和之前一样预处理一个状态数组 s,同时仍然设有数组 c 记录状态对应的 1 的个数。和问题三相同,仍然以 f【i,j,k】表示第 i 行状态为 s【j】,且前 i 行已经放置了 k 个棋子的方案数。
这题不但要求不能行、列相邻,甚至不能对角线相邻。s【j】 与 s【p】 不冲突怎么“微观地”表示呢?其实,可以尝试用预处理解决这个问题。数组 q 保存在当前行的状态为 s 的情况 下上一行不允许放置的情况(不允许放置为 1,否则为 0)。这样,可以用 q【j 】代替 s【j 】去和 s【p】 进行“微观的”比较, 得出条件是:q【j】&s【p】= 0。 解决掉这唯一的问题,接下来的工作就没有什么难度了。
那么数组p怎么求呢?有两种办法,参考上述代码中求状态数组s的两种方法。其实都可以在那两种方法中加入同一个式子 p【i】= s || s<<1 || s>>1 。不管是dfs方式求s还是循环求s 都可以在求出s的同时添加上式求出p。
这里其实 p 数组也可以不预处理。 直接枚举判断状态合不合法的时候,添加让s分别左移右移1位后 & 上一行状态 的条件就可以了。下面贴一个不处理p的代码。

这道题在南阳oj上可以交,地址: http://acm.nyist.net/JudgeOnline/problem.php?pid=492&rec=sim

代码

#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;
long long f[12][1<<11][110];//第i行状态为s[j],前i行已经放了k个棋子,此时的方案数。
int s[1<<11];//每行能放的各种状态
int c[1<<11];//每种状态对应得1的个数
int n,t,k,sum,num=0;//num表示每一行最多有多少种放法。


int Cnt(int s)//当前状态有多少个1
{
    int cnt = 0;
    while (s > 0)
    {
        cnt++;
        s &= (s-1);
    }
    return cnt;
}

//枚举各种状态
void init()
{
    num = 0;
    for (int i=0; i<(1<<n); i++)
        if ( !(i & i<<1) )
        {
            s[++num] = i;
            c[num] = Cnt(i);
        }
}


int main()
{
    int i,j;
    while(scanf("%d%d",&n,&sum)!=EOF)
    {
        memset(s,0,sizeof(s));
        memset(c,0,sizeof(c));
        memset(f,0,sizeof(f));

        init();

        //处理第一行
        for(i=1;i<=num;++i)
            if(sum>=c[i])
                f[1][i][c[i]]=1;

        for(i=2;i<=n;++i)//枚举行
        {
            for(j=1;j<=num;++j)//当前行状态
            {
                if(c[j]>sum) continue;
                for(t=1;t<=num;++t)//上一行状态
                {
                    if( (s[j]&s[t]) || ((s[j]>>1)&s[t]) || ((s[j]<<1)&s[t]) ) continue;
                    for(k=0;k<=sum;++k)
                    {
                        if(k>=c[j])
                            f[i][j][k] += f[i-1][t][k-c[j]];
                    }
                }
            }
        }
        long long ans=0;
        for(i=1;i<=num;++i)
            ans += f[n][i][sum];
        cout<<ans<<endl;
    }
    return 0;
}



问题7:

在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。
你的任务是,对于给定的N,求出有多少种合法的放置方法。


解题思路
当然这道题最简单的办法是打表,这里是为了学习状态压缩。用col表示列,maindia表示主对角线,dia表示辅对角线。0表示可以放,1表示不可以放。那么( col | maindia | dia) 就是当前皇后们的攻击范围。那么取反就是还可以放的位置。即canput= ~(col | maindia | dia)。
这里有一个问题是int 是32位,取反后 n位 前面的0全变成1了,超出了棋盘的范围,所以用 canput & ((1<<n) -1)。这里解释一下,因为(1<<n)-1 的二进制只有n为是1,n位前的位全是0。所以可以这么做。
求出了可以放的位置之后,我们每次都选最右的位置放。即put_on= canput & (-canput)。 解释下x&(-x),通过几次尝试可以发现一个规律,x&(-x)的结果是 x 从左往右数第一个1乘以权值。
然后就是怎么递归进下一行的问题,下一行的col =(col | put_on),maindia=(put_on | maindia)>>1 , 同理 dia = (put_on | dia)<<1。 这里出现了问题,左移操作会使得dia超出棋盘范围,所以同样处理:dia = ((put_on | dia)<<1) & ((1<<n) -1)。
为什么是(put_on | maindia)>>1 而不是先>>1后 | 呢?   这里是一行一行的搜,没办法直接一下子把当前对角线对所有行的影响表示出来,所以每次放一个棋子,按位 | 上之前的状态然后右移,这样上一行放棋子的位置全部在下一行右移了一个位置,就是对角线的位置。上上行放棋子的位置继续右移,以此类推,就形成了对角线。


代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
int n,ans;
int maxn;

void dfs(int col,int maindia,int dia)
{
    if(col == maxn) {ans++;return;}//每一列都放上了
    int canput = ((~(col |  maindia| dia)) & maxn);
    while(canput)
    {
        int put_on=(canput & (-canput));//放在从右向左数第一个能放的位置
        dfs((put_on|col), ((put_on|maindia)>>1), (((put_on|dia)<<1) & maxn) );
        canput &= (~put_on);//去放从右向左下一个位置
    }
}

int main()
{
    while(scanf("%d",&n) && n)
    {
        ans = 0;
        maxn = (1<<n)-1;
        dfs(0,0,0);
        printf("%d\n",ans);
    }

    return 0;
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值