动态规划:
转自https://blog.youkuaiyun.com/na_beginning/article/details/62884939
基本思想:
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中, 可能会有很多可行解。没一个解都对应于一个值,我们希望找到具有最优值的解。胎动规划算法与分治法类似,其基本思想也是将待求解问题分解为若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适用于动态规划算法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算很多次。如果我们能保存已解决子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解决的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划算法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
与分治法最大的差别是:适用于动态规划求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)
应用场景:
适用于动态规划的问题必须满足最优化原理、无后效性和重叠性。
(1) 最优化原理(最优子结构性质):一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
(2) 无后效性:将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称无后效性。
(3) 子问题的重叠性:动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这就是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其他算法。
二维背包
//N为物品数目
//W为背包所承载的最大重量
//V为背包所承载的最大容量
//weights[]为每个物品的重量
//volume[]为每个物品的体积
//values[]为每个物品的价值
public int twoDimensionKnapack(int N, int W, int V, int[] weights, int[] volume, int[] values ){
int[][] dp = new int[W+1][V+1];
for(int i = 1; i <= N; i++){
int w = weights[i-1], vi = volume[i-1], v = values[i-1];
for(int j = W; j >= w; j--){
for(int k = V; k >= vi; k--){
dp[j][k] = Math.max(dp[j][k], dp[j-w][k-vi]+v);
}
}
}
return dp[W][V];
}
多重背包
需要再考虑一下每件物品的数量
import java.util.*;
public class Main {
/**
* 多重背包问题
*
* @param V 背包容量
* @param N 物品种类
* @param weight 物品重量
* @param value 物品价值
* @return
*/
public static String ZeroOnePack(int V, int N, int[] weight, int[] value,int[] num) {
//初始化动态规划数组,程序的目的是列出整个状态表,即背包容量为0~V,物品个数为0~N的所有最大价值dp[i][j];
int[][] dp = new int[N + 1][V + 1];
//为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算
//
for (int i = 1; i < N + 1; i++) {//前i件物品
for (int j = 1; j < V + 1; j++) {//背包目前容量
//如果第i件物品的重量大于背包容量j,则不装入背包
if (weight[i - 1] > j) {
dp[i][j] = dp[i - 1][j];
}
//由于weight和value数组下标都是从0开始,故注意第i个物品的重量为weight[i-1],价值为value[i-1]
else {
for (int k = 0; k * weight[i - 1] <= j && k <= num[i - 1]; k++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - k * weight[i - 1]] + k * value[i - 1]);
}
}
}
}
//则容量为V的背包能够装入物品的最大值为
int maxValue = dp[N][V];
System.out.println(maxValue);
//逆推找出装入背包的所有商品的编号
int j = V;
String numStr = "";
for (int i = N; i > 0; i--) {
//若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的
if (dp[i][j] > dp[i - 1][j]) {
numStr = i + " " + numStr;
j = j - weight[i - 1];
}
if (j == 0)
break;
}
return numStr;
}
public static void main(String[] args) {
int v=5;
int n=4;
int[] w=new int[]{1,2,3,4};
int[] value=new int[]{2,4,4,5};
int[] num=new int[]{3,1,3,2};
System.out.println(ZeroOnePack(v,n,w,value,num));
}
}
完全背包
import java.util.*;
public class Main {
/**
* 第二类背包:完全背包
* 思路分析:
* 01背包问题是在前一个子问题(i-1种物品)的基础上来解决当前问题(i种物品),
* 向i-1种物品时的背包添加第i种物品;而完全背包问题是在解决当前问题(i种物品)
* 向i种物品时的背包添加第i种物品。
* 推公式计算时,f[i][y] = max{f[i-1][y], (f[i][y-weight[i]]+value[i])},
* 注意这里当考虑放入一个物品 i 时应当考虑还可能继续放入 i,
* 因此这里是f[i][y-weight[i]]+value[i], 而不是f[i-1][y-weight[i]]+value[i]。
* @param V
* @param N
* @param weight
* @param value
* @return
*/
public static String ZeroOnePack(int V, int N, int[] weight, int[] value) {
//初始化动态规划数组,程序的目的是列出整个状态表,即背包容量为0~V,物品个数为0~N的所有最大价值dp[i][j];
int[][] dp = new int[N + 1][V + 1];
//为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算
//
for (int i = 1; i < N + 1; i++) {//前i件物品
for (int j = 1; j < V + 1; j++) {//背包目前容量
//如果第i件物品的重量大于背包容量j,则不装入背包
if (weight[i - 1] > j)
dp[i][j] = dp[i - 1][j];
//由于weight和value数组下标都是从0开始,故注意第i个物品的重量为weight[i-1],价值为value[i-1]
else{
int value1=dp[i - 1][j];//第i件物品的重量小于背包容量j,但不装入背包
int value2=dp[i][j-weight[i-1]]+value[i-1];//和0-1背包不同,重点在这里!!!第i件物品的重量小于背包容量j,装入背包
//放入第i件后,一个分支是求背包容量为j-weight[i-1]时,前i件物品的最大价值
dp[i][j] = Math.max(value1, value2);
}
}
}
//则容量为V的背包能够装入物品的最大值为
int maxValue = dp[N][V];
//逆推找出装入背包的所有商品的编号
int j = V;
String numStr = "";
int num=0;
for (int i = N; i > 0; i--) {
//若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的
if (dp[i][j] > dp[i - 1][j]) {
num++;
numStr = i + " " + numStr;
j = j - weight[i - 1];
}
if (j == 0)
break;
}
System.out.println(num);//最大价值下装入物品的数量
return numStr;
}
public static void main(String[] args) {
int v=10;
int n=4;
int[] w=new int[]{2,3,4,7};
int[] value=new int[]{1,3,5,9};
System.out.println(ZeroOnePack(v,n,w,value));
}
}
0-1背包:
0-1背包:每类物品最多只能装一次
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中
每种物品都只有一件。
令dp[i][j]来表示前i件物品装入容量为j的背包所能得到的最大总价值。
对于dp[i][j]来说,i指的是前i件物品,j指的是还剩下多少背包空间。于是对于dp[i][j]来说,有公式
接下来对表格每个位置进行说明,那第一行来说,表示的是前0件物品,背包容量为0~20时可以放进去的最大价值,物品都没有,不管容量多大可以容纳的价值都是0;
同样的,比如第二行,表示前1件物品(此物品重量为2,价值为3)在背包剩余容量为0时,放不下去,此时最大价值是0;背包剩余容量是1时,放不下去,此时最大价值是0;背包剩余容量为2时,能放进去,此时最大价值是3···
那么到了第3行(前2件物品),在背包容量是5的时候,为了达到最大价值,可以把第1件和第2件商品都放进去,5 >= 2 + 3,可以放下,最大价值为3 + 4 = 7。
整个表格的填写由程序来完成,计算公式为上面所列出的dp[i][j]计算公式。最后只需要取出我们所需要的dp[5][20]即可得到前5件物品,背包容量为20时的最大价值。
import java.util.*;
public class Main {
/**
* 0-1背包问题
*
* @param V 背包容量
* @param N 物品种类
* @param weight 物品重量
* @param value 物品价值
* @return
*/
public static String ZeroOnePack(int V, int N, int[] weight, int[] value) {
//初始化动态规划数组,程序的目的是列出整个状态表,即背包容量为0~V,物品个数为0~N的所有最大价值dp[i][j];
int[][] dp = new int[N + 1][V + 1];
//为了便于理解,将dp[i][0]和dp[0][j]均置为0,从1开始计算
//
for (int i = 1; i < N + 1; i++) {//前i件物品
for (int j = 1; j < V + 1; j++) {//背包目前容量
//如果第i件物品的重量大于背包容量j,则不装入背包
if (weight[i - 1] > j)
dp[i][j] = dp[i - 1][j];
//由于weight和value数组下标都是从0开始,故注意第i个物品的重量为weight[i-1],价值为value[i-1]
else{
int value1=dp[i - 1][j];//第i件物品的重量小于背包容量j,但不装入背包
int value2=dp[i-1][j-weight[i-1]]+value[i-1];//第i件物品的重量小于背包容量j,装入背包
dp[i][j] = Math.max(value1, value2);
}
}
}
//则容量为V的背包能够装入物品的最大值为
int maxValue = dp[N][V];
//逆推找出装入背包的所有商品的编号
int j = V;
String numStr = "";
int num=0;
for (int i = N; i > 0; i--) {
//若果dp[i][j]>dp[i-1][j],这说明第i件物品是放入背包的
if (dp[i][j] > dp[i - 1][j]) {
num++;
numStr = i + " " + numStr;
j = j - weight[i - 1];
}
if (j == 0)
break; //break只能跳出1层循环。
}
System.out.println(num);//最大价值下装入物品的数量
return numStr;
}
public static void main(String[] args) {
int v=20;
int n=5;
int[] w=new int[]{2,3,4,5,9};
int[] value=new int[]{3,4,5,8,10};
System.out.println(ZeroOnePack(v,n,w,value));
}
}