【问题标题】:I need a better algorithm to solve this我需要一个更好的算法来解决这个问题
【发布时间】:2012-10-27 08:21:42
【问题描述】:

这是问题(链接:http://opc.iarcs.org.in/index.php/problems/FINDPERM):

数字 1, ..., N 的排列是这些数字的重新排列。例如
2 4 5 1 7 6 3 8
是 1,2, ..., 8 的排列。当然,
1 2 3 4 5 6 7 8
也是 1, 2, ..., 8 的排列。
与 N 的每个排列相关的是一个长度为 N 的特殊正整数序列,称为它的反转序列。该序列的第 i 个元素是严格小于 i 并且在此排列中出现在 i 右侧的数字 j 的数量。对于排列
2 4 5 1 7 6 3 8
反转序列是
0 1 0 2 2 1 2 0
第二个元素是 1,因为 1 严格小于 2,并且在这个排列中它出现在 2 的右边。同样,第 5 个元素是 2,因为 1 和 3 严格小于 5,但在此排列中出现在 5 的右侧,依此类推。
再举一个例子,排列的反转序列
8 7 6 5 4 3 2 1

0 1 2 3 4 5 6 7
在这个问题中,您将得到一些排列的反转序列。你的任务是从这个序列重建排列。

我想出了这个代码:

#include <iostream>

using namespace std;

void insert(int key, int *array, int value , int size){
    int i = 0;
    for(i = 0; i < key; i++){
        int j = size - i;
        array[ j ] = array[ j - 1 ];
    }
    array[ size - i ] = value;
}

int main(){

    int n;
    cin >> n;
    int array[ n ];
    int key;

    for( int i = 0; i < n; i++ ){
        cin >> key;
        insert( key, array, i + 1, i);
    }

    for(int i = 0;i < n;i ++){
        cout << array[i] << " ";
    }

return 0;
} 

它工作正常,并且对 70% 的测试用例给出了正确答案,但超过了剩余的时间限制。 有没有其他更快的算法来解决这个问题?

【问题讨论】:

  • "您可以假设 N ≤ 100000。"
  • 如果不需要使用 C++,python 有集合、排列和重新排列(我认为它们的命名不同)。您还可以在 python 中的集合之间进行交叉联合和差异。如果你从未尝试过 python,那就试试吧,它对数学来说非常棒。

标签: c++ algorithm sequences


【解决方案1】:

您的算法具有复杂的O(N^2) 操作,因此对于大小为10^5 的数组,它需要太多时间来执行。我尝试描述更好的解决方案:

我们有N 号码。让我们调用逆数组I。解决这个问题,我们需要知道K-th 位置从排列的末尾到哪里,仍然是空闲的(让我们调用这个函数F(K))。首先,我们将号码N 放在F(I[N] + 1) 的位置,然后将号码N-1 放在F(I[N-1] + 1) 的位置,以此类推。

F可以计算如下:声明数组M,大小为N:1 1 1 ... 1,定义S(X) = M[1] + M[2] + ... + M[X]S 被称为前缀总和F(K) 等于 N 加上 1 减去如此低的 XS(X) = K。每次我们将数字 Z 放置到位置 N + 1 - X(for K = I[Z] + 1) 时,我们将零放置到 M[X]。为了比O(N) 更快地找到X,我可以建议使用Binary Indexed Trees 来计算O(logN) 时间内的前缀总和,并使用Binary Search 来找到X 等于某个预定义值的S(X)

这种算法的总复杂度是O(N(log(N))^2)This is Ruby 中的实现(您可以直接在 ideone 中使用它:更改输入、运行并检查输出):

# O(n(log(n))^2) solution for http://opc.iarcs.org.in/index.php/problems/FINDPERM

# Binary Indexed Tree (by Peter Fenwick)
# http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees
class FenwickTree

  # Initialize array 1..n with 0s
  def initialize(n)
    @n = n
    @m = [0] * (n + 1)
  end

  # Add value v to cell i
  def add(i, v)
    while i <= @n
      @m[i] += v
      i += i & -i
    end
  end

  # Get sum on 1..i
  def sum(i)
    s = 0
    while i > 0
      s += @m[i]
      i -= i & -i
    end
    s
  end

  # Array size
  def n
    return @n
  end

end

# Classical binary search
# http://en.wikipedia.org/wiki/Binary_search_algorithm
class BinarySearch

  # Find lower index i such that ft.sum(i) == s
  def self.lower_bound(ft, s)
    l, r = 1, ft.n
    while l < r
      c = (l + r) / 2
      if ft.sum(c) < s
        l = c + 1
      else
        r = c
      end
    end
    l
  end

end

# Read input data
n = gets.to_i
q = gets.split.map &:to_i

# Initialize Fenwick tree
ft = FenwickTree.new(n)
1.upto(n) do |i|
  ft.add i, 1
end

# Find the answer
ans = [0] * n
(n - 1).downto(0) do |i|
  k = BinarySearch.lower_bound(ft, q[i] + 1)
  ans[n - k] = i + 1
  ft.add k, -1
end
puts ans.join(' ')

O(N(log(N))) 时间的解决方案也存在。它使用某种Binary Search Tree:我们在顶点上创建带有数字1, 2, 3, ... N 的BST,然后我们可以在O(log(N)) 中按值找到K-th 数字,并在O(log(N)) 时间删除顶点。

std::set 的解决方案也可能存在,但我现在想不出。

PS。我还可以建议您阅读一些有关算法和奥林匹克竞赛的书籍,例如 Skienna(编程挑战)或 Cormen(算法简介)

PPS.对于我之前描述的错误解决方案非常抱歉

【讨论】:

  • 对于输入 0 1 0 2 2 1 2 0,您的算法输出 0 4 5 2 7 1 3 8,而不是 2 4 5 1 7 6 3 8。您能解释一下原因吗?
  • 很抱歉,我的解决方案是错误的......让我尝试再给你一个
  • 谢谢!在更改了以前算法的某些部分后,它已经起作用了,但是它也是 n^2 次​​span>
  • (很抱歉再次打扰您)您的算法运行良好,但仍然和我最初的算法一样慢,我仍然获得 70% 的分数。
  • 是的,因为您的 get_x 函数需要执行 ~N 次操作 + 您调用它 N 次。所以结果是 O(N^2) 算法。但我建议您在二进制索引树上使用二进制搜索:请参阅上面的代码 - k = BinarySearch.lower_bound(ft, q[i] + 1) 每次调用时都需要 ~(log(N))^2 操作。我也叫它N次,所以总体复杂度是O(N((LogN)^2))
【解决方案2】:

最昂贵的部分显然是在结果数组中移动多达 100 000 个元素。

如果您将该数组拆分为更多块,您可以通过一些重要因素加速它。

如果您有 10 个块并记住每个块的元素数量,则您可以根据键选择要写入的正确块,然后只需为该块移动元素(最多减少 10 倍)。

新的问题是如何在块之间实现良好的数字分布。


你也可以使用链表:http://www.cplusplus.com/reference/stl/list/

它在插入时非常有效,但对于随机搜索却很糟糕。但仍然寻找只是读取操作,因此寻找 x 元素可能比在数组中移动 x 元素更快(IDK)。

然后你可以使用组合的方法,并有多个指针的链表,所以你总是可以从最近的一个开始寻找。

【讨论】:

  • 这也是个好方法。如果您将使用大小为 sqrt(N) 的块,则可以使用 O(Nsqrt(N)) 解决方案......它称为 sqrt-decomposition。
【解决方案3】:

这是一个非常好的算法以及所需的 C++ 编码:

问题是这样解决的 在放置 7 之前留下。因此,如果 0 在 8 处,2 在 7 处,则反向结果数组看起来 喜欢:8 _ _ 7 _ _ _ _。

现在,平方根分解完成,插入完成:

#include <iostream>
#include <math.h>
using namespace std;

int main()
{
    int n, k = 0, d, r, s, sum, temp, m, diff, check = 1;
    cin >> n;

    d = sqrt(n) + 1;
    int arr[n], result[n], indices[d], arr2[d][d], num = 1;

    for (int i = 0; i < n; i++)
        cin >> arr[i];               //The inversion sequence is accepted.

    for (int i = 0; i < d; i++)
        indices[i] = 0;              //Indices tell where to start counting from in each row.

    for (r = 0; r < d; r++)
    {
        for (s = 0; s < d; s++)
        {
            arr2[r][s] = num;       //Array is filled with 1 to n (after sqrt decomposition).
            num = num + 1;
            if (num == n+1)
            {
                check = 0; break;
            }
        }
        if (check == 0)
            break;
    }

    int l = s;
    while (l >= 0)                  //Non-Zero numbers are shifted to right and 0 placed in
    {                               //empty spaces.
        arr2[r][d-1 - k] = arr2[r][l];
        k = k + 1; l = l - 1;
    }

    k = d-1 - k + 1;
    for (int t = 0; t < k; t++)
        arr2[r][t] = 0;

    indices[r] = indices[r] + k;    //Index of the last row is shifted to first non-zero no.

    for (int i = n-1; i >= 0; i--)
    {
        sum = 0; m = 0;
        while (sum < arr[i] + 1)
        {
            sum = sum + (d - indices[m]); //Empty boxes in each row are counted.
            m = m + 1;
        }

        m = m - 1;
        sum = sum - (d - indices[m]);     //When sum = 1 + corresponding value in inversion
        diff = arr[i] + 1 - sum;          //sequence, then that particular value is made 0
        temp = indices[m] + diff - 1;     //and (that value - 1) is the index of the number
                                      //to be placed in result array.
        result[arr2[m][temp] - 1] = i+1;
        for (int w = temp - 1; w >= indices[m]; w--)
        {
            arr2[m][w + 1] = arr2[m][w];  //Then, 0 is shifted to just before non-zero number
        }                                 //in the row, and the elements are shifted right
        arr2[m][indices[m]] = 0;          //to complete the sort.
        indices[m] = indices[m] + 1;
    }                                     //This is done n times for 1 to n integers thus
                                      //giving the permutation in reverse order in result
    for (int p = n-1; p >= 0; p--)        //array.
        cout << result[p] << ' ';

    return 0;
}

【讨论】:

    【解决方案4】:

    您的算法对这个问题效率不高,因为您的复杂度是 O(n^2),这意味着对于某些输入情况需要 10^10 次操作。你必须想出一个更便宜的解决方案。

    我建议您使用以下算法(索引从 1 到 N):

    for i=1 to N
       input a(i)
       if i==1 insert 1 into b
       else insert i into b at place i-a(i)
    end
    

    【讨论】:

    • 我建议你对b使用链表而不是数组
    • 如果使用链表不会产生 O(n^2) 算法吗?
    • 如果你计算插入次数,它是 O(n)。但是如果你计算比较次数,它是 O(n^2)。
    猜你喜欢
    • 2020-08-06
    • 2022-11-02
    • 2020-09-26
    • 2021-06-17
    • 2012-01-25
    • 1970-01-01
    • 2016-02-13
    • 2021-07-04
    • 1970-01-01
    相关资源
    最近更新 更多