二维动态规划降维误差一般为多少_动态规划--5道题入门

动态规划是由递归一步步优化出来的

递归–>记忆化递归–>动态规划

动态规划与其说是一个算法,不如说是一种方法论。该方法论主要致力于将合适的问题拆分成三个子目标——击破:

1.建立状态转移方程

2.缓存并复用以往结果

3.按顺序从小往大算

直接看题吧

比如:

一、斐波那契额数列(剑指offer10-I)

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1

F(N) = F(N - 1) + F(N - 2), 其中 N > 1.

斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2

输出:1

示例 2:

输入:n = 5

输出:5

1.最常用的解法,递归:

class Solution {

public:

int fib(int n) {

//1.结束条件

if(n<2) return n;

if(n>(1e9+7))return 1;

//2.找等价函数

return (fib(n-1)+fib(n-2));

}

};

结果超时,因为一旦要求的n偏大,就会出现大量的重复计算:

2.递归+记忆化:

到达32的时候仍然会超时。按理说应该可以通过1

class Solution {

public:

int fib(int n) {

if(n==0) return 0;

if(n==1) return 1;

vectormem(n+1,-1);

//1.结束条件

//mem[0]=0;

//mem[1]=1;

//2.找等价函数

if(mem[n]==-1) mem[n]=(fib(n-1)+fib(n-2))%1000000007;

return mem[n] ;

}

};

修改后成功了!!!!!!!!!!!!!!!!!!!!!!!!!!!!

class Solution {

public:

int fib(int n) {

mem=vector(n+1,-1);

return fibs(n);

}

private:

vectormem;

int fibs(int n) {

if(n<2) return n;

if(mem[n]==-1) mem[n]=(fibs(n-1)+fibs(n-2))%1000000007;

return mem[n] ;

}

};

**总结:

递归到记忆化递归有三步:

1.将递归函数转换为辅助函数,新建全局容器,并在主函数里面初始化大小。

2.在辅助函数中添加if条件语句,考虑当数组容器为-1时的情况。

3.将递归等价关系取得的值记忆到数组容器中去,最后返回容器。

分开的原因暂时未知!

3.动态规划

自下而上的解决问题

class Solution {

public:

int fib(int n) {

if(n==0) return 0;

vectormem(n+1,-1);

//1.结束条件

mem[0]=0;

mem[1]=1;

//注意for循环里面时三个mem

for (int i = 2; i <= n; i++){

mem[i]=(mem[i-1]+mem[i-2])%1000000007;

}

return mem[n] ;

}

};

记忆化递归到动态规划总结:

1.又合并只有一个函数,对数组容器原地进行创建并初始化。

2.把递归的结束条件转换为数组容器的值。

3.利用遍历和数组容器中已存在的值,找到数组容器中的等价关系,最后返回所需要的容器值。

4.优化空间之后

class Solution {

public int fib(int n) {

if(n==0)return 0;

if(n==1)return 1;

int a=0,b=1;

for(int i=2;i<=n;i++){

int x = (a+b)%1000000007;

a=b;

b=x;

}

return b;

}

}

二、青蛙跳台问题(剑指offer10-II)

1.递归:

class Solution {

public:

int numWays(int n) {

if(n==0)return 1;

if(n==1)return 1;

return numWays(n-1)+numWays(n-2);

}

};

2.递归+记忆优化

因为重复太多,所以需要记忆。

递归到记忆化递归有三步:

1.将递归函数转换为辅助函数,新建全局容器,并在主函数里面初始化大小。

2.在辅助函数中添加if条件语句,考虑当数组容器为-1时的情况。

3.将递归等价关系取得的值记忆到数组容器中去(注意等价关系的容器数组下标不再含有n),最后返回容器。

class Solution {

public:

int numWays(int n) {

mem=vector(n+1,-1);

return tiaotai(n);

}

private:

vectormem;

int tiaotai(int n){

if(n==0)return 1;

if(n==1)return 1;

if(mem[n]==-1)

mem[n]=(tiaotai(n-1)+tiaotai(n-2))%1000000007;

return mem[n];

}

};

记忆化递归到动态规划总结:

1.又合并只有一个函数,对数组容器原地进行创建并初始化。

2.把递归的结束条件转换为数组容器的值。

3.利用遍历和数组容器中已存在的值,找到数组容器中的等价关系(注意等价关系的容器数组下标不再含有n),最后返回所需要的容器值。

3.动态规划

直接这么写输入0总是报错

class Solution {

public:

int numWays(int n) {

vectormem(n+1,-1);

mem[0]=1;

mem[1]=1;

for(int i=2;i<=n;i++){

mem[i]=(mem[i-1]+mem[i-2])%1000000007;

}

return mem[n];

}

};

直接把1储存到所有的数组元素中反而正确

class Solution {

public:

int numWays(int n) {

//动态规划4行解决

//声明n+1大小的vector,+1是要存储0至n共n+1个数据。

vector v(n + 1, 1);

for(int i = 2; i <= n; i++)

v[i] = (v[i - 1] + v[i - 2]) % 1000000007;//注意别忘记取余

return v[0];//直接返回最后一个数,即最终结果

}

};

4.空间优化:

class Solution {

public:

int numWays(int n) {

vectormem(n+1,1);

if(n<=1)return 1;

int a=1,b=1,c=0;

for(int i=2;i<=n;i++){

c=(a+b)%1000000007;

a=b;

b=c;

}

return b;

}

};

或者这种

class Solution {

public:

int numWays(int n) {

//创建一个数组,

//数组大小尽量大,因为它是可以取的n的最大范围

int ans[500]={1,1};

//把每一种台阶数对应的跳法总数存到对应数组中

for(int i=2;i<=n;i++) ans[i]=(ans[i-1]+ans[i-2])%(int(1e9+7));

//返回所取的台阶对应跳法数

return ans[n];

}

};

三、 整数拆分(Leetcode343)

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

输入: 2

输出: 1

解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: 10

输出: 36

解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

1.普通递归(超时)

class Solution {

private:

int Max(int a, int b, int c){

return max(a,max(b,c));

}

int _integerBreak(int n){

//结束条件

if(n==1)return 1;

//寻找等价关系

int res=-1;

for(int i=1; i

res=Max(res,i*(n-i), i*_integerBreak(n-i));

}

return res;

}

public:

int integerBreak(int n) {

//vector(n+1,-1);

return _integerBreak(n);

}

};

2.记忆化递归

class Solution {

private:

vectormem;

int Max(int a, int b, int c){

return max(a,max(b,c));

}

int _integerBreak(int n){

//结束条件

if(n==1)return 1;

//寻找等价关系

if(mem[n]!=-1)return mem[n];

int res=-1;

for(int i=1; i

res=Max(res,i*(n-i), i*_integerBreak(n-i));

}

mem[n]=res;

return res;

}

public:

int integerBreak(int n) {

assert(n>=2);

mem = vector(n+1,-1);

return _integerBreak(n);

}

};

3.动态规划

记忆化递归到动态规划总结:

1.又合并只有一个函数,对数组容器原地进行创建并初始化。

2.把递归的结束条件转换为数组容器的值。

3.利用遍历和数组容器中已存在的值,找到数组容器中的等价关系(注意等价关系的容器数组下标不再含有n),最后返回所需要的容器值。

class Solution {

private:

int Max(int a, int b, int c){

return max(a,max(b,c));

}

public:

int integerBreak(int n) {

assert(n>=2);

vectormem(n+1,-1);

mem[1]=1;

int res=-1;

for(int i=2; i<=n; i++){

for(int j=1;j

res=Max(res,j*(i-j), j*mem[i-j]);

mem[i]=res;

}

}

return mem[n];

}

};

四、三角形求和(Leetcode120)

题意:

给定三角形,每次只能移动到下一行中的相邻结点,求从顶点到底边的最小路径和。

[

[2],

[3,4],

[6,5,7],

[4,1,8,3]

]

相邻结点:与(i, j) 点相邻的结点为 (i + 1, j) 和 (i + 1, j + 1)。

分析:

若定义 f(i, j)f(i,j) 为 (i, j)(i,j) 点到底边的最小路径和,则易知递归求解式为:

f(i, j) = min(f(i + 1, j), f(i + 1, j + 1)) + triangle[i][j]

由此,我们将任一点到底边的最小路径和,转化成了与该点相邻两点到底边的最小路径和中的较小值,再加上该点本身的值。这样本题的 递归解法(解法一) 就完成了。

1.纯递归的错误方法,因为会超时:

class Solution {

private:

int dfp(vector>& triangle,int i, int j){

//结束条件

//i,j代表从哪个点一直向下的路径之和

//下面的条件是i已经超过最底端,所以路径和返回0

if(i==triangle.size())return 0;

//等价关系

return min(dfp(triangle,i+1,j),dfp(triangle,i+1,j+1))+triangle[i][j];

}

public:

int minimumTotal(vector>& triangle) {

int n = triangle.size();

//对辅助容器的初始化,初始化后二维数组的空间值自动赋值为0

return dfp(triangle,0,0);

}

};

暴力搜索会有大量的重复计算,导致 超时,因此在 解法二 中结合记忆化数组进行优化。

2.递归加记忆化的方法:

class Solution {

//递归的记忆化辅助

vector>mem;

private:

int dfp(vector>& triangle,int i, int j){

//结束条件

//i,j代表从哪个点一直向下的路径之和

//下面的条件是i已经超过最底端,所以路径和返回0

if(i==triangle.size())return 0;

if (mem[i][j] != 0) {

return mem[i][j];

}

//等价关系

return mem[i][j]=min(dfp(triangle,i+1,j),dfp(triangle,i+1,j+1))+triangle[i][j];

}

public:

int minimumTotal(vector>& triangle) {

int n = triangle.size();

//对辅助容器的初始化,初始化后二维数组的空间值自动赋值为0

mem=vector >(n, vector(n));

return dfp(triangle,0,0);

}

};

3.动态优化

定义二维 dp 数组,将解法二中「自顶向下的递归」改为「自底向上的递推」

class Solution {

public:

int minimumTotal(vector>& triangle) {

int n = triangle.size();

//对辅助容器的初始化,初始化后二维数组的空间值自动赋值为0

vector>dp(n+1, vector(n+1));

//把每一个位置从底到该处的最小路径之和都算出来

for (int i = n - 1; i >= 0; i--) {

for (int j = 0; j <= i; j++) {

dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle[i][j];

}

}

return dp[0][0];

}

};

4.空间优化

3的解法把三角形每一个位置从下到上的路径之和都搞了出来,其实没必要,因为我们只要求从下到最顶点的路径之和,所以只需要用一行数组保存前一层的路径之和让后一层用就可以,这样就更加节省空间。

class Solution {

public:

int minimumTotal(vector>& triangle) {

int n = triangle.size();

vectordp(n+1);

for (int i = n - 1; i >= 0; i--) {

for (int j = 0; j <= i; j++) {

dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j];

}

}

return dp[0];

}

};

五、求左上到右下的最小路径和(Leetcode64)

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]

输出:7

解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]

输出:12

1.路径规划的方法

最主要是的搞清楚往临时容器中放的是什么,一次性循环放和分开放都可以!

class Solution {

public:

int minPathSum(vector>& grid) {

int m=grid.size(),n=grid[0].size();

if(m==0 && n==0)return 0;

//对辅助容器的初始化,初始化后二维数组的空间值自动赋值为0

vector>dp(m, vector(n));

//int dp[m][n]; //容器或数组两个都可以

dp[0][0]=grid[0][0];

// 第1行 计算

//第一行初始化

//注意后面是加grid[][]!

for(int i=1; i

dp[0][i]=dp[0][i-1]+grid[0][i];

}

//第一列的初始化

for(int j=1; j

dp[j][0]=dp[j-1][0]+grid[j][0];

}

//把不是第一行第一列位置从起始处到该位置的和算出来初始化

for (int i = 1; i

for (int j = 1; j

dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) +grid[i][j];

}

}

return dp[m-1][n-1];

}

};

2.进行空间优化

想不到想不到

不知道这种二维变一维的空间优化大佬们是怎样想到的,反正我想不到!

class Solution {

public:

int minPathSum(vector>& grid) {

int rows = grid.size();

int cols = grid[0].size();

vector dp(cols, grid[0][0]);

for (int i = 1; i < cols; ++i)

dp[i] = grid[0][i] + dp[i - 1];

for (int i = 1; i < rows; ++i) {

dp[0] += grid[i][0];

for (int j = 1; j < cols; ++j) {

dp[j] = grid[i][j] + min(dp[j], dp[j - 1]);

}

}

return dp[cols-1];

}

};

个人在大学时习惯于根据决策推进方向可化分为向前和向后两类。

官方视频讲解针对样例给出了由后向前推进的解析,并给出了简化dp数组为一维的方法。本文以一维dp数组为解题基础通过向前推进决策的方法给与此题解答。

本文将解决此题的方法概括为“数组降维,逐行更新”

一.数组降维

开辟一个长度等于原二维grid数组列长(设为cols)的一维数组(vector dp(cols, grid[0][0]))

动态规划问题又可以理解为多段决策最优问题,那么dp[i]表示在前面i-1步已做出最优决策的前提下第i步做出的决策,并且使得前i步得到的收益最大(本题中指路径最短)。

值得提出的是,虽然题目中规定了每个点只能向下或者向右移动,但对于第一行的任意点来说,没有上一行的点会向下移动到该位置。由此对于第一行建立以下等式

dp[i] = grid[0][i] + dp[i - 1]; (1)

以题干给出的示例为例:遍历第一行以后

dp数组的值为{1, 4, 5}

二.逐行更新

设想我们从第二行开始,每一行的每一个点都可以由上边的点向下移动经过。

对于第i行第j列的点(i > 1)逐行遍历至该点时,有两种情况

(1)对于每行的第一个点只有他上边的点向下移动才可以经过该点

(2)剩下的点都可以由他上边的点向下移动或左边的点向右移动到达

对于第一类点,我们遍历到新一行时,由于该点只能由其他点向下移动到达我们就可以根据前一行的dp[0]来更新该点的决策值

dp[0] += grid[i][0] (2)

对于第二类点,我们可以得到如下等式

dp[j] = grid[i][j] + min(dp[j-1],dp[j])

等式右边dp[j]代表当前点上一行对应位置点的决策收益值

dp[j-1]代表当前位置左侧点的决策收益值。

大佬详解链接:动态规划的优化

其他习题:

另外还有力扣的279题,91题,64题,63题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值