青训营 动态规划之剪枝 特例题解 | 豆包MarsCode AI刷题

40 阅读5分钟

青训营 动态规划之剪枝 特例题解 | 豆包MarsCode AI刷题

案例引入

在前几篇笔记中,我归纳了动态规划的入门一般解法,其中提到了剪枝操作。而在今日份刷题中,便遇到需要运用剪枝算法的动态规划,特此归纳。

题目描述:

image-20241117151459581.png

题目初步分析:

在本题中,小R在选择甜品的同时也需要选择是否使用魔术棒,所以首先考虑背包问题来进行优化。不过本题解是通过剪枝来优化时间,因此使用最简单的递归动态规划。本题使用go语言进行解答。

题目详解:

  1. 先考虑使用魔法棒需要计算喜爱度的阶乘,因此需要创建一个函数递归计算阶乘值。

    func Factorial(n int) int {
    
    	if n ==1 {
            return 1
    	}
    	return n*Factorial(n-1)
    }
    

    又因为每次都从头递归计算太消耗时间,创建一个全局化变量来存储已经递归计算完成的甜品度,若该值大于零则直接返回,否则才计算。

    var factorialCache = make(map[int]int)//设置全局变量
    
    func Factorial(n int) int {
    	if val, ok := factorialCache[n]; ok {
    		return val
    	}//如果已经计算过则返回
    	if n > 0 {
    		result := n * Factorial(n-1)
    		factorialCache[n] = result
    		return result
    	}//否则进行计算并记录
    	factorialCache[n] = 1
    	return 1
    }
    
  2. 考虑动态规划的初始化:

    本题中需要考虑的前提条件为已经选择的甜品度之和,剩余魔法棒的数量,当前位置索引。同时因为只需要考虑是否能到达总和为s因此不需要设置返回值。

    var dp func(sweet, mNum, i int)
    
  3. 动态规划状态转移:

    首先设置好递归终止条件,当当前甜品度到达S时便终止循环同时方案数加一。

    if sweet == s {
    			*p++
    			return
    		}
    

    为方便进行遍历,在递归开始前对喜爱度数组进行排序。初始从最小喜爱度的甜品开始递归,同时计算当前喜爱度与目标值的差值,如果差值小于当前索引的喜爱度说明无法再选择甜品,则进行剪枝操作停止递归。如果差值大于当前索引的喜爱度的阶乘则考虑是否剩余魔法棒,否则不使用魔法棒直接选取。

for j := i + 1; j < n; j++ {
			temp := s - sweet//计算当前喜爱度与目标值差值
			if mNum > 0 && temp >= Factorial(like[j]) {
				dp(sweet+Factorial(like[j]), mNum-1, j)//使用魔法棒
				dp(sweet+like[j], mNum, j)//不使用魔法棒
			} else if temp >= like[j] {
				dp(sweet+like[j], mNum, j)//不适用魔法棒直接选择
			} else if temp < like[j] {
				break//进行剪枝操作 
			}
		}
  1. 调用函数进行遍历:

    从最小喜爱度进行遍历

    dp(0, m, -1)
    

完整代码如下:

package main

import (
	"fmt"
	"sort"
)

var factorialCache = make(map[int]int)

func Factorial(n int) int {
	if val, ok := factorialCache[n]; ok {
		return val
	}
	if n > 0 {
		result := n * Factorial(n-1)
		factorialCache[n] = result
		return result
	}
	factorialCache[n] = 1
	return 1
}

func solution(n, m, s int, like []int) int {
	// Please write your code here
	cnt := 0
	p := &cnt
	sort.Ints(like)
	var dp func(sweet, mNum, i int)
	dp = func(sweet, mNum, i int) {
		if sweet == s {
			*p++
			return
		}

		for j := i + 1; j < n; j++ {
			temp := s - sweet
			if mNum > 0 && temp >= Factorial(like[j]) {
				dp(sweet+Factorial(like[j]), mNum-1, j)
				dp(sweet+like[j], mNum, j)
			} else if temp >= like[j] {
				dp(sweet+like[j], mNum, j)
			} else if temp < like[j] {
				break
			}
		}
	}

	dp(0, m, -1)

	return cnt
}

运行结果如下:

image-20241117154649593.png

运用豆包AI进行帮助与归纳:

通过AI进行语言转化:

由于判题暂不支持go语言因此使用ai将代码转化为java语言。

代码如下:

import java.util.*;

public class Main {
    private static Map<Integer, Integer> factorialCache = new HashMap<>();

    public static int factorial(int n) {
        if (factorialCache.containsKey(n)) {
            return factorialCache.get(n);
        }
        if (n > 0) {
            int result = n * factorial(n - 1);
            factorialCache.put(n, result);
            return result;
        }
        factorialCache.put(n, 1);
        return 1;
    }

    public static int solution(int n, int m, int s, int[] like) {
        // 使用数组来存储计数器的值,因为数组是引用类型,可以在内部类中修改
        final int[] cnt = { 0 };
        Arrays.sort(like);

        class DP {
            void dp(int sweet, int mNum, int i) {
                if (sweet == s) {
                    cnt[0]++;
                    return;
                }

                for (int j = i + 1; j < n; j++) {
                    int temp = s - sweet;
                    if (mNum > 0 && temp >= factorial(like[j])) {
                        dp(sweet + factorial(like[j]), mNum - 1, j);
                        dp(sweet + like[j], mNum, j);
                    } else if (temp >= like[j]) {
                        dp(sweet + like[j], mNum, j);
                    } else if (temp < like[j]) {
                        break;
                    }
                }
            }
        }

        DP dpInstance = new DP();
        for (int i = 0; i < n; i++) {
            if (m>0&&s >= factorial(like[i])) {
                dpInstance.dp(factorial(like[i]), m - 1, i);
                dpInstance.dp(like[i], m, i);
            } else if (s >= like[i]) {
                dpInstance.dp(like[i], m, i);
            } else if (s < like[i]) {
                break;
            }
        }

        return cnt[0];
    }

剪枝算法归纳

剪枝(Pruning)是一种优化技术,用于减少搜索空间,从而提高算法的效率。在你的代码中,剪枝可以通过提前终止不必要的递归调用来实现。以下是一些可以实施的剪枝策略:

  1. 提前终止递归:如果当前甜点的喜爱值加上已选甜点的喜爱值之和已经超过目标值s,则可以提前终止递归。
  2. 避免重复计算:如果当前甜点的喜爱值已经计算过,则可以直接使用缓存的结果。
  3. 优化阶乘计算:在递归过程中,避免重复计算阶乘,可以使用缓存。

总结与思考

本题主体思路为使用动态规划进行解题,但是为了优化时间复杂度,再进行阶乘时运用全局变量进行剪枝操作,同时在进行递归调用时通过判断喜爱度是否溢出来进行剪枝操作。