动态规划整理之一

动态规划整理之一

算法解释

      动态规划是一种自下而上的寻优遍历,它只能运用于有最优子结构的问题,就是说能够分解为局部的子问题来求解的问题。
      需要注意的是。动态规划和其他的深度/广度优先遍历一样都是将问题拆分为子问题来解决的。其中最本质的区别就在于动态规划在解决子问题的过程当中会不断的记录子问题的解,从而相比于递归,避免了大量的重复子问题的问题。而动态问题处理的关键就在于如何正确有效的列出状态转移方程,同时在存储记录的方面,还要考虑常用的采用滚动数组的方式来压缩空间。
      对于可以才分为子问题的题目一般来说都可以考虑自上而下的递归和自下而上的动态规划。其中对于递归会面临大量的重复计算子问题的状态,所以需要掌握合理的去重和剪枝操作,比如可以使用哈希表来存储子问题的解,以及简化每次递归的分支。对于动态规划的话,就是注意如何从一个状态推导到下一个状态,并且采用滚动数组等方法来优化空间。同时,如果问题需要记录最终解的路径,比如方案中具体的选择等,这个时候考虑使用递归回溯会好一些,如果只是追求最后的结果,那么使用动态规划无疑是更佳的选择。

经典例题(时刻考虑空间的压缩问题)

基本动态规划:一维

典中典的入门级别的动态规划问题 爬楼梯 leetcode 70

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。 示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。 1 阶 + 1 阶 || 2 阶

示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1 阶 + 1 阶 + 1 阶 || 1 阶 + 2 阶 || 2 阶 + 1 阶

class Solution {
public:
    //递归的话超时了,考虑用其他的方法解决问题
    //这道题是一道经典的动态规划的一维问题
    int climbStairs(int n) {
    if(n<=2) return n;
    vector<int>dp(n+1,1);
    for(int i = 2;i<=n;++i)
    {
      dp[n] = dp[n-1] + dp[n-2];
    }
    return dp[n];
    }
};

      状态方程就是说,我当前的状态是如何通过过去的状态得来的,当前走了m阶楼梯,那一定是前面的m-1阶时走了一步或者说m-2阶时走了两步。同时也要注意临界初始条件,0阶,1阶的时候的取值。
      最后再考虑对于整体的算法的优化,可以看到i,i-1,i-2的关系,明显的是当前的值只是和过去的两个值有关,所以完全可以用两个不断的更新的值去代替。

class Solution {
public:
    //递归的话超时了,考虑用其他的方法解决问题
    //这道题是一道经典的动态规划的一维问题
    int climbStairs(int n) {
    if(n<=2) return n;
    int pre1 = 1,pre2 = 2,cur;
    for(int i = 2;i < n;++i)
    {
        cur = pre1+pre2;//这里需要小心交换的顺序
        pre1 = pre2;
        pre2 = cur;
    }
    return cur;
    }
};

      屋子的抢劫问题也是很经典

基本动态规划:二维

leetcode 64 最小路径和

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]] 输出:7 解释:因为路径1→3→1→1→1 的总和最小

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
    for(int i = 0;i<grid.size();i++)
    {
        for(int j = 0;j<grid[0].size();j++)
        {
           //这里主要是对于第一行和第一列的特殊情况进行处理
           //第一行的只能是左边的路线向右产生的
           //第一列的只能是上边的路线向下产生的
            if(i==0&&j!=0)
            {
             grid[i][j] += grid[i][j-1];
            }
            else if(j==0&&i!=0)
            {
             grid[i][j] += grid[i-1][j];
            }
            else if(j!=0&&i!=0)
            {
             grid[i][j] += min(grid[i-1][j],grid[i][j-1]);
            }
        }
    }
     return grid[grid.size()-1][grid[0].size()-1];
    } 
};

      对于二维的动态规划问题,我们一定要考虑滚动数组来进行空间的压缩,当然我在这里并没有额外的设置数组来存放数据,而是直接在原来的基础上进行更新的.

leetcode 542 01矩阵

给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。两个相邻元素间的距离为 1 。
示例 1:
输入:
[[0,0,0],
[0,1,0],
[0,0,0]]
输出:
[[0,0,0],
[0,1,0],
[0,0,0]]

class Solution {
public:
    vector<vector<int>> updateMatrix(vector<vector<int>>& matrix) {
    int n = matrix.size(),m = matrix[0].size();
    queue<pair<int,int>>q;
    //超时,n3复杂度
    for(int i = 0;i<n;i++)
    {
     for(int j = 0;j<m;j++)
        {
           if(matrix[i][j] == 0)
           {
               q.push(make_pair(i,j));
           }  
        }
    }
    int level = 1;
    while(!q.empty())
    {
        int num = q.size();
        for(int k = 0;k<num;k++)
        {
            auto [i,j] = q.front();
            q.pop();
            if(j-1>=0&&matrix[i][j-1]>0)
            {
                q.push(make_pair(i,j-1));
                matrix[i][j-1] = -level;
            }
            if(j+1<m&&matrix[i][j+1]>0)
            {
                q.push(make_pair(i,j+1));
                matrix[i][j+1] = -level;
            }
            if(i-1>=0&&matrix[i-1][j]>0)
            {
                q.push(make_pair(i-1,j));
                matrix[i-1][j] = -level;
            }
            if(i+1<n&&matrix[i+1][j]>0)
            {
                q.push(make_pair(i+1,j));
                matrix[i+1][j] = -level;
            }
        }
        ++level;
    }
    for(int i = 0;i<n;i++)
    {
        for(int j = 0;j < m;j++)
        {
            matrix[i][j] *= -1;
        }
    }
    return matrix;
    }
};

      这是一种思路,那就是直接采取广度优先的思路来进行解题,但是为了做好标记和存储,都是在原来的基础上进行的,首先是遍历一遍整体的数组,将矩阵位置为零的坐标压入设置好的队列之中,然后不断的从这些0出发,采用广度优先遍历的思路向这个位置的四个未被标记的位置(已标记是小于等于0)延伸,把层数的信息包含进来的同时,也记录访问的情况,减少问题的复杂度。
      当然这里也有另一个思路,那就是将四个方向的变化转化为两次双向的变化,从左上到右下进行一次动态搜索,再从右下到坐上进行一次动态搜索。

class Solution {
public:
    vector<vector<int>> updateMatrix(vector<vector<int>>& matrix) {
    if(matrix.empty()) return {};
    int m = matrix.size(),n = matrix[0].size();
    vector<vector<int>>dp(m,vector<int>(n,INT_MAX-1));//为什么要减一,这是为之后初始化加一预留位置,防止溢出
    //这里不考虑第一行第一列的关键就在于,我们是二次翻转的,第一次从左上到右下的第一行第一列对于第二次从右下到左上来说,是最后一行,最后一列,是不用做特殊处理的
    //第一次从左上到右下,这样每一个零到一的两个方向的位置就知道了(就是leetcode 64 的这个变形问题)
    for(int i = 0;i<m;++i)
    {
        for(int j = 0;j<n;++j)
        {
            if(matrix[i][j]==0)
            {
                dp[i][j] = 0;
            }
            else
            {
                if(j>0)
                {
                   dp[i][j] = min(dp[i][j-1]+1,dp[i][j]);
                }
                if(i>0)
                {
                   dp[i][j] = min(dp[i-1][j]+1,dp[i][j]);
                }
            }
        }
    }
    //第二次从右下到左上,这样第二个两个方向就知道了
    for(int i = m-1;i>=0;--i)
    {
        for(int j = n-1;j>=0;--j)
        {
            if(matrix[i][j]==0)
            {
                dp[i][j] = 0;
            }
            else
            {
                if(j<n-1)
                {
                   dp[i][j] = min(dp[i][j+1]+1,dp[i][j]);
                }
                if(i<m-1)
                {
                   dp[i][j] = min(dp[i+1][j]+1,dp[i][j]);
                }
            }
        }
    }
    return dp;
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值