动态规划作为经典的算法,在使用上现在十分广泛,机器人走路问题、背包问题、八皇后问题,可以说是用的地方十分广泛。
动态规划从简单的地方说起,最初的时候可以理解为循环问题,就是常说的递归,我们仔细的想一想,如果一个问题能够使用递归得到解,那么只要在递归过程中有重复的计算,都可以换做动态规划来解决。
首先了解动态规划是什么?
就是将大问题,分解为小问题,然后一步步的解决它,很类似于分治算法,但是用很不相同。
再次了解递归的要点?
递归必须右函数出口,否则会死递归下去
递归过程必须是要向着出口的条件逼近,否则也会是死递归过程
可以将递归理解为栈结构,当主递归进去后,就是方法入栈,一直递归调用,知道最后一个递归方法全部进栈,开始出栈回溯。
例子:
假设有一个直线,可以使用一个数组表示,机器人每次可以向左或者向右移动一个位置,在规定的移动次数下,有多少种方法可以到达指定位置?
输入:N S A K
N: 这条路一共有多少位置可以移动
S: 机器人初始位置
A: 需要到达的目的地
K: 可以移动多少步
输出:ANS
ANS: 共有多少种移动方法
我们假设机器人当前所在的位置为 C,还可以移动M次,那么接下来的一步,机器人要么去到左边,要么去到右边
1 2 3 4 5 6
C A
剩余步数 K: 4
那么机器人接下来,要么向左移动到1位置,剩余步数为3,目的地不变;要么向右移动到3位置,剩余步数为3,目的地不变。那么就出现了递归调用。
因为每次机器人移动时候所要考虑的就是两件事情,
1.还有没有可以动的步数
2.往左还是往右
所以递归的方法体就可以出来了
/**
* 机器人可以到达的方式
* @param N 有多少可以走的位置
* @param start 开始的位置
* @param aim 到达的目的
* @param K 剩余步数
* @return 所有的组合
*/
public static int ways(int N, int start, int aim, int K){
}
参数选定了,就考虑方法体的具体细节
注意1: 如果机器人走到了边界,也就是1,或者6的位置,他就只能往反方向走
注意2: 方法的出口就是,当机器人可移动的步数为0之后,所在的位置,是否是目的地
那么递归方法体就是:
/**
* 机器人可以到达的方式
* @param N 有多少可以走的位置
* @param start 开始的位置
* @param aim 到达的目的
* @param K 剩余步数
* @return 所有的组合
*/
public static int process(int N, int start, int aim, int K){
// 递归终止条件
if (K == 0){
return start == aim ? 1: 0;
}
// 如果在起点,只能往右走
if (start == 0){
return process(N, start+1, aim, K-1);
}
// 如果在终点,只能往左走
if (start == N){
return process(N, start-1, aim, K-1);
}
// 否则,往右往左都试一试
return process(N, start-1, aim, K-1) + process(N, start+1, aim, K-1);
}
调用测试:
@Test
public void test1(){
// 假如路径为下
// 1 2 3 4 5 6
// 初始在位置2,目的地达到6, 共可移动6步
System.out.println(ways(4, 2, 4, 4));
}
结果:
3
所谓的动态规划,可以理解为 把每次计算的过程都记录下来,再次使用到这次计算时候,可以直接在记录数据中拿到,而不需要再次计算了。
上述的递归解法中,因为在递归调用的过程中,会存在重复计算的过程,所以使用一个缓存表,记录每次的结果。
上述例子中,一直在变动的参数有两个
所以可以实例一个二维数组,记录 在 cur位置时候,剩余 step步数时候,返回的结果是什么样的?
因为机器人是从1开始的,为了迎合,所以二维数组多实例一行一列,初始值设为-1,如果是-1,就认为该递归方法体未被调用过
int[][] dp = new int[N+1][K+1];
for (int i = 0; i < N+1; i++) {
for (int j = 0; j < K+1; j++) {
dp[i][j] = -1;
}
}
那么在每次调用的时候,方法中都夹带着这个缓存表,如果调用的方法已经被计算过了,就从缓存表里拿结果,如果没有,就计算。
那么就需要在开始加一个判断,是否该次计算已经执行过,也就是缓存表相对位置为-1时候,就是未被执行
/**
*
* 机器人可以到达的方式
* @param N 有多少可以走的位置
* @param start 开始的位置
* @param aim 到达的目的
* @param K 剩余步数
* @return 所有的组合
*/
public static int process2(int N, int start, int aim, int K, int[][] dp){
// 作为缓存,把每次[start][K]都放进去
if (dp[start][K] != -1){
return dp[start][K];
}
// 记录本次结果,因为要放进去缓冲表
int ans = 0;
// 就是正经的递归调用了
// 递归终止条件
if (K == 0){
ans = (start == aim ? 1: 0);
}else if (start == 1){
ans = process2(N, start+1, aim, K-1, dp);
}else if (start == N){
ans = process2(N, start-1, aim, K-1, dp);
}else{
ans = process2(N, start-1, aim, K-1, dp) + process2(N, start+1, aim, K-1, dp);
}
dp[start][K] = ans;
return ans;
}
测试:
@Test
public void test1(){
// 1 2 3 4 5 6
// 在位置2,达到6, 共6步
System.out.println(ways(4, 2, 4, 4));
}
public static int ways(int N, int start, int aim, int K){
int[][] dp = new int[N+1][K+1];
for (int i = 0; i < N+1; i++) {
for (int j = 0; j < K+1; j++) {
dp[i][j] = -1;
}
}
return process2(N, start, aim, K, dp);
}
假设有一个背包,容量为k,有n个物品,其重量和价值为w[i],v[i],把东西放进背包,如何拿到最有价值的组合
同样,先考虑暴力递归的问题
假如我们需要把所有的组合都算一遍,那么先定义一下参数都有哪些?
可选的物品重量,可选的物品价值,背包容量
只有三个,那么主函数的参数列表就有了:
/**
*
* @param weight 物品重量
* @param value 物品价值
* @param capacity 背包容量
* @return
*/
public static int bag(int[] weight, int[] value, int capacity){
}
那递归时候怎么做呢?
首先思考函数出口在哪里?
出口选择好了,那么我们需要构思递归方法体,按照特点,递归调用要朝着出口去
那么我们在方法体中增加一个index,认为是递归选择到了当前的物品,那么参数也有了
/**
* 背包问题,当前考虑index号货物,index后所有货物都可以选择
* @param weight 可以拿的物品重量
* @param value 可以拿的物品价值
* @param cap 背包剩余容量
* @return
*/
public static int maxValue(int[] weight, int[] value, int index, int cap){
// 函数出口
if (cap < 0){
return -1;
}
if (index == weight.length){
return 0;
}
// 函数体
}
那么考虑方法体,在到了一个物品时候,有两种选择
那么方法体就有了
/**
* 背包问题,当前考虑index号货物,index后所有货物都可以选择
* @param weight 可以拿的物品重量
* @param value 可以拿的物品价值
* @param cap 背包剩余容量
* @return
*/
public static int maxValue(int[] weight, int[] value, int index, int cap){
if (cap < 0){
return -1;
}
if (index == weight.length){
return 0;
}
// 有货,index位置有货
// 两种选择,要 / 不要
// 不选择当前商品
int p1 = maxValue(weight, value, index+1, cap);
// 选择当前商品
int m = maxValue(weight, value, index+1, cap-weight[index]);
int p2 = 0;
// 如果选择后背包变成了负数,也就是背包不能选择这个商品了,那么不做处理
if (m != -1) {
p2 = value[index] + m;
}
return Math.max(p1, p2);
}
测试:
@Test
public void test2(){
int[] w = {5, 2, 3, 6, 7};
int[] v = {10, 5, 8, 2, 6};
System.out.println(bag(w, v, 15));
}
同样因为在上述递归过程中,有重复的计算问题,所以我们用一个缓存表,来存放每次的递归结果,那么这张缓存表该怎么设置?
因为在方法递归时候,只有两个参数是变换的
确定了之后,那么改为动态规划,就是变成缓存表
int[][] dp = new int[weight.length+1][capacity+1];
for (int i = 0; i < weight.length + 1; i ++){
for (int j = 0; j < capacity + 1; j ++){
dp[i][j] = -2;
}
}
写入缓存表时候:
/**
* 背包问题,当前考虑index号货物,index后所有货物都可以选择
* @param weight 可以拿的物品重量
* @param value 可以拿的物品价值
* @param cap 背包剩余容量
* @return
*/
public static int maxValue2(int[] weight, int[] value, int index, int cap, int[][] dp){
if (cap < 0){
return -1;
}
if (index == weight.length){
return 0;
}
if (dp[index][cap] != -2){
return dp[index][cap];
}
int ans = 0;
// 有货,index位置有货
// 两种选择,要 / 不要
int p1 = maxValue2(weight, value, index+1, cap, dp);
int m = maxValue2(weight, value, index+1, cap-weight[index], dp);
int p2 = 0;
if (m != -1) {
p2 = value[index] + m;
}
ans = Math.max(p1, p2);
dp[index][cap] = ans;
return ans;
}
测试:
@Test
public void test2(){
int[] w = {5, 2, 3, 6, 7};
int[] v = {10, 5, 8, 2, 6};
System.out.println(bag(w, v, 15));
}
/**
*
* @param weight 物品重量
* @param value 物品价值
* @param capacity 背包容量
* @return
*/
public static int bag(int[] weight, int[] value, int capacity){
if (weight == null || weight == null || weight.length != value.length || weight.length == 0){
return 0;
}
int[][] dp = new int[weight.length+1][capacity+1];
for (int i = 0; i < weight.length + 1; i ++){
for (int j = 0; j < capacity + 1; j ++){
dp[i][j] = -2;
}
}
// 尝试函数
return maxValue2(weight, value, 0, capacity, dp);
}