更有效的算法来查找两组的OR

时间:2017-06-03 20:47:05

标签: algorithm binary-operators

给定n行和m10列的矩阵,需要找出可以选择的行数对,以使其OR11111....m times

示例:

1 0 1 0 1
0 1 0 0 1
1 1 1 1 0

答案:

2 ---> OR of row number [1,3] and [2,3]

鉴于nm可以是<= 3000的订单,这个问题的解决效率如何?

PS:我已经尝试过一种天真的O(n*n*m)方法。我在考虑一个更好的解决方案。

5 个答案:

答案 0 :(得分:3)

<强> 1。琐碎的解决方案 琐碎的算法(您已经发现但未发布)是采用n行的所有(n选择2)组合,或者它们,并查看它是否有效。这是O(n^2 * m)。编码看起来像:

for (i = 0; i < n; ++i)
  for (j=i+1; j < n; ++ j) {
    try OR of row[i] with row[j] to see if it works, if so record (i,j)
  }

<强> 2。不断加速 您可以通过将位打包到单词中来将运行时间提高一个字长。这仍然提供相同的渐近,但实际上是64位机器上64位加速的因素。上面的评论已经注意到这一点。

第3。启发式加速 我们可以做启发式来进一步改善实践的时间,但没有渐近保证。考虑通过汉明重量对行进行排序,前面的汉明重量最小,最后的汉明重量最大(运行时间O(m * n * log m ))。然后,您只需要将低权重行与高权重行进行比较:具体而言,权重需要为>= m。然后搜索看起来像这样:

for (i = 0; i < n; ++i)
  for (j=n-1; j > i; --j) /* go backwards to take advantage of hmwt */
  {
    if ( (hmwt(row[i]) + hmwt(row[j])) < m)
      break;
    try OR of row[i] with row[j] to see if it works, if so record (i,j)
  }

<强> 4。采取更好的方法 可以提供更好回报的另一种方法是选择低汉明重量的列。然后将行组合成两组:在该列中具有1的组(组A)与在该列中具有0的组(组B)。然后你只需要考虑行的组合,其中一个来自A组而另一个来自B组,或者两者都来自A组(感谢@ruakh来抓住我的疏忽)。这些方面的东西应该有很多帮助。但同样这仍然是渐近相同的更糟糕的情况,但在实践中应该更快(假设我们并非所有组合都是答案)。

<强> 5。可以做什么的极限 很容易构建一些例子,其中有效的向量对的数量是O(n^2),因此感觉很难打败O(m*n^2更糟糕的情况。我们应该寻求的是一种与工作对的数量有某种关系的解决方案。上面的启发式方法正朝这个方向发展。如果有一个汉明重量较小的列h,则上面的第4点会将运行时间降至O(h*n*m + h^2*m)。如果h明显小于n,那么您将获得重大改进。

答案 1 :(得分:2)

扩展TheGreatContini的想法:

首先尝试

让我们把它看作是找到属于AxB的组合,有A组和B组。这些组合必须满足条件或条件,但我们还假设a的汉明重量至少与b一样大(以避免一些重复)。

现在将A拆分为A0(以0开头的行)和A1(以1开头的行)。为B做同样的事情。我们现在已将问题简化为三个较小的问题:A0xB1,A1xB1和A1xB0。如果A和B相同,A0xB1和A1xB0是相同的,所以我们只需要做一个。这三个子问题不仅比第一个更小,我们还完全检查了第一列,从现在开始可以忽略它。

要解决这些子问题,我们可以使用相同的方法,但现在使用第2,3列,......在某些时候,要么我们将检查所有列,要么#A和#B将为1。

根据实施情况,更快停止可能会更有效率。此时,我们可以对剩余的组合进行详尽的检查。但请记住,如果我们已经检查了k列,那么每个组合只会花费m-k。

更好的列选择

正如TheGreatContini建议的那样,我们可以选择导致最小子问题的列,而不是选择第一列。在每个步骤中找到此列的成本相当高,但权重可以在开头计算一次,然后用作最佳列的估计值。然后我们可以正常重新排列列使用算法,完成后再重新排列它们。

确切的最佳列是A中零的数量,即B中零的数量最大的列。

汉明重量修剪

我们知道a和b的汉明重量之和必须至少为m。而且由于我们假设a是最高的汉明重量,我们可以删除汉明重量小于m / 2的所有值。 (这给出的加速可能是可以忽略不计的,我不确定)。计算所有汉明重量的成本为O(m * n)。

高效分割

如果我们对行进行排序,使用二分算法可以更快地完成分组。这也可以导致在存储器中有效地表示集合。我们可以指定最小和最大行。排序可以在O(n * m * log(n))中完成。然后可以在log(n)中完成拆分。

这里有一些代码无法编译,但应该给出正确的想法。

private List<Row> rows;
public int findFirstOne(int column, int start, int end){
    if(rows.get(start).get(column) == 1) return start;
    if(rows.get(end).get(column) == 0) return -1;
    while(start < end){
        int mid = (start+end)/2;
        if(rows.get(mid).get(column) == 0){
            start = mid+1;
        }else{
            end = mid;
        }
    }
    return start;
}

<强>复杂性

在以下计算中,忽略了更好的列选择的效果,因为它对最坏情况的效率几乎没有改进。但是,在平均情况下,它可以通过尽快减少搜索空间来提供大量改进,从而使其他检查更快。

算法运行时间以n²m为界。 但是,我发现的最糟糕的例子都是O(n * log(n)* m)。

首先,矩阵的排序将为行的O(n * log(n)* m),并且可选地,对列进行排序将为O(n * m + m * log(m))。

然后,创建子问题。让我们先高估一下。我们需要最多m次细分,并且深度为i的完整级别细分的成本可能被高估为log(n)* 3 ^ i(每个细分的成本乘以细分数)。这导致总共O(log(n)* 3 ^ m)。

它还必须保持3 ^ i&lt; =n²/ 2,因为这是可能的最大组合数,因此对于大m,它在O(n 2 * log(n)* m)处上限。我很难找到一个实际上表现得像这样的例子。

我认为假设许多子问题很早就变得微不足道是合理的。通向O(log(n)* m * n)(如果有人想检查这个,我真的不确定)。

答案 2 :(得分:2)

这是一个可能会有更糟糕的渐近(甚至是平均)行为的离题概念 - 但它以一种有趣的方式推广,至少提供了一种不同的方法。此问题可以视为doesn't support DNS SRV records。 n行中的每一行都包含一组来自集合{1,2,...,m}的值S,对应于行具有值1的列索引。问题的任务是查找集合其集合形成{1,2,... m}的不相交分区的行。当精确封面中只有两个这样的行时,这些行是您正在寻找的那种行的二元对立面。但是,可能存在更复杂的精确封面,例如涉及三行的封面:

0 1 0 0 1
1 0 0 0 0
0 0 1 1 0

确切的封面问题会查找所有这样的确切封面,并且是NP完全问题。规范的解决方案是exact cover problem,由Donald Knuth创建。

答案 3 :(得分:2)

如果我没弄错的话,以下应该是O(n * m):

  • 对于每列,计算此列中具有“1”的行的索引集,并将其存储为从列索引到行索引集的映射
  • 对于每一行,计算可以“完成”该行的行索引集(通过在行具有“0”的列中添加“1”)。这可以通过计算在步骤1中针对相应列
  • 计算的集合的交集来完成
  • 计算完成的行索引

对于你的例子:

1 0 1 0 1
0 1 0 0 1
1 1 1 1 0
  1. 每列的“1”行的索引是

    • 第0列:[0,2]
    • 第1栏:[1,2]
    • 第2栏:[0,2]
    • 第3栏:[2]
    • 第4栏:[0,1]
  2. 用于“填充”每一行的所有索引集的联合是

    • 第0行:[2]
    • 第1行:[2]
    • 第2行:[]
  3. 总共2个。

    为什么人们可以争论运行时间的主要原因是m集合的交叉点的计算最多n可以被认为是O(m * n) ),但我认为这些集合的大小将是有限的:条目是1或0,当有许多1 s(并且大小很大)时,相交的集合较少,反之亦然 - 但我没有在这里做严格的证明......

    我用来玩这个基于Java的实现(以及一些基本的“测试”):

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.LinkedHashMap;
    import java.util.LinkedHashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Random;
    import java.util.Set;
    
    public class SetOrCombinations
    {
        public static void main(String[] args)
        {
            List<Integer> row0 = Arrays.asList(1, 0, 1, 0, 1);
            List<Integer> row1 = Arrays.asList(1, 1, 0, 0, 1);
            List<Integer> row2 = Arrays.asList(1, 1, 1, 1, 0);
            List<Integer> row3 = Arrays.asList(0, 0, 1, 1, 1);
            List<List<Integer>> rows = Arrays.asList(row0, row1, row2, row3);
            run(rows);
    
            for (int m = 2; m < 10; m++)
            {
                for (int n = 2; n < 10; n++)
                {
                    run(generateRandomInput(m, n));
                }
            }
        }
    
        private static void run(List<List<Integer>> rows)
        {
            int m = rows.get(0).size();
            int n = rows.size();
    
            // For each column i: 
            // Compute the set of rows that "fill" this column with a "1"
            Map<Integer, List<Integer>> fillers =
                new LinkedHashMap<Integer, List<Integer>>();
            for (int i = 0; i < m; i++)
            {
                for (int j = 0; j < n; j++)
                {
                    List<Integer> row = rows.get(j);
                    List<Integer> list = 
                        fillers.computeIfAbsent(i, k -> new ArrayList<Integer>());
                    if (row.get(i) == 1)
                    {
                        list.add(j);
                    }
                }
            }
    
            // For each row, compute the set of rows that could "complete"
            // the row (by adding "1"s in the columns where the row has
            // a "0"). 
            int count = 0;
            Set<Integer> processedRows = new LinkedHashSet<Integer>();
            for (int j = 0; j < n; j++)
            {
                processedRows.add(j);
                List<Integer> row = rows.get(j);
                Set<Integer> completers = new LinkedHashSet<Integer>();
                for (int i = 0; i < n; i++)
                {
                    completers.add(i);
                }
                for (int i = 0; i < m; i++)
                {
                    if (row.get(i) == 0)
                    {
                        completers.retainAll(fillers.get(i));
                    }
                }
                completers.removeAll(processedRows);
                count += completers.size();
            }
    
            System.out.println("Count "+count);
            System.out.println("Ref.  "+bruteForceCount(rows));
        }
    
        // Brute force
        private static int bruteForceCount(List<List<Integer>> lists)
        {
            int count = 0;
            int n = lists.size();
            for (int i = 0; i < n; i++)
            {
                for (int j = i + 1; j < n; j++)
                {
                    List<Integer> list0 = lists.get(i);
                    List<Integer> list1 = lists.get(j);
                    if (areOne(list0, list1))
                    {
                        count++;
                    }
                }
            }
            return count;
        }
    
        private static boolean areOne(List<Integer> list0, List<Integer> list1)
        {
            int n = list0.size();
            for (int i=0; i<n; i++)
            {
                int v0 = list0.get(i);
                int v1 = list1.get(i);
                if (v0 == 0 && v1 == 0)
                {
                    return false;
                }
            }
            return true;
        }
    
    
        // For testing
        private static Random random = new Random(0);
        private static List<List<Integer>> generateRandomInput(int m, int n)
        {
            List<List<Integer>> rows = new ArrayList<List<Integer>>();
            for (int i=0; i<n; i++)
            {
                List<Integer> row = new ArrayList<Integer>();
                for (int j=0; j<m; j++)
                {
                    row.add(random.nextInt(2));
                }
                rows.add(row);
            }
            return rows;
        }
    
    }
    

答案 4 :(得分:0)

这是一种算法,它利用了在同一列中具有零的两行自动取消作为伙伴的资格的知识。我们在当前行中的零越少,我们访问其他行的次数就越少;但是我们整体上的零越多,我们访问其他行的次数就越少。

create two sets, one with a list of indexes of all rows, and the other empty
assign a variable, total = 0

从右到左,从底行到顶部迭代每一行(也可以按其他顺序迭代;我只是按照这种方式描绘它)。

while row i is not the first row:
  call the non-empty set A and the empty set dont_match
  remove i, the index of the current row, from A

  traverse row i:
    if A is empty:
      stop the traversal
    if a zero is encountered:
      traverse up that column, visiting only rows listed in A:
        if a zero is encountered:
          move that row index from A to dont_match

  the remaining indexes in A point to row partners to row i
  add their count to total and move the elements from the
  shorter of A and dont_match to the other set

return total