在 3n − ⌊lg n⌋ − 3 的二进制矩阵中找到所需的索引

Find a desired index in a binary matrix in 3n − ⌊lg n⌋ − 3

给定一个 n×n 矩阵 M,其中每个条目要么是 0 要么是 1。提出一个算法来确定是否 ∃i, 1 ≤ i ≤ n,使得 M[i,j] = 0 和 M [j,i] = 1, ∀j, 1 ≤j≤ n ∧ j!=i, 以检查 M 的一个条目作为关键操作。你的算法最多只能检查 3n − ⌊lg n⌋ − 3 个条目 M.

提示:将“检查 M[i, j]”与比较 i 与 j 联系起来。最初,每个索引都可能是所需的索引 i。将潜在索引i的个数从n减为1。然后验证该索引是否确实是想要的索引i。

有聪明的人可以遮挡更多的灯光或提示来解决这个问题吗?我是这个领域的新手,想到的任何方法都以 O(n^2) 结束。有什么建议吗?

我考虑过寻找任何模式的基本案例:

接下来,我想把问题简化为证明M是或不是对称的,这意味着存在i使得Mij != Mji(或Mij = Mji)并且条件将被满足,因为M是二进制的.我的想法是看看我是否可以在线性时间内证明或反驳它,但我还没有找到它的算法。

将您的规则分成两部分。索引 i 有效当且仅当它同时满足:

  1. 行条件:M[i, j] = 0 for all j != i
  2. 列条件:M[k, i] = 1 for all k != i

这让我们注意到索引 i 的条件为真等同于 i'th 行全为零,而 i'th 列全为 1,除了它们相交于 M[i, i] 的地方,它可以有任何值。

对于具有 i=3 个有效(并且 non-numbered 个条目无关紧要)的插图:

. . . 1 . .
. . . 1 . .
0 0 0 x 0 0
. . . 1 . .
. . . 1 . .
. . . 1 . .

现在,考虑任何一对 i, ji != j

  • 如果 M[i, j] = 1,我们知道 i 不是有效索引,因为它不符合行条件。
  • 如果 M[i, j] = 0,我们知道 j 不是有效索引,因为它不符合列条件。

因此,对于矩阵元素的每个查询 M[i, j],我们可以从考虑中排除一个索引。 这也意味着最多一个索引是有效的。否则,如果 i1i2 都有效,根据上述规则测试 M[i1, i2],我们就会产生矛盾。


下面是整个算法在长度为 3 的矩阵上的示例。锦标赛树结构仅取决于叶节点的数量,稍后详述。

0 0 1
1 0 1
0 1 1

这里,第一个匹配(即兄弟叶节点)是(0, 1)。查询M[0, 1] returns 0,所以第二个索引1被淘汰。我们删除叶子的父节点,并用剩余索引的节点替换它,0。然后 (0, 2) 被匹配为新的兄弟叶节点。查询 M[0, 2] returns 1,因此第一个索引 0 被删除。 2 是唯一可能有效的剩余索引。

现在,要测试2是否有效,我们需要知道M[2, 0], M[2, 1], M[0, 2], M[1, 2]的值。我们已经查询了 M[0, 2] 的值,因此我们不再重复该查询。这给了我们 2 + 3 = 5 总查询,正好满足您的限制 3*n - 3 - floor(log_2(n)) = 9 - 3 - 1 = 5.


一旦我们最多拥有一个潜在的有效索引,我们就需要 2n - 2 对其列和行的查询来测试这两个条件。目前,这给了我们 n-1 个查询来消除除一个索引以外的所有索引,然后 2n-2 个查询来测试该索引,总共 3n-3 太多了。

但是,我们可以使用最初的 'elimination' 查询来计算后来的 'test' 查询。创建一个完整的完整二叉树,其中叶节点是索引0, 1, ... n-1。将其用作锦标赛树来确定初始消除查询:每个叶节点与其兄弟叶节点重复配对,直到剩下一个节点。

对于 n 个叶节点,叶节点的最小深度(因此任何索引参与的 matchups/elimination 查询的最小数量)在任何完整的二叉树中总是 floor(lg_2(n))。所以我们要查询的总数最多为
(n-1) + (2n-2) - floor(lg_2(n)),也就是 (3n-3) - floor(lg_2(n)).


这是算法的 Python 实现,应该与命令式伪代码相差不远。它特别冗长,并且分解成小函数,因此对 M 的所有访问都通过特殊的 query 函数完成并记录。这应该更清楚地表明您实际上满足了总查询的限制。

def find_valid_index(matrix: List[List[int]]) -> Optional[int]:
    """Given square binary matrix, return the index i
    such that, for all j != i,
    matrix[i, j] = 0 and matrix[j, i] = 1, or None if none exist."""

    num_rows = len(matrix)
    assert num_rows == len(matrix[0])

    if num_rows == 1:
        return 0

    fulfilled_queries = {}

    def query(i: int, j: int) -> int:
        if (i, j) not in fulfilled_queries:
            fulfilled_queries[i, j] = matrix[i][j]

        return fulfilled_queries[i, j]

    def index_to_eliminate(i: int, j: int) -> int:
        assert i != j

        return j if query(i, j) == 0 else i

    def is_valid_index(i: int) -> bool:
        """Test full row and column for validity"""
        for j in range(num_rows):
            if j == i:
                continue
            if query(i, j) == 1 or query(j, i) == 0:
                return False
        return True

    candidate_indices = list(range(num_rows))

    # Find distance from nearest power of two at most num_rows
    leftover_leafs = num_rows - 2**(math.floor(math.log2(num_rows)))

    if leftover_leafs > 0:
        eliminated_indices = set()
        for k in range(leftover_leafs):
            index1 = candidate_indices[2*k]
            index2 = candidate_indices[2*k+1]
            eliminated_indices.add(index_to_eliminate(index1, index2))

        candidate_indices = [x for x in candidate_indices
                             if x not in eliminated_indices]

    while len(candidate_indices) > 1:
        assert len(candidate_indices) % 2 == 0

        eliminated_indices = set()
        for k in range(0, len(candidate_indices), 2):
            index1 = candidate_indices[k]
            index2 = candidate_indices[k + 1]
            eliminated_indices.add(index_to_eliminate(index1, index2))

        candidate_indices = [x for x in candidate_indices
                             if x not in eliminated_indices]

    if len(candidate_indices) == 0:
        return None

    if is_valid_index(candidate_indices[0]):
        return candidate_indices[0]

    return None