首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 为什么要学习数据结构和算法?
02 | 如何抓住重点,系统高效地学习数据结构与算法?
03 | 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
04 | 复杂度分析(下):浅析最好、最坏、平均、均摊时间复杂度
05 | 数组:为什么很多编程语言中数组都从0开始编号?
06 | 链表(上):如何实现LRU缓存淘汰算法?
07 | 链表(下):如何轻松写出正确的链表代码?
08 | 栈:如何实现浏览器的前进和后退功能?
09 | 队列:队列在线程池等有限资源池中的应用
10 | 递归:如何用三行代码找到“最终推荐人”?
11 | 排序(上):为什么插入排序比冒泡排序更受欢迎?
12 | 排序(下):如何用快排思想在O(n)内查找第K大元素?
13 | 线性排序:如何根据年龄给100万用户数据排序?
14 | 排序优化:如何实现一个通用的、高性能的排序函数?
15 | 二分查找(上):如何用最省内存的方式实现快速查找功能?
16 | 二分查找(下):如何快速定位IP对应的省份地址?
17 | 跳表:为什么Redis一定要用跳表来实现有序集合?
18 | 散列表(上):Word文档中的单词拼写检查功能是如何实现的?
19 | 散列表(中):如何打造一个工业级水平的散列表?
20 | 散列表(下):为什么散列表和链表经常会一起使用?
21 | 哈希算法(上):如何防止数据库中的用户信息被脱库?
22 | 哈希算法(下):哈希算法在分布式系统中有哪些应用?
23 | 二叉树基础(上):什么样的二叉树适合用数组来存储?
24 | 二叉树基础(下):有了如此高效的散列表,为什么还需要二叉树?
25 | 红黑树(上):为什么工程中都用红黑树这种二叉树?
26 | 红黑树(下):掌握这些技巧,你也可以实现一个红黑树
27 | 递归树:如何借助树来求解递归算法的时间复杂度?
28 | 堆和堆排序:为什么说堆排序没有快速排序快?
29 | 堆的应用:如何快速获取到Top 10最热门的搜索关键词?
30 | 图的表示:如何存储微博、微信等社交网络中的好友关系?
31 | 深度和广度优先搜索:如何找出社交网络中的三度好友关系?
32 | 字符串匹配基础(上):如何借助哈希算法实现高效字符串匹配?
33 | 字符串匹配基础(中):如何实现文本编辑器中的查找功能?
34 | 字符串匹配基础(下):如何借助BM算法轻松理解KMP算法?
35 | Trie树:如何实现搜索引擎的搜索关键词提示功能?
36 | AC自动机:如何用多模式串匹配实现敏感词过滤功能?
37 | 贪心算法:如何用贪心算法实现Huffman压缩编码?
38 | 分治算法:谈一谈大规模计算框架MapReduce中的分治思想
39 | 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想
40 | 初识动态规划:如何巧妙解决“双十一”购物时的凑单问题?
41 | 动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
42 | 动态规划实战:如何实现搜索引擎中的拼写纠错功能?
43 | 拓扑排序:如何确定代码源文件的编译依赖关系?
44 | 最短路径:地图软件是如何计算出最优出行路径的?
45 | 位图:如何实现网页爬虫中的URL去重功能?
46 | 概率统计:如何利用朴素贝叶斯算法过滤垃圾短信?
47 | 向量空间:如何实现一个简单的音乐推荐系统?
48 | B+树:MySQL数据库索引是如何实现的?
49 | 搜索:如何用A*搜索算法实现游戏中的寻路功能?
50 | 索引:如何在海量数据中快速查找某个数据?
51 | 并行算法:如何利用并行处理提高算法的执行效率?
52 | 算法实战(一):剖析Redis常用数据类型对应的数据结构
53 | 算法实战(二):剖析搜索引擎背后的经典数据结构和算法
54 | 算法实战(三):剖析高性能队列Disruptor背后的数据结构和算法
55 | 算法实战(四):剖析微服务接口鉴权限流背后的数据结构和算法
56 | 算法实战(五):如何用学过的数据结构和算法实现一个短网址系统?
当前位置:
首页>>
技术小册>>
数据结构与算法之美
小册名称:数据结构与算法之美
### 章节 40 | 初识动态规划:如何巧妙解决“双十一”购物时的凑单问题? 在数字时代,每年的“双十一”购物狂欢节已成为全球瞩目的消费盛宴。面对琳琅满目的商品与复杂的优惠策略,如何高效、精准地凑单以最大化节省成本,成为了众多消费者的关注焦点。这一过程中,隐藏着计算机科学中一个经典而强大的问题解决方法——动态规划(Dynamic Programming, DP)。本章将带领读者通过“双十一”凑单这一实际场景,初识动态规划的魅力,并学会如何利用它来解决类似的复杂优化问题。 #### 一、问题背景与定义 假设在“双十一”期间,你计划在一家电商平台上购买若干商品,每件商品有其固定的原价和优惠后的价格(可能是满减、折扣或直降等形式),同时平台还提供了一种或多种形式的满减优惠,如“满300减50”,“满600减120”等。你的目标是,在不超过预算的前提下,通过合理选择购买的商品和凑单方式,使得实际支付金额最少。 #### 二、动态规划基础 动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。它利用子问题的重叠性质(即不同子问题之间可能会用到相同的子子问题的解)和最优子结构性质(即原问题的最优解包含其子问题的最优解),来避免重复计算,从而提高效率。 #### 三、凑单问题的动态规划建模 ##### 3.1 状态定义 首先,我们需要定义状态。在这个问题中,状态可以定义为`dp[i][j]`,表示在前`i`件商品中选取若干件,使得总价格恰好为`j`时,能够获得的最大优惠金额(或理解为最小支付金额,因为两者在本质上是等价的,只是计算方式略有不同)。注意,这里的`j`应当覆盖所有可能的满减门槛值及其整数倍,以确保能捕捉到所有可能的优惠。 ##### 3.2 状态转移方程 接下来,我们需要构建状态转移方程。对于每件商品,我们有两个选择:买或不买。如果不买,那么当前状态`dp[i][j]`的值应与前一件商品在相同总价`j`下的状态相同,即`dp[i][j] = dp[i-1][j]`。如果买,则需要考虑购买这件商品后总价变为`j`(或`j`的某个大于原价且满足某个满减条件的值)时,能获得的优惠。假设第`i`件商品的原价为`price[i]`,优惠后价格(或视为直接减去折扣部分)为`cost[i]`,则状态转移方程可以表示为: \[ dp[i][j] = \max(dp[i-1][j], dp[i-1][j-price[i]] + \text{getDiscountForJ}(j)) \] 其中,`getDiscountForJ(j)`是一个根据当前总价`j`判断并返回对应满减优惠的函数。注意,这里简化了处理,实际中可能需要根据具体的满减规则来实现这个函数。 ##### 3.3 初始化 在动态规划开始前,需要对所有状态进行初始化。通常,`dp[0][0]`被初始化为0,表示没有商品时,总价为0时的最大优惠也是0。对于其他`dp[0][j]`(`j > 0`),由于没有任何商品可选,所以应设置为一个很大的负数(表示不可达状态),或者根据具体情况设定。 ##### 3.4 边界条件与特殊情况 - **总价超出预算**:如果某个状态的总价超出了预设的预算,该状态应视为无效,可通过设置极大负数来避免选择。 - **无有效满减**:当总价不满足任何满减条件时,`getDiscountForJ(j)`应返回0。 - **商品单价大于满减门槛**:对于单价极高的商品,如果其单独购买就能满足某个满减条件,需要特别处理这种情况下的优惠计算。 #### 四、算法实现与优化 ##### 4.1 伪代码实现 ```plaintext function dynamicProgrammingForCoupon(prices, costs, discounts, budget): // prices: 商品原价列表 // costs: 商品优惠后价格(或视为直接折扣后的价格)列表 // discounts: 满减规则,如[(300, 50), (600, 120)] // budget: 预算上限 n = len(prices) maxDiscount = max(discount[1] for discount in discounts) // 最大可能优惠 maxValue = budget + maxDiscount // 最大值考虑最大优惠 // 初始化dp数组 dp = [[float('-inf')] * (maxValue + 1) for _ in range(n + 1)] dp[0][0] = 0 // 没有商品时,总价为0的优惠为0 // 填充dp数组 for i in range(1, n + 1): for j in range(maxValue + 1): dp[i][j] = dp[i-1][j] // 不买第i件商品 if j >= prices[i-1]: # 计算购买第i件商品后的优惠 for (threshold, discount) in discounts: if j >= threshold and (j - prices[i-1]) % threshold == 0: # 假设简化处理,直接按门槛计算优惠 discountedAmount = discount * ((j - prices[i-1]) // threshold) dp[i][j] = max(dp[i][j], dp[i-1][j-prices[i-1]] + discountedAmount) # 找到预算内最小支付金额 minPayment = float('inf') for j in range(budget + 1): if dp[n][j] != float('-inf'): minPayment = min(minPayment, j - dp[n][j]) return minPayment ``` ##### 4.2 优化考虑 - **空间优化**:由于当前状态只依赖于前一状态,可以使用一维数组代替二维数组,减少空间复杂度。 - **预处理满减条件**:对于复杂的满减规则,可以提前预处理出每个总价`j`对应的最大优惠,避免在状态转移时重复计算。 - **剪枝**:当总价`j`已经远超过预算且无法获得更多优惠时,可以提前终止该路径的计算。 #### 五、实战应用与扩展 通过上述动态规划模型的建立与实现,我们不仅能够解决“双十一”凑单问题,还能将这一思路应用于更广泛的场景,如背包问题、最长公共子序列、最短路径问题等。在实际应用中,根据具体问题的特点,可能需要调整状态定义、状态转移方程以及优化策略,以达到最佳的求解效果。 #### 六、总结 本章通过“双十一”凑单这一生动有趣的实例,带领读者初步领略了动态规划的魅力。动态规划作为一种强大的算法设计技巧,其核心在于将复杂问题分解为简单子问题,并通过存储中间结果来避免重复计算,从而高效求解。希望读者能够通过本章的学习,掌握动态规划的基本思想和方法,进而在解决实际问题时能够灵活运用,达到事半功倍的效果。
上一篇:
39 | 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想
下一篇:
41 | 动态规划理论:一篇文章带你彻底搞懂最优子结构、无后效性和重复子问题
该分类下的相关小册推荐:
算法面试通关 50 讲
数据结构与算法(下)
编程之道-算法面试(上)
数据结构与算法(上)
数据结构与算法(中)
编程之道-算法面试(下)
业务开发实用算法精讲