动态规划(2)——背包问题、数字转字母问题

208 阅读1分钟

我们从递归聊到动态规划,很显然,动态规划还没结束,今天继续来聊从暴力递归到动态规划的套路

一、背包问题

1、题目描述

有n种物品,w数组表示物品重量、v数组表示物品的价值,物品 i 的重量为 w[i],价值为 v[i],假定所有物品的重量和价值都是非负的,背包所能承受的最大重量为bag,在背包承重范围内,如何挑选物品使得价值最大,返回最大的价值。

2、思路1 从尝试开始

对于每一个货物,我都可以选择要或不要,所以最大价值必定在其中一个分支下,因为我们把所有情况都枚举了。

11.png

/**
 * @author Java和算法学习:周一
 */
public static int maxValue(int[] w, int[] v, int bag) {
    if (w == null || v == null || w.length != v.length || w.length == 0) {
        return 0;
    }
    return process1(w, v, 0, bag);
}

/**
 * 当前考虑到了index位置的货物,index前的已经做好选择了不能改,index及以后的还能随意选择
 * 返回不超过背包容量的最大价值
 */
public static int process1(int[] w, int[] v, int index, int bag) {
    // 背包已经装不下了
    if (bag < 0) {
        return -1;
    }
    // 当前已经没有货物可以选择了
    if (index == w.length) {
        return 0;
    }
    // 不要当前位置的货物
    int p1 = process1(w, v, index + 1, bag);
    // 要当前位置的货物
    int p2 = 0;
    // 需要判断当前背包减去当前货物后的容量,小于0则也不能要当前货物
    int next = process1(w, v, index + 1, bag - w[index]);
    if (next != -1) {
        p2 = v[index] + next;
    }
    return Math.max(p1, p2);
}

3、动态规划版本

首先分析有无重复调用的过程。例如w = 5、2、3、4……v = 10、4、6、8

12.png

(1)分析可变参数

(2)定义动态规划数组,边界赋值

(3)将递归过程转换为动态规划

/**
 * @author Java和算法学习:周一
 */
public static int dp(int[] w, int[] v, int bag) {
    if (w == null || v == null || w.length != v.length || w.length == 0) {
        return 0;
    }
    int N = w.length;
    // 根据可变参数index和bag定义动态规划数组
    int[][] dp = new int[N + 1][bag + 1];
    // bag < 0 时,返回值-1表示无效值可以不用管,因为bag最小值就是0,在数组下标就不会小于0
    // index == w.length时,返回值0,因为int默认就是0,所以不用手动设置
    // 根据递归过程可知,当前index位置的值总是依赖index+1(下一行)位置的值,所以行index从下往上
    // 同一行的bag之间相互不依赖,所以从任意位置遍历bag(列)均可
    for (int index = N - 1; index >= 0; index--) {
        for (int restBag = 0; restBag <= bag; restBag++) {
            // 以下直接由暴力递归修改即可
            int p1 = dp[index + 1][restBag];
            int p2 = 0;
            int next = restBag - w[index] < 0 ? -1 : dp[index + 1][restBag - w[index]];
            if (next != -1) {
                p2 = v[index] + next;
            }
            dp[index][restBag] = Math.max(p1, p2);
        }
    }
    // 根据暴力递归的主函数调用process1(w, v, 0, bag)可得返回(0, bag)位置的值
    return dp[0][bag];
}

所有代码:

github.com/monday-pro/…

二、数字转字母

1、题目描述

规定1和A对应、2和B对应、3和C对应……26和Z对应。那么一个数字字符串比如"111”就可以转化为:“AAA"、 "KA"和"AK"。给定一个只有数字字符组成的字符串str,返回有多少种转化结果。

2、从尝试开始

(1)分析转换任意 i 位置的字符时,i 能够到最后位置,说明之前的转换有效,产生了一种转换方法;如果 i 位置单独为0字符,说明之前的转换方法无效

(2)i 位置的字符可以选择自己单转,也可以和后面的字符一起转,但是二者数字字符的值必须小于等于26才对

/**
 * @author Java和算法学习:周一
 */
public static int number(String string) {
    if (string == null || string.length() == 0) {
        return 0;
    }
    return process1(string.toCharArray(), 0);
}

/**
 * arr[0...i)上的字符已经转换完成,无需修改
 * arr[i...)以后的字符还能自由转换
 *
 * @param arr 待转换的数组
 * @param i   当前转换的位置
 * @return arr的i位置及以后还有多少种转换方法
 */
private static int process1(char[] arr, int i) {
    // i已经到了最后位置了,说明之前的转换有效,产生了一种转换方法
    if (i == arr.length) {
        return 1;
    }
    // 当前单独面对0字符,说明之前的选择不对
    if (arr[i] == '0') {
        return 0;
    }

    // i位置单独转换
    int p1 = process1(arr, i + 1);
    // i位置和i+1位置一起转换
    // i+1存在,且i和i+1组成的数字字符小于27
    int p2 = i + 1 < arr.length && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? process1(arr, i + 2) : 0;

    return p1 + p2;
}

3、动态规划

(1)分析可变参数

暴力递归的可变参数只有每次转换的位置 i,i 的取值范围为[0, arr.length]

(2)定义动态规划数组,根据base case 进行边界赋值

1)由第(1)步可以定义动态规划数组为:

int n = arr.length;
int[] dp = new int[n + 1];

2)base case为

// i已经到了最后位置了,说明之前的转换有效,产生了一种转换方法
if (i == arr.length) {
    return 1;
}

所以,边界赋值dp[n] = 1;

(3)将递归过程转换为动态规划

即将以下过程的process1过程转换为获取dp数组的值

// 当前单独面对0字符,说明之前的选择不对
if (arr[i] == '0') {
    return 0;
}

// i位置单独转换
int p1 = process1(arr, i + 1);
// i位置和i+1位置一起转换
// i+1存在,且i和i+1组成的数字字符小于27
int p2 = i + 1 < arr.length && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? process1(arr, i + 2) : 0;

return p1 + p2;

转换后即为:

if (arr[i] != '0') {
    int p1 = dp[i + 1];
    int p2 = i + 1 < n && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? dp[i + 2] : 0;
    dp[i] = p1 + p2;
}

(4)根据暴力递归主函数的调用,确定动态规划的返回值

暴力递归主函数的调用如下:

public static int number(String string) {
    if (string == null || string.length() == 0) {
        return 0;
    }
    return process1(string.toCharArray(), 0);
}

所以,动态规划的返回值为dp[0]

(5)最后整个动态规划的代码如下:

/**
 * 直接由上面的暴力递归修改
 *
 * @author Java和算法学习:周一
 */
public static int dp(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    char[] arr = s.toCharArray();
    // 1.根据暴力递归的可变参数i的取值范围,确定动态规划的数组大小
    int n = arr.length;
    int[] dp = new int[n + 1];
    // 2.根据暴力递归的 base case 进行边界赋值
    dp[n] = 1;
    // 3.将暴力递归的递归过程转换为动态规划
    for (int i = n - 1; i >= 0; i--) {
        if (arr[i] != '0') {
            int p1 = dp[i + 1];
            int p2 = i + 1 < n && (arr[i] - '0') * 10 + (arr[i + 1] - '0') < 27 ? dp[i + 2] : 0;
            dp[i] = p1 + p2;
        }
    }
    // 4.根据暴力递归主函数的调用,确定动态规划的返回值
    return dp[0];
}

所有代码:

github.com/monday-pro/…