算法设计--动态规划

1. 动态规划

动态规划(dynamic programming)是一种动态地求解问题的解的算法,其核心在于动态二字,就是问题的最优解依赖于子问题的最优解,因此要求原问题就要知道子问题的解,还要知道原问题和子问题之间的关系,也就是递推方程。尽管动态规划听起来很容易用递归实现,但是实际上往往我们使用的是迭代的方法,因为递归过程有很多重复的子问题,造成时间复杂度过高。
递推方程是核心,动态规划问题最重要的就是找出正确的递推方程。
子问题划分是首要,如果你的子问题都没定义明白,那就不要想得到递推方程了。
哈哈,上面两句话是模仿政治书里的语言。写完一读,嘿,还挺是那么回事,自夸一波。
在这里插入图片描述

2. 矩阵链相乘问题

给定矩阵序列: M 0 M 1 M 2 M 3 . . . M n − 1 M_0M_1M_2M_3...M_{n-1} M0M1M2M3...Mn1
和矩阵的大小:
m 0 m 1 m 2 m 3 . . . m n m_0m_1m_2m_3...m_n m0m1m2m3...mn
其中矩阵Mk的大小为mk*mk+1
求这些矩阵做乘法运算时需要的最小运算次数时的运算顺序。
矩阵链相乘的子问题是:counter[i][j],表示矩阵序列MiMi+1Mi+2…Mj相乘的最少次数。
初值:i等于j时,只有一个矩阵,counter[i][j]=0;
递推方程 c o u n t e r [ i ] [ j ] = m i n ( c o u n t e r [ i ] [ k ] + c o u n t e r [ k + 1 ] [ j ] + m i ∗ m k + 1 ∗ m j + 1 ∣ k ∈ [ i , j − 1 ] ) counter[i][j] = min(counter[i][k] + counter[k+1][j] + m_i*m_{k+1}*m_{j+1} | k\in [i, j-1]) counter[i][j]=min(counter[i][k]+counter[k+1][j]+mimk+1mj+1k[i,j1])
这些都有了就可以实现了:

//    写一下动态规划的矩阵乘法问题
public String dp(int[] matrixSize, String matrix){
    long[][] count = new long[matrix.length()][matrix.length()];
    int[][] partition = new int[matrix.length()][matrix.length()];
    //初始化
    for (int i = 0; i < count.length; i++) {
        for (int j = 0; j < count.length; j++) {
            if (i == j) count[i][j] = 0;
            if(i == j-1) count[i][j] = matrixSize[i]*matrixSize[j]*matrixSize[j+1];
        }
    }
    for (int k = 2; k < count.length; k++) {//遍历矩阵序列长度(为了便于计算,-1)
        for (int i = 0; i < count.length - k; i++) {//遍历矩阵序列开头
            long min_ = Long.MAX_VALUE;
            int par = -1;
            for (int j = i; j < i+k; j++) {//遍历划分位置
                long c = count[i][j] + count[j+1][i+k] + matrixSize[i]*matrixSize[j+1]*matrixSize[i+k+1];
                if(c < min_){
                    min_ = c;
                    par = j;
                }
            }
            count[i][i+k] = min_;
            partition[i][i+k] = par;
        }
    }
    return fun(matrix, partition, 0, matrix.length()-1, 0);
}
public String fun(String matrix, int[][] partition, int left, int right, int offset){
    if(right - left < 2) return matrix;
    int par = partition[left][right];
    String lstr = matrix.substring(left-offset, par+1-offset);
    String rsrt = matrix.substring(par+1-offset);
    String l = fun(lstr, partition, left, par, offset);
    String r = fun(rsrt, partition, par+1, right, par+1);
    if (l.length() > 1) l = "(" + l + ")";
    if (r.length() > 1) r = "(" + r + ")";
    return l + r;
}

3. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
子问题: 假设word1长为m,word2长为n。定义oper[i][j]为子串word1[0:i]转换成word2[0:j]所需要的最小操作数。
初值: 当i=0时,j从0变到n,表示word1的子串只有第一个字符,变成word2的子串。如果word2子串中有字符与word1第一个字符相同的,操作数就等于子串长度减1,否则就等于子串长度;当j=0时同理。
递推方程:

if(word1.charAt(i) == word2.charAt(j)){
    oper[i][j] = Math.min(oper[i-1][j-1], Math.min(oper[i-1][j]+1, oper[i][j-1]+1));
}else{
    oper[i][j] = Math.min(oper[i-1][j-1],Math.min(oper[i-1][j], oper[i][j-1])) + 1;
}

最终的实现:

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        if(m == 0) return n;
        if(n == 0) return m;
        int[][] oper = new int[m][n];
        //初始化
        int flag = 0;//一旦出现相同的字符,flag就变成1
        for(int i = 0; i < m; i++){
            if(word1.charAt(i) == word2.charAt(0)){
                flag = 1;
                oper[i][0] = i+1 - flag;
            } else oper[i][0] = i+1 - flag;           
        }
        //注意这里的初始化稍稍复杂一点点
        //如果出现过相同的字符,那么操作数就会比子串长度少一
        flag = 1 - oper[0][0];
        for(int j = 1; j < n; j++){
            if(word2.charAt(j) == word1.charAt(0)){
                flag = 1;
                oper[0][j] = j+1 - flag;
            }else{
                oper[0][j] = j+1 - flag;
            }
        }

        //
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                if(word1.charAt(i) == word2.charAt(j)){
                    oper[i][j] = Math.min(oper[i-1][j-1], Math.min(oper[i-1][j]+1, oper[i][j-1]+1));
                }else{
                    oper[i][j] = Math.min(oper[i-1][j-1],Math.min(oper[i-1][j], oper[i][j-1])) + 1;
                }
                
            }
        }
        return oper[m-1][n-1];
    }
}

这是一道力扣的难题,可以看到其实还是不难的。

4. 递归实现?

因为这里的子问题划分和原问题具有相同的性质,因此我们想到了递归,但是递归可以吗?
在这里插入图片描述
上面是力扣的一道中等题,其实算是简单题。
如果用递归代码很简单:

class Solution {
    public int uniquePaths(int m, int n) {
        if(m < 1 || n < 1) return 0;
        if(m == 1 && n == 1) return 1;
        return uniquePaths(m, n-1) + uniquePaths(m-1, n);
    }
}

作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/unique-paths/solution/kong-jian-huan-shi-jian-sha-ye-bu-shuo-liao-by-fan/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

但是很不幸,超时了,因为什么呢?
在这里插入图片描述
从上图可以看到,递归的时候有大量的重复子问题需要计算,耗费了时间。
因此在使用动态规划时,一般都是用数组将前面子问题的解保存起来,然后再进行更大子问题的计算。
通过的代码:

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] count = new int[m+1][n+1];
        for(int i = 1; i < n+1; i++) count[1][i] = 1;
        for(int i = 1; i< m+1; i++) count[i][1] = 1;
        for(int i = 2; i <= m; i++){
            for(int j = 2; j <= n; j++){
                count[i][j] = count[i-1][j] + count[i][j-1];
            }
        }
        return count[m][n];
    }
}

作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/unique-paths/solution/kong-jian-huan-shi-jian-sha-ye-bu-shuo-liao-by-fan/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

动态规划先介绍到这里。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值