【leetcode】(十四)从暴力递归到动态规划

本文系统讲解从暴力递归演进至动态规划的四个关键阶段:暴力递归(try)、记忆化搜索(记搜DP)、严格表结构DP、空间压缩DP。以机器人位置、01背包、两端抽牌、马走日、死亡越界、零钱兑换等经典算法题为例,剖析状态定义、依赖关系、填表顺序与空间优化策略,强调可变参数分析、DP表维度确定及状态转移方程构建等核心技术要点。

1.思路流程

暴力递归到动态规划的思路流程:

(1)从尝试方法进行递归优化:try(递归)

(2)记忆搜索动态规划:记搜(dp)

(3)严格表结构的动态规划:严格表结构(dp)

在某些问题中,记忆搜索动态规划和严格表结构的动态规划的时间复杂度近似。

(4)精确严格表结构的动态规划:严格表结构精版(dp)

严格表建立流程:

(1)分析可变参数变化范围,两个可变参数就是二维表,三个可变参数就是三维表;

(2)标出你要计算的终止位置

(3)根据basecase标出不用计算直接出答案的位置

(4)推普遍位置是如何依赖其它关系的

(5)定出严格表从哪些格子推出到哪些格子的顺序,将try(递归)得到的东西改动态规划表实现拿值的过程,进行替代

现在用一些题目来讲明以上的流程。

2.机器人位置问题

给定四个整数 N、S、E、K:

  • N:一共有N个位置,分别编号为1、2、3、……、N。

  • S:机器人的起始位置,范围为1~N。

  • E:机器人的最终目标位置,范围为1~N。

  • K:机器人必须行走的步数。

机器人每次只能向左或向右移动一个位置,不能停留在原地,并且必须遵守边界条件:

  • 机器人处于位置1时,只能向右移动到位置2。

  • 机器人处于位置N时,只能向左移动到位置N-1。

  • 机器人处于中间位置时,可以选择向左或向右移动。

问题:机器人必须恰好走K步,从S到达E,共有多少种不同的走法?(参考leetcode62和70,754,2400)

例如,N=5,S=2,E=4,K=4。

符合条件的路径共有4条:

(1) 2 → 3 → 4 → 5 → 4
(2) 2 → 3 → 4 → 3 → 4
(3) 2 → 3 → 2 → 3 → 4
(4) 2 → 1 → 2 → 3 → 4

最终结果:4种方法

下面按照四个阶段进行优化:

暴力递归
    ↓
记忆化搜索动态规划
    ↓
严格表结构动态规划
    ↓
严格表结构精简版(空间压缩)

2.1 方法一:暴力递归(try)

首先,采用从左到右的尝试方法,让机器人每次选择向左或向右移动,直到剩余步数为0。

定义递归函数:

f1(int N, int E, int rest, int cur)

其中:

N:总位置数量,固定参数
E:目标位置,固定参数
rest:机器人还需要走多少步
cur:机器人当前所在位置

返回值:
从cur出发,恰好走rest步到达E的方法数。

由于 N 和 E 在整个递归过程中保持不变,因此我们只需要关注 rest 和 cur 两个变化的参数。

(1)递归终止条件

如果机器人已经走完要求的步数,即 rest=0,则判断当前是否到达目标位置。

if (rest == 0) {
    return cur == E ? 1 : 0;
}

如果 cur==E,说明找到了一条合法路径,返回1;否则返回0。

(2)递归尝试过程

根据当前位置分为三种情况:

情况一:cur=1

机器人只能向右移动。
f(rest,1) = f(rest-1,2)

情况二:cur=N

机器人只能向左移动。
f(rest,N) = f(rest-1,N-1)

情况三:1<cur<N

机器人可以向左或者向右移动。
f(rest,cur)= f(rest-1,cur-1)+ f(rest-1,cur+1)

(3)递归展开过程

例如,N=5,S=2,E=4,K=4。

初始状态是:

f(4,2)

表示:
当前位置为2,剩余4步,
最终需要到达位置4。

展开递归树:

                   f(4,2)
                  /      \
                 /        \
             f(3,1)      f(3,3)
                |         /   \
                |        /     \
             f(2,2)   f(2,2)  f(2,4)
               / \       / \     / \
              ... ...   ... ... ... ...

观察发现:f(2,2) 被重复计算了两次。继续递归下去,还会出现大量相同的子问题。这些子问题具有相同的 rest 和 cur,因此它们的计算结果必然相同。但是暴力递归每次遇到这些状态时,都会重新展开递归树,导致大量重复计算。

(4)时间复杂度

每次递归最多分裂成两个子问题,而递归深度最大为K,因此递归树可以看成一棵高度为K的二叉树。最坏情况下,时间复杂度为:O(2^K)},递归调用栈的空间复杂度为:O(K) 

因此,暴力递归能够解决问题,但当K较大时,执行效率较低。

2.2 方法二:记忆化搜索动态规划(记搜DP)

通过前面的递归树可以发现,递归过程中存在大量重复状态。例如:

f(4,2)
   |
   ├── f(3,1)
   |      |
   |      └── f(2,2)
   |
   └── f(3,3)
          |
          ├── f(2,2)
          |
          └── f(2,4)

其中,f(2,2) 无论通过哪一条路径进入,返回值都相同。因此,可以建立一张二维表,将已经计算过的状态保存起来。当某个状态再次出现时,直接从表中读取结果,不再重复递归。

(1)建立记忆化搜索表

因为递归过程中变化的参数只有:

rest:0~K
cur:1~N

所以建立:

int[][] dp = new int[K + 1][N + 1];

其中:dp[rest][cur]表示:从cur位置出发,还需要走rest步到达E的方法数。

将所有位置初始化为 -1,表示该状态还没有计算过。

for (int i = 0; i <= K; i++) {
    for (int j = 0; j <= N; j++) {
        dp[i][j] = -1;
    }
}

(2)记忆化搜索过程

当程序进入 f2() 时,首先检查缓存:

if (dp[rest][cur] != -1) {
    return dp[rest][cur];
}

如果之前已经计算过该状态,就直接返回。否则继续进行递归,并将最终结果记录在表中。

例如:

dp[rest][cur] =
        f2(N, E, rest - 1, cur - 1, dp)
      + f2(N, E, rest - 1, cur + 1, dp);

这样每个状态最多只会真正计算一次。

(3)记忆化搜索的优化效果

在暴力递归中:

f(2,2)

第一次出现:
展开递归,计算结果。

第二次出现:
再次展开递归,重新计算。

而在记忆化搜索中:

f(2,2)

第一次出现:
展开递归,计算结果并存入dp。

第二次出现:
直接读取dp[2][2]。

第三次出现:
仍然直接读取dp[2][2]。

此时,重复计算被消除。整个问题最多存在:(K+1) N 个不同的有效状态,因此时间复杂度降低为:O(KN)。空间复杂度为:O(KN)} 。此外,还需要最多 O(K) 的递归调用栈空间。

需要注意的是,记忆化搜索仍然保留递归过程,只是为递归增加了缓存。

2.3 方法三:严格表结构动态规划(严格表结构DP)

通过记忆化搜索,我们已经发现,机器人位置问题的计算结果只依赖两个变量:

rest:剩余步数
cur:当前位置

因此,可以直接建立二维DP表,而不再依赖递归函数。

(1)建立DP表

例如:

N=5,S=2,E=4,K=4

建立:

int[][] dp = new int[K + 1][N + 1];

计算完成的DP表如下:

目标位置 E=4

rest \ cur

1

2

3

4

5

0

0

0

0

1

0

1

0

0

1

0

1

2

0

1

0

2

0

3

1

0

3

0

2

4

0

4 ✓

0

5

0

列表示当前位置cur,行表示剩余步数rest。最终取dp[4][2]=4。

(2)确定初始状态

当 rest=0 时,机器人已经不能继续移动。

因此:

如果cur=E:
dp[0][cur] = 1

如果cur≠E:
dp[0][cur] = 0

本题目标位置为4,因此:

dp[0][4] = 1;

对应第0行:

cur:   1  2  3  4  5
rest=0:0  0  0  1  0

(3)分析表结构的依赖关系

由于每走一步,剩余步数都会减少1,因此当前状态依赖上一行。

对于左边界位置1:

dp[rest][1] = dp[rest-1][2]

对于右边界位置N:

dp[rest][N] = dp[rest-1][N-1]

对于中间位置:

dp[rest][cur]
    = dp[rest-1][cur-1]
    + dp[rest-1][cur+1]

可以发现:

  • 第一列依赖上一行的右上角。

  • 最后一列依赖上一行的左上角。

  • 中间位置依赖上一行的左上角与右上角之和。

因此,只需要从上往下依次填写DP表。

(4)逐行填写DP表

第一行:

rest=0:

0 0 0 1 0

第二行:

rest=1:

dp[1][3] = dp[0][2] + dp[0][4]
         = 0 + 1
         = 1

dp[1][5] = dp[0][4]
         = 1

结果:
0 0 1 0 1

第三行:

rest=2:

dp[2][2] = dp[1][1] + dp[1][3]
         = 0 + 1
         = 1

dp[2][4] = dp[1][3] + dp[1][5]
         = 1 + 1
         = 2

结果:
0 1 0 2 0

第四行:

rest=3:

dp[3][1] = dp[2][2] = 1

dp[3][3] = dp[2][2] + dp[2][4]
         = 1 + 2
         = 3

dp[3][5] = dp[2][4] = 2

结果:
1 0 3 0 2

第五行:

rest=4:

dp[4][2] = dp[3][1] + dp[3][3]
         = 1 + 3
         = 4

dp[4][4] = dp[3][3] + dp[3][5]
         = 3 + 2
         = 5

结果:
0 4 0 5 0

最终机器人从位置2出发,恰好走4步到达位置4的方法数为:

dp[4][2] = 4;

(5)严格表结构DP的特点

严格表结构动态规划不再使用递归,而是根据递归关系确定DP表中每个位置的依赖关系,然后按照正确顺序填表。

由于有K+1行、N个有效位置,每个状态的计算只需要常数次操作,因此:

时间复杂度:O(KN)

空间复杂度:O(KN)

与记忆化搜索相比,两者时间复杂度相同,但严格表结构DP不需要递归调用栈。

2.4 方法四:严格表结构精简版动态规划(空间压缩DP)

观察第三种方法的状态转移方程:

dp[rest][cur]
    = dp[rest-1][cur-1]
    + dp[rest-1][cur+1]

我们发现,当前行的计算只依赖上一行,与更早的行没有关系。

例如:计算rest=4时:只需要rest=3的数据。计算rest=3时:只需要rest=2的数据。

因此,没有必要保留整个二维DP表,只需要保存当前行和上一行。

(1)使用两个一维数组

原来:

int[][] dp = new int[K + 1][N + 1];

现在:

int[] dp = new int[N + 1];
int[] next = new int[N + 1];

其中:

dp:上一行的状态
next:当前正在计算的状态

初始状态仍然为:

dp[E] = 1;

(2)空间压缩的计算过程

以 N=5,E=4,K=4 为例。

初始化:rest=0

dp:
位置  1  2  3  4  5
数值  0  0  0  1  0


第一轮:rest=1

根据dp计算next:

next:
位置  1  2  3  4  5
数值  0  0  1  0  1

计算结束后:
dp = next


第二轮:rest=2

根据上一轮dp计算next:

next:
位置  1  2  3  4  5
数值  0  1  0  2  0

计算结束后:
dp = next


第三轮:rest=3

next:
位置  1  2  3  4  5
数值  1  0  3  0  2

计算结束后:
dp = next


第四轮:rest=4

next:
位置  1  2  3  4  5
数值  0  4  0  5  0

计算结束后:
dp = next


最终:

dp[S] = dp[2] = 4

(3)精简版状态转移

虽然不再使用二维数组,但状态转移方程完全不变。

for (int rest = 1; rest <= K; rest++) {

    int[] next = new int[N + 1];

    for (int cur = 1; cur <= N; cur++) {

        if (cur == 1) {
            next[cur] = dp[2];

        } else if (cur == N) {
            next[cur] = dp[N - 1];

        } else {
            next[cur] = dp[cur - 1] + dp[cur + 1];
        }
    }

    dp = next;
}

需要注意,不能在没有额外处理的情况下直接覆盖原来的 dp[cur]。因为当前行的状态计算需要使用上一行的数据。如果一边计算一边覆盖原数组,就可能破坏后面状态所依赖的旧数据。所以这里使用 next 保存当前行,计算完成后再令 dp=next。

(4)复杂度分析

每一轮仍然需要计算N个位置,一共计算K轮。因此时间复杂度仍然为:O(KN)} 

但是只需要保存两个长度为N+1的一维数组。所以空间复杂度降低为:O(N)} 

第四种方法没有改变第三种方法的递推关系,只是利用相邻行之间的依赖关系,减少了不必要的历史状态存储。

2.5 代码和总结对比

代码如下:

package Class014;

public class Code_RobotWalk {
    public static int walkWays1(int N,int E,int S,int K){
        return f1(N,E,K,S);
    }
    //一共是1~N这么多位置,固定参数
    //最终的目标是E,固定参数
    //当前在cur位置
    //还剩rest步需要走
    //返回从cur出发,恰好走rest步到达E的方法数
    public static int f1(int N,int E,int rest,int cur){
        if (rest==0){
            //来到终止位置,记录1个路径;没有来到终止位置,0个路径
            return cur==E?1:0;
        }
        //res>0有路可走
        //当前位置是1,只能从1->2,rest-1
        if (cur==1){
            return f1(N,E,rest-1,2);
        }
        //当前位置是N,要走,rest-1
        if (cur==N){
            return f1(N,E,rest-1,N-1);
        }
        //机器人来达的是中间位置,可以往左一步,或者往右一步。
        return f1(N,E,rest-1,cur-1)+f1(N,E,rest-1,cur+1);
    }

    // 方法二:记忆化搜索
    public static int walkWays2(int N,int E,int S,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 f2(N,E,K,S,dp);
    }

    //一共是1~N这么多位置,固定参数
    //最终的目标是E,固定参数
    //当前在cur位置
    //还剩rest步需要走
    //返回方法数
    public static int f2(int N,int E,int rest,int cur,int[][]dp){
        //如果之前算过,那么就从缓存里面取
        if (dp[rest][cur]!=-1){
            return dp[rest][cur];
        }
        //缓存没有命中的情况
        if (rest==0){
            //来到终止位置,记录1个路径;没有来到终止位置,0个路径
            dp[rest][cur]=cur==E?1:0;
            return dp[rest][cur];
        }
        //res>0有路可走
        //当前位置是1,只能从1->2,rest-1
        if (cur==1){
            dp[rest][cur]=f2(N,E,rest-1,2,dp);
        }
        //当前位置是N,要走,rest-1
        else if (cur==N){
            dp[rest][cur]=f2(N,E,rest-1,N-1,dp);
        }
        //机器人来达的是中间位置,可以往左一步,或者往右一步。
        else {
            dp[rest][cur]=f2(N,E,rest-1,cur-1,dp)+f2(N,E,rest-1,cur+1,dp);
        }
        return dp[rest][cur];
    }

    // 方法三:严格表结构动态规划
    // P:目标位置E
    // M:起始位置S
    // K:必须走的步数
    public static int dpWay(int N,int P,int M,int K){
        // dp[rest][cur]:
        // 从cur出发,剩余rest步到达P的方法数
        int[][]dp=new int[K+1][N+1];//dp[...][0]废了不用
        // 剩余0步时,只有已经到达P才有1种方法
        dp[0][P]=1;
        // 从第1行开始,依次填写到第K行
        for (int rest=1;rest<=K;rest++){
            for (int cur=1;cur<=N;cur++){
                if (cur==1){
                    // 左边界,只能向右
                    dp[rest][cur]=dp[rest-1][2];
                }else if (cur==N){
                    // 右边界,只能向左
                    dp[rest][cur]=dp[rest-1][N-1];
                }else {
                    // 中间位置,左右方法数相加
                    dp[rest][cur]=dp[rest-1][cur-1]+dp[rest-1][cur+1];
                }
            }
        }
        // 从M出发,走K步,到达P
        return dp[K][M];
    }
    // 方法四:严格表结构精简版(空间压缩)
    // 保持原来的状态转移方程不变
    // 只保留上一行和当前行
    public static int dpWayFine(int N,int P,int M,int K){
        //dp[cur]保存上一行的结果
        int[] dp=new int[N+1];
        //对应原二维表的dp[0][P]=1
        dp[P]=1;
        for (int rest=1;rest<=K;rest++){
            //当前行
            int[] next=new int[N+1];
            for (int cur=1;cur<=N;cur++){
                //左边界
                if (cur==1){
                    next[cur]=dp[2];
                }
                //右边界
                else if (cur==N){
                    next[cur]=dp[N-1];
                }
                //中间位置
                else {
                    next[cur]=dp[cur-1]+dp[cur+1];
                }
            }
            //当前行成为下一轮的上一行
            dp=next;
        }
        return dp[M];
    }

    public static void main(String [] args){
        //每组:N,S,E,K,预期结果
        int[][]tests={
                {5, 2, 4, 4, 4},
                {5, 2, 4, 2, 1},
                {5, 2, 4, 3, 0},
                {5, 2, 2, 0, 1},
                {5, 2, 4, 0, 0},
                {5, 1, 5, 4, 1},
                {2, 1, 2, 3, 1}
        };
        for (int[] t:tests){
            int N=t[0];
            int S=t[1];
            int E=t[2];
            int K=t[3];
            int expected=t[4];

            int ans1=walkWays1(N,E,S,K);
            int ans2=walkWays2(N,E,S,K);
            int ans3=dpWay(N,E,S,K);
            int ans4=dpWayFine(N,E,S,K);

            System.out.println("N="+N+",S="+S+",E="+E+",K="+K);
            System.out.println("暴力递归:" + ans1);
            System.out.println("记忆化搜索:" + ans2);
            System.out.println("严格表结构DP:" + ans3);
            System.out.println("精简版DP:" + ans4);
            if (ans1==expected && ans1==ans2 && ans2==ans3 && ans3==ans4){
                System.out.println("测试提过");
            }else {
                throw new AssertionError("四种方法结果不一致");
            }
            System.out.println("-------------------------");
        }

    }

}

运行结果:

N=5,S=2,E=4,K=4
暴力递归:4
记忆化搜索:4
严格表结构DP:4
精简版DP:4
测试提过
-------------------------
N=5,S=2,E=4,K=2
暴力递归:1
记忆化搜索:1
严格表结构DP:1
精简版DP:1
测试提过
-------------------------
N=5,S=2,E=4,K=3
暴力递归:0
记忆化搜索:0
严格表结构DP:0
精简版DP:0
测试提过
-------------------------
N=5,S=2,E=2,K=0
暴力递归:1
记忆化搜索:1
严格表结构DP:1
精简版DP:1
测试提过
-------------------------
N=5,S=2,E=4,K=0
暴力递归:0
记忆化搜索:0
严格表结构DP:0
精简版DP:0
测试提过
-------------------------
N=5,S=1,E=5,K=4
暴力递归:1
记忆化搜索:1
严格表结构DP:1
精简版DP:1
测试提过
-------------------------
N=2,S=1,E=2,K=3
暴力递归:1
记忆化搜索:1
严格表结构DP:1
精简版DP:1
测试提过
-------------------------

四种方法对比:

方法

核心思想

时间复杂度

空间复杂度

暴力递归 walkWays1()

每一步尝试左右移动,递归枚举所有路径

最坏O(2^K)

O(K)

记忆化搜索 walkWays2()

将已计算的递归状态记录到二维表,避免重复计算

O(KN)

O(KN)

严格表结构DP dpWay()

根据递归依赖关系,自底向上填写完整二维表

O(KN)

O(KN)

严格表结构精简版 dpWayFine()

当前行只依赖上一行,用滚动数组压缩空间

O(KN)

O(N)

3.01背包问题

leetcode322零钱兑换。

[2,7,3,5,3,......]正数序列,每个代表一枚硬币,aim=10,问最少要多少枚硬币?

思路:


3. 01背包问题:最少硬币数量

题目:给定正整数数组arr,每个元素代表一枚硬币,每枚硬币最多只能使用一次。给定目标金额aim,求凑出aim所需的最少硬币数量,无法凑出则返回-1。

例如:arr=[2,7,3,5,3],aim=10。
可以选择7+3=10(2枚)、2+3+5=10(3枚)、2+5+3=10(3枚),因此最优结果为2枚硬币。


一、方法一:暴力递归(try)

1. 定义递归状态

process1(arr,index,rest):使用arr[index...]中的硬币,凑出剩余金额rest所需要的最少硬币数量。

其中index表示当前考虑的硬币位置,rest表示还需要凑出的金额。返回-1表示无法凑出,返回0表示已经凑够,返回正数表示需要的最少硬币数量。

2. 递归终止条件

(1)rest<0:选择硬币后金额超过目标,返回-1。
(2)rest==0:已经凑够目标金额,不需要更多硬币,返回0。
(3)index==arr.length且rest>0:全部硬币考虑完毕,但仍未凑够金额,返回-1。

3. 每个位置的两种选择

第一种:不选择当前硬币,剩余金额不变,直接考虑下一枚硬币。

p1=process1(arr,index+1,rest)

第二种:选择当前硬币,剩余金额减少arr[index],再考虑下一枚硬币。

p2Next=process1(arr,index+1,rest-arr[index])

因为已经选择了当前硬币,所以在p2Next有效时,实际需要的硬币数量为p2Next+1。

4. 递归结果的合并

(1)p1==-1且p2Next==-1:两种选择都无法凑出,返回-1。
(2)p1==-1且p2Next有效:只能选择当前硬币,返回p2Next+1。
(3)p1有效且p2Next==-1:只能不选择当前硬币,返回p1。
(4)两种选择都有效:返回Math.min(p1,p2Next+1)。

5. 具体计算过程

arr=[2,7,3,5,3],aim=10,初始调用process1(arr,0,10),当前考虑的硬币为2。

                  f(0,10)
                 /       \
             不选择2     选择2
               /           \
           f(1,10)       f(1,8)

(1)计算左侧f(1,10),当前硬币为7。

不选择7:进入f(2,10),剩余硬币为[3,5,3]。这些硬币无法凑出10,所以f(2,10)=-1。

选择7:进入f(2,3),剩余硬币为[3,5,3]。只需要再选择一枚3,例如选择index=2的硬币3,进入f(3,0)=0,因此f(2,3)=0+1=1。

综合两种选择:不选择7的分支无效,选择7的分支需要1+1=2枚,所以f(1,10)=2,对应7+3=10。

(2)计算右侧f(1,8),当前硬币为7。

不选择7:进入f(2,8),剩余硬币为[3,5,3],可以选择3+5=8,所以f(2,8)=2。

选择7:进入f(2,1),剩余硬币为[3,5,3],无法凑出1,所以f(2,1)=-1。

因此f(1,8)=2,因为只有不选择7的分支有效。

(3)回到初始状态f(0,10)。

不选择2:p1=f(1,10)=2。
选择2:p2Next=f(1,8)=2,算上当前硬币2,实际需要2+1=3枚。

最终:f(0,10)=min(2,3)=2。

6. 时间复杂度

每枚硬币都有“选择”和“不选择”两种情况,递归树深度最大为N,因此最坏时间复杂度为O(2^N),递归调用栈空间复杂度为O(N)。


二、方法二:记忆化搜索动态规划(记搜DP)

1. 分析重复子问题

暴力递归中真正变化的参数只有index和rest。如果两个递归状态的index、rest相同,那么它们对应的剩余问题完全相同,可以直接复用计算结果。

例如arr=[2,7,3,5,3],aim=10,以下两条路径会到达同一个状态:

第一条路径:不选择2 → 选择7 → 不选择index=2的3 → 不选择5,此时index=4、rest=3。

第二条路径:选择2 → 不选择7 → 不选择index=2的3 → 选择5,此时index=4、rest=3。

两条路径最终都需要计算f(4,3)。此时只剩最后一枚3,选择它后进入f(5,0)=0,因此f(4,3)=1。

暴力递归会重复计算f(4,3),而记忆化搜索只计算第一次,随后直接读取缓存结果。

2. 建立二维缓存表

int[][] dp=new int[N+1][aim+1];

dp[index][rest]表示使用arr[index...]中的硬币,凑出rest所需要的最少硬币数量。

状态约定:-2表示尚未计算,-1表示已经计算但无法凑出,0表示已经凑够,正数表示最少硬币数量。初始化时将整个dp数组填充为-2。

3. 记忆化搜索的执行过程

以process2(arr,4,3,dp)为例:

第一次访问时,dp[4][3]=-2,说明尚未计算,需要继续递归。

不选择最后一枚3:f(5,3)=-1。
选择最后一枚3:f(5,0)=0,算上当前硬币得到0+1=1。

因此将dp[4][3]=1,并返回1。

第二次访问process2(arr,4,3,dp)时,发现dp[4][3]=1,直接返回1,不再展开递归。

4. 整体递归计算

从process2(arr,0,10,dp)开始,每次进入递归状态时,先检查dp[index][rest]。

若结果不等于-2,说明该状态已经计算过,直接返回;否则按照“选择”和“不选择”两种情况递归,并将最优结果写入dp[index][rest]。

以后再次遇到相同状态时,直接读取缓存。最终dp[0][10]=2,返回2。

5. 时间复杂度

index的取值范围为0~N,rest的有效取值范围为0~aim,因此最多存在(N+1)*(aim+1)个状态。

每个状态最多真正计算一次,每次计算只需要常数次运算,所以时间复杂度为O(N*aim),二维缓存空间复杂度为O(N*aim),另外需要O(N)的递归调用栈。


三、方法三:严格表结构动态规划(dp)

1. 从递归结构推导表结构

根据前面两种方法,process(arr,index,rest)存在两种选择:

不选择当前硬币:p1=dp[index+1][rest]。
选择当前硬币:p2Next=dp[index+1][rest-arr[index]],实际硬币数量为p2Next+1。

因此,dp[index][rest]依赖下一行的两个位置。如果两条路径都有效,则:

dp[index][rest]
=min(dp[index+1][rest],
     dp[index+1][rest-arr[index]]+1)

如果只有一条路径有效,则采用有效路径的结果;如果两条路径均不可达,则dp[index][rest]=-1。

2. 确定DP表的初始状态

arr=[2,7,3,5,3],aim=10,N=5。

建立int[][] dp=new int[6][11],其中行index为0~5,列rest为0~10。

首先填写第0列:dp[index][0]=0,因为剩余金额为0时不需要任何硬币。

然后填写最后一行:dp[N][rest]=-1(rest>0),因为已经没有剩余硬币,无法凑出正数金额。

3. 分析表格填写方向

dp[index][rest]依赖dp[index+1][rest]和dp[index+1][rest-arr[index]],因此当前行只依赖下一行。

填写方向必须从最后一行向上,即index=N-1,N-2,...,0。同一行中的状态互不依赖,因此rest可以从1递增到aim。

4. 逐行填写DP表

(1)第5行:index=5,没有剩余硬币。

rest:  0  1  2  3  4  5  6  7  8  9 10
dp[5]: 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1


(2)第4行:index=4,当前硬币为3。

计算dp[4][3]:不选择3,dp[5][3]=-1;选择3,dp[5][0]+1=1,所以dp[4][3]=1。

dp[4]: 0 -1 -1  1 -1 -1 -1 -1 -1 -1 -1


(3)第3行:index=3,当前硬币为5,剩余硬币为[5,3]。

计算dp[3][5]:不选择5,dp[4][5]=-1;选择5,dp[4][0]+1=1,所以dp[3][5]=1。

计算dp[3][8]:不选择5,dp[4][8]=-1;选择5,dp[4][3]+1=1+1=2,所以dp[3][8]=2。

dp[3]: 0 -1 -1  1 -1  1 -1 -1  2 -1 -1


(4)第2行:index=2,当前硬币为3,剩余硬币为[3,5,3]。

计算dp[2][3]:不选择3,dp[3][3]=1;选择3,dp[3][0]+1=1,所以dp[2][3]=min(1,1)=1。

计算dp[2][6]:不选择3,dp[3][6]=-1;选择3,dp[3][3]+1=1+1=2,所以dp[2][6]=2。

计算dp[2][8]:不选择3,dp[3][8]=2;选择3,dp[3][5]+1=1+1=2,所以dp[2][8]=min(2,2)=2。

dp[2]: 0 -1 -1  1 -1  1  2 -1  2 -1 -1


(5)第1行:index=1,当前硬币为7,剩余硬币为[7,3,5,3]。

计算dp[1][7]:不选择7,dp[2][7]=-1;选择7,dp[2][0]+1=1,所以dp[1][7]=1。

计算dp[1][10]:不选择7,dp[2][10]=-1;选择7,dp[2][3]+1=1+1=2,所以dp[1][10]=2。

dp[1]: 0 -1 -1  1 -1  1  2  1  2 -1  2


(6)第0行:index=0,当前硬币为2,剩余硬币为[2,7,3,5,3]。

计算dp[0][10]:不选择2,dp[1][10]=2;选择2,dp[1][8]+1=2+1=3。

两条路径都有效,因此dp[0][10]=min(2,3)=2。

dp[0]: 0 -1  1  1 -1  1  2  1  2  2  2


5. 最终DP表

                 rest
index    0  1  2  3  4  5  6  7  8  9 10
------------------------------------------------
  5      0 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
  4      0 -1 -1  1 -1 -1 -1 -1 -1 -1 -1
  3      0 -1 -1  1 -1  1 -1 -1  2 -1 -1
  2      0 -1 -1  1 -1  1  2 -1  2 -1 -1
  1      0 -1 -1  1 -1  1  2  1  2 -1  2
  0      0 -1  1  1 -1  1  2  1  2  2  2
------------------------------------------------

最终取dp[0][aim],即dp[0][10]=2。

6. 时间复杂度

DP表共有(N+1)*(aim+1)个状态,每个状态只依赖下一行的两个位置,使用常数时间即可完成计算。

因此,时间复杂度为O(N*aim),空间复杂度为O(N*aim)。

代码:

package Class014;

import java.util.Arrays;

public class Code_CoinsMin {
    public static int minCoins1(int[]arr,int aim){
        return process1(arr,0,aim);
    }
    //arr[index.....]组成出rest这么多钱,最少的硬币数量返回
    public static int process1(int[] arr,int index,int rest){
        if (rest<0){
            return -1;
        }
        if (rest==0){
            return 0;
        }
        //rest>0
        if (index==arr.length){
            return -1;
        }
        //rest>0 且也有硬币
        //第一种情况是不要当前硬币,用剩下的硬币搞定
        //第二种是用当前硬币,接着用剩下的硬币搞定
        int p1=process1(arr,index+1,rest);
        int p2Next=process1(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 minCoins2(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 process2(arr,0,aim,dp);
    }
    //arr[index.....]组成出rest这么多钱,最少的硬币数量返回
    public static int process2(int[] arr,int index,int rest,int[][] dp){
        //此步可以视为缓存区域
        //由于无法表示-1,这些都是无效区域,故将表中低于0的位置全部标为-1
        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){
            //rest>0
            dp[index][rest]=-1;
        }else {
            int p1=process2(arr,index+1,rest,dp);
            int p2Next=process2(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 minCoins3(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]];
                }
                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];
    }

    //for test
    public 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 check(int[] arr, int aim, int expected) {
        int ans1 = minCoins1(arr, aim);
        int ans2 = minCoins2(arr, aim);
        int ans3 = minCoins3(arr, aim);
        System.out.println("arr=" + Arrays.toString(arr)
                        + ", aim=" + aim
                        + " | 递归=" + ans1
                        + " 记搜=" + ans2
                        + " DP=" + ans3
        );
        if (ans1 != expected
                || ans2 != expected
                || ans3 != expected) {
            throw new AssertionError("测试失败");
        }
    }
    public static void main(String[] args) {
        System.out.println("===== 固定测试 =====");
        check(new int[]{2, 7, 3, 5, 3}, 10, 2);
        check(new int[]{5, 2, 3}, 5, 1);
        check(new int[]{2, 5}, 6, -1);
        check(new int[]{3, 3, 3}, 6, 2);
        check(new int[]{2, 2, 2}, 4, 2);
        check(new int[]{1, 2, 4}, 7, 3);
        check(new int[]{1, 2, 4}, 8, -1);
        check(new int[]{}, 0, 0);
        check(new int[]{}, 5, -1);
        System.out.println("\n===== 随机交叉测试 =====");
        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;
            int ans1 = minCoins1(arr, aim);
            int ans2 = minCoins2(arr, aim);
            int ans3 = minCoins3(arr, aim);
            // 任意两个答案不同,都报告错误
            if (ans1 != ans2 || ans2 != ans3) {
                System.out.println("ooops!");
                System.out.println(Arrays.toString(arr));
                System.out.println("aim=" + aim);
                System.out.println(ans1 + ", " + ans2 + ", " + ans3);
                return;
            }
        }
        System.out.println("通过 " + testTime + " 组随机测试");
    }
}

运行结果:

===== 固定测试 =====
arr=[2, 7, 3, 5, 3], aim=10 | 递归=2 记搜=2 DP=2
arr=[5, 2, 3], aim=5 | 递归=1 记搜=1 DP=1
arr=[2, 5], aim=6 | 递归=-1 记搜=-1 DP=-1
arr=[3, 3, 3], aim=6 | 递归=2 记搜=2 DP=2
arr=[2, 2, 2], aim=4 | 递归=2 记搜=2 DP=2
arr=[1, 2, 4], aim=7 | 递归=3 记搜=3 DP=3
arr=[1, 2, 4], aim=8 | 递归=-1 记搜=-1 DP=-1
arr=[], aim=0 | 递归=0 记搜=0 DP=0
arr=[], aim=5 | 递归=-1 记搜=-1 DP=-1

===== 随机交叉测试 =====
通过 10000 组随机测试

三种方法的复杂度总结

设 N=arr.length,A=aim。

方法

时间复杂度

空间复杂度

暴力递归 minCoins1()

最坏 O(2^N)

O(N)

记忆化搜索 minCoins2()

O(NA)

O(NA)

严格表结构DP minCoins3()

O(NA)

O(NA)

4.两端抽牌问题

给定一个整型数组 arr,代表数值不同的纸牌排成一条线。玩家 A 和玩家 B 依次拿走每张纸牌,规定玩家 A 先拿,玩家 B 后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家 A 和玩家 B 都绝顶聪明。请返回最后获胜者的分数。(leetcode486,1423)

【举例】

arr = [1, 2, 100, 4]。

开始时,玩家 A 只能拿走 1 或 4。如果开始时玩家 A 拿走 1,则排列变为 [2, 100, 4],接下来玩家 B 可以拿走 2 或 4,然后继续轮到玩家 A……

如果开始时玩家 A 拿走 4,则排列变为 [1, 2, 100],接下来玩家 B 可以拿走 1 或 100,然后继续轮到玩家 A……

玩家 A 作为绝顶聪明的人不会先拿 4,因为拿 4 之后,玩家 B 将拿走 100。所以玩家 A 会先拿 1,让排列变为 [2, 100, 4],接下来玩家 B 不管怎么选,100 都会被玩家 A 拿走。玩家 A 会获胜,分数为 101。所以返回 101。

arr = [1, 100, 2]。

开始时,玩家 A 不管拿 1 还是 2,玩家 B 作为绝顶聪明的人,都会把 100 拿走。玩家 B 会获胜,分数为 100。所以返回 100。
 

详细的分析见【leetcode】(八)暴力递归。在那一章里,我们设置了一个arr[L.....R]数组,接着设置了一个int f(arr,L,R)函数进行分析。具体代码如下:

package Class014;

public class Code_CardsInLine {
    public static int win1(int[] arr){
        if(arr==null||arr.length==0){
            return 0;
        }
        //返回最后的获胜者
        return Math.max(f(arr,0,arr.length-1),s(arr,0,arr.length-1));
    }
    //先手函数
    //当前该你拿,arr[i,,j]
    //返回你的最好分数
    public static int f(int[] arr,int i,int j){
        if(i==j){
            return arr[j];
        }
        //i..j
        return Math.max(arr[i]+s(arr,i+1,j),arr[j]+s(arr,i,j-1));
    }
    //后手函数
    //当前不该你拿,是对方在arr[i..j]范围上拿
    //返回你的最好分数
    public static int s(int[]arr,int i,int j){
        if(i==j){
            return 0;
        }
        //i..j
        return Math.min(f(arr,i+1,j),f(arr,i,j-1));
    }
    public static int dp(int[] arr){
        if (arr==null|| arr.length==0){
            return 0;
        }
        int[][]f=new int[arr.length][arr.length];
        int[][]s=new int[arr.length][arr.length];
        for(int j=0;j<arr.length;j++){
            f[j][j]=arr[j];
            for(int i=j-1;i>=0;i--){
                f[i][j]=Math.max(arr[i]+s[i+1][j],arr[j]+s[i][j-1]);
                s[i][j]=Math.min(f[i+1][j],f[i][j-1]);
            }
        }
        return Math.max(f[0][arr.length-1],s[0][arr.length-1]);
    }

    public static void main(String []args){
        int[]arr1={1,9,10};
        System.out.println("列表:1,9,10");
        System.out.println(win1(arr1));
        System.out.println(dp(arr1));
        // A是先手,B是后手
        int A=f(arr1,0,arr1.length-1);
        int B=s(arr1,0,arr1.length-1);
        System.out.println("玩家A得分:"+A);
        System.out.println("玩家B得分:"+B);
        if(A>B){
            System.out.println("玩家A获胜");
        }else if(A<B){
            System.out.println("玩家B获胜");
        }else{
            System.out.println("双方平局");
        }

        int[]arr2={1,9,2,4,9,13,12};
        System.out.println("列表:1,9,2,4,9,13,12");
        System.out.println(win1(arr2));
        System.out.println(dp(arr2));
        // A是先手,B是后手
        int A1=f(arr2,0,arr2.length-1);
        int B1=s(arr2,0,arr2.length-1);
        System.out.println("玩家A得分:"+A1);
        System.out.println("玩家B得分:"+B1);
        if(A1>B1){
            System.out.println("玩家A获胜");
        }else if(A1<B1){
            System.out.println("玩家B获胜");
        }else{
            System.out.println("双方平局");
        }

        int[]arr3={1,9,9,1};
        System.out.println("列表:1,9,9,1");
        System.out.println(win1(arr3));
        System.out.println(dp(arr3));
        // A是先手,B是后手
        int A2=f(arr3,0,arr3.length-1);
        int B2=s(arr3,0,arr3.length-1);
        System.out.println("玩家A得分:"+A2);
        System.out.println("玩家B得分:"+B2);
        if(A2>B2){
            System.out.println("玩家A获胜");
        }else if(A2<B2){
            System.out.println("玩家B获胜");
        }else{
            System.out.println("双方平局");
        }
    }
}

运行结果:

列表:1,9,10
11
11
玩家A得分:11
玩家B得分:9
玩家A获胜
列表:1,9,2,4,9,13,12
26
26
玩家A得分:24
玩家B得分:26
玩家B获胜
列表:1,9,9,1
10
10
玩家A得分:10
玩家B得分:10
双方平局

那么如何从暴力递归到动态规划?现在来展示一下代码的思路。

首先构造两张表,一张为f表,另一张为s表,对于arr[3,100,4,50],f表和s表构造如下

arr[3,100,4,50]
    0  1  2  3 
       f
    0 1 2 3 ->i
0         *
1
2
3
|
i

        s
    0 1 2 3 ->j
0         *
1
2
3
|
j

由于递归尝试模型,左下半区域一概无效:

arr[3,100,4,50]
    0  1  2  3 
       f
    0 1 2 3 ->i
0         *
1   x 
2   x x
3   x x x
|
i

        s
    0 1 2 3 ->j
0         *
1   x
2   x x
3   x x x
|
j

由于f函数中if(i==j){ return arr[j]; },s函数中if(i==j){ return 0; }所以对角线上的点可以直接填写值:

arr[3,100,4,50]
    0  1  2  3 
       f
    0    1    2    3    ->i
0   3              *
1   x   100
2   x    x    4
3   x    x    x    50
|
i

        s
    0    1    2    3    ->j
0   0              *
1   x    0
2   x    x    0
3   x    x    x    0
|
j

由于f函数中的上三角的区域依赖s函数的上三角区域:Math.max(arr[i]+s(arr,i+1,j),arr[j]+s(arr,i,j-1));由于s函数中的上三角的区域依赖f函数的上三角区域:Math.min(f(arr,i+1,j),f(arr,i,j-1))。

而f函数矩阵中的上三角区域的某一个点的值,是由s函数矩阵中上三角区域的左边与下方的点的值决定的;而s函数矩阵中的上三角区域的某一个点的值,是由f函数矩阵中上三角区域的左边与下方的点的值决定的。比方说s函数矩阵中的(0,1)点的值是由f函数矩阵中的(0,0)和(1,1)决定的,对应的,f函数矩阵中的(0,1)点的值是由s函数矩阵中的(0,0)和(1,1)决定的。也就是说,s矩阵的上三角对角线能够推f矩阵的倒数第二条上三角对角线,f矩阵的上三角对角线能够推s矩阵的倒数第二条上三角对角线;s矩阵的倒数第二条上三角对角线能够推f矩阵的倒数第三条上三角对角线,f矩阵的倒数第二条上三角对角线能够推s矩阵的倒数第三条上三角对角线;以此类推,直到推出矩阵的顶端(0,3)。

最后,能够推出动态规划的矩阵:

arr[3,100,4,50]

            f
    0    1    2    3    ->j
0   3   100   7   150
1   x   100  100  104
2   x    x    4    50
3   x    x    x    50
|
i


            s
    0    1    2    3    ->j
0   0    3   100    7
1   x    0    4    50
2   x    x    0     4
3   x    x    x     0
|
i


最终结果:

f[0][3] = 150
s[0][3] = 7

获胜者分数 = max(150,7) = 150

玩家A获胜。

5.象棋问题(马走日)

马初始在(0,0)位置,对应(x,y),给定(a,b)是马最终要去的位置,但是马一定要跳k步,问有多少种跳法。

y
|
9

8

7

6

5

4

3

2

1

0
    0    1    2    3    4    5    6    7    8    ->x

马初始在(0,0)位置,对应(x,y),给定(a,b)是马最终要去的位置,但是马一定要跳k步,问有多少种跳法。

分析,采用倒推的方法,也就是从(x,y)到(0,0),每一步都有8种走法。

y
|
9

8

7

6

5                        a        a
 
4                  a                   a

3                            p

2                  a                   a
                
1                       a         a

0
    0    1    2    3    4    5    6    7    8    ->x


比如,从(0,0)出发,,必须走8步,现在走了7步,到达a这个点
那么对于p(x,y)这个目标,再走一步就能到达,那么a的位置有多少种可能?

由于马走日的特性,我们发现有8种可能性。

举例:

y
|
9

8                           a          a           
    
7                       a                   a   

6                                 p
                                  |
5                       a         |         a
                                  |
4                            a    |    a
                                  |    
3                                 |
                                  |
2                                 |
                                  |  
1                                 |
                                  |
0                                 |
    0    1    2    3    4    5    6    7    8    ->x


从(0,0)出发,必须走10步,到达(6,6)这个位置,那么有多少种走法?
分别对应(x-1,y+2)(x+1,y+2)(x+2,y+1)(x+2,y-1)
        (x+1,y-2)(x-1,y-2)(x-2,y-1)(x-2,y+1)

也就是说:

第0层:h=0,初始化起点。

第1层:h=1,根据第0层计算。

第2层:h=2,根据第1层计算。

第3层:h=3,根据第2层计算。

……

第K层:h=K,根据第K-1层计算。

最后建立一张小三维表来说明:

y                               
|                                  
5             a        a
                                  
4       a                    a
                                     
3                  p              
                                  
2       a          |         a   

1             a    |    a         
                    
0                  |               
    0    1    2    3    4    5 ->x    

目标是(3,3),当k=3时,f(3,3,3),倒数一步为f(2,5,2),f(1,4,2)
那么如何从(0,0)实现两步就达到f(2,5,2)或f(1,4,2)?

最后建立了一个三维立方体,如图所示:

注意:这里只用两步就能实现。

示例代码:

package Class014;

public class Code_HorseJump {
    public static int getWay(int x,int y,int k){
        return process(x,y,k);
    }
    //跳过了多少步
    //x,y在的位置
    //从(0,0)出发跳必须跳step步之后,来到(x,y)位置的方法数有多少种
    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-1,y+2,step-1)
                +process(x+1,y+2,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-2,y-1,step-1)
                +process(x-2,y+1,step-1);
    }
    public static int dpWays(int x,int y,int step){
        if (x<0||x>8||y<0||y>9||step<0){
            return 0;
        }
        int[][][] dp=new int[9][10][step+1];
        dp[0][0][0]=1;
        for (int h=1;h<=step;h++){//层
            //枚举每一层
            for (int r=0;r<9;r++){
                for (int c=0;c<10;c++){
                    dp[r][c][h]+=getValue(dp,r-1,c+2,h-1);
                    dp[r][c][h]+=getValue(dp,r+1,c+2,h-1);
                    dp[r][c][h]+=getValue(dp,r+2,c+1,h-1);
                    dp[r][c][h]+=getValue(dp,r+2,c-1,h-1);
                    dp[r][c][h]+=getValue(dp,r+1,c-2,h-1);
                    dp[r][c][h]+=getValue(dp,r-1,c-2,h-1);
                    dp[r][c][h]+=getValue(dp,r-2,c-1,h-1);
                    dp[r][c][h]+=getValue(dp,r-2,c+1,h-1);
                }
            }
        }
        return dp[x][y][step];
    }

    //为了检验枚举的顺序性对答案毫无影响,只是层之间存在依赖关系,设置dpWays01函数
    public static int dpWays01(int x,int y,int step){
        if (x<0||x>8||y<0||y>9||step<0){
            return 0;
        }
        int[][][] dp=new int[9][10][step+1];
        dp[0][0][0]=1;
        for (int h=1;h<=step;h++){//层
            //枚举每一层
            for (int r=8;r>=0;r--){
                for (int c=0;c<10;c++){
                    dp[r][c][h]+=getValue(dp,r-1,c+2,h-1);
                    dp[r][c][h]+=getValue(dp,r+1,c+2,h-1);
                    dp[r][c][h]+=getValue(dp,r+2,c+1,h-1);
                    dp[r][c][h]+=getValue(dp,r+2,c-1,h-1);
                    dp[r][c][h]+=getValue(dp,r+1,c-2,h-1);
                    dp[r][c][h]+=getValue(dp,r-1,c-2,h-1);
                    dp[r][c][h]+=getValue(dp,r-2,c-1,h-1);
                    dp[r][c][h]+=getValue(dp,r-2,c+1,h-1);
                }
            }
        }
        return dp[x][y][step];
    }
    //相当于一个全是零的立方体,被分成许多层(step是层数)许多step平面堆叠成立方体(x,y,step)
    public static int getValue(int[][][] dp,int row,int col,int step){
        //越界就累加0
        if (row<0||row>8||col<0||col>9){
            return 0;
        }
        //不越界就累加它原来的值
        return dp[row][col][step];
    }
    public static void main(String [] args){
        int x=7;
        int y=7;
        int step=10;
        System.out.println(getWay(x,y,step));
        System.out.println(dpWays(x,y,step));
        System.out.println(dpWays01(x,y,step));
    }
}

运行结果:

515813
515813
515813

方法对比:

方法

时间复杂度

空间复杂度

暴力递归 process()

最坏 O(8^K)

O(K)

严格表结构DP dpWays()

O(9*10*K*8)

O(9*10*(K+1))

改变遍历顺序 dpWays01()

与 dpWays() 相同

与 dpWays() 相同

6.死亡越界

给定一个 N×M 的二维网格,Bob初始位于 (i,j)。每一步可以等概率地向上、下、左、右移动一格,共移动K步。

如果Bob在移动过程中越过网格边界,则立即死亡。求Bob完成K步后仍然存活的概率。

例如:N=10,M=10,i=3,j=2,K=5,最终存活概率为 945/1024。

代码:

package Class014;

public class Code_BobDie {
    //现在bob在row行,col列
    //区域是N*M的
    //如果bob在1走rest步的过程中2,都没有出圈,那么在走完的时候,bob就获得一个生存点
    //返回bob的生存点数
    public static int live(int N,int M,int row,int col,int rest){
        if (row<0||row>=N||col<0||col>=M){
            return 0;
        }
        if (rest==0){
            return 1;
        }
        int point=0;
        point+=live(N,M,row+1,col,rest-1);
        point+=live(N,M,row-1,col,rest-1);
        point+=live(N,M,row,col-1,rest-1);
        point+=live(N,M,row,col+1,rest-1);
        return point;
    }
    public static String bob1(int N,int M,int i,int j,int K){
        //所有可能的场景
        long all=(long) Math.pow(4,K);
        //存活下来的场景
        long live=process(N,M,i,j,K);
        //计算最大公约数,便于huaj
        long gcd=gcd(all,live);
        return String.valueOf((live/gcd)+"/"+(all/gcd));
    }
    //N*M的区域,Bob在(row,col)位置,走rest步之后,获得的生存方法数
    public static long process (int N,int M,int row,int col,int rest){
        if (row<0||row==N||col<0||col==M){
            return 0;
        }
        //row,col没越界
        if(rest==0){
            return 1;
        }
        //还没走完,row,col也没越界,上下左右移动存活的方法数累加
        //得到总体生存的方法数
        long live=process(N,M,row-1,col,rest-1);
        live+=process(N,M,row+1,col,rest-1);
        live+=process(N,M,row,col-1,rest-1);
        live+=process(N,M,row,col+1,rest-1);
        return live;
    }
    public static long gcd(long m,long n){
        return n==0?m:gcd(n,m%n);
    }
    public static String bob2(int N,int M,int i,int j,int K){
        int[][][]dp=new int[N+2][M+2][K+1];
        for (int row=1;row<=N;row++){
            for (int col=1;col<=M;col++){
                dp[row][col][0]=1;
            }
        }
        for (int rest=1;rest<=K;rest++){
            for (int row=1;row<=N;row++){
                for (int col=1;col<=M;col++) {
                    dp[row][col][rest]=dp[row-1][col][rest-1];
                    dp[row][col][rest]+=dp[row+1][col][rest-1];
                    dp[row][col][rest]+=dp[row][col-1][rest-1];
                    dp[row][col][rest]+=dp[row][col+1][rest-1];
                }
            }
        }
        long all=(long) Math.pow(4,K);
        long live=dp[i+1][j+1][K];
        long gcd=gcd(all,live);
        return String.valueOf((live/gcd)+"/"+(all/gcd));
    }
    public static void main(String[] args){
        int N=10;
        int M=10;
        int i=3;
        int j=2;
        int K=5;
        System.out.println(bob1(N,M,i,j,K));
        System.out.println(bob2(N,M,i,j,K));
    }
}

运行结果:

945/1024
945/1024

分析:

方法一:暴力递归

1. 定义递归状态

process(N,M,row,col,rest)

表示Bob当前处于 (row,col),还需要走 rest 步,在整个过程中不越界的合法走法数量。

2. 递归终止条件

(1)如果row<0、row>=N、col<0或col>=M,说明已经越界,返回0。
(2)如果rest==0且仍在网格内,说明成功完成所有步数,返回1。

3. 递归尝试过程

Bob每次可以向上下左右四个方向移动,因此:

process(row,col,rest)
    = process(row-1,col,rest-1)
    + process(row+1,col,rest-1)
    + process(row,col-1,rest-1)
    + process(row,col+1,rest-1)

每条路径的计算结果为0或正整数,四个方向的合法方法数相加,就是当前位置剩余 rest 步的总存活方法数。

4. 存活概率的计算

每一步有4种等概率选择,K步共有:

all=4^K

种可能的移动序列。

设 live 为不会越界的合法移动序列数,则:

$ P = \frac{\mathrm{live}}{4^K} $

例如K=5:

全部可能走法:4^5=1024
存活方法数量:945

存活概率=945/1024≈92.29%

使用 gcd(all,live) 求最大公约数,可以将最终概率化为最简分数。

5. 复杂度

每一步递归最多展开4个分支,递归深度为K,因此最坏时间复杂度为 O(4^K),递归栈空间复杂度为 O(K)。由于相同的 (row,col,rest) 可能重复出现,因此存在大量重复计算。

方法二:严格表结构动态规划

1. 建立三维DP表

递归过程中变化的参数只有行、列和剩余步数,因此可以建立:

int[][][] dp = new int[N+2][M+2][K+1];

其中 dp[row][col][rest] 表示从对应位置出发,继续走 rest 步仍然存活的方法数。

为了简化边界处理,原代码把实际网格坐标 (i,j) 映射为DP数组中的 (i+1,j+1),在网格外增加一圈值为0的边界。

2. 初始化第0层

for (int row=1; row<=N; row++) {
    for (int col=1; col<=M; col++) {
        dp[row][col][0]=1;
    }
}

当剩余步数为0时,只要Bob还在网格内,就有1种存活方式。因此第0层的所有有效位置均为1,网格外的边界保持0。

3. 状态转移

当前状态依赖上一层的四个相邻位置:

dp[row][col][rest]
    = dp[row-1][col][rest-1]
    + dp[row+1][col][rest-1]
    + dp[row][col-1][rest-1]
    + dp[row][col+1][rest-1]

如果某个相邻位置越界,对应的DP值为0,不会增加存活方法数。

4. 逐层填写DP表

从 rest=1 开始,依次计算到第K层。每一层都由上一层推导得到,同一层内部的状态互不依赖,因此行列的遍历顺序不会影响结果。

以 N=10,M=10,i=3,j=2,K=5 为例,实际起点对应DP数组中的 (4,3)。

第0层:dp[4][3][0] = 1
第1层:dp[4][3][1] = 4
第2层:dp[4][3][2] = 16
第3层:dp[4][3][3] = 63
第4层:dp[4][3][4] = 245
第5层:dp[4][3][5] = 945

其中第5层的计算为:

dp[4][3][5]
    = dp[3][3][4]
    + dp[5][3][4]
    + dp[4][2][4]
    + dp[4][4][4]

    = 236+246+209+254
    = 945

因此,最终返回:

long live = dp[i+1][j+1][K];

存活方法数为945,全部移动序列数为1024,得到:

\boxed{P=\frac{945}{1024}\approx92.29\%}

5. 复杂度

三维DP表共有约 N×M×K 个有效状态,每个状态只需要累加4个相邻位置,因此时间复杂度为 O(NMK),空间复杂度为 O(NMK)。

需要注意:原代码中的 bob2() 使用 int[][][] 存储路径数,当K较大时可能发生整数溢出,应改用 long[][][];对于更大的K,还需要考虑

两种算法对比

方法

时间复杂度

空间复杂度

暴力递归 bob1()

O(4^K)

O(K)

三维动态规划 bob2()

O(NMK)

O(NMK)

本题与前面的象棋马跳跃问题类似,都是通过累加不同方向的合法路径数来完成状态转移。区别在于:象棋问题每次有8种移动方向,并要求到达指定目标;Bob死亡越界问题每次有4种移动方向,只要求整个移动过程中不越界,最后再用存活方法数除以全部可能移动序列数,计算存活概率。

7.零钱兑换

给一个数组arr[],都是正数,无重复值,一个位置的值代表一个面值的货币,比如arr[3,5,10,2],要求拿任意个货币,凑够aim=1000的零钱数进行兑换,求一共有多少种方法。(leetcode322,518)

原始暴力递归代码:

package Class014;
public class Code_CoinsWays {
    //arr里都是正数,没有重复值,每一个值代表一种货币,每一种都可以用无限张
    //最终要找零钱数是aim,
    //找零钱方法数返回
    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){
        //basecase
        //没有方法可以选了,要求搞定的是0,则返回1;要求搞定的不是0,则返回0
        if (index==arr.length){
            return rest==0?1:0;
        }
        //arr[index]0张1张。。。不要超过rest的钱数
        int ways=0;
        for (int zhang=0;arr[index]*zhang<=rest;zhang++){
            ways+=process(arr,index+1,rest-arr[index]*zhang);
        }
        return ways;
    }
}

这种初始方法过于暴力,现在对它进行分析:

[3,5,10,1]
 0,1, 2,3

aim=1000

f(0,1000)
0代表3及其之后的货币可选,1000代表需要搞定的钱数

3:0张->f(1,1000)
3:1张->f(1,997)
3:2张->f(1,994)
......
3:333张->f(1,1)

以此类推

那么如何改造呢?基本情况,index==arr.length,确定一个中间位置(2,5),分析它需要依赖哪些点

[3,5,1,2]
 0,1,2,3

aim=10

    0    1    2    3    4    5    6    7    8    9    10
0                                                     *

1

2                            ?

3   n    n    n    n    n    n

4   1    0    0    0    0    0    0    0    0    0    0    

由于代码中的for (int zhang=0;arr[index]*zhang<=rest;zhang++){ways+=process(arr,index+1,rest-arr[index]*zhang);}

我们发现,一块钱货币做选择的时候,?(2,5)位置依赖index+1行,也就是第3行,rest-arr[index]*zhang列,张数是1,index=1,也就是(3,0)(3,1)(3,2)(3,3)(3,4)(3,5)这些点。

[3,5,1,2]
 0,1,2,3

aim=10

    0    1    2    3    4    5    6    7    8    9    10
0                                                     *

1                                           ?

2                  1                        0

3   

4   1    0    0    0    0    0    0    0    0    0    0    

五块钱货币做选择的时候,0张的时候对应(2,8),1张的时候对应(2,3)。其他位置以此类推。

不优化枚举:

 public static int way2(int[] arr,int aim){
        if (arr==null||arr.length==0){
            return 0;
        }
        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[index]*zhang<=rest;zhang++){
                    ways+=dp[index+1][rest-arr[index]*zhang];
                }
                dp[index][rest]=ways;
            }
        }
        return dp[0][aim];
    }

不优化时,枚举行为的时间复杂度=O(N*aim)*O(aim)=O(N*aim^2),那么枚举行为有必要吗?

如图所示,我们发现对于?位置j和x位置它们的方法数为:?=a+b+c+d+...,x=b+c+d+...,,?位置j和x位置只差了一个偏移量,既然算x时已经加过了,那么算?的方法数时只需要把x的方法数叠加,即?=x+a就行了。也就是一种斜率优化,用一种枚举行为用临近方法替代(四边形不等式)。


    0    1    2    3    4    5    6    7    8    9    10
0                                                     *

1                                      x    ?

2                            d    c    b    a

3   

4   1    0    0    0    0    0    0    0    0    0    0    

于是得到了第三种方法:

public static int way3(int[] arr,int aim){
        if (arr==null||arr.length==0){
            return 0;
        }
        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];
    }

优化后,枚举行为的时间复杂度=O(N*aim),时间复杂度明显下降。

代码:

package Class014;
public class Code_CoinsWays {
    //arr里都是正数,没有重复值,每一个值代表一种货币,每一种都可以用无限张
    //最终要找零钱数是aim,
    //找零钱方法数返回
    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){
        //basecase
        //没有方法可以选了,要求搞定的是0,则返回1;要求搞定的不是0,则返回0
        if (index==arr.length){
            return rest==0?1:0;
        }
        //arr[index]0张1张。。。不要超过rest的钱数
        int ways=0;
        for (int zhang=0;arr[index]*zhang<=rest;zhang++){
            ways+=process(arr,index+1,rest-arr[index]*zhang);
        }
        return ways;
    }
    public static int way2(int[] arr,int aim){
        if (arr==null||arr.length==0){
            return 0;
        }
        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[index]*zhang<=rest;zhang++){
                    ways+=dp[index+1][rest-arr[index]*zhang];
                }
                dp[index][rest]=ways;
            }
        }
        return dp[0][aim];
    }

    public static int way3(int[] arr,int aim){
        if (arr==null||arr.length==0){
            return 0;
        }
        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];
    }

    //for test
    public 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){
        System.out.println("hi");
        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 (way1(arr,aim)!=way2(arr,aim)||way2(arr,aim)!=way3(arr,aim)){
                System.out.println("ooops");
                break;
            }
        }
    }
}

运行结果:

hi

8.动态规划思路总结

尝试方法(左..右,范围),应对七成题目->记忆化搜索(列表分析表格的格子之间的关系)->严格表结构的动态规划->精细表结构的动态规划。

那么如何知晓哪一种尝试是好的?

(1)单可变参数维度(对于int整型来说,f(int)可以看作0维可变参数,即一个点,维度很低。如果函数变成f(int[]a,int x),这样维度就特别高,变成了1维,可能性猛增。前者的面试题目占绝对多数。)

(2)可变参数的个数(随个数上升,依次为线性数组,表格,立方体,可变参数越少,动态规划修改起来越简单)

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值