元素成对比较的高效算法

Posted

技术标签:

【中文标题】元素成对比较的高效算法【英文标题】:Efficient algorithm for pairwise comparison of elements 【发布时间】:2021-11-03 14:28:28 【问题描述】:

给定一个包含一些键值对的数组:

[
  'a': 1, 'b': 1,
  'a': 2, 'b': 1,
  'a': 2, 'b': 2,
  'a': 1, 'b': 1, 'c': 1,
  'a': 1, 'b': 1, 'c': 2,
  'a': 2, 'b': 1, 'c': 1,
  'a': 2, 'b': 1, 'c': 2
]

我想找到这些对的交集交集 意味着只留下那些可以被其他人覆盖或独特的元素。例如, 'a': 1, 'b': 1, 'c': 1'a': 1, 'b': 1, 'c': 2 完全覆盖 'a': 1, 'b': 1,而 'a': 2, 'b': 2 是独一无二的。所以,在

[
  'a': 1, 'b': 1,
  'a': 2, 'b': 1,
  'a': 2, 'b': 2,
  'a': 1, 'b': 1, 'c': 1,
  'a': 1, 'b': 1, 'c': 2,
  'a': 2, 'b': 1, 'c': 1,
  'a': 2, 'b': 1, 'c': 2
]

找到交叉点后应该保留

[
  'a': 2, 'b': 2,
  'a': 1, 'b': 1, 'c': 1,
  'a': 1, 'b': 1, 'c': 2,
  'a': 2, 'b': 1, 'c': 1,
  'a': 2, 'b': 1, 'c': 2
]

我尝试遍历所有对并找到相互比较的覆盖对,但时间复杂度等于O(n^2)是否有可能在线性时间内找到所有覆盖或唯一对?

这是我的代码示例 (O(n^2)):

public Set<Map<String, Integer>> find(Set<Map<String, Integer>> allPairs) 
  var results = new HashSet<Map<String, Integer>>();
  for (Map<String, Integer> stringToValue: allPairs) 
    results.add(stringToValue);
    var mapsToAdd = new HashSet<Map<String, Integer>>();
    var mapsToDelete = new HashSet<Map<String, Integer>>();
    for (Map<String, Integer> result : results) 
      var comparison = new MapComparison(stringToValue, result);
      if (comparison.isIntersected()) 
        mapsToAdd.add(comparison.max());
        mapsToDelete.add(comparison.min());
      
    
    results.removeAll(mapsToDelete);
    results.addAll(mapsToAdd);
  
  return results;

MapComparison 在哪里:

public class MapComparison 

    private final Map<String, Integer> left;
    private final Map<String, Integer> right;
    private final ComparisonDecision decision;

    public MapComparison(Map<String, Integer> left, Map<String, Integer> right) 
        this.left = left;
        this.right = right;
        this.decision = makeDecision();
    

    private ComparisonDecision makeDecision() 
        var inLeftOnly = new HashSet<>(left.entrySet());
        var inRightOnly = new HashSet<>(right.entrySet());

        inLeftOnly.removeAll(right.entrySet());
        inRightOnly.removeAll(left.entrySet());

        if (inLeftOnly.isEmpty() && inRightOnly.isEmpty()) 
            return EQUALS;
         else if (inLeftOnly.isEmpty()) 
            return RIGHT_GREATER;
         else if (inRightOnly.isEmpty()) 
            return LEFT_GREATER;
         else 
            return NOT_COMPARABLE;
        
    

    public boolean isIntersected() 
        return Set.of(LEFT_GREATER, RIGHT_GREATER).contains(decision);
    

    public boolean isEquals() 
        return Objects.equals(EQUALS, decision);
    

    public Map<String, Integer> max() 
        if (!isIntersected()) 
            throw new IllegalStateException();
        
        return LEFT_GREATER.equals(decision) ? left : right;
    

    public Map<String, Integer> min() 
        if (!isIntersected()) 
            throw new IllegalStateException();
        
        return LEFT_GREATER.equals(decision) ? right : left;
    

    public enum ComparisonDecision 
        EQUALS,
        LEFT_GREATER,
        RIGHT_GREATER,
        NOT_COMPARABLE,

        ;
    

【问题讨论】:

我不确定这是否可以在线性时间内完成,但如果您首先对数据进行排序,它可能在 O(n*log(n)) 中是可行的 相关关键字:在多目标优化领域,您尝试计算的子列表称为pareto front 我想知道是否将每个元素视为多项式(假设每个键值对都可以唯一地散列)是否可以让人们找到多项式算术的交集。元素中的每一对都是第 n 阶系数。但是,需要更清楚地说明问题集 - 例如a:1, b:2 等于 b:2, a:1 - a:1, c:1, d:1, b:1 是否包含 a:1, b:1。我建议让您的输入集更加全面。 我觉得 union-find 实际上可能是这个问题的近似值。 (至少是算法的查找部分),即 O(log*(n))。可以从使用元素数量最少的集合开始,并将它们用作“查找”算法的元素。这将导致与@Thomas 答案相同的时间复杂度。我不认为一个人可以走得更快,尽管这可能会引起争论。赞成这个问题,因为算法总是很有趣。编辑:根据cstheory.stackexchange.com/a/41388/62830 不可能在 O(n) 中做到这一点 我不了解 java,但Fast calculation of Pareto front in Python 的公认答案在 4 秒内解决了 10,000 个数组和每个数组 15 个键值的问题。这对您来说足够高效吗? 【参考方案1】:

假设列表中的每个元素都是唯一的。 (元素是具有键值对的对象。)对于每个唯一的键值对,存储包含它的列表元素集。按大小增加的顺序迭代元素。对于每个元素,通过查找包含它们的元素集并将该集与当前交集相交来搜索它的键值对。如果交叉点大小小于 2(假设交叉点至少包含一个元素,这是我们正在调查的元素),请尽早退出。根据数据,我们可以为这些集合使用位集(每个位将表示排序列表中映射元素的索引),这可以通过并行比较执行交集。同样根据数据,交叉点可以显着减少搜索空间。

Python 代码:

import collections

def f(lst):
  pairs_to_elements = collections.defaultdict(set)

  for i, element in enumerate(lst):
    for k, v in element.items():
      pairs_to_elements[(k, v)].add(i)

  lst_sorted_by_size = sorted(lst, key=lambda x: len(x))

  result = []

  for element in lst_sorted_by_size:
    pairs = list(element.items())
    intersection = pairs_to_elements[pairs[0]]
    is_contained = True

    for i in range(1, len(pairs)):
      intersection = intersection.intersection(pairs_to_elements[pairs[i]])
      if len(intersection) < 2:
        is_contained = False
        break

    if not is_contained:
      result.append(element)

  return result

输出:

lst = [
  'a': 1, 'b': 1,
  'a': 2, 'b': 1,
  'a': 2, 'b': 2,
  'a': 1, 'b': 1, 'c': 1,
  'a': 1, 'b': 1, 'c': 2,
  'a': 2, 'b': 1, 'c': 1,
  'a': 2, 'b': 1, 'c': 2
]

for element in f(lst):
  print(element)

"""
'a': 2, 'b': 2
'a': 1, 'b': 1, 'c': 1
'a': 1, 'b': 1, 'c': 2
'a': 2, 'b': 1, 'c': 1
'a': 2, 'b': 1, 'c': 2
"""

【讨论】:

【参考方案2】:

这里的算法可能更好或更差,具体取决于数据的形状。让我们通过将输入行表示为集合而不是映射来简化问题,因为本质上您只是将这些映射视为对/条目的集合。如果集合类似于[a1, b1] 等,则问题是等价的。目标是制作一个线性时间算法假设输入行的长度很短。设n为输入行数,k为行的最大长度;我们的假设是 k 远小于 n。

使用counting sort 按长度对行进行排序。 为结果初始化一个空的HashSet,其中集合的成员将是行(您需要一个不可变的、可散列的类来表示行)。 对于每一行: 从结果中删除行的power set 中的每个子集(如果存在)。 将行添加到结果中。

由于行是按长度排序的,因此可以保证如果行 i 是行 j 的子集,则行 i 将在行 j 之前添加,因此稍后将被正确删除从结果集中。一旦算法终止,结果集将准确包含那些不是任何其他输入行的子集的输入行。

计数排序的时间复杂度为 O(n + k)。每个幂集的大小最多为 2k,幂集的每个成员的长度最多为 k,因此每个HashSet 操作是 O(k) 时间。所以算法其余部分的时间复杂度为O(2k·kn),这在计数排序中占主导地位。

因此,如果我们将 k 视为常数,则整体时间复杂度为 O(n)。如果不是,那么当 k 2 n.

时,该算法仍将渐近优于朴素 O(n2·k) 算法*

*请注意,朴素算法是 O(n2·k) 而不是 O(n2),因为两行之间的每次比较需要 O (k) 时间。

【讨论】:

从技术上讲,这些地图被视为多重集。 如果你假设 k @Stef 我不关注 - 地图怎么可能像 a: 1, a: 1?我从未见过这样的地图,而且问题并不表明输入可能是这样的。 什么?我不知道你在上一条评论中在说什么? @Stef 映射被视为像a2, b1 这样的集合,即成对集合、映射条目集合。请注意,在 OP 的示例中,'a': 1, 'b': 1, 'c': 1 没有根据预期输出被'a': 2, 'b': 1, 'c': 2“覆盖”。

以上是关于元素成对比较的高效算法的主要内容,如果未能解决你的问题,请参考以下文章

是否有任何标准算法可以对向量元素进行成对比较?

过滤List中的重复元素有没有啥高效的算法(C#语言)?

使用JavaScript进行数组去重——一种高效的算法

LeetCode 0091.解码方法 - 动态规划+原地滚动(比较高效的算法)

高效的knn算法

用于成对比较和跟踪最大/最长序列的 STL 算法