动态规划专题(笔记)

文章概述了五道LeetCode题目,涉及动态规划解决最小路径和、字符串编辑距离计算、股票买卖最佳时机以及解码方法,展示了C++代码实现,重点在于递归、状态机和边界条件处理。
摘要由CSDN通过智能技术生成

LeetCode 64. 最小路径和
打了一天游戏,没时间学习,要努力辽。
今天只是刷了一题leetcode,暂时以动态规划相关题型为复习点,编点工程代码。
新建./include/Solution.h

#include <vector>
using namespace std;
class Solution {
public:
    int minPathSum(vector<vector<int>> &grid);
};

新建./source/Solution.cpp

#include <vector>
#include <algorithm>
#include "Solution.h"
using namespace std;

int Solution::minPathSum(vector<vector<int>> &grid) {
    int rows = grid.size();
    int cols = grid[0].size();

    auto dp = vector<vector<int>> (rows, vector<int> (cols));
    dp[0][0] = grid[0][0];

    for(int i = 1; i < rows; i++) {
        dp[i][0] = dp[i-1][0] + grid[i][0];
    }
    for(int i = 1; i < cols; i++) {
        dp[0][i] = dp[0][i-1] + grid[0][i];
    }
    for(int i = 1; i < rows; i++) {
        for(int j = 1; j < cols; j++) {
            dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
        }
    }
    return dp[rows - 1][cols - 1];
}

新建./main.cpp

#include <vector>
#include <iostream>
#include "Solution.h"
using namespace std;

int main(int argc, char **argv) {
    int n,m;
    cin >> n >> m;
    auto grid = vector<vector<int>> (n, vector<int> (m));
    for (auto i = 0; i < n; i++) {
        for (auto j = 0; j < m; j++) {
            cin >> grid[i][j];
        }
    }
    Solution mySolution;
    cout << mySolution.minPathSum(grid) <<endl;
    return 0;
}

新建./CMakeLists.txt

#声明要求的cmake最低版本
cmake_minimum_required(VERSION 3.0)

#声明一个cmake工程
project(Solution)

include_directories(include)

#添加一个可执行程序
#语法:add_executable(程序名 源代码文件)
add_executable(main_cmake main.cpp source/Solution.cpp)

执行

mkdir build
cd build
cmake ..
make
./main_cmake

测试用例

3
3
1 3 1 1 5 1 4 2 1

测试结果

7

LeetCode 72. 编辑距离
今天又是做帕鲁的一天,强撑着学点。
在这里插入图片描述
从字符串 S1 经过增/删/替换 转换到 S2 与
从字符串 S2 经过删/增/替换 转换到 S1 互为逆过程
上图中
1号单元记入字符串 S2 和字符串 S1 互转需要的最小步数
2号单元记入字符串 S2 和字符串( S1 + C1 )互转需要的最小步数
3号单元记入字符串( S2 + C2 )和字符串 S1 互转需要的最小步数
那么
4号单元可由1、2、3号单元内容计算得出。

class Solution {
public:
    int minDistance(string word1, string word2) {
        int s1 = word1.size();
        int s2 = word2.size();
        auto dp = vector<vector<int>>(s1 + 1, vector<int>(s2 + 1));
        for (int i = 1; i <= s2; i++) {
            dp[0][i] = i;
        }
        for (int i = 1; i <= s1; i++) {
            dp[i][0] = i;
        }
        for (int i = 1; i <= s1; i++) {
            for (int j = 1; j <= s2; j++) {
                if (word1[i - 1] == word2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;
                }
            }
        }
        return dp[s1][s2];
    }
};

LeetCode 121. 买卖股票的最佳时机
不断更新最低价格。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        int min_price = prices[0];
        int profit = 0;
        for (int i = 1; i < len; i++) {
            if (prices[i] < min_price) {
                min_price = prices[i];
            } else {
                profit = max(prices[i] - min_price, profit);
            }
        }
        return profit;
    }
};

LeetCode 122. 买卖股票的最佳时机 II
不断累加上升区间。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        int ans = 0;
        for (int i = 0; i < len - 1; i++) {
            if (prices[i] < prices[i + 1]) {
                ans += prices[i + 1] - prices[i];
            }
        }
        return ans;
    }
};

LeetCode 123. 买卖股票的最佳时机 III
以价格数组为[2, 3, 4, 2, 1, 2, 3, 2, 4]为例。
在这里插入图片描述
状态机模型如下图:
第一天是no state。
在这里插入图片描述
如果只看前4天,树高度为4,从根节点到叶子节点有8种不同的买卖方式,如下图所示:
在这里插入图片描述
每个节点的数据结构中需要存储以下三种信息:

#当前是第几天:*dayx*
#当前状态:no state / bought state,换句话说,是否持有股票,状态用0/1表示
#交易次数:已经交易了几次,状态用0/1/2表示

也可以发现每一层节点,它的状态没有什么规律可言,节点存储的信息自然也就不同,所以不属于重叠子问题。

可以用一张 map 表存储第 n 天,持有/不持有股票,完成若干比交易时的最大利润。
表中 key 由三个维度组成,其实就是节点信息
key = day(n) + bought(0/1) + transaction(0/1/2)
value = Max Profit Calculated
在这里插入图片描述
从 mem[n][2][3] 中取值时间复杂度是O(1)。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        // 三维状态表示:第i天,j表示是否持有股票,k表示交易完成次数
        int dp[n][2][3];
        dp[0][0][0] = 0; 
        dp[0][0][1] = 0;
        dp[0][0][2] = 0;
        dp[0][1][0] = - prices[0];
        dp[0][1][1] = - prices[0];
        dp[0][1][2] = - prices[0];
        for(int i = 1; i < n; i++) {
            dp[i][0][0] = 0; 
            dp[i][0][1] = max(dp[i - 1][1][0] + prices[i], dp[i - 1][0][1]);
            dp[i][0][2] = max(dp[i - 1][1][1] + prices[i], dp[i - 1][0][2]);
            dp[i][1][0] = max(dp[i-1][0][0]-prices[i], dp[i-1][1][0]);            
            dp[i][1][1] = max(dp[i-1][0][1]-prices[i], dp[i-1][1][1]); 
            dp[i][1][2] = max(dp[i-1][0][2]-prices[i], dp[i-1][1][2]); 
        }
        return dp[n-1][0][2];
    }
};

LeetCode 312. 戳气球
以给定数组为:
nums = [3, 5, 8]
进行分析

为了计算方便,先在 nums 数组两侧各添加一个“1”
nums’ = 1, [3, 5, 8], 1

更具一般性的,假设我先戳破 5。那么该数组被划分成了两部分,分别是[1, 3]和[8, 1],由于戳破了5,得到了3 * 5 * 8 枚硬币。接下来如果能够独立的解决[1, 3]和[8, 1]形成的两个子问题,就皆大欢喜,但是这两个数组并不独立,比如戳破 8 时,需要用到3;戳破 3 时需要用到 8。

我们希望沿着上面的思路,可以将一个问题拆分为子问题。
这题不妨将最先戳破 5 换成是最后一个戳破 5,那就说明了戳破 5 之前一步,仅遗留[1, 5, 1],3 和 8不发生关系,被 5 隔断开。该问题求解变成了:
1, [3, 5, 8], 1最后戳破 5,最大硬币数 = 1, [3], 5最大硬币数 + 1 * 5 * 1 + 5, [8], 1最大硬币数。(中括号内的气球才能戳破)
1, [3], 5最大硬币数,只能戳破 3,得到1 * 3 * 5;[5, 8, 1]最大硬币数,只能戳破 8,得到 5 * 8 * 1。
上面是在最后戳破 5 的情况下,只得到唯一答案 60。

如果1, [3, 5, 8], 1最后戳破 3,该问题求解变成了:
1, [3, 5, 8], 1最后戳破 3,最大硬币数 = 1, [nil], 3 最大硬币数 + 1 * 3 * 1 + 3, [5, 8], 1最大硬币数
1, [nil], 3 最大硬币数,没有气球可供戳破了。
3, [5, 8],1最大硬币数,可以选择最后一个戳破 5,也可以选择最后一个戳破 8。

如果 3, [5, 8],1 最后戳破 5,该问题求解变成了:
3, [5, 8],1最后戳破5,最大硬币数 = 3, [nil], 5 最大硬币数 + 3 * 5 * 8 + 5, [8], 1最大硬币数,而5, [8], 1已经是最小的子问题了,即递归基,此时只能戳破 8,得到 40 枚硬币。

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        int n = nums.size();
        vector<int> val(n + 2);
        val[0] = val[n + 1] = 1;
        for (int i = 1; i <= n; i++) {
            val[i] = nums[i - 1];
        }
        vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));

        for (int window_size = 1; window_size <= n; window_size ++) {
            for (int left = 1; left <= n - window_size + 1; left ++) {
                int right = left + window_size - 1;
                for (int i = left; i <= right; i++) {
                    dp[left][right] = max((dp[left][i - 1] + 
                                      val[left - 1] * val[i] * val[right + 1] +
                                      dp[i + 1][right]),
                                      dp[left][right]);
                                      
                }
            }
        }
        return dp[1][n];
    }
};

LeetCode 91. 解码方法
这题就是斐波那契数列的变种,无非要考虑一些情况。
比如字符串“128207”
从左往右,每次取的数字,单个的情况是否合法?和前一个数字组合成的新数字是否合法?是需要我们判断的。
比如取到 2,2自身合法,和前一个 1 组成的 12 也合法;
比如取到8,8自身合法,但和前一个2 组成的 28 非法。
比如取到0,0自身非法,但和前一个2 组成的 20 合法。
比如取到7,7自身合法,但和前一个0 组成的 07 非法。

class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        if (n == 0 || s[0] == '0') {
            return 0;
        }
        if (n == 1) {
            return 1;
        }

        vector<int> dp(n + 1);
        dp[0] = 1; // "空串"
        dp[1] = 1; // 仅一个数字
        for (int i = 2; i <= n; i++) {
            int digit_first = s[i - 2] - '0';
            int digit_second = s[i - 1] - '0';
            int digit = digit_first * 10 + digit_second;
            if (digit_second != 0) {
                dp[i] = dp[i - 1];
            }
            if (digit_first != 0 && 1 <= digit && digit <= 26) {
                dp[i] += dp[i - 2];
            }
        }
        return dp[n];
    }
};

LCR 003. 比特位计数

十进制二进制
6890010 1011 0001
6880010 1011 0000
3440001 0101 1000
十进制二进制
91001
81000
40100
class Solution {
public:
    vector<int> countBits(int n) {
        vector<int> bits(n + 1);
        for (int i = 1; i <= n; i ++)
        {
            bits[i] = bits[i >> 1] + (i % 2);
        }
        return bits;
    }
};
  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值