人类分块精华(Ex)

人类分块精华(Ex)

优雅,永不过时。

艺术的暴力,暴力的艺术——分块。

文艺分块术

没错,不是文艺平衡树,而是文艺分块术!!!

Part 1 Problem

您需要写一种数据结构,来维护一个长度为 \(n\) 的有序数列 \(A\),其中 \(A_i=i\)

其中需要提供以下操作:反转一个区间,例如原有序列是 5 4 3 2 1 ,反转区间是 [2,4] 的话,结果是 5 2 3 4 1 。

一共有 \(m\) 次操作,每次给定两个整数 \(l,r(l<r)\) 表示反转 \([l,r]\) 的元素。

输出 \(m\) 次操作后的序列。

Part 2 Solution

正解显然是文艺平衡树,然而块状链表也可以维护这样的一个序列,来看看此题的块状链表解法。

根据分块思想的“大段维护,局部暴力”的思想,可以分开考虑 \([l,r]\) 在同一块内和不在同一块内的情况。

\([l,r]\) 在同一块内,直接暴力 swap ,复杂度 \(O(\sqrt n)\)

\([l,r]\) 不在同一块内,那么需要考虑整段如何维护(设计标记),局部如何暴力。

在此之前,不妨先来看看链表、块状链表是如何支持区间翻转的:

假设现在有一个包含 1 2 3 4 5 的链表,现在要翻转区间 [2,4] ,如下图:

可以看到,实际上不需要真的“翻转”,只要适当改变前驱后继就可以满足要求,像上图,翻转 [2,4] 的实质就是:翻转区间第一个元素 (2) 的前驱 (1) 的后继变为翻转区间最后一个元素 (4) ,翻转区间第一个元素 (2) 的后继变为翻转区间最后一个元素 (4) 的后继,中间元素的前驱变后继,后继变前驱。

对于块状链表,每个链表节点内有多个元素,反转后,不仅前驱后继改变,块内元素也要改变访问顺序。可以用一个翻转标记来维护,每次翻转操作,把这个标记异或 1 ,得到这个块的状态。

对于左右零散块,先下传块内翻转标记,处理出要翻转的元素是谁。

这里显然不可以直接暴力 swap ,因为左右段要翻转的元素不一定一样多,如下图:

此时如果给中间段整段打 tag 标记的话,由于左右元素不一样多,暴力 swap 之后还需要再进行调整,很麻烦。此时应该下传中间段的标记,调整左右翻转元素的个数,再整体打标记。如下图:

剩下的左右翻转元素数量一样,暴力 swap 即可。

或者直接把所有元素全塞到别的块(也需要先下传标记,否则插入位置不对)里,再整体打标记。如下图:

这里第二种办法实现起来比较简单,不需要讨论左右块中,哪一个块要调整的元素多。(如果采用第一种方法,如果左块翻转元素多,则需要左块后插,右块元素多,则需要右块前插,细节处理较繁琐,并且实测代码比第二种长2.5k。当然另外一种做法是把中间的块里的元素补到零散块里,只是比第一种还要麻烦一些。)

调整完了之后要注意合并、维护块长正确,总复杂度 \(O(n\sqrt n)\)

Part 3 Code

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
//using namespace std;

const int maxn=200005;
const int maxblock=2005;

int Trashcan[maxn],top,used;
int n,m,tot,len;
struct Node{
    int pre,nxt;//前驱后继
    int tag;//翻转标记
    int lenth;//块长
    int a[maxblock];
}Block[maxblock];

inline int read(){
    int x=0,fh=1;
    char ch=getchar();
    while(!isdigit(ch)){
        if(ch=='-')
            fh=-1;
        ch=getchar();
    }
    while(isdigit(ch)){
        x=(x<<3)+(x<<1)+ch-'0';
        ch=getchar();
    }
    return x*fh;
}//快读

inline void swap(int &a,int &b){
    if(a^b) a^=b^=a^=b;
}//快换

inline void Init(){
    n=read(),m=read();
    len=sqrt(n);//设定标准块长len
    tot=(n-1)/len+1;//设定初始总块数tot
    for(int i=1;i<=n;++i){
        int belong=(i-1)/len+1;
        Block[belong].a[++Block[belong].lenth]=i;//预处理块内元素
    }
    for(int i=1;i<=tot;++i)
        Block[i].pre=i-1,Block[i].nxt=i+1;//链接为链表
    Block[tot].nxt=0;
}

inline void reset(int id){//下传id块标记
    if(Block[id].tag){
        int mid=(Block[id].lenth+1)>>1;
        for(int i=1;i<=mid;++i)
            swap(Block[id].a[i],Block[id].a[Block[id].lenth+1-i]);
        Block[id].tag=0;
    }
}

inline void clear(int id){//彻底清除、回收id块
    Trashcan[++top]=id;
    Block[id].lenth=Block[id].nxt=Block[id].pre=Block[id].tag=0;
    memset(Block[id].a,0,sizeof Block[id].a);
}

inline void build(const int id){//在id后面新建一个块
    int ip;
    if(used<top) ip=Trashcan[++used];
    else ip=++tot;
    Block[ip].nxt=Block[id].nxt;
    Block[Block[id].nxt].pre=ip;
    Block[id].nxt=ip;
    Block[ip].pre=id;
}

inline void merge(int id){//合并id和其后面的块
    int ip=Block[id].nxt;
    reset(id);reset(ip);
    for(int i=1;i<=Block[ip].lenth;++i)
        Block[id].a[i+Block[id].lenth]=Block[ip].a[i];
    Block[id].lenth+=Block[ip].lenth;
    Block[id].nxt=Block[ip].nxt;
    Block[Block[id].nxt].pre=id;
    clear(ip);
}

inline void split(int id){//分裂id块
    reset(id);build(id);
    int ip=Block[id].nxt;
    for(int i=len+1;i<=Block[id].lenth;++i)
        Block[ip].a[i-len]=Block[id].a[i];
    Block[ip].lenth=Block[id].lenth-len;
    Block[id].lenth=len;
}

inline void Print(){//遍历链表,输出答案
    for(int i=1;i!=0;i=Block[i].nxt){
        if(!Block[i].tag)
            for(int j=1;j<=Block[i].lenth;++j)
                printf("%d ",Block[i].a[j]);
        else
            for(int j=Block[i].lenth;j>=1;--j)
                printf("%d ",Block[i].a[j]);
    }
}

int belongL,belongR;
inline void Getpoistion(int &l,int &r){//找到l,r的位置
    belongL=1,belongR=1;
    while(l>Block[belongL].lenth){
        l-=Block[belongL].lenth,r-=Block[belongL].lenth;
        belongL=belongR=Block[belongL].nxt;
    }
    while(r>Block[belongR].lenth){
        r-=Block[belongR].lenth;
        belongR=Block[belongR].nxt;
    }
}

inline void reverse(int l,int r){//最重要的翻转操作
    Getpoistion(l,r);
    if(belongL==belongR){//在同一段内,直接暴力
        reset(belongL);
        int mid=(l+r)>>1;
        for(int i=l,j=r;i<=mid;++i,--j)
            swap(Block[belongL].a[i],Block[belongL].a[j]);
        return;
    }else{
        reset(belongL);reset(belongR);//下传零散段标记
        if(Block[belongL].nxt==belongR)
//如果l所处段后继为r所处段,不能直接往中间段塞元素(因为它不存在)要先新建一个
            build(belongL);
        int revL=Block[belongL].lenth-l+1;//左端零散翻转元素数量
        int Nxt=Block[belongL].nxt;
        int Pre=Block[belongR].pre;
        reset(Nxt);reset(Pre);//找到即将被塞进元素的块,下传他们的标记
        for(int i=Block[Nxt].lenth;i>=1;--i)
            Block[Nxt].a[i+revL]=Block[Nxt].a[i];
        for(int i=l,j=1;i<=Block[belongL].lenth;++i,++j)
            Block[Nxt].a[j]=Block[belongL].a[i];//左端往后塞零散元素
        Block[belongL].lenth-=revL;
        Block[Nxt].lenth+=revL;//重新处理段长
        for(int i=Block[Pre].lenth+1,j=1;j<=r;++i,++j)
            Block[Pre].a[i]=Block[belongR].a[j];
        for(int i=1;i<=Block[belongR].lenth;++i)
            Block[belongR].a[i]=Block[belongR].a[i+r];//右端往前塞零散元素
        Block[Pre].lenth+=r;
        Block[belongR].lenth-=r;//重新处理段长
        for(int i=Block[belongL].nxt;i!=belongR;i=Block[i].pre)
            swap(Block[i].pre,Block[i].nxt),Block[i].tag^=1;//改变前驱后继,整体标记
        swap(Block[belongL].nxt,Block[belongR].pre);
        Block[Block[belongL].nxt].pre=belongL;
        Block[Block[belongR].pre].nxt=belongR;
        if(Block[Nxt].lenth>(len<<1)) split(Nxt);
        if(Block[Pre].lenth>(len<<1)) split(Pre);
        if(Block[belongL].lenth+Block[Block[belongL].pre].lenth<len && Block[belongL].pre) merge(Block[belongL].pre);
        if(Block[belongR].lenth+Block[Block[belongR].nxt].lenth<len && Block[belongR].nxt) merge(belongR);//及时分裂、合并维护块长大约为len
    }
}

signed main(){
    Init();
    while(m--){
        int L=read(),R=read();
        reverse(L,R);          
    } 
    Print(); 
    return 0;
}

再扩展

块状链表不仅是链表,还是分块!

它在变成一棵文艺平衡树的同时,并没有失去分块的性质。也就是说,它依旧可以维护序列中的问题,再换句话说,在某种情况下,它可以完美的替代掉文艺平衡树,譬如下面这道题目:

P4146 序列终结者

题目中要求支持翻转、区间加、区间最大值,翻转操作显然可以用上述的文艺分块术解决。

区间加是分块的基本操作,需要在块内维护一个 add 标记。区间最大值可以通过 \(O(\sqrt n)\) 遍历块提前预处理求出,每次区间加 \(v\) 的时候,最大值也加 \(v\) , 而整块翻转操作显然对这两个标记没有影响。

对于零散块,要把翻转标记和 add 标记都下传,然后再交换对应块元素或者往中间块塞元素。交换后,不管是被塞进元素的块还是往外塞元素的块,都要重新遍历块统计最大值,因为这个最大值可能在这个过程中被交换掉。

Code

这里给出在上面分析原理时提到的第一种方法的代码,另外,介于分块文艺术的代码上面已经有详细介绍了,这里不再给出代码注释。(别的就是分块维护 add 标记什么的不是很简单?好叭其实是我懒

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>

//using namespace std;

inline int read(){
	int x=0,fh=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')
			fh=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+ch-'0';
		ch=getchar();
	}
	return fh*x;
}

const int maxn=100005;
#define inf 0x3f3f3f3f

int n,m,tot,len;
int Trashcan[maxn],top,used;

struct Node{
	int a[2005];
	int pre,nxt;//前驱、后继
	int lenth;//长度
	int tag;//反转标记
	int add;//增量标记
	int maxmum;//区间最大值
}Block[2005];

inline void swap(int &a,int &b){
	if(a^b) a^=b^=a^=b;
}

inline void reset(const int id){//下传标记
	if(Block[id].tag==1){
		int mid=(Block[id].lenth+1)>>1;
		for(int i=1,j=Block[id].lenth;i<=mid;++i,--j)
			swap(Block[id].a[i],Block[id].a[j]);
		Block[id].tag=0;
	}
	for(int i=1;i<=Block[id].lenth;++i)
		Block[id].a[i]+=Block[id].add;
	Block[id].add=0;
}

inline void update(const int id){//在没有add标记时,重新计算最大值
	Block[id].maxmum=-inf;
	for(int i=1;i<=Block[id].lenth;++i)
		Block[id].maxmum=std::max(Block[id].maxmum,Block[id].a[i]);
}

inline void clear(const int id){//删除编号为id的块
	Trashcan[++top]=id;
	memset(Block[id].a,0,sizeof Block[id].a);
	Block[id].lenth=0;
	Block[id].pre=Block[id].nxt=0;
	Block[id].tag=0;
	Block[id].maxmum=0;
	Block[id].add=0;
}

inline void Build(const int id){//在id后面新建一个块
	int ip;
	if(used<top) ip=Trashcan[++used];
	else ip=++tot;
	Block[ip].nxt=Block[id].nxt;
  Block[Block[ip].nxt].pre=ip;
  Block[ip].pre=id;
  Block[id].nxt=ip;
}

inline void Delete(const int id){//删除编号为id的块
  int Nxt=Block[id].nxt;
  int Pre=Block[id].pre;
  Block[Pre].nxt=Nxt;
  Block[Nxt].pre=Pre;
  clear(id);
}

int belongL,belongR;

inline void Getpoisition(int &l,int &r){
	belongL=belongR=1;
	while(l>Block[belongL].lenth){
		l-=Block[belongL].lenth,r-=Block[belongL].lenth;
		belongL=belongR=Block[belongL].nxt;
	}
	while(r>Block[belongR].lenth){
		r-=Block[belongR].lenth;
		belongR=Block[belongR].nxt;
	}
}

inline void split(const int id){
	Build(id);reset(id);
	int ip=Block[id].nxt;
	for(int i=len+1;i<=Block[id].lenth;++i)
		Block[ip].a[i-len]=Block[id].a[i];
	Block[ip].lenth=Block[id].lenth-len;
	Block[id].lenth=len;
	update(id);update(ip);
}

inline void merge(const int id){
	int ip=Block[id].nxt;
	reset(id);reset(ip);
	for(int i=Block[id].lenth+1,j=1;j<=Block[ip].lenth;++i,++j)
		Block[id].a[i]=Block[ip].a[j];
	Block[id].lenth+=Block[ip].lenth;
	update(id);Delete(ip);
}

inline void Add(int l,int r,const int v){
	Getpoisition(l,r);
	if(belongL==belongR){
		reset(belongL);
		for(int i=l;i<=r;i++)
			Block[belongL].a[i]+=v;
		update(belongL);
	}else{
		reset(belongL);
		for(int i=l;i<=Block[belongL].lenth;++i)
			Block[belongL].a[i]+=v;
		update(belongL);
		for(int i=Block[belongL].nxt;i!=belongR;i=Block[i].nxt)
			Block[i].add+=v,Block[i].maxmum+=v;
		reset(belongR);
		for(int i=1;i<=r;++i)
			Block[belongR].a[i]+=v;
		update(belongR);
	}
}

inline int Getmax(int l,int r){
	int max=-inf;
	Getpoisition(l,r);
	if(belongL==belongR){
		reset(belongL);
		for(int i=l;i<=r;++i)
			max=std::max(max,Block[belongL].a[i]);
	}else{
		reset(belongL);
		for(int i=l;i<=Block[belongL].lenth;++i)
			max=std::max(max,Block[belongL].a[i]);
		for(int i=Block[belongL].nxt;i!=belongR;i=Block[i].nxt)
			max=std::max(max,Block[i].maxmum);
		reset(belongR);
		for(int i=1;i<=r;i++)
			max=std::max(max,Block[belongR].a[i]);
	}
	return max;
}

inline void reverse(int l,int r){
	Getpoisition(l,r);
	if(belongL==belongR){
		reset(belongL);
		int mid=(l+r)>>1;
		for(int i=l,j=r;i<=mid;++i,--j)
			swap(Block[belongL].a[i],Block[belongR].a[j]);
		return;
	}else{
		reset(belongL);reset(belongR);
		int revL=Block[belongL].lenth+1-l;
		if(revL==r){
			for(int i=l,j=r;i<=Block[belongL].lenth;++i,--j)
				swap(Block[belongL].a[i],Block[belongR].a[j]);
			update(belongL);update(belongR);
		}else if(Block[belongL].nxt!=belongR){
			if(revL>r){
				int tmp=revL-r;
				int Nxt=Block[belongL].nxt;
				reset(Nxt);
				for(int i=Block[Nxt].lenth;i>=1;--i)
					Block[Nxt].a[i+tmp]=Block[Nxt].a[i];
				for(int i=Block[belongL].lenth-tmp+1,j=1;j<=tmp;++i,++j)
					Block[Nxt].a[j]=Block[belongL].a[i];
				Block[Nxt].lenth+=tmp;	
				Block[belongL].lenth-=tmp;
				update(Nxt);
				for(int i=l,j=r;i<=Block[belongL].lenth;++i,--j)
					swap(Block[belongL].a[i],Block[belongR].a[j]);
				update(belongL);update(belongR);
			}else{
				int tmp=r-revL;
				int Pre=Block[belongR].pre;
				reset(Pre);
				for(int i=Block[Pre].lenth+1,j=1;j<=tmp;++i,++j)
					Block[Pre].a[i]=Block[belongR].a[j];
				for(int i=1;i<=Block[belongR].lenth;++i)
					Block[belongR].a[i]=Block[belongR].a[i+tmp];
				Block[Pre].lenth+=tmp;
				Block[belongR].lenth-=tmp;
				update(Pre);
				for(int i=Block[belongL].lenth,j=1;i>=l;--i,++j)
					swap(Block[belongL].a[i],Block[belongR].a[j]);
				update(belongL);update(belongR);
			}
		}else{
			if(revL>r){
        for(int i=r,j=l;i>=1;--i,++j)
          swap(Block[belongL].a[j],Block[belongR].a[i]);      
        int mid=(l+r+Block[belongL].lenth)>>1;
        for(int i=l+r,j=Block[belongL].lenth;i<=mid;++i,--j)
          swap(Block[belongL].a[i],Block[belongL].a[j]);
				update(belongL);update(belongR);
      }else{
        for(int i=l,j=r;i<=Block[belongL].lenth;++i,--j)
          swap(Block[belongL].a[i],Block[belongR].a[j]);  
        int mid=(r-revL+1)>>1;
        for(int i=1,j=r-revL;i<=mid;++i,--j)
          swap(Block[belongR].a[i],Block[belongR].a[j]); 
				update(belongL);update(belongR);        
      }
		}
		if(Block[belongL].nxt!=belongR){//puts("QWQ");
			for(int i=Block[belongL].nxt;i!=belongR;i=Block[i].pre)
				swap(Block[i].pre,Block[i].nxt),Block[i].tag^=1;
			swap(Block[belongL].nxt,Block[belongR].pre);
			Block[Block[belongL].nxt].pre=belongL;
      Block[Block[belongR].pre].nxt=belongR;
		}
		if(Block[Block[belongL].nxt].lenth>(len<<1)) split(Block[belongL].nxt);
    if(Block[Block[belongR].pre].lenth>(len<<1)) split(Block[belongR].pre);
    if(Block[belongL].lenth+Block[Block[belongL].nxt].lenth<len) merge(belongL);
    if(Block[belongR].lenth+Block[Block[belongR].nxt].lenth<len && Block[belongR].nxt) merge(belongR);
	}
}

inline void Init(){
	n=read(),m=read();
	len=sqrt(n);
	tot=(n-1)/len+1;
	for(int i=1;i<=n;++i){
    int belong=(i-1)/len+1;
    Block[belong].a[++Block[belong].lenth]=0;
  }
	for(int i=1;i<=tot;++i)
		Block[i].pre=i-1,Block[i].nxt=i+1;
	Block[tot].nxt=0;
}

inline void Print(){
  for(int i=1;i!=0;i=Block[i].nxt){
    reset(i);
    for(int j=1;j<=Block[i].lenth;++j)
      printf("%d ",Block[i].a[j]);
  }
	puts("");
}

signed main(){
	// freopen("1.in","r",stdin);
	// freopen("ans.out","w",stdout);
	Init();
	while(m--){
		int opt=read(),l=read(),r=read(),v;
		if(opt==1){
			v=read();
			Add(l,r,v);
		}else if(opt==2)
			reverse(l,r);
		else printf("%d\n",Getmax(l,r));
		//Print();
	}
	return 0;
}

代码略长,仅供娱乐和参考。

posted @ 2021-06-27 10:05  ZTer  阅读(139)  评论(0编辑  收藏  举报