剪枝

剪枝

前言

图床在 \(Github\) 中 ,加载图片可能需要梯子

剪枝,字面的理解就是减去枝条删去不重要的节点,它实际上是一种来减少计算量的优化,它的应用十分广泛,如:决策树,神经网络,搜索算法,数据库的设计等,本次仅讨论在搜索算法中的剪枝

1 2

PS:在蓝桥杯等赛事中,剪枝可以很有效的拿到更多的分

回溯法伪码

int ans = 最坏情况;

void dfs(传入数值) {
  if (到达目的地){
     ans = 从当前解与已有解中选最优;
  	 return...
  }
  for (遍历所有可能性)
    if (可行) {
      进行操作;
      dfs(缩小规模);
      撤回操作;
    }
}

剪枝中最常用的三种方法

记忆化搜索、最优性剪枝、可行性剪枝。

记忆化搜索

定义

记忆化搜索是一种通过记录已经遍历过的状态的信息,从而避免对同一状态重复遍历的搜索实现方式。

因为记忆化搜索确保了每个状态只访问一次,它也是一种常见的动态规划实现方式。

记忆化数组

就拿斐波那契数列来看

\(fib[n]=fib[n-1]+fib[n-2]\),其中\(fib[1]=1,fib[2]=1\)

int fib(int n) {
	if (n==1||n==2) return 1;
	return fib(n - 1) + fib(n - 2);
}

如果我们要求 \(fib(5)\),以下是计算过程

3

可以发现 \(fib(3)\) 的值重复分解计算了,在更大的数据量时,重复计算的个数会更多

对此,可以通过用一个数组,记录下已经计算过的值,如果再次需要该状态的值,就不用继续分解计算了

import java.util.Arrays;
public class Main {
    static long[] memory = new long[50];
    static long fib(int n) {
        //如果记忆化数组里的值不等于初始值
        //说明此时该状态已经被计算过,直接返回记忆化数组内的值
        if (memory[n] != -1) return memory[n];
        //接下来在返回此状态的值之前,先赋值给记忆化数组
        if (n == 1 || n == 2) {
            memory[n] = 1;
        } else {
            memory[n] = fib(n - 1) + fib(n - 2);
        }
        return memory[n];
    }
    static void run() {
        Arrays.fill(memory, -1);//给记忆化数组赋初值,该值需要是计算过程中不可能出现的值
        System.out.println(fib(45));
    }
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        run();
        System.out.println("\n程序运行时间:" + (System.currentTimeMillis() - startTime) + "ms.");
    }
}

对于计算 \(fib(45)\)

45

不适用记忆化数组的程序运行结果约为:\(2178\) 毫秒

而使用记忆化数组的程序运行结果为:小于 \(1\) 毫秒

记忆化搜索伪码

对回溯法使用记忆化数组进行优化

int[] memory=new int[MAXSIZE];
int ans = 最坏情况;

void dfs(传入数值){
    if(memory[规模] != 无效数值) return...
    if (到达目的地){
    	ans = 从当前解与已有解中选最优;
  		return...
    }
    for (遍历所有可能性){
		if (可行) {
      	进行操作;
      	dfs(缩小规模);
      	撤回操作;
    	}
    }
}

main(){
   对memory赋初值,该值需要为无效数值
   dfs(...);
}

最优性剪枝

简述

如果当前解已经比已有解差时,则不再继续往下搜索

最优性剪枝伪码

int ans = 最坏情况;

void dfs(传入数值){
    if(当前解比ans更差) return...
    if (到达目的地){
    	ans = 从当前解与已有解中选最优;
  		return...
    }
    for (遍历所有可能性){
		if (可行) {
      	进行操作;
      	dfs(缩小规模);
      	撤回操作;
    	}
    }
}

可行性剪枝

简述

如果当前解已经不能用了,后面更不可能用,则不再继续往下搜索

可行性剪枝伪码

int ans = 最坏情况;

void dfs(传入数值){
    if(当前解已不可用) return...
    if (到达目的地){
    	ans = 从当前解与已有解中选最优;
  		return...
    }
    for (遍历所有可能性){
		if (可行) {
      	进行操作;
      	dfs(缩小规模);
      	撤回操作;
    	}
    }
}

例子

024. 字典排列\(dfs\) 就可以用可行性剪枝进行优化


public class Main {
    static int[] num = new int[10]; //存放十个数字0~9
    static boolean[] exist = new boolean[10]; //判断每个数字是否已经存放(默认初始化为false没被存放)
    static int n = 9;
    static int flag = 0; //计数器

    static void dfs(int step) {
        if (flag >= 1000000) return;//计数已经完成,后面的就不用走了,直接返回
		//终止条件
        if (step == n + 1) { //走到最后一个数字啦,搜索结束的标志
            flag++;
            if (flag == 1000000) {
                for (int i = 0; i <= n; i++)
                    System.out.print(num[i]);
            }
            return; //返回到上一级
        }
        for (int i = 0; i <= n; i++) {
            if (!exist[i]) {
                num[step] = i;
                exist[i] = true;
                dfs(step + 1);
                exist[i] = false;//回溯
            }
        }
    }
}

其中dfs方法内的if (flag >= 1000000) return;就是在进行可行性剪枝

总结

通过观察以上方法,均是跳过必不可能的搜索路径

对此,可以在做题时仔细思考边界、极端情况来进行剪枝优化,不必局限于各种方法

参考资料

优化 - OIWiki

posted @ 2023-01-10 14:31  Cattle_Horse  阅读(52)  评论(0编辑  收藏  举报