如果将搜索的状态看作结点,状态之间的转移看作边,搜索的过程就构建出了一棵“搜搜树”。其实,这也是判定搜索题目的一个方法。
所谓“剪枝”,就是在搜索树上去掉一些枝杈不进行搜索,从而减少小时间消耗。搜索树的剪枝如下图所示。
常用的思考策略
1、优化搜索顺序
大多数情况下优先搜索分枝较少的结点
2、排除等效冗余
3、可行性剪枝
4、最优性剪枝
但需要指出的是,搜索题目的性质极其灵活,在应用时一定要具体问题具体分析,设计合适的剪枝策略,不必拘泥于所学。
例1:数的划分
【问题描述】
将整数n分成k份,且每份不能为空,问有多少种不同的分法。当n=7,k=3,下面三种分法被认为是相同的:1,1,5; 1,5,1; 5,1,1
【输入格式】
输入文件只有一行为两个整数n和k (6【输出格式】
输出文件仅有一行为一个整数,即不同的分法数。
【样例输入】
7 3
【样例输出】
4
【样例解释】
四种分法为:1,1,5;1,2,4;1,3,3;2,2,3;
/*#include
using namespace std;
int k,n,ans;
void dfs(int last,int sum,int p)//从last位置开始,划分的和sum;划分的段数p
{
int i;
if(p==k)
{
if(sum==n)
ans++;
return;
}
for(i=last;sum+i*(k-p)<=n;i++)
dfs(i,sum+i,p+1);
}
int main()
{
cin>>n>>k;
dfs(1,0,0);
cout<
#include
#include
#include
#include
using namespace std;
int n,k,ans;
int f[250][10];//已经分j份,且该份的和是i;这里n是资源,待分配
/*1.先初始化,任何当k=1的情况下仅有一种分法。
2.然后如分析3组建dp方程,从i中分j份的方案数 (f[i,j]) , 为 i-1 中分 j-1 份 ( f[i-1,j-1] ,即分出_1_)
和 i-j 分 j 份 (f[i-j][j],因为分出1后剩下 i-j 个可分的1,j 次机会) 的方案数之和。
3.同时要注意一个小小的细节(未经实测,可能不会导致WA)。
*/
int main()
{
cin>>n>>k;
f[0][0]=1;//初始化
for(int t=1;t<=n;t++)//分出来的整块的大小
for(int i=t;i<=n;i++)//整个一块的大小
for(int j=1;j<=k;j++)//分的段数
f[i][j]+=f[i-t][j-1];
cout<<f[n][k];//输出将整数n分成k份的分法
return 0;
}
例2:生日蛋糕(上下届剪枝,优化搜索顺序,可行性剪枝,最优性剪枝)
【问题描述】
一个体积为Nπ的M层生日蛋糕,每层都是一个圆柱体。 从下往上数第i(1<=i<=M)层蛋糕是半径为Ri,高度为Hi的圆柱。当iRi+1且Hi>Hi+1。希望蛋糕外表面(最下一层的下底面除外)的面积Q最小。令Q=Sπ,请编程对给出的N和M,找出、适当的Ri和Hi的值,使S最小。(除Q外,以上所有数据皆为正整数) 【输入格式】
第一行为N(N≤10000),表示待制作的蛋糕的体积为Nπ;
第二行为M(M≤20),表示蛋糕的层数为M。
【输出格式】
输出仅一行,是一个整数S(若无解则S=0)。
附:圆柱公式:体积V=πR2H;侧面积A’=2πRH;底面积A=πR2
【算法分析】
搜索框架:从下往上搜索,枚举搜索面对的状态有:正在搜索蛋糕第dep层,当前外表面面积s,当前体积v,第dep + 1 层的高度和半径。不妨用数组h和r分别记录每层的高度和半径。整个蛋糕的“上表面”面积之和等于最底层的圆面积,可以在第M层直接累加到 s 中。这样在第 M-1层往上的搜索中,只需要计算侧面积。
剪枝:
上下界剪枝
在第dep层时,只在下面的范围内枚举半径和高度即可。
首先,枚举R∈[dep,min(⌊√N−v⌋,r[dep+1]−1)]
其次,枚举H∈[dep,min(⌊N−v/R2⌋,ℎ[dep+1]−1)]
上面两个区间右边界中的式子可以通过圆柱体积公式:πR2H=π(N-v)得到.
优化搜索顺序
在上面确定的范围中,使用倒序枚举。
可行性剪枝
可以预处理出从上往下前i(1≤i≤M)层的最小体积和侧面积。显然,当第1~i层的半径分别取1,2,3,…,i,高度也分别取1,2,3,…,i时,有最小体积与侧面积。
如果当前体积v加上1~dep-1层的最小体积大于N,可以剪枝。
最优性剪枝1
如果当前表面积s加上1~dep-1层的最小侧面积大于已经搜到的答案,剪枝。
最优性剪枝2
加入以上五个剪枝后,搜索算法就可以快速求出该问题的最优解了。
#include
#include
#include
using namespace std;
const int INF = 0x7fffffff;
int n, m;/*n,m是给定的体积以及层数*/
int minv[30], mins[30], ans = INF;
int h[30], r[30], s = 0, v = 0;
void dfs(int dep) {
if (!dep) {/*还在搜索的过程中*/
if (v == n) ans = min(ans, s);/*体积已经全部用完了,此时的不合法的状态下的面积一定比合法的面积小*/
return;
}
/*sqrt(n - v):数学公式可以把半径进一步的缩小;
而r[dep + 1] - 1是因为当前dep层一定是比dep+1层小的,最少小一,这样子的话下面一层就会有更多的选择,h同r;
r[dep] >= dep是因为如果r小于dep的话,那么之后的层数一定不够m层,所以该结果不合法*/
for (r[dep] = min((int)sqrt(n - v), r[dep + 1] - 1); r[dep] >= dep; r[dep]--)/*确定半径*/
for (h[dep] = min((int)((double)(n-v) / r[dep] / r[dep]), h[dep+1] - 1); h[dep] >= dep; h[dep]--)
{
if (v + minv[dep-1] > n) continue;
if (s + mins[dep-1] > ans) continue;
if (s + (double)2 * (n - v) / r[dep] > ans) continue;/*鬼畜剪枝*/
/*用剩余体积估计之后的比实际要小的需要用到的表面积+s判断当前状态是否可行*/
if (dep == m) s += r[dep] * r[dep];/*所有层的裸露的部分的面积*/
s += 2 * r[dep] * h[dep];/*s=2rπh*/
v += r[dep] * r[dep] * h[dep];/*v=r*r*πh*/
dfs(dep - 1);
if (dep == m) s -= r[dep] * r[dep];/*回溯的现场还原*/
s -= 2 * r[dep] * h[dep];
v -= r[dep] * r[dep] * h[dep];
}
}
int main() {
cin >> n >> m;
minv[0] = mins[0] = 0;/*初始化处理从上到下的每一层之前的最小的体积以及侧面积*/
for (int i = 1; i <= m; i++) {
minv[i] = minv[i-1] + i * i * i;/*有要求每一层的半径和高度都应该是逐个递增的*/
mins[i] = mins[i-1] + i * i;
}
h[m+1] = r[m+1] = INF;/*因为一共只需要m层,在搜索的for循环中有体现*/
dfs(m);/*搜索的顺序是从最大的一层到最小的一层*/
cout << ans << endl;
return 0;
}
例3:Addition Chains(优化搜索顺序)
【问题描述】
已知一个数列a0,a1…am(其中a0 = 1,am = n,a0 < a1 < a2 < … < am-1 < am)。对于每个k(1<=k<=m),需要满足ak=ai+aj(0 <= i, j <= k-1,这里i与j可以相等)。
现给定n的值,要求m的最小值(并不要求输出),及这个数列的值(可能存在多个数列,只输出任一个满足条件的就可以了)。
【输入格式】
多组数据,每行给定一个正整数n。输入以0结束。
【输出格式】
对于每组数据,输出满足条件的长度最小的数列。
【算法分析】
由于ak=ai+aj(0≤i,j
可见,选择合适的搜索顺序对于提高程序的效率是非常重要的,运用良好的搜索顺序来对搜索题目进行优化是性价比很高的技巧。
#include
#include
using namespace std;
int a[105],bo[105],ans[105];
int n,m;
bool check(int v,int x)
{
for (int i=0; i<=x; ++i)
if (v-a[i]>=0 && bo[v-a[i]]) return 1;
return 0;
}
void dfs(int x)
{
if (x>=m) return;
if (a[x]==n) {
if (x<m) m=x,memcpy(ans,a,sizeof(ans));
return;
}
for (int j=n; j>a[x]; --j)
if (check(j,x)){
a[x+1]=j;
bo[j]=1;
dfs(x+1);
bo[j]=0;
}
return;
}
int main()
{
bo[1]=1;
a[0]=1;
while (scanf("%d",&n),n){
for (int i=2; i<=n; ++i) bo[i]=0;
m=1e5;
dfs(0);
for (int i=0; i<=m; ++i) printf("%d ",ans[i]);
printf("\n");
}
}
标程2
#include
#include
#include
#include
#include
#define INF 0X3F3F3F3F
#define N 2019
using namespace std;
int n,minn,ans[N],a[N];
//n是序列的最大值
//minn是拆数字最小层数
//ans存答案
//a是一个过程中起记录作用的数组
void dfs(int x) //递归第x层
{
if(x-1 > minn) return ; //超过最小层数,剪枝
if(a[x-1] > n) return; //上一层的数字大于最大值n,剪枝
if(a[x-1] == n) //上一层的数字=n,有希望构成一个完整解答
{
if((x-1) >= minn) return; //层数超了
minn = x-1; //记下暂定答案
for(int i=1;i < x;i++) ans[i] = a[i];
}
else
{
for(int j = x-1;j >= 1;j--)
//ak=ai+aj,倒着枚举,看看能否继续扩展下一层
{
if(a[x-1]+a[j] <= n)
{
a[x] = a[x-1]+a[j]; //可以继续扩展
dfs(x+1);
a[x] = 0; //回溯
}
}
}
}
int main()
{
while(scanf("%d",&n) != EOF) //输入非空
{
if(n == 0) break;
else
{
a[1] = 1; //初始化第一层为1
minn = INF; //拆数字的最小层数
dfs(2); //从第二层开始递归
for(int i = 1;i <= minn;i++) printf("%d ",ans[i]); //确定层数,输出答案
printf("\n");
}
}
return 0;
}
例4:小木棍(多种剪枝策略)
【问题描述】
同样长的小木棍被随意砍成几段,已知每段的长都不超过50。现在,要把小木棍拼接成原来的样子,但却不知道开始时有多少根木棍和它们的长度。给出每段小木棍的长度,求原始木棍的最小可能长度。
【文件输入】
第一行为一个单独的整数 N 表示砍过以后的小木棍的总数,其中N<=60;
第二行为 N 个用空个隔开的正整数,表示 N根小木棍的长度。
【文件输出】
输出文件仅一行,表示要求的原始木棍的最小可能长度。
【算法分析】
要得到最小的原始木棍长度,可以按照分段数的长度,依次枚举所有的可能长度len;每次枚举len时,深搜判断是否能用截断后的木棍拼合出整数个len,能用的话,找出最小的len即可。对于1S的时间限制,用不加任何剪枝的深搜时,时间效率为指数级,效率非常低。对于此题,可从可行性和最优性上加以剪枝。
从最优性上来分析,可以剪枝以下2点:
①设所有木棍的长度和是sum,那么原长度(也就是需要输出的长度)一定能够被sum整除,即一定是拼出整数根。
②木棍原来的长度一定大于等于所有木棍中最长的那根。
综合上述两点,可知原木棍的长度len在最长木棍的长度到sum之间,且sum能被len整除。所以,在搜索原木棍的长度时,可以设定为从截断后所有木棍中最长的长度开始,每次增加长度后,必须能整除sum。
从可行性上来分析,可以再剪枝以下7点:
③短木棍比长木棍更灵活组合,所以可以对木棍按长度从大到小排序。
④在截断后的排好序的木棍中,当用木棍i拼合原始木棍时,可以从第i+1后的木棍开始搜。
⑤用当前最长长度的木棍开始搜,如果拼不出当前设定的原木棍长度len,则直接返回,换一个原始木棍长度len。
⑥相同长度的木棍不要搜索多次。用当前长度的木棍搜下去得不出结果时,可以提前返回。
⑦判断搜到的几根木棍组成的长度是否大于原始长度len,如果大于,可以提前返回。
⑧判断当前剩下的木棍根数是否能够拼的木棍根数,如果不够,肯定拼合不成功,直接返回。
⑨找到结果后,在能返回的地方马上返回到上一层递归处。
#include
using namespace std;
const int maxn = 66;
int n,sum,nm=maxn,mx,d;
int len[maxn],a[maxn],pre[maxn];
void dfs(int u,int k,int p)
{
//当前长棍还有u没有拼,还有k根长棍没有拼,当前长棍的最短短棍长度是p
if(u==0)
{
dfs(d,k-1,a[n]);
return;
}
if(k==0)
{
cout<<d;
exit(0);
}
p=(p<u)?p:u;
while(p&&len[p]==0)--p;
while(p)
{
if(len[p])//枚举当前棍长
{
--len[p];
dfs(u-p,k,p);
++len[p];
if((u==p)||(u==d))return;
p=pre[p];
}
else p=pre[p];
}
}
int main()
{
cin>>n;
for(int i = 1,x;i<=n;++i)
{
cin>>x;
sum+=x;
++len[a[i]=x];
}
sort(a+1,a+1+n);
for(int i = 1;i<=n;i++)//预处理长度i的上一个长度
{
if(a[i]!=a[i-1])pre[a[i]]=a[i-1];
}
for(d=a[n];(d<<1)<=sum;++d)
{
if((sum%d)==0)
dfs(d,sum/d,a[n]);
}
cout<<sum;
return 0;
}
练习题目:(www.51nod.com,自行注册,免费)
1、分成2组
2、邻之差为k
3、挑选数字
4、小明爱正方形