前言
众所周知,深搜(深度优先搜索)的时间复杂度在不加任何优化的情况下是非常慢的,时间复杂度大多是指数级的,难以满足对程序运行时间的限制要求,为使降低时间复杂度,对深度优先搜索可以进行一种优化的基本方法——剪枝。
搜索的进程可以看做是从树根出发,遍历一颗倒置树(搜索树)的过程,所谓剪枝,就是 通过某些判断,避免一些不必要的遍历过程 ,形象的说,就是剪去搜索树中的某些枝条。
显而易见,应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条舍弃哪些枝条保留,设计出好的剪枝判断方法,可以使得程序运行时间大大缩短,否则会适得其反。
正文
搜索的剪枝必需遵循三个原则:
- 正确性(不能把错误解排除,还搜什么呢?)
- 准确性(尽可能把不能通向正解的枝条剪去)
- 高效性(因为在每个枝条上都要进行一次判断,如果判断的复杂度很高,不但没有起到优化的效果,还让程序的复杂度更高了)
常见的几种剪枝方式:
1.优化搜索顺序
在一些搜索问题中,搜索树的各个层次,各个分支之间的顺序不是固定的。
不同的搜索顺序会产生不同的搜索树形态,其规模大小也相差甚远。
2.排除等效冗余
在搜索过程中,如果我们能够判定从搜索树的当前节点上沿着某几条不同分支到达的子树是等效的,那么只需要对其中的一条分支执行搜索。
3.可行性剪枝
在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。这好比我们在道路上行走时,远远看到前方是一个死胡同,就应该立即折返绕路,而不是走到路的尽头再返回。
某些题目条件的范围限制是一个区间,此时可行性剪枝也被称为上下界剪枝
4.最优化剪枝
在最优化问题的搜索过程中,如果当前花费的代价已经超过当前搜到的最优解,那么无论采取多么优秀的策略到达递归边界,都不可能更新答案。此时可以停止对当前分支的搜索,执行回溯。
5.记忆化
可以记录每个状态的搜索结果,在重复遍历一个状态时直接检索并返回
这好比我们对图进行深度优先遍历时标记一个节点是否已经被访问过。
这里解释一下为什么说不一样的搜索顺序,相应的搜索树的形态结构也不一样
当不加任何剪枝时,最直观的体现就是树从根节点开始分支不一样多。
因为最终的可行解是一样多的(先不考虑最优性排除的非最优解),而只有搜索树搜到了叶节点才会搜到可行解。
如果搜索深度也一样,那么两个搜索顺序对应的搜索树即为两个深度、叶节点树相同,但节点分支的递增顺序不同(可能是一个数较浅地方的分支较小,另一个反之…)。
进行剪枝后,情况就不一样了:剪枝时,若剪去的枝条规模越大,那么剪枝也就越成功。
那么,在一定的深度剪枝时,对更深的地方分支多的那颗搜索树剪枝的效果就要比对另一颗更深的地方分支少的搜索树剪枝的效果好。
因此在剪枝判断很难继续优化的前提下,搜索顺序也会很大程度地影响剪枝的效果。
接下来看 1 1 1 个例题:
题面:
题目描述
将整数 n n n 分成 k k k 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。
例如: n = 7 n=7 n=7, k = 3 k=3 k=3,下面三种分法被认为是相同的。
1
,
1
,
5
1,1,5
1,1,5;
1
,
5
,
1
1,5,1
1,5,1;
5
,
1
,
1
5,1,1
5,1,1.
问有多少种不同的分法。
输入格式
n , k n,k n,k ( 6 < n ≤ 200 6<n \le 200 6<n≤200, 2 ≤ k ≤ 6 2 \le k \le 6 2≤k≤6)
输出格式
1 1 1 个整数,即不同的分法。
样例 #1
样例输入 #1
7 3
样例输出 #1
4
提示
四种分法为:
1
,
1
,
5
1,1,5
1,1,5;
1
,
2
,
4
1,2,4
1,2,4;
1
,
3
,
3
1,3,3
1,3,3;
2
,
2
,
3
2,2,3
2,2,3.
【题目来源】
NOIP 2001 提高组第二题
题目解法:
为了确保出现过的方案不重复,可以规定在后面的分组中的数必须要大于前面分组中的数, n o w now now 代表上一个出现过的数,初值为 1 1 1,只要让下一个数从 n o w now now 开始循环,便可达成上述方案。
p a r t part part 代表还需多少次递归,即要分成几份,初值为 k k k,递归 k k k 次,即分为 k k k 份后便可退出循环。
n u m num num 代表到此次还剩多大的数可以分,初值定为 n n n。
同时循环最大只能进行到 t s \large\frac{t}{s} st,避免出现因前面的数过大而导致后面的数无法取的情况,其实这就是剪枝的方法。
下面就放代码了:
#include<bits/stdc++.h>
using namespace std;
int n,k;
int dfs(int num,int part,int now)//剩余待分的数,分的个数,要选出的数
{
if(part==1) return 1;
int cnt=0;
for (int i=now;i<=num/part;i++) cnt+=dfs(num-i,part-1,i);
return cnt;
}
int main()
{
scanf("%d%d",&n,&k);
printf("%d",dfs(n,k,1));
return 0;
}