状压(广场铺转,拉灯游戏)

广场铺砖

题目:

有一个W行H列的广场,需要用1*2小砖铺盖,小砖之间互相不能重叠,问有多少种不同的铺法?
只有一行2个整数,分别为W和H,(1<=W,H<=11)

分析:

状压经典??
其实状态非常的普遍性,f[i][s]中i表示铺到第i行,且第i行的状态为s(0表示这个格子未铺,1表示已铺)。因为是计数,所以采用以第i行拓展到第i+1行。而骨牌可以竖放,则可以影响到两行。所以不妨将状态理解为前i-1行已经铺满,且因为前一行,才将第i行铺成了s这个状态。所以对于在第i行铺砖时,实际上是要将第i行剩下的铺满,且顺便去更新影响下一行,也就是说你只铺第i行,不管下一行。
综上所述,
f[i][s]表示前i-1行以铺满,且由于第i-1行铺砖,第i行状态为s的方案数。
转移方程
我用的dfs去拓展。
如果第i行第j列已经被铺,那这个地方肯定不铺了,则对下一行也没什么影响。
如果第i行第j列没有被铺,那么可以分两种情况:
1.如果要横放,那么第i行第j+1列的格子也要没有放,所以行数要加2,对下一行也没啥影响。
2.如果要竖放,那就放吧,然后你把下一行这个格子给铺了。
等到把第i行铺完了,f[i+1][ss]+=f[i][s];
相信大家都知道第i行的状态s在每次dfs过程中不会改变,因为对于每个f[i][s]来讲,其实前一行都是铺满的。
PS:对于转移可以去找一下图片,而且我代码很丑的。

代码:

#include <bits/stdc++.h>
using namespace std;
int n,m;
long long f[20][3000];
void dfs(int h,int k,int s,int ss)//第i行要铺第k列,第i行状态为s,第i+1行状态为ss
{
	if (k>m) {f[h+1][s]+=f[h][ss];return;}//铺完就累加方案数
	if (!(ss&(1<<(k-1))))//这个格子没有铺过
	{   
	    dfs(h,k+1,s^(1<<(k-1)),ss);//竖铺,只有此时会影响下一行
	    if (k<m&&(!(ss&(1<<k)))) //横铺,判断下一列有没有铺过
		    dfs(h,k+2,s,ss);//注意列要+2
    }
    else dfs(h,k+1,s,ss);
    return;
}//其实为了操作方便,此处将s的末位当作第一列的状态,都一样的啦。
int main()
{
	freopen("floor.in","r",stdin);
	freopen("floor.out","w",stdout);
    cin>>n>>m;
    f[1][0]=1;
	for (int i=1;i<=n;i++)//枚举行
      for (int j=0;j<=(1<<m)-1;j++)//枚举状态
      	dfs(i,1,0,j);//dfs枚举的是铺完第i行能够将第i+1行铺成的状态
    cout<<f[n+1][0]; 
    //这是一个很重要的地方,如果是f[n][(1<<m)-1的话,仅是由于第n-1行把第n行铺满的方案数,这明显不全。
	return 0;
}

最后,我相信各位dalao可以写出不一样的转移不一样的状态以及不一样的理解。
其实加强版只是比这道题多一些情况,只要改一下dfs就好了(多些判断)。

拉灯游戏

题目

又名费解的开关??
好啦,就是一个5*5的矩阵,1为亮,0为不亮,没改变一盏灯的状态,那么他上下左右的状态都要改变,求全部亮的最少改变次数,且如果改变次数超过6次就输出-1

分析

这里分两种方法。

方法1.

最终状态是一定的,所以不妨从最终状态开始逆推,首先将灯的状态从二维转为一维,用状压的方法来存储每个状态是否已经被拓展过,由于BFS的特性,当一个状态第一次入队时所需的改变次数一定最优。
用BFS,如果拓展出的状态没有到达过,就入队,由于改变次数超过6次就输出-1,所以如果一个状态至少需要6次才能达到,那么这个状态就不必再去扩展其他状态。
分析一下空间时间复杂度什么的:
空间,存储状态需要一个2^25的一个数组;
时间,C ( 6 25 ) 6 \choose 25 (256);
时间很稳,空间似乎要128MB的样子,空间不够优秀。
代码:

#include <bits/stdc++.h>
using namespace std;
int q[(1<<25)],dis[(1<<25)],n;              
int main()
{
	int head=1,tail=0;
	q[++tail]=(1<<25)-1;
	for (int i=0;i<=(1<<25)-1;i++)
	  dis[i]=-1;
	dis[(1<<25)-1]=0;                        
    while (head<=tail)
	{
		int x=q[head];
		if (dis[x]==6) break;
		for (int i=1;i<=25;i++)
		{
			int xx=x;
			xx=xx^(1<<(i-1));
			if (i>5) xx=xx^(1<<(i-5-1)); 
            if (i+5<=25) xx=xx^(1<<(i+5-1));
            if (i+1<=25&&i%5) xx=xx^(1<<(i+1-1));
			if (i-1>0&&i%5!=1) xx=xx^(1<<(i-1-1));
			//这里要注意边界情况,上下左右都要改变的
			if (dis[xx]==-1)
			{
			    dis[xx]=dis[x]+1;
				q[++tail]=xx;
		    }
	    }
	    head++;
    }
    cin>>n;
    for (int i1=1;i1<=n;i1++)
	{
		int s=0;
	    for (int i=1;i<=5;i++)
		  for (int j=1;j<=5;j++)
		  {
		  	  char x;
		      cin>>x;
		      s=s+((int(x-48))<<(25-5*(i-1)-j));
		  }
		cout<<dis[s]<<endl;
    }
    return 0;
}

方法2.

Acwing此题空间只有64MB,上述做法显然不行。
假设我们现在已近确定了第一行改变了哪些灯,那么改变完以后,第一行有些灯亮着,有一些则没亮。
由于此时第一行改变的灯是确定的,所以第一行那些没亮的灯想要亮的唯一方法就是让这盏灯下面的那盏灯改变。反之若这盏灯亮了,那么它下面那盏灯就一定不能改变(否则他就不亮了,也没有灯能去让它亮了)。然后根据数学归纳法,只要确定了第一行改变的灯,那么改变灯的方案就是一定的(如果到了最后一行还有一些灯没有亮,那么说明这个方案是不可行的)
所以首先枚举一下第一行改变了哪些灯,方案数 2 5 2^5 25,最于每个方案推下来就好,看看是否有解,然后求个最小值什么的。
所以再*25
所以时间复杂度大概 2 5 2^5 25*25(反正很小就对了,实际上你还要干一些事)
空间:只要存每一行的状态就好了(上一行的状态此时已经没有用了)
代码:

#include <bits/stdc++.h>
using namespace std;
int num,ss[1010],s[1010],a[6][6],ans_,n,aa[6][6];
int main()
{
    num=0;
    cin>>n;
    for (int i1=1;i1<=n;i1++)
    {
        for (int i=1;i<=5;i++)
          for (int j=1;j<=5;j++)
          {
            char ch;
            cin>>ch;
            aa[i][j]=int(ch-48);
          }
        int ans_=1e9;
        for (int i=0;i<=31;i++)//枚举第一行改变了那些灯(0表示不改变,1表示改变)
        {
            s[i]=i;
            int ans=0;
            for (int j=1;j<=5;j++)
              for (int k=1;k<=5;k++)
                a[j][k]=aa[j][k];
            for (int j=1;j<=5;j++)
            {
              for (int k=1;k<=5;k++)
                if (s[i]&(1<<(k-1))) //如果这盏灯要改变
                  {
                      ans++;
                      if (k>1) a[j][k-1]=(!a[j][k-1]);
                      a[j][k]=(!a[j][k]);
                      if (k<5) a[j][k+1]=(!a[j][k+1]);
                      if (j<5) a[j+1][k]=(!a[j+1][k]);//改变上下左右的灯
                  }
              int aaa=0;
              for (int k=1;k<=5;k++)
                if (!a[j][k])
                  aaa|=(1<<(k-1))//这里统计下一行哪些灯要改变
              s[i]=aaa;
            }
            if (s[i]||ans>6) continue;
            ans_=min(ans,ans_);
        }
        if (ans_!=1e9) cout<<ans_<<endl;else cout<<-1<<endl;
    }
    return 0;
}

总结

一些技巧:
逆向思维(种花小游戏,拉灯游戏)
预处理出每一行可行的状态,推一下这些方案数大概有多少(炮兵阵地)
如果数据范围太大就放弃吧…
易错点:
数组开太小?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值