剑指offer 学习笔记 动态规划与贪婪算法
如果面试题是求一个问题的最优解(通常是求最大值或最小值),而且
1.该问题能分解成若干个子问题。
2.子问题也存在最优解,如果把小问题的最优解组合起来能够得到整个问题的最优解,即整体问题的最优解依赖于各个子问题的最优解。
3.这些小问题之间有重叠的问题,即在分解大问题的过程中反复出现相同子问题。
就可以使用动态规划来解决这个问题。
动态规划算法中为了避免重复子问题的求解,我们可以用从下往上的顺序,先计算小问题的最优解并存储下来,再以此为基础求取大问题的最优解。
贪婪算法每一步都做出当前最优的选择。
面试题14:剪绳子。给你一段长度为n的绳子,请把绳子剪m次(m、n都是整数,并且n>1、m>=1),并且每段绳子长度都为整数,记为k[0]、k[1]…k[m]。请问k[0]*k[1]*…*k[m]可能的最大乘积是多少?例如,当一段绳子长为8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
方法一:动态规划。首先定义函数f(n)为把长度为n的绳子剪成若干段后各段长度乘积的最大值。在剪第一刀的时候,我们有n-1种可能的选择,也就是剪出来的第一段绳子的可能长度分别为1,2,…,n-1。因此f(n)=max(f(i)*f(n-i)),其中0<i<n。这是一个从上至下的递归公式,由于递归会产生很多重复的子问题,从而有大量不必要的计算。更好的方法是从下至上计算,也就是先得到f(2)、f(3),再得到f(4)、f(5),直到得到f(n):
#include <iostream>
using namespace std;
int MaxProductAfterCutting(int length) {
if (length < 2) { // 绳子长度小于2时不符合题意
return 0;
}
if (length == 2) { // 绳子长为2时只能剪成1*1
return 1;
}
if (length == 3) { // 绳长为3时只能剪成1*2
return 2;
}
int* products = new int[length + 1]; // 存放子问题的最优解,其实只需要length长度的数组
// 如输入绳长为5,求f(5),只需f(1)~f(5)
// 但为了书写方便,如需要f(3)时只需找数组下标为3的元素而不用找下标为2的元素
// 需要将数组长扩充一位,其中第0个元素不会使用到
products[0] = 0; // 0号元素随便初始化,不会用到
products[1] = 1;
products[2] = 2;
products[3] = 3; // 下标1~3存放绳长即可,下标4~length存放的才是绳长为下标时的最优解
// 因为在绳长为1时,不能再分割
// 绳长为2时,若分割,只能分成1*1=1,不分割时为2,此时最优解为2
// 绳长为3时,若分割,只能分成1*2=2,不分割时为3,此时最优解为3
// 由于题目要求必须分割一次,因此当输入绳长大于3时,用到绳长为1~3时的最优解时,必定已经被分割过一次了
int max; // 存放绳长为a时的最大值,a为任意中间子问题解
for (int i = 4; i <= length; ++i) { // 从绳长为4开始
max = 0; // 每次开始找绳长为a的最优解时先初始化max
for (int j = 1; j <= i / 2; ++j) {
int product = products[j] * products[i - j];
if (max < product) {
max = product;
}
}
products[i] = max;
}
delete[] products;
return max;
}
int main(int argc, char **argv) {
cout << MaxProductAfterCutting(8) << endl;
return 0;
}
动态规划的时间复杂度为O(n²),空间复杂度为O(n),n为绳长。
方法二:贪婪算法。按以下策略剪绳子,得到的各段绳子的长度的乘积将最大:当n>=5时,尽可能多地剪长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子,数学证明在代码后:
#include <iostream>
using namespace std;
int MaxProductAfterCutting(int length) {
if (length < 2) { // 绳子长度小于2时不符合题意
return 0;
}
if (length == 2) { // 绳子长为2时只能剪成1*1
return 1;
}
if (length == 3) { // 绳长为3时只能剪成1*2
return 2;
}
int timesOf3 = length / 3; // 当前输入的length长能分出几段长为3的绳段
if (length - 3 * timesOf3 == 1) { // 如果每次取一段长为3绳段,最后能剩下一段长为4的绳段时
--timesOf3; // 最后的长为4的绳段不再分为1和3
}
int timesOf2 = (length - timesOf3 * 3) / 2; // 最后剩下的是2或4时能分为几个2
return (int)pow(3, timesOf3) * (int)pow(2, timesOf2);
}
int main(int argc, char **argv) {
cout << MaxProductAfterCutting(8) << endl;
return 0;
}
以下为LeetCode上的数学推导过程:https://leetcode-cn.com/problems/jian-sheng-zi-lcof/solution/mian-shi-ti-14-i-jian-sheng-zi-tan-xin-si-xiang-by/
C++pow函数的时间复杂度取决于底层架构,x86上这是一个固定时间的操作,时间复杂度为O(1),空间复杂度为O(1)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2019-02-17 JAVA注释