Python处理双11购物的凑单问题与财务凑数问题

大家好,我是强哥。

今天分享一篇用python处理双11购物的凑单问题与财务凑数问题的文章。


凑单问题

对于各类凑单问题,最经典的就是淘宝双十一的满减促销活动,比如“满 200 元减 50 元”。假设你的购物车中有 n 个(n>100)想买的商品,希望从里面选几个,在凑够满减条件的前提下,让选出来的商品价格总和最大程度地接近满减条件(200 元),如何编程解决这个问题?

动态规划解决

使用传统的编程思路就是使用动态规划,思路如下:

购物车中有 n 个商品,针对每个商品都决策是否购买。每次决策之后,对应不同的状态集合。用一个二维数组 s t a t e s [ n ] [ x ] states[n][x] states[n][x],来记录每次决策之后所有可达的状态。

python实现代码为:

def double11advance(items_info: list, w: int):  
    """  
    动态规划解决双11凑单问题  
    :param items_info: 每个商品价格  
    :param w: 满减条件,比如 200  
    :return:  
    """  
    n = len(items_info)  
    # 超过 3 倍就没有薅羊毛的价值了  
    states = [[False] * (3 * w + 1) for i in range(n)]  
    states[0][0] = True  
    states[0][items_info[0]] = True  
    for i in range(1, n):  
        for j in range(3 * w + 1):  
            if states[i - 1][j]:  
                # 不购买第i个商品  
                states[i][j] = states[i - 1][j]  
                # 购买第i个商品  
                nw = j + items_info[i]  
                if nw <= 3 * w:  
                    states[i][nw] = True  
    j = w  
    while j < 3 * w + 1 and not states[n - 1][j]:  
        j += 1  
    # j是大于等于 w 的最小值  
    if j == 3 * w + 1:  
        return  # 没有可行解  
    idx = []  
    for i in range(n - 1, 0, -1):  
        if j - items_info[i] >= 0 and states[i - 1][j - items_info[i]]:  
            idx.append(i)  
            j -= items_info[i]  
    if j != 0:  
        idx.append(0)  
    return sorted(idx)

假设,我们的购物车中每件商品的价格为:

48, 30, 19, 36, 36, 27, 42, 42, 36, 24,  40, 70, 32  

我们执行代码:

import numpy as np  
items_info = np.array([48, 30, 19, 36, 36, 27, 42, 42, 36, 24,  40, 70, 32])  
  
idx = double11advance(items_info, 200)  
print("选中商品的索引:", idx)  
print("选中商品的价格:", items_info[idx])  
print("总价格:", sum(items_info[idx]))
选中商品的索引: [1, 4, 7, 8, 9, 12]  
选中商品的价格: [30 36 42 36 24 32]  
总价格:200

可以看到程序完美的找到了一组可行解。

除了动态规划,我们还可以使用回溯算法解决,参考代码就不公布了,接下来我们直接使用优化算法解决这个问题。

优化算法解决

在前面的文章《OR-Tools官档中文用法大全(CP、LP、VRP、Flows等)https://xxmdmst.blog.csdn.net/article/details/124203863》中的 背包与装箱问题 一章中,我演示了使用SCIP求解器解决该问题。

不过SCIP求解器速度较慢,而且想获取多个可行解实现起来较为麻烦,所以这里我演示使用ortools的cp_model求解器来解决该问题。

cp_model求解器相对于前面的SCIP求解器的缺点在于只能处理整数。

代码如下:

from ortools.sat.python import cp_model  
import numpy as np  
  
model = cp_model.CpModel()  
items_info = np.array([48, 30, 19, 36, 36, 27, 42, 42, 36, 24,  40, 70, 32])  
items = np.arange(items_info.shape[0])  
x = [model.NewBoolVar(f'x_{i}') for i in range(len(items_info))]  
obj = (x*items_info).sum()  
model.Add(obj >=200)  
model.Minimize(obj)  
solver = cp_model.CpSolver()  
status = solver.Solve(model)  
result = [bool(solver.Value(i)) for i in x]  
print("选中商品的索引:", items[result])  
print("选中商品的价格:", items_info[result])  
print("总价格:", items_info[result].sum())
选中商品的索引: [ 1  4  7  8  9 12]  
选中商品的价格: [30 36 42 36 24 32]  
总价格:200

可以看到 ortools 库得到了与前面动态规划一致的结果。

ortools获取多个可行解

下面我们考虑使用cp_model求解器获取多个可行解,前面我们已经可行解的最小值为200,下面我们可以限制总价格等于200:

from ortools.sat.python import cp_model  
import numpy as np  
  
  
class MyCpSolver(cp_model.CpSolverSolutionCallback):  
    def __init__(self, x):  
        cp_model.CpSolverSolutionCallback.__init__(self)  
        self.x = x  
        self.num = 0  
  
    def on_solution_callback(self):  
        self.num += 1  
        print(f"第{self.num}个解")  
        result = [bool(self.Value(i)) for i in self.x]  
        print("选中商品的索引:", items[result])  
        print("选中商品的价格:", items_info[result])  
        print("总价格:", items_info[result].sum())  
  
  
model = cp_model.CpModel()  
items_info = np.array([48, 30, 19, 36, 36, 27, 42, 42, 36, 24,  40, 70, 32])  
items = np.arange(items_info.shape[0])  
x = [model.NewBoolVar(f'x_{i}') for i in range(len(items_info))]  
obj = (x*items_info).sum()  
model.Add(obj == 200)  
solver = cp_model.CpSolver()  
myCpSolver = MyCpSolver(x)  
solver.parameters.enumerate_all_solutions = True  
status = solver.Solve(model, myCpSolver)  
print(solver.StatusName(status))  
print("解的个数:", myCpSolver.num)

最终得到了30个可行解:

如此多的可行解是因为36出现了三次,导致可行解的个数也被翻了3倍,实际可行解就只有10个。下面我们改进一下上面代码,让其获取唯一的可行解:

from collections import Counter  
from ortools.sat.python import cp_model  
import numpy as np  
  
  
class MyCpSolver(cp_model.CpSolverSolutionCallback):  
    def __init__(self, x):  
        cp_model.CpSolverSolutionCallback.__init__(self)  
        self.x = x  
        self.num = 0  
  
    def on_solution_callback(self):  
        self.num += 1  
        print(f"第{self.num}个解")  
        idx = []  
        result = []  
        for i, xi in enumerate(self.x):  
            v = self.Value(xi)  
            if v == 0:  
                continue  
            idx.append(i)  
            result.extend([items_info[i]]*v)  
        print("选中商品的索引:", idx)  
        print("选中商品的价格:", result)  
        print("总价格:", sum(result))  
  
  
arr = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24,  40, 70, 32]  
  
model = cp_model.CpModel()  
items_info = []  
x = []  
for i, (k, v) in enumerate(Counter(arr).items(), 1):  
    items_info.append(k)  
    x.append(model.NewIntVar(0, v, f"x{i}"))  
items_info = np.array(items_info)  
obj = (items_info*x).sum()  
model.Add(obj == 200)  
solver = cp_model.CpSolver()  
myCpSolver = MyCpSolver(x)  
solver.parameters.enumerate_all_solutions = True  
status = solver.Solve(model, myCpSolver)  
print(solver.StatusName(status))  
print("解的个数:", myCpSolver.num)
第1个解  
选中商品的索引: [1, 2, 4, 5, 7]  
选中商品的价格: [30, 19, 27, 42, 42, 40]  
总价格:200  
第2个解  
选中商品的索引: [2, 3, 4, 5, 7]  
选中商品的价格: [19, 36, 36, 27, 42, 40]  
总价格:200  
第3个解  
选中商品的索引: [2, 4, 5, 8]  
选中商品的价格: [19, 27, 42, 42, 70]  
总价格:200  
第4个解  
选中商品的索引: [0, 2, 3, 4, 8]  
选中商品的价格: [48, 19, 36, 27, 70]  
总价格:200  
第5个解  
选中商品的索引: [0, 2, 4, 5, 6, 7]  
选中商品的价格: [48, 19, 27, 42, 24, 40]  
总价格:200  
第6个解  
选中商品的索引: [0, 1, 2, 3, 4, 7]  
选中商品的价格: [48, 30, 19, 36, 27, 40]  
总价格:200  
第7个解  
选中商品的索引: [0, 5, 7, 8]  
选中商品的价格: [48, 42, 40, 70]  
总价格:200  
第8个解  
选中商品的索引: [1, 3, 6, 7, 8]  
选中商品的价格: [30, 36, 24, 40, 70]  
总价格:200  
第9个解  
选中商品的索引: [1, 3, 5, 6, 9]  
选中商品的价格: [30, 36, 36, 42, 24, 32]  
总价格:200  
第10个解  
选中商品的索引: [0, 3, 5, 9]  
选中商品的价格: [48, 36, 42, 42, 32]  
总价格:200  
OPTIMAL  
解的个数:10

可以看到顺利的获取到唯一解。

财务凑数问题

财务凑数问题与前面的问题模型一致,区别在于存在小数,例如从一大批金额中找出能够合并出指定金额的组合。

假设我们要查找的金额列表如下:

7750.0, 50000.0, 94693.0, 89159.18, 59000.0, 19634.94, 27000.0, 37770.17, 55631.64, 23800.0,  
20000.0, 20000.0, 20000.0, 72985.45, 48000.0, 48000.0, 58750.0, 22000.0, 11219.61, 45600.0,  
90500.0, 84288.0, 930.0, 1352.0, 120.0, 750.0, 22880.0, 45678.0, 49555.0, 17181.54, 1925.0,  
1500.0, 83325.0, 500.0, 1298.5, 36936.34, 91933.67, 5205.0, 20195.0, 20550.0, 10600.0, 3200.0,  
6400.0, 6900.0, 9900.0, 9750.0, 9600.0, 7200.0, 15208.41, 10550.0, 21077.02, 75437.51, 73515.11,  
3140.0, 85128.6, 87095.74, 22806.24, 961.72, 13285.47, 28980.0, 67997.62, 35955.33, 12890.27, 15459.47,  
20124.58, 25246.66, 13216.11, 89400.0, 89400.0, 26800.0, 11365.0, 16457.0, 50000.0, 54309.0, 12000.0,  
39000.0, 70569.5, 45231.5, 56400.0, 86400.0, 86400.0, 86400.0, 86400.0, 12000.0, 390.0, 2500.0, 38109.79,  
5968.63, 14862.6, 45038.91, 63189.17, 80784.86, 37664.87, 4981.44, 50000.0, 50000.0, 32323.01, 567.73, 66056.88, 26400.0

我们需要找到95984的组合。

SCIP求解器直接计算

如果使用SCIP求解器可以直接计算结果,编码如下:

from ortools.linear_solver import pywraplp  
import numpy as np  
  
arr = [7750.0, 50000.0, 94693.0, 89159.18, 59000.0, 19634.94, 27000.0, 37770.17, 55631.64, 23800.0,  
       20000.0, 20000.0, 20000.0, 72985.45, 48000.0, 48000.0, 58750.0, 22000.0, 11219.61, 45600.0,  
       90500.0, 84288.0, 930.0, 1352.0, 120.0, 750.0, 22880.0, 45678.0, 49555.0, 17181.54, 1925.0,  
       1500.0, 83325.0, 500.0, 1298.5, 36936.34, 91933.67, 5205.0, 20195.0, 20550.0, 10600.0, 3200.0,  
       6400.0, 6900.0, 9900.0, 9750.0, 9600.0, 7200.0, 15208.41, 10550.0, 21077.02, 75437.51, 73515.11,  
       3140.0, 85128.6, 87095.74, 22806.24, 961.72, 13285.47, 28980.0, 67997.62, 35955.33, 12890.27, 15459.47,  
       20124.58, 25246.66, 13216.11, 89400.0, 89400.0, 26800.0, 11365.0, 16457.0, 50000.0, 54309.0, 12000.0,  
       39000.0, 70569.5, 45231.5, 56400.0, 86400.0, 86400.0, 86400.0, 86400.0, 12000.0, 390.0, 2500.0, 38109.79,  
       5968.63, 14862.6, 45038.91, 63189.17, 80784.86, 37664.87, 4981.44, 50000.0, 50000.0, 32323.01, 567.73, 66056.88, 26400.0]  
  
items_info = np.array(arr)  
items = np.arange(items_info.shape[0])  
solver = pywraplp.Solver.CreateSolver('SCIP')  
x = [solver.BoolVar(f'x_{i}') for i in range(len(items_info))]  
obj = (x*items_info).sum()  
solver.Add(obj >= 95984)  
solver.Minimize(obj)  
status = solver.Solve()  
print("总重量:", obj.solution_value())  
result = np.frompyfunc(lambda x: x.solution_value(), 1, 1)(x).astype(bool)  
print("选中商品的索引:", items[result])  
print("选中商品的价值:", items_info[result])  
print("总价值:", items_info[result].sum())
总重量:95984.3  
选中商品的索引: [22 24 33 34 38 40 41 44 58 61]  
选中商品的价值: [  930.     120.     500.    1298.5  20195.   10600.    3200.    9900.  
 13285.47 35955.33]  
总价值:95984.3

不过这并不是真正的最优解,如果我们把约束设置为必须为目标值:

solver = pywraplp.Solver.CreateSolver('SCIP')  
x = [solver.BoolVar(f'x_{i}') for i in range(len(items_info))]  
obj = (x*items_info).sum()  
solver.Add(obj == 95984)  
status = solver.Solve()  
print("总重量:", obj.solution_value())  
result = np.frompyfunc(lambda x: x.solution_value(), 1, 1)(x).astype(bool)  
print("选中商品的索引:", items[result])  
print("选中商品的价值:", items_info[result])  
print("总价值:", items_info[result].sum())
总重量:95984.0  
选中商品的索引: [ 5 18 25 30 38 39 43 45 53 57 84 97]  
选中商品的价值: [19634.94 11219.61   750.    1925.   20195.   20550.    6900.    9750.  
  3140.     961.72   390.     567.73]  
总价值:95984.0

可惜耗时接近10秒。

cp_model求解器

cp_model求解器只能处理整数,为了能够处理小数,我们可以将其乘以100后转换为整数:

from ortools.sat.python import cp_model  
import numpy as np  
  
arr = [7750.0, 50000.0, 94693.0, 89159.18, 59000.0, 19634.94, 27000.0, 37770.17, 55631.64, 23800.0,  
       20000.0, 20000.0, 20000.0, 72985.45, 48000.0, 48000.0, 58750.0, 22000.0, 11219.61, 45600.0,  
       90500.0, 84288.0, 930.0, 1352.0, 120.0, 750.0, 22880.0, 45678.0, 49555.0, 17181.54, 1925.0,  
       1500.0, 83325.0, 500.0, 1298.5, 36936.34, 91933.67, 5205.0, 20195.0, 20550.0, 10600.0, 3200.0,  
       6400.0, 6900.0, 9900.0, 9750.0, 9600.0, 7200.0, 15208.41, 10550.0, 21077.02, 75437.51, 73515.11,  
       3140.0, 85128.6, 87095.74, 22806.24, 961.72, 13285.47, 28980.0, 67997.62, 35955.33, 12890.27, 15459.47,  
       20124.58, 25246.66, 13216.11, 89400.0, 89400.0, 26800.0, 11365.0, 16457.0, 50000.0, 54309.0, 12000.0,  
       39000.0, 70569.5, 45231.5, 56400.0, 86400.0, 86400.0, 86400.0, 86400.0, 12000.0, 390.0, 2500.0, 38109.79,  
       5968.63, 14862.6, 45038.91, 63189.17, 80784.86, 37664.87, 4981.44, 50000.0, 50000.0, 32323.01, 567.73, 66056.88, 26400.0]  
model = cp_model.CpModel()  
items_info = (np.array(arr)*100).astype(int)  
items = np.arange(items_info.shape[0])  
x = [model.NewBoolVar(f'x_{i}') for i in range(len(items_info))]  
obj = (x*items_info).sum()  
model.Add(obj == 95984*100)  
solver = cp_model.CpSolver()  
status = solver.Solve(model)  
print(solver.StatusName(status))  
result = [bool(solver.Value(i)) for i in x]  
print("选中商品的索引:", items[result])  
print("选中商品的价格:", items_info[result]/100)  
print("总价格:", items_info[result].sum()/100)
OPTIMAL  
选中商品的索引: [ 0 23 24 41 42 47 53 70 71 75]  
选中商品的价格: [ 7750.  1352.   120.  3200.  6400.  7200.  3140. 11365. 16457. 39000.]  
总价格:95984.0

获取多个可行解

可以看到财务的金额数据存在大量重复,所以必须先进行计数处理,最终代码为:

from collections import Counter  
from ortools.sat.python import cp_model  
import numpy as np  
  
  
class MyCpSolver(cp_model.CpSolverSolutionCallback):  
    def __init__(self, x):  
        cp_model.CpSolverSolutionCallback.__init__(self)  
        self.x = x  
        self.num = 0  
  
    def on_solution_callback(self):  
        self.num += 1  
        print(f"第{self.num}个解")  
        idx = []  
        result = []  
        for i, xi in enumerate(self.x):  
            v = self.Value(xi)  
            if v == 0:  
                continue  
            idx.append(i)  
            result.extend([items_info[i]/100]*v)  
        print("选中商品的索引:", idx)  
        print("选中商品的价格:", result)  
        print("总价格:", sum(result))  
  
  
arr = [7750.0, 50000.0, 94693.0, 89159.18, 59000.0, 19634.94, 27000.0, 37770.17, 55631.64, 23800.0,  
       20000.0, 20000.0, 20000.0, 72985.45, 48000.0, 48000.0, 58750.0, 22000.0, 11219.61, 45600.0,  
       90500.0, 84288.0, 930.0, 1352.0, 120.0, 750.0, 22880.0, 45678.0, 49555.0, 17181.54, 1925.0,  
       1500.0, 83325.0, 500.0, 1298.5, 36936.34, 91933.67, 5205.0, 20195.0, 20550.0, 10600.0, 3200.0,  
       6400.0, 6900.0, 9900.0, 9750.0, 9600.0, 7200.0, 15208.41, 10550.0, 21077.02, 75437.51, 73515.11,  
       3140.0, 85128.6, 87095.74, 22806.24, 961.72, 13285.47, 28980.0, 67997.62, 35955.33, 12890.27, 15459.47,  
       20124.58, 25246.66, 13216.11, 89400.0, 89400.0, 26800.0, 11365.0, 16457.0, 50000.0, 54309.0, 12000.0,  
       39000.0, 70569.5, 45231.5, 56400.0, 86400.0, 86400.0, 86400.0, 86400.0, 12000.0, 390.0, 2500.0, 38109.79,  
       5968.63, 14862.6, 45038.91, 63189.17, 80784.86, 37664.87, 4981.44, 50000.0, 50000.0, 32323.01, 567.73, 66056.88, 26400.0]  
  
model = cp_model.CpModel()  
items_info = []  
x = []  
for i, (k, v) in enumerate(Counter(arr).items(), 1):  
    items_info.append(k)  
    x.append(model.NewIntVar(0, v, f"x{i}"))  
items_info = (np.array(items_info)*100).astype(int)  
obj = (items_info*x).sum()  
model.Add(obj == 95984*100)  
solver = cp_model.CpSolver()  
myCpSolver = MyCpSolver(x)  
solver.parameters.enumerate_all_solutions = True  
status = solver.Solve(model, myCpSolver)  
print(solver.StatusName(status))  
print("解的个数:", myCpSolver.num)

最终再经过一小时的等待后,并未找出全部的可行解,程序还在运行中,1小时找到一千多个可行解:

为了避免计算时间过长,我们可以设置最大执行时间,例如设置30秒:

solver.parameters.max_time_in_seconds = 30  

可以看到30秒内能够找到45个解:

感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。

一、Python所有方向的学习路线

Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。

img
img

二、Python必备开发工具

工具都帮大家整理好了,安装就可直接上手!img

三、最新Python学习笔记

当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。

img

四、Python视频合集

观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。

img

五、实战案例

纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

img

六、面试宝典

在这里插入图片描述

在这里插入图片描述

简历模板在这里插入图片描述
若有侵权,请联系删除
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java和Python都内置了处理编码问题的工具。 在Java中,字符串是使用Unicode字符集编码的,因此Java提供了许多用于将字符串转换为不同编码格式的类和方法,例如String.getBytes()和new String(byte[], Charset)等。 而在Python中,字符串也是使用Unicode字符集编码的,但是Python的字符串是可变的,因此Python提供了许多用于编码和解码字符串的方法和模块,如encode()和decode()方法、codecs模块等。 这些内置的工具可以帮助我们在处理不同编码格式的数据时更加方便和高效。 ### 回答2: Java和Python都是常用的编程语言,它们提供了自带处理编码问题的工具。这些工具的意思是它们可以帮助程序员在处理不同编码方式的数据时更加方便和有效。 在Java中,Java提供了java.nio.charset包来处理编码问题。通过使用该包中的类,如Charset和CharsetEncoder,程序员可以将文本从一种编码转换为另一种编码,或者验证特定编码是否支持。Java还提供了一些用于处理读取和写入文本文件时的编码问题的类,如InputStreamReader和OutputStreamWriter。 在Python中,Python的内置模块codecs提供了处理编码问题的工具。使用codecs模块,程序员可以更方便地处理不同编码方式的数据。codecs模块中的函数和类可以用于将文本从一种编码转换为另一种编码,或者用于读取和写入不同编码方式的文本文件。 总的来说,Java和Python提供的自带处理编码问题的工具使得程序员可以更轻松地处理不同编码方式的数据。这些工具可以帮助程序员转换编码、验证编码是否支持,以及读取和写入不同编码方式的文本文件,从而提高编码相关任务的效率和准确性。 ### 回答3: Java和Python都有自带的处理编码问题的工具。所谓的处理编码问题的工具,是指这两种编程语言内置的功能或库,用于解决字符编码方面的各种问题。 在Java中,处理编码问题的工具主要是通过内置的 java.nio.charset 包来实现的。这个包提供了一套用于字符编码和解码的类和接口,包括 Charset、CharsetDecoder、CharsetEncoder等。通过这些类,Java可以方便地进行字符编码和解码的转换,包括将字符串按照不同的字符编码方式存储、读取和输出。 而在Python中,处理编码问题的工具则是通过内置的 unicode 类型和相应的字符串方法来实现的。Python的字符串可以是Unicode字符串,这意味着它们可以表示任何Unicode字符。此外,Python还提供了编码和解码相关的方法,如encode()和decode(),可以在不同的字符编码方式之间进行转换。 总之,无论是Java还是Python,它们都提供了内置的工具来处理编码问题使用户能够更加方便地进行字符编码的转换和处理,确保程序在不同编码环境下的正确运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值