动态规划

动态规划

这类题目有许多共同的特点,我们需要理解问题的含义,并且加以梳理和分解,我们先来看一道题目,大家都比较熟悉的爬楼梯的问题。

爬楼梯

题目描述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼梯顶呢?
在这里插入图片描述
这个时候我们就会想,如果只有2个台阶,我们就可以先爬一级再爬一级或者一次爬2级,所以有2种方法,但是如果台阶再多一些呢,100级?200级?显然我们不能从第一级开始推理,反过来,当我们还有最后一次机会时,会是以什么样的方式成功登顶呢,很容易想到:1,跨一级登顶 2,跨2级登顶。那么我们最后登顶的方法(N)就可以写成:(N-1)+(N-2)。同理,我们到N-1级的方法可以是:(N-2)+(N-3),而到N-2级的方法是:(N-3)+(N-4),这样我们可以推理出一个公式:
在这里插入图片描述
这时很容易想到使用递归来实现这个算法:

public int climb(int n) {
	if(n == 1) return 1;//为1级台阶
	if(n == 2) return 2;//2级台阶
	return climb(n-1) + climb(n-2);//n级台阶=(n-1)级+(n-2)级
}

但是这个写法不建议使用,因为做了太多的重复计算,如下为计算n级台阶需要计算的其他的台阶数:
在这里插入图片描述
这个时候我们就可以用数据结构储存我们已经计算过的台阶,因此很容易想到1维数组:


class Solution {
    public int climbStairs(int n) {
        if(n<=2) return n;
        int[] num=new int[n+1];//我们从1下标开始,所以要初始化比n大1
        num[1]=1;//初始化1级台阶
        num[2]=2;//初始化2级台阶
        climb(num,n);
        return num[n];
    }
    void climb(int[] num, int n){
        if(n-1>2 && num[n-1]==0) climb(num, n-1);
        if(n-2>2 && num[n-2]==0) climb(num, n-2);
        num[n] = num[n-1]+num[n-2];
    }
}

通过这种方法,我们的算法在效率上会高很多。
其实,这就是一种动态规划的算法思想。

下面我来说一说动态规划具体是怎么解决这类问题的:
在这之前现需要介绍一下动态规划;动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。
其实当我们这类问题遇到多了之后,会发现它有一些共性:

  • 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
  • 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
  • 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。

通常我们需要创建一个一维数组或者二维数组来储存中间数据。然后通过将问题拆分成多个类似的小问题,定义问题状态和状态之间的关系,从而使得问题能够以递推(或者分治)的方式去解决;动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

接下来给一些具体的例子:

机器人迷宫

题目描述:
一个机器人位于一个 m x n 网格的左上角 。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。
问总共有多少条不同的路径?
在这里插入图片描述

这种题目分析之后,发现属于动态规划的这类问题,因此先得出需要一个2维数组储存数据int[m][n]。机器人走的每一步可以由下走一步或者右走一步得来,因此反过来思考最后一步的得来,是由END格子上面的加上左边的方法次数相加,即int[m][n]=in[m-1][n]+int[m][n-1],可写出代码逻辑:

class Solution {
    public int robotLoad(int m, int n) {
        int[][] load=new int[m][n];
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(i==0) load[0][j]=1;
                else if(j==0) load[i][0]=1;
                else load[i][j] = load[i-1][j] +load[i][j-1];
            }
        }
        return load[m-1][n-1];
    }
}

但是,这样写算法的时间复杂度为O(n2),空间复杂度为O(n2),不是特别的理想。通过观察:
在这里插入图片描述
因此,我们可以得出结论:
可以将2维数组的int[m][n]=in[m-1][n]+int[m][n-1]公式用1维数组代替:int[n]=int[n]+int[n-1]
这样我们可以考虑把2维数组用1维数组代替,算法将变为:

class Solution {
    public int robotLoad(int m, int n) {
        int[] load=new int[n+1];
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(j==0 || i==0) load[j]=1;//边上的步数都为1
                else load[j]+=load[j-1];
            }
        }
        return load[n-1];
    }
}

其实这个问题还有一种更简单的解法。
在这里插入图片描述
现在我们来仔细观察一下这个m*n的网格图,机器人要从最左上角走到最右下角,那么机器人的竖直方向上一定要走(n-1)步,在水平方向上一定会走(m-1)步,总共的步数一定是(m+n-2)步,这样我们就可以用排列组合来解决这个问题,这个是无顺序的组合,所以我们可以先在(m+n-2)中选出无序个m-1,代表着哪一步向水平方向走一步,然后剩下的就是竖直方向的步数了,因此公式可以写成:
接着用代码实现了一下:

class Solution {
    public int robotLoad(int m, int n) {
        return molecule(m+n-2, m-1)/denominator(m-1);
    }
    //分子
    public int molecule(int a, int b) {
        int result = 1;
        for(int i=0;i<b;i++){
            result*=a--;
        }
        return result;
    }
    //分母
    public int denominator(int a) {
        int result = 1;
        while(a>1){
            result*=a--;
        }
        return result;
    }
}

但是运行的时候总是报如下错误:

java.lang.ArithmeticException: / by zero

找了许久,发现当阶乘的数有点大的时候,result会超过了int的上限231-1, 相当于左移34位超过了int的位数32位,所以就变成0了。

进一步加大难度,如果在线路中间加上一个障碍物,机器人不能通过,即:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
其实不难,因为障碍物不能通过,那么存在障碍物的位置的代表到达方法路径和为0即可,因此可得:
图
写代码时,判断是否是障碍物,是的话设置为0即可:

class Solution {
    public int robotLoad(int[][] obstacleLoad) {
        int n=obstacleLoad.length;//m*n二维数组
        int m=obstacleLoad[0].length;
        int[] dp=new int[n];
        dp[0]=1;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(obstacleGrid[j][i] == 1) dp[j]=0;
                else dp[j]=dp[j]+dp[j-1];
            }
        }
        return dp[n-1];
    }
}

接着下一题:

  • 给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。(回文串是指这个字符串无论从左读还是从右读,所读的顺序是一样的;简而言之,回文串是左右对称的。所谓最长回文子串问题,是指对于一个给定的母串)
    例子:
    给出:‘ecbcw’
    答案:‘cbc’

思路:
将字符串s倒置为s*,然后与s进行对比,有相同的最长部分即位答案;因此,申请一个二维的数组arr初始化为 0,然后判断s与s*对应的字符是否相等,相等的话arr [ i ][ j ] = arr [ i - 1 ][ j - 1] + 1 。当 i = 0 或者 j = 0 的时候单独分析,字符相等的话 arr [ i ][ j ] 就赋为 1 。arr [ i ][ j ] 保存的就是公共子串的长度。
在这里插入图片描述
写代码如下:

public String longestHw(String s) {
    String str= s;
    String reverse = new StringBuffer(s).reverse().toString(); //字符串倒置
    int length = s.length();
    int[][] arr = new int[length][length];
    int maxLen = 0;//最大长度
    int maxEnd = 0;//最大长度的结束下标
    for (int i = 0; i < length; i++)
        for (int j = 0; j < length; j++) {
            if (str.charAt(i) == reverse.charAt(j)) {
                if (i == 0 || j == 0) {
                    arr[i][j] = 1;//i或者j=0时,元素相同赋1
                } else {
                    arr[i][j] = arr[i - 1][j - 1] + 1;
                }
            }
            /*判定当前是否为最长的*/
            if (arr[i][j] > maxLen) { 
                maxLen = arr[i][j];
                maxEnd = i; //以 i 位置结尾的字符
            }
        }
	}
	return s.substring(maxEnd - maxLen + 1, maxEnd + 1);
}

当然上面的是一种动态规划的解法,这里给出另外一种解法。
回文串有一个特点,它是从中间位置对称的,我们可以利用这一点来解题;即用2个指针,从中间开始向左右分别扩散,每一次都判断当前扩散位置的元素是否相同,并且每次扩散到最大时,2个指针重新右移再开始扩散,进而找到最长解。图解如下:
在这里插入图片描述
回文串也分为2种,一种是以元素对称,一种是长度对称
在这里插入图片描述
因此写代码时要注意:

public String longestHw(String s) {
    int start = 0, end = 0;//定义2个指针
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);//以元素对称
        int len2 = expandAroundCenter(s, i, i + 1);//以长度对称
        int len = Math.max(len1, len2);
        if (len > end - start) {//判断是否是最长长度
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {//当2个下标元素相同时继续扩散
        L--;
        R++;
    }
    return R - L - 1;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值