使用动态规划解决购物时的满减问题

574 阅读5分钟

双十一有很多促销活动,比如“满200元减50元”。假如购物车有 n 个(n>100)商品,要从中选几个,在凑够满减条件前提下,让选出的商品价格总和最大程度地接近满减条件(200)元。这怎么使用代码解决呢?

这时候可以使用动态规划的方式, 代码如下:

import copy
import random


def solution(items, n, w, amt):
    tmp = [False for _ in range(3 * w + 1)]
    states = [copy.deepcopy(tmp) for _ in range(n)]
    states[0][0] = True
    if items[0] <= 3 * w:
        states[0][items[0]] = True

    for i in range(1, n):
        # 不放
        for j in range(3 * w + 1):
            if states[i - 1][j] is True:
                states[i][j] = True
        # 放
        j = 0
        while j <= 3 * w - items[i]:
            if states[i - 1][j] is True:
                states[i][j + items[i]] = True

            j += 1

    j = w
    while j < 3 * w + 1:
        if states[n - 1][j] is True:
            break
        j += 1
    if j == 3 * w + 1:
        print('没有可行解')
        return False, 9999999999

    sum_all = 0
    cnt = 0
    for i in range(n - 1, 0, -1):
        if j - items[i] >= 0 and states[i - 1][j - items[i]] is True:
            print(f'买它: {items[i] / dividend} (第{i + 1}个商品)')
            j -= items[i]
            sum_all += items[i]
            cnt += 1

    if j != 0:
        print(f'买它: {items[0] / dividend} (第1个商品)')
        sum_all += items[0]
        cnt += 1
    all_save_money = w / test_w * amt
    avg_money = (sum_all - w / test_w * amt) / cnt
    print(f"达到满减的条件: {w / dividend}")
    print(f"总满减金额: {all_save_money / dividend}")
    print(f"实际花费: {(sum_all - all_save_money) / dividend}")
    print(f"购买件数: {cnt}")
    print(f"商品均价: {avg_money / dividend}")
    print(f"差额: {(sum_all - w) / dividend}")
    return True, (sum_all - w) / dividend, avg_money / dividend


if __name__ == '__main__':
    # 可重复购买的商品(用于凑单的商品)
    candidates = [136.4, 25]
    test_items = [random.choice(candidates) for _ in range(20)]
    # 限制购买数量的商品(要多少买多少的商品)
    limited_items = [34, 29, 28, 28, 29]
    test_items += limited_items
    test_n = len(test_items)
    test_w = 199
    test_amt = 25
    min_diff = 999999999
    idx = -1
    lowest_avg_money = 9999999
    lowest_avg_idx = -1

    # 适配价格有小数点的
    is_float = False
    for item in candidates:
        if isinstance(item, float):
            test_items = [int(each * 100) for each in test_items]
            test_w *= 100
            test_amt *= 100
            is_float = True
            break

    if is_float:
        dividend = 100
    else:
        dividend = 1

    print(f"商品价格表:\n{[item / dividend for item in test_items]}\n")

    # 凑单满减次数
    max_times = 10
    for idx in range(1, max_times):
        print(f'满足{idx}次满减: {(test_w * idx) / dividend}元')
        res = solution(test_items, test_n, test_w * idx, test_amt)
        if not res[0]:
            break
        if min_diff > res[1]:
            min_diff = res[1]
            idx = idx
        if lowest_avg_money > res[2]:
            lowest_avg_money = res[2]
            lowest_avg_idx = idx
        print()
    
    print("结论:")
    print(f"在第 {idx} 轮找到最小差额: {min_diff / dividend}")
    print(f"在第 {lowest_avg_idx} 轮找到最小单价: {lowest_avg_money / dividend}")

实际效果:

商品价格表:
[136.4, 136.4, 136.4, 25.0, 136.4, 25.0, 25.0, 25.0, 136.4, 25.0, 136.4, 25.0, 136.4, 136.4, 25.0, 25.0, 136.4, 25.0, 136.4, 25.0, 34.0, 29.0, 28.0, 28.0, 29.0]

满足1次满减: 199.0元
买它: 29.0 (第25个商品)
买它: 34.0 (第21个商品)
买它: 136.4 (第19个商品)
达到满减的条件: 199.0
总满减金额: 25.0
实际花费: 174.4
购买件数: 3
商品均价: 58.13333333333333
差额: 0.4

满足2次满减: 398.0元
买它: 29.0 (第25个商品)
买它: 28.0 (第24个商品)
买它: 28.0 (第23个商品)
买它: 29.0 (第22个商品)
买它: 34.0 (第21个商品)
买它: 25.0 (第20个商品)
买它: 25.0 (第18个商品)
买它: 25.0 (第16个商品)
买它: 25.0 (第15个商品)
买它: 25.0 (第12个商品)
买它: 25.0 (第10个商品)
买它: 25.0 (第8个商品)
买它: 25.0 (第7个商品)
买它: 25.0 (第6个商品)
买它: 25.0 (第4个商品)
达到满减的条件: 398.0
总满减金额: 50.0
实际花费: 348.0
购买件数: 15
商品均价: 23.2
差额: 0.0

满足3次满减: 597.0元
买它: 29.0 (第25个商品)
买它: 34.0 (第21个商品)
买它: 25.0 (第20个商品)
买它: 136.4 (第19个商品)
买它: 25.0 (第18个商品)
买它: 136.4 (第17个商品)
买它: 25.0 (第16个商品)
买它: 25.0 (第15个商品)
买它: 136.4 (第14个商品)
买它: 25.0 (第12个商品)
达到满减的条件: 597.0
总满减金额: 75.0
实际花费: 522.2
购买件数: 10
商品均价: 52.22
差额: 0.2

满足4次满减: 796.0元
买它: 29.0 (第25个商品)
买它: 28.0 (第24个商品)
买它: 28.0 (第23个商品)
买它: 29.0 (第22个商品)
买它: 136.4 (第19个商品)
买它: 136.4 (第17个商品)
买它: 136.4 (第14个商品)
买它: 136.4 (第13个商品)
买它: 136.4 (第11个商品)
达到满减的条件: 796.0
总满减金额: 100.0
实际花费: 696.0
购买件数: 9
商品均价: 77.33333333333333
差额: 0.0

满足5次满减: 995.0元
买它: 29.0 (第25个商品)
买它: 34.0 (第21个商品)
买它: 25.0 (第20个商品)
买它: 136.4 (第19个商品)
买它: 25.0 (第18个商品)
买它: 136.4 (第17个商品)
买它: 25.0 (第16个商品)
买它: 25.0 (第15个商品)
买它: 136.4 (第14个商品)
买它: 136.4 (第13个商品)
买它: 25.0 (第12个商品)
买它: 136.4 (第11个商品)
买它: 25.0 (第10个商品)
买它: 25.0 (第8个商品)
买它: 25.0 (第7个商品)
买它: 25.0 (第6个商品)
买它: 25.0 (第4个商品)
达到满减的条件: 995.0
总满减金额: 125.0
实际花费: 870.0
购买件数: 17
商品均价: 51.1764705882353
差额: 0.0

满足6次满减: 1194.0元
买它: 28.0 (第24个商品)
买它: 25.0 (第20个商品)
买它: 136.4 (第19个商品)
买它: 25.0 (第18个商品)
买它: 136.4 (第17个商品)
买它: 25.0 (第16个商品)
买它: 136.4 (第14个商品)
买它: 136.4 (第13个商品)
买它: 136.4 (第11个商品)
买它: 136.4 (第9个商品)
买它: 136.4 (第5个商品)
买它: 136.4 (第3个商品)
达到满减的条件: 1194.0
总满减金额: 150.0
实际花费: 1044.2
购买件数: 12
商品均价: 87.01666666666667
差额: 0.2

满足7次满减: 1393.0元
买它: 29.0 (第25个商品)
买它: 136.4 (第19个商品)
买它: 136.4 (第17个商品)
买它: 136.4 (第14个商品)
买它: 136.4 (第13个商品)
买它: 136.4 (第11个商品)
买它: 136.4 (第9个商品)
买它: 136.4 (第5个商品)
买它: 136.4 (第3个商品)
买它: 136.4 (第2个商品)
买它: 136.4 (第1个商品)
达到满减的条件: 1393.0
总满减金额: 175.0
实际花费: 1218.0
购买件数: 11
商品均价: 110.72727272727272
差额: 0.0

满足8次满减: 1592.0元
买它: 28.0 (第24个商品)
买它: 25.0 (第20个商品)
买它: 136.4 (第19个商品)
买它: 25.0 (第18个商品)
买它: 136.4 (第17个商品)
买它: 25.0 (第16个商品)
买它: 25.0 (第15个商品)
买它: 136.4 (第14个商品)
买它: 136.4 (第13个商品)
买它: 25.0 (第12个商品)
买它: 136.4 (第11个商品)
买它: 25.0 (第10个商品)
买它: 136.4 (第9个商品)
买它: 25.0 (第8个商品)
买它: 25.0 (第7个商品)
买它: 136.4 (第5个商品)
买它: 136.4 (第3个商品)
买它: 136.4 (第2个商品)
买它: 136.4 (第1个商品)
达到满减的条件: 1592.0
总满减金额: 200.0
实际花费: 1392.0
购买件数: 19
商品均价: 73.26315789473684
差额: 0.0

满足9次满减: 1791.0元
没有可行解

结论:
在第 9 轮找到最小差额: 0.0
在第 2 轮找到最小单价: 0.2319999999999999999