算法-动态规划

123 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

\

动态规划理解

例子1-斐波那契数列

动态规划就是利用空间换时间

以斐波那契数列为例

static int f(int n){
        if(n == 1)
            return 1;
        if(n == 2)
            return 1;
        return f(n-1)+f(n-2);
    }

​编辑

当想要求解f(6)时, f(4),f(3)需要求解多遍,很浪费时间,如果把第一遍的求解结果直接存起来,再次需要求解时直接调用,就可以节省时间了。

如果求f(6)函数的调用过程为:f(6)->f(5)->f(4)->f(3)->f(2)->f(1)->f(1)->f(2)->f(3)->f(4),复杂度是O(n),而递归方式的斐波那契数列复杂度是O(2^n)

例子2

​编辑

利用递归处理

public class Main1 {
    public static void main(String[] args) {
        System.out.println(f(4,2,4,4));
    }
    static int f(int n,int start,int aim,int k){
        return process1(start,k,aim,n);
    }
    //机器人现在在cur位置
    //还有rest步
    //需要走到aim位置
    //一共有1-2个位置
    //返回机器从cur走rest步后,停到aim的方法数
    static int process1(int cur,int rest,int aim,int n){
        if(rest == 0){//没有步数了,如果此时在aim位置,就说明有一种方法
            return cur==aim ? 1 : 0;
        }
        //rest > 0 还需要继续走
        if(cur == 1){ //只能走到第二个位置
            return process1(2,rest-1,aim,n);
        }
        if(cur == n){ //只能走到倒数第二个位置
            return process1(n-1,rest-1,aim,n);
        }
        //中间位置
        return process1(cur-1,rest-1,aim,n) + process1(cur+1,rest-1,aim,n);
    }
}

优化1:将重复计算的值缓存起来

​编辑

在7位置还有10步等价于在6位置还有9步加在8位置还有9步,此过程出现很多重复计算,可以将已经计算过的数缓存起来

public class Main1 {
    public static void main(String[] args) {
        System.out.println(f(4,4,2,4)); //一共4个位置,从位置2走4步走到位置4的走法
    }
    static int f(int n,int k,int begin,int aim){
        int [][] table= new int[n+1][k+1]; //改表用于缓存计算过的结果
        for(int i = 0; i < n+1; i++)
            for(int j = 0; j < k+1; j++)
                table[i][j] = -1; //某个位置的值为-1代表这个位置没被计算过

        return process(table,begin,k,aim,n); //返回问题最终结果
    }
    //当前位置cur的范围是1-n
    //剩余步数rest的范围是0-k
    //将这些取值放到table[n+1][k+1]中
    static int process(int[][] table,int cur,int rest,int aim,int n){
        //之前计算过
        if(table[cur][rest] != -1)
            return table[cur][rest]; //计算过就直接返回值
        //之前没有计算过
        int ans = 0; //存储最终方法数的结果
        if(rest == 0){ //如果剩余步数为0
            ans = cur == aim ? 1 : 0;
        }
        else if(cur == 1){ //如果当前在第一个位置
            ans = process(table,2,rest-1,aim,n);
        }
        else if(cur == n){ //如果当前在最后一个位置
            ans = process(table,n-1,rest-1,aim,n);

        }else { //如果当前在中间
            ans = process(table,cur-1,rest-1,aim,n) + process(table,cur+1,rest-1,aim,n);
        }
        table[cur][rest] = ans; //将计算结果缓存到表中
        return ans;
    }
}

优化2:可以根据递归过程直接将二维表画出

根据第一个语句

if(rest == 0){//没有步数了,如果此时在aim位置,就说明有一种方法
            return cur==aim ? 1 : 0;
        }

当step等于0时,只有当cur等于目标位置时,才填入1,其他都填入0,所以可以填好第一列

根据第二个语句

        if(cur == 1){ //只能走到第二个位置
            return process1(2,rest-1,aim,n);
        }
        if(cur == n){ //只能走到倒数第二个位置
            return process1(n-1,rest-1,aim,n);
        }

当cur为1或n时,分别等于cur为2,step-1和cur为n-1,step-1位置的数,也就是第一行都依赖于左下,最后一行都依赖于左上

根据第三个语句

 return process1(cur-1,rest-1,aim,n) + process1(cur+1,rest-1,aim,n);

当处于中间时,分别依赖左上和左下

​编辑

 可以通过分析直接得出左右情况的缓存表,可以直接查询想要的信息

static int process2(int n,int k,int aim,int start){
        int [][] table= new int[n+1][k+1];
        for(int rest = 0; rest <= k; rest++){ //列
            for(int cur = 1; cur <= n; cur++){ //行
                if(rest == 0){
                    if(cur != aim){
                        table[cur][rest] = 0;
                    }else {
                        table[cur][rest] = 1;
                    }
                }else {
                    if(cur == 1){
                        table[cur][rest] = table[cur+1][rest-1];
                    }
                    else if(cur == n){
                        table[cur][rest] = table[cur-1][rest-1];
                    }
                    else {
                        table[cur][rest] = table[cur-1][rest-1] + table[cur+1][rest-1];
                    }
                }
            }
        }
        return table[start][k];
    }

代码优化

static int process3(int n,int k,int aim,int start){
        int [][] table = new int[n+1][k+1];
        table[aim][0] = 1;
        for(int rest = 1; rest <=k; rest++){
            table[1][rest] = table[2][rest-1];
            for(int cur = 2; cur < n; cur++){
                table[cur][rest] = table[cur-1][rest-1]+table[cur+1][rest-1];
            }
            table[n][rest] = table[n-1][rest-1];
        }
        return table[start][k];
    }

最终思路

起始位置:start

目标位置:aim

位移数:k

位置个数:n

当前位置:cur

剩余步数:rest

在递归中发现只有cur和rest是变化的,所以根据cur和rest画一个二维表,表的大小由n和k限制,绘制完后,每个横坐标都可看作是一个start,列都可以看作是一个k,可以根不同的start和key直接查询出到达终点aim的方法数。

例子3

​编辑

public class Main2 {
    public static void main(String[] args) {
        int[] a = {5,7,4,5,8,1,6,0,3,4,6,1,7};
        int first = before(a,0,a.length-1);
        int second = after(a,0,a.length-1);
        System.out.println(Math.max(first,second));
    }
    //先手
    static int before(int[] a,int L,int R){
        //只有一张牌
        if(L==R)
            return a[L];
        int degree1 = a[L] + after(a,L+1,R); //先手拿L位置的牌
        int degree2 = a[R] + after(a,L,R-1); //先手拿R位置的牌
        //返回最大值
        return Math.max(degree1,degree2);
    }
    //后手
    static int after(int[] a,int L,int R){
        //只有一张牌,并且是后手拿,那么等于拿不到牌
        if(L == R)
            return 0;
        int degree1 = before(a,L+1,R); //对手拿走了L位置的牌
        int degree2 = before(a,L,R-1); //对手拿走了R位置的牌
        //返回最小值
        return Math.min(degree1,degree2);
    }
}

\