剪枝可谓是搜索的灵魂所在,我们知道搜索是个愣头青小伙,一路撞到底可能都撞不到答案,他还可能要撞很多次。所以有什么方法可以让他撞的次数少一点呢?我们知道搜索会形成一个搜索树,这其中有很多的枝杈,但是他们中许多其实是无用或者重复的,我们就可以把他们都”剪“掉,或者我们可以使用别的方法去减少枝杈,这样的过程称为剪枝,我们之后的搜索题目都可以有体现。
常见的套路剪枝方法有这几种:
1、优化搜索顺序:有时候需要由大到小倒序。
2、排除重复情况:如果一个状态之前已经在之前被搜索过,那么我们没必要再搜索一次。
3、可行性剪枝:如果一个状态本来就无法达到最终状态,那么我们根本不需要再进行下去。形象的理解就是我们如果看到前面有一堵墙,那么我们就不会再走这条路,因为这条路是不通的,而不是一直走到撞墙在回去。
4、最优性剪枝:如果当前花费的代价已经超过了当前的最优解,那么也不需要再搜索下去了。
5、记忆化:记录每个状态的搜索结果,在重复遍历一个状态时直接返回。相当于我们在对图进行深搜的时候,标记一个节点是否被访问过。记忆化搜索也可以用在动态规划上。
【例题】Sudoku1(poj2676)
题意就是一个数独游戏。
爆搜,无剪枝可过。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int a[10][10];
bool check(int x,int y,int now)
{
for(int i=0;i<9;i++)
if(a[x][i]==now)
return false;
for(int i=0;i<9;i++)
if(a[i][y]==now)
return false;
int u=x-x%3,v=y-y%3;
for(int i=u;i<u+3;i++)
for(int j=v;j<v+3;j++)
if(a[i][j]==now)
return false;
return true;
}
bool flag;
void dfs(int x,int y)
{
if(flag || x==9)
{
flag=true;
return;
}
while(a[x][y])
{
if(y==8)
{
y=0;
x++;
if(x==9){flag=true; return;}
}
else y++;
}
for(int i=1;i<=9;i++)
{
if(check(x,y,i))
{
a[x][y]=i;
if(y==8) dfs(x+1,0);
else dfs(x,y+1);
if(flag) return;
a[x][y]=0;
}
}
}
char s[10][10];
int main()
{
int T;scanf("%d",&T);
while(T--)
{
for(int i=0;i<9;i++)
{
scanf("%s",s[i]);
for(int j=0;j<9;j++){
a[i][j]=s[i][j]-'0';
}
}
flag=false; dfs(0,0);
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
printf("%d",a[i][j]);
printf("\n");
}
}
return 0;
}
【例题】Sudoku2(poj3074)
题意还是一个数独游戏
在爆搜的基础上用位运算剪枝。由于原来的搜索方法是从左上角一个一个找到右下角,其中有两个可以优化的地方:
1、原来的我们在找到一个位置的时候有可能它已经被填写,我们还需要找到它往后没有被填的位置,消耗了大量时间。我们需要精准的找到未被填写的位置。
2、我们填数的方式太过于笨拙。我们思考人如何填写数独,一般先找到最容易确定的位置(也就是未填写的最少)来填写,一步一步推出来。
所以我们把每一行,每一列,每一个九宫格用一个9位二进制表示,一开始全部为1,填数则将对应位变为0。我们可以对于一行一列一个九宫格通过与运算可以得出一个val,他就代表在当前行列九宫格没有填的数的二进制表示,我们可以通过lowbit运算找到没有填的位,再找到最小的这样的位置填写。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int row[10],col[10],tub[20];
int cnt[600],num[600];
char str[10][10];
//填了的为0,没填的为1,可以用lowbit找出没填的
int get(int x,int y){
return (x/3*3)+(y/3);}
void change(int x,int y,int k)
{
row[x]^=1<<k;
col[y]^=1<<k;
tub[get(x,y)]^=1<<k;
}
bool dfs(int now)
{
if(now==0) return true;
int tmp=10,x,y;
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
{
if(str[i][j]!='.') continue;
int val=row[i] & col[j] & tub[get(i,j)];//没填的
if(!val) return false;
if(cnt[val]<tmp)
{
//找到最容易确定的位置(也就是未填写的最少)来填写
tmp=cnt[val];
x=i,y=j;
}
}
int val = row[x] & col[y] & tub[get(x,y)];
for(;val;val-=val&-val)
{
int k=num[val&-val];
str[x][y]=k+'1';
change(x,y,k);
if(dfs(now-1)) return true;
change(x,y,k);
str[x][y]='.';
}
return false;
}
char s[100];
int main()
{
for(int i=0;i<1<<9;i++)
for(int j=i;j;j-=j&-j)
cnt[i]++;//计算一个数的二进制位有几个1
for(int i=0;i<9;i++)
num[1<<i]=i;
while(~scanf("%s",s) && s[0]!='e')
{
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
str[i][j]=s[i*9+j];
for(int i=0;i<9;i++) row[i]=col[i]=tub[i]=(1<<9)-1;
int tot=0;
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
{
if(str[i][j]!='.') change(i,j,str[i][j]-'1');
else tot++;
}
dfs(tot);
for(int i=0;i<9;i++)
for(int j=0;j<9;j++)
s[i*9+j]=str[i][j];
puts(s);
}
return 0;
}
【例题】Sticks(poj1011)
给出一堆小木棍,将这些小木棍拼成一些大木棍,要求每个大木棍的长度相同。问大木棍最小是多少。
非常经典的搜索题,写一下我在草稿纸上写下的思路吧:
大体思路:枚举长度len(保证sum%len==0),搜索大木棍的组成。
则搜索函数的参数有
dfs(已经处理完的大木棍数量,现在正在处理的大木棒已经增加的长度,当前正在处理的小木棒的下标)
那么剪枝有:
1、枚举因数len时可以用sqrt来枚举,枚举复杂度降至2*sqrt(n)
2、从大到小排序,让大的先尝试
3、记录一个last,表示上次拼接的小木棍长度,那么假如这个长度不成功则相同长度的都不需要再考虑。、
4、检查木棍时,任意一个失败都意味着前面出现了问题,就可以直接退出了。(这是一个大剪枝,因为我们大部分情况都是失败的)
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<cmath>
using namespace std;
const int N=110;
int a[N],n;
int len,sum,sticks;
bool vis[N],flag;
bool cmp(int x,int y){
return x>y;}
bool dfs(int cnt,int now,int k)
{
if(cnt>sticks)
return true;
if(now==len)
return dfs(cnt+1,0,1);
int last=0;
for(int i=k;i<&#