【问题标题】:Determine conflict-free sets?确定无冲突集?
【发布时间】:2013-04-09 12:58:34
【问题描述】:

假设您有一堆集合,而每个集合都有几个子集。

Set1 = {(香蕉、菠萝、橙子)、(苹果、羽衣甘蓝、黄瓜)、(洋葱、大蒜)}

Set2 = {(香蕉、黄瓜、大蒜)、(鳄梨、番茄)}

...

SetN = { ... }

现在的目标是从每个集合中选择一个子集,而每个子集必须与任何其他选定的子集没有冲突。对于这个玩具大小的示例,一个可能的解决方案是选择(香蕉、菠萝、橙子)(来自 Set1)和(鳄梨、番茄)(来自 Set2)。

如果选择 Set1 和 Set2 的第一个子集,则会发生冲突,因为香蕉将包含在两个子集中(这是不可能的,因为它只存在一次)。

即使有很多算法,我也无法选择合适的算法。我不知何故陷入困境,希望针对以下问题提供答案:

1) 如何找到合适的算法,并以算法可以处理的方式表示这个问题?

2) 这个玩具大小示例的可能解决方案可能是什么样的(任何语言都可以,我只是想了解一下)。

Edit1:我也在考虑模拟退火(返回一个可能的解决方案)。这对于最小化例如选择集合的总成本可能是有意义的。但是,我不知道如何做出适当的问题描述,将“冲突”考虑在内。

【问题讨论】:

  • 我不太确定第二个答案如何无法解决 (1)。它将输入表示为一组字符串。作者提到它效率低下,因为字符串比较很慢。要回答您关于 100 个集合和每个集合 30 个集合的问题……我认为回溯算法(据我所知,所提出的两种解决方案都是相同的)运行速度不够快。在输入该大小时,您可能会考虑使用机器学习算法
  • 基于这两个建议,我现在能够使用算法 X 和主要/次要列来实现这一点。基于此,鉴于无论如何它是投票率最高的答案,我已经接受了这个解决方案。 Haskell 中的解决方案是正确的,但对我来说并不那么容易获得。

标签: algorithm design-patterns


【解决方案1】:

这个问题可以表述为generalized exact cover problem

为每组集合(Set1、Set2 等)创建一个新原子,并将您的输入转换为如下实例:

{Set1, banana, pineapple, orange}
{Set1, apple, kale, cucumber}
{Set1, onion, garlic}
{Set2, banana, cucumber, garlic}
{Set2, avocado, tomato}
...

使Set* 原子成为主要原子(仅覆盖一次),其他原子为次要原子(最多覆盖一次)。然后你可以用 Knuth 算法 X 的推广来解决它。

【讨论】:

  • 本来想问你如何概括它,但这解释了它:en.wikipedia.org/wiki/Exact_cover#Generalizations
  • 太好了,我没想过“扁平化”我的套装。算法 X 确实似乎是正确的选择。有没有考虑到这些初级和次级原子的算法描述?
  • @user26372 一旦你理解了Algorithm X 只有主要约束,我认为它就像修改步骤1一样简单,改为“如果A有为空只有辅助行,终止”并修改第 2 步以读取“确定性选择主要行”,因为您的实例没有仅包含次要原子的集合。
  • 也许您可以稍微扩展您的答案。我特别不明白关于算法 X 的两件事:I)“每个辅助列的标题应该具有简单地指向自身的 L 和 R 字段”-> 那么如何将它们与主列/集合互连? II)目前,我不明白它对于其中包含完全不同元素的集合是如何工作的(例如,Knuth 提到的示例是“对称的”。这里不是这种情况。这可能不是问题,只要我理解我)正确。
  • @user26372 我在维基百科上的 DLX 文章中没有看到你的 I)。对于广义算法和主要/次要列,它声明了一个短语:这将算法的解决方案测试从没有列的矩阵更改为没有主要列的矩阵,但不需要任何进一步的更改。因此,辅助列仍然与主要列链接,只是“完整解决方案”检查不同。彻底放弃 I)。
【解决方案2】:

查看集合列表,我想到了一个有多个入口的迷宫。该任务类似于从上到下跟踪没有子集交叉点的路径。 Haskell 中的示例选择所有入口,并尝试每条路径,返回成功的路径。

我对代码工作原理的理解(算法):

对于第一组中的每个子集,选择下一组中的每个子集,其中该子集与累积结果中的每个子集的交集为空。如果没有符合条件的子集,请打破循环的压力。如果没有可供选择的集合,则返回该结果。为所有选择的子集(以及相应的累积结果)递归调用该函数。

import Data.List (intersect)
import Control.Monad (guard)

sets = [[["banana", "pineapple", "orange"], ["apple", "kale", "cucumber"], ["onion", "garlic"]]
       ,[["banana", "cucumber", "garlic"], ["avocado", "tomato"]]]

solve sets = solve' sets [] where
  solve' []         result = [result]
  solve' (set:rest) result = do
    subset <- set
    guard (all null (map (intersect subset) result))
    solve' rest (result ++ [subset])

输出:

*Main> solve sets
[[["banana","pineapple","orange"],["avocado","tomato"]]
,[["apple","kale","cucumber"],["avocado","tomato"]]
,[["onion","garlic"],["avocado","tomato"]]]

【讨论】:

  • 我想说这个算法看起来不是很有效,尽管它很优雅。 David Eisenstat 上面的链接让我觉得没有更快的已知解决方案。也许这就是为什么回溯是 N-queen 问题(显然是“广义精确覆盖问题”)最流行的解决方案。
  • @roliu 是的,你说的可能是真的。您能否帮助我/我们了解回溯解决方案如何比这个更有效?
  • 我喜欢这种方法和想法,尽管我还不懂代码。我也会对预期的运行时间感兴趣(与算法 X 相比)。大约运行这个是否可行。 100 套,每套约 100 套。 30 个子集?
  • @user26372 当然,您可以下载一个 haskell 平台 (haskell.org/platform) 并将示例 sets 替换为您想要的任何内容。如果您想以相同的形式在网络上的某处提供数据,我很乐意尝试:[[[1,2],[3,4]], [[1,2],[5,6]]] 用于数字,[[["1","2"],["3","4"]], [["1","2"],["5","6"]]] 用于字符串。我在其中看到的主要低效率是(1)对于每个路径树,将重复比较(尽管不是针对任何一个路径树),以及(2)我猜数字比较会比字符串比较快。
  • @groovy 您的解决方案回溯算法...除非我不知道我在说什么。基本上你说你认为这是一个迷宫。相反,将其视为决策树。回溯算法是一种您只需执行 DFS 的算法,只要您失败,您只需返回(您忽略以该节点为根的子树,因为某些约束将在该子树中的 any 节点处失败) .在您的情况下,您在guard 失败(我认为,至少?我不知道 Haskell)和“回溯”。否则,您将继续执行 DFS。如果你到达一个叶节点,那就是一个解决方案。
猜你喜欢
  • 1970-01-01
  • 2013-02-18
  • 1970-01-01
  • 2013-07-20
  • 1970-01-01
  • 2020-03-01
  • 2015-05-07
  • 1970-01-01
  • 2016-07-09
相关资源
最近更新 更多