【问题标题】:Algorithm to count the number of valid blocks in a permutation [duplicate]计算排列中有效块数的算法[重复]
【发布时间】:2010-07-26 22:37:28
【问题描述】:

可能重复:
Finding sorted sub-sequences in a permutation

给定一个数组 A,它包含 1,2,...,n 的排列。一个子块 A[i..j]
如果数组 A 中的所有数字都出现在 A[i..j]
是连续的数字(可能不按顺序)。

给定一个数组 A= [ 7 3 4 1 2 6 5 8] 有效块是 [3 4], [1,2], [6,5],
[3 4 1 2]、[3 4 1 2 6 5]、[7 3 4 1 2 6 5]、[7 3 4 1 2 6 5 8]

所以上述排列的计数是 7。

给出一个 O(n log n) 算法来计算有效块的数量。

【问题讨论】:

  • 一个快速的谷歌搜索显示它之前在 SO 和另一个网站上被问过,但没有人弄清楚。我没有考虑太多,但如果可能的话,我猜它涉及一些巧妙的数学技巧。恭喜谁解决了...
  • 天啊,动态编程伤了我的脑筋。
  • 你不能比O(N^2) 做得更好,因为在排序序列的琐碎情况下,你将得到O(N^2) 结果。有趣的问题。想到动态编程或分而治之。
  • @Nikita Rybak,啊......他们只是想要一个总数。一定是一个非常聪明的算法。这可能是一个可怕的面试问题 - 大多数优秀的开发人员不会在给定的时间内解决它,而那些能够解决的人可能无论如何也无法在那里工作。
  • 复制:stackoverflow.com/questions/1824388/…,具有相同的数据和公式

标签: algorithm permutation


【解决方案1】:

好的,因为我在一个相关问题上放了 200 个赏金:Finding sorted sub-sequences in a permutation 所以我暂时不能离开 cmets。

我有一个想法: 1) 找到所有排列组。它们是:(78)、(34)、(12)、(65)。与群论不同,它们的顺序和位置,以及它们是否相邻都很重要。因此,组 (78) 可以表示为结构 (7, 8, false),而 (34) 将表示为 (3,4,true)。我正在使用 Python 的元组表示法,但实际上最好为组使用整个类。这里的真或假意味着连续或不连续。如果(max(gp1) == min(gp2) + 1 or max(gp2) == min(gp1) + 1) and contigous(gp1) and contiguos(gp2),则两个组是“相邻的”。这不是union(gp1, gp2) 连续的唯一条件,因为(14)(23) 很好地组合成(14)。这对于算法课作业来说是一个很好的问题,但对于面试来说却是一个糟糕的问题。我怀疑这是家庭作业。

【讨论】:

    【解决方案2】:

    只是一些想法:

    乍一看,这听起来不可能:一个完全排序的数组会有 O(n2) 个有效的子块。

    因此,您一次需要计算多个有效子块。检查子块的有效性是 O(n)。检查子块是否完全排序也是O(n)。完全排序的子块包含 n·(n - 1)/2 个有效子块,您无需进一步分解此子块即可对其进行计数。

    现在,整个数组显然总是有效的。对于分而治之的方法,您需要将其分解。有两个可能的断点:最高元素的位置和最低元素的位置。如果您在其中一个点将数组一分为二,包括包含第二个到极值元素的部分中的极值,则不会有有效的子块穿过该断点。

    通过始终选择产生更均匀分割的极值,这对于“随机”数组应该工作得很好(平均 O(n log n))。但是,当您的输入类似于 (1 5 2 6 3 7 4 8) 时,我会看到问题,这似乎会产生 O(n2) 行为。 (1 4 7 2 5 8 3 6 9) 会类似(我希望你能看到模式)。我目前看不出捕捉这种更坏情况的技巧,但它似乎需要其他拆分技术。

    【讨论】:

    • 一切皆有可能,是我们的思想限制了我们。
    【解决方案3】:

    这个问题确实涉及一些“数学技巧”,但一旦你得到它就相当简单了。但是,我的解决方案的其余部分不符合 O(n log n) 标准。

    数学部分

    对于任意两个连续数字,它们的和为2k+1,其中k 是最小元素。对于三个,它是3k+3,4:4k+6,对于N,这样的数字是Nk + sum(1,N-1)。因此,您需要两个可以同时完成的步骤:

    1. 创建所有子数组的总和。
    2. 确定子数组的最小元素。

    动态规划部分

    使用前一行条目的结果构建两个表,以构建每个连续行的条目。不幸的是,我完全错了,因为这仍然需要 n^2 子数组检查。啊!

    【讨论】:

      【解决方案4】:

      我的建议

      STEP = 2 // 考试人数

      B [0,0,0,0,0,0,0,0]

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

      VALID(A,B) - 如果无效则移动一

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

      VALID(A,B) - 如果是有效的移动一个和步骤

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

      有效(A,B)

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

      步骤 = 3

      B [1,1,1,0,0,0,0,0] 不行

      B [0,1,1,1,0,0,0,0] 好的

      B [0,0,0,0,1,1,1,0] 不行

      步骤 = 4

      B [1,1,1,1,0,0,0,0] 不行

      B [0,1,1,1,1,0,0,0] 好的

      .....

      CON <- 0
      STEP <- 2
      i <- 0
      j <- 0
      WHILE(STEP <= LEN(A)) DO
       j <- STEP
       WHILE(STEP <= LEN(A) - j) DO
        IF(VALID(A,i,j)) DO
         CON <- CON + 1
         i <- j + 1
         j <- j + STEP
        ELSE
         i <- i + 1
         j <- j + 1
        END
       END
       STEP <- STEP + 1
      END
      

      检查所有元素是否连续的有效方法

      从未测试过,但可能没问题

      【讨论】:

      • 在我看来至少 O(n^2)。实际上甚至可能是O(n^3)
      • 我想我可以让VALID 在 O(1) 中运行,如果在 O(N^2) 中预先填充了一些结构。例如,它可以是映射到值1 &lt;&lt; low | 1 &lt;&lt; low + 1 | 1 &lt;&lt; low + 2 ... | 1 &lt;&lt; high -1 | 1 &lt;&lt; high 的元组(低,高)的哈希表。该值可以即时计算,因此摊销运行时间将为O(N^2) + O(NumResults),但numResults \in O(N^2),因此将为O(N^2)。这仅对 64 位整数最有用(这是一种可能性)。如果 N 的大小可以任意增长,那么就不得不担心 BigInt 的长度,然后就需要了。
      • @IVlad 是的,它是 n^2 ......昨天我有点累了,我已经忘记了这个小细节 ;-),但这是一个开始。 Maby 今天我会想出一些新的东西;-)。
      • @Hamish Grubijan 你是对的,但我认为你让工作复杂化了。我们假设我们必须验证一个置换表 1..n 的结果数,所以我们需要检查左边或右边的值是否大于 2 如果我们发现像这整个部分这样的情况不是有效 (ABS(A[i] - A[i+1]) == 1 || ABS(A[i] - A[i-1]) == 1) 不
      【解决方案5】:

      原始数组不包含重复项,因此它本身必须是一个连续的块。让我们将此块称为 (1 ~ n)。我们可以通过检查第一个元素是 1 还是 n 来测试块 (2 ~ n) 是否是连续的,即 O(1)。同样,我们可以通过检查最后一个元素是 1 还是 n 来测试块 (1 ~ n-1)。

      我无法将其完全塑造成一个可行的解决方案,但也许它会帮助某人......

      【讨论】:

      • 在示例中您有 7,因此它不是 1 或 n。所以不。
      • @Vash,是的,我知道这不是正确的答案。我只是想加入讨论,希望有人能明白。
      【解决方案6】:

      像其他人一样,我只是把它扔掉......它适用于下面的单个示例,但是 YMMV!

      这个想法是计算非法子块的数量,然后从可能的总数中减去它。我们通过依次检查每个数组元素并排除包含该元素但不包含其前任或后继元素的子块来计算非法元素。

      1. Foreach i in [1,N],计算 B[A[i]] = i.

      2. 令 Count = 长度>1 的子块的总数,即 N-choose-2(每个可能的开始和结束索引组合一个)。

      3. 对于 i,考虑 A[i]。忽略边缘情况,令 x=A[i]-1,令 y=A[i]+1。 A[i] 不能参与任何不包括 x 或 y 的子块。令 iX=B[x] 和 iY=B[y]。这里有几个案例需要独立处理。一般情况是iX&lt;i&lt;iY&lt;i。在这种情况下,我们可以消除子块 A[iX+1 .. iY-1] 和所有包含 i 的中间块。有 (i - iX + 1) * (iY - i + 1) 个这样的子块,所以称这个数字为 Eliminate。 (其他情况留给读者练习,就像那些边缘情况一样。)设置计数 = 计数 - 消除。

      4. 返回计数。

      总成本似乎是 N *(步骤 2 的成本)= O(N)。

      WRINKLE:在第 2 步中,我们必须注意不要多次消除每个子区间。我们可以通过仅消除完全或部分位于位置 i 右侧的子区间来实现这一点。

      示例:
      A = [1, 3, 2, 4] B = [1, 3, 2, 4]

      初始计数 = (4*3)/2 = 6

      i=1:A[i]=1,所以需要有 2 个子块。我们可以从考虑中排除[1,3]。消除 = 1,计数 -> 5。

      i=2:A[i]=3,所以需要有 2 或 4 个子块。这排除了 [1,3],但我们在从 i=1 向右看时已经考虑了它。消除 = 0。

      i=3: A[i] = 2,所以需要有 [1] 或 [3] 的子块。我们可以从考虑中排除[2,4]。消除 = 1,计数 -> 4。

      i=4: A[i] = 4,所以我们需要包含 [3] 的子块。这排除了 [2,4],但我们在从 i=3 向右看时已经考虑了它。消除 = 0。

      Final Count = 4,对应子块[1,3,2,4]、[1,3,2]、[3,2,4]和[3,2]。

      【讨论】:

        【解决方案7】:

        (这是尝试执行此 N.log(N) 最坏的情况。不幸的是,它是错误的 - 它有时会低估。它错误地假设您可以通过仅查看相邻的较小有效块对来找到所有块。在事实上,你必须查看三元组、四元组等,才能获得所有更大的块。)

        您可以使用表示子块的结构和子块队列来完成。

          struct
        c_subblock
        {
          int           index   ;  /* index into original array, head of subblock */
          int           width   ;  /* width of subblock > 0 */
          int           lo_value;
          c_subblock *  p_above ;  /* null or subblock above with same index */
        };
        

        分配与原始数组大小相同的子块数组,并初始化每个子块以使其中恰好有一个项目。随时将它们添加到队列中。如果你从数组 [ 7 3 4 1 2 6 5 8 ] 开始,你会得到一个这样的队列:

        队列:( [7,7] [3,3] [4,4] [1,1] [2,2] [6,6] [5,5] [8,8] )

        subbblock [7,7] 的 { index, width, lo_value, p_above } 值为 { 0, 1, 7, null }。

        现在很容易。原谅 c-ish 伪代码。

        loop {
          c_subblock * const p_left      = Pop subblock from queue.
          int          const right_index = p_left.index + p_left.width;
          if ( right_index < length original array ) {
            // Find adjacent subblock on the right.
            // To do this you'll need the original array of length-1 subblocks.
            c_subblock const * p_right = array_basic_subblocks[ right_index ];
            do {
              Check the left/right subblocks to see if the two merged are also a subblock.
                If they are add a new merged subblock to the end of the queue.
              p_right = p_right.p_above;
            }
            while ( p_right );
          }
        }
        

        这将找到我所想的一切。通常是 O(N log(N)),但对于完全排序或反排序的列表,它将是 O(N^2)。不过,我认为有一个答案——当您构建原始子块数组时,您会查找已排序和反排序序列并将它们添加为基础级子块。如果您要保持计数,则将基本级别的计数增加 (width * (width + 1))/2。这将为您提供包括所有长度为 1 的子块的计数。

        之后只需使用上面的循环,弹出并推送队列。如果您要计算,则必须在左右子块上都有一个乘数,并将它们相乘以计算增量。乘数是最左边(对于 p_left)或最右边(对于 p_right)基本级子块的宽度。

        希望这很清楚,不要太麻烦。我只是把它敲出来,所以它甚至可能是错误的。 [后注。毕竟这行不通。请参阅下面的注释。]

        【讨论】:

        • FTFY,使用 1010 按钮,缩进 4 个空格,或突出显示并按 ctrl-k 格式化代码。对于内联代码,请使用反引号。
        • 谢谢斯蒂芬,我听从了你的建议。
        • 这个算法在数组 {2, 4, 1, 3} 上的表现如何?
        • Maciej,好点子。该算法毕竟不起作用,如果您不计算琐碎的子块,它会给您 4 而不是 5. 0 而不是 1。参加设计/代码审查会很棒。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-05-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多