DFS之剪枝与优化

DFS之剪枝与优化

  • 概括
  • 方法
    • 1、优化搜索顺序
    • 2、排除等效冗余
    • 3、可行性剪枝
    • 4、最优性剪枝
    • 5、记忆化
  • 例题
    • 1、小猫爬山
    • 2、木棒

概括

剪枝 就是减小搜索树规模、尽早排除搜索树中不必要的分支的一种手段。形象地看,就好像剪掉了搜索树的枝条,故称为剪枝。

方法

1、优化搜索顺序

在一些搜索问题中,搜索树的各个层次、各个分支之间的顺序不是固定的。不同的搜索顺序会产生不同的搜索树形态,其规模大小也相差甚远。

2、排除等效冗余

在搜索过程中,如果我们能够判定从搜索树的当前节点上沿着某几条不同分支到达的子树是等效的,那么只需要对其中的一条分支执行搜索。

3、可行性剪枝

在搜索过程中,及时对当前状态进行检查,如果发现分支已经无法到达递归边界,就执行回溯。(某些题目条件的范围限制是一个区间,此时可行性剪枝也被称为“上下界剪枝”。

4、最优性剪枝

在最优化问题的搜索过程中,如果当前花费的代价已经超过了当前搜到的最优解,那么无论采取多么优秀的策略到达递归边界,都不可能更新答案。此时可以停止对当前分支的搜索,执行回溯。

5、记忆化

可以记录每个状态的搜索结果,在重复遍历一个状态时直接检索并返回。(就好像我们对图进行深度优先遍历时,标记一个节点是否已经被访问过。

例题

1、小猫爬山

原题链接: 小猫爬山

题目描述

翰翰和达达饲养了 N 只小猫,这天,小猫们要去爬山。
经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。
翰翰和达达只好花钱让它们坐索道下山。
索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是 C1、C2……CN。
当然,每辆缆车上的小猫的重量之和不能超过 W。
每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?

输入格式

第 1 行:包含两个用空格隔开的整数,N 和 W。
第 2…N+1 行:每行一个整数,其中第 i+1 行的整数表示第 i 只小猫的重量 Ci。

输出格式

输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。

数据范围

1 ≤ \leq N ≤ \leq 18
1 ≤ \leq C i C_i Ci ≤ \leq 1 0 8 10^8 108

输入样例

5 1996
1
2
1994
12
29

输出样例

2

解题思路

这个题目可以将所有的缆车合并看成一个状态,状态之间的转移可以看成由将小猫放在那辆缆车决定
剪枝1 : 优化搜索顺序,将小猫按照重量递减排序(先放重量大的可以相对较快搜索出最优解)
剪枝2:最优性剪枝 如果当前搜到的小猫组数已经大于当前搜到的最优解的时候就立即回溯

#include
#include
#include
#include

using namespace std;

const int N = 25;

int sum[N], n, a[N], k, res, m;

void dfs(int u)
{
    if(k >= res)    return ; // 最优性剪枝
    if(u == n + 1){ // 递归完所有的小猫
        res = k;
        return ;
    }

    for(int i = 0; i < k; i ++) // 遍历当前的所有缆车
        if(sum[i] + a[u] <= m){ // 如果能够放下当前小猫
            sum[i] += a[u];
            dfs(u + 1);
            sum[i] -= a[u];
        }

    sum[k ++ ] = a[u]; // 新开一个缆车
    dfs(u + 1);
    sum[ -- k] = 0;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)    cin >> a[i];

    sort(a + 1, a + 1 + n); // 优化搜索顺序
    reverse(a + 1, a + 1 + n);

    res = n;
    dfs(1);
    cout << res << endl;

    return 0;
}

2、木棒

原题链接: 木棒

题目描述

乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。
然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
请你设计一个程序,帮助乔治计算木棒的可能最小长度。
每一节木棍的长度都用大于零的整数表示。

输入格式

输入包含多组数据,每组数据包括两行。
第一行是一个不超过 64 的整数,表示砍断之后共有多少节木棍。
第二行是截断以后,所得到的各节木棍的长度。
在最后一组数据之后,是一个零。

输出格式

为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

数据范围

数据保证每一节木棍的长度均不大于 50。

输入样例

9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0

输出样例

6
5

解题思路

从小到大枚举原始木棒的长度 len (也就是枚举答案)。当然, len 应该是所有木棍长度总和 sum 的约数 ,并且原始木棒的根数 cnt 就等于 sum / len

剪枝 1 : 优化搜索顺序 : 把木棍长度从大到小排序 (感觉像是减少高层次的树枝数量)

剪枝 2 : 排除等效冗余
(1) 可以限制先后加入一根原始木棒的木棍长度是递减的 (这是因为先拼上一根长度为 x 的木棍,再拼上一根长为 y 的木棍(x < y),与先拼上 y 再拼上 x 显然是等效的,只需要搜索其中一种)
(2)当一根木棍在某一个位置上拼接失败时,那么相同长度的木棍拼接在此处也是失败的
(3) 如果在当前原始木棒中 “尝试拼入的第一根木棍” 的递归分支返回失败,那么直接判定当前分支失败,立即回溯。这是因为在拼入这根木棍前,面对的原始木棒都是 ” 空 “的(还没有进行拼接),这些木棒是等效的。木棍拼在当前的木棒中失败,拼在其它木棒中一样会失败。
(4) 如果在当前原始木棒中拼入一根木棍后,木棒恰好被拼接完整,并且 “接下来拼接剩余原始木棒” 的递归分支返回失败,那么直接判定当前分支失败,立即回溯,该剪枝可以用贪心来解释,”再用1跟木棍恰好拼完当前原始木棒” 必然比 “再用若干跟木棍拼完当前原始木棒” 更好。

#include
#include
#include
#include

using namespace std;

const int N = 110;

int len, sum, w[N], n;
bool st[N];

bool dfs(int cnt, int s, int u)
{
    if(cnt * len == sum)  return true;
    if(s == len)    return dfs(cnt + 1, 0, 0); // 拼完当前木棒,新开一根木棒

    for(int i = u; i < n; i ++){ // 剪枝 2(1) 
        if(st[i] || s + w[i] > len)   continue;// 已经用过当前木棍或者拼上当前木棍超过当前的木棒长度

        st[i] = 1;
        if(dfs(cnt, s + w[i], i))   return true;
        st[i] = 0;

        if(!s || s + w[i] == len)   return false; // 剪枝 2(3) 和 2(4) 

        int j = i;
        while(j < n && w[j] == w[j + 1]){ // 剪枝 2(2)
            j ++;
        }
        i = j;
    }
    return false;
}

int main()
{
    while(cin >> n, n){
        sum = 0;
        for(int i = 0; i < n; i ++){ cin >> w[i];sum += w[i];}
        // cout << sum << endl;
        memset(st, 0, sizeof st);
        sort(w, w + n);// 剪枝 1
        reverse(w, w + n);

        len = 1;
        while(1){
            if(sum % len == 0 && dfs(0, 0, 0)){
                cout << len << endl;
                break;
            }else   len ++;
        }
    }
    return 0;
}

本文档基于 AcWing 制作

你可能感兴趣的:(深度优先,剪枝,算法)