都说程序员是直男, 聊天聊不过三句, 看下边这位朋友求助小鹿, 抱怨说, 学习数据结构那么难, 除了优化程序算法, 其他的啥都不能干, 学它干啥, 哎, 撩个妹子都撩不到.
说真的, 我都嫌弃他, 哈哈, 嫌弃归嫌弃, 我还想教给你撩妹的正确方式的, 该说不说的, 为了能让这位朋友可以撩上妹子, 小鹿耗时假期三天时光, 出了这篇文章, 希望帮助各位宅男们脱单.
故事背景
快过年了, 很多吃货都安奈不住了, 也包括爱吃零食的小鹿. 天猫超市也很多 "满减活动". 我想很多程序员和小鹿一样, 会在天猫超市有 "满减活动" 的时候才会买一大些零食, 一次性吃个够.
那么机会来了, 你是个吃货, 很多小姐姐也是吃货, 何不借此机撩一撩呢?
这怎么撩? 凭着你学了数据结构与算法嘛? 凭着你是做底层架构的? 凭着你会....
这点小事, 包在小鹿身上了, 下面就给你来个用数据结构与算法实战撩小姐姐, 让小姐姐对你的满意度从 0 飙升到 100.
开始搭讪
很多女生说程序员都是直男, 没错, 我也承认我也是其中一位.
直男是程序员的通病, 但是程序员这个职业也赋予了我们随机应变能力呀, 懂得自救才行(很多宅男们就败在这了).
你不仔细想想, 作为一个非程序员的妹子, 谁喜欢跟你闲着没事讨论这自己不懂的东西.
帮小姐姐解决问题
这个时候, 该你展现真正的技术了, 可不要搞砸了, 不认你的印象又被小姐姐大大降低.
那么问题来了, 怎么选择购物车中的零食才能满足尽最大可能凑到 "满减活动" 呢?
1, 动态规划
我们就会用到动态规划去计算, 动态规划在数据结构与算法中属于重点也是难点, 也会涉及到递归, 这部分也是基础中最难的. 之前很多读反馈, 说分享的教程太基础, 所以关于递归基础, 这里不讲了, 可以公众号后台回复 "递归", 获取基础教程.
首先, 我们将复杂问题简单化, 假如我们购物车中有 2 元, 4 元, 2 元, 3 元, 6 元的五个价格的零食. 假如买够 9 元, 我们立减 5 元, 我们应该凑满 9 元或者尽可能的大于 9 元并接近 9 元呢?
其实这个问题一眼就能选择出来, 但是我们为了将复杂问题解决掉, 我们还是用动态规划去解决.
其实这个问题类似于递归, 第一个零食买不买有两种选择, 一个是买, 一个就是不买, 第二个, 第三个, 第四个..... 都有两种选择, 这时候我们可以用到递归.
你会说, 我们可以用回溯算法把所有可能列出来呀, 选择合适的不就好了? 记住, 我们是程序员, 要尽可能的降低时间复杂度, 等你全部穷举出来, 估计你的小姐姐早就等不及下单了.
这个问题用动态规划怎么解决呢?
我们用 (a,b) 来表示每个零食是否被选择, a 表示对第 a 个零食是否购买, b 表示已选择购买零食的总价格.
2, 用递归树来表示
上边的递归树把所有的情况都列举了出来, 就像是我们用到的回溯算法, 之所以时间复杂度很高, 是因为递归树中有的层次很多重复计算的步骤, 如果我们把重复计算的状态只记录一种不就可以减少时间复杂度, 提高程序效率了吗?
上边的递归树怎么表示呢? 我们可以一个二维数组, tree[a][b],a 表示第 a 个零食是否购买, b 表示已选择购买零食的总价格.
上边递归树我们用代码实现, 如下:
- /**
- *
- * @param prices 各个商品的价格
- * @param n 商品的个数
- * @param w 满减条件(满 199-99 元)
- */
- public static void ShoppingSnacks(int[] prices,int n,int w){
- boolean[][] tree = new boolean[n][w+1];
- tree[0][0] = true;
- tree[0][prices[0]] = true;
- // 动态规划
- for (int i = 1; i <n; i++) {
- // 不购买当前商品
- for(int j = 0;j <=w; j++) {
- // 寻找上一个商品决策状态
- if(tree[i-1][j] == true) {
- tree[i][j] = true;
- }
- }
- // 购买当前商品
- for(int j = 0;j <=w-prices[i]; j++) {
- // 寻找上一个商品决策状态
- if(tree[i-1][j] == true) {
- tree[i][j+prices[i]] = true;
- }
- }
- }
- }
上边代码执行完成, 所有的情况我们就用二维数组来表示, 零食购买的决策状态如下:
◆ 横坐标代表已经从购物车选择的零食的总价格;
◆ 纵坐标代表对第 i 个零食进行选择是否购买;
但是上边的代码存在一个问题, 求得的最大满足满减条件价格是尽可能小于接近 9 元, 我们想要达到的结果是尽可能的达到或超过接近满减条件. 怎么做呢? 我们做出以下修改, 让满减价格尽可能的大三倍, 就是说我们将满足 9 元提升到 27 元, 也就是说, 在所有零食中, 尽可能满足接近 27 元的情况有哪些, 我们都能选择出来, 接下来我们选择出最接近超过 9 元的情况就可以了.
代码修改如下:
- /**
- *
- * @param prices 各个商品的价格
- * @param n 商品的个数
- * @param w 满减条件(满 199-99 元)
- */
- public static void ShoppingSnacks(int[] prices,int n,int w){
- // 将商品的价格扩展到三倍
- boolean[][] tree = new boolean[n][3*w+1];
- tree[0][0] = true;
- tree[0][prices[0]] = true;
- // 动态规划
- for (int i = 1; i < n; i++) {
- // 不购买当前商品
- for(int j = 0;j <=3*w; j++) {
- // 寻找上一个商品决策状态
- if(tree[i-1][j] == true) {
- tree[i][j] = true;
- }
- }
- // 购买当前商品
- for(int j = 0;j <=3*w-prices[i]; j++) {
- // 寻找上一个商品决策状态
- if(tree[i-1][j] == true) {
- tree[i][j+prices[i]] = true;
- }
- }
- }
- }
那么还有一个问题就是最大可能超过并接近 9 元的情况我们选择出来了, 但是怎么把选择的哪些零食列出来呢?
我们利用倒推的方法, 这个地方很难理解, 可以自己尝试着画图或者根据代码和上边画的二维数组图对应着选择, 多看几遍估计就可以看会了, 问题不大.
怎么个倒推法呢? 假如我们最后一个商品在数组中用 n-1 表示, 如果可以通过 tree[n-2][ j - prices[i]], 或者 tree[i-1][j] 推到出来, 那么这两种状态可以达到, 就可以判断当前的这个零食是否购买, 如果两种状态都可以达到, 我们选择一种就好, 以为有这样一种情况就是, 到达满减最接近的值可能有很多中情况.
上边介绍完了, 那么给小姐姐选择零食尽最大可能的接近满减(199 元), 我们就可以享受立减 99 元的优惠了!
购物车物品如下:
我们会发现一个问题, 小数不能用下标表示, 那我们可以将价格以及满减价格同时乘以 10, 那么就可以计算了.
代码如下:
- /**
- * [动态规划]
- * 功能: 实现天猫超市 "满减凑单活动"
- * @author 小鹿
- *
- */
- public class TBShopping {
- /**
- *
- * @param prices 各个零食的价格
- * @param n 零食的个数
- * @param w 满减条件(满 199-99 元)
- */
- public static void ShoppingSnacks(int[] prices,int n,int w){
- // 将零食的价格扩展到三倍
- boolean[][] tree = new boolean[n][3*w+1];
- tree[0][0] = true;
- tree[0][prices[0]] = true;
- // 动态规划
- for (int i = 1; i < n; i++) {
- // 不购买当前零食
- for(int j = 0;j <=3*w; j++) {
- // 寻找上一个零食决策状态
- if(tree[i-1][j] == true) {
- tree[i][j] = true;
- }
- }
- // 购买当前零食
- for(int j = 0;j <=3*w-prices[i]; j++) {
- // 寻找上一个商品决策状态
- if(tree[i-1][j] == true) {
- tree[i][j+prices[i]] = true;
- }
- }
- }
- // 找出需要凑单的零食
- int j;
- for(j = w;j < 3*w+1; j++) {
- // 在最后一个零食寻找满足最接近 200 的条件状态
- if(tree[n-1][j]==true) {
- System.out.println("满减的最大条件为"+(float)j/10+"元");
- break;
- }
- }
- // 没有可选择零食
- if(j == -1) {
- return;
- }
- // 倒推遍历满足条件的零食
- for(int i = n-1; i>=1; i--) {
- // 当前账单的总金额大必须于当前零食金额
- // 且上一个商品的决策状态为 true
- if(j - prices[i]>=0 && tree[i-1][j-prices[i]] == true) {
- // 已购买该零食
- System.out.println(Snacks[i]+p[i]);
- j = j - prices[i];
- }else {
- // 没有购买该零食
- }
- }
- if(j != 0) {
- System.out.print(Snacks[1]+p[0]);
- }
- }
- }
运行结果:
当你给小姐姐完成了这项艰巨的选择之后, 小姐姐的态度就 180 度大转弯了!
本公众号专注于「前端」,「数据结构」互联网技术领域, 通俗简单的文字和动漫配图, 让你爱上编程.
公众号: 一个不甘平凡的码农
[GitHub 源码获取方式] :https://github.com/luxiangqiang/Data-Structure-Coding/tree/master / 动态规划 / 满减活动源码
来源: https://juejin.im/post/5c91f989f265da610f7c0065