目录
动态规划:
概念:问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,在构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上从最终子问题向原问题逐步求解
特点:
- 把原始问题划分成一系列子问题;
- 求解每个子问题仅一次,并将其结果保存在一个表中,以后用到时直接存取,不重复计算,节省计算时间
- 自底向上地计算。
- 整体问题最优解取决于子问题的最优解(状态转移方程)(将子问题称为状态,最终状态的求解归结为其他状态的求解)
最长子序列
从序列中选出若干个数组成一个新的序列,不改变他们的队伍的顺序,要求新的序列里xi≤xi+1≤xi+1.....举个例子{4,6,5,7,3},最长不下降子序列就是{4,6,7}/{4,5,7}
子问题表示:令dp[i]表示以第i个元素结尾的前i个元素构成的最长不下降子序列的长度
方程:dp[i] = max{dp[j] | 0<j<i , aj≥ai} + 1
package com.anran.example.test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
/**
*/
public class Main10 {
public static void main(String[] args){
int[] nums = getInput();
// 用于存储当前位置到之前最大子序列的长度信息
List<Map<String, Object>> dp = new ArrayList<>();
int maxNum = 0;
for (int i = 0; i < nums.length; i++) {
// 先设置当前节点之前满足降序的长度为0
dp.add(initDP());
// 获取当前节点大于之前节点并且子序列最长的信息
for (int j = 0; j <= i - 1; j++) {
if (nums[j] <= nums[i] && (int)dp.get(j).get("count") >= (int)dp.get(i).get("count")) {
// 添加对应子序列信息
List<String> info = (List<String>) dp.get(i).get("info");
if ((int)dp.get(j).get("count") > (int)dp.get(i).get("count")) {
info.clear();
}
info.addAll((List<String>) dp.get(j).get("info"));
dp.get(i).put("count", dp.get(j).get("count"));
}
}
// 添加当前节点长度和当前节点信息
dealAfter(dp.get(i), nums[i]);
maxNum = maxNum > (int)dp.get(i).get("count") ? maxNum : (int)dp.get(i).get("count");
}
// 找到早的的子序列并输出
for (Map<String, Object> map : dp){
if ((int)map.get("count") == maxNum) {
List<String> info = (List<String>) map.get("info");
for (String str : info) {
System.out.println(str);
}
}
}
}
private static void dealAfter(Map<String, Object> dpMap, int num) {
dpMap.put("count", (int)dpMap.get("count") + 1);
List<String> info = (List<String>) dpMap.get("info");
if (info.size() == 0) {
info.add(String.valueOf(num));
} else {
for (int i = 0; i < info.size(); i++) {
info.set(i, info.get(i) + "-" + num);
}
}
}
private static Map initDP(){
Map<String, Object> map = new HashMap<>();
map.put("count", 0);
map.put("info", new ArrayList<String>());
return map;
}
private static int[] getInput() {
Scanner sc=new Scanner(System.in);
String input = sc.next();
String[] strs = input.split(",");
int[] nums = new int[strs.length];
for (int i = 0; i < strs.length; i++) {
nums[i] = Integer.parseInt(strs[i]);
}
return nums;
}
}
运行截图:
0-1背包问题
问题描述:
现有n件物品和一个空间为c的背包。第i件物品的重量是重量为w[i],价值是v[i]。已知对于一件物品必须选择取(用1表示)或者不取(用0表示),且每件物品只能被取一次(这就是“0-1”的含义)。求放置哪些物品进背包,可使这些物品的空间总和不超过背包空间,且价值总和最大。
求解思路:
假设有5件物品,其空间分别是w={2,2,6,5,4},价值分别是v={6,3,5,4,6},背包空间为10。在数学问题中这是典型的线性规划问题,我们可以在线性约束范围内求解目标表达式。但是怎么用计算机语言实现呢?我们可以先这样考虑,当背包空间为1时,如何放置物品才能使背包中价值最大;同样当背包空间为2时,如何放置能使背包中价值最大,以此类推,直到背包空间为10。此时我们需要维护一张二维表m[i][j],其中横坐标i表示物品,纵坐标表示背包空间(1<=j<=10)。
m[i][j]表示当可以放入前i件物品且背包空间为j时的最大价值。当只能放入第一件物品即i=0时:若背包空间j<w[0],物品不能够被放入背包;若j>=w[0]时,物品可以放入背包,此时m[0][j]=v[0]。当可以放入前2件物品即i=1时,我们需要进行这样的处理:若j<w[1]时,说明第2件物品不能被放入背包内,此时背包的最大价值为背包中只放入第一件物品的最大价值,即m[1][j]=m[0][j];若j>=w[1]时,假设此时背包空间j=8,第二件物品可以被放入背包内,那么便会出现两种情况:
(1)将第二件物品放入背包,那么背包中物品的最大价值是多少呢?因为第二件物品空间为w[1]=2,在将第二件物品放入背包之前,背包的空间应为j-w[1]=8-2=6,此时背包的最大价值是m[0][6],因此若将第二件物品放入背包,其背包的最大价值m[1][j]=m[0][j-w[1]]+v[1];
(2)不将第二件物品放入背包,那么此时背包中物品的最大价值依然为只放入第一件物品时背包的最大价值,即m[1][j]=m[0][j];
我们选取(1)(2)中价值的较大者作为i=1,j=8时背包中的最大价值。
i=2,3,4时的分析同上,直到背包的空间为10,此时m[4][10]即为背包中物品的最大价值。
有了上面的分析,我们很容易写出下面的递归关系:
(1)i=0 当j<w[0]时,m[0][j]=0;当j>=w[0]时,m[0][j]=v[0]。
(2)i>0 当j<w[i],m[i][j]=m[i-1][j];当j>=w[i],m[i][j]=max{m[i-1][j-w[i]]+v[i],m[i-1][j]}。
得到了满足约束条件的背包中物品的最大价值后,需要知道是哪些物品被放入了背包。观察二维表m[i][j],我们注意到m[i][c]表示当背包空间为题目中要求的c时背包的最大价值,那么在得到m[i][c]之前,我们必然是比较了m[i-1][j-w[i]]+v[i]与m[i-1][j]的大小,从而决定是否将物品放入背包。所以我们可以利用回溯的方法,若m[i][j]=m[i-1][j],那么物品没有放入背包;否则物品一定被放入背包。因此我们可以从最后一件物品开始,一步一步回退到第一件物品,直到找到所有的物品放入背包的情况。本题中物品的装入情况如表中红色和蓝色部分所示,其中红色表示当前物品被装入背包,蓝色表示没有装入背包。
package com.anran.example.test;
import java.util.*;
/**
*/
public class Main11 {
public static void main(String[] args){
// 背包空间
int packageNum = 10;
// 商品所有空间
int[] w = {2, 2, 6, 5, 4};
// 商品价值
int[] v = {6, 3, 5 ,4, 6};
// 用于存放背包容量和存放物品时的最大价值
int[][] maxValues = new int[w.length][packageNum + 1];
// 单个善意一次加入背包时,计算背包的自大价值
for (int i = 0; i < w.length; i++) {
for (int j = 0; j <= packageNum; j++) {
// 判断商品空间和背包空间大小
if (w[i] > j) {
// 当前商品空间大于背包空间,商品不能够放入背包,如果是第一次放入价值是0,如果不是第一次放入,最大价值是上一次放入相同空间的值
maxValues[i][j] = i == 0 ? 0 : maxValues[i - 1][j];
} else {
// 当前商品可以放入背包
if (i == 0) {
// 如果是第一次放入,对应最大价值就是当前商品的价值
maxValues[i][j] = v[i];
} else {
// 当前商品放入:最大值等于当前商品的价值加上剩余空间的上一个商品的最大价值
int addMaxValue = v[i] + maxValues[i - 1][j - w[i]];
// 当前商品不放入:最大价值等于上一个商品相同容量的最大值
int noAddMaxVaule = maxValues[i - 1][j];
maxValues[i][j] = addMaxValue > noAddMaxVaule ? addMaxValue : noAddMaxVaule;
}
}
}
}
System.out.println("可以装的最大价值 : " + maxValues[w.length - 1][packageNum]);
// 通过回溯的方式计算那些商品加入到背包
int[] x = new int[w.length];
int nextPackageNum = packageNum;
for (int i = w.length - 1; i > 0; i--) {
// 如果当产品的值和上一个产品的值相等
if (maxValues[i][nextPackageNum] > maxValues[i - 1][nextPackageNum]) {
// 大于表示当前产品装入了背包
x[i] = 1;
nextPackageNum = nextPackageNum - w[i];
} else {
// 没有装入
x[i] = 0;
}
}
// 处理第一个产品
if (nextPackageNum >= w[0]) {
x[0] = 1;
}
// 输出装入的产品
System.out.print("装入产品的下标是:");
for (int i = 0; i < x.length; i++) {
if (x[i] == 1) {
System.out.print(i + " ");
}
}
}
}
运行截图:
传送门:https://www.nowcoder.com/discuss/3574
石头相撞求最后剩余石头最小值(0-1背包)
题目:两个石头相撞,生成一个新的石头,新石头的质量是两者差的绝对值,求一堆石头相撞剩余最后一个的最小质量?
输入描述: 第一行输入石头个数(<=100) 第二行输入石头质量,以空格分割,石头质量总和<=10000 输出描述: 最终的石头质量 输入例子1: 6 2 7 4 1 8 1 输出例子1: 1
变相理解:将一堆石头分成两堆,然后这两堆相撞,最终生成一个石头,要想最后一个石头质量最小,则这两堆的质量应该相近,也就是说从一堆石头怎样组合质量能够保证最接所有石头的中间质量
实现逻辑: |
1:计算石头的总重量以及中间重量的数值,将中间数值作为背包的中最大量容量,计算那种场景背包可以放入的重量最大 |
2:因为背包要放入最大的质量,因此当前背包的最大值应该从(上一次当前容量的最大值)和(当前放入质量和剩余容量在上一次背包的最大值)的最大值 |
3:在计算完所有物品放入场景之后,当前背包中最大容量对应的实际数量就是一个堆里面放入石头的重量 |
4:此时用总重量减去当前计算出堆中最合理存放的最大重量就是最后剩余石头最小质量 |
图表展示如下:
编号 | 重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
1 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | ||
2 | 7 | 7 | 7 | 9 | 9 | 9 | |||||||
3 | 4 | 4 | 4 | 6 | 7 | 7 | 9 | 9 | 11 | ||||
4 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | |
5 | 8 | 8 | 9 | 10 | 11 | ||||||||
6 | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
代码实现如下:
package com.anran.example.kuaishou0511;
import java.util.Scanner;
public class Main2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int count = sc.nextInt();
int sum = 0;
int[] infos = new int[count];
for (int i = 0; i < count; i++) {
infos[i] = sc.nextInt();
sum += infos[i];
}
// 获取背包的最大容量
int packageMaxNum = (sum / 2);
// 使用一维数组保存当前背包容量下最多可以放入多少石头,数组+1是因为存在都不放入的数据
// (数组下标表示容量,内容表示最大放入的质量)
int[] dp = new int[packageMaxNum + 1];
// 依次将石头放入背包
for (int info : infos) {
// 从背包最大容量到背包容量和相等,依次放入同一个石头
for (int i = packageMaxNum; i >= info;i--) {
// 上一次比对当前容量的最大值
// 当前石头的重量+背包剩余容量在上一次的最大值
// 两者取其重量大的作为当前背包重量的最优选
dp[i] = Math.max(dp[i], dp[i - info] + info);
}
}
// 所有石头都尝试放入之后,背包中最后一位存放的是背包容量最多的值
System.out.println(sum - dp[packageMaxNum] * 2);
}
}
运行截图: