一种逆序对新解法

洛谷P1908 https://www.luogu.com.cn/problem/P1908

逆序对常规解法归并排序或者离散化+树状数组(线段树),网上教程很多这里暂且不表,AVL树和红黑树也可以解决,但是略麻烦(比较小众),笔者在这里提供一种新解法——跳表(暂时在网上没看到有发的)。

跳表

https://oi-wiki.org/ds/skiplist/

具体可以在这里了解,简略来讲就是可以进行二分查找插入等操作的单调链表,只不过这个链表的结点多了很多层(有点像树了,但实际不是),便于操作。

类似这种(网上随便扒的图)

思路来源

其实这种思路与树状数组解法或者AVL解法类似,倒序遍历+找比当前数字小的数字总数,问题在于如何找变得不同了。

本蒟蒻在看到题目的第一刻便想到了奶牛那道题 洛谷P2947(虽然有很多奶牛的题),题目解法是倒序遍历+单调栈,对于这道题,倒序遍历维护一个单调栈会漏很多结果,所以本蒟蒻将单调栈换成了单调数组,二分维护,想介于vector中insert的速度快的特点,强行过一下,可惜不行。

void solve() {
	vector<int> b;
	ans = 0;
	b.push_back(a[n]);
	for (int i = n - 1; i >= 1; i--) {
		auto it = upper_bound(b.begin(), b.end(), a[i], greater<int>());
		int pos = b.end() - it;
		ans += pos;
		b.insert(it, a[i]);
	}
	cout << ans<< endl;
}

于是,便想到维护单调链表,查了一下(),果然可以,便有了这个跳表解法。(AVL树也想到了,但是本蒟蒻实力有限,没做出来)。

本题解法

对于本道题来讲,构造一个普通的跳表并不难,难点在于如何计算到末尾的长度,即上面代码的第7 8行,本蒟蒻将此节点为开始,到指向结点为重点,这段左闭右开的长度分别记录下来,最后从最大的高度向后遍历,依旧是logn的时间复杂度。

参考此图两个结点相同高度之间的长度记录下来就好了。结点结构体如下:

typedef struct skiplist {
	int size;//结点高度
	int data;//结点数据
	vector<skiplist *> point;//指向下一个指针
	vector<int> lr;//以当前节点为起始,相同高度下一节点为结束,左闭右开长度
	skiplist(int s, int d) {//初始化
		size = s;
		data = d;
		point.resize(size + 1);
		lr.resize(size + 1);
	}
} SL;

常规跳表高度为随机值,为保持稳定,这里先排序,然后取中位数,其高度比左右最高高度高一,保证每步都是二分进行,避免不稳定性。

typedef struct aa {
	int pos;
	int data;
	int size;
	bool operator <(const aa &a) {
		return data < a.data;
	}
} A;

void setsize(int l, int r, int s) {
	if (l > r)
		return;
	int mid = (l + r) >> 1;
	b[a[mid].pos].size = s;
	setsize(l, mid - 1, s - 1);
	setsize(mid + 1, r, s - 1);
}

于是setsize(1, n + 1, Log[n] + 1);便能初始化所有结点高度。

那么如何更新一个结点的高度呢,新加入一个节点,我们从最高的高度开始遍历,找到结点所在的区间,使区间长度加一,这便是此高度下包含区间总长度。下面分为两种情况,第一种高度大于新加入结点的高度,继续向下进行就好了。第二种,高度小于等于新加入结点高度,那么我们就要把这个总长度分为两个部分,一个是l到当前节点,一个是当前节点到r,其中一个计算出来,另一个自然而然就出来了。那么如何计算其中一个呢,我们这里计算左边的长度,在遍历高度时,我们用ss数组和st数组储存,当前高度 i 时,左端点为 st[i] ,st[i] 在当前高度总共经过位移为 ss[i] 。

新加入结点17,从高度4开始遍历,在高度为3时,区间分成两个部分,暂时不计算长度,遍历到高度为1时,st位移了2个单位,故 ss[1]=2,然后对ss数组求其前缀和,上一层的前缀和+1代表st到当前节点这个左区间的长度,左区间长度有了,右区间自然就出来了。

最后求当前结点之后的总长度只需要从最大高度向后遍历加总(贪心),最后总结果减一(不包含当前结点)。

那么,代码便完成了,下面展示总代码(Onlogn)(二分查找加更新Ologn,寻找结果Ologn)

#include <bits/stdc++.h>
#define lowbit(x) ((x) & -(x))
#define rep(i,l,r) for(register int i=l;i<=r;i++)
using namespace std;

const int N = 5e5 + 5;
const int INF = 0x3fffffff;
const double eps = 1e-6;
typedef long long ll;
const int modp = 1e6 + 37;

typedef struct skiplist {
	int size;
	int data;
	vector<skiplist *> point;
	vector<int> lr;
	skiplist(int s, int d) {
		size = s;
		data = d;
		point.resize(size + 1);
		lr.resize(size + 1);
	}
} SL;

typedef struct aa {
	int pos;
	int data;
	int size;
	bool operator <(const aa &a) {
		return data < a.data;
	}
} A;

int n;
ll ans;//注意结果爆int
A a[N];
A b[N];
int Log[N];

void init() {//浅浅手写log2一下
	Log[0] = -1;
	rep(i, 1, N) Log[i] = Log[i >> 1] + 1;
}

void setsize(int l, int r, int s) {//找中位数,方便稳定二分,随机高度也可以(没有瞧不起快排)
	if (l > r)
		return;
	int mid = (l + r) >> 1;
	b[a[mid].pos].size = s;
	setsize(l, mid - 1, s - 1);
	setsize(mid + 1, r, s - 1);
}

void solve() {
	ans = 0;
	sort(a + 1, a + n + 1);
	setsize(1, n + 1, Log[n] + 1);
	SL *first = new SL(Log[n] + 1, INF);
	rep(i, 1, first->size) first->point[i] = NULL, first->lr[i] = 1;
	for (int i = n; i >= 1; i--) {//从最高高度遍历,毕竟只有向后的指针
		SL *p = new SL(b[i].size, b[i].data);
		rep(i, 1, p->size) p->lr[i] = 0, p->point[i] = NULL;
		SL *l = first;
		int ss[p->size + 1] = {0};
		SL *st[p->size + 1];//两个记录位移,方便分区间长度的数组
		for (int i = first->size; i >= 1; i--) {
			while (l->point[i] != NULL && l->point[i]->data >= p->data) {//位移,且避免重复数字
				if (p->size >= i)
					ss[i] += l->lr[i];//记录位移长度
				l = l->point[i];
			}
			l->lr[i]++;//总长度加一
			if (p->size >= i)
				st[i] = l;//当前左节点
			if (p->size >= i) {
				p->point[i] = l->point[i];
				l->point[i] = p;//加入新节点
			}
		}
		rep(i, 1, p->size) ss[i] += ss[i - 1];//计算前缀和
		rep(i, 1, p->size) {
			int x = ss[i - 1] + 1;//当前结点长度也占1,其实ss[0]=1也可以
			p->lr[i] = st[i]->lr[i] - x;//先计算右区间长度
			st[i]->lr[i] = x;//左区间长度
		}
		while (p != NULL) {//计算结果
			ans += p->lr[p->size];
			p = p->point[p->size];
		}
		ans--;//去除当前节点
	}
	cout << ans << endl;
}

int main() {
	//ios::sync_with_stdio(0), cout.tie(0), cin.tie(0);
	init();
	if (~scanf("%d", &n)) {
		rep(i, 1, n) {
			scanf("%d", &a[i].data);
			a[i].pos = i;
			b[i].data = a[i].data;
			b[i].pos = a[i].pos;
		}
		solve();
	}
	return 0;
}


总结:跳表是一个非常好用的数据结构,插入删除或者进行区间运算(RMQ之类的)都有相当高的性能,相较于二叉堆,能干的事情多了很多,加上长度之后按下标索引链表值效率也从On提高到Ologn,至于缺点包括空间复杂度高,写法麻烦,链表的惯性麻烦等等,而且只允许数据单调,在维护单调数组时AVL等算法不能进行下去的话,跳表或许能给你思路。

修正:两个图中结点9有个长度为1的箭头

全部评论

相关推荐

评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务