在 O(n log n) 时间内生成长度为 n 且反转次数为 k 的数组的算法?

Algorithm to generate an array with n length and k number of inversions in O(n log n) time?

我正在编写一个算法,该算法将 return 一个具有确定长度和反转次数的数组(数字对,其中左侧数字大于右侧数字)。 IE。数组 [3, 1, 4, 2] 包含三个反转 (3, 1)、(3, 2) 和 (4, 2)。所以在实践中,当给定 n=3 的长度和反转次数 k=3 时,算法应该生成数组 [3, 1, 4, 2](或满足这些要求的另一个数组)。

由于反转的次数也是要按升序排序的数组必须进行的交换次数,因此我通过创建从 1 到 n - 1 的数组并使用插入排序来解决这个问题反向算法进行 k 次交换。

这种方法适用于较小的输入,但该算法应该能够有效地生成最多 n=10^6 和 k=n(n-1)/2 以及介于两者之间的数组,因此该算法应该在 O(n log n) 时间内工作而不是 O(n^2)。下面是代码:

import java.util.*;

public class Inversions {

    public int[] generate(int n, long k) {        

        // Don't mind these special cases

        if (n == 1) {

            int[] arr = {1};

            return arr;
        }

        if (k == 0) {

            int[] arr = new int[n];

            for (int i = 0; i < n; i++) {

                arr[i] = 1;                    
            }

            return arr;
        }

        int[] arr = new int[n];

        for (int i = 0; i < n; i++) {

            arr[i] = i + 1;
        } 

        int inversions = 0;
        int i = 0;    

        while (inversions < k && i < n) {                                    

            int j = i - 1;                        

            while (j >= 0 && arr[j] < arr[j + 1] && inversions < k) {

                int helper = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = helper;
                inversions++;
                j--;
            }     

            i++;
        }

        return arr;
    }
}

以及用于测试不同输入数组的主要 class:

public class Main {

    public static void main(String[] args) {

        Inversions in = new Inversions();
        int[] arr1 = in.generate(4,3);
        int[] arr2 = in.generate(4,0);
        int[] arr3 = in.generate(4,6);        

        System.out.println(Arrays.toString(arr1)); // [3,1,4,2]
        System.out.println(Arrays.toString(arr2)); // [1,1,1,1]
        System.out.println(Arrays.toString(arr3)); // [4,3,2,1]
    }
}

该算法没有return与示例结果完全相同的数组,但通过了所有测试,除了输入大小非常大的那些。我还尝试了合并排序的不同变体,因为它在 O(n log n time) 中工作但无济于事。

如果你们有一些想法就太好了。如果您不熟悉 Java,没关系,我们非常欢迎伪代码或任何其他类型的建议!

如果反转数组中最初的 m 个元素,则会创建 m(m-1)/2 个反转。

如果反转最初的 m+1 个元素,则会创建 m(m+1)/2 个反转。

这两者的区别只有m.

所以:

  1. 生成排序数组
  2. 找到最大的 m 使得 m(m-1)/2 <= k
  3. 反转数组中的前 m 个元素以创建 m(m-1)/2 个反转
  4. 将下一个元素向前移动 k - m(m-1)/2 个位置以创建剩余的所需反转。

这需要 O(n) 时间,比您要求的要好。

另一个O(n)算法:从排序数组开始。当您交换第一个和最后一个元素时,您会得到 x = 2 * (n-2) + 1 个反转。考虑这两个元素是固定的,只处理剩余的数组。如果 x 太大,请考虑使用较小的数组。根据需要重复此操作。

未经测试的代码:

for (int first=0, last = n-1; remainingInversions>0; ) {
    int x = 2 * (last-first-1) + 1;
    if (x <= remainingInversion) {
        first++;
        last--;
        remainingInversion -= x;
    } else {
        last--; // consider a smaller array
    }
}

事实上,每次将最后一个元素与其前面的元素交换时,反转次数都会增加。这是一个 java 解决方案:

public static int[] generate(int n, long k) {
    int[] arr = new int[n];

    for(int i = 0; i < n; i++) {
        arr[i] = i;
    }

    long inversions = 0;
    int j = (n-1);
    int s = 0;

    while(inversions < k) {
        int temp = arr[j];
        arr[j] = arr[j-1];
        arr[j-1] = temp;

        inversions++;
        j--;
        if(j == s) {
            j = (n-1);
            s++;
        }
    }

    return arr;
}

我在 Python 中得到了一个具有 O(n) 复杂度的实现。

它基于两个规则。

  1. 反转大小为 m 的数组会得到 m*(m-1)/2 个反转。
  2. 将元素移动 m 个位置,创建 m 个反转。
def get_m(k):
    m=0
    while m*(m-1)/2<=k:
        m+=1
    else:
        m-=1
    return m

def generate(l, k):
    """
    Generate array of length l with k inversions.
    """
    # Generate a sorted array of length l
    arr = list(range(0,l))
    
    # If no inversions are needed, return sorted array. 
    if k==0:
        return arr
    
    # Find largest m such that m*(m-1)/2 <= k
    m=get_m(k)

    # Reverse first m elements in the array which will give m*(m-1)/2 inversions
    arr = arr[m-1::-1]+arr[m:]

    # Calculate for any remaining inversions
    remaining_k = k-(m*(m-1)/2)

    # For remaining inversions, move the last element to its left by remaining_k
    if remaining_k>0:
        arr.insert(int(len(arr)-remaining_k - 1), arr[-1])
        arr = arr[:-1]
    return arr

if __name__ == '__main__':
    l = int(sys.argv[1])
    k = int(sys.argv[2])
    arr = generate(l, k)
    print(arr)

@zhong yang:它在预期范围内工作得很好 0 <= k <= n(n-1)/2 但最好抛出异常或 null 如果 k 不在这个范围而不是返回一些数组!

有一种非常简单的方法可以创建 n 个反转... 也就是将最后一个元素移到最前面。 由于使用了额外的内存,它不是很有效,但我会这样做:

创建一个长度为n 两倍的数组。 如果我们使用 Integer[] 而不是 int[],则用标记(即 null)从开始到中间填充它。 从中间填充它,上升。 然后做类似下面的事情...... 我确信我有一个错误和其他错误,但一般的想法在下面的代码中得到了体现。

int start = 0;
int mid = arr.length / 2;
int end = arr.length - 1;

while (v > 0)
{
    if (v < (end - mid))
    {
        arr[start++] = arr[mid + v];
        arr[mid + v] = null;
    }
    else
    {
        arr[start++] = arr[end];
        v -= (end - mid);
        end--;
    }
}

所以我们有一个数组,其中填充了起始值、一堆空值,然后是原始增量值,其中一个可能已变为空值,以及一个指向原始区域中间的“结束”指针.

所以最后一步是从 0 -> endPos 复制到最终数组,忽略空值。

如果k >= n - 1,则将元素n - 1 放在数组的最前面,使其与n - 1 个元素倒置;否则将其放在数组的最后,使其与 0 个元素倒置。继续这种贪婪方法来确定其余元素的去向。

这是一个解决方案,它通过一些数学知识在线性时间内将 generate() 实现为 运行。

public class Inversions {

  public static int[] generate(int n, long k) {

    int[] array = new int[n];
    
    // locate k in various sums of (n-1), (n-2), ..., 1
    int a = (int) Math.sqrt((n * (n - 1) - 2 * k)); // between the sum of [(n-1)+...+(n-a)] and the sum of [(n-1)+...+(n-a-1)]
    int b = n - 1 - a; // counts of (n-1), (n-2), ..., (n-a)
    int c = (int) (k - n * b + (b * b + b) / 2); // spillover = k - [(n-1)+(n-b)]*b/2;

    // put elements in the array
    for (int i = 0; i < b; i++) {
        array[i] = n - 1 - i;
    }
    for (int i = b; i < n - 1 - c; i++) {
        array[i] = i - b;
    }
    array[n - 1 - c] = n - 1 - b;
    for (int i = n - c; i < n; i++) {
        array[i] = i - b - 1;
    }

    return array;

  }

  public static void main(String[] args) {

    int n = Integer.parseInt(args[0]);
    long k = Long.parseLong(args[1]);

    for (int i = 0; i < n; i++) {
        StdOut.print(generate(n, k)[i] + " ");
    }

  }
}

逻辑并不难。例如,我们有 10 个数字 [0,1,2,3,4,5,6,7,8,9] 来生成 18 个反转。首先,在0之前插入9,--->[9,0,1,2,3,4,5,6,7,8],产生9次反转。还剩下 9 个反转,所以我们在 0 之前插入 8,---->[9,8,0,1,2,3,4,5,6,7],所以我们得到额外的 8 个反转。最后还剩1个倒置,我们在6----->[9,8,0,1,2,3,4,5,7,6]前插入7个。在这种情况下我只使用数组。该程序以 O(n) 复杂度运行。下面的代码只考虑n个数(0,1,2.....n-1)及其倒数。

    public static int[] generate(int n, long k) {
    int[] a = new int[n];
    int[] b = new int[n];
    for (int i = 1; i < n; i++) {
        a[i] = 1 + a[i - 1];
    }
    if (n == 0 || k == 0) return a;
    else {
        int i = 0;
        while (k > 0) {
            if (k > n - i - 1) {
                b[i] = a[n - 1 - i];
            }
            else {
                //auxilary array c to store value 
                int[] c = new int[(int) (k + 1)];
                for (int j = i; j < n - 1 - k; j++) {
                    b[j] = j - i;
                }
                for (int j = (int) (n - 1 - k); j < n; j++) {
                    c[j - (int) (n - 1 - k)] = j - i;
                }
                b[(int) (n - 1 - k)] = c[(int) k];
                for (int j = (int) (n - k); j < n; j++) {
                    b[j] = c[j - (int) (n - k)];
                }
                break;
            }
            k = k - (n - 1 - i);
            i++;

        }
        return b;
    }
}