🥳前端算法面试--逆向解析动态规划表格-每日一练

331 阅读5分钟

承接上文,今天分享的内容是逆向解析动态规划生成的表格

回顾上文

上篇文章用动态规划求解了 0-1 背包问题,求解的过程中借用了一个二维数组,行表示物品,列表示背包当前的重量。需要回顾的宝子可以回去看看:

上篇文章传送门:🥳前端算法面试--动态规划之0-1背包问题

并且最后的结果是二维数组的最后一行的最后一列所代表的背包重量,0-1 背包问题是解决了,即固定容量的背包最大能放多少东西。但是产生了一个新问题,只知道物品组合的总重量,却不知道放了哪些东西

借用上篇的数据举个例子,方便大家理解:
背包的最大容量是 9kg, 现在有物品若干个,它们的重量如下:

const weight = [2, 2, 6, 4, 3];

并且有动态规划生成的二维数组如下:
image.png
可以在最后一行看到,最后一列为 true 的是 9kg,说明背包最多可以 9kg 的物品,也就是说物品的组合可以将背包塞满。那么你知道这 9kg 是由哪些物品组成吗

可以是[6, 3],也可以是[2,4,3] 。物品种类比较少,我们可以很轻松的猜出所有的物品组合,但当物品种类达到上百个的时候,阁下如何应对。

实际场景

有一个很实际的场景--双 11 凑单。双 11 要来了, 你女朋友告诉你,双 11 需要满 500 才能使用优惠券,现在她想知道,购物车里哪些物品组合能够最逼近 500(这样才能羊毛最大化嘛)。

到你表现的机会了,你说:“这个嘛,不难,分分钟帮你解决”。

然后你吭哧啃哧用动态规划算出来最接近 500 的物品组合数值是 488。然后你女朋友问你,哪些物品才能组合成 488?

你是不是傻眼了,说:“这个嘛,我再帮你看看”。

看吧,想表现一下不是那么容易滴

为什么要逼近 500,不是说要满 500 才能用优惠券吗?因为凑成了 488 后,再随便搭个十几元的物品不就满 500 啦

思路分析

image.png

const weight = [2, 2, 6, 4, 3];

看表格,我们要看两个东西,第一个,表格 true 是不是一定放了当前物品;第二个,表格的 true 是不是一定没有放当前物品

先看第一个,如何判断一定放了当前物品?设行为 i,列为 j,只有当<i, j>为 true,<i - 1, j>为 false 时,当前物品就一定放了。可以表格中的最后一行,9kg那一列,记为<5, 9> ,<5, 9>为 true,并且<4, 9>不为 true, 这说明 9kg 里面一定含有 5 号物品。

再看第二个,如何判断一定没有放当前物品?只有当<i, j>为 true,<i - 1, j - weight[i]>为 false 时,当前物品就一定没放。为什么?如果是当前物品放了,导致<i, j>为 true,那么就一定有:

let j2 = j - weight[i];
<i - 1, j2> == true -> <i, j2 + weight[i]> == true  // 即在上一个状态的基础之上,增加放入当前物品的重量的状态

那万一<i - 1, j><i - 1, j - weight[i]>都为 true 呢?这好办,说明当前物品可放可不放😁

就是这个“可放可不放”增加了问题的复杂度,不过不用怕,用递归代码简单解决

代码实现

// store是动态规划产生的二维数组,i表示当前是第几个物品
// j表示当前背包重量,res表示当前推断出的物品
const getProject = (store, i, j, res) => {
	if (i == 0) {
		const temp = [...res];
		if (j > 0) {
			temp.unshift(weight[0]);
		}
		console.log("input projects is ", temp);
		return;
	}
  
	if (j - weight[i] >= 0 && store[i - 1][j - weight[i]]) {
		getProject(store, i - 1, j - weight[i], [weight[i], ...res]);
	}
  
	if (store[i - 1][j]) {
		getProject(store, i - 1, j, res);
	}
};

想到了“可放可不放”,那组合就有多种可能性。要遍历每一种可能性,就派出回溯算法出场啦。getProject函数中重点就是后面的两个 if 判断,判断当前物品是否可行?不放是否可行?如果可行,就会继续往下推断。不可行就会跳过

妥妥的回溯算法呀

第一个 if 判断j - weight[i] >= 0 && store[i - 1][j - weight[i]],如果不满足这个条件,那当前物品就一定没有放进去,那就不能递归调用判断里面的getProjecti-1 表示接下来判断下一个物品,j - weight[i] 减去当前物品的重量。[weight[i], ...res] 表示将当前物品放入推断结果中

第二个 if 判断store[i - 1][j],如果不满足这个条件,就表示当前物品就一定放进去了。那就不能递归调用判断里面的getProject,不能做没有放入的假设。

判断到最后一个物品时,i==0,这时候还需要看 j 是否等于 0。如果等于 0,那就说明背包已经空了,直接输出 res 就好。如果不等于 0,那 0 号物品就一定放进了背包,所以就有temp.unshift(weight[0])

测试代码:

/**
 * 动态规划0-1背包
 */

const weight = [2, 2, 6, 4, 3];

const packageWeight = 9;

const findWeight = (weight) => {
	const store = Array(weight.length)
		.fill(1)
		.map(() => Array(packageWeight + 1).fill(false));
	store[0][0] = true;
	store[0][weight[0]] = true;

	for (let i = 1; i < store.length; i++) {
		for (let j = 0; j <= packageWeight; j++) {
			if (store[i - 1][j]) store[i][j] = true;
		}

		for (let j = 0; j <= packageWeight - weight[i]; j++) {
			if (store[i - 1][j]) store[i][j + weight[i]] = true;
		}
	}
	// 获得可以放进去的最大重量
	let maxWeight = 0;
	for (let i = packageWeight; i >= 0; i--) {
		if (store[weight.length - 1][i]) {
			console.log("the max weight is ", i);
			maxWeight = i;
			break;
		}
	}

  //调用getProject
	getProject(store, weight.length - 1, maxWeight, []);
};

findWeight(weight);

因为getProject的调用需要二维数组 store,所以直接借用了上篇文章的代码。

输出结果是:
image.png

有两个 [2, 4, 3],因为有两个 2

再换个数据:

const weight = [2, 1, 6, 4, 3];

输出结果是:
image.png

总结

这篇文章分享了如何分析动态规划表格,并且使用回溯算法找出固定重量的物品组合。虽然分析的过程有些难,但是代码还是很简单的吧。学会了吗,赶快找女朋友露一手吧😄

可以评论区留言哦。我每天都会分享一篇算法小练习,喜欢就点赞+关注吧