[算法基础]动态规划(初识)

Dynamic Programming

DP定义:
动态规划是分治思想的延伸,通俗一点来说就是大事化小、小事化无的艺术。
将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并为后面处理更大规模的问题的时候,直接使用这些结果
动态规划具备三个特点:
1、将原来的问题分解为几个相似的子问题
2、所有的子问题只需要解决一次
3、存储子问题的解

动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:
1、状态定义
2、状态之间的转移方程定义
3、状态的初始化
4、返回结果
状态定义的要求:定义的状态一定要形成递推关系
适用场景:
最值、可不可行、是不是、方案个数

Fibonacci

思路:
状态F(i):第i项的值——要看这个第i项的值能不能返回到最后第n项的结果
返回结果:F(n)
状态转移方程:根据前面已经求解的状态结果,求解未知的状态结果,F(i)=F(i-1)+F(i-2)
初始状态:需要几个,看看初始状态方程需要几个项让这个方程运转下去:F(0)=0,F(1)=1
需要保存这些中间状态的解需要一个数组
本题是从0-n总共n+1项

class Solution {
public:
    int Fibonacci(int n) {
        int* F = new int[n + 1];
        F[0] = 0;
        F[1] = 1;
        for (int i = 2; i <= n; ++i)
        {
            F[i] = F[i - 1] + F[i - 2];
        }
        return F[n];
    }
};

接下来我们做一些优化,求当前第i位结果只需要前面两个状态的结果,其余结果是用不到的,空间进行了优化

class Solution 
{
public:
    int Fibonacci(int n) 
    {
        if (n == 0)
            return 0;
        if (n == 1)
            return 1;
        int fn; // 第n项的结果
        int fn1 = 1; // n - 1项
        int fn2 = 0; // n - 2项
        for (int i = 2; i <= n; ++i)
        {
            fn = fn1 + fn2;
            fn2 = fn1; // 更新n-2
            fn1 = fn; // 更新n-1项
        }
        return fn;
    }
};

拆分词句——字符串分割

问题是字符串s能否被分割 -> 如何抽象状态?
状态定义:F(i)如何定义?——分解为相似的子问题
相似的一个子问题:某一个子串能不能被分割,也就是字符串的前i个字符是否可以被分割,然后看F(i)能不能对应到最终的解
状态转移方程:F(i)如何求?这里拿“leetcode”举例子,先试着分析F(4):前四个字符是否可以被分割——true;F(8):F(4)&&看看[5,8]是否可以在字典中能否被找到——true,因此这里最后可以返回F(8)
F(1) && [2,8]
F(2) && [3,8]
F(3) && [4,8] // F(3)的结果是前面递推出来的
F(4) && [5,8]
F(5) && [6,8]
F(6) && [7,8]
F(7) && [8,8]
F(i):j < i (前面状态小于当前状态)&& F(j)&& [j+1,i]是否可以在词典中找到
F(0):true
返回结果:F(字符串长度):f(s.size())

三角形顶部到底部的最小路径和
思路:从某一个位置(i,j)——>(i+1,j)和(i+1,j+1)就是移动到下一行相邻的位置
问题:从顶部到底部最小的路径和
状态F(i,j):从(0,0)到(i,j)的最小路径和
转移方程:F(i,j)=?,谁可以到达(i,j)这个点?(i - 1,j - 1)(i - 1,j)可以到达(i,j)点

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5100, INF = 1e9;
int f[N][N], a[N][N];


int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= i; ++j)
        {
            scanf("%d", &a[i][j]);
        }
    }
    for (int i = 0; i <= n; ++i)
    {
        for (int j = 0; j <= i + 1; ++j) // 注意此处是i + 1,防止边界问题,所以也将边界外的也初始化为-无穷
        {
            f[i][j] = -INF;
        }
    }
    f[1][1] = a[1][1];
    for (int i = 2; i <= n; ++i) // 从第二行开始
    {
        for (int j = 1; j <= i; ++j)
        {
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);
        }
    }
    int ans = -INF;
    for (int i = 1; i <= n; ++i)
    {
        ans = max(ans, f[n][i]);
    }
    cout << ans << endl;
    return 0;
}

不同路径数目i
在这里插入图片描述
分析:
子问题就是到达任意一个点(i,j)的路径个数
状态定义F(i,j):从(0,0)到达(i,j)的路径个数
状态转移方程(某个状态可以只用一步到达这个状态,不能是多步到达):F(i,j):

class Solution {
public:
    /**
     *
     * @param m int整型
     * @param n int整型
     * @return int整型
     */
    int uniquePaths(int m, int n)
    {
        // write code here
        vector<vector<int>> dp(m, vector<int>(n, 0)); // 初始化二维vector
        for (int i = 0; i < m; ++i)
        {
            dp[i][0] = 1; // 第一行的方案数只有一个
        }
        for (int j = 0; j < n; ++j)
        {
            dp[0][j] = 1; // 第一列的方案数只有一个
        }
        for (int i = 1; i < m; ++i)
        {
            for (int j = 1; j < n; ++j)
            {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; 
            }
        }
        return dp[m - 1][n - 1];
    }
};

带权值的最小路径和
在这里插入图片描述

class Solution {
public:
    /**
     * 
     * @param grid int整型vector<vector<>> 
     * @return int整型
     */
    int minPathSum(vector<vector<int> >& grid) {
        // write code here
        int row = grid.size();
        int col = grid[0].size();
        for (int i = 1; i < col; ++i)
        {
            grid[0][i] = grid[0][i - 1] + grid[0][i];
        }
        for (int j = 1; j < row; ++j)
        {
            grid[j][0] = grid[j - 1][0] + grid[j][0];
        }
        for (int i = 1; i < row; ++i)
        {
            for (int j = 1; j < col; ++j)
            {
                grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
            }
        }
        return grid[row - 1][col - 1];
    }
};

01背包问题
01背包:每个物品只有一个
在这里插入图片描述
总共四种情况:
价值大、体积小
价值大、体积大
价值小、体积大
价值小、体积小
状态F(i):从前i个商品中做选择, 包的最大值——>可以定位到最终问题的解,所以状态应该是可行的(做题时候的思维)
第i个商品放还是不放?——对应两种情况
放入第i个商品放入F(i):F(i - 1) + V[i - 1] // F中的i-1就表示前i - 1,V[i - 1]是因为下标从0开始——这里还是有问题的,没有考虑到体积的问题,需要考虑包内剩余空间大小是多少因此还需要进行修改:
因此进行修改:
状态索引的范围是从1开始,价值数组是从0开始
状态F(i,j):从前i个商品中选择,包的大小为j时,最大价值
状态转移方程:
能直接在剩余的位置放入:F(i,j):F(i-1,j)+V[i - 1]
需要把某些物品抠出来,在放入第i个:
F(i-1, j- A[i - 1])+V[i - 1]
因此要判断第i个物品要不要放入:
第i个商品可以放入大小为j的包中
F(i,j) = max(F(i - 1, j), F(i - 1, j - A[i - 1]) + V[i - 1])
第i个商品太大,大小为j的包放不下第i个商品
F(i,j) = F(i - 1,j)

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N], w[N]; // 体积和价值的数组
int f[N][N];
int main()
{
    int n, m;
    cin >> n >> m; // 物品数量和背包容量
    for (int i = 1; i <= n; ++i) // 1~n 总共n个物品
    {
        cin >> v[i] >> w[i];
    }
    // 一开始由于f是全局,因此f[0][1~m] 前0个物品使得总体积不超过1~m的状态方程为0
    for (int i = 1; i <= n; ++i)
    {
        for (int j = 0; j <= m; ++j)
        {
            // 划分两个集合,不包含第i个物品
            f[i][j] = f[i - 1][j];
            if (j >= v[i])
            {
                // 含第i个物品,在包含i和不包含i之间选择一个最大值
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

优化
转化为一维:
若j从小到大,f[j-v[i]]中,由于j-v[i]小于j,f[j-v[i]]已经在i这层循环被计算了,而我们想要的f[j-v[i]]应该是i-1层循环里面的,所以j从大到小的话保证此时的f[j-v[i]]还未被计算,也就是第i-1层的数据

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N], w[N]; // 体积和价值的数组
int f[N];
int main()
{
    int n, m;
    cin >> n >> m; // 物品数量和背包容量
    for (int i = 1; i <= n; ++i) // 1~n 总共n个物品
    {
        cin >> v[i] >> w[i];
    }
    // 一开始由于f是全局,因此f[0][1~m] 前0个物品使得总体积不超过1~m的状态方程为0
    for (int i = 1; i <= n; ++i)
    {
        for (int j = m; j >= v[i]; --j) // if条件原本要满足j >= v[i],因此j范围在0~v[i - 1]是没有用的,可以直接不考虑
        {
            // 由于f[i]只用到了上一层f[i - 1],因此可以直接变为1维
            /*
            f[i][j] = f[i - 1][j]; -> f[j] = f[j]; 
            */
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值