深搜剪枝总结
深搜剪枝分5种:
1.优化搜索顺序
2.排除等效冗余
3.可行性剪枝
4.最优性剪枝
5.记忆化搜索
数的划分
可行性(上下界)剪枝
由于答案中的数不考虑顺序,不妨设它是单调递增的
则对a[i],下界是a[i-1],上界是rest/(k-i+1)
#include <iostream>
#include <cstdio>
using namespace std;
int n,k;
int ans=0;
int a[10];
void dfs(int layer,int rest)
{
if(rest==0)return ;
if(layer==k-1&&rest<a[layer])return ;
if(layer==k-1&&rest>=a[layer])
{
ans++;return;
}
int x=rest/(k-layer);
for(int i=a[layer];i<=x;i++)
{
a[layer+1]=i;
dfs(layer+1,rest-i);
}
}
int main()
{
scanf("%d%d",&n,&k);
a[0]=1;
dfs(0,n);
printf("%d",ans);
return 0;
}
生日蛋糕
最优性剪枝:当前面积+之后最小面积>ans
可行性剪枝:当前体积-之后最大体积>0 (可以预处理)
上下界剪枝:上界:上一层的半径和体积(或通过剩余体积计算,二者取最小值)
下界:层数
搜索顺序剪枝:倒序枚举,能更快达到目标体积,加速最优性剪枝
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
int n,m;
int ans;
void dfs(int cen,int restv,int r,int h,int s)
{
if(s+m-cen>=ans)return ;
if(cen==m)
{
if(restv==0)ans=min(ans,s);
return ;
}
if(restv<=0)return ;
if(restv-r*r*h*(m-cen)>0)return ;
for(int i=r-1;i>=m-cen;i--)
{
for(int j=h-1;j>=m-cen;j--)
{
if(cen!=0)
{
dfs(cen+1,restv-i*i*j,i,j,s+2*i*j);
}
else
{
dfs(cen+1,restv-i*i*j,i,j,s+2*i*j+i*i);
}
}
}
}
int main()
{
scanf("%d%d",&n,&m);
ans=1e9;
dfs(0,n,sqrt(n),sqrt(n),0);
printf("%d",ans);
return 0;
}
小木棍
最优性剪枝:1.木棍原来的长度一定大于等于所有木棍中最长的那根,小于等于木棍的长度和,枚举时只需枚举到
木棍长度和的一半,因为如果超过一半,那只能是由一根原木棍切成所有的木棍
2.设所有木棍长度和为sum则原长度一定能被sum整除
可行性剪枝:3.短的木棍一定比长度更灵活,所以先从长到短排序,把难搞的先搞了,其实很多dp题也有着类似
的贪心思想(比如洛谷P4138挂饰),需要先排序再进行操作,不然就可能会WA。
4.从第一根长度小于或等于当前木棍剩余长度的开始搜索(二分查找)
5.预处理出一个数组表示跟当前木棍长度相等的木棍的最后一根,因为如果当前木棍搜不出结果,等
长的木棍也同样搜不出结果(如果能搜出结果,那pd就会为1,这于深搜的性质有关)
6.用pd记录是否已查找到符合条件的情况,若找到,立刻返回
小细节:注意搜索树的性质,一定要回溯,读入数据时要过滤掉长度大于50的木棍
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int n,maxx,sum;
int a[80],ans,pd=0,len;
int pre[80],used[80];
bool cmp(int x,int y)
{
return x>y;
}
void dfs(int k,int rest,int last)
{
if(pd)return ;
if(rest==0)
{
if(k==sum/len)
{
pd=1;return;
}
int cc=0;
for(int i=1;i<=n;i++)
{
if(!used[i])
{
cc=i;
used[i]=1;
break;
}
}
dfs(k+1,len-a[cc],cc);
used[cc]=0;//一定要记得回溯,《一本通》上这儿忘了回溯
if(pd)return;
}
int l=last+1,r=n,cnt=n+1;
while(l<=r)//优化4
{
int mid=(l+r)>>1;
if(a[mid]<=rest)
{
cnt=mid;r=mid-1;
}
else l=mid+1;
}
for(int i=cnt;i<=n;i++)
{
if(!used[i])
{
used[i]=1;
dfs(k,rest-a[i],i);
used[i]=0;
if(pd)return;//优化6
if(rest==a[i])return;
i=pre[i];//优化5
if(i==n)return;
}
}
}
void solve()
{
for(int i=maxx;i<=sum/2;i++)//优化1
{
len=i;
if(sum%i==0)//优化2
{
dfs(0,0,0);
if(pd==1)
{
ans=i;
return ;
}
}
}
return ;
}
int main()
{
scanf("%d",&n);
int tot=0;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
if(x>50)continue;
a[++tot]=x;
maxx=max(maxx,x);
sum+=x;
}
n=tot;
sort(a+1,a+n+1,cmp);//优化3
ans=sum;
for(int i=1;i<=n;i++)
{
int j=i;
while(a[i]==a[j])j++;
pre[i]=j-1;
}
solve();
printf("%d
",ans);
return 0;
}
Addition Chains
迭代加深+剪枝
因为这道题要求的是满足条件的最小深度,所以用迭代加深会更可做一些
这道题还有一个显然的贪心性质,每一个数一定等于上一个数加上 之前的数(可以是上一个数),否则的话如果是两个上一个数之前的数相加,那么上一个数就可以得到了,不需要多占一个地方。
可行性剪枝:数列每增加一个数,这个数的大小最大是在上一个数的基础上*2,如果剩下的长度中每个数都达到最大值,但最后一个数任然小于n则返回
优化搜索顺序:倒序枚举,因为这样能够更快地到达n(这道题是一道典型的优化搜索顺序题)
小细节:这道题特别坑的一个点是针对每组数据,行尾不能有空格 真的玄学坑
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define ll long long
ll a[10010],dep,n;
int pd=0;
void dfs(ll step)
{
if((a[step]*(1<<(dep-step)))<n)return;
if(step==dep)
{
if(a[step]==n)pd=1;
return;
}
for(ll i=step;i>=1;i--)
{
a[step+1]=a[i]+a[step];
dfs(step+1);
if(pd)return;
a[step+1]=0;
}
}
void solve()
{
a[1]=1;
for(ll i=1;i<=n;i++)
{
dep=i;
dfs(1);
if(pd)return ;
}
}
int main()
{
while(scanf("%lld",&n))
{
if(n==0)break;
memset(a,0,sizeof(a));
dep=0;pd=0;
solve();
for(ll i=1;i<dep;i++)printf("%lld ",a[i]);
printf("%d
",a[dep]);
}
return 0;
}
埃及分数
迭代加深+剪枝
上下界剪枝:上界:max(上一个分母+1,b/a+1(通过乘除法运算可以推得))
下界:当前深度所得到的答案的最后一位
可行性剪枝:当前分数 - 剩下分数的最大和>0,则舍去
至于题目要求的不能选的数,可以用set来储存(插入和查询都是logn的复杂度)
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<set>
using namespace std;
#define ll long long
ll A,B,T,k,len;
ll ans[1005],cur[1005];
int pd=0;
set<ll> s;
ll read()
{
ll x=0;
char ch;ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x;
}
ll gcd(ll x,ll y){return y==0?x:gcd(y,x%y);}
bool better()
{
for(int i=len;i>=1;i--)
{
if(ans[i]==cur[i])continue;
return ans[i]==-1||cur[i]<ans[i];
}
return false;
}
ll minn;
void dfs(ll a,ll b,ll last,ll cnt)
{
if(cnt==len)
{
if(b%a!=0)return;
b/=a;
if(s.count(b))return;
cur[cnt]=b;
if(better())
{
pd=1;
minn=b;
for(int i=1;i<=len;i++)ans[i]=cur[i];
}
return ;
}
last++;
last=max(last,b/a+1);
for(int i=last;i<=minn;i++)
{
if(a*i-b*(len-cnt+1)>=0)return;
if(s.count(i))continue;
ll g=gcd(a*i-b,b*i);
cur[cnt]=i;
dfs((a*i-b)/g,b*i/g,i,cnt+1);
}
}
int main()
{
T=read();int tot=0;
while(T--)
{
tot++;
A=read();B=read();k=read();pd=0;minn=1e7;
s.clear();
printf("Case %d: %lld/%lld=",tot,A,B);
for(int i=1;i<=k;i++)
{
int x=read();s.insert(x);
}
for(int i=1;i<=1e3;i++)
{
len=i;
memset(ans,-1,sizeof(ans));
dfs(A,B,0,1);
if(pd==1)break;
}
for(int i=1;i<len;i++)printf("1/%lld+",ans[i]);
printf("1/%lld
",ans[len]);
}
return 0;
}
平板涂色
深搜+剪枝
这道题最多只能刷16次,感觉不需要迭代加深
最优性剪枝:1.当前涂色次数>ans,就返回
可行性剪枝:2.在搜索前需要先按照y坐标为第一关键字,x坐标为第二关键字从小到大进行排序
这道题用一个二维的f数组,第一维是i,第二维是j,来表示j是否是紧靠i上方的矩形,若f的值为1,则为是,f的值为0,则为否。我将点的坐标直接转化为单位小正方形的坐标进行处理。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
int n;
int f[20][20],ans=0;//f[i][j]表示第i个矩形上方是否是第j个矩形
struct node{
int x1,y1,x2,y2,col;
}a[25];
bool cmp(node x,node y)
{
if(x.y2!=y.y2)return x.y2<y.y2;
return x.x1<y.x1;
}
int had(int lx1,int rx1,int y1,int lx2,int rx2,int y2)
{
if(y1==y2+1)
{
if(min(rx1,rx2)>=max(lx1,lx2))return 1;
}
return 0;
}
int vis[50];
int check(int x)
{
for(int i=1;i<x;i++)
{
if(f[x][i]==1)
{
if(vis[i]==1)continue;
return 0;
}
}
return 1;
}
void dfs(int dr,int last,int nowco,int cnt)
{
if(dr>=ans)return ;
if(cnt==n)
{
ans=min(ans,dr);
return ;
}
for(int i=1;i<=n;i++)
{
if(vis[i])continue;
if(check(i)==1)
{
if(a[i].col==nowco)
{
vis[i]=1;
dfs(dr,i,a[i].col,cnt+1);
vis[i]=0;
}
else
{
vis[i]=1;
dfs(dr+1,i,a[i].col,cnt+1);
vis[i]=0;
}
}
}
}
int main()
{
scanf("%d",&n);ans=1e9;
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d%d",&a[i].y1,&a[i].x1,&a[i].y2,&a[i].x2,&a[i].col);
a[i].x1++;a[i].y1++;
}
sort(a+1,a+n+1,cmp);
for(int i=2;i<=n;i++)
for(int j=1;j<i;j++)
{
if(had(a[i].x1,a[i].x2,a[i].y1,a[j].x1,a[j].x2,a[j].y2)==1)f[i][j]=1;
}
dfs(0,0,0,0);
printf("%d",ans);
return 0;
}
靶形数独
深搜+剪枝
计算方格(x,y)所在小九宫格的公式:(x-1)/3*3+(y-1)/3+1
方格的分值直接用一个数组储存
剪枝:玩过数独的人应该知道,我们需要从未知数字少的一行开始填,所以先按照每一行已知数的数目从大到小排序,先处理已知数多的行
用三维数组vis中的vis[0][line][i]表示第line行的i是否被取过
vis[1][column][i]表示第column列的i是否被取过
vis[3][palace][i]表示第palace个小九宫格的i是否被取过
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int score[10][10]=
{
{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}
};
int mp[15][15];
int vis[4][15][15];
struct node{
int cnt,id;
}a[15];
int palace(int x,int y)
{
return (x-1)/3*3+(y-1)/3+1;
}
bool cmp(node x,node y){return x.cnt>y.cnt;}
int tot=1;
int ans=0;
int sum=-1;
void dfs(int line,int column)
{
if(tot>=10)
{
sum=max(sum,ans);
return ;
}
if(column==10)
{
tot++;
dfs(a[tot].id,1);
tot--;
return ;
}
if(!mp[line][column])
{
for(int i=1;i<=9;i++)
{
if(vis[0][line][i] || vis[1][column][i] || vis[2][palace(line,column)][i])continue;
vis[0][line][i]=1;
vis[1][column][i]=1;
vis[2][palace(line,column)][i]=1;
mp[line][column]=i;
ans+=i*score[line][column];
dfs(line,column+1);
vis[0][line][i]=0;
vis[1][column][i]=0;
vis[2][palace(line,column)][i]=0;
mp[line][column]=0;
ans-=i*score[line][column];
}
}
else dfs(line,column+1);
return;
}
int main()
{
for(int i=1;i<=9;i++)
{
a[i].id=i;
for(int j=1;j<=9;j++)
{
scanf("%d",&mp[i][j]);
if(mp[i][j]!=0)
{
vis[0][i][mp[i][j]]=1;
vis[1][j][mp[i][j]]=1;
vis[2][palace(i,j)][mp[i][j]]=1;
a[i].cnt++;ans+=mp[i][j]*score[i][j];
}
}
}
sort(a+1,a+10,cmp);
dfs(a[1].id,1);
printf("%d",sum);
return 0;
}