从访谈中:删除n×n矩阵中的行和列,以最大化剩余值的总和

时间:2009-11-12 08:25:46

标签: algorithm multidimensional-array matrix

给出n×​​n实数矩阵。您可以擦除任何数字(从0到n)的行和任何数字(从0到n)的列,然后计算剩余条目的总和。想出一个算法,找出要擦除的行和列,以便最大化该总和。

16 个答案:

答案 0 :(得分:13)

问题是NP-hard。 (所以你不应该期望用多项式时间算法来解决这个问题。但是,仍然可能有(非多项式时间)算法略强于蛮力。)NP-硬度证明背后的想法是如果我们可以解决这个问题,那么我们可以在一般图中解决the clique problem。 (最大集团问题是在图中找到最大的成对连接顶点集。)

具体来说,给定任何带有 n 顶点的图形,让我们形成带有条目a[i][j]的矩阵A,如下所示:

  • a[i][j] = 1代表i == j(对角线条目)
  • a[i][j] = 0如果图表中存在边(i,j)(和i≠j
  • a[i][j] = -n-1如果图表中的边缘(i,j)

现在假设我们解决了删除某些行和列的问题(或等效地,保持某些行和列),以便最大化矩阵中条目的总和。然后答案给出了图表中的最大集团:

  1. 声明:在任何最佳解决方案中,没有保留行i和列j,其中边(i,j)不存在图表。证明:由于a[i][j] = -n-1和所有正条目的总和最多为n,因此选择(i,j)将导致负数。 (请注意,删除所有行和列将提供更好的总和,为0.。

  2. 声明:在(某些)最佳解决方案中,保留的行和列集是相同的。这是因为从任何最佳解决方案开始,我们只需删除尚未保留列i的所有行i,反之亦然。请注意,由于唯一的正条目是对角条目,我们不会减少总和(并且通过之前的声明,我们也不会增加它)。

  3. 所有这些意味着如果图表的最大集团大小为k,那么我们的矩阵问题就会有一个求和k的解决方案,反之亦然。因此,如果我们能够在多项式时间内解决我们的初始问题,那么集团问题也将在多项式时间内得到解决。这证明了最初的问题是NP-hard。 (实际上,很容易看出初始问题的决策版本 - 是否有一种方法可以删除一些行和列,使得总和至少为k - 是在NP中,所以(决定版本的))最初的问题实际上是NP-complete。)

答案 1 :(得分:9)

蛮力方法就是这样的:

  • 对于n行,有2个 n 子集。
  • 对于n列,有2个 n 子集。
  • 对于n×n矩阵,有2个 2n 子集。

0个元素是一个有效的子集,但很明显,如果你有0行或0列,则总数为0,所以实际上有2个 2n-2 +1个子集,但这没有什么不同。

因此,您可以通过强力计算每个组合作为O(a n )算法。快速。 :)

通过计算网格中的所有正数,可以更快地计算出最大可能值,并且可以做到这一点。如果这些数字碰巧形成一个有效的子矩阵(意味着您可以通过删除行和/或列来创建该集合),那么就是您的答案。

隐含的是,如果没有数字是负数,那么根据定义,完整矩阵就是答案。

另外,知道最高可能的最大值可以让你快速进行强力评估,因为如果你得到任何等于那个最大值的组合,那么这就是你的答案,你可以停止检查。

此外,如果所有数字都是非正数,则答案是最大值,因为根据定义,您可以将矩阵缩减为1 x 1矩阵,其中包含1个值。

这是一个想法:构造2 n -1 n×m矩阵,其中1 <= m <= n。一个接一个地处理它们。对于每个n x m矩阵,您可以计算:

  1. 最高可能的最高金额(按上述计算);和
  2. 是否没有数字是正面的,允许您快捷回答。
  3. 如果(1)低于当前计算的最高最大总和,则可以丢弃此n x m矩阵。如果(2)为真,那么您只需要与当前最高最大和进行简单比较。

    这通常被称为修剪技术

    你可以开始说n x n矩阵中的最高数字是起始最高总和,因为它显然可以是1 x 1矩阵。

    我确信你可以将它调整成一个(稍微更有效)递归的基于树的搜索算法,通过上述测试可以有效地消除(希望很多)不必要的搜索。

答案 2 :(得分:4)

我们可以通过将其建模为有向图来改进Cletus的广义蛮力解决方案。初始矩阵是图的起始节点;它的叶子是缺少一行或一列的所有矩阵,依此类推。它是一个图形而不是树,因为没有第一列和第一行的矩阵节点将有两个父节点 - 只有第一列或行丢失的节点。

我们可以通过将图形转换为树来优化我们的解决方案:在我们删除到达当前节点之前,删除列或行的子矩阵从来没有任何意义,因为该子矩阵将到达反正。

当然,这仍然是一种蛮力搜索 - 但我们已经消除了我们以不同顺序删除相同行的重复案例。

这是Python中的一个示例实现:

def maximize_sum(m):
  frontier = [(m, 0, False)]
  best = None
  best_score = 0

  while frontier:
    current, startidx, cols_done = frontier.pop()
    score = matrix_sum(current)
    if score > best_score or not best:
      best = current
      best_score = score
    w, h = matrix_size(current)
    if not cols_done:
      for x in range(startidx, w):
        frontier.append((delete_column(current, x), x, False))
      startidx = 0
    for y in range(startidx, h):
      frontier.append((delete_row(current, y), y, True))
  return best_score, best

这是280Z28示例矩阵的输出:

>>> m = ((1, 1, 3), (1, -89, 101), (1, 102, -99))
>>> maximize_sum(m)
(106, [(1, 3), (1, 101)])

答案 3 :(得分:2)

以简单的方式尝试:

我们需要条目集{A00,A01,A02,...,A0n,A10,...,Ann}的有效子集,其最大值为和。

首先计算所有子集(功率集)。

有效子集是幂集的成员,对于每两个包含的条目Aij和A(i + x)(j + y),还包含元素A(i + x) j和Ai(j + y)(它们是由Aij和A(i + x)(j + y)跨越的矩形的剩余角)。

Aij ...
 .       .   
 .       .
    ... A(i+x)(j+y)     

通过这种方式,您可以从电源组中消除无效的电源,并找到剩余电量最大的一个。

我确信可以通过改进功率集生成算法来改进,以便仅生成有效的子集,并避免步骤2(调整功率集)。

答案 4 :(得分:2)

由于没有人要求有效的算法,因此请使用强力:生成可以通过从原始矩阵中删除行和/或列来创建的每个可能的矩阵,选择最佳的矩阵。一个稍微高效的版本,很可能被证明仍然是正确的,只生成那些删除的行和列包含至少一个负值的变体。

答案 5 :(得分:1)

  • 创建一个n-by-1向量RowSums和一个n-by-1向量ColumnSums。将它们初始化为原始矩阵的行和列总和。 O(N²)
  • 如果任何行或列的总和为负数,请移除编辑:具有最小值的那个,并更新另一个方向的总和以反映其新值。为O(n)
  • 当没有行或列的总和小于零时停止。

这是一个改进另一个答案的迭代变体。它在O(n²)时间运行,但在其他答案中提到的某些情况下失败,这是该问题的复杂性限制(矩阵中有n 2个条目,甚至找到最小值,你必须检查每个单元一次)

编辑:以下矩阵没有负行或列,但未最大化,我的算法无法捕获它。

1     1     3        goal     1    3
1   -89   101        ===>     1  101
1   102   -99

以下矩阵确实有负行和列,但我的算法选择了错误的行和列。

 -5     1    -5      goal     1
  1     1     1      ===>     1
-10     2   -10               2

                     mine
                     ===>     1     1     1

答案 6 :(得分:1)

我认为有些攻击角度可能会对蛮力有所改善。

  1. memoization,因为有很多不同的编辑序列会到达同一个子矩阵。
  2. 动态编程。因为矩阵的搜索空间是高度冗余的,我的直觉是会有一个可以节省大量重复工作的DP配方
  3. 我认为这是一种启发式方法,但我无法确定它:

    如果有一个负数,您可以按原样取矩阵,删除负数列,或删除其行;我认为任何其他“移动”都不会导致更高的金额。对于两个负数,您的选项是:不删除,删除一个,删除另一个,或删除两者(删除行为是通过砍掉行或列)。

    现在假设矩阵只有一个正数,其余都是&lt; = 0。除了正面条目,你显然想删除所有内容。对于只有2个正条目且其余<= 0的矩阵,选项是:什么都不做,减少到1,减少到另一个,或者减少到两者(导致1x2,2x1或2x2矩阵) )。

    一般来说,这最后一个选项会崩溃(想象一个有50个正数和50个负数的矩阵),但根据你的数据(少数负数或少数正数),它可以提供一个捷径。

答案 7 :(得分:1)

计算每行和每列的总和。这可以在O(m)(其中m = n ^ 2)

中完成

虽然存在总和为负的行或列,但删除具有小于零的最小总和的行或列。然后重新计算每行/列的总和。

一般的想法是,只要有一行或一列与nevative相加,删除它将导致更大的整体价值。您需要一次删除一个并重新计算,因为删除那一行/列会影响其他行/列的总和,它们可能会或可能不会有负数。

这将产生最佳的最大结果。运行时间为O(mn)或O(n ^ 3)

答案 8 :(得分:0)

我无法真正生成一种算法,但对我而言,如果它是一个起点,它就像动态编程一样“闻起来”。

答案 9 :(得分:0)

大编辑:老实说,我认为没有办法评估矩阵并确定它是最大化的,除非它是完全正面的。

也许它需要分支,并了解所有消除路径。如果昂贵的淘汰将在以后实现更多的更好的淘汰,那么你永远都不会。如果找到理论最大值,我们可以短路,但除了任何算法之外,我们必须能够前进和后退。我已经调整了原始解决方案以通过递归实现此行为。

双重秘密编辑:如果每次迭代都不需要找到所有负面元素,那么它也会在降低复杂度方面取得很大进展。考虑到它们在调用之间没有太大变化,将它们的位置传递给下一次迭代更有意义。

采用矩阵,矩阵中当前负元素的列表,以及初始矩阵的理论最大值。返回矩阵的最大总和以及到达那里所需的移动列表。在我看来,移动列表包含一个移动列表,表示从上一个操作的结果中删除的行/列。

即:r1,r1

会翻译

-1  1  0           1  1  1
-4  1 -4           5  7 1
 1  2  4    ===>  
 5  7  1  
  1. 如果矩阵之和是理论最大值,则返回

  2. 查找所有负面元素的位置,除非传入空集。

  3. 计算矩阵之和并将其与空移动列表一起存储。

    • 对于每个元素的否定:

      1. 计算该元素的行和列的总和。

      2. 克隆矩阵并消除哪个集合具有该克隆的最小总和(行/列),请注意该操作作为移动列表。

      3. 克隆负面元素列表,并删除上一步中所采取行动所造成的任何影响。

      4. 递归调用此算法,提供克隆矩阵,更新的负元素列表和理论最大值。将返回的移动列表附加到生成传递给递归调用的矩阵的操作的移动列表中。

      5. 如果递归调用的返回值大于存储的总和,请将其替换并存储返回的移动列表。

  4. 返回存储的总和并移动列表。

  5. 我不确定它是否比蛮力方法更好或更差,但它现在处理所有测试用例。即使是最大值包含负值的那些。

答案 10 :(得分:0)

这是优化问题,可以通过基于simulated annealing的迭代算法近似解决:

表示法:C是列数。

对于J迭代:

  1. 查看每一列并计算切换它的绝对好处(如果当前打开则将其关闭,如果当前关闭则将其打开)。这给你C值,例如-3,1,4。贪婪的确定性解决方案只会选择最后一个动作(切换最后一列以获得4的好处),因为它在本地改善了目标。但这可能会将我们锁定在局部最优状态。相反,我们概率地选择三种行为中的一种,概率与收益成比例。为此,通过将它们放入Sigmoid function并进行标准化,将它们转换为概率分布。 (或者使用exp()而不是sigmoid()?)因此,对于-3,1,4,从sigmoid得到0.05,0.73,0.98,在归一化后得到0.03,0.42,0.56。现在根据概率分布选择动作,例如以概率0.56切换最后一列,以概率0.42切换第二列,或以微小概率0.03切换第一列。

  2. 对行执行相同的过程,从而切换其中一行。

  3. 迭代J迭代直到收敛。

    在早期迭代中,我们也可以使这些概率分布中的每一个更加均匀,这样我们就不会在早期就陷入错误的决策。所以我们将非标准化概率提高到1 / T的幂,其中T在早期迭代中很高并且慢慢减小直到它接近0.例如,从上面的0.05,0.73,0.98,到1/10的结果导致0.74 ,0.97,1.0,归一化后为0.27,0.36,0.37(因此它比原来的0.05,0.73,0.98更均匀)。

答案 11 :(得分:0)

显然NP-Complete(如上所述)。鉴于此,如果我不得不提出我能解决问题的最佳算法:

  • 尝试一些二次整数规划的迭代,将问题表达为:SUM_ij a_ij x_i y_j,x_i和y_j变量被约束为0或1.对于某些矩阵,我认为这将很快找到解决方案,对于最难的情况,它不会比蛮力更好(而且不会太多)。

  • 并行(并使用大部分CPU),使用近似搜索算法生成越来越好的解决方案。在另一个答案中提出了模拟退火,但是我已经对类似的组合优化问题进行了研究,我的经验是禁忌搜索可以更快地找到好的解决方案。如果您使用逐步更新单个更改成本的技巧,这可能在最短时间内在不同的“潜在更好”解决方案之间徘徊方面接近最优(请参阅我的论文“图表统治,禁忌搜索和足球池问题”) “)。

  • 使用上面第二个方面的最佳解决方案,通过避免搜索下限比它更差的可能性来引导第一个。

显然,这并不能保证找到最大的解决方案。但是,通常情况下这是可行的,否则它将提供非常好的局部最大解决方案。如果某人有实际情况需要进行此类优化,那么这就是我认为最有效的解决方案。

停止确定问题可能是NP-Complete在面试中看起来不太好看! (除非工作在复杂性理论中,但即便如此,我也不会。)你需要提出好的方法 - 这就是这样的问题。要了解你在压力下能想出什么,因为现实世界经常需要处理这些事情。

答案 12 :(得分:0)

是的,这是NP完全问题。

很难轻易找到最佳的子矩阵,但我们可以很容易地找到一些更好的子矩阵。

假设我们在矩阵中给出m个随机点作为“feed”。然后让他们按照以下规则自动扩展:

如果在Feed-matrix中添加一个新行或列,请确保总和为增量。

,然后我们可以比较m子矩阵找到最好的子矩阵。

答案 13 :(得分:0)

假设n = 10。

蛮力(所有可能的行集x所有可能的列集)都需要

2 ^ 10 * 2 ^ 10 = ~1,000,000个节点。

我的第一种方法是将此视为树搜索,并使用

正条目的总和是子树中每个节点的上限

作为修剪方法。结合贪婪算法以便廉价地生成良好的初始界限,这平均产生了大约80,000个节点的答案。

但还有更好的方法!我后来意识到了

修复一些行X的选择。 计算出这组行的最佳列现在是微不足道的(如果列X在行X中的总和为正,则保留一列,否则丢弃它。)

所以我们可以对所有可能的行选择蛮力;这需要2 ^ 10 = 1024个节点。

添加修剪方法平均降低到600个节点。

在遍历行集树时保持“列总和”并逐步更新它们应该允许每个节点的计算(矩阵之和等)为O(n)而不是O(n ^ 2)。给出总复杂度为O(n * 2 ^ n)

答案 14 :(得分:-1)

取每行和每列计算总和。对于2x2矩阵,这将是:

2    1

3    -10

行(0)= 3 行(1)= -7 Col(0)= 5 Col(1)= -9

撰写新矩阵

Cost to take row      Cost to take column
       3                      5

      -7                      -9

拿出你需要的东西,然后重新开始。

您只需在新矩阵上查找负值。这些值实际上是从整体矩阵值中减去的。当没有更多的负“SUMS”值被取出时它会终止(因此所有列和行都会向最终结果求值)

在nxn矩阵中,我认为是O(n ^ 2)Log(n)

答案 15 :(得分:-1)

function pruneMatrix(matrix) {
  max = -inf;
  bestRowBitField = null;
  bestColBitField = null;
  for(rowBitField=0; rowBitField<2^matrix.height; rowBitField++) {
    for (colBitField=0; colBitField<2^matrix.width; colBitField++) {
      sum = calcSum(matrix, rowBitField, colBitField);
      if (sum > max) {
        max = sum;
        bestRowBitField = rowBitField;
        bestColBitField = colBitField;
      }
    }
  }
  return removeFieldsFromMatrix(bestRowBitField, bestColBitField);
}

function calcSumForCombination(matrix, rowBitField, colBitField) {
  sum = 0;
  for(i=0; i<matrix.height; i++) {
    for(j=0; j<matrix.width; j++) {
      if (rowBitField & 1<<i && colBitField & 1<<j) {
        sum += matrix[i][j];
      }
    }
  }
  return sum;
}