动态规划随笔
整数拆分
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
该将给定的正整数拆分成尽可能多的 3,推导过程如下:
01背包(二维数组)
import java.util.*;
public class Main
{
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
int M = sc.nextInt();//物品种类
int N = sc.nextInt();//背包容量
int[] weights = new int[M + 1];//物品重量
int[] values = new int[M + 1];//物品价值
for(int i = 1;i <= M;i++)
weights[i] = sc.nextInt();
for(int i = 1;i <= M;i++)
values[i] = sc.nextInt();
//dp[i][j]含义:当选取0-i物品放入容量为j的背包中能获取的最大利益
int[][] dp = new int[M + 1][N + 1];
//初始化:当背包容量为0时或物品编号为0时总价值为0,当只能放入编号为1的物品时,从它能放进去往后最大价都为该物品价值
for(int i = 0;i <= M;i++)
dp[i][0] = 0;
for(int i = 0;i <= N;i++)
dp[0][i] = 0;
for(int i = 1;i <= N;i++)
{
if(i < weights[0])
continue;
dp[1][i] = values[0];
}
//递推公式:dp[i][j] = max{dp[i - 1][j],dp[i - 1][j - weights[i]] + values[i]}
//本题dp[i][j]的值是根据左上角和正上方的值推导出来,所以先遍历物品(行),背包(列)都可以
for(int i = 1;i <= M;i++)
{
for(int j = 1;j <= N;j++)
{
if(j >= weights[i])
dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - weights[i]] + values[i]);
else dp[i][j] = dp[i - 1][j];
}
}
System.out.println(dp[M][N]);
}
}
01背包(滚动数组版详细推导过程)
import java.util.*;
public class Main
{
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
int M = sc.nextInt();//物品种类
int N = sc.nextInt();//背包容量
int[] weights = new int[M + 1];//物品重量
int[] values = new int[M + 1];//物品价值
for(int i = 1;i <= M;i++)
weights[i] = sc.nextInt();
for(int i = 1;i <= M;i++)
values[i] = sc.nextInt();
//dp[j]含义:容量为j的背包放入物品所能获得的最大收益
int[] dp = new int[N + 1];
//初始化:递推公式 dp[j] = max{dp[j],dp[j - weights[i]]+values[i]}
//物品两种状态,放:dp[i][j] = dp[i-1][j - weights[i]+values[i]
//不放:dp[i][j] = dp[i-1][j]
//两者都只和dp[i-1],即上一行的数据有关,那么原来的表格可以压缩成两行
//比如dp[0] = 第一行 dp[1] = 算出的第二行 之后dp[0] = dp[1] dp[1] = 第三行结果
//还可以进一步压缩,从右往左遍历,dp[j]之前的元素都是还没被更新的上一行结果,那么直接用之前的元素就行
//这样只需要一行
//如果从左向右遍历会导致重复,假如背包容量为10,新的物品容量为4,价值为9
//此时如果不放,最大价值仍是dp[10],而如果放的话就要为它腾出空间,取上一轮的结果比较
//要与dp[10 - 4] + 9 = dp[6] + 9 比较,注意这里的dp[6]应该是上一轮还没放入此物品的最大利益
//如果正序遍历,那么这里的dp[6]被更新过了,变成了本轮考虑放入这个物品后的结果,这里假定放入了这个物品
//当这个dp[6](此时这个dp[6]已经包含了一个9)+9 > dp[10]时,这个物品就要放入,那么dp[10] = dp[6](包含一个9) + 9
//则dp[10]中包含了2个9,即这个价值为9的物品被放入了两次,价格算错了
for(int i = 1;i <= M;i++)
{
for(int j = N;j >= weights[i];j--)
{
dp[j] = Math.max(dp[j],dp[j - weights[i]] + values[i]);
}
}
System.out.println(dp[N]);
}
}
完全背包状态转移方程推导
对于每个物品,两种选择 放/不放
放(假设放k个,k ≥ 1):dp[i][j] = dp[i - 1][j - k * weights[i]] + k * values[i]
不放:dp[i][j] = dp[i - 1][j]
完全背包状态转移方程:
dp[i][j] = max {dp[i - 1][j],max{ dp[i - 1][j - k * weights[i]] + k * values[i]} }
①
(1 ≤ k ≤ j / weights[i]) (取值范围由j - k * weights[i] ≥ 0求得)
上述遍历复杂度为O(N3),太高,尝试优化为0(N2),放入时至少放1个,不妨先放一个进去,等价为:
dp[i][j] = max {dp[i - 1][j],max{dp[i - 1][j - weights[i] - k * weights[i]] + k * values[i] + values[i]} }
②
此时k取值范围变为0≤ k ≤ j / weights[i] - 1
观察式①,发现当k = 0时,dp[i - 1][j - k * weights[i]] + k * values[i] = dp[i - 1][j]
因此式①等价为dp[i][j] = max{dp[i - 1][j - k * weights[i]] + k * values[i]}
③
0 ≤ k ≤ j / weight[i] 可以看到,该式子消掉了一层遍历!
①与②③等价,因此②与③等价。
令j = j - weights[i],dp[i][j - weights[i]] = max{dp[i - 1][j - weights[i] - k * weights[i]] + k * values[i]}
④
0 ≤ k ≤ (j - weights[i])/weights[i] = j / weights[i] - 1
观察②④,有相同部分max{dp[i - 1][j - weights[i] - k * weights[i]] + k * values[i]}
②中max{dp[i - 1][j - weights[i] - k * weights[i]] + k * values[i] + values[i]} = max{dp[i - 1][j - weights[i] - k * weights[i] + k * values[i]} + values[i]
替换得: dp[i][j] = max{dp[i - 1][j],dp[i][j - weights[i]]}
,消除了k且复杂度变回O(N2)!
因此得到二维完全背包公式:
dp[i][j] = max{dp[i - 1][j],dp[i][j - weights[i]]},j ≥ weights[i]
和0 - 1背包仅仅相差一个 -1
否则 dp[i][j] = dp[i - 1][j]
之所以一维背包不逆序:
需要用到的是正上方以及左侧的信息,如果逆序的话,那么左边所用的信息就没有经过更新了
可以先遍历背包或先遍历物品的原因:完全背包递推公式每一项使用的是左边以及正上方的信息,先遍历背包等价于一列一列赋值,左边和正上方信息足够;而0-1背包需要的是左上方和正上方信息(上一行信息),必须保证行遍历。