算法之路:动态规划(一)

简介: 学习动态规划后写的练习题。

1.何为动态规划

动态规划(Dynamic Programming)是动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

动态规划具备以下三个特点:

1. 把原来的问题分解成了几个相似的子问题。

2. 所有的子问题都只需要解决一次。

3. 储存子问题的解。

动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)。

在思考动态规划问题的时候,可以从这四个方向去思考:

1. 状态定义。

2. 状态间的转移方程定义。

3. 状态的初始化。

4. 返回结果。

对于状态定义的要求:定义的状态一定要形成递推关系。

动态规划一般的场景:适用场景:最大值/最小值, 可不可行, 是不是,方案个数。

2.题目练习

2.1、字符串分割

题目描述:

给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。

例如:

给定s=“nowcode”;

dict=["now", "code"].

返回true,因为"nowcode"可以被分割成"now code".

思路分析:

这个问题,问的就是这个字符串s是否能够被分割。

状态定义:我们可以用F(i)代表前i个字符串是否可以被分割。

状态转移方程:我们先来分析一下分割的过程以及判断是否可以被切割的结果。

F(3):前3个字符可以被切割,因为它是"now",在字典中可以被找到。于是判定为true

F(7):F(3)&&[4,7]这段字符串是否可以被切割,可以的话,就是true;

设i和j,于是有以下判断过程:

F(1)&&[2,7];

F(2)&&[3,7];

F(4)&&[5,7];

F(5)&&[6,7];

F(6)&&[7,7];

F(0)&&[1,8]为一个整体。

通过上面的判断过程,就可以推出状态转移方程为:

F(i): true{ j <i && F(j) && substr[j+1,i]能在词典中找到 } OR false。在j小于i中,只要能找到一个F(j)为true,并且从j+1到i之间的字符能在词典中找到,则F(i)为true。

初始值:我们使用vector作为容器,用来保存判断的结果。对于初始值无法确定的,可以引入一个不代表实际意义的空状态,作为状态的起始空状态的值需要保证状态递推可以正确且顺利的进行,到底取什么值可以通过简单的例子进行验证。因此初始化为F(0)=true;

返回值:F(s.size());

代码如下:

class Solution {
public:
    bool wordBreak(string s, unordered_set<string> &dict) {
        if(s.empty())
        {
            return false;
        }
        if(dict.empty())
        {
            return false;
        }
        vector<bool> can_break(s.size()+1,false);
        //初始化为true
        can_break[0] = true;
        for(int i = 1;i<=s.size();++i)
        {
            for(int j = i-1;j>=0;--j)
            {
                if(can_break[j] && dict.find(s.substr(j,i-j))!=dict.end())
                {
                    can_break[i]=true;
                    break;
                }
            }
        }
        return can_break[s.size()];
    }
};

image.gif

2.2、三角矩阵

题目描述:

给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字。

例如,给出的三角形如下:

[[2],[3,4],[6,5,7],[4,1,8,3]]

最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。

注意:

如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。

思路分析:

0B([O}9EST5}8GS$@NV}C~0.png

状态:F[i,j]表示从(0,0)到(i,j)的最小路径和。

状态转移方程F(i,j):

由于在边缘部分的路径,只能从它的上一级走来,而中间的路可以由上一级的两个路径和中选择一个最小的。因此,分成两种情况:

①当j==0或者j==i,即在边缘部分的路,其状态转移方程F(i,j):

j==0:F(i-1,0)+F(i,j);

j==i:F(i-1,j-1)+F(i,j);

②中间的路,其状态转移方程为F(i,j) = min( F(i-1,j), F(i-1,j-1) ) + F(i,j);

初始状态:初始时,就F(0,0)自己。F(0,0) = triangle[0][0];

返回结果:在结果集中最后一行找到最小的那个。min(F(row-1, i));

代码如下:

class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
        if(triangle.empty())
        {
            return 0;
        }
        //拷贝构造,对F[0][0]和F[i][j]初始化
        vector<vector<int>> result(triangle);
        int row = 0,col = 0;
        for(row = 1;row<result.size();++row)
        {
            for(col = 0;col<=row;++col)
            {
                //边界
                if(col==0)
                {
                    result[row][col] = result[row-1][0]+result[row][col];
                }
                else if(col==row)
                {
                    result[row][col] = result[row-1][col-1]+result[row][col];
                }
                else //非边界
                {
                    result[row][col] = min(result[row-1][col],result[row-1][col-1])+result[row][col];
                }
            }
        }
        //取最小值
        int min_result = result[result.size()-1][0];
        for(int i = 1;i<result.size();++i)
        {
            if(min_result > result[result.size()-1][i])
            {
                min_result = result[result.size()-1][i];
            }
        }
        return min_result; 
    }
};

image.gif

2.3、路径总数

题目描述:

一个机器人在m×n大小的地图的左上角(起点)。机器人每次可以向下或向右移动。机器人要到达地图的右下角(终点)。可以有多少种不同的路径从起点走到终点?

SUN0CL((B[YG44L9E7375_C.png

备注:m和n小于等于100,并保证计算结果在int范围内

数据范围:0 < n,m \le 1000<n,m≤100,保证计算结果在32位整型范围内

要求:空间复杂度 O(nm)O(nm),时间复杂度 O(nm)O(nm)

进阶:空间复杂度 O(1)O(1),时间复杂度 O(min(n,m))O(min(n,m))

思路分析:

O_)FQW8LQ9NZ88V8Z_J(5VJ.png

状态F(i,j)表示从(0,0)到(i,j)的路径数。

状态转移方程:因为只能向下走或者向右走。因此,可以分成两种情况:

①当在边界,即i==0或j==0的时候,路径数只有1条。这也是初始值。

F(i, 0) = 1;

F(0, j) = 1;

②不在边界,对于每一个(i,j)的路径数,是把向上的总路径数+向左的总路径数。

F(i,j) = F(i-1,j) +F(i,j-1);

返回结果:F(row-1,col-1);

代码如下:

int uniquePaths(int m, int n) {
        // write code here
        if(m<0 || n<0)
        {
            return 0;
        }
        //开辟空间并初始化
        vector<vector<int>> tmp(m,vector<int>(n,1));
        for(int i = 1;i<m;++i)
        {
            for(int j = 1;j<n;++j)
            {
                tmp[i][j] = tmp[i-1][j]+tmp[i][j-1];
            }
        }
        int result = tmp[m-1][n-1];
        return result;
    }

image.gif

2.4、最小路径和

题目描述:

给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。

注意:你每次只能向下或向右移动。

思路分析:

这道题与上一道题思路很相似。算是第二道题和第三道题的结合,找最小路径的权值的和。

状态F(i,j)表示从(0,0)到达F(i,j)的最短路径。

状态转移方程:因为只能向下或向右移动。因此有两种情况:

①边界:即i==0和j==0的情况。这种情况每次都是只能从它的正上一级走来。

F(i,0) = F(i-1,0)+F(i,0);

F(0,j) = F(0,j-1) +F(0,j);

②非边界。这种情况下,F(i,j)从它的左路径和上路径中选择最小的那个,再加上自己的一个便可。

F(i,j) = min(F(i-1,j),F(i,j-1))+F(i,j);

初始化:除了两种边界情况,对于F(0,0)本身就自己这个值。

返回结果:F(row-1,col-1)。走到最后,就是最小权值的和了。

代码如下:

int minPathSum(vector<vector<int> >& grid) {
        // write code here
        const int M = grid.size();
        const int N = grid[0].size();
        //全部初始化为0
        vector<vector<int>> ret(M,vector<int>(N,0));
        //初始化F(0)(0)
        ret[0][0] = grid[0][0];
        //初始化F(0,i)和F(i,0)
        for(int i = 1;i!= M ;++i)
        {
            ret[i][0] = ret[i-1][0]+grid[i][0];
        }
        for(int i = 1;i != N ;++i)
        {
            ret[0][i] = ret[0][i-1]+grid[0][i];
        }
        for(int i = 1;i!=M;++i)
        {
            for(int j = 1;j!=N;++j)
            {
                ret[i][j] = min(ret[i-1][j],ret[i][j-1])+grid[i][j];
            }
        }
        int result = ret[M-1][N-1];
        return result;
    }

image.gif

相关文章
|
2月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
60 8
|
2月前
|
算法 Python
算法不再难!Python分治法、贪心、动态规划实战解析,轻松应对各种算法挑战!
【7月更文挑战第8天】掌握Python算法三剑客:分治、贪心、动态规划。分治如归并排序,将大问题拆解递归解决;贪心策略在每步选最优解,如高效找零;动态规划利用子问题解,避免重复计算,解决最长公共子序列问题。实例展示,助你轻松驾驭算法!**
50 3
|
1月前
|
机器学习/深度学习 算法 Java
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
这篇文章介绍了基于贪婪技术思想的Prim算法和Dijkstra算法,包括它们的伪代码描述、Java源代码实现、时间效率分析,并展示了算法的测试用例结果,使读者对贪婪技术及其应用有了更深入的理解。
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
|
1月前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
|
2月前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
62 2
|
2月前
|
算法 程序员 Python
算法小白到大神的蜕变之路:Python分治法、贪心、动态规划,一步步带你走向算法巅峰!
【7月更文挑战第9天】探索算法之旅,以Python解锁编程高手之路。分治法如二分查找,将复杂问题拆解;贪心算法解决活动选择,每次选取局部最优;动态规划求斐波那契数列,避免重复计算,实现全局最优。每一步学习,都是编程能力的升华,助你应对复杂挑战,迈向算法大师!
34 1
|
2月前
|
存储 算法 Python
Python算法界的秘密武器:分治法巧解难题,贪心算法快速决策,动态规划优化未来!
【7月更文挑战第9天】Python中的分治、贪心和动态规划是三大关键算法。分治法将大问题分解为小问题求解,如归并排序;贪心算法每步选局部最优解,不保证全局最优,如找零钱;动态规划存储子问题解求全局最优,如斐波那契数列。选择合适算法能提升编程效率。
49 1
|
2月前
|
存储 算法 Python
震撼!Python算法设计与分析,分治法、贪心、动态规划...这些经典算法如何改变你的编程世界!
【7月更文挑战第9天】在Python的算法天地,分治、贪心、动态规划三巨头揭示了解题的智慧。分治如归并排序,将大问题拆解为小部分解决;贪心算法以局部最优求全局,如Prim的最小生成树;动态规划通过存储子问题解避免重复计算,如斐波那契数列。掌握这些,将重塑你的编程思维,点亮技术之路。
51 1
|
2月前
|
存储 算法 大数据
Python算法高手的必修课:深入理解分治法、贪心算法、动态规划,让你的代码更智能!
【7月更文挑战第9天】在Python算法学习中,分治法(如归并排序)将大问题分解为小部分递归解决;贪心算法(如货币找零)在每步选择局部最优解尝试达到全局最优;动态规划(如斐波那契数列)通过存储子问题解避免重复计算,解决重叠子问题。掌握这三种方法能提升代码效率,解决复杂问题。
35 1
|
2月前
|
算法 索引 Python
逆袭算法界!Python分治法、贪心算法、动态规划深度剖析,带你走出算法迷宫!
【7月更文挑战第8天】分治法,如快速排序,将大问题分解并合并解;贪心算法,选择局部最优解,如活动选择;动态规划,利用最优子结构避免重复计算,如斐波那契数列。Python示例展示这些算法如何解决实际问题,助你精通算法,勇闯迷宫。
30 1