暴力递归到动态规划

某一个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划任何动态规划问题,都一定对应着某一个有重复过程的暴力递归,但不是所有的暴力递归,都一定对应着动态规划

如何找到某个问题的动态规划方式?

1)设计暴力递归:重要原则+4种常见尝试模型!重点!

2)分析有没有重复解

3)用记忆化搜索 -> 用严格表结构实现动态规划

4)看看能否继续优化

常见的4种尝试模型

1)从左往右的尝试模型

2)范围上的尝试模型

3)多样本位置全对应的尝试模型

4)寻找业务限制的尝试模型

动态规划的进一步优化

1)空间压缩

2)状态化简(枚举优化)

3)四边形不等式(比较难)

4)其他优化技巧

----------------------题目演练-----------------------

题目1

假设有排成一行的N个位置,记为1~NN 一定大于或等于 2

开始时机器人在其中的M位置上(M 一定是 1~N 中的一个)

如果机器人来到1位置,那么下一步只能往右来到2位置;

如果机器人来到N位置,那么下一步只能往左来到 N-1 位置;

如果机器人来到中间位置,那么下一步可以往左走或者往右走;

规定机器人必须走 K 步,最终能来到P位置(P也是1~N中的一个)的方法有多少种

给定四个参数 NMKP返回方法数。

暴力递归

public static int ways1(int N, int start, int aim, int K) {
		if (N < 2 || start < 1 || start > N || aim < 1 || aim > N || K < 1) {
			return -1;
		}
		return process1(start, K, aim, N);
	}
// 机器人当前来到的位置是cur,
	// 机器人还有rest步需要去走,
	// 最终的目标是aim,
	// 有哪些位置?1~N
	// 返回:机器人从cur出发,走过rest步之后,最终停在aim的方法数,是多少?
public static int process1(int cur, int rest, int aim, int N) {
		if (rest == 0) { // 如果已经不需要走了,走完了!
			return cur == aim ? 1 : 0;
		}
		// (cur, rest)
		if (cur == 1) { // 1 -> 2
			return process1(2, rest - 1, aim, N);
		}
		// (cur, rest)
		if (cur == N) { // N-1 <- N
			return process1(N - 1, rest - 1, aim, N);
		}
		// (cur, rest)
		return process1(cur - 1, rest - 1, aim, N) + process1(cur + 1, rest - 1, aim, N);
	}

动态规划

/**
	 * Description:
	 * @param N 1-N start开始位置  aim 结果位置 K 走K步
	 *  
	 */
	public static int ways3(int N, int start, int aim, int K) {
		if (N < 2 || start < 1 || start > N || aim < 1 || aim > N || K < 1) {
			return -1;
		}
		int[][] dp = new int[N + 1][K + 1];
		dp[aim][0] = 1;
		for (int rest = 1; rest <= K; rest++) {
			dp[1][rest] = dp[2][rest - 1];
			for (int cur = 2; cur < N; cur++) {
				dp[cur][rest] = dp[cur - 1][rest - 1] + dp[cur + 1][rest - 1];
			}
			dp[N][rest] = dp[N - 1][rest - 1];
		}
		return dp[start][K];
	}

题目2

给定一个整型数组arr代表数值不同的纸牌排成一条线

玩家A和玩家B依次拿走每张纸牌

规定玩家A先拿,玩家B后拿

但是每个玩家每次只能拿走最左或最右的纸牌

玩家A和玩家B都绝顶聪明

请返回最后获胜者的分数

暴力:

这块F1 就是自己作为先手肯定是 拿走的值加上G1

而G1 还是对于自己而言,别人拿走了一个,给你留下的取最小,不需要加别人拿走的那个

所以前面的是ARR[X]+?而后面没有这个ARR[X]

// 根据规则,返回获胜者的分数
	public static int win(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		int first = f1(arr, 0, arr.length - 1);
		int second = g1(arr, 0, arr.length - 1);
		return Math.max(first, second);
	}

	// arr[L..R],先手获得的最好分数返回
	public static int f1(int[] arr, int L, int R) {
		if (L == R) {
			return arr[L];
		}
		int p1 = arr[L] + g1(arr, L + 1, R);
		int p2 = arr[R] + g1(arr, L, R - 1);
		return Math.max(p1, p2);
	}

	// // arr[L..R],后手获得的最好分数返回
	public static int g1(int[] arr, int L, int R) {
		if (L == R) {
			return 0;
		}
		int p1 = f1(arr, L + 1, R); // 对手拿走了L位置的数
		int p2 = f1(arr, L, R - 1); // 对手拿走了R位置的数
		return Math.min(p1, p2);
	}

动态规划

先手对角线就是arr[i] 后手对角线为0

列从1到N-1 行从0-N

public static int win(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		int N = arr.length;
		int[][] fmap = new int[N][N];
		int[][] gmap = new int[N][N];
		for (int i = 0; i < N; i++) {
			fmap[i][i] = arr[i];
		}
		for (int startCol = 1; startCol < N; startCol++) {
			int L = 0;
			int R = startCol;
			while (R < N) {
				fmap[L][R] = Math.max(arr[L] + gmap[L + 1][R], arr[R] + gmap[L][R - 1]);
				gmap[L][R] = Math.min(fmap[L + 1][R], fmap[L][R - 1]);
				L++;
				R++;
			}
		}
		return Math.max(fmap[0][N - 1], gmap[0][N - 1]);
	}

题目3 经典的背包问题

给定两个长度都为N的数组weightsvalues

weights[i]values[i]分别代表 i号物品的重量和价值。

给定一个正数bag表示一个载重bag的袋子,

你装的物品不能超过这个重量。

返回你能装下最多的价值是多少?

暴力递归

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 process(w, v, 0, bag);
	}

	// index 0~N
	// rest 负~bag
	public static int process(int[] w, int[] v, int index, int rest) {
		if (rest < 0) {
			return -1;
		}
		if (index == w.length) {
			return 0;
		}
		int p1 = process(w, v, index + 1, rest);
		int p2 = 0;
		int next = process(w, v, index + 1, rest - w[index]);
		if (next != -1) {
			p2 = v[index] + next;
		}
		return Math.max(p1, p2);
	}

暴力递归可以看出来  index=长度为0 那么dp默认就是0,index作为行,当前行依赖于下面一行,而最后一行已经都为0了 动态规划如下

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;
		int[][] dp = new int[N + 1][bag + 1];
		for (int index = N - 1; index >= 0; index--) {
			for (int rest = 0; rest <= bag; rest++) {
				int p1 = dp[index + 1][rest];
				int p2 = 0;
				int next = rest - w[index] < 0 ? -1 : dp[index + 1][rest - w[index]];
				if (next != -1) {
					p2 = v[index] + next;
				}
				dp[index][rest] = Math.max(p1, p2);
			}
		}
		return dp[0][bag];
	}

题目4

规定1A对应、2B对应、3C对应...26Z对应

那么一个数字字符串比如"111”就可以转化为:

"AAA""KA""AK"

给定一个只有数字字符组成的字符串str返回有多少种转化结果

分析 1-26对应 26个字符,而<0    >27需要过滤

暴力:

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

	// str[0..i-1]转化无需过问
	// str[i.....]去转化,返回有多少种转化方法
	public static int process(char[] str, int i) {
		if (i == str.length) {
			return 1;
		}
		// i没到最后,说明有字符
		if (str[i] == '0') { // 之前的决定有问题
			return 0;
		}
		// str[i] != '0'
		// 可能性一,i单转
		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;
	}

动态:

public static int dp1(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];
	}

题目5

https://leetcode.com/problems/stickers-to-spell-word

给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文

arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str

返回需要至少多少张贴纸可以完成这个任务。

例子:str= "babac"arr = {"ba","c","abcd"}

ba + ba + c  3  abcd + abcdabcd+ba 2

所以返回2

思路

递归思路就是str的词频统计出来比如2个a,2个b,1个c

然后贴纸遍历 for arr  然后递归 知道str消除干净 

注意的是 如果消除后str没有变化那么直接返回 因为这个贴纸无法消除这个str会进入死循环

暴力:

public static int minStickers1(String[] stickers, String target) {
		int ans = process1(stickers, target);
		return ans == Integer.MAX_VALUE ? -1 : ans;
	}

	// 所有贴纸stickers,每一种贴纸都有无穷张
	// target
	// 最少张数
	public static int process1(String[] stickers, String target) {
		if (target.length() == 0) {
			return 0;
		}
		int min = Integer.MAX_VALUE;
		for (String first : stickers) {
			String rest = minus(target, first);
			if (rest.length() != target.length()) {
				min = Math.min(min, process1(stickers, rest));
			}
		}
		return min + (min == Integer.MAX_VALUE ? 0 : 1);
	}

	public static String minus(String s1, String s2) {
		char[] str1 = s1.toCharArray();
		char[] str2 = s2.toCharArray();
		int[] count = new int[26];
		//词频统计
		for (char cha : str1) {
			count[cha - 'a']++;
		}
		for (char cha : str2) {
			count[cha - 'a']--;
		}
		StringBuilder builder = new StringBuilder();
		for (int i = 0; i < 26; i++) {
			if (count[i] > 0) {
				for (int j = 0; j < count[i]; j++) {
					builder.append((char) (i + 'a'));
				}
			}
		}
		return builder.toString();
	}

优化版本:

public static int minStickers2(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 cha : str) {
				counts[i][cha - 'a']++;
			}
		}
		int ans = process2(counts, target);
		return ans == Integer.MAX_VALUE ? -1 : ans;
	}

	// stickers[i] 数组,当初i号贴纸的字符统计 int[][] stickers -> 所有的贴纸
	// 每一种贴纸都有无穷张
	// 返回搞定target的最少张数
	// 最少张数
	public static int process2(int[][] stickers, String t) {
		if (t.length() == 0) {
			return 0;
		}
		// target做出词频统计
		// target  aabbc  2 2 1..
		//                0 1 2..
		char[] target = t.toCharArray();
		int[] tcounts = new int[26];
		for (char cha : target) {
			tcounts[cha - 'a']++;
		}
		int N = stickers.length;
		int min = Integer.MAX_VALUE;
		for (int i = 0; i < N; i++) {
			// 尝试第一张贴纸是谁
			int[] sticker = stickers[i];
			// 最关键的优化(重要的剪枝!这一步也是贪心!)
			//比如我们求aabbccc  贴纸为  kkk   accc  ddd  那么target[0] - 'a'=a 只有accc贴纸满足 别的根本不可能进来
			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, process2(stickers, rest));
			}
		}
		return min + (min == Integer.MAX_VALUE ? 0 : 1);
	}

此题因为可变参数是str  并不是一个整数的值 所以只需要记忆化搜索就好

public static int minStickers3(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 cha : str) {
				counts[i][cha - 'a']++;
			}
		}
		HashMap<String, Integer> dp = new HashMap<>();
		dp.put("", 0);
		int ans = process3(counts, target, dp);
		return ans == Integer.MAX_VALUE ? -1 : ans;
	}

	public static int process3(int[][] stickers, String t, HashMap<String, Integer> dp) {
		if (dp.containsKey(t)) {
			return dp.get(t);
		}
		char[] target = t.toCharArray();
		int[] tcounts = new int[26];
		for (char cha : target) {
			tcounts[cha - '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, process3(stickers, rest, dp));
			}
		}
		int ans = min + (min == Integer.MAX_VALUE ? 0 : 1);
		dp.put(t, ans);
		return ans;
	}

题目6 

https://leetcode.com/problems/longest-common-subsequence/

给定两个字符串str1str2

返回这两个字符串的最长公共子序列长度

比如 : str1 = “a12b3c456d”,str2 = “1ef23ghi4j56k”

最长公共子序列是“123456”,所以返回长度6

思路:

str1[0...i]和str2[0...j],这个范围上最长公共子序列长度是多少?
可能性分类:
a) 最长公共子序列,一定不以str1[i]字符结尾、也一定不以str2[j]字符结尾
b) 最长公共子序列,可能以str1[i]字符结尾、但是一定不以str2[j]字符结尾
c) 最长公共子序列,一定不以str1[i]字符结尾、但是可能以str2[j]字符结尾
d) 最长公共子序列,必须以str1[i]字符结尾、也必须以str2[j]字符结尾
注意:a)、b)、c)、d)并不是完全互斥的,他们可能会有重叠的情况
但是可以肯定,答案不会超过这四种可能性的范围
那么我们分别来看一下,这几种可能性怎么调用后续的递归。
a) 最长公共子序列,一定不以str1[i]字符结尾、也一定不以str2[j]字符结尾
   如果是这种情况,那么有没有str1[i]和str2[j]就根本不重要了,因为这两个字符一定没用啊
   所以砍掉这两个字符,最长公共子序列 = str1[0...i-1]与str2[0...j-1]的最长公共子序列长度(后续递归)
b) 最长公共子序列,可能以str1[i]字符结尾、但是一定不以str2[j]字符结尾
   如果是这种情况,那么我们可以确定str2[j]一定没有用,要砍掉;但是str1[i]可能有用,所以要保留
   所以,最长公共子序列 = str1[0...i]与str2[0...j-1]的最长公共子序列长度(后续递归)
c) 最长公共子序列,一定不以str1[i]字符结尾、但是可能以str2[j]字符结尾
   跟上面分析过程类似,最长公共子序列 = str1[0...i-1]与str2[0...j]的最长公共子序列长度(后续递归)
d) 最长公共子序列,必须以str1[i]字符结尾、也必须以str2[j]字符结尾
   同时可以看到,可能性d)存在的条件,一定是在str1[i] == str2[j]的情况下,才成立的
      所以,最长公共子序列总长度 = str1[0...i-1]与str2[0...j-1]的最长公共子序列长度(后续递归) + 1(共同的结尾)
综上,四种情况已经穷尽了所有可能性。四种情况中取最大即可
其中b)、c)一定参与最大值的比较,
当str1[i] == str2[j]时,a)一定比d)小,所以d)参与
当str1[i] != str2[j]时,d)压根不存在,所以a)参与
但是再次注意了!
a)是:str1[0...i-1]与str2[0...j-1]的最长公共子序列长度
b)是:str1[0...i]与str2[0...j-1]的最长公共子序列长度
c)是:str1[0...i-1]与str2[0...j]的最长公共子序列长度
a)中str1的范围 < b)中str1的范围,a)中str2的范围 == b)中str2的范围
所以a)不用求也知道,它比不过b)啊,因为有一个样本的范围比b)小啊!
a)中str1的范围 == c)中str1的范围,a)中str2的范围 < c)中str2的范围
所以a)不用求也知道,它比不过c)啊,因为有一个样本的范围比c)小啊!
至此,可以知道,a)就是个垃圾,有它没它,都不影响最大值的决策
所以,当str1[i] == str2[j]时,b)、c)、d)中选出最大值
当str1[i] != str2[j]时,b)、c)中选出最大值

暴力
 

public static int longestCommonSubsequence1(String s1, String s2) {
		if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
			return 0;
		}
		char[] str1 = s1.toCharArray();
		char[] str2 = s2.toCharArray();
		// 尝试
		return process1(str1, str2, str1.length - 1, str2.length - 1);
	}
public static int process1(char[] str1, char[] str2, int i, int j) {
		if (i == 0 && j == 0) {
			// str1[0..0]和str2[0..0],都只剩一个字符了
			// 那如果字符相等,公共子序列长度就是1,不相等就是0
			// 这显而易见
			return str1[i] == str2[j] ? 1 : 0;
		} else if (i == 0) {
			// 这里的情况为:
			// str1[0...0]和str2[0...j],str1只剩1个字符了,但是str2不只一个字符
			// 因为str1只剩一个字符了,所以str1[0...0]和str2[0...j]公共子序列最多长度为1
			// 如果str1[0] == str2[j],那么此时相等已经找到了!公共子序列长度就是1,也不可能更大了
			// 如果str1[0] != str2[j],只是此时不相等而已,
			// 那么str2[0...j-1]上有没有字符等于str1[0]呢?不知道,所以递归继续找
			if (str1[i] == str2[j]) {
				return 1;
			} else {
				return process1(str1, str2, i, j - 1);
			}
		} else if (j == 0) {
			// 和上面的else if同理
			// str1[0...i]和str2[0...0],str2只剩1个字符了,但是str1不只一个字符
			// 因为str2只剩一个字符了,所以str1[0...i]和str2[0...0]公共子序列最多长度为1
			// 如果str1[i] == str2[0],那么此时相等已经找到了!公共子序列长度就是1,也不可能更大了
			// 如果str1[i] != str2[0],只是此时不相等而已,
			// 那么str1[0...i-1]上有没有字符等于str2[0]呢?不知道,所以递归继续找
			if (str1[i] == str2[j]) {
				return 1;
			} else {
				return process1(str1, str2, i - 1, j);
			}
		} else { // i != 0 && j != 0
			// 这里的情况为:
			// str1[0...i]和str2[0...i],str1和str2都不只一个字符
			// p1就是可能性c)
			int p1 = process1(str1, str2, i - 1, j);
			// p2就是可能性b)
			int p2 = process1(str1, str2, i, j - 1);
			// p3就是可能性d),如果可能性d)存在,即str1[i] == str2[j],那么p3就求出来,参与pk
			// 如果可能性d)不存在,即str1[i] != str2[j],那么让p3等于0,然后去参与pk,反正不影响
			int p3 = str1[i] == str2[j] ? (1 + process1(str1, str2, i - 1, j - 1)) : 0;
			return Math.max(p1, Math.max(p2, p3));
		}
	}

动态规划直接改

public static int longestCommonSubsequence2(String s1, String s2) {
		if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
			return 0;
		}
		char[] str1 = s1.toCharArray();
		char[] str2 = s2.toCharArray();
		int N = str1.length;
		int M = str2.length;
		int[][] dp = new int[N][M];
		dp[0][0] = str1[0] == str2[0] ? 1 : 0;
		for (int j = 1; j < M; j++) {
			dp[0][j] = str1[0] == str2[j] ? 1 : dp[0][j - 1];
		}
		for (int i = 1; i < N; i++) {
			dp[i][0] = str1[i] == str2[0] ? 1 : dp[i - 1][0];
		}
		for (int i = 1; i < N; i++) {
			for (int j = 1; j < M; j++) {
				int p1 = dp[i - 1][j];
				int p2 = dp[i][j - 1];
				int p3 = str1[i] == str2[j] ? (1 + dp[i - 1][j - 1]) : 0;
				dp[i][j] = Math.max(p1, Math.max(p2, p3));
			}
		}
		return dp[N - 1][M - 1];
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

普朗克的朗姆酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值