如何在O(n log n)时间内生成一个长度为n且包含k个逆序对的数组?

5
我正在编写一个算法,它将返回一个确定长度和逆转数(左侧数字大于右侧数字的数字对数)的数组。例如,数组 [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;
    }
}

用于测试不同输入数组的主要类:

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]
    }
}

算法返回的数组与样本结果不完全相同,但通过了所有测试,除了输入规模非常大的测试。我尝试了不同的变体使用归并排序,因为它可以在O(n log n时间)内工作,但未能解决问题。
如果您有任何想法,那将是太棒了。如果您不熟悉Java,也没关系,伪代码或任何其他类型的建议都可以!

这在技术上是一个重复问题。然而,它已经很老了并且已经有答案了。有人能告诉我是否应该将已回答的问题标记为重复吗?当得到答案后我会删除这个评论,只是不知道问谁! - AviFS
8个回答

10

如果您反转数组中的前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)的时间,比您需要的速度更快。


输入数组长度为5,k=5时会如何运作?原始数组=[0,1,2,3,4]; 最大m的值=3;前半部分倒置=[2,1,0];现在在剩余的数组[3,4]中,您将如何将第一个元素移动5-3=2个位置?长度本身只有2。 [4,3]将再产生1个倒置。因此,总倒置次数变为4而不是k=5。 - Nazil Khan
1
@Nazil 将前三个数字翻转得到 [2,1,0,3,4],包含 3 个逆序对。然后将数字 3 左移两个位置,得到 [2,3,1,0,4],包含 5 个逆序对。 - Matt Timmermans
谢谢@Matt。我已经得到了一个Python实现,其中我将数组[2,1,0,3,4]的最后一个元素向左移动2个位置,以获得5个反转。结果= [2, 1, 4, 0, 3]。 - Nazil Khan
什么?(m + 1 - m) = 1,(m + 1 - m + 1) = 2? - user894319twitter
如果您需要有关此公式的详细解释:https://math.stackexchange.com/questions/905700/maximum-and-average-number-of-inversions-in-array-by-induction - user894319twitter
m(m-1)/2+m=m(m+1)/2这个等式并不容易看出来。 - undefined

2
另一个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
    }
}

1
如果 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] + " ");
    }

  }
}

0
逻辑并不太难。例如,我们有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之前插入7---> [9,8,0,1,2,3,4,5,7,6]。我只在这种情况下使用数组。此程序的时间复杂度为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;
    }
}

0

我用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)

0

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

创建一个长度为2n的数组。 如果我们使用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的内容复制到最终数组中,忽略空值。


0
事实上,每当您将最后一个元素与其前面的元素交换时,逆序数就会增加。以下是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;
}

-1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接