【问题标题】:Ask for a solution about python combination algorithm in the real world practice在现实世界实践中求一个关于python组合算法的解决方案
【发布时间】:2014-01-06 21:33:29
【问题描述】:

我正在为一家水果店编写一个智能柜台程序,它的作用如下:

当客户购买n类型的水果时,该程序会检查部分/全部这些水果是否符合/几种折扣优惠,如果符合,这些水果将享受相对折扣。

具体的Discount定义是这样的(每个Discount包含的tag_clause个数是不确定的,每个水果都有几个tag):

<discount name='aaa' discount='90%'>
    <tag_clause>bestseller</tag>
    <tag_clause>juicy</tag>
    <tag_clause>badseller</tag>        
</discount>

意思是:如果客户想要享受这个折扣,他/她至少会购买三种不同的水果,并且他们的每个标签至少匹配折扣定义中的一个tag_clauses。

举个例子:如果你买的是苹果(标签:畅销书,多汁),西瓜(标签:多汁) 和 banana(tags:badseller),那么您可以享受 90% 的折扣。

由于每个折扣由几个(不确定)数量的 tag_clauses 组成,挑战是找到所有可能的组合,其中几个水果可以匹配折扣 A,另外几个匹配折扣 B,其余的可能匹配折扣 C 或不匹配(将以原价出售)。

这里是模型FruitDiscount的定义:

class Fruit(object):
    def __init__(self):
        self.name = None
        self.weight = Decimal('0.0')
        self.price = Decimal('0.00')
        self.tags = None

class Discount(object):      
    def __init__(self):
        self.name = None
        self.discount = Decimal('0.0')
        self.clauseTagNum = 0
        self.clauseTags = []

我的问题是:给定 n 种水果和 m 个折扣,尝试找出所有可能的尝试匹配组合。也就是说,我想生成所有组合(所以我可以过滤它们),但不知道如何生成它们。

例如,如果客户购买水果A, B, C, D, E,而这里我们有三个不同的折扣Disc-3, Disc-2_1, Disc-2_2,它们的tag_clause 编号是3、2、2。部分可能的组合是:

A,B,C try-match Disc-3 | D,E try-match Disc-2_2
B,C,D try-match Disc-3 | A,E try-match Disc-2_1
C,E try-match Disc-2_2 | A,D try-match Disc-2_1 | B try-match nothing
A,B try-match Disc-2_1 | C,E try-match Disc-2_1 | D try-match nothing

非常感谢您的宝贵时间。

【问题讨论】:

  • 几个明确的问题:是否需要满足每个 tag_clause 才能应用折扣?你的意思是 90% 的折扣,还是 10% 的折扣?
  • 这个问题看起来像一个多维背包问题,所以它可能是 NP 难的。对于正常数量的杂货,只要您的算法不是效率低下,这应该不是问题,但是像暴力破解所有可能的折扣组合这样的东西可能会导致如此多的滞后,以至于您的程序无法使用。
  • 你坚持哪一部分?例如,也许您想生成所有组合(以便您可以过滤它们),但不知道如何生成所有组合。在这种情况下,你可以问一个具体的问题,而不是仅仅说“这是我的任务,我该怎么做?”或者,如果您知道这不是完全可以解决的,而无需花费太长时间,并且您需要帮助实施您在网上找到但不理解其中一个步骤的优化算法,请向我们展示您理解的部分以及您从哪里获得迷路了。
  • 为什么这些折扣是根据水果的种类来的?如果我想买 3 盒苹果,我是不是要买一个西瓜和一个香蕉来获得折扣?每种水果的数量重要吗?
  • @user2357112 问题1:是的,每个tag_clause都需要满足,这里的折扣意味着10% off |问题2:买3盒苹果就另当别论了,不过还是谢谢你提到这个想法。最后感谢你的解决方案分析。

标签: python algorithm combinations


【解决方案1】:

考虑以下几点:

import itertools

class Fruit:
    def __init__(self, name=None, tags=None):
        self.name = name
        self.tags = tags
    def __repr__(self):
        return self.name

class Discount:
    def __init__(self, name=None, clause_tags=None):
        self.name = name
        if clause_tags is None:
            self.clause_tags_num = 0
            self.clause_tags = []
        else:
            self.clause_tags_num = len(clause_tags)
            self.clause_tags = clause_tags
    def __repr__(self):
        return self.name

A = Fruit('A', ['bestseller'])
B = Fruit('B', ['juicy', 'delicious'])
C = Fruit('C', [])
D = Fruit('D', ['badseller', 'juicy'])
E = Fruit('E', ['bestseller', 'delicious'])
fruits = [A, B, C, D, E]

Disc21 = Discount('Disc-2_1', ['delicious', 'bestseller'])
Disc22 = Discount('Disc-2_2', ['juicy', 'delicious'])
Disc3 = Discount('Disc-3', ['bestseller', 'juicy', 'badseller'])
discounts = [Disc21, Disc22, Disc3]

def match(fruits, discount):
    '''Determine whether a given set of fruits earns a given discount.'''
    # we must have at least as many fruits as clause tags
    if len(fruits) < discount.clause_tags_num:
        return False
    else:
        # now, we check to see that every clause tag in the discount is
        # present in at least one of the fruits.
        discount_tags = set(discount.clause_tags)
        fruit_tags = set()
        for fruit in fruits:
            fruit_tags = fruit_tags.union(set(tag for tag in fruit.tags))
        if fruit_tags.issuperset(discount_tags):
            return True
        else:
            return False

def match_fruits_to_discounts(fruits, discounts):
    successful_combinations = {discount: [] for discount in discounts}
    for discount in discounts:
        # iterate over all possible amounts of fruit that at least have one
        # fruit per clause tag in the discount
        for i in range(discount.clause_tags_num, len(fruits)):
            # try all combinations for each amount of fruit
            for fruit_combination in itertools.combinations(fruits, i):
                if match(fruit_combination, discount):
                    successful_combinations[discount].append(fruit_combination)
    return successful_combinations

result = match_fruits_to_discounts(fruits, discounts)
for key in result:
    print(key)
    print(result[key])

首先,我们定义FruitDiscount 类(为了简洁和可读性,我对它们进行了一些修改,但应该清楚如何根据您的需要更改它们)。然后,我们定义了一些FruitDiscount 对象。

函数match 确定给定的一组水果是否足以满足给定的折扣。我相信我已经按照您描述的方式实现了这一点,但是您应该验证这一点,因为我不清楚一个水果是否应该能够满足多个标签子句。

最后,函数match_fruits_to_discounts 查找与discounts 参数中给出的任何折扣匹配的fruits 参数的所有组合。该程序将在最后打印出哪些水果满足给定的折扣。

我认为您可能遇到的问题是您不知道itertools.combinations(iterable, r),顾名思义,它会生成从iterable 中选择的r 元素的组合(因为这些是组合,顺序无关)。

如 cmets 中所说,这可能是 NP-Hard,意思是如果你尝试运行这个以获得太多的水果或太多的折扣,最后一个函数中的循环迭代次数会爆炸,你会发现自己,正如他们所说,搞砸了。


针对您的评论,这里是match 的一个版本,它在给定的Discount 中容纳重复的标签:

def match(fruits, discount):
    # no explicit check for number of tags needed here
    discount_tags = discount.clause_tags[:] # make a copy
    fruit_tags = []
    for fruit in fruits:
        for tag in fruit.tags:
            fruit_tags.append(tag)
    # check that every tag in discount_tags is also represented in
    # fruit_tags the same number of times
    for tag in discount_tags:
        if tag not in fruit_tags:
            return False
        else:
            fruit_tags.remove(tag)
    # if we haven't returned False so far, that means that every discount
    # tag has also been a fruit tag, and so we're happy.
    return True

请注意,此实现仍然假定Fruit 不会多次具有相同的标记。我认为会是这种情况,因为我不明白为什么有人想要一个Fruit 是双bestselling 或其他什么。

【讨论】:

  • 当折扣标签彼此唯一时,您的解决方案效果很好,例如 Discount_A['bestseller', 'badseller', 'juicy'],但是当折扣标签变得不唯一时,您的match() 函数将失败。比如一个Discount,它有2个标签子句:['badseller', 'badseller'],它会提供50%的折扣!而且......您知道,您可以以 50% 的折扣价购买任意数量的产品,您需要做的只是确保您的众多水果中的一个是“坏卖家”。无论如何,非常感谢您的宝贵时间!
  • @Lyfing 好点,我认为您不需要重复标签条款的折扣。请参阅我在答案底部所做的编辑;那个版本的match 应该适应带有重复标签的Discounts。
  • 是的,你是对的。你只是把我推回到起点重新考虑关于折扣的整个想法和规则,这让我发现你的想法更有意义。感谢您在match()function 中提供的解决方案,它对我有很大帮助(事实上它对我的电脑伙伴有很大帮助:))。
【解决方案2】:

鉴于这些水果和折扣:

class Fruit(object):
    def __init__(self, name, tags):
        self.name = name
        self.tags = tags
    def __repr__(self):
        return self.name

class Discount(object):      
    def __init__(self, name, tags):
        self.name = name
        self.tags = tags
    def __repr__(self):
        return self.name

apple = Fruit('apple', ['bestseller','juicy'])
banana = Fruit('banana', ['badseller'])
watermelon = Fruit('watermelon', ['juicy'])
melon = Fruit('melon', ['juicy'])

discount_a = Discount('a', ['bestseller','juicy','badseller'])
discount_b = Discount('b', ['juicy','bestseller'])

fruits = (apple,banana,watermelon,melon)
discounts = (discount_a,discount_b)

我们可以生成所有可能组合的列表:

from operator import attrgetter

class Match(object):
    def __init__(self, fruit, tag):
        self.fruit = fruit
        self.tag = tag
    def __repr__(self):
        return '%s (%s)' % (self.fruit.name,self.tag)

def analyze(fruits,discounts):
    for discount in discounts:
        matching = matching_fruits_for_tags(fruits,discount.tags)
        if matching:
            for perm in permutations(matching):
                print discount, ':', sorted(perm, key=attrgetter('fruit.name'))

def matching_fruits_for_tags(fruits,tags):
    matching = [
        [Match(f,tag) for f in fruits if tag in f.tags] 
        for tag in tags]
    if all(matching):
        return matching

def permutations(matching):
    perm = []
    for fruits in matching:
        if perm:
            perm = [list(set(p+[f])) for f in fruits for p in perm]
        else:
            perm = [[f] for f in fruits]
    return perm

analyze(fruits,discounts)

它将打印:

a : [apple (juicy), apple (bestseller), banana (badseller)]
a : [apple (bestseller), banana (badseller), watermelon (juicy)]
a : [apple (bestseller), banana (badseller), melon (juicy)]
b : [apple (bestseller), apple (juicy)]
b : [apple (bestseller), watermelon (juicy)]
b : [apple (bestseller), melon (juicy)]

我正在使用Match 对象,因此标签显示在水果名称旁边。 matching_fruits_for_tags 收集每个折扣标签的所有匹配水果。例如 discount_a:

[[apple (bestseller)],
 [apple (juicy), watermelon (juicy), melon (juicy)],
 [banana (badseller)]]

permutations 从中生成所有可能的排列:

[[apple (juicy), apple (bestseller), banana (badseller)],
 [banana (badseller), apple (bestseller), watermelon (juicy)],
 [melon (juicy), apple (bestseller), banana (badseller)]]

其中所有 3 种排列都有苹果(畅销书)和香蕉(坏书)以及(多汁)的 3 种可能变化。

对于 discount_b,matching_fruits_for_tags 返回:

[[apple (juicy), watermelon (juicy), melon (juicy)], 
 [apple (bestseller)]]

我们为 (juicy) 提供 3 种变体,为 (bestseller) 提供一种变体。所以permutations的结果:

[[apple (juicy), apple (bestseller)],
 [watermelon (juicy), apple (bestseller)],
 [melon (juicy), apple (bestseller)]]

【讨论】:

  • 这对我要找的东西来说有点太容易了,谢谢你的回答。
  • 我添加了一个新的 getCombinations 函数。这是你要找的吗?
  • 重新检查我的问题可能会有帮助。
  • 啊,对不起,我没有仔细阅读您的帖子。我重写了我的答案。你在寻找那个输出吗?
  • 你真是个好人。谢谢。
猜你喜欢
  • 2011-01-07
  • 1970-01-01
  • 2018-12-11
  • 2010-12-17
  • 2010-09-15
  • 2023-03-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多