一种逆序对新解法
洛谷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的箭头