状压dfs小记

一点前(tu)言(cao)

真的考起dfs来可谓是什么都能往dfs上套
状压不止能dp,还能与dfs结合成为搜索好(duliu)题
剪枝卡常司空见惯(打开题解一看并不是纯dfs,emmmm)

开始正文

说到搜索,大家都做过八皇后对叭。现在我们让八皇后变成N皇后
还是N皇后

这里和八皇后不同的是地图中有些地方不可以放皇后(而且数据范围还比八皇后大1)然后它就蓝了.jpg

我们考虑像八皇后一样直接标记列,对角线爆搜,发现会T。

何以解T?唯有剪压(全称剪枝状压)

我们发现(n leq 14),放在dp中这个数据一般是让你状压,于是我们可以尝试一下状压剪枝。在某个位置放皇后,会对列,左斜的对角线,右斜的对角线有影响,所以我们可以把它们分别状压成三个量,表示下一行的状态。用1表示不能放,用0表示能放。由于我们是表示的下一行的状态,所以对于左斜的对角线来说,每往下传递一行,要左移一位,右斜的对角线就要右移一位。这样很可能会导致某些1被移出棋盘。被移到棋盘右边的1在位运算里直接没了,不用管。对于被移到棋盘左边的1,我们可以拿放满的状态(即棋盘范围内全是1)和它进行and操作,消除棋盘外的1。对于每一行来说,当前行的状态就是列,左斜,右斜的三个数或起来,当然别忘了最开始输入的不可放的位置。

我们用0表示可以放的位置,但是找到当前最低位的0似乎并没有太好的做法。这时候通过脑洞想到lowbit可以找到当前二进制表示中最低位的1。于是我们可以进行取反操作,然后对取反后的数不断取它的lowbit值来找到当前可以放皇后的位置。

同时我们还要状压一下哪些行可以放皇后,如果能放满,就说明这是个合法的方案。

由于位运算常数小,所以在我们没有剪枝的情况下也是可以轻松AC这道题的。

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#define pa pair<int,int>
using namespace std;
typedef long long ll;
inline ll read()
{
	char ch=getchar();
	ll x=0;bool f=0;
	while(ch<'0'||ch>'9')
	{
		if(ch=='-')f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<3)+(x<<1)+(ch^48);
		ch=getchar();
	}
	return f?-x:x;
}
ll n,m,ok[19],k,ans,en;//ok[i]表示输入的地图中第i行的状态,en就是每行都放满的状态
char mp[19][19];
ll lowbit(int x)
{
    return x&(-x);
}
void dfs(int now,ll lie,ll x1,ll x2,ll hang)//x1是左斜对角线,x2是右斜对角线,now记录dfs到了第几行,hang记录哪些行放了皇后
{
   if(hang==en){ans++;return ;}
   ll kk=~(ok[now]|x1|x2|lie);
   kk&=en;
   while(kk)
   {
       ll v=lowbit(kk);
       kk-=v;
       dfs(now+1,lie|v,(x1|v)<<1,(x2|v)>>1,hang|(1<<now));//注意更新在当前列放皇后的影响
   }
}
int main()
{
   n=read();
   for(int i=1;i<=n;i++)
    scanf("%s",mp[i]+1);
   for(int i=1;i<=n;i++)
   {
       for(int j=1;j<=n;j++)
        if(mp[i][j]=='.')
            ok[i]|=(1<<j);
   }
   en=(1<<n+1)-1;
   en^=1;//在这里2^1表示第一位,而不是2^0
   dfs(1,0,0,0,0);
   printf("%lld
",ans);  
}

状压+dfs不止能解决皇后问题,还能解决数独问题。
靶形数独



咱也不造为啥状压完了不用剪枝就能过.jpg
显然我们需要先处理每个九宫格的表示方法和每个格子的分数,由于我太菜了,所以打了个表然后进行(O(1))询问即可

打的表:

int gt[15][15]=//分数
{ {0,0,0,0,0,0,0,0,0,0},
  {0,6,6,6,6,6,6,6,6,6},
  {0,6,7,7,7,7,7,7,7,6},
  {0,6,7,8,8,8,8,8,7,6},
  {0,6,7,8,9,9,9,8,7,6},
  {0,6,7,8,9,10,9,8,7,6},
  {0,6,7,8,9,9,9,8,7,6},
  {0,6,7,8,8,8,8,8,7,6},
  {0,6,7,7,7,7,7,7,7,6},
  {0,6,6,6,6,6,6,6,6,6},
},
gz[15][15]=//每个格子
{ {0,0,0,0,0,0,0,0,0,0},
  {0,1,1,1,2,2,2,3,3,3},
  {0,1,1,1,2,2,2,3,3,3},
  {0,1,1,1,2,2,2,3,3,3},
  {0,4,4,4,5,5,5,6,6,6},
  {0,4,4,4,5,5,5,6,6,6},
  {0,4,4,4,5,5,5,6,6,6},
  {0,7,7,7,8,8,8,9,9,9},
  {0,7,7,7,8,8,8,9,9,9},
  {0,7,7,7,8,8,8,9,9,9},
};

各位大佬如果有公式求告知

我们先考虑如果人类玩数独应该怎么玩。显然我们要选一个确定的数比较多的地方开始搜索。在这里为了调试的时候比较优雅我只会从行搜,我们选取已知数字较多的行开始搜索。

我们先参照上一个题的思路,考虑对行,列,九宫格进行状压。wait!对九宫格进行状压!压个(peach)

所以我们就这么干脆的放弃了全部状压。其实对列,九宫格状压没有什么意义,只要标记一下哪一些填了哪些数就可以了。bool数组肯定是开的下的。那么我们考虑对每一行中填了哪些数进行状压,以便快速的找到第一个可以填数的位置。状压方法和上一个题相同,也是用0表示可以填的位置,1表示不可以填的位置,询问的时候取反+lowbit即可。

判断无解:在输入的时候先统计好确定的数能拿到的总分数,如果最终分数没变,即无解

由于这题玄学的数据和我也不会算的复杂度,我们可以以最高666ms/一个数据点的速度AC掉这道题

具体看代码叭(博主语文能力有限无法详细表达.jpg

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline ll read()
{
	char ch=getchar();
	ll x=0;bool f=0;
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<3)+(x<<1)+(ch^48);
		ch=getchar();
	}
	return f?-x:x;
}//被某些毒瘤题卡掉过的快读
int mp[10][10],ans,jg[10][10],hh[15];//mp为初始的数独,jg记录每个九宫格里面哪个数被用过了
struct ifm{
	int id,wz,lz;//wz记录信息完整度(也就是有多少个非零数),lz记录状压之后的数
}hang[15],lie[15];//在博主一番调(mo)试(gai)之后lie没有用了
int cpy[15][15],wwz;//wwz记录有多少个位置不是0,cpy在博主的一番调(mo)试(gai)之后也没有用了
bool bjh[15][10],bjl[15][10];//bjh[i][j]表示在第i行里,j这个数是否使用过,bjl是记录列
int gt[15][15]=
{ {0,0,0,0,0,0,0,0,0,0},
  {0,6,6,6,6,6,6,6,6,6},
  {0,6,7,7,7,7,7,7,7,6},
  {0,6,7,8,8,8,8,8,7,6},
  {0,6,7,8,9,9,9,8,7,6},
  {0,6,7,8,9,10,9,8,7,6},
  {0,6,7,8,9,9,9,8,7,6},
  {0,6,7,8,8,8,8,8,7,6},
  {0,6,7,7,7,7,7,7,7,6},
  {0,6,6,6,6,6,6,6,6,6},
},
gz[15][15]=
{ {0,0,0,0,0,0,0,0,0,0},
  {0,1,1,1,2,2,2,3,3,3},
  {0,1,1,1,2,2,2,3,3,3},
  {0,1,1,1,2,2,2,3,3,3},
  {0,4,4,4,5,5,5,6,6,6},
  {0,4,4,4,5,5,5,6,6,6},
  {0,4,4,4,5,5,5,6,6,6},
  {0,7,7,7,8,8,8,9,9,9},
  {0,7,7,7,8,8,8,9,9,9},
  {0,7,7,7,8,8,8,9,9,9},
};//上面打好的表
int er[1029];//er[2^x]=x,在lowbit之后算出是第几列
bool cmp(ifm a,ifm b)
{
	return a.wz>b.wz;
}
int lowbit(int x)
{
	return x&(-x);
} 
void dfs(int ha,int ann,int now)//ha表示当前填第几行,ann表示当前的分数,now表示这是在排序后的hang里的第几个
{
	if(wwz==81)
	{
		ans=max(ans,ann);
		return;
	}
	if(hh[ha]!=1022)//由于窝习惯用2^1表示第一个数,所以是1023-1=1022
	{
		int kkk=~hh[ha];
		kkk^=1;
		int v=lowbit(kkk);
		for(int i=1;i<=9;i++)
		{
			if(!bjh[ha][i]&&!bjl[er[v]][i]&&!jg[gz[ha][er[v]]][i])//判断能否填数
			{
				bjh[ha][i]=1;
				hh[ha]|=v;
				cpy[ha][er[v]]=i;	
				bjl[er[v]][i]=1;	
				jg[gz[ha][er[v]]][i]=1;//一堆神烦的标记
				if(hh[ha]!=1023)//如果填了之后,这一行还没填满,就去填这一行的下一个数 
				{
					wwz++;
					dfs(ha,ann+i*gt[ha][er[v]],now);
				}
				else wwz++,dfs(hang[now+1].id,ann+i*gt[ha][er[v]],now);//这一行填满了,搜下一个要搜的行
				bjl[er[v]][i]=0;
				jg[gz[ha][er[v]]][i]=0;
				hh[ha]^=v;
				bjh[ha][i]=0;
				wwz--;
				cpy[ha][er[v]]=0;
			}
		}
	}
	else//如果当前行已经填满了,就填下一个
	 dfs(hang[now+1].id,ann,now+1);
}
int main()
{
	er[1]=0;
	for(int i=1;i<=9;i++)
	  er[1<<i]=i;  
    for(int i=1;i<=9;i++)
     for(int j=1;j<=9;j++)
      {
      	mp[i][j]=read();
		if(mp[i][j])
      	{
      		hang[i].wz++;
      		lie[j].wz++;
      		hang[i].lz|=(1<<j);
      		lie[j].lz|=(1<<i);//应该没有用了叭
      		jg[gz[i][j]][mp[i][j]]=1;
      		ans+=mp[i][j]*gt[i][j];
      		wwz++;
      		bjh[i][mp[i][j]]=1;
      		bjl[j][mp[i][j]]=1;
		}
	  }  
	for(int i=1;i<=9;i++)
	 hang[i].id=i,lie[i].id=i;  
    for(int i=1;i<=9;i++)
      hh[i]=hang[i].lz;
	sort(hang+1,hang+10,cmp);//保证先搜已知数字多的行
	sort(lie+1,lie+10,cmp);//大概没有用了叭
	int qwq=ans;
	dfs(hang[1].id,ans,1);
	if(ans==qwq) ans=-1;//判断无解
	printf("%d
",ans);
}

提交一下

草.jpg

原文地址:https://www.cnblogs.com/lcez56jsy/p/11818540.html