文章目录
- 上文链接
- 一、剪枝与优化
-
- [1. 排除等效冗余](#1. 排除等效冗余)
- [2. 可行性剪枝](#2. 可行性剪枝)
- [3. 最优性剪枝](#3. 最优性剪枝)
- [4. 优化搜索顺序](#4. 优化搜索顺序)
- [5. 记忆化搜索](#5. 记忆化搜索)
- [二、OJ 练习](#二、OJ 练习)
-
- [1. 数的划分](#1. 数的划分)
-
- [(1) 解题思路](#(1) 解题思路)
- [(2) 代码实现](#(2) 代码实现)
- [2. 小猫爬山](#2. 小猫爬山)
-
- [(1) 解题思路](#(1) 解题思路)
- [(2) 代码实现](#(2) 代码实现)
上文链接
一、剪枝与优化
剪枝,形象地看,就是剪掉搜索树的分支,从而减小搜索树的规模,排除掉搜索树中没有必要的分支,优化时间复杂度。 在深度优先遍历中,有几种常见的剪枝方法:
1. 排除等效冗余
如果在搜索过程中,通过某一个节点往下的若干分支中,存在最终结果等效的分支,那么就只需要搜 索其中一条分支。
2. 可行性剪枝
如果在搜索过程中,发现有一条分支是无论如何都拿不到最终解,此时就可以放弃这个分支,转而搜索其它的分支。
3. 最优性剪枝
在最优化的问题中,如果在搜索过程中,发现某一个分支已经超过当前已经搜索过的最优解,那么这个分支往后的搜索,必定不会拿到最优解。此时应该停止搜索,转而搜索其它情况。
4. 优化搜索顺序
在有些搜索问题中,搜索顺序是不影响最终结果的,此时搜索顺序的不同会影响搜索树的规模。 因此,应当先选择一个搜索分支规模较小的搜索顺序,快速拿到一个最优解之后,用最优性剪枝剪掉别的分支。
5. 记忆化搜索
记录每一个状态的搜索结果,当下一次搜索到这个状态时,直接找到之前记录过的搜索结果。 记忆化搜索,有时也叫动态规划。
二、OJ 练习
1. 数的划分
【题目链接】
P1025 [NOIP 2001 提高组\] 数的划分 - 洛谷](https://www.luogu.com.cn/problem/P1025)
【题目描述】
将整数 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 个整数,即不同的分法。
【示例一】
输入
7 3
输出
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 提高组第二题
(1) 解题思路
我们一共要把数分成 k k k 份,那么每一份我们选择从 1 1 1 一直枚举到 n n n,当枚举了 k k k 份时,就是递归终止条件。如果此时的和正好为 n n n,那么就统计一次次数。
但是显然我们不能每一个数都从 1 1 1 枚举到 n n n ,这样必然会超时,因此我们需要剪枝。
**剪枝一:**由于顺序不同的序列我们认为是同一个序列,所以如果我们枚举的前一个数是 m m m,那么下一个数我们就应该从 m m m 开始枚举。剪掉 [ 1 , m − 1 ] [1 , m-1] [1,m−1] 的所有分支。
**剪枝二:**比如说 n = 7 n = 7 n=7, k = 3 k = 3 k=3 时,当我们枚举到 [ 1 , 6 , _ _ ] [1,\ 6,\ \\\ ] [1, 6, __ ] 的时候,还有必要再枚举第三个数吗?显然是没必要的,因为前两个数的和已经等于 7 7 7 了,再往后枚举的结果永远不可能等于 7 7 7。因此我们此时就要把这种情况剪掉。
(2) 代码实现
cpp
#include<iostream>
using namespace std;
int n, k;
int path, cnt;
void dfs(int pos, int begin)
{
if(pos == k)
{
if(path == n) cnt++;
return;
}
// 剪枝二: 可行性剪枝
// 如果把剪枝放在这里的话那么递归就会进入到不合法的情况中再判断,就会超时
// if(path + begin * (k - pos) > n) return;
for(int i = begin; i <= n; i++)
{
// 剪枝二: 可行性剪枝
if(path + i * (k - pos) > n) return;
path += i;
dfs(pos + 1, i);
path -= i;
}
}
int main()
{
cin >> n >> k;
dfs(0, 1);
cout << cnt;
return 0;
}
2. 小猫爬山
【题目链接】
【题目描述】
Freda 和 rainbow 饲养了 N ( N ≤ 18 ) N(N\le 18) N(N≤18) 只小猫,这天,小猫们要去爬山。经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了
Freda 和 rainbow 只好花钱让它们坐索道下山。索道上的缆车最大承重量为 W W W,而 N N N 只小猫的重量分别是 C 1 , C 2 , ... C N C_1,C_2,\dots C_N C1,C2,...CN。当然,每辆缆车上的小猫的重量之和不能超过 W ( 1 ≤ C i , W ≤ 1 0 8 ) W(1\le C_i,W \le 10^8) W(1≤Ci,W≤108)。每租用一辆缆车,Freda 和 rainbow 就要付 1 1 1 美元,所以他们想知道,最少需要付多少美元才能把这 N N N 只小猫都运送下山?
【输入格式】
第一行包含两个用空格隔开的整数, N N N 和 W W W。
接下来 N N N 行每行一个整数,其中第 i + 1 i+1 i+1 行的整数表示第 i i i 只小猫的重量 C i C_i Ci。
【输出格式】
输出一个整数,最少需要多少美元,也就是最少需要多少辆缆车。
【示例一】
输入
5 1996 1 2 1994 12 29
输出
2
(1) 解题思路
**搜索策略:**依次处理每一只猫,对于每一只猫,我们都有两种处理方式:
要么把这只猫放在已经租好的缆车上;
要么重新租一个缆车,把这只猫放上去。
剪枝一:
在搜索过程中,我们用全局变量记录已经搜索出来的最小缆车数量。如果当前搜索过程中,已经用的缆车数量大于全局记录的最小缆车数量,那么这个分支一定不会得到最优解,剪掉。
剪枝二:
-
优化枚举顺序一:从大到小安排每一只猫
-
重量较大的猫能够快速把缆车填满,较快得到一个最小值;
-
通过这个最小值,能够提前把分支较大的情况提前剪掉。
-
-
优化枚举策略二:先考虑把小猫放在已有的缆车上,然后考虑重新租一辆车
- 因为如果反着来,我们会先把缆车较大的情况枚举出来,这样就起不到剪枝的效果了。
(2) 代码实现
cpp
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 20;
int c[N];
int s[N]; // 每一辆车目前的总重
int n, w;
int cnt; // 当前用了多少辆缆车
int ret = N; // 全局最优解
void dfs(int pos)
{
// 最优性剪枝
if(cnt >= ret) return;
if(pos > n)
{
ret = cnt;
return;
}
// 优化搜索顺序
// 先安排在已有的车,再重开一辆车
for(int i = 1; i <= cnt; i++)
{
if(s[i] + c[pos] > w) continue;
s[i] += c[pos];
dfs(pos + 1);
s[i] -= c[pos];
}
// 重开一辆车
cnt++;
s[cnt] = c[pos];
dfs(pos + 1);
s[cnt] = 0;
cnt--;
}
int main()
{
cin >> n >> w;
for(int i = 1; i <= n; i++) cin >> c[i];
sort(c + 1, c + 1 + n, greater<int>());
dfs(1);
cout << ret;
return 0;
}