【问题标题】:Codility Chromium challenge: count ascending sequences that alternate left and rightCodility Chromium 挑战:计算左右交替的升序序列
【发布时间】:2020-01-17 12:18:10
【问题描述】:

我在 Codility 编码挑战中遇到了很多困难:https://app.codility.com/programmers/challenges/chromium2017/

(基本问题是:给定一个整数列表,计算可能的升序数在i 的同一侧。例如,给定 [6, 2, 3, 4],从 2 开始,我们可以去 [2], [2, 3], [2, 4], [2, 6 ]、[2、3、6] 或 [2、4、6]。)

到目前为止,我只能考虑时间复杂度为 O(N^2) 而需要 O(N*log(N)) 的解决方案。尽管有人在 GitHub 上发布了解决方案,但我不知道发生了什么:

https://github.com/kalwar/Codility/blob/master/chromimum2017_solution.c

他似乎在来回进行仿射变换,但我缺乏对为什么这样做有效以及为什么可以用 O(N*Log(N)) 时间复杂度实现它的洞察力。我希望有人能解释一下。

我在下面发布了自己的解决方案(用 Java 编写):

final class Chromium {

    final long MODULUS = 1000000007L;

    static class Nest implements Comparable<Nest> {

        Nest(int index, int height) {
            this.index = index;
            this.height = height;
        }

        int index;
        int height;

        public int compareTo(Nest nest2) {
            return Integer.compare(height, nest2.height);
        }
    }

    /**
     * Calculates the possibilities based on the fact that it is a multiplication of the runs of consecutive nests
     * left and right of the nest in focus.
     */
    private long getPossibleWays(Nest[] orderedNests, int startIndex) {

        Nest startNest = orderedNests[startIndex];

        long ways = 0;
        long oppositeNumberOfWays = 0;

        boolean previousLeft = false;
        boolean first = true;
        int runLength = 0;

        for (int i = orderedNests.length - 1; i > startIndex; --i) {

            Nest n = orderedNests[i];
            boolean left = n.index < startNest.index;

            if (left != previousLeft && !first) {
                ways += (runLength * (oppositeNumberOfWays + 1)) % MODULUS;
                long w = oppositeNumberOfWays;
                oppositeNumberOfWays = ways;
                ways = w;

                runLength = 1;
            } else {
                runLength++;
            }

            first = false;
            previousLeft = left;
        }
        ways += (runLength * (oppositeNumberOfWays + 1)) % MODULUS;
        return 1 + ways + oppositeNumberOfWays;
    }

    public int solution(int[] H) {
        Nest[] nests = new Nest[H.length];
        for (int i = 0; i < H.length; ++i) {
            nests[i] = new Nest(i, H[i]);
        }

        // Sort the nests by height
        Arrays.sort(nests);

        long possibleWays = 0;

        for (int i = 0; i < nests.length; ++i) {
            possibleWays += getPossibleWays(nests, i);
            possibleWays = possibleWays % MODULUS;
        }

        return (int) possibleWays;
    }
}

【问题讨论】:

  • 为什么不联系 GitHub 解决方案的作者以获得基本解释?
  • 有一个 C# 解决方案 here,由 A Jordan 引用(并且可能编写)here

标签: algorithm linear-algebra


【解决方案1】:

我想我现在通过阅读数据结构,尤其是 Segment Trees with lazy updates,了解了 A. Jordan's solution

不过,对于这个问题,有很多见解要先做:

准备工作:

  • 按高度对巢进行分类很方便。完成此操作后,高度本身无关紧要,仅与嵌套数组中的位置相关。 (即所有高度将被标准化为递增整数 0,1,2,3,4...)。这意味着一旦对巢穴进行分类,我们就可以忘记它们的实际高度。
  • 跟踪原始数组中嵌套的索引,因为需要确定其他嵌套相对于它的相对左/右位置。在上图中,索引以红色显示,高度以黑色显示。

现在确定有效方式总数的一般策略是:

  • 计算任何Nest(k)(0 最终目的地的方式数> 并总结所有这些方式(这将是最终答案)。调用 Nest(k) 作为最终目的地 W(k) 的到达方式数

由于任何巢都只能从较低的巢到达,如果我们按升序(即按高度排序)遍历巢,我们应该能够根据之前的所有值计算 W(k)计算出来的。这是标准的动态规划方法。

现在考虑将 Nest(k) 作为终点的有效方式:

  • Nest(k) 开始并保持在那里,这有 1 种可能性
  • 从任何先前计算的方式开始,通过较低的Nest(i),其中 i > k 并且该方式的起始巢具有索引 j,其中 k
  • 从任何先前计算的方式开始,通过较低的Nest(i),其中 i 并且该方式的起始巢具有索引 j,其中 i

这可以被可视化为起始巢必须位于最终目的地和到达该目的地的巢之间的任何有效方式。

例如,取图中高度为 6 的 Nest(1)。它可以通过高度为 5 的 Nest(4) 从各个方向到达其中起始嵌套的索引位于最后的 Nest(1)Nest(4) 之间。请注意,该范围包括在内,因为它也可以从 Nest(4) 直接到达。然而,从 Nest(0) 开始并到 Nest(4) 的方式无法到达它,因为最后两次跳跃将在同一侧起始嵌套,这是不允许的。

现在调用从特定起始嵌套 Nest(i) 到其右侧 R(i) 的路数以及到其结束的路数离开 L(i) 并假设我们已经计算了所有这些可能的低巢方式。

然后:

  1. W(k) = 1 + sum([R(j) for j in k+1...N-1]) + sum([L(j) for j in 0...k -1])

这个计算的时间复杂度为 O(N)(对于每个 k),因此需要优化,但我们稍后会谈到。首先我们需要了解如何跟踪R(j)L(j)

这同样可以通过动态编程方法来完成:

假设我们知道当前从 Nest(i) 到其右侧 R(i) 的路数。每次我们在 Nest(i) 的左边遇到一个新的巢时,到左边 L(i) 的路数会增加它当前的 R(i)。这是因为在右边结束的每条路都有一个新的选择,可以更进一步到左边的新巢。 反过来当然也是正确的:R(i) = L(i) 如果遇到右侧的嵌套。

所以我们可以再提取两个计算:

  1. 对于 0...k-1 中的 i:R(i) += L(i)
  2. 对于 k+1...N-1 中的 i:L(i) += R(i)

最初,L(i) 和 R(i) 的路数都应设置为 1,以允许直接选项从起始嵌套到目标嵌套。

对于每个位于左侧的巢,增加正确的路数,如上所述,对于每个位于右侧的巢,增加相同的方式。

不过,这又需要 O(N) 次操作。

作为参考,下面列出了一个非常简单的 python 程序,它执行上述计算(给出正确的结果,但速度慢 O(N^2)):

def solution(H):
    MOD = 1000000007
    N = len(H)
    nests = [(height, index) for index, height in enumerate(H)]
    nests.sort()
    dp_left = [0] * N
    dp_right = [0] * N
    total = 0

    for nest in nests:
        index = nest[1]

        # Add 1 way for the current nest
        total += 1

        # Add all possible ways reaching this nest from the left
        for i in range(index):
            total += dp_left[i]
            total %= MOD

        # Add all possible ways reaching this nest from the right
        for i in range(index + 1, N):
            total += dp_right[i]
            total %= MOD

        # Initialize left and right ways to 1 for the current nest
        dp_right[index] = 1
        dp_left[index] = 1

        # Update the right possible ways for nests to the left
        for i in range(index):
            dp_right[i] += dp_left[i]
            dp_right[i] %= MOD

        # Update the left possible ways for nests to the right
        for i in range(index + 1, N):
            dp_left[i] += dp_right[i]
            dp_left[i] %= MOD

    return total % MOD

现在要达到 O(N Log(N)) 时间复杂度,我们必须找到一种方法来提高 O(N) 计算 1、2 和 3 的时间复杂度O(log(N)).

现在Lazy Segment Trees 的概念在这里发挥作用,它可以做到这一点。

按照说明,基本上更新和查询都可以转换为 O(log(N)) 操作。

现在要实现这一点,还有一些技术细节需要解决。特别是对于进行延迟更新,因为要应用的更新不是每个嵌套的常量值,而是取决于要更新的​​嵌套的索引。参考this question

这里是 a link to my solution,基于 A. Jordan's solution 用 Swift 重写,并进行了大量的代码清理和简化。

【讨论】:

    猜你喜欢
    • 2014-12-21
    • 1970-01-01
    • 2015-05-02
    • 2022-10-04
    • 1970-01-01
    • 2017-02-17
    • 1970-01-01
    • 2014-10-29
    • 1970-01-01
    相关资源
    最近更新 更多