优化路线:
try(递归尝试)-> 记忆化搜索(dp)-> 严格表结构(dp)-> 严格表结构高精(dp)
try(递归尝试):会产生重复计算,浪费时间
记忆化搜索:用一个缓存来缓存之前的数据,避免重复计算。不整理位置之间的依赖关系。
严格表结构:纠结位置之间的依赖顺序。
做动态规划的题目,首先进行递归尝试,然后优化成记忆化搜索,最后根据位置的依赖关系,转换成严格表结构,直接将递归尝试的代码,复制然后进行边界修改,不需要的进行删除。
严格表结构高精:在严格表结构的基础上进行观察产生的
决定暴力尝试好坏的因素有哪些?
- 单个可变参数的维度,最好是一个整数,最好不要用一个数组来表示不确定的状态。
可变参数的个数少的好。一维就是一行值,二维是一个二维表,三维就是一个立体。
阿里笔试题:机器人走K步到达M点问题
假设有排成一行的N个位置,记为1~N,N一定大于或等于2;
- 开始时机器人在M(1 <= M <= N)位置上,
- 如果机器人位于1位置,那么下一步只能走到2位置,
- 如果机器人位于N位置,那么下一步只能走到N-1位置,
- 如果机器人位于中间的任一位子,那么下一步可以向左走,也可以向右走。
机器人必须走K步,最终来到P(1 <= P <= N),给定参数N,M,K,P,有多少种走法?
public class RobotWalk {public static int ways1(int N, int M, int K, int P) {// 参数无效直接返回0if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}// 总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数return walk(N, M, K, P);}// 递归版本// N : 位置为1 ~ N,固定参数// cur : 当前在cur位置,可变参数// rest : 还剩res步没有走,可变参数// P : 最终目标位置是P,固定参数// 该函数的含义:只能在1~N这些位置上移动,当前在cur位置,走完rest步之后,// 停在P位置的方法数作为返回值返回// 这种方式会产生重复计算,浪费时间// f(N,4,2,P) --> f(N,3,1,P)、f(N,3,3,P)// f(N,3,1,P) --> f(N,2,2,P)// f(N,3,3,P) --> f(N,2,2,P)、f(N,2,4,P)public static int walk(int N, int cur, int rest, int P) {// 如果没有剩余步数了,当前的cur位置就是最后的位置// 如果最后的位置停在P上,那么之前做的移动是有效的// 如果最后的位置没在P上,那么之前做的移动是无效的if (rest == 0) {return cur == P ? 1 : 0;}// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2// 后续的过程就是,来到2位置上,还剩rest-1步要走if (cur == 1) {return walk(N, 2, rest - 1, P);}// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1// 后续的过程就是,来到N-1位置上,还剩rest-1步要走if (cur == N) {return walk(N, N - 1, rest - 1, P);}// 如果还有rest步要走,而当前的cur位置在中间位置上,那么当前这步可以走向左,也可以走向右// 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走// 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走// 走向左、走向右是截然不同的方法,所以总方法数要都算上return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);}// 对上面方式进行记忆化搜索public static int ways1(int N, int M, int K, int P) {if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}int[][] dp = new int[K + 1][N + 1];for (int i = 0; i <= K; i++) {for (int j = 0; j <= N; j++) {dp[i][j] = -1;}}return walk(N, M, K, P,dp);}public static int walk(int N, int cur, int rest, int P,int[][] dp) {if (dp[rest][cur] != -1) {return dp[rest][cur];}if (rest == 0) {dp[rest][cur] = cur == P ? 1 : 0;return dp[rest][cur];}if (cur == 1) {dp[rest][cur] = walk(N, 2, rest - 1, P,dp);} else if (cur == N) {dp[rest][cur] = walk(N, N - 1, rest - 1, P,dp);} else {dp[rest][cur] = walk(N, cur + 1, rest - 1, P,dp) +walk(N, cur - 1, rest - 1, P,dp);}return dp[rest][cur];}public static int ways2(int N, int M, int K, int P) {// 参数无效直接返回0if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}int[][] dp = new int[K + 1][N + 1];dp[0][P] = 1;for (int i = 1; i <= K; i++) {for (int j = 1; j <= N; j++) {if (j == 1) {dp[i][j] = dp[i - 1][2];} else if (j == N) {dp[i][j] = dp[i - 1][N - 1];} else {dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];}}}return dp[K][M];}public static int ways3(int N, int M, int K, int P) {// 参数无效直接返回0if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {return 0;}int[] dp = new int[N + 1];dp[P] = 1;for (int i = 1; i <= K; i++) {int leftUp = dp[1];// 左上角的值for (int j = 1; j <= N; j++) {int tmp = dp[j];if (j == 1) {dp[j] = dp[j + 1];} else if (j == N) {dp[j] = leftUp;} else {dp[j] = leftUp + dp[j + 1];}leftUp = tmp;}}return dp[M];}public static void main(String[] args) {System.out.println(ways1(7, 4, 9, 5));System.out.println(ways2(7, 4, 9, 5));System.out.println(ways3(7, 4, 9, 5));}}
给一个int[]{2,7,3,5,3}正数,里面的值代表一枚硬币的面值,一个数就代表一枚硬币,给你一个数,假如是10,用最少的硬币组成10这个数
从左往右模型,从左往右进行遍历尝试
// 暴力尝试public static int minCoins(int[] arr, int aim) {return process(arr, 0, aim);}// arr[index...]组成出rest这么多钱,最少的硬币数量返回public static int process(int[] arr,int index, int rest) {if (rest < 0) {return -1;}if (rest == 0) {return 0;}// rest > 0if (index == arr.length) {return -1;}// rest > 0 并且 也有硬币int p1 = process(arr, index + 1, rest);int p2Next = process(arr, index + 1, rest - arr[index]);if (p1 == -1 && p2Next == -1) {return -1;} else {if (p1 == -1) {return p2Next + 1;}if (p2Next == -1) {return p1;}return Math.min(p1, p2Next + 1);}}// 记忆化搜索public static int minCoins(int[] arr, int aim) {int[][] dp = new int[arr.length + 1][aim + 1];for (int i = 0; i <= arr.length; i++) {for (int j = 0; j <=aim; j++) {dp[i][j] = -2;}}return process(arr, 0, aim, dp);}// arr[index...]组成出rest这么多钱,最少的硬币数量返回public static int process(int[] arr,int index, int rest, int[][] dp) {if (rest < 0) {return -1;}if (dp[index][rest] != -2) {return dp[index][rest];}if (rest == 0) {dp[index][rest] = 0;} else if (index == arr.length) {dp[index][rest] = -1;} else {int p1 = process(arr, index + 1, rest,dp);int p2Next = process(arr, index + 1, rest - arr[index],dp);if (p1 == -1 && p2Next == -1) {dp[index][rest] = -1;} else {if (p1 == -1) {dp[index][rest] = p2Next + 1;} else if (p2Next == -1) {dp[index][rest] = p1;} else {dp[index][rest] = Math.min(p1, p2Next + 1);}}}return dp[index][rest];}// 严格表结构public static int minCoins(int[] arr, int aim) {int N = arr.length;int[][] dp = new int[N + 1][aim + 1];for (int row = 0; row <= N; row++) {dp[row][0] = 0;}for (int col = 1; col <= aim; col++) {dp[N][col] = -1;}for (int index = N - 1; index >= 0; index--) {for (int rest = 1; rest <= aim; rest++) {int p1 = dp[index + 1][rest];int p2Next = -1;if (rest - arr[index] >= 0) {p2Next = dp[index + 1][rest - arr[index]];}// rest > 0 并且 也有硬币if (p1 == -1 && p2Next == -1) {dp[index][rest] = -1;} else {if (p1 == -1) {dp[index][rest] = p2Next + 1;}if (p2Next == -1) {dp[index][rest] = p1;}dp[index][rest] = Math.min(p1, p2Next + 1);}}}return dp[0][aim];}
// 无效解不能返回0,返回0算是有解,解就0个硬币public class CoinsMin {public static int minCoins1(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return -1;}return process(arr, 0, aim);}// 当前考虑的面值是arr[i],还剩rest的钱需要找零// 如果返回-1说明自由使用arr[i..N-1]面值的情况下,无论如何也无法找零rest// 如果返回不是-1,代表自由使用arr[i..N-1]面值的情况下,找零rest需要的最少张数public static int process(int[] arr, int i, int rest) {// base case:// 已经没有面值能够考虑了// 如果此时剩余的钱为0,返回0张// 如果此时剩余的钱不是0,返回-1if (i == arr.length) {return rest == 0 ? 0 : -1;}// 最少张数,初始时为-1,因为还没找到有效解int res = -1;// 依次尝试使用当前面值(arr[i])0张、1张、k张,但不能超过restfor (int k = 0; k * arr[i] <= rest; k++) {// 使用了k张arr[i],剩下的钱为rest - k * arr[i]// 交给剩下的面值去搞定(arr[i+1..N-1])int next = process(arr, i + 1, rest - k * arr[i]);if (next != -1) { // 说明这个后续过程有效res = res == -1 ? next + k : Math.min(res, next + k);}}return res;}public static int minCoins2(int[] arr, int aim) {if (arr == null || arr.length == 0 || aim < 0) {return -1;}int N = arr.length;int[][] dp = new int[N + 1][aim + 1];// 设置最后一排的值,除了dp[N][0]为0之外,其他都是-1for (int col = 1; col <= aim; col++) {dp[N][col] = -1;}for (int i = N - 1; i >= 0; i--) { // 从底往上计算每一行for (int rest = 0; rest <= aim; rest++) { // 每一行都从左往右dp[i][rest] = -1; // 初始时先设置dp[i][rest]的值无效if (dp[i + 1][rest] != -1) { // 下面的值如果有效dp[i][rest] = dp[i + 1][rest]; // dp[i][rest]的值先设置成下面的值}// 左边的位置不越界并且有效if (rest - arr[i] >= 0 && dp[i][rest - arr[i]] != -1) {if (dp[i][rest] == -1) { // 如果之前下面的值无效dp[i][rest] = dp[i][rest - arr[i]] + 1;} else { // 说明下面和左边的值都有效,取最小的dp[i][rest] = Math.min(dp[i][rest],dp[i][rest - arr[i]] + 1);}}}}return dp[0][aim];}// for testpublic static int[] generateRandomArray(int len, int max) {int[] arr = new int[(int) (Math.random() * len) + 1];for (int i = 0; i < arr.length; i++) {arr[i] = (int) (Math.random() * max) + 1;}return arr;}public static void main(String[] args) {int len = 10;int max = 10;int testTime = 10000;for (int i = 0; i < testTime; i++) {int[] arr = generateRandomArray(len, max);int aim = (int) (Math.random() * 3 * max) + max;if (minCoins1(arr, aim) != minCoins2(arr, aim)) {System.out.println("ooops!");break;}}}}
