剑指offer — 动态规划专题



1. 剪绳子I

  • 题目描述

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
原题链接

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:
2 <= n <= 58

  • 思路

    此题是一道DP常规问题,根据题意搞清楚状态表示状态转移即可;

    可以注意到,题目中并没有给出m的上限,但实际上我们也可以不用它来建立状态表示,也就是说一维表示足矣,dp[i]表示为将长度为i的绳子剪成合法的若干份对应的方案,抽象为最大乘积

    如何进行状态转移?也就是所谓的集合划分,我们可以枚举上述方案中最后一段长度为j的所有情况:

    • 前面的 i - j 不再划分: j * (i - j)
    • 前面的 i - j 继续划分:j * dp[i - j]
class Solution {
public:
    int cuttingRope(int n) {

        // dp[i][j] 状态表示:长度为i的绳子对应的所有方案
        //          状态转移:枚举最后一段的长度 max(j * (i - j), j * dp[i - j])
        vector<int> dp(n + 1, 0);
        for(int i = 2; i <= n; i ++) 
            for(int j = 1; j < i; j ++) 
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));

        return dp[n];
    }
};



2. 剪绳子II

  • 题目描述

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
原题链接

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:
2 <= n <= 1000

  • 思路

    此题在剪绳子I的基础上扩充了数据范围,用DP做的话会超时,这里给出一种数学证明的方式解决此题:

    下面我们给出证明:

    首先把一个正整数 NN 拆分成若干正整数只有有限种拆法,所以存在最大乘积。
    假设 N=n1+n2+…+nk,并且 n1×n2×…×nk 是最大乘积。

    显然1不会出现在其中;

    • 如果对于某个 i 有 ni≥5,那么把 ni 拆分成 3+(ni−3),我们有3(ni−3)=3ni−9>ni
    • 如果 ni=4,拆成 2+2 乘积不变,所以不妨假设没有4;
    • 如果有三个以上的2,那么 3×3>2×2×2,所以替换成3乘积更大;
      综上,选用尽量多的3,直到剩下2或者4时,用2。

    时间复杂度分析:当 n 比较大时,n 会被拆分成 n/3 个数,我们需要计算这么多次减法和乘法,所以时间复杂度是 O(n)

class Solution {
public:
    int cuttingRope(int n) {

        if(n <= 3) return 1 * (n - 1);
        
        long res = 1;
        if(n % 3 == 1) res = 4, n -= 4;
        else if(n % 3 == 2) res = 2, n -= 2;
        
        while(n) {

            res *= 3;
            res %= 1000000007;
            n -= 3;
        }

        return (int)res;
    }
};



3. 连续子数组的最大和

  • 题目描述

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
原题链接

示例1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100

  • 思路

    此题是可以用一个变量表示状态的 DP 问题,设变量为s

    状态表示
    s表示为:以前一个数结尾的连续子数组的最大和;
    状态转移

    • s < 0 以当前数x结尾的连续子数组最大和为 x;
    • s >= 0 以当前数x结尾的连续子数组最大和为 s + x;
class Solution {
public:
    int maxSubArray(vector<int>& nums) {

        int res = INT_MIN, s = 0;
        for(auto num : nums) {

            if(s < 0) s = 0;
            s += num;   //更新(转移)
            res = max(res, s);
        }
        return res;
    }
};



4. 1~n整数中1出现的次数

  • 题目描述

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
原题链接

示例 1:

输入:n = 12
输出:5

示例 2:

输入:n = 13
输出:6

限制:
1 <= n < 2^31

  • 思路

    此题的主要思路是:遍历 n 的每一位,每轮迭代求出该位为1时的所有合法数字的个数,因此需要对各种情况进行讨论。

    n为abcdef,假设此时遍历到了第2位(从左到右0算起),下面求第2位是1在1~n中的数字个数:

    • 当前两位是 00 ~ ab - 1: 后三位000 ~ 999 -> ab ✖️ 1000
    • 当前两位是 ab:
      • c = 0: 0
      • c = 1: 0 ~ def -> def + 1
      • c > 1: 000 ~ 999 -> 1000

    综上,res = ab * 1000 + 0 + def + 1 + 1000.

class Solution {
public:
    int countDigitOne(int n) {
        
        if(n <= 0) return 0;
        vector<int> num; 
        
        // 将n的每一位都提取到num中
        while(n) {

            num.push_back(n % 10);
            n /= 10;
        }

        long long res = 0;
        for(int i = num.size() - 1; i >= 0; i --) {

            // left存第i位左边的数,right存第i位右边的数,t用来存10的整数幂
            int left = 0, right = 0, t = 1; 
            for(int j = num.size() - 1; j > i; j --) 
                left = left * 10 + num[j];
            for(int j = i - 1; j >= 0; j --) {
                
                right = right * 10 + num[j];
                t *= 10;
            }

            res += left * t;
            if(num[i] == 1) 
                res += right + 1;
            else if(num[i] > 1)
                res += t;
        }
        return res;
    }
};



5. 把数字翻译成字符串

  • 题目描述

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
原题链接

示例 1:

输入: 12258
输出: 5
解释: 122585种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi""mzi"

提示:
0 <= num < 231

  • 思路

    此题求方案数,应该马上想到使用 dfsdp,一般情况下前者比后者的复杂度要高而且递归函数也比较难构造 ,所以这题我们就用 dp 来做(事实上 dfs 也确实超时)。

    • 状态表示
      f [i]: 以第 i 个数为结尾的方案数
    • 状态转移
      f [i] = f[i-1] + f[i - 2]
      f[i - 1]: 表示第 i 个数字对应一个字符;
      f[i - 2]: 表示第 i/i - 1 两个数字对应一个字符;

    这个 dp 很容易构造,但有几个问题需注意:

    • 若第 i/i-1 两个数字 (设此数为t) 构成了一个字符,由于合法字符对应的数字范围在0 ~ 25之间,因此 t<=25。同时,类似07这样的数是无法对应一个字符的,因此t>=10;
    • 此外,注意到存在i-1/i-2这样的下标,我们必须处理边界。
class Solution {
public:
    int translateNum(int num) {

        vector<int> s;
        while(num) s.push_back(num % 10), num /= 10;
        reverse(s.begin(), s.end());
        int n = s.size();
        vector<int> f(n + 1);
        f[0] = 1;
        
        for(int i = 1; i <= n; i ++) {

            f[i] = f[i - 1];
            if(i > 1) {

                int t = s[i - 2] * 10 + s[i - 1];
                if(t >= 10 && t <= 25) f[i] += f[i - 2];
            }
        }
        return f[n];
    }
};



6. 礼物的最大价值

  • 题目描述

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
原题链接

示例 1:

输入: 
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 12
解释: 路径 13521 可以拿到最多价值的礼物

提示:
0 < grid.length <= 200
0 < grid[0].length <= 200

  • 思路

    此题是经典的dp问题:

    • 状态表示
      f[i][j]: 表示走到第(i,j)个格子能得到的最大价值;
    • 状态转移
      f[i][j] = max(f[i-1][j], f[i][j-1]) + grid[i-1[j-1]
      从上边来的:f[i-1][j]
      从左边来的: f[i][j-1]
      最后别忘了加上(i,j)当前格子对应的价值。
class Solution {
public:
    int maxValue(vector<vector<int>>& grid) {

        int n = grid.size(), m = grid[0].size();
        vector<vector<int>> f(n + 1, vector<int>(m + 1));
        for(int i = 1; i <= n; i ++)    
            for(int j = 1; j <= m; j ++)
                f[i][j] = max(f[i - 1][j], f[i][j - 1]) + grid[i - 1][j - 1];

        return f[n][m];
    }
};



7. 丑数

  • 题目描述

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
原题链接

示例:

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

说明:
1 是丑数。
n 不超过1690。

  • 思路

    此题的做法很具技巧性,我们考虑将原丑数序列拆解成四部分:

    其中s是我们需要求的丑数序列,s1 s2 s3分别是包含质因子2、3、5的子序列;
    s: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 … …
    s1: 2, 4, 6, 8, 10, 12 … …
    s2: 3, 6, 9, 12 … …
    s3: 5, 10 … …
    那么,原丑数序列应该是1和上面三个子序列的并集,也就是所谓的四部分。

    看完上面的思路你肯定会提出疑问,s1 s2 s3 并不是包含质因子2、3、5的子序列啊,难道取并集不会重复吗?确实会重复的,注意这里的并集不是标准意义上的并集,我们可以在代码中略施小计将这些重复元素过滤掉。

    那你又该问了,直接将原序列分解成包含2、3、5的子序列不就好了吗,这就是此题的关键所在,如果带上了这个“只”字,那么就无法得到下面这个最关键的性质:

    如果 s1 s2 s3 分别为包含2、3、5质因子的子序列,那么它们与原序列 s 存在下面的关系:
    s1 / 2 = s
    s2 / 3 = s
    s3 / 5 = s
    上面这三个表达式什么意思呢,就是对于 s1 s2 s3 三个子序列而言,它们的元素分别除以2、3、5会得到原丑数序列,反之原丑数序列分别乘以2、3、5也会得到这三个子序列。

    这样一来就将此问题转化成了一个三路归并问题,可以分别设三个指针i j k 分别指向这三个子序列的头,实际上就是指向原序列的头,因为根据上面的分析原序列可以转化为这三个子序列,而且在原序列s的扩充过程中,i j k 是不会访问越界的,因为这三个指针从0开始扫描每次最多移动一位,而原序列每轮循环一定会增加一个元素且在开始时就存在1这个元素。

class Solution {
public:
    int nthUglyNumber(int n) {

        vector<int> q(1, 1);
        int i = 0, j = 0, k = 0;
        while(-- n) {

            int t = min(q[i] * 2, min(q[j] * 3, q[k] * 5));
            q.push_back(t);
            if(t == q[i] * 2) i ++;
            if(t == q[j] * 3) j ++;
            if(t == q[k] * 5) k ++;
        } 
        return q.back();
    }
};



8. n个骰子的点数

  • 题目描述

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
原题链接

示例 1:

输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]

示例 2:

输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]

限制:
1 <= n <= 11

  • 思路

    此题属于典型的dp问题,注意扔n个骰子一个骰子扔了n次是等价的。

    • 状态表示
      dp[i][j]: 扔i个骰子相应的点数之和为j的投法
    • 状态转移
      根据最后一个骰子对应的点数k进行集合划分:
      dp[i][j] += dp[i - 1][j - k] 枚举k (1 <= k <= 6)
class Solution {
public:
    vector<double> dicesProbability(int n) {

        vector<double> res;
        vector<vector<int>> f(n + 1, vector<int>(6 * n + 1, 0));
        f[0][0] = 1;

        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= 6 * i; j ++)
                for(int k = 1; k <= 6; k ++)
                    if(j >= k)
                        f[i][j] += f[i - 1][j - k];

        vector<int> nums(f[n].begin() + n, f[n].end());
        
        int sum = 0;
        for(auto x : nums)
            sum += x;
        for(auto x : nums) 
            res.push_back((double)x / sum);
        
        return res;
    }
};



9. 股票的最大利润

  • 题目描述

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
原题链接

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0

限制:
0 <= 数组长度 <= 10^5

  • 思路

    此题可以用双指针解决,i j 的目标是寻找序列中最小和最大的数字,其中这个最大数字一定要在最小数字的右侧才符合题目要求。

class Solution {
public:
    int maxProfit(vector<int>& prices) {

        if(prices.empty()) return 0;
        
        // 分别找到最低和最高股票价格
        int minv = nums[0], res = 0;
        for(int i = 1; i < prices.size(); i ++) {

            res = max(res, nums[i] - minv);
            minv = min(nums[i], minv);
        } 
    }
};



10. 斐波那契数列

  • 题目描述

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(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

提示:
0 <= n <= 100

  • 思路
class Solution {
public:
    int fib(int n) {
        
        if(n == 0) return 0;
        if(n == 1) return 1;
        
        vector<int> dp(n + 1, 0);
        dp[1] = 1;
        for(int i = 2; i <= n; i ++) {

            dp[i] = dp[i - 1] + dp[i - 2];
            dp[i] %= 1000000007;
        }
        return dp[n];
    }
};



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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值