【基础提升】左神算法课笔记(十三)从暴力递归到动态规划
暴力递归——>记忆化搜索——>严格表缓存1、确定可变参数范围2、标记出终止位置3、标出不用计算就能得到的答案(根据base case)4、推普遍位置如何依赖其他位置5、确定依次计算的顺序【象棋马跳问题】给定一个象棋棋盘,x~[0,.....,8],y~[0,......,9],马从(0, 0)出发,到(x, y)的位置必须要跳k步,求一共有多少种方式。1、暴力递归,将问题转化为从(x, y)跳到(
一、暴力递归到动态规划
try (暴力递归)
|
|
记忆化搜索(加入记忆数组)
|
|
严格表结构(dp)
【从起点到到终点的走法】
给定一个参数N,代表N个位置,N>1;给定一个参数M,表示开始的位置,1<=M<=N;给定一个参数P,表示结束的位置,1<=P<=N;给定一个参数K,表示机器人必须走K步从S到E。
走的规则:随意往左或者往右,来到1位置,只能往2走;来到N位置,只能往N-1走
求:M -> P,必须走K步,有多少种方法?
1、暴力递归
public static int walkWay1(int N, int P, int M, int K){
return process(N, P, K, M);
}
public static int process(int N, int E, int rest, int cur){
if(rest==0){ //basecase
return cur==E?1:0;
}
if(cur==1){
res += process(N, E, rest-1, cur+1);
}
if(cur==N){
res += process(N, E, rest-1, cur-1);
}
return process(N, E, rest-1, cur-1) + process(N, E, rest-1, cur+1);
}
2、记忆化搜索
如:p(3, 1) -> p(2, 2) p(3, 3) -> p(2, 2) + p(2, 4)
p(3, 1) 和 p(3, 3)都能走到p(2, 2),因此对于p(2, 2)来说,无论之前的路如何选择,走到这一步,p(2, 2)的结构都一样,因此,可以构建一个类似“缓存”的结构来记录
这样每个格子只计算一遍,总格子大小为O(K*N),访问临近格子的时间复杂度为O(1)
public static int walkWays2(int N, int P, int M, int K){
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 process2(N, P, K, M, dp);
}
public static int process2(int N, int E, int rest, int cur, int[][] dp){
if(dp[rest][cur]!=-1){ // basecase
return dp[rest][cur];
}
if(rest==0){ // basecase
dp[rest][cur] = cur==E?1:0;
return dp[rest][cur];
}
if(cur==1){
dp[rest][cur] = process(N, E, rest-1, cur+1, dp);
}else if(cur==N){
dp[rest][cur] = process(N, E, rest-1, cur-1, dp);
}else {
dp[rest][cur] = process(N, E, rest-1, cur-1, dp) + process(N, E, rest-1, cur+1, dp);
}
return dp[rest][cur];
}
3、动态规划
假设N=5,K=3,E=4,有
1)根据base case,K=0的时候,N为4时为1,其余全为0
2)查看边界情况,N为1的时候,依赖(K-1, N+1)的位置;N为5的时候,依赖(K-1, N-1)的位置;N只考虑1-5的情况,不考虑0
3)查看递归中中间值的依赖,依赖的是(K-1, N+1)和(K-1, N-1)的和
N 0 1 2 3 4 5 K ------------- 0 X 0 0 0 1 0 1 X 0 0 1 0 1 2 X 0 1 0 2 0 3 X 1 0 3 0 2 4 X 0 4 0 5 0
public static int walkWays3(int N, int P, int M, int K){
int[][] dp = new int[K+1][N+1];
dp[0][P] = 1; // base case
for(int rest = 1; rest<=K; rest++){
for(int cur=1; cur<=N; cur++){
if(cur==1){
dp[rest][cur] = dp[rest-1][cur+1]; // 边界条件
} else if(cur==N){
dp[rest][cur] = dp[rest-1][cur-1]; // 边界条件
} else {
dp[rest][cur] = dp[rest-1][cur+1] + dp[rest-1][cur-1]; // 中间值
}
}
}
return dp[K][M]; //目标是K,M
}
【硬币组成方式】
给定一个正数数组,每个数代表一枚硬币的面值,给一个值10,最少是多少枚硬币能组成该值?
1、暴力递归:
public static int minCoins1(int[] arr, int aim){
int num = process(arr, 0, aim);
return num==-1?-1: num;
}
// arr[index....]组成rest这么多钱,最少的硬币数量返回
public static int process(int[] arr, int index, int rest){
//rest < 0
if(rest < 0){
return -1;
}
// rest == 0
if(rest==0){
return 1;
}
// rest > 0,但没硬币
if(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 && p2==-1){
return -1;
} else {
if(p1==-1){ //选1是错误的
return 1 + p2Next;
}else if(p2==-1){ //选2是错误的
return p1;
}else {
return Math.min(p1, 1+p2Next);
}
}
}
2、记忆化搜索
public static int min2(int[] arr, int aim){
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
for(int i = 0; i<=N; i++){
for(int j = 0; j<=aim; j++){
dp[i][j] = -2;
}
}
int num = f2(arr, 0, aim dp);
}
public static int f2(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 = f2(arr, index+1, rest);
int p2Next = f2(arr, index+1, rest-arr[index]);
if(p1==-1&&p2Next==-1){
dp[index][rest] = -1;
} else{
if(p1==-1){
dp[index][rest] = 1+p2Next;
}else if(p2==-1){
dp[index][rest] = p1;
}else {
dp[index][rest] = Math.min(1+p2Next, p1);
}
}
}
return dp[index][rest];
}
3、动态规划
假设arr [2, 3, 5, 7, 2], aim = 10
rest 0 1 2 3 4 5 6 7 8 9 10 --------------------------------------------------- index 0 0 T 1 0 2 0 3 0 4 0 5 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 1)确定base case: rest = 0 的时候,为0 index == 终止位置,为-1 2) 普遍位置: 主函数都会调用 index+1,rest的值,以及index+1,rest-arr[index]的值 3) 目标值: T(0, aim),表示从0出发,最少多少枚硬币能组成aim的面值
public static int min3(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 = rest-arr[index]>=0?dp[index+1][rest-arr[index]]:-1;
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[0][aim];
}
二、暴力递归改动态规划总结
暴力递归——>记忆化搜索——>严格表缓存
1、确定可变参数范围
2、标记出终止位置
3、标出不用计算就能得到的答案(根据base case)
4、推普遍位置如何依赖其他位置
5、确定依次计算的顺序
【象棋马跳问题】
给定一个象棋棋盘,x~[0,.....,8],y~[0,......,9],马从(0, 0)出发,到(x, y)的位置必须要跳k步,求一共有多少种方式。
1、暴力递归,将问题转化为从(x, y)跳到(0, 0)有多少种方法
public static int getWays(int x, int y, int k){
return process(x, y, k);
}
public static int process(int x, int y, int step){
if(x<0 || x>8 || y<0 || y>9){ // 越界
return 0;
}
if(step==0){
return (x==0 && y==0)? 1: 0;
}
return process(x-2, y-1, step-1)+
process(x+2, y-1, step-1)+
process(x-2, y+1, step-1)+
process(x+2, y+1, step-1)+
process(x-1, y-2, step-1)+
process(x+1, y-2, step-1)+
process(x-1, y+2, step-1)+
process(x+1, y+2, step-1);
}
2、动态规划
public static int dpWay(int x, int y, int k){
int[][][] dp = new int[9][10][k+1];
dp[0][0][0] = 1;
for(int h=1; h<=k; h++){
for(int r=0; r<=8; r++){
for(int c=0; c<=9; c++){
dp[r][c][h] += getValues(dp, r-1, c-2, h-1);
dp[r][c][h] += getValues(dp, r+1, c-2, h-1);
dp[r][c][h] += getValues(dp, r-1, c+2, h-1);
dp[r][c][h] += getValues(dp, r+1, c+2, h-1);
dp[r][c][h] += getValues(dp, r-2, c-1, h-1);
dp[r][c][h] += getValues(dp, r+2, c-1, h-1);
dp[r][c][h] += getValues(dp, r-2, c+1, h-1);
dp[r][c][h] += getValues(dp, r+2, c+1, h-1);
}
}
}
return dp[x][y][k];
}
public static int getValues(int[][][] dp, x, y, step){
if(x<0 || x>8 || y<0 || y>9){
return 0;
}
return dp(x, y, step);
}
三、斜率优化
通过观察,若存在枚举行为,可以通过某种方式省掉
【找零问题】
给定一个正数数组,里面的数字不重复,每一个数字代表铅笔的面值,给定数X,在同一种纸币能使用任意张的情况下,完成X的找零有多少种方法?
1、暴力递归
public static int way1(int[] arr, int aim){
return process(arr, 0, aim);
}
// 可以自由使用arr[index...]所有的货币
// 需要搞定的钱数是rest
// 返回找零的方法
public static int process(int[] arr, int index, int rest){
if(index==arr.length){
return rest==0?1: 0;
}
int ways = 0;
// arr[index] 0张 1张 ... 不要超过rest的张数
for(int zhang=0; arr[index]*zhang<=rest; zhang++){
ways += process(arr, index+1, rest-arr[index]*zhang);
}
return ways;
}
2、动态规划
假设arr[3, 5, 1, 2],aim=10 aim 0 1 2 3 4 5 6 7 8 9 10 index ------------------------ 0 T 1 2 ? 3 4 1 0 0 0 0 0 0 0 0 0 0 1) 目标值:(0, 10) 2) base case: index==n:rest==0的时候是1;rest其余时候是0 3) 普遍位置依赖: 假设这会要处理的位置是(2, 6),arr[index]=1 zhang=0时,依赖(index+1, rest) = (3, 6) zhang=1时,依赖(index+1, rest-arr[index]) = (3, 5) zhang=2时,依赖(index+1, rest-2*arr[index]) = (3, 4) ....... zhang=6时,依赖(index+1, rest-6*arr[index]) = (3, 0) 加起来
public static int dpWay(int[] arr, int aim){
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
dp[N][0] = 1;
for(int index=N-1; index>=0; index--){
for(int rest=0; rest<=aim; rest++){
int ways = 0;
for(int zhang=0; arr[i]*zhang<=rest;zhang++){
ways += dp[index+1][rest-arr[index]*zhang];
}
dp[index][rest] = ways;
}
}
return dp[0][aim];
}
3、斜率优化
通过进一步观察,可以发现
aim 0 1 2 3 4 5 6 7 8 9 10 index ------------------------ 0 T 1 2 x ? 3 ... c b a 4 1 0 0 0 0 0 0 0 0 0 0?位置的值取决于a、b、c、.....,x位置的值取决于b、c、.....
因此,求?位置的值可以简化为 x + a
public static int dpWay(int[] arr, int aim){
int N = arr.length;
int[][] dp = new int[N+1][aim+1];
dp[N][0] = 1;
for(int index=N-1; index>=0; index--){
for(int rest=0; rest<=aim; rest++){
dp[index][rest] = dp[index+1][rest];
if(rest-arr[index]>0){
dp[index][rest] += dp[index][rest-arr[index]];
}
}
}
return dp[0][aim];
}
更多推荐


所有评论(0)