我们能否在O(n*n)以下的时间复杂度内进行计算?(使用nlogn或n的时间复杂度)

9

这是一个非常著名的跨国公司问我的问题。问题如下...

输入一个由0和1组成的二维N*N数组。如果A(i,j) = 1,则与第i行和第j列对应的所有值都将变为1。如果已经有一个1,则它仍然保持为1。

例如,如果我们有以下数组:

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

我们应该得到以下输出结果:
 1 1 1 1 1
 1 1 1 1 1
 1 1 1 1 0
 1 1 1 1 1
 1 1 1 1 0

输入矩阵中有很多未填充的部分。
这是否可能在小于O(N^2)的时间内完成?
另一个条件是没有提供额外的空间。我想知道是否有办法使用空间<= O(N)来实现复杂度。
附:我不需要复杂度为O(N*N)的答案。这不是一道作业题。我已经尝试过很多次,但没有得到合适的解决方案,想在这里获取一些想法。请将打印内容排除在复杂性之外。
我的大致想法是动态消除遍历的元素数量,限制它们在2N左右。但是我不能得到一个合适的想法。

6
也许他已经想过,但不明白为什么一个“非常非常著名的星期一夜战”会问这样的问题? - Stephen Canon
“sparsely populated” 究竟是什么意思? - Rex Kerr
1
稀疏矩阵是指只有 O(n) 个非零元素的矩阵。 - Phil Miller
@x3ro:跨国公司本身通常不会四处询问人们理论软件问题。将MNC视为个体是很自然的想法。 - David Thornley
由于似乎没有解决方案,争论你的基本解决方案不是O(nn)的一个愚蠢方法是说矩阵最初具有O(nn)个元素,因此你所做的实际上是线性的 :) 大O符号取决于输入,但在学术/教科书中,将其视为O(n*n)非常流行。如果你没有更多的选择/想法,你得说些类似这样的话。 - satyajit
显示剩余4条评论
13个回答

8
在最坏的情况下,您可能需要将N * N - N个位从0切换为1以生成输出。看起来您几乎无法避免O(N * N)。

但实际上,矩阵是稀疏填充的。 - Flash
4
你是正确的;然而,一个简单的单位矩阵,同样是稀疏的,会导致 N*N-N 个位被切换为 1。在这里,稀疏性似乎没有在最坏情况下提供任何优势。 - kbrimington
2
@Tgr 是正确的。一旦你注意到结果矩阵具有感兴趣的属性,即如果a(i,j)=0和a(i',j'),那么a(i,j')和a(i',j)应该等于0。你只需要存储否定输出矩阵的有效i和有效j列表。在你的例子中,i=[2,4],j=[4]。这样,身份证就是一个长度为N的2个列表,O(2*N)= O(N) :-) - mb14
2
@kbrimington:我明白你的意思,但如果你使用“普通”的二维矩阵,即使创建一个空数组也是O(NxN),所以在这种情况下,这个问题甚至都不值得一提。我不是稀疏矩阵方面的专家,但我想你可以使用任何你认为适合表示矩阵的格式。而且,无论你选择什么格式,将其显示为二维矩阵都将是O(NxN)。但如果想要找到解决问题的方法,我认为你可以自由地使用任何你想要的表示方式,甚至对于输入和输出结果可以有不同的表示方式。你只需选择最有效的一种。 - mb14
我发布了一个最坏情况为O(N*N)的答案...看看是否可能。 - Flash
显示剩余8条评论

6
显然,输出矩阵及其否定版本都不必稀疏(可以采用半个第一行设置为1且其他均为0的矩阵来证明),因此时间取决于可用于输出的格式。(假设输入是元素列表或相当物品,否则将无法利用矩阵稀疏的优势。)
一个简单的解决方案需要O(M + N)的空间和时间(其中M是输入矩阵中1的数量):取两个长度为N的数组,填充1,遍历所有输入中的1,并删除第一个数组中的X坐标以及第二个数组中的Y坐标。 输出是这两个数组,它们清楚地定义了结果矩阵:如果第一个数组的X坐标和第二个数组的Y坐标都为0,则其(X,Y)坐标为0。
更新:根据语言,您可以使用一些巧妙的方法通过多次引用同一行来返回常规2D数组。例如,在PHP中:
// compute N-length arrays $X and $Y which have 1 at the column 
// and row positions which had no 1's in the input matrix
// this is O(M+N)
$result = array();
$row_one = array_fill(0,N,1);
for ($i=0; $i<N; $i++) {
    if ($Y[$i]) {
         $result[$i] = &$row_one;
    } else {
         $result[$i] = &$X;
    }
}
return $result;

当然,只要你不尝试对其进行写入操作,这就是一个普通的数组。

+1. 我认为重要的是要认识到稀疏意味着M/N(填有1的对角线或行或列的等效数量)不随N增加而增加,并且比N小得多。我认为,如果没有除了普通的NxN数组之外的输出数据结构,那么性能优于O(N ^ 2)是无法实现的。 - Andre Holzner

6
我想你可以针对最佳情况进行优化,但我倾向于认为最坏情况仍然是O(N*N):最坏情况是所有元素都是0的数组,你将不得不检查每一个元素。
优化方法涉及到在找到“1”时跳过一行或一列(我可以提供细节,但你说你不关心O(N*N)),但除非你有元数据来指示整个行/列为空,或者除非你有一种SIMD风格的方式来一次检查多个字段(例如,如果每行都对齐4个,并且你可以读取32位数据,或者如果你的数据以位掩码的形式存在),否则你总是必须处理全零数组的问题。

我实际上有一些关于N*N的解决方案...使用多处理器不是一个选项... - Flash
我没有说多处理器。SIMD = 单指令,多数据,即一条指令可以访问多个数据。 - EboMike

3
输入矩阵可能是稀疏的,但是除非您能以稀疏格式(即最初设置为(i,j)对列表)获得它,否则仅读取输入将消耗Ω(n^2)时间。即使有稀疏输入,也很容易最终要写O(n^2)个输出。如果您可以输出一组已设置的行和列的列表,则可以缩短到线性时间。当您的算法实际上必须产生比“是”或“否”更实质性的结果时,就没有魔法可言。
Mcdowella在另一个答案中的评论提出了另一种替代输入格式:游程编码。对于稀疏输入,这显然需要不超过O(n)的时间来读取它(考虑0和1之间的转换次数)。但是,从那里开始就会失败。考虑作为以下结构的输入矩阵:
0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 . . . 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 . . .
.     .
.       .
.         .

即在第一行交替出现0和1,其他地方均为0。显然稀疏,因为总共只有n/2个1。但是,RLE输出必须在每一行中重复此模式,导致O(n^2)的输出。


3

由于需要检查矩阵的每个条目,因此最坏情况始终是N * N。

通过额外使用小的2 * N存储空间,您可以在O(N * N)中执行操作。只需为每行创建一个掩码,为每列创建另一个掩码-扫描数组并在进行时更新掩码。然后再次扫描以根据掩码填充结果矩阵。

如果您正在执行输入矩阵正在更改的操作,则可以为输入的每行和列存储非零条目的计数(而不是简单的掩码)。然后,当输入中的条目更改时,相应地更新计数。在那时,我会完全放弃输出矩阵,并直接查询掩码/计数,而不是甚至维护输出矩阵(如果您确实想保留它,则可以在小于N * N的时间内更新它)。因此,加载初始矩阵仍将是O(N * N),但更新可以更少。


1
我喜欢跟踪已设置行的掩码和已设置列的掩码的想法。您可以以运行长度编码格式进行输入和输出,例如 1 个 '1',10 个 '0',3 个 '1' 等等。在更新已设置行的掩码和已设置列的掩码时,请记住以 RLE 格式记录输入内容。然后,在重复输入内容时,考虑行和列掩码。事实上,您也可以使用 RLE 格式来跟踪掩码。使用 RLE,对于几乎所有输入为 0 和几乎所有输出为 1 的情况,您都可以获得紧凑的形式。 - mcdowella

2

你说:

我们应该得到以下输出...

因此,您需要输出完整的矩阵,它具有N^2个元素。这是O(N*N)。

问题本身不是O(N*N):您不必计算和存储整个矩阵:您只需要两个大小为N的向量L和C:

L[x]为1表示第x行是由1组成的行,否则为0;

C[x]为1表示第x列是由1组成的列,否则为0;

您可以在O(N)中构造这些向量,因为初始矩阵是稀疏的;您的输入数据将不是矩阵,而是包含每个非零元素的坐标(行、列)的列表。在读取此列表时,您设置L[line]=1和C[column]=1,问题就解决了:如果L[l]==1或C[c]==1,则M[l,c]==1。


我已经提到了,请不要考虑打印的复杂性。 - Flash

2

大家好,

感谢mb14的评论,我认为我可以在少于O(NN)的时间内解决它... 最坏情况下需要O(NN)...

实际上,我们有一个给定的数组,假设

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

让我们有两个大小为N的数组(这将是最坏的情况)...一个专门用于索引行,另一个用于列... 将a [i] [1] = 0的值放在一个数组中,然后将a [1] [j] = 0的值放在另一个数组中。
然后仅取这些值并检查第二行和列...通过这种方式,我们得到了只有0的行和列的值...
行数组中的值的数量给出了结果数组中0的数量,而点a [行数组值] [列数组值]给出了这些点....
我们可以在O(N²)以下解决它,最坏的情况是O(N²)...正如我们所看到的,数组(大小为N)会减少....
我对一些数组进行了此操作,并获得了所有结果... :)
如果我哪里错了,请纠正我...
谢谢你们所有人的评论...你们都非常有帮助,我也学到了很多东西... :)

1

这里显然有高达O(N^2)的工作要做。在矩阵中。

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

所有位必须设为1,并且N*(N-1)个位不设为一(在这个5x5的情况下是20)。

相反地,您可以提出一个算法,总是以O(N^2)的时间完成:沿着顶部行和列求和,如果行或列得到非零答案,则填充整个行或列;然后解决较小的(N-1)x(N-1)问题。

所以存在至少需要N^2时间的情况,并且任何情况都可以在N^2时间内解决,而不需要额外的空间。


0
#include<stdio.h>

包含

int main() { int arr[5][5] = { {1,0,0,0,0}, {0,1,1,0,0}, {0,0,0,0,0}, {1,0,0,1,0}, {0,0,0,0,0} }; int var1=0,var2=0,i,j;

for(i=0;i<5;i++)
   var1 = var1 | arr[0][i];

for(i=0;i<5;i++)
   var2 = var2 | arr[i][0];

for(i=1;i<5;i++)
   for(j=1;j<5;j++)
      if(arr[i][j])
         arr[i][0] = arr[0][j] = 1;

for(i=1;i<5;i++)
   for(j=1;j<5;j++)
          arr[i][j] = arr[i][0] | arr[0][j];

for(i=0;i<5;i++)
   arr[0][i] = var1;

for(i=0;i<5;i++)
   arr[i][0] = var2;

for(i=0;i<5;i++)
{
   printf("\n");             
   for(j=0;j<5;j++)
      printf("%d ",arr[i][j]);
}

getch();

}

这个程序只使用了2 4个临时变量(var1,var2,i和j),因此在时间复杂度为O(n^2)的情况下以恒定的空间运行。我认为不可能以小于O(n^2)的时间复杂度解决这个问题。


0

这取决于你的数据结构。

对于行,只有两种可能情况:

  • 如果输入中存在一个元素(i, _),则第i行填充为1
  • 所有其他行都相同:即第j个元素为1,当且仅当输入中存在一个元素(_, j)。

因此,结果可以被紧凑地表示为对行的引用数组。由于我们只需要两行,结果也只消耗O(N)内存。例如,在Python中可以实现如下:

def f(element_list, N):
  A = [1]*N
  B = [0]*N
  M = [B]*N
  for row, col in element_list:
    M[row] = A
    B[col] = 1
  return M

一个示例调用可能是
 f([(1,1),(2,2),(4,3)],5)

带着结果

[[0, 1, 1, 1, 0], [1, 1, 1, 1, 1], [1, 1, 1, 1, 1], [0, 1, 1, 1, 0], [1, 1, 1, 1, 1]]

重要的一点是这里没有复制数组,即M[row]=A只是一个引用赋值。因此,复杂度为O(N+M),其中M是输入的长度。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接