青训营 动态规划之剪枝 特例题解 | 豆包MarsCode AI刷题
案例引入
在前几篇笔记中,我归纳了动态规划的入门一般解法,其中提到了剪枝操作。而在今日份刷题中,便遇到需要运用剪枝算法的动态规划,特此归纳。
题目描述:
题目初步分析:
在本题中,小R在选择甜品的同时也需要选择是否使用魔术棒,所以首先考虑背包问题来进行优化。不过本题解是通过剪枝来优化时间,因此使用最简单的递归动态规划。本题使用go语言进行解答。
题目详解:
-
先考虑使用魔法棒需要计算喜爱度的阶乘,因此需要创建一个函数递归计算阶乘值。
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 } -
考虑动态规划的初始化:
本题中需要考虑的前提条件为已经选择的甜品度之和,剩余魔法棒的数量,当前位置索引。同时因为只需要考虑是否能到达总和为s因此不需要设置返回值。
var dp func(sweet, mNum, i int) -
动态规划状态转移:
首先设置好递归终止条件,当当前甜品度到达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//进行剪枝操作
}
}
-
调用函数进行遍历:
从最小喜爱度进行遍历
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
}
运行结果如下:
运用豆包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)是一种优化技术,用于减少搜索空间,从而提高算法的效率。在你的代码中,剪枝可以通过提前终止不必要的递归调用来实现。以下是一些可以实施的剪枝策略:
- 提前终止递归:如果当前甜点的喜爱值加上已选甜点的喜爱值之和已经超过目标值
s,则可以提前终止递归。 - 避免重复计算:如果当前甜点的喜爱值已经计算过,则可以直接使用缓存的结果。
- 优化阶乘计算:在递归过程中,避免重复计算阶乘,可以使用缓存。
总结与思考
本题主体思路为使用动态规划进行解题,但是为了优化时间复杂度,再进行阶乘时运用全局变量进行剪枝操作,同时在进行递归调用时通过判断喜爱度是否溢出来进行剪枝操作。