【问题标题】:Last remaining number最后剩余号码
【发布时间】:2019-12-04 16:38:54
【问题描述】:

我在一次采访中被问到这个问题。

给定一个正整数数组“arr”和数组的起始索引“k”。删除 k 处的元素并以循环方式在数组中跳转 arr[k] 步。重复此操作,直到只剩下一个元素。找到最后剩下的元素。

我想到了使用有序映射的 O(nlogn) 解决方案。是否有可能的 O(n) 解决方案?

【问题讨论】:

  • 我想用笔和纸,有点想你可以找到一个 O(1) 的解决方案。
  • 我在这个问题上思考并付出了很多时间。这与约瑟夫斯问题非常相似。但我无法找到类似的解决方案来解决这个问题。
  • @Sumit Kumar 在我看来,它看起来就像约瑟夫斯问题。为什么你认为它不一样?
  • 在约瑟夫问题中,跳跃的长度是恒定的。这取决于我们当前所在的数组索引。由于删除元素时索引会发生变化,因此很难通过删除来获取数组元素。
  • @MrSmith42 当问题取决于任意大小 n 的输入中的特定元素时,我不明白我们如何可以在这里使用 O(1) 过程。

标签: algorithm data-structures time-complexity


【解决方案1】:

我想不出O(n) 解决方案。但是,我们可以通过使用treap 或增强的BST 来获得O(n log n) 的平均时间,每个节点的子树大小都有一个值。 treap 使我们能够在 O(log n) 平均时间内找到并删除 kth 条目。

例如,A = [1, 2, 3, 4]k = 3(正如 Sumit 在 cmets 中提醒我的那样,使用数组索引作为树中的值,因为它们是有序的):

          2(0.9)
         /     \
      1(0.81)   4(0.82)
               /
              3(0.76)

查找并删除第三个元素。从 2 开始,大小 = 2(包括左子树)。向右走。左子树大小为 1,加起来为 3,所以我们找到了第 3 个元素。删除:

          2(0.9)
         /     \
      1(0.81)   4(0.82)

现在我们从包含n - 1 = 3 元素的数组中的第三个元素开始,并从那里寻找第三个元素。我们将使用零索引与我们的模运算相关联,因此模数 3 中的第三个元素将是 2 和 2 + 3 = 5 mod 3 = 2,第二个元素。我们立即找到它,因为根及其左子树的大小为 2。删除:

          4(0.82)
         /
      1(0.81)

现在我们从模数 2 中的第二个元素开始,即 1,我们正在添加 2。3 mod 2 是 1。删除第一个元素,我们剩下 4 作为最后一个元素。

【讨论】:

  • @SumitKumar 是的,当然。我们将使用数组索引作为树中的值。
【解决方案2】:

我的猜测是这个问题没有O(n) 的解决方案,因为它似乎涉及做一些不可能的事情。您需要在线性时间内解决这个问题的显而易见的事情是一个数据结构,如数组,它公开了对有序值集合的两个操作:

  1. O(1) order-preserving 从数据结构中删除。
  2. O(1) 查找数据结构中第 n 个未删除的项目。

然而,这样的数据结构已经被正式证明是不存在的;请参阅“Optimal Algorithms for List Indexing and Subset Rank”及其引文。不能证明如果解决某些问题的自然方法涉及使用不可能的数据结构,那么问题本身可能是不可能的,但这种直觉通常是正确的。

无论如何,O(n log n) 有很多方法可以做到这一点。下面是在数组中维护未删除范围树的实现。 GetIndex() 如果项目已从中删除,则返回原始数组的索引,给定数组的从零开始的索引。这样的树不是自平衡的,因此在最坏的情况下会有O(n) 操作,但在平均情况下DeleteGetIndex 将是O(log n)

namespace CircleGame
{
    class Program
    {
        class ArrayDeletes
        {
            private class UndeletedRange
            {
                private int _size;
                private int _index;
                private UndeletedRange _left;
                private UndeletedRange _right;

                public UndeletedRange(int i, int sz)
                {
                    _index = i;
                    _size = sz;
                }

                public bool IsLeaf()
                {
                    return _left == null && _right == null;
                }

                public int Size()
                {
                    return _size;
                }

                public void Delete(int i)
                {
                    if (i >= _size)
                        throw new IndexOutOfRangeException();

                    if (! IsLeaf())
                    {
                        int left_range = _left._size;
                        if (i < left_range)
                            _left.Delete(i);
                        else
                            _right.Delete(i - left_range);
                        _size--;
                        return;
                    }

                    if (i == _size - 1)
                    {
                        _size--; // Can delete the last item in a range by decremnting its size
                        return;
                    }

                    if (i == 0)  // Can delete the first item in a range by incrementing the index
                    {  
                        _index++;
                        _size--;
                        return;
                    }

                    _left = new UndeletedRange(_index, i);
                    int right_index = i + 1;
                    _right = new UndeletedRange(_index + right_index, _size - right_index);
                    _size--;
                    _index = -1; // the index field of a non-leaf is no longer necessarily valid.
                }

                public int GetIndex(int i)
                {
                    if (i >= _size)
                        throw new IndexOutOfRangeException();

                    if (IsLeaf())
                        return _index + i;

                    int left_range = _left._size;
                    if (i < left_range)
                        return _left.GetIndex(i);
                    else
                        return _right.GetIndex(i - left_range);
                }

            }

            private UndeletedRange _root;

            public ArrayDeletes(int n)
            {
                _root = new UndeletedRange(0, n);
            }

            public void Delete(int i)
            {
                _root.Delete(i);
            }

            public int GetIndex(int indexRelativeToDeletes )
            {
                return _root.GetIndex(indexRelativeToDeletes);
            }

            public int Size()
            {
                return _root.Size();
            }
        }

        static int CircleGame( int[] array, int k )
        {
            var ary_deletes = new ArrayDeletes(array.Length);
            while (ary_deletes.Size() > 1)
            {
                int next_step = array[ary_deletes.GetIndex(k)];
                ary_deletes.Delete(k);
                k = (k + next_step - 1) % ary_deletes.Size();
            }
            return array[ary_deletes.GetIndex(0)];
        }

        static void Main(string[] args)
        {
            var array = new int[] { 5,4,3,2,1 };
            int last_remaining = CircleGame(array, 2); // third element, this call is zero-based...
        }
    }
}

还要注意,如果已知数组中的值是有界的,因此它们总是小于小于 n 的某个 m,则有很多 O(nm) 算法——例如,仅使用循环链表。

【讨论】:

  • 您最后一次使用循环链表的观察很棒。谢谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-04
  • 1970-01-01
  • 2012-07-14
  • 2013-08-07
  • 2019-04-22
  • 2013-10-02
相关资源
最近更新 更多