c++自带的可持久化平衡树?rope大法好!(超详细解答 + 5道例题讲解,可直接替代可持久化的线段树、并查集、平衡树!)

整理的算法模板合集: ACM模板


c++自带的可持久化平衡树?rope大法好!

C++官方说明

C++11及以后开始支持

可以当作可持久化平衡树使用,内部构造是一个块状链表

1. 声明

1)头文件

#include<ext/rope>

2)调用命名空间

using namespace __gnu_cxx;

3)声明使用

rope<int>Irp;

rope<long long>Lrp;

crope crp;//相当于定义成rope<char>,即定义为string类型

rope<char>rop;

rope<double>Drp;

//rope<PII>Prp;不合法

2. 支持操作

push_back(x);	  在末尾添加x

insert(pos,x);	  在pos插入x

erase(pos,x);	  从pos开始删除x个

replace(pos,x);	  从pos开始换成x

substr(pos,x);	  提取pos开始x个
 
at(x)/[x];	      访问第x个元素

test.clear();     清空元素

char类型的rope

  • insert(int pos, string &s, int n);

将字符串s的前n位插入rope的下标pos处

  • append(string &s,int pos,int n);

把字符串s中从下标pos开始的n个字符连接到rope的结尾,如没有参数n则把字符串s中下标pos后的所有字符连接到rope的结尾,如没有参数pos则把整个字符串s连接到rope的结尾

  • substr(int pos, int len);

提取rope的从下标pos开始的len个字符

  • at(int x);

访问rope的下标为x的元素

  • erase(int pos, int num);

从rope的下标pos开始删除num个字符

  • copy(int pos, int len, string &s);

从rope的下标pos开始的len个字符用字符串s代替,如果pos后的位数不够就补足

  • replace(int pos, string &x);

从rope的下标pos开始替换成字符串x,x的长度为从pos开始替换的位数,如果pos后的位数不够就补足


int n, m;
char a[N];
rope<char>r;
int main()
{
    for(int i = 0; i <= 10; ++ i)
        a[i] += i +'0';
    r.insert(0, a + 2, 8);
    for(int i = 0; i <= 10; ++ i)
        cout << r.at(i) << " ";
    puts("");
    for(int i = 0; i <= 10; ++ i)
        cout << r[i] << " ";
    puts("");
    return 0;

}

2 3 4 5 6 7 8 9
2 3 4 5 6 7 8 9

int类型的rope

  • insert(int pos, int *s, int n);

将int数组(以下的s都是int数组)s的前n位插入rope的下标pos处,如没有参数n则将数组s的所有位都插入rope的下标pos处

  • append(int *s,int pos,int n);

把数组s中从下标pos开始的n个数连接到rope的结尾,如没有参数n则把数组s中下标pos后的所有数连接到rope的结尾,如没有参数pos则把整个数组s连接到rope的结尾

  • substr(int pos, int len);

提取rope的从下标pos开始的len个数

  • at(int x);

访问rope的下标为x的元素

  • erase(int pos, int num);

从rope的下标pos开始删除num个数

  • copy(int pos, int len, int *s);

从rope的下标pos开始的len个数用数组s代替,如果pos后的位数不够就补足

  • replace(int pos, int *x);

从rope的下标pos开始替换成数组x,x的长度为从pos开始替换的位数,如果pos后的位数不够就补足

rope<int>rp;
int main()
{
    rp.append(3);
    rp.append(1);
    rp.append(2);
    rp.append(1);
    rp = rp.substr(1, 3);//从1开始截3个数字,注意rope是从0开始的,所有的容器都是从0开始的
    for(int i = 0; i < rp.size(); ++ i)
        cout << rp[i] << " ";
    puts("");
    return 0;
}

输出:

1 2 1

3. 具体的细节

时间复杂度: O ( n n ) O(n\sqrt{n}) O(nn )(因为是用块状链表实现的)

空间复杂度: O ( n ) O(\sqrt{n}) O(n )

4. “可持久化”

定义方法

rope<char> *now[p];

如何申请更新一个新的可持久化版本:

now[0]=new rope<char>();

如何继承版本

now[cnt]=new rope<char>(*now[cnt-1]);

如何回到查询过去的版本

ans=now[cnt]->at(num);

这样我们就得到了一个可持久化线段树!

5. 例题

1. luogu P3919 【模板】可持久化线段树 1(可持久化数组)【代替可持久化线段树】

在这里插入图片描述
数据太大了… 1 e 6 1e6 1e6

由于本题的时空限制比较严格,所以只能拿94分,最后一个点MLE…一般的题目不会卡的这么严。

这道题因为有M个版本,也就是说空间复杂度为 O ( n m ) O(n\sqrt{m}) O(nm ) = 1 e 9 = 1e9 =1e9,也就是最大开了 1 e 9 1e9 1e9 i n t int int,一个 i n t int int占4个字节,也就是B,1KB=1024B,1MB=1024KB,1Gb=1024MB,妈呀,我算了一下,我这个程序在最后一个点的时候用了大概3814MB,三个G的内存hhh…

//#pragma GCC optimize("Ofast")
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<ext/rope>

#define debug(x) cout << x << "ok" << endl
typedef long long ll;
#define file freopen("1.in", "r", stdin);freopen("1.out", "w", stdout);
const int N = 1e6 + 1, M = 1e5 + 7, INF = 0x3f3f3f3f;

using namespace std;
using namespace __gnu_cxx;
//可持久化平衡树
typedef pair<int, int> PII;

int n, m;
int a[N];
rope<int> *S[N];

ll read()
{
    ll x = 0, f = 1;char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
    return x * f;
}

int main()
{
    n = read(), m = read();
    S[0] = new rope<int>();
    S[0]->append(0);
    for(int i = 0; i < n; ++ i)
        a[i] = read();
    S[0]->insert(1, a, n);//从1开始(如果不使用这个方法而是每次输入一个放进去一个有个点会MLE...)
    //for(int i = 0; i <= n; ++ i)
    //    cout << S[0]->at(i) << endl;
    for(int i = 1; i <= m; ++ i){
        int v, k, a, b;
        v = read(), k = read();
        S[i] = new rope<int>(*S[v]);
        if(k == 1){
            a = read(), b = read();
            S[i]->replace(a, b);
        }
        else {
            a = read();
            printf("%d\n", S[i]->at(a));
        }
    }
    return 0;
}

2. 牛客 Shuffle Cards 【代替Splay】

题目传送门
在这里插入图片描述

  • 首先分析数据范围

1 e 5 1e5 1e5,加上是区间移动,一看就是一个数据结构问题,区间问题一般用Splay解决,但是我只会treap…。但是我们有rope这个神器!

  • 然后来分析题意

题目大意:有n个数,m次操作,每次选择从x开始长度为y的数放到数组的最前面。问m次操作后的序列。直接用rope暴力跑啊!

//#pragma GCC optimize("Ofast")
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<bitset>
#include<map>
#include<ext/rope>

#define debug(x) cout << x << "ok" << endl
typedef long long ll;
#define file freopen("1.in", "r", stdin);freopen("1.out", "w", stdout);
const int N = 2e5 + 7, M = 1e5 + 7, INF = 0x3f3f3f3f;

using namespace std;
using namespace __gnu_cxx;

ll read()
{
    ll x = 0, f = 1;char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
    return x * f;
}

int n, m, x, y;
rope<int>r;
int a[N];

int main()
{
    n = read(), m = read();
    //for(int i = 1; i <= n; ++ i)
    //    r.push_back(i);
    //r.append(0);
    for(int i = 1; i <= n; ++ i)
        a[i] = i;
    r.insert(0, a + 1, n);//这样写会比上面那种每次都放到后面快一点
    while(m -- ){
        x = read(), y = read();
        r = r.substr(x - 1, y) + r.substr(0, x - 1) + r.substr(x + y - 1, n - x - y + 1);
    }
    for(int i = 0; i < n; ++ i){
        printf("%d%s", r[i], i == (n - 1) ? "\n" : " ");
    }
    return 0;
}

3. luogu P3402 可持久化并查集【代替可持久化并查集】

在这里插入图片描述
实际写法和可持久化线段树一样开一堆数组,直接暴力维护

注意:BZOJ 3673: 可持久化并查集可以AC,因为数据范围只有 0 < n , m < = 2 ∗ 1 0 4 0<n,m<=2*10^4 0<n,m<=2104

但是这道题只能拿24分…因为数据范围拉到了 2 e 5 2e5 2e5

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<ext/rope>
#define pii pair<int,int>
#define N 200005
using namespace std;
using namespace __gnu_cxx;
rope<int>*his[N];
int find(int x,int root)
{
	if(x==his[root]->at(x))return x;
	int t=find(his[root]->at(x),root);
	if(t!=his[root]->at(x))his[root]->replace(x,t);//不判的话会mle 
	return his[root]->at(x);
}
int main()
{
	int n,m,last=0;
	scanf("%d%d",&n,&m);
	his[0]=new rope<int>;
	his[0]->push_back(0);
	for(int i=1;i<=n;i++)his[0]->push_back(i);
	for(int i=1;i<=m;i++)
	{
		int op,a,b,u,v,k;
		scanf("%d",&op);
		if(op==1)
		{
			scanf("%d%d",&a,&b);
			a^=last,b^=last;
			u=find(a,i-1);
			v=find(b,i-1);
			his[i]=new rope<int>(*his[i-1]);
			if(u==v)continue;
			his[i]->replace(v,u);
		}
		if(op==2)
		{
			scanf("%d",&k);
			k^=last;
			his[i]=new rope<int>(*his[k]);
		}
		if(op==3)
		{
			scanf("%d%d",&a,&b);
			a^=last,b^=last;
			u=find(a,i-1);
			v=find(b,i-1);
			his[i]=new rope<int>(*his[i-1]);
			printf("%d\n",u==v);
			last=u==v;
		}
	}
}

4. luogu P6166 [IOI2012]scrivener【替代可持久化线段树】

题目链接:https://www.luogu.com.cn/problem/P6166

题意

设计支持如下 3 种操作:
1.T x:在文章末尾打下一个小写字母 x。(type 操作)
2.U x:撤销最后的x 次修改操作。(Undo 操作)
(注意Query 操作并不算修改操作)
3.Q x:询问当前文章中第x 个字母并输出。(Query 操作)

操作数n<=100000 在线算法

超简单的直接实现

#include<cstdio>
#include<cstring>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<ext/rope>
using namespace std;
using namespace __gnu_cxx;
typedef long long ll;

const int N = 5000007, M = 5000007, INF = 0x3f3f3f3f;

ll read()
{
    ll x = 0, f = 1;char ch = getchar();
    while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
    return x * f;
}

int cnt;
rope<char> * now[N];
int n, m;
int num;
char op, str;

int main()
{
    scanf("%d", &n);
    now[0] = new rope<char>();
    for(int i = 1; i <= n; ++ i){
        cin >> op;
        if(op == 'T'){
            cin >> str;
            cnt ++ ;//新版本
            now[cnt] = new rope<char>(*now[cnt - 1]);
            now[cnt]->push_back(str);
        }
        if(op == 'U'){
            cin >> num;
            cnt ++ ;
            now[cnt] = new rope<char>(*now[cnt - num - 1]);//num个修改操作前的版本
        }
        if(op == 'P'){
            cin >> num;
            cout << now[cnt]->at(num) << endl;
            //cout << now[cnt][num] << endl;//不能这么写
        }
    }
    return 0;
}

某位大佬的另一种写法,用树状数组维护当前i是第几个修改操作,然后交上去0分…

#include<cstdio>
#include<cstring>
#include<cctype>
#include<iostream>
#include<algorithm>
#include<ext/rope>
using namespace std;
using namespace __gnu_cxx;
const int maxn=1e5+10;
rope<char> *his[maxn];
int n;
int d[maxn];
inline int lowbit(int x){ return x&-x; }
 
inline void updata(int x){ while(x<=n){ d[x]++; x+=lowbit(x); } }
inline int get(int x){ int res=0; while(x){ res+=d[x]; x-=lowbit(x); }return res; }
 
inline char getC(){ char ch=getchar(); while(!isalpha(ch))ch=getchar(); return ch; }
 
inline int getint()
{
    int res=0; char ch,ok=0;
    while(ch=getchar())
    {
        if(isdigit(ch)){ res*=10;res+=ch-'0';ok=1; }
        else if(ok)break;
    }
    return res;
}
void deb(rope<char> s)
{
    for(int i=0;i<s.length();i++)
        cout<<s[i];puts("");
}
 
int main()
{
//    freopen("type.in","r",stdin);
//    freopen("type.out","w",stdout);
    n=getint();
    his[0]=new rope<char>();
    for(int i=1;i<=n;i++)
    {
        his[i]=new rope<char>(*his[i-1]);
        // deb(*his[i]);
        char opt=getC();
        if(opt=='T')
        {
            char x=getC();
            his[i]->push_back(x);
            updata(i);
        }
        else if(opt=='U')
        {
            updata(i);
            int x=getint();
            int l=1,r=i,mid,now=get(i);
            while(l<r)
            {
                mid=(l+r)>>1;
                if(now-get(mid)>x) l=mid+1;
                else r=mid;
            }
            his[i]=his[l-1];
        }
        else if(opt=='Q')
        {
            int x=getint()-1;
            putchar(his[i]->at(x));
            putchar('\n');
        } // deb(*his[i]);
    }
    return 0;
}

5. luogu AHOI2006文本编辑器editor【代替Splay】

题意

设计数据结构支持,插入删除反转字符串

分析:

由于rope的底层实现,insert,erase,get都是logn的
就是翻转不行,不是自己手写的打不了标记啊!!
怎么办?
答:同时维护一正一反两个rope……反转即交换两个子串……Orz……
区间循环位移?简单,拆成多个子串连起来就好了……
区间a变b b变c c变d …… z变a? 呃……维护26个rope?
区间和?滚蛋,那是线段树的活
区间kth?sorry,与数值有关的操作rope一概不支持……
5555 维修数列只能自己写了……

本题题解来源:https://www.cnblogs.com/vivym/p/3927949.html

#include <cstdio> 
#include <ext/rope> 
#include <iostream> 
#include <algorithm> 
using namespace std; 
using namespace __gnu_cxx; 
crope a,b,tmp; 
char s[10]; 
int now,n,len,size; 
char str[2000000],rstr[2000000]; 
int main()
{ 
    scanf("%d",&n); 
    while(n--)
    { 
        scanf("%s",s); 
        switch(s[0])
        { 
            case 'M':
            {
                scanf("%d",&now);
                break;
            } 
            case 'P':
            {
                now--;
                break;
            } 
            case 'N':
            {
                now++;
                break;
            } 
            case 'G':
            {
                putchar(a[now]);
                putchar('\n');
                break;
            } 
            case 'I':
            { 
                scanf("%d",&size); 
                len=a.length(); 
                for(int i=0;i<size;i++)
                { 
                    do
                    {
                        str[i]=getchar();
                    } 
                    while(str[i]=='\n'); 
                    rstr[size-i-1]=str[i]; 
                } 
                rstr[size]=str[size]='\0'; 
                a.insert(now,str); 
                b.insert(len-now,rstr); 
                break; 
            }               
            case 'D':
            { 
                scanf("%d",&size); 
                len=a.length(); 
                a.erase(now,size); 
                b.erase(len-now-size,size); 
                break; 
            } 
            case 'R':
            { 
                scanf("%d",&size); 
                len=a.length(); 
                tmp=a.substr(now,size); 
                a=a.substr(0,now) + b.substr(len-now-size,size) + a.substr(now+size,len-now-size); 
                b=b.substr(0,len-now-size)+tmp+b.substr(len-now,now); 
                break; 
            } 
        } 
    } 
    return 0; 
}

另一种写法+详细题解
https://wa-derfulanswer.blog.luogu.org/P4567
AC 代码 :

#include <cstdio>
#include <ext/rope>

const int N = 1<<22 + 7;
int n, k, cnt;
char now, inst[N], goal[N], bac[N];
struct readers{
	char now;
	bool state;
	readers operator >>(int &goal){
		goal = 0;
		state = true;
		while (now = getchar(), now < 48 || now > 57)state = now^45;
		while (48 <= now && now <= 57){
			goal = (goal<<1) + (goal<<3) + (now^48);
			now = getchar();
		}
		if (!state)goal = -goal;
		return *this;
	}
}reader;
__gnu_cxx::rope<char> a, b, tmp;

int main(){
	reader>>n;
	while (n --){
		/*bef:*/scanf("%s", inst);
//		if (inst[0] == 10)goto bef;
//		printf("inst:"), puts(inst);
		if (inst[0] == 'I'/*"Insert"*/ || inst[0] == 'M'/* == "Move"*/ || inst[0] == 'D'/* == "Delete"*/ || inst[0] == 'R'/* == "Rotate"*/){
			reader>>k;
			if (inst[0] == 'M')cnt = k;
			else if (inst[0] == 'I'){
				register int length = a.size();
				for (register int i = 0;i < k;i ++){
					bac[k - i - 1] = goal[i] = getchar();
				}
				goal[k] = bac[k] = '\0';
				a.insert(cnt, goal);
				b.insert(length - cnt, bac);
			}else if (inst[0] == 'D'){
				register int length = a.size();
				a.erase(cnt, k);
				b.erase(length - cnt - k, k);
			}else if (inst[0] == 'R'){
				register int length = a.size();
				tmp = a.substr(cnt, k);
				a = a.substr(0, cnt) + b.substr(length - cnt - k, k) + a.substr(cnt + k, length - cnt - k);
				b = b.substr(0, length - cnt - k) + tmp + b.substr(length - cnt, cnt);
			}
		}else if (inst[0] == 'P'/* == "Prev"*/)cnt --;
		else if (inst[0] == 'N'/* == "Next"*/)cnt ++;
		else if (inst[0] == 'G'/* == "Get"*/){
			putchar(a[cnt]);
			if (a[cnt] != 10)putchar(10);
		}
	}	
	return 0;
}

6. 总结

还是不太好用,数据大了容易MLE什么的,几道模板题都不能直接A,都是只能拿部分分,所以能不用就不用,如果遇见数据比较小的( 1 0 4 10^4 104 ~ 1 0 5 10^5 105非可持久化可以一试或者 1 0 4 10^4 104的可持久化数据结构可以一试)需要写可持久化的数据结构或者平衡树的基本操作什么的可以用rope试一下,但还是不要报有太大的幻想hhh。

也就是说只要不是卡的特别严格的题,都可以用rope试一下,我们可以想出思路之后先用rope实现一下,看看思路对不对,可以先交上去试试hhh,实在不行也可以用来对拍!

7. C++官方自带可持久化平衡树rope的3000行源码

将近三千行的源码hhh,放到这里太多了,我就放到一个新的博客里了。还是读一读源码会好一点

博客链接:https://fanfansann.blog.csdn.net/article/details/109608491

参考

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页