块状链表(STL rope)
首先介绍一下块状链表。
我们都知道:
数组 具有 O(1)的查询时间,O(N)的删除,O(N)的插入。
链表 具有 O(N)的查询时间,O(1)的删除,O(1)的插入。
既然数组和链表各有优劣,那么我们为何不将链表和数组组合起来,一起来均摊时间呢?
做法就是维护一个链表,链表中的每个单元包含一段数组,以及这个数组中的数据个数。每个链表中的数据连起来就是整个数据。
- 时间复杂度证明:
设链表的长度为a,每个单元中数组的长度为b。那么无论是插入还是删除,在寻址的时候都要遍历整个链表,复杂度是O(a);
对于插入操作,如果直接在链表中加入一个新的单元,时间复杂度是O(1),如果要在一个单元内插入一个新的数,那么则需要移动数组中的数据,复杂度是O(b),总的复杂度是O(a + b)。
对于删除操作,可能会涉及多个连续的单元,如果一个单元中的所有数据均要删除,直接删除这个单元,复杂度是O(1),如果只删除部分的数据,则要移动数组中的数据,复杂度是O(b)。总的复杂度是O(a + b)。
因为ab = n,取a = b = √n,则总的复杂度是O(2√n)= O(√n)。
问题是如何维护a和b大致等于√n?
插入操作:
每个链表节点的数据是一个数组块,那么问题来了,我们是根据什么将数组切开呢?总不能将所有的数据都放在一个链表的节点吧,那就退化成数组了。在理想的情况下,为了保持√N的数组个数,所以我们定了一个界限2√N,当链表中的节点数组的个数超过2√N的时候,当下次插入数据的时候,我们有两种做法:
① 在元素的数组插入处,将当前数组切开,插入元素处之前为一个链表节点,插入元素后为一个链表节点。
② 将元素插入数组后,将数组从中间位置切开。
删除操作:
跟插入道理一样,既然有裂开,就有合并,我们同样定义一个界限值√n/2,当链表节点的数组个数小于这个界限值的时候,就需要将此节点和后面的链表节点进行合并。
执行上述维护操作需要移动数组中的数据,复杂度是O(b),对于单元的分割和合并均是O(1)的,总的复杂度是O(b)的。这样,维护操作并不会使总复杂度增加。最终得到一个复杂度是O(√n)的数据结构。
因为块状链表的代码太过于冗长,所以如果实在要用到的话我推荐使用STL库里的rope(C++11及以上可使用),相关的rope讲解:
Rope大法(可持久化平衡树)
【可持久化线段树?!】rope史上最全详解
这里注意rope是支持"+"操作的。
问题很简单,但是需要涉及到区间的操作,用rope暴力就行。复杂度为O(n√n)。
#include<bits/stdc++.h>
#include <ext/rope>
#define ll long long
#define inf 0x3f3f3f3f
#define endl '\n'
#define IOS std::ios::sync_with_stdio(false),cin.tie(0), cout.tie(0)
using namespace std;
using namespace __gnu_cxx;
const int N = 1e5 + 10;
rope<int>r;
int n, m, a, b;
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) r.push_back(i);
while(m --)
{
scanf("%d%d", &a, &b);
r = r.substr(a - 1, b) + r.substr(0, a - 1) + r.substr(a + b - 1, n - a - b + 1); // -1是因为rope的下标从0开始
}
for(int i = 0; i < n; i ++)
printf("%d ", r[i]);
return 0;
}