你们好,最近因为刚刚入职要作的事情不少,疏于更新一段时间,从今天开始,我会慢慢恢复更新,与你们分享一些算法方面的经验。java
很久没说动态规划了,通过上次的分析,你们应该已经对动态规划有了个大致的认识,今天咱们一块儿来看一个经典的问题--0/1背包问题。可能有些同窗以为背包问题很简单,无非写个判断条件,递归执行就能解决。可是想要拿到最优解,咱们仍然有许多须要细细思量的东西。c++
咱们先来看一下题目的定义:给定N种水果的重量跟收益,咱们须要把它们放进一个可容重量为C的背包里,使得包里的水果在总重量不超过C的同时拥有最高的收益,假设水果数量有限,一种只能选一个
。算法
题目很短,很容易理解,咱们再具体化一点,看一个例子。假设我如今要去卖水果,如今的状况以下: 水果: { 苹果, 橙子, 香蕉, 西瓜 }
重量: { 2, 3, 1, 4 }
收益: { 4, 5, 3, 7 }
背包可容重量: 5
数组
先来试试不一样的组合的结果: 苹果 + 橙子 (总重量5) => 9
苹果 + 香蕉 (总重量 3) => 7
橙子 + 香蕉 (总重量 4) => 8
香蕉 + 西瓜 (总重量 5) => 10
缓存
咱们能够看到西瓜跟香蕉是绝配,在有限的重量限制下给咱们最大的收益。咱们来尝试用算法把它描述出来。如我前面所说,最简单的就是暴力递归,每次遇到一种水果,咱们只有两个选择,要么在背包还放得下它的时候把它放进去,要么就直接不放它,这样就能帮咱们列举出全部的情形,而后咱们只取收益最大的那种。优化
private int knapsackRecursive(int[] profits, int[] weights, int capacity, int currentIndex) {
if (capacity <= 0 || currentIndex >= profits.length)
return 0;
// 在当前元素能够被放进背包的状况下递归的处理剩余元素
int profit1 = 0;
if( weights[currentIndex] <= capacity )
profit1 = profits[currentIndex] + knapsackRecursive(profits, weights,
capacity - weights[currentIndex], currentIndex + 1);
// 跳过当前元素处理剩余元素
int profit2 = knapsackRecursive(profits, weights, capacity, currentIndex + 1);
return Math.max(profit1, profit2);
}
复制代码
这样的解法时间复杂度得在O(2^n),数据量稍微一大就会出现明显的耗时。spa
private int knapsackRecursive(Integer[][] dp, int[] profits, int[] weights, int capacity, int currentIndex) {
if (capacity <= 0 || currentIndex >= profits.length)
return 0;
// 若是已经算得结果,直接返回
if (dp[currentIndex][capacity] != null)
return dp[currentIndex][capacity];
// 在当前元素能够被放进背包的状况下递归的处理剩余元素
int profit1 = 0;
if (weights[currentIndex] <= capacity)
profit1 = profits[currentIndex] + knapsackRecursive(dp, profits, weights,
capacity - weights[currentIndex], currentIndex + 1);
// 跳过当前元素处理剩余元素
int profit2 = knapsackRecursive(dp, profits, weights, capacity, currentIndex + 1);
dp[currentIndex][capacity] = Math.max(profit1, profit2);
return dp[currentIndex][capacity];
}
复制代码
好啦,最终全部的结果都存储在这个二维数组里面,咱们能够肯定咱们不会有超过NC个子问题,N是元素的数量,C是背包可容重量,也就是说,到这儿咱们时间空间复杂度都只有O(NC)了。设计
事情到这里尚未结束,咱们来尝试用自下而上的方法来考虑这道题,来看看能不能得到更优解。本质上,咱们想在上面的递归过程当中,对于每个索引,每个剩余的可容重量,咱们都想在这一步得到能够的最大收益。处理第3个元素时,咱们想得到能拿到的最大收益。处理第4个元素时,咱们仍是想得到能够拿到的最大收益。(毕竟获取最大利润是每一个人的目标嘛)dp[i][c]
就表明从最开始i=0时计算到当前i的最大收益。那每次咱们也只有两种选择:3d
dp[i-1][c]
。profit[i] + dp[i-1][c-weight[i]]
。最终咱们想要得到的最大收益就是这俩中的最大值。dp[i][c] = max (dp[i-1][c], profit[i] + dp[i-1][c-weight[i]])
。code
public int solveKnapsack(int[] profits, int[] weights, int capacity) {
if (capacity <= 0 || profits.length == 0 || weights.length != profits.length)
return 0;
int n = profits.length;
int[][] dp = new int[n][capacity + 1];
// 0空间就0收益
for(int i=0; i < n; i++)
dp[i][0] = 0;
// 在处理第一个元素时,只要它重量能够被背包容下,那确定放入比不放入收益高
for(int c=0; c <= capacity; c++) {
if(weights[0] <= c)
dp[0][c] = profits[0];
}
// 循环处理全部元素全部重量
for(int i=1; i < n; i++) {
for(int c=1; c <= capacity; c++) {
int profit1= 0, profit2 = 0;
// 包含当前元素
if(weights[i] <= c)
profit1 = profits[i] + dp[i-1][c-weights[i]];
// 不包含当前元素
profit2 = dp[i-1][c];
// 取最大值
dp[i][c] = Math.max(profit1, profit2);
}
}
// dp的最后一个元素就是最大值
return dp[n-1][capacity];
}
复制代码
这样时间空间复杂度也都在O(N*C)。
那怎么找到选择的元素呢?其实很简单,咱们以前说过,不选中当前元素的话,当前的最大收益就是处理前一个元素时的最大收益,换言之,只要在dp里的上下俩元素相同的,那那个索引所表明的元素确定没被选中,dp里第一个不一样的总收益所在的位置就是选中的元素所在的位置。
private void printSelectedElements(int dp[][], int[] weights, int[] profits, int capacity) {
System.out.print("Selected weights:");
int totalProfit = dp[weights.length - 1][capacity];
for (int i = weights.length - 1; i > 0; i--) {
if (totalProfit != dp[i - 1][capacity]) {
System.out.print(" " + weights[i]);
capacity -= weights[i];
totalProfit -= profits[i];
}
}
if (totalProfit != 0)
System.out.print(" " + weights[0]);
System.out.println("");
}
复制代码
这个算法够简单吧?但我以为仍是不能就这么结束了,咱们大费周章地换了一种思路来解题,取得一样的复杂度就结束了吗,咱们再来观察下咱们这个算法。咱们发现咱们在处理当前元素时,咱们须要的仅仅是在前一个元素时各个索引最大的收益,再往前的数据咱们根本不关心,那这就是一个优化的点,咱们能够把dp的size大幅缩减。
static int solveKnapsack(int[] profits, int[] weights, int capacity) {
if (capacity <= 0 || profits.length == 0 || weights.length != profits.length)
return 0;
int n = profits.length;
// 咱们只须要前面一次的结果来得到最优解,所以咱们能够把数组缩减成两行
// 咱们用 `i%2` 代替`i` 跟 `(i-1)%2` 代替`i-1`
int[][] dp = new int[2][capacity+1];
// 在处理第一个元素时,只要它重量能够被背包容下,那确定放入比不放入收益高
for(int c=0; c <= capacity; c++) {
if(weights[0] <= c)
dp[0][c] = dp[1][c] = profits[0];
}
// 循环处理全部元素全部重量
for(int i=1; i < n; i++) {
for(int c=0; c <= capacity; c++) {
int profit1= 0, profit2 = 0;
// 包含当前元素
if(weights[i] <= c)
profit1 = profits[i] + dp[(i-1)%2][c-weights[i]];
// 不包含当前元素
profit2 = dp[(i-1)%2][c];
// 取最大值
dp[i%2][c] = Math.max(profit1, profit2);
}
}
return dp[(n-1)%2][capacity];
}
复制代码
这时候空间复杂度就只剩下O(N)了,嘿嘿,这是比较让人满意的结果了。不过要是同窗们再丧心病狂一点,再变态一点,再观察一下咱们的算法,能够发现其实咱们只须要前面一次结果中的两个值dp[c]
跟 dp[c-weight[i]]
。那咱们可不能够把结果都放在一个一维数组里面,来看看:
dp[c-weight[i]]
的时候,若是weight[i]>0,那么dp[c-weight[i]]
是有可能已经被覆盖掉了。这并非什么难题,只要咱们改变处理顺序就行了:c:capacity-->0
。从后往前处理,就能保证咱们在修改dp里面任何值得时候,这个被修改的值都用不到了,你们想一想,是否是这么个道理。 思路想明白了,那手写代码就很简单了:
static int solveKnapsack(int[] profits, int[] weights, int capacity) {
if (capacity == 0 || profits.length == 0 || weights.length != profits.length) {
return 0;
}
int n = profits.length;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= capacity; i++) {
if (weights[0] <= i) {
dp[i] = profits[0];
}
}
for (int j = 1; j < n; j++) {
for (int c = capacity; c >= 0; c--) {
int profit1 = 0;
if (weights[j] <= c) {
profit1 = profits[j] + dp[c - weights[j]];
}
int profit2 = dp[c];
dp[c] = Math.max(profit1, profit2);
}
}
return dp[capacity];
}
复制代码
如今咱们的算法能够说是最优咯!最后你们再来好好地总结下,其实动态规划就是想办法减小没必要要的内存消耗,跟复用以前问题的结果来解决如今的问题以用最少的时间解决问题。思路就是这么简单,可是关于内存优化,这就得靠经验的积累了,你们多加练习作手熟了就好啦。