前端刷华为机考第16题购物车

题目地址

先看一下正确且符合时间和内存要求的答案,我模拟了牛客网的输入

    let count = 0,
      totalMoney = 0,
      firstList = [],
      secondObj = {};
    `14000 25
100 3 0
400 5 0
1300 5 1
1400 2 2
500 2 0
800 2 0
1400 5 0
300 5 0
1400 3 0
500 2 0
1800 4 0
440 5 10
1340 5 10
1430 3 0
500 2 0
800 2 0
1400 5 0
300 4 0
1400 3 0
500 2 0
1800 2 0
400 5 20
1310 5 20
1400 3 0
500 2 0`
      .trim()
      .split("\n")
      .forEach((item, i) => {
        if (i == 0) {
          [totalMoney, count] = item.split(" ");
        } else {
          const dataList = item.split(" ");
          if (dataList[2] == 0) {
            firstList.push({
              key: i,
              price: +dataList[0],
              weight: +dataList[1] * +dataList[0],
            });
          } else {
            if (!secondObj[dataList[2]]) {
              secondObj[dataList[2]] = [];
            }

            secondObj[dataList[2]].push({
              price: +dataList[0],
              weight: +dataList[1] * +dataList[0],
            });
          }
        }
      });

        function able() {
      let goodTwoList = [];
      for (let i = 0; i < firstList.length; i++) {
        const goods = firstList[i];
        goodTwoList[i] = [];
        goodTwoList[i].push(goods);

        let childList = [...(secondObj[goods.key] || [])];
        if (childList.length == 2) {
          childList.push({
            price: childList[0].price + childList[1].price,
            weight: childList[0].weight + childList[1].weight,
          });
        }
        childList.forEach((item) => {
          const price = goods.price + item.price;
          if (price <= totalMoney) {
            goodTwoList[i].push({
              price,
              weight: goods.weight + item.weight,
            });
          }
        });
      }

      const weightTwoList = [];
      for (let i = 0; i < 2; i++) {
        weightTwoList[i] = [];
        for (let j = 0; j <= totalMoney; j++) {
          weightTwoList[i][j] = 0;
        }
      }

      for (let i = 0; i < goodTwoList.length; i++) {
        let j = totalMoney;
        while (j > 10) {
          weightTwoList[1][j] = weightTwoList[0][j];
          goodTwoList[i].forEach((item) => {
            if (j >= item.price) {
              weightTwoList[1][j] = Math.max(
                weightTwoList[1][j],
                weightTwoList[0][j - item.price] + item.weight
              );
            }
          });
          j -= 10;
        }

        [weightTwoList[1], weightTwoList[0]] = [
          weightTwoList[0],
          weightTwoList[1],
        ];
      }

      console.log(weightTwoList[0][totalMoney]);
    }
    able();

首先每件物品只能购买一次;购买主件时可以不买附件的,可以买一个也可以买两个。

1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0

400 5 1800 2 0的附件,在这里很容易直观的看出来,但在收集数据的时候要注意索引,因为这条测试用例就算主附件关系搞错了答案也是对的,最佳选择是购买400 3 0500 2 0

为了便于展示在简化下

10 5
8 2 0
4 5 1
3 5 1
4 3 0
5 2 0

这时候j-=10需要改成j--j>10要改为j>0

firstList存储主件,secondObj存储附件,weight一开始就算好,分别分:

 [
  {
    "key": 1,
    "price": 8,
    "weight": 16
  },
  {
    "key": 4,
    "price": 4,
    "weight": 12
  },
  {
    "key": 5,
    "price": 5,
    "weight": 10
  }
]
{
  "1": [
    {
      "price": 4,
      "weight": 20
    },
    {
      "price": 3,
      "weight": 15
    }
  ]
}

因为当主件有附件时产生的结果就多于一个,所以用二维数组goodTwoList存储

if (childList.length == 2) {先把两个附件一起的情况算好,这里childList.forEach就可以统一处理,goodTwoList内容如下

[
  [
    {
      "key": 1,
      "price": 8,
      "weight": 16
    }
  ],
  [
    {
      "key": 4,
      "price": 4,
      "weight": 12
    }
  ],
  [
    {
      "key": 5,
      "price": 5,
      "weight": 10
    }
  ]
]

虽然8 2 0有两个附件,但他们组合的情况已大于总钱数,所以没有出现

初始化 weightTwoList

weightTwoList[1][j] = weightTwoList[0][j]; 这个是同步上一次的值,也代表了没有当前物品的情况。

weightTwoList[0][j - item.price] + item.weight 表示买下当前物品后,再用剩余的钱还能买到物品的期望值,买不到就是 0,因为 weightTwoList[0]的值全部初始化为 0

当执行到weightTwoList[1][j] = Math.max 就是对比最优解,先看一下 weightTwoList[1]每次存储的值:

[0, 0, 0, 0, 0, 0, 0, 0, 16, 16, 16]
[0, 0, 0, 0, 12, 12, 12, 12, 16, 16, 16]
[0, 0, 0, 0, 12, 12, 12, 12, 16, 22, 22]

也就是w[i][j]j就是预算为ji 件物品的最优解,因此weightTwoList长度为 2 就可以了

因为执行了互换操作,所以最优解是weightTwoList[0][totalMoney]

想起来之间遇到过类似的题算法=>魔法硬币,j可以先从totalMoney开始,每次减去 10,虽然减少不了二维数组的长度,但是到了最后一个商品时,结果可以优先产出。

时间超出了要求

我想到了递归,

   let i = 0,
      count = 30,
      money = 18000,
      minPrice = 0;
    const firstObj = {
      1: {
        price: 100,
        weight: 3,
      },
      2: {
        price: 400,
        weight: 5,
      },
      5: {
        price: 500,
        weight: 2,
      },
      6: {
        price: 800,
        weight: 2,
      },
      7: {
        price: 1400,
        weight: 5,
      },
      8: {
        price: 300,
        weight: 5,
      },
      9: {
        price: 1400,
        weight: 3,
      },
      10: {
        price: 500,
        weight: 2,
      },
      11: {
        price: 1800,
        weight: 4,
      },
      14: {
        price: 1400,
        weight: 3,
      },
      15: {
        price: 500,
        weight: 2,
      },
      16: {
        price: 800,
        weight: 2,
      },
      17: {
        price: 1400,
        weight: 5,
      },
      18: {
        price: 300,
        weight: 4,
      },
      19: {
        price: 1400,
        weight: 3,
      },
      20: {
        price: 500,
        weight: 2,
      },
      21: {
        price: 1800,
        weight: 2,
      },
      25: {
        price: 1400,
        weight: 3,
      },
      25: {
        price: 500,
        weight: 2,
      },
      26: {
        price: 800,
        weight: 5,
      },
      27: {
        price: 1400,
        weight: 5,
      },
      28: {
        price: 300,
        weight: 5,
      },
    };
    const secondObj = {
      1: [
        {
          price: 1300,
          weight: 5,
        },
      ],
      2: [
        {
          price: 1400,
          weight: 2,
        },
      ],
      9: [
        {
          price: 400,
          weight: 5,
        },
        {
          price: 1300,
          weight: 5,
        },
      ],
      20: [
        {
          price: 400,
          weight: 5,
        },
        {
          price: 1300,
          weight: 5,
        },
      ],
    };
    function able() {
      const firstKeyList = Object.keys(firstObj);
      let maxWeight = 0;

      getCountWeight(firstObj[firstKeyList[0]], 0, 0, 0);
      console.log(maxWeight);

      function getCountWeight(infos, currentMoney, currentWeight, deep) {
        for (let i = 0; i < 2; i++) {
          currentMoney += infos.price * i;
          currentWeight += infos.price * i * infos.weight;

          let childList = secondObj[firstKeyList[deep]];
          let dataList = [{ currentMoney, currentWeight }];

          if (childList && i > 0) {
            if (childList.length > 0) {
              dataList.push({
                currentMoney: currentMoney + childList[0].price,
                currentWeight:
                  currentWeight + childList[0].price * childList[0].weight,
              });
            }

            if (childList.length > 1) {
              dataList.push({
                currentMoney: currentMoney + childList[1].price,
                currentWeight:
                  currentWeight + childList[1].price * childList[1].weight,
              });
              dataList.push({
                currentMoney:
                  currentMoney + childList[1].price + childList[0].price,
                currentWeight:
                  currentWeight +
                  childList[1].price * childList[1].weight +
                  childList[0].price * childList[0].weight,
              });
            }
          }

          dataList.forEach((item) => {
            if (item.currentMoney <= money) {
              if (deep < firstKeyList.length - 1) {
                getCountWeight(
                  firstObj[firstKeyList[deep + 1]],
                  item.currentMoney,
                  item.currentWeight,
                  deep + 1
                );
              } else if (item.currentWeight > maxWeight) {
                maxWeight = item.currentWeight;
              }
            }
          });
        }
      }
    }
    able();

答案是正确的,但是当数据较大时,它超时了。

把主件存在firstObj里,附件存在secondObj

getCountWeight的形参分别是,infos:当前主件,currentMoney:现在用了多少钱,currentWeight:现在的期望,deep:处理到哪个主件

dataList先存下不买的情况,因为主间可能有附件,再依次处理,这里的for (let i = 0; i < 2; i++) {是多余的

Java 算法

我看了下其它答案,其中有一个 Java 算法,我把它转换为了 JS

let i = 0,
      m = 5,
      N = 10,
      minPrice = 0;

    const goods = [
      {
        v: 8,
        p: 1600,
        main: true,
        a1: 1,
        a2: 2,
      },
      {
        v: 4,
        p: 2000,
        main: false,
        a1: -1,
        a2: -1,
      },
      {
        v: 3,
        p: 1500,
        main: false,
        a1: -1,
        a2: -1,
      },
      {
        v: 4,
        p: 1200,
        main: true,
        a1: -1,
        a2: -1,
      },
      {
        v: 5,
        p: 1000,
        main: true,
        a1: -1,
        a2: -1,
      },
    ];

    function able() {
      let dp = [];
      for (let i = 0; i <= m; i++) {
        dp[i] = [];
        for (let j = 0; j <= N; j++) {
          dp[i][j] = 0;
        }
      }

      for (let i = 1; i <= m; i++) {
        for (let j = 0; j <= N; j++) {
          //情况一:什么都不选
          dp[i][j] = dp[i - 1][j];
          if (!goods[i - 1].main) {
            //附件,直接跳过
            continue;
          }
          //情况二:只选择主件
          if (j >= goods[i - 1].v) {
            dp[i][j] = Math.max(
              dp[i][j],
              dp[i - 1][j - goods[i - 1].v] + goods[i - 1].p
            );
          }
          //情况三:只选择主件和第一个附件
          if (
            goods[i - 1].a1 != -1 &&
            j >= goods[i - 1].v + goods[goods[i - 1].a1].v
          ) {
            dp[i][j] = Math.max(
              dp[i][j],
              dp[i - 1][j - goods[i - 1].v - goods[goods[i - 1].a1].v] +
                goods[i - 1].p +
                goods[goods[i - 1].a1].p
            );
          }
          //情况四:只选择主件和第二个附件
          if (
            goods[i - 1].a2 != -1 &&
            j >= goods[i - 1].v + goods[goods[i - 1].a2].v
          ) {
            dp[i][j] = Math.max(
              dp[i][j],
              dp[i - 1][j - goods[i - 1].v - goods[goods[i - 1].a2].v] +
                goods[i - 1].p +
                goods[goods[i - 1].a2].p
            );
          }
          //情况五:选择主件和两个附件
          if (
            goods[i - 1].a1 != -1 &&
            goods[i - 1].a2 != -1 &&
            j >=
              goods[i - 1].v +
                goods[goods[i - 1].a1].v +
                goods[goods[i - 1].a2].v
          ) {
            dp[i][j] = Math.max(
              dp[i][j],
              dp[i - 1][
                j -
                  goods[i - 1].v -
                  goods[goods[i - 1].a1].v -
                  goods[goods[i - 1].a2].v
              ] +
                goods[i - 1].p +
                goods[goods[i - 1].a1].p +
                goods[goods[i - 1].a2].p
            );
          }
        }
      }
      console.log(dp);
    }
    able();

这里为了更直观的的看结果,我简化了输入,dp的存储是这样的:

[0, 0, 0, 0, 0, 0, 0, 0, 1600, 1600, 1600]
[0, 0, 0, 0, 0, 0, 0, 0, 1600, 1600, 1600]
[0, 0, 0, 0, 0, 0, 0, 0, 1600, 1600, 1600]
[0, 0, 0, 0, 1200, 1200, 1200, 1200, 1600, 1600, 1600]
[0, 0, 0, 0, 1200, 1200, 1200, 1200, 1600, 2200, 2200]

一维数组代表货物。二维数组的下标代表花的钱,对应的值代表最大的期望,所以dp[m][N]就是最大的期望,这很好的控制空间,最后两个双重循环搞定,时间也没问题,这比递归的穷举好多了。

首先这个变量命名增大了阅读难度。

算法选择先初始化goods来解决先遇到附件的问题,但是主附件放到一起可以很明显的看到,dp[0]dp[1]dp[2]完全是重复的。

算法选择进入双重循环的时候再去处里主附件产生的不同的情况,其实增加了重复逻辑的抒写,而且有的附件加主件可能超过总钱数,没必要处理

j 没必要每次加一,因为商品的价格都是 10 的倍数

dp数组只需要两个就够了,可以看到dp[4]存储的每个数据就是当前与预算对应的最佳期望值

存储空间超出限制的算法

正所谓时间不够空间来补,看来上面的解法,我有了灵感

  let count = 0,
      totalMoney = 0,
      firstList = [],
      secondObj = {};
    `14000 25
100 3 0
400 5 0
1300 5 1
1400 2 2
500 2 0
800 2 0
1400 5 0
300 5 0
1400 3 0
500 2 0
1800 4 0
440 5 10
1340 5 10
1430 3 0
500 2 0
800 2 0
1400 5 0
300 4 0
1400 3 0
500 2 0
1800 2 0
400 5 20
1310 5 20
1400 3 0
500 2 0`
      .trim()
      .split("\n")
      .forEach((item, i) => {
        if (i == 0) {
          [totalMoney, count] = item.split(" ");
        } else {
          const dataList = item.split(" ");
          if (dataList[2] == 0) {
            firstList.push({
              key: i,
              price: +dataList[0],
              weight: +dataList[1] * +dataList[0],
            });
          } else {
            if (!secondObj[dataList[2]]) {
              secondObj[dataList[2]] = [];
            }

            secondObj[dataList[2]].push({
              price: +dataList[0],
              weight: +dataList[1] * +dataList[0],
            });
          }
        }
      });

    function able() {
      let goodTwoList = [];
      for (let i = 0; i < firstList.length; i++) {
        const goods = firstList[i];
        goodTwoList[i] = [];
        goodTwoList[i].push(goods);

        let childList = secondObj[goods.key] || [];
        if (childList.length == 2) {
          childList.push({
            price: childList[0].price + childList[1].price,
            weight: childList[0].weight + childList[1].weight,
          });
        }
        childList.forEach((item, index) => {
          const price = goods.price + item.price;
          if (price <= totalMoney) {
            goodTwoList[i].push({
              price,
              weight: goods.weight + item.weight,
            });
          }
        });
      }

      let wayList = [],
        maxWeight = 0,
        baseLength = 0;

      for (let i = 0; i < goodTwoList.length; i++) {
        let tempList = [...goodTwoList[i]];
        goodTwoList[i].forEach((goods) => {
          wayList.forEach((item) =>
            tempList.push({
              price: item.price + goods.price,
              weight: item.weight + goods.weight,
            })
          );
        });
        tempList = tempList.filter((item) => {
          if (item.price <= totalMoney) {
            if (item.weight > maxWeight) {
              maxWeight = item.weight;
            }
            return true;
          }
        });

        wayList = [...wayList, ...tempList];
      }
      console.log(maxWeight);
    }
    able();

其实原计划wayList = [...wayList, ...tempList];只需用把每个货物单独的情况放进去,剩下货物之间的组合只要最后一次的就好,但其实逻辑不对,算法本身还是穷举的,例如 1,2,3,4

1
1 2 12
1 2 3 12 23 123
1 2 3 4  12 23 123 14 24 34 124 234 1234

按照优化算法在 3 的时候就只有1 2 3 23 123,少了12这种情况,如果最优解产生在这里就出问题了。

其实这种算法我在一个方便买足球彩票的前端工具前端面试用来收尾的一道算法题都用过的,就是为了要全部结果才这么写,优化失败。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容