给定n
行和m
列1
和0
列的矩阵,需要找出可以选择的行数对,以使其OR
为11111....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]
鉴于n
和m
可以是<= 3000
的订单,这个问题的解决效率如何?
PS:我已经尝试过一种天真的O(n*n*m)
方法。我在考虑一个更好的解决方案。
答案 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 0 1
0 1 0 0 1
1 1 1 1 0
每列的“1”行的索引是
用于“填充”每一行的所有索引集的联合是
总共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