拼图游戏中逆序数的实现的三种方式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了拼图游戏中逆序数的实现的三种方式相关的知识,希望对你有一定的参考价值。
一、定义
在一个排列中,如果一对数的前后位置与大小顺序相反,即前面的数大于后面的数,那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。如2431中,21,43,41,31是逆序,逆序数是4。
二、计算方法
逆序数的计算方法主要有直接计算,归并和树状数组三种,下面将一一介绍。
2.1 直接计算
即根据定义用枚举的方法求逆序数。对于数列中的每一个数a[i],遍历数列中的数a[j] (其中j < i),若a[i] < a[j],则逆序数加1,这样就能统计出该数列的逆序数总和。
该方法的时间复杂度为O(n^2)
代码实现如下:
public static void main(String[] args) {
int[] array = {1,3,5,4,7, 8, 2, 6, 9};
int arraySize = array.length;
int inverseNumber = 0;
for (int i = 0; i < arraySize; i++) {
for (int j = i + 1; j < arraySize; j++) {
if (array[i] > array[j])
inverseNumber++;
}
}
System.out.print("数列:");
for(int i=0;i<arraySize;i++)
System.out.print(array[i]+" ");
System.out.println("\n数列的逆序数:"+inverseNumber);
}
结果为:
数列:1 3 5 4 7 8 2 6 9
数列的逆序数:8
2.2 采用归并思想
参考求逆序数。
归并排序是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的,然后再把有序的子序列合并为整体有序序列归并排序是分治算法的一个典型的应用,而且是稳定的一种排序。这题利用归并排序的过程中,计算每个小区间的逆序数,进而得到大区间的逆序数。那么,问题就解决了。
假设以mid为区分,第start到第mid个元素属于左区间,第mid+1到第end个元素属于右区间。由于每个小区间都是良序的,当左区间的数字a[i]大于右区间的数字a[j]时,可以保证左区间第i到第mid这mid-i+1个元素都大于a[j],即元素a[j]在逆序数计算中被计入mid-i+1次。
排序完成后,累加的结果即所有元素对逆序数的贡献和。
该方法的时间复杂度与归并排序的时间复杂度相同为O(nlogn)。
代码实现如下:
public class InverseNumberByMerge {
private int method = 0;
private int inverseNumber = 0;
void Merge(int[] array, int start, int mid, int end) // 归并排序的合并部分
{
int[] b = new int[array.length];
int i = start, j = mid + 1, k = start;
while (i <= mid && j <= end) {
if (array[i] <= array[j]) {
b[k++] = array[i++];
} else {
inverseNumber += j - k;// 等价于mid-i+1,因为左右两区间都为排序后的结果,array[i]>array[j]意味着array中从i到mid的元素都大于array[j]
b[k++] = array[j++];
}
}
while (i <= mid) {
b[k++] = array[i++];
}
while (j <= end) {
b[k++] = array[j++];
}
for (i = start; i <= end; i++) {
array[i] = b[i];
}
}
private void MergeSort(int[] array, int start, int end) // 归并排序
{
if (start < end) {
int mid = (start + end) / 2;
MergeSort(array, start, mid); // 将前半部分排序
MergeSort(array, mid + 1, end); // 将后半部分排序
Merge(array, start, mid, end); // 合并前后两个部分
}
}
public int getInverseNumber(){
return this.inverseNumber;
}
public static void main(String[] args) {
//数列:3 5 4 8 2 6 9
// Arrange
int[] array = {3,5,4,8,2,6,9};
int arraySize = array.length;
int inverseNumber = 0;
InverseNumberByMerge comp = new InverseNumberByMerge();
comp.MergeSort(array, 0, arraySize - 1);
inverseNumber = comp.getInverseNumber();
System.out.print("数列:");
for(int i=0;i<arraySize;i++)
System.out.print(array[i]+" ");
System.out.println("\n数列的逆序数:"+inverseNumber);
}
}
结果为:
数列:2 3 4 5 6 8 9
数列的逆序数:6
2.3 树状数组
树状数组的介绍:树状数组
求逆序数的思路,参考并修改自逆序数的几种求法
以数列 3 5 4 8 2 6 9 为例,大体思路如下:
-
新建一个数组,将数组中每个元素置0
0 0 0 0 0 0 0 -
取数列中最大的元素,将该元素所在位置置1
0 0 0 0 0 0 1
统计该位置前已放置的元素的个数,为0 -
接着取第二大元素8,将第其位置置1
0 0 0 1 0 0 1
统计该位置前已放置的元素的个数,为0 -
继续取第三大元素6,将其位置置1
0 0 0 1 0 1 1
统计该位置前已放置的元素的个数,为1 -
……..
这样直到取出最小元素,将每次取元素时该元素前已放置的元素的个数进行累加,这样就算出总的逆序数来了。
可以很明显的看出,每次取新的元素a[i]时,该元素的位置i前已放置的元素个数n代表a[i]前有n个元素比a[i]大,即有n对逆序对,逆序对的第二个元素为a[i]。
但是,如果在统计和计算每次放某个元素时,一个一个地数该元素前边已放置元素的个数,那么一趟复杂度为O(n),总共操作n趟,复杂度为O(n^2),和第一种方法的复杂度一样了。所以,我们采用树状数组,把每一趟计数个数的复杂度降为O(logn),这样整个复杂度就变为O(nlogn)。
将序列中的每个数按照从大到小的顺序插入到树状数组中,给当前插入节点及其父节点的个数加1,然后统计该节点下边及左边放置元素的个数。这与前面讲述的大体思路中的过程是一致的。
在逆序数的几种求法中,写的是该节点下边及右边,但这与思路中的说法不同,经过分析代码,每一次求和时,pos-=LowBit(pos);这正是去求该节点的左节点的公式,所以应为该节点下边及左边放置元素的个数。
代码如下:
public class Node {
public int data;
public int pos;
public Node(){
}
public Node(int data,int pos){
this.data = data;
this.pos = pos;
}
@Override
public String toString() {
return "data = "+this.data +"\npos = "+this.pos;
}
}
----------------------------------------------------
import java.util.Comparator;
public class NodeComparator implements Comparator {
public int compare(Object num1, Object num2) {
return( (Node)num2).data-((Node)num1).data;
}
}
----------------------------------------------------
import java.util.Arrays;
public class InverseNumberByTreemap {
private int Length = 0;
private int[] TreeArray = null;
public InverseNumberByTreemap(int[] array) {
this.TreeArray = array;
this.Length = array.length;
}
private int LowBit(int num) {
//System.out.println(num & (num ^ (num - 1)));
return num & (num ^ (num - 1));
}
// 统计pos节点下方及左边节点元素的个数
private int sum(int[] TreeArray, int pos) {
int result = 0;
while (pos != 0) {
result += TreeArray[pos-1];
pos -= LowBit(pos);
}
return result;
}
// 使pos位置的节点及其父节点的元素个数加1
private void INC(int[] TreeArray, int pos) {
while (pos < Length) {
TreeArray[pos-1]++;
pos += LowBit(pos);
}
}
int insertNum(Node[] seq) {
int reverseNum = 0;
for (int i = 0; i < Length; i++) {
reverseNum += sum(TreeArray, seq[i].pos); // 累加逆序数
INC(TreeArray, seq[i].pos); // 将该节点及父节点的数加1
}
return reverseNum;
}
public static void main(String[] args) {
int array[]={3,5,4,8,2,6,9};
int Length = array.length;
Node[] seq = new Node[Length];
seq[0] = new Node(0, 0);
for (int i = 0; i < seq.length; i++) {
seq[i] = new Node(array[i], i+1);
// System.out.println(seq[i]);
}
int[] TreeArray = new int[Length];
for(int i=0;i<TreeArray.length;i++)
TreeArray[i] = 0;
// 从大到小排序
NodeComparator comp = new NodeComparator();
Arrays.sort(seq, comp);
int reverseNum = 0;
// 边插入边计数
InverseNumberByTreemap inverse = new InverseNumberByTreemap(TreeArray);
reverseNum = inverse.insertNum(seq);
System.out.println(reverseNum);
}
}
结果为:
数列:2 3 4 5 6 8 9
数列的逆序数:6
以上是关于拼图游戏中逆序数的实现的三种方式的主要内容,如果未能解决你的问题,请参考以下文章