动态规划就是将暴力递归中重复的结果记录在DP表中,从而提高效率,就是用空间换时间。
1.1. 斐波那契数列暴力递归解法
public static int f(int n) {
if (n == 1) {//N为1时,返回1
return 1;
}
if (n == 2) {//N为2时,返回1
return 1;
}
//否则返回:
return f(n - 1) + f(n - 2);
}
这样算的过程中有很多重复计算的结果,大大降低运行效率。如果将计算过的记录存进缓存就可以大大节约运行时间。就要用到动态规划。
1.2.斐波那契数列动态规划解法
public static int f(int n) {
if (n <= 1) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
2.1. 题目:
假设有排成一行的N个位置,记为1~N, N-定大于或等于2,
开始时机器人在其中start的位置上(start -定是1~N中的- -个)
规定机器人必须走rest步,最终能来到aim位置(rest也是1~N中的一个)的方法有多少种
给定四个参数start ,rest,aim,N返回方法数。
2.2. 暴力递归解法
分析:
如果机器人来到1位置,那么下一步只能往右来到2位置;
如果机器人来到N位置,那么下一步只能往左来到N-1位置;
如果机器人来到中间位置,那么下一步可以往左走或者往右走;
//暴力解法
//start为起始位置,rest为还有多少步结束,aim为目标位置,N为总长度
public static int f1(int start, int rest, int aim, int N) {
if (rest == 0) {//还有0步
return start == aim ? 1 : 0; //如果起始位置在目标位置返回1,否则0;
}
if (start == 1) { //如果起始位置在1 ,下一步只能走2
return f1(2, rest - 1, aim, N);
}
if (start == N) { //如果起始位置在N ,下一步只能走N-1
return f1(N - 1, rest - 1, aim, N);
}
//起始位置在中间
return f1(start - 1, rest - 1, aim, N) + f1(start + 1, rest - 1, aim, N);
}
2.3第一次优化
有2个可变参数影响结果。start和rest
start范围时1~N
rest范围时0~rest
所以建一个2维表,把所有可能都装下,把二维表全部初始为-1。
public static int f2(int start, int rest, int aim, int N, int dp[][]) {
//之前算过
if (dp[start][rest] != -1) {
return dp[start][rest];
}
//之前没算过
int ans = 0;
if (rest == 0) {//还有0步
ans = start == aim ? 1 : 0; //如果起始位置在目标位置返回1,否则0;
} else if (start == 1) {//如果起始位置在1 ,下一步只能走2
ans = f2(2, rest - 1, aim, N, dp);
} else if (start == N) { //如果起始位置在N ,下一步只能走N-1
ans = f2(N - 1, rest - 1, aim, N, dp);
} else
//起始位置在中间
ans = f2(start - 1, rest - 1, aim, N, dp) + f2(start + 1, rest - 1, aim, N, dp);
dp[start][rest] = ans;
return ans;
}
public static int way2(int start, int rest, int aim, int N) {
int dp[][] = new int[N + 1][rest + 1];
for (int i = 0; i <= N; i++) {
for (int j = 0; j <= rest; j++) {
dp[i][j] = -1;
}
}
return f2(start, rest, aim, N, dp);
}
2.4 最后优化
最后优化就是一个填表过程,根据暴力递归的过程来填表。
行为起始位置star,列为还剩多少步rest。第一列只有aim位置为1,其余全部为0,这样第一列就填好了,第一行没有用,从第二行开始,第二行的数据是依赖箭头所指的位置的值,最后一行的数据是依赖箭头所指的值。 其余中间位置的值是也是依赖箭头所指的值(第二张图)。
接下来的代码就是一个填表的过程。
//最终解法
public static int way3(int start, int rest, int aim, int N) {
int dp[][] = new int[N + 1][rest + 1];
dp[aim][0] = 1; //第1列其他位置为 0
for (int r = 1; r <= rest; r++) {
dp[1][r] = dp[2][r - 1];
for (int c = 2; c < N; c++) {
dp[c][r] = dp[c - 1][r - 1] + dp[c + 1][r - 1];
}
dp[N][r] = dp[N - 1][r - 1];
}
return dp[start][rest];
}
3.1 题目
给定-个整型数组arr,代表数值不同的纸牌排成一条线
玩家A和玩家B依次拿走每张纸牌
规定玩家A先拿,玩家B后拿
但是每个玩家每次只能拿走最左或最右的纸牌
玩家A和玩家B都绝顶聪明
请返回最后获胜者的分数。
3.2 暴力递归
分析:
1.先手函数:
当L==R时,只有一张牌,返回的就是L或者R的值;
L!=S时,返回先拿走右边牌的值加上去掉这张牌的后手函数的值:arr[L] + befor(arr, L + 1, R);
或者返回先拿走左边牌的值加上去掉这张牌的后手函数的值 :arr[R] + befor(arr, L, R - 1);
返回其中最大值;
2.后手函数:
当L==R时,返回0;
L!=S时,返回少左边牌之后的先手函数返回的值:first(arr, L + 1, R),
或者,返回少右边牌之后的先手函数返回的值:first(arr, L, R - 1);
返回其中最小值,因为先手会把最好的拿走,留下最小的给后手;
//先手能得到的最大分数
public static int first(int[] arr, int L, int R) {
if (L == R) {
return arr[L];
}
//先手取得一张牌后就相当于变成减一张牌的后手,就是加上减一张牌的后手函数
int p1 = arr[L] + befor(arr, L + 1, R);
int p2 = arr[R] + befor(arr, L, R - 1);
return Math.max(p1, p2);
}
//后手只能得到先手过后的最小值,所以是MIN
public static int befor(int arr[], int L, int R) {
if (R == L) {
return 0;
}
int b1 = first(arr, L + 1, R);
int b2 = first(arr, L, R - 1);
return Math.min(b1, b2);
}
//返回先手或后手的最大值
public static int f(int arr[]) {
if (arr == null || arr.length == 0) {
return 0;
}
int N = arr.length;
int first = first(arr, 0, arr.length-1);
int befor = befor(arr, 0, arr.length-1);
return Math.max(first, befor);
}
3.3 加缓存法
先手函数加一张缓存表 fgame[L][R],后手函数加一张缓存表,bgame[L][R],表中的数据都初始化为-1,进入先手后手函数前先判断是否值为-1,不为-1直接返回值,否则往下走,过程与暴力递归很相似。
public static int first(int arr[], int L, int R, int fgame[][], int bgame[][]) {
if (fgame[L][R] != -1) {
return fgame[L][R];
}
int ans = 0;
if (L == R) {
return arr[L];
} else {
int f1 = arr[L] + before(arr, L + 1, R, fgame, bgame);
int f2 = arr[R] + before(arr, L, R - 1, fgame, bgame);
ans = Math.max(f1, f2);
}
fgame[L][R] = ans;
return ans;
}
private static int before(int arr[], int L, int R, int fgame[][], int bgame[][]) {
if (bgame[L][R] != -1) {
return bgame[L][R];
}
int ans = 0;
if (L != R) {
int b1 = first(arr, L + 1, R, fgame, bgame);
int b2 = first(arr, L, R - 1, fgame, bgame);
ans = Math.min(b1, b2);
}
bgame[L][R] = ans;
return ans;
}
public static int f2(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
int N = arr.length;
int fgame[][] = new int[N][N];
int bgame[][] = new int[N][N];
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
fgame[i][j] = -1;
bgame[i][j] = -1;
}
}
int first = first(arr, 0, arr.length - 1, fgame, bgame);
int before = before(arr, 0, arr.length - 1, fgame, bgame);
return Math.max(first, before);
}
3.4 动态规划法
给先手后手都创建一张表,打X的地方是不合法的,用不到。然后根据暴力递归来推出其他格子的数据,F表中的?依赖于G表中对应位置(R-1,L)和(R,L-1)这2个格子的值,同理G表中的?位置也是依赖F表中对应位置(R-1,L)和(R,L-1)这2个格子的值。最终我们返回的是(0,N-1)这个位置的值,这也是从暴力递归中得出的。
public static int f3(int arr[]) {
if (arr == null || arr.length == 0) {
return 0;
}
int N = arr.length;
int fgame[][] = new int[N][N];
int bgame[][] = new int[N][N];
for (int i = 0; i < N; i++) {
fgame[i][i] = arr[i];
}
for (int S = 1; S < N; S++) {
int L = 0;
int R = S;
while (R<N) {
fgame[L][R] = Math.max(arr[L] + bgame[L + 1][R], arr[R] + bgame[L][R - 1]);
bgame[L][R] = Math.min(fgame[L + 1][R], fgame[L][R - 1]);
L++;
R++;
}
}
return Math.max(fgame[0][N - 1], bgame[0][N - 1]);
}
这个循环中表示当L==R时,在表中就表示给对角线赋值,因为G表中对角线初始值为0,所以不用赋值。
这个for中表示沿着这条线依次赋值,直到星星位置。S为列,L为行。
4.1 背包问题(01)
题目:有W件物品和一个容量为bag的背包。第 index件物品的费用(即体积,下同)是 ,价值是 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
4.2 暴力递归解法
//w为物品重量,
// v为物品价值,
// index为考虑到了这个物品 要还是不要,
// bag为背包容量
public static int process(int w[], int v[], int index, int bag) {
if (bag < 0) {
return -1;
}
if (index == w.length) {
return 0;
}
//不要这个物品
int p1 = process(w, v, index + 1, bag);
//要这个物品,因为考虑到如果要了这件物品可以会超重,所以判断一下
int next = process(w, v, index + 1, bag - w[index]);
int p2 = 0;
if (next != -1) {
p2 = v[index] + next;
}
return Math.max(p1, p2);
}
4.3 动态规划解法
动态规划解法就是在暴力递归的基础上做一个填表过程。二维表的长度根据可变参数index和bag的长度决定,分析得出index的范围是0~N,bag的范围是负数到bag,所以二维表int[N+1][bag+1],
从暴力递归看出表中数据总是依赖上一行数据,所以便宜顺序从下到上,左右不影响。最后return的结果也是从暴力递归中得出dp[0][bag]。
//动态规划解法
public static int dp(int w[], int v[], int bag) {
if (w == null || v == null || w.length != v.length) {
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];
}