2021 第十二届 蓝桥杯 双向排序 题解 栈+文艺平衡树

本文解析2021年第十二届蓝桥杯比赛中的一道排序问题,利用栈配合文艺平衡树实现O(n logn)时间复杂度的解决方案。文章介绍了思路、实现方法,并指出在实际操作中栈的高效性,最后提供了只使用栈的代码实现。
摘要由CSDN通过智能技术生成

2021 第十二届蓝桥杯 双向排序 题解 栈+文艺平衡树

题目描述

原题链接:https://www.lanqiao.cn/problems/1458/learning/
题目描述

思路

快排的时间复杂度是O(n*(n logn )),考虑到可能会超时,所以比赛的时候想用文艺平衡树这种单次操作只需要最多O(logn)就能翻转任意区间的神奇数据结构,整体的时间复杂度一下子就降到了优秀的O(n logn)。

实现方法

本篇不详细介绍文艺平衡树,若您需要这项前置技能,那么强烈为您推荐洛谷模板题

如何操作?这里有一个难点,题目的要求是排序,而文艺平衡树的区间翻转是不能保证有序的,所以每次翻转区间一定是在一个有序的区间内部进行操作,才能保证答案的正确。

那这一点又如何保证呢?维护一个降序栈,使得每次翻转区间都在上一个翻转后的区间内部进行。
如果有两个连续的指令都是让我们排降序,比如0 3和0 5,那么0 3这条指令就不需要进行操作,因为只进行0 5这步操作 和 进行两步操作是一样的(需要稍微脑补一下)。
{ 1 2 3 4 5 6 7 }
0 3
{ 3 2 1 4 5 6 7 }
0 5
{ 5 4 3 2 1 6 7 }

所以,连续的0类指令,只需要取最大的那个,连续的1类指令,只需要取最小的那个,于是最后我们就得到了一堆0类1类交替的指令,这一点很重要,我们需要维护0类1类指令的严格交替!观察一下下面的两个操作。
{ 1 2 3 4 5 6 7 }
0 5
{ 5 4 3 2 1 6 7 }
1 4
{ 5 4 3 1 2 6 7 }
0 5的意义就是将[1,5]这个区间进行降序操作,而一开始整个区间[1,7]都是升序的,我们要让[1,5]降序,那就只需要取[1,7]和[1,5]的交集,然后翻转这个交集,显然这个交集就是[1,5],翻转[1,5],于是我们得到了一个降序的[1,5]区间,1 4的意义就是将[4,7]这个区间进行升序操作,而最右边的区间[6,7]已经是升序的了,并且里面所有的数字都比它们左边所有的数字都要大,所以[6,7]区间的数字并不会变化,我们只需要接着取交集,取[1,5]和[4,7]的交集,得到一个[4,5]的区间,翻转[4,5]就是我们最终的答案。

于是,你可能跟我一样,发现了华点,要是上面两个操作之后,突然出现一个0 7的指令怎么办?按照之前的说法,我们不能通过取 [1,7]和[4,5]的交集翻转,显然答案是错误的。

所以不是连续的0操作最大就行了,而是要让后面比较大的0操作,覆盖掉之前所有比它小的操作,其间的1和0操作都会因为这个比较大的0操作而变得无效。

考虑维护两个栈,或者用数组模拟写在一个栈里也行,0操作严格降序,1操作严格升序,并且一定要让0和1交替出现,最差的情况会长这样:
案例1
这个栈维护完了之后就直接按照之前的方法一直循环取交集翻转就可以了,并且这个交集的长度只有1的时候就可以直接跳出了,后面的操作都没有意义。

本人Java C组,学了很多算法今天终于能用上了 很开心,最后一分钟的时候把那个栈维护出来了,非常兴奋交了上去。结果比赛结束后发现没有改类名!!!!铁定0分了,还不如直接sort呢 真是悲伤,java组的同胞们一定要引以为戒,仔细检查类名和包!

代码

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;


public class Main
{
	static int[]val,f,siz;//val-值,f-当前节点的父亲,siz-当前节点的大小
	static boolean[]lazy;//当前节点的 翻转懒标记
	static int[][]ch;//ch[i][0]-i节点的左儿子,ch[i][1]-i节点的右儿子
	static int len,n,root;//len用来初始化splay树,n-输入的n,root-splay树的根节点
	static int init(int be,int ed,int fa)
	{
		/* 模访线段树的初始化方法
		 * 初始化splay
		 */
		if(be>ed)return 0;
		int a=len++;
		f[a]=fa;
		int mid=(be+ed)>>1;
		int l=init(be,mid-1,a);
		val[a]=mid;
		int r=init(mid+1,ed,a);
		ch[a][0]=l;
		ch[a][1]=r;
		update(a);
		return a;
	}
	static void pushdown(int a){
		/*
		 * 向下传递懒标记
		 */
		if(lazy[a]){
			int stack=ch[a][0];
			ch[a][0]=ch[a][1];
			ch[a][1]=stack;
			lazy[ch[a][0]]^=true;
			lazy[ch[a][1]]^=true;
			lazy[a]=false;
		}
	}
	static void connect(int a,int fa,int s){
		if(fa!=0)ch[fa][s]=a;
		if(a!=0)f[a]=fa;
	}
	static void update(int a){
		siz[a]=siz[ch[a][0]]+siz[ch[a][1]]+1;
	}
	static void rotate(int a){
		int fa=f[a],ffa=f[fa];
		int s=ch[fa][0]==a?0:1;
		connect(ch[a][1^s],fa,s);
		connect(a,ffa,ch[ffa][0]==fa?0:1);
		connect(fa,a,1^s);
		update(fa);
		update(a);
	}
	static int find(int s){
		int a=root;
		while(true){
			pushdown(a);
			if(siz[ch[a][0]]>=s)a=ch[a][0];
			else {
				s-=siz[ch[a][0]]+1;
				if(s==0)return a;
				a=ch[a][1];
			}
		}
	}
	static void splay(int a,int to){
		while(f[a]!=to){
			int fa=f[a];
			if(f[fa]!=to)rotate((ch[fa][0]==a)==(ch[f[fa]][0]==fa)?a:fa);
			rotate(a);
		}
		if(to==0)root=a;
	}
	static void re(int a,int b)
	{
		/*
		 * 翻转区间[a,b]
		 */
		splay(find(a),0);
		splay(find(b+2),root);
		lazy[ch[ch[root][1]][0]]^=true;
	}
	static boolean flag=false;
	static void dfs(int a)
	{
		/*
		 * 最后dfs中序遍历输出
		 */
		pushdown(a);
		if(ch[a][0]!=0)dfs(ch[a][0]);
		if(val[a]>=1&&val[a]<=n){
			if(flag)out.print(" ");
			else flag=true;
			out.print(val[a]);
		}
		if(ch[a][1]!=0)dfs(ch[a][1]);
	}
	public static void main(String[]args) throws IOException{
		n=in();
		ch=new int[n+3][2];
		val=new int[n+3];
		f=new int[n+3];
		siz=new int[n+3];
		lazy=new boolean[n+3];
		len=1;
		int m=in();
		root=init(0,n+1,0);
		int[]stack=new int[m];
		int cnt=0;
		while(m-->0){
			int op=in();
			int mid=in();
			{//维护栈
				if(op==0){
					if(cnt%2!=op)//如果要放入的指令与栈中最后一个指令类型相同
					{
						if(cnt-1>=0&&stack[cnt-1]<=mid)cnt--;//则进行第一轮比较,如果当前更大则弹出最后一个
						else continue;//否则直接舍去当前指令,进入下一层循环
					}
					while(cnt-2>=0&&stack[cnt-2]<=mid) cnt-=2;//循环弹出
				}else{
					if(cnt%2!=op)//如果要放入的指令与栈中最后一个指令类型相同
					{
						if(cnt-1>=0&&stack[cnt-1]>=mid)cnt--;//则进行第一轮比较,如果当前更小则弹出最后一个
						else continue;//否则直接舍去当前指令,进入下一层循环
					}
					while(cnt-2>=0&&stack[cnt-2]>=mid) cnt-=2;//循环弹出
				}
				stack[cnt++]=mid;//压入栈
			}
		}
		int l=1;
		int r=n;
		for(int i=0;i<cnt;i++){
			if(i%2==1){
				l=stack[i];
			}
			else {
				r=stack[i];
			}
			if(r-l<1)break;
			re(l,r);
		}
		flag=false;
		dfs(root);
		out.flush();
	}
	static StreamTokenizer in=new StreamTokenizer (new BufferedReader(new InputStreamReader(System.in)));
	static PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
	static int in() throws IOException{
		in.nextToken();
		return(int)in.nval;
	}
}


只用栈

在快要写完这篇博客的时候,写着写着发现这东西根本不需要文艺平衡树,栈维护出来之后,区间外部的东西就永远都不会动了,所以只需要一边缩区间一边填数字就行了,时间复杂度甚至近似于O(n)。。。。。。。。。。。。。。。。。我裂了。

只用栈的代码

Java

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Main
{
	public static void main(String[]args) throws IOException{
		int n=in();
		int m=in();
		int[]sta=new int[m];
		int cnt=0;
		while(m-->0){
			int op=in();
			int mid=in();
			{//维护栈
				if(op==0){
					if(cnt%2!=op)//如果要放入的指令与栈中最后一个指令类型相同
					{
						if(cnt-1>=0&&sta[cnt-1]<=mid)cnt--;//则进行第一轮比较,如果当前更大则弹出最后一个
						else continue;//否则直接舍去当前指令,进入下一层循环
					}
					while(cnt-2>=0&&sta[cnt-2]<=mid) cnt-=2;//循环弹出
				}else{
					if(cnt%2!=op)//如果要放入的指令与栈中最后一个指令类型相同
					{
						if(cnt-1>=0&&sta[cnt-1]>=mid)cnt--;//则进行第一轮比较,如果当前更小则弹出最后一个
						else continue;//否则直接舍去当前指令,进入下一层循环
					}
					while(cnt-2>=0&&sta[cnt-2]>=mid) cnt-=2;//循环弹出
				}
				sta[cnt++]=mid;//压入栈
			}
		}
		int l=1;
		int r=n;
		int[] ans=new int[n+1];
		//x从大到小,从外到内填数字
		int x=n;
		for(int i=0;i<cnt;i++){
			int mid=sta[i];
			if(i%2==1){
				mid=Math.min(r, mid);
				while(l<mid)ans[l++]=x--;
			}
			else {
				mid=Math.max(l, mid);
				while(r>mid)ans[r--]=x--;
			}
			if(r-l<1)break;
		}
		if(l<=r)
			if(cnt%2==1) {
				while(l<=r)ans[l++]=x--;
			}else {
				while(l<=r)ans[r--]=x--;
			}
		out.print(ans[1]);
		for(int i=2;i<=n;i++) {
			out.print(" "+ans[i]);
		}
		out.println();
		out.flush();
	}
	static StreamTokenizer in=new StreamTokenizer (new BufferedReader(new InputStreamReader(System.in)));
	static PrintWriter out=new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
	static int in() throws IOException{
		in.nextToken();
		return(int)in.nval;
	}
}

c++:

#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    int sta[m];//栈数组
    int cnt=0;//栈的长度
    while(m-->0)
    {
        int op;
        int mid;
        scanf("%d %d",&op,&mid);

        if(op==0){
            if(cnt%2!=op)
            {
                if(cnt-1>=0&&sta[cnt-1]<=mid)cnt--;
                else continue;
            }
            while(cnt-2>=0&&sta[cnt-2]<=mid){
                cnt-=2;
            }
        }else{
            if(cnt%2!=op)
            {
                if(cnt-1>=0&&sta[cnt-1]>=mid)cnt--;
                else continue;
            }
            while(cnt-2>=0&&sta[cnt-2]>=mid){
                cnt-=2;
            }
        }
        sta[cnt++]=mid;
    }
    int l=1;
    int r=n;
    int ans[n+1];//答案数组
    int x=n;
    for(int i=0;i<cnt;i++){
        int mid=sta[i];
        if(i%2==1){
            mid=min(r, mid);
            while(l<mid)ans[l++]=x--;
        }
        else {
            mid=max(l, mid);
            while(r>mid)ans[r--]=x--;
        }
        if(r-l<1)break;
    }
    if(l<=r)
        if(cnt%2==1) {
            while(l<=r)ans[l++]=x--;
        }else {
            while(l<=r)ans[r--]=x--;
        }

    printf("%d",ans[1]);
    for(int i=2;i<=n;i++)
    {
        printf(" %d",ans[i]);
    }
    printf("\n");
}
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值