前言
动态规划,按个人理解,一句话就是:在暴力递归的工程中,使用缓存,减少重复计算,就是所谓的动态规划。
紧跟大佬学习的脚步~~~
动态规划的套路就是:先写出优秀的递归函数,然后将递归函数改写成动态规划
下面就以实际题目进行学习。
一、背包问题
需求:
给定两个长度都为N的数组weights和values,
weights[i]和values[i]分别代表 i号物品的重量和价值。
给定一个正数bag,表示一个载重bag的袋子,
你装的物品不能超过这个重量。
返回你能装下最多的价值是多少?
思路:
1、先想如何尝试,对于每个i位置下的weight,我们可以要,也可以不要
2、基于第一步实现的递归函数改写动态规划
代码如下:
纯递归版本:
/**
* 返回不超出bag重量限制下,最大的价值数
*
* @param W 重量
* @param V 价值
* @param bag 背包最多的重量
* @return
*/
public static int maxValue(int[] W, int[] V, int bag) {
if (W.length == 0 || V.length == 0 || V.length != W.length || bag < 0) {
return 0;
}
return process(W, V, 0, bag);
}
/**
* 返回,从index后最大价值
*
* @param W
* @param V
* @param index
* @param restBag
* @return
*/
public static int process(int[] W, int[] V, int index, int restBag) {
//base case
if (index == W.length || restBag < 0) {
return 0;
}
//index位置不选择
int p1 = process(W, V, index + 1, restBag);
//index位置选择
int rest = restBag - W[index];
int p2 = Integer.MIN_VALUE;
if (rest >= 0) {
p2 = V[index] + process(W, V, index + 1, rest);
}
return Math.max(p1, p2);
}
思考是否需要改动态规划
考虑是否需要改动态规划,我们举个例子
比如w[1,1,3] bag=6
假设第一次1选择,则后续需要算bag=5情况下价值最大
然后第一次不选,我们选第二个1,此时仍需要算bag=5情况下价值最大。
由此我们知道动态规划有存在的必要。
动态规划代码如下:
public static int dp(int[] W, int[] V, int bag) {
if (W.length == 0 || V.length == 0 || V.length != W.length || bag < 0) {
return 0;
}
int N = W.length;
int[][] dp = new int[N + 1][bag + 1];
//index位置不选择
for (int i = N - 1; i >= 0; i--) {
for (int j = 0; j <= bag; j++) {
//index位置不选择
int p1 = dp[i + 1][j];
//index位置选择
int rest = j - W[i];
int p2 = Integer.MIN_VALUE;
if (rest >= 0) {
p2 = V[i] + process(W, V, i + 1, rest);
}
dp[i][j] = Math.max(p1, p2);
}
}
return dp[0][bag];
}
二、数字字符串转字母字符串
需求:
规定1和A对应、2和B对应、3和C对应...26和Z对应
那么一个数字字符串比如"111”就可以转化为:
"AAA"、"KA"和"AK"
给定一个只有数字字符组成的字符串str,返回有多少种转化结果
思路
1、单个数字可以直接转成字母,
2、两个数字需要判断小于27也可转成字母
3、先写递归函数,再改写动态规划
递归代码:
public static int number(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] str = s.toCharArray();
return process(str, 0);
}
/**
* 返回str[i...]可以转化的结果
*
* @param str
* @param i
* @return
*/
public static int process(char[] str, int i) {
if (i == str.length) {
return 1;
}
if (str[i] == '0') {
return 0;
}
int ways = process(str, i + 1);
if (i + 1 < str.length && ((str[i] - '0') * 10 + (str[i + 1] - '0')) < 27) {
ways += process(str, i + 2);
}
return ways;
}
思考是否需要改动态规划
对于递归函数,假设str[]={"1","2","3"}
假设有一个递归分支跳到“3”算了一遍
然后有一个从“2”正常到“3”,这个时候,又要算一遍,这个时候存在重复计算了过来
所以动态规划就有必要了
动态规划的代码
public static int dp(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] str = s.toCharArray();
int N = str.length;
int[] dp = new int[N + 1];
dp[N] = 1;
for (int i = N - 1; i >= 0; i--) {
if (str[i] != '0') {
int ways = dp[i + 1];
if (i + 1 < str.length && ((str[i] - '0') * 10 + (str[i + 1] - '0')) < 27) {
ways += dp[i + 2];
}
dp[i] = ways;
}
}
return dp[0];
}
三、贴纸拼接问题
需求
leetcode原题:stickers-to-spell-word
给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文
arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来
返回需要至少多少张贴纸可以完成这个任务。
例子:str= "babac",arr = {"ba","c","abcd"}
ba + ba + c 3 abcd + abcd 2 abcd+ba 2
所以返回2
思路:
1、字符为顺序要求,只要能剪出对应数量的字符就行
2、统计出目标的26个字母词频,分别统计出所有贴纸的26个字母的词频
3、递归尝试,假设每种贴纸都是第一张,分别去尝试
4、后续根据情况再改写动态规划
递归代码
public static int minStickers(String[] stickers, String target) {
int N = stickers.length;
//词频统计
int[][] counts = new int[N][26];
for (int i = 0; i < N; i++) {
char[] str = stickers[i].toCharArray();
for (char c : str) {
counts[i][c - 'a']++;
}
}
int ans = process(counts, target);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
public static int process(int[][] stickers, String t) {
//base case
if (t.length() == 0) {
return 0;
}
//target做出词频统计
char[] target = t.toCharArray();
int[] tcounts = new int[26];
for (char c : target) {
tcounts[c - 'a']++;
}
int N = stickers.length;
int min = Integer.MAX_VALUE;
for (int i = 0; i < N; i++) {
int[] sticker = stickers[i];
//剪枝
if (sticker[target[0] - 'a'] > 0) {
StringBuilder builder = new StringBuilder();
for (int j = 0; j < 26; j++) {
if (tcounts[j] > 0) {
int nums = tcounts[j] - sticker[j];
for (int k = 0; k < nums; k++) {
builder.append((char) (j + 'a'));
}
}
}
String rest = builder.toString();
min = Math.min(min, process(stickers, rest));
}
}
return min + (min == Integer.MAX_VALUE ? 0 : 1);
}
思考是否需要改动态规划
根据递归函数方法的定义,我们知道可变参数是String字符串,这种情况下就没有办法做出严格意义上数组格式的动态规划了。因为无法穷举所有的字符串,加张缓存表就足够了
加缓存的代码
public static int minStickers(String[] stickers, String target) {
int N = stickers.length;
//所有贴纸的词频
int[][] count = new int[N][26];
for (int i = 0; i < N; i++) {
String sticker = stickers[i];
char[] str = sticker.toCharArray();
for (char c : str) {
count[i][c - 'a']++;
}
}
HashMap<String, Integer> map = new HashMap<>();
int ans = process(count, target, map);
return ans == Integer.MAX_VALUE ? -1 : ans;
}
/**
* 返回,需要最少几张贴纸完成目标
*
* @param stickers 贴纸的词频
* @param target 目标字符串
* @param map 缓存表
* @return
*/
public static int process(int[][] stickers, String target, HashMap<String, Integer> map) {
if (map.containsKey(target)) {
return map.get(target);
}
//base case
if (target.length() == 0) {
map.put("", 0);
return 0;
}
//目标的词频
char[] tStr = target.toCharArray();
int[] tCount = new int[26];
for (char c : tStr) {
tCount[c - 'a']++;
}
int N = stickers.length;
int min = Integer.MAX_VALUE;
for (int i = 0; i < N; i++) {
int[] sticker = stickers[i];
//剪枝
if (sticker[tStr[0] - 'a'] > 0) {
//词频相减,求出剩余的目标
StringBuilder builder = new StringBuilder();
for (int j = 0; j < 26; j++) {
if (tCount[j] > 0) {
int restNum = tCount[j] - sticker[j];
for (int k = 0; k < restNum; k++) {
builder.append((char) (j + 'a'));
}
}
}
String rest = builder.toString();
min = Integer.min(min, process(stickers, rest, map));
}
}
int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
map.put(target, ans);
return ans;
}
总结
动态规划其实就是在递归函数的基础上面根据可变参数改写的。动态规划就是为了减少递归函数的重复计算