题解 SP3544 【BST - Binary Search Tree】

· · 题解

题解 SP3544 【BST - Binary Search Tree】

题目传送门:

https://www.luogu.org/problemnew/show/SP3544

这题。。。题目迷惑性很大

不得不说是一道够毒的题。。。但是脑洞还是很大的

================================================================

先扯一些没用的东西。。。

这题是我们同学没事闲的跳到的。。。然后就开始尝试切掉

可能是我太pupil了,一上来就按着题目的“指示”打了个BST,结果T到死233

本来以为是个BST的板子题,来练练手,结果我的一下午就这样过去了233

(我不会告诉你我把我同学的做法卡下去了嘿嘿嘿)

那就来讲一下如何做吧

这题上来就丢给我们一个BSTinsert函数的板子

之后就问我们(其实也是题目大意):

向一个最初为空的BST中依次插入一个给定的数列,其中每一次调用insert函数都给计数器C加上1,试求每次插入完数之后计数器C的值

我们直接模拟一下题面里的$insert$函数交上去,会发现完美地$T$掉了 其实仔细想一下都可以知道,这题肯定没有这么简单 ~~(怎么能让你简简单单地过掉一道题呢QAQ)~~ 我们知道对于一个普通的$BST$,当向其中**插入一个有序数列**时,$BST$会退化成一条链,因此它的$insert$函数的单次时间复杂度会变成$O(n)$的,那么插入$n$个数最后就是$O(n^2)$的,又因为$n<=300000$,于是$O(n^2)$是妥妥的$T$掉了 不妨。。。换个思路? 换位思考一下,我们何必维护一颗$BST$呢 对于一颗$BST$,可以证明的是,对于要插入的数$x

x只会最终接在x的前驱或后继的下一层,这是十分显然的

而且x最终一定是这两者中深度较大的那一个的儿子节点上

况且对于x的前驱和后继,x的前驱一定是x的后继的祖先

那么我们就不必建立一颗BST去模拟过程

而是可以每次二分O(logn)的寻找x的前驱和后继

找到x前驱和后继二者深度最大的那个其深度+1即为x的深度

对答案的贡献其实就是这个深度累加起来即可

可以直接建立map,在map内存上每个数的dep和值(key)

之后在maplower_bound查找就行了

又由于lower_bound单次复杂度是O(logn)

那么最终插入完整个序列则是O(nlogn)

写完之后发现好像有点不太好。。。可能数据稍微大一些就死了

但是总感觉这个过程可以用别的方法来写

或许还有O(n)做法吧。。。可是我太菜了想不出。。。

也许等我想出了O(n)做法就来更新一下题解233

到这里大概也就讲完了。。。

如果不会c++ map自带的lower_bound请去问度娘

(我也没有什么好的链接233)

下面放两个代码,第一个是最开始做的时候的代码(比较慢),第二个是(丧心病狂)卡完常之后的代码

本人代码丑,各位dalao不喜勿喷

code1:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<map>
using namespace std;
typedef long long ll;
inline int read()
{
    int p=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
    return f*p;}
//const int maxn=3e5+3;
int n;
ll ans;
map <int,int> dep;
map <int,int>::iterator it;
int main()
{
    /*(以下是我写题的时候的心路历程)
    分析:
    可以发现这题直接模拟是肯定会挂的
    因为n<=300000,而数据又是不一定的
    可能出现一个有序序列使得每次插入元素都是O(n)的 
    那么累加起来就是个O(n^2)的模拟算法
    显然复杂度不可接受 

    那么不妨换位思考一下 
    我们何必维护一颗二叉搜索树呢 
    由于这是一颗BST
    可以证明的是,对于一个序列中的要插入的数x 
    它只会最终接在x的前驱或后继的下一层,这是十分显然的 
    而且x最终一定是这两者中深度较大的那一个的儿子节点上
    况且对于x的前驱和后继,一定有x的前驱是x的后继的祖先 

    那么我们其实就不必建立一颗BST去模拟过程 
    而是每次在map中O(logn)的寻找x的前驱和后继 
    找到一个二者深度最大的位置+1即为x的深度

    对答案的贡献其实就是这个深度,累加起来即可
    那么直接建立map,之后在map内lower_bound查找就行了
    lower_bound单次复杂度是O(logn)的 
    那么最终插入完整个序列则是O(nlogn)的 

    可能还有O(n)做法吧。。。可是我太菜了想不出。。。 
    */
    n=read();
    for(int i=1;i<=n;i++)
        {
            int x=read(),pos=0,new_dep1,new_dep2;
            it=dep.lower_bound(x);//找到第一个>=x的数 
            if(it!=dep.end())//存在一个第一个>=x且已经插入了的数 
                {
                    new_dep1=it->second+1;
                    pos=max(pos,new_dep1);
//                  printf("\nQAQ %d\n",it->first);
                }
            if(it!=dep.begin())//找不到一个>=x的数,则找第一个<x的数的位置 
                {
                    new_dep2=(--it)->second+1;
                    pos=max(pos,new_dep2);
//                  printf("\nQWQ %d\n",it->first);
                }
            dep[x]=pos,ans+=pos;
            //在较深的位置插入节点,并累加新的深度贡献 
            printf("%lld\n",ans);
        }
    return 0;
}

code2:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<map>
#define re register
using namespace std;
typedef long long ll;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()
{
    int now=0,f=1;re char c=gc();
    for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
    for(;isdigit(c);now=now*10+c-'0',c=gc());
    return now*f;}
int n;
ll ans;
map <int,int> dep;
map <int,int>::iterator it;
int main()
{
    n=read();
    for(re int i=1;i<=n;i++)
        {
            int x=read(),pos=0;
            it=dep.lower_bound(x);
            if(it!=dep.end())pos=max(pos,it->second+1);
            if(it!=dep.begin())pos=max(pos,(--it)->second+1);
            dep[x]=pos,ans+=pos;
            printf("%lld\n",ans);
        }
    return 0;
}

大概。。。这题就到这里了吧。。。

=================================================================

前方高能预警!

上面的$map$做法写完之后我又回去思索了一下 大概想了想优化。。。最开始感觉没啥可以优化的 但是总觉得$O(nlogn)$有点扯,数据大真的会挂 毕竟在$SPOJ$上我的$O(nlogn)$都进不了$rk$前$20

所以滚回来再想一想优化方法。。。

仔细想了一想(大概又过了1个半小时吧)发现好像可以O(n)

我上面也说可能有一种O(n)的方法,但是当时没想出来,所以写的是O(nlogn)

但是现在,O(n)做法终于搞完辣!

先公布一下好结果:O(n)做法在SPOJ上杀到了c++4.3.2rk 1!

(我的SPOJ用户名:new2zy

其实自己都没想到能rk 1的。。。

行了,不说没用的了,直接开讲

上面的思路很清晰了,对于每一个要插入的数找到前驱和后继就行了

但是发现maplower_boundO(nlogn)的,我们想能不能改进一下,使它的效率能大大提高,甚至变成O(1)

而对于找前驱和后继的问题,貌似也就只能想到平衡树了233

可能我是因为我不会平衡树的原因(我还是太菜了),我想到了另外一种东西:链表

因为链表是一个链状结构,每一个节点都有一个前驱节点和后继节点

发现正好符合上面的要求:对于一个数要找前驱和后继

那么我们不妨换个思路,倒着来

(我用了数组模拟链表)

对于一个初始有n个节点的链表,我们要在其中删除数

删除一个数(所在节点)后,它原位置的前驱和后继的指针会随之更新

那么在删除前它的前驱和后继肯定也是固定的

我们只需要倒序删掉所有数,同时记下每个数前驱后继就行了

最后正着还是按depth[i]=max(depth[pre],depth[nex])+1算就行了

也是累加贡献,即为每一次的答案

考虑一下这个算法的时间复杂度

由于每个元素在链表中只被删除一次,所以复杂度是O(n)

那么。。。这就是改进之后的算法了

这样就再也不用效率低的map

可能自己又口胡了一个神奇做法?结果神奇的过掉了233

下面还是放两个代码,一个是我写的第一个优化(带注释版),另一个是过掉之后的卡常版

还是那句话,本人代码丑,各位dalao不喜勿喷

code3:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf=1e9+7;
inline int read()
{
    int p=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){p=p*10+c-'0';c=getchar();}
    return f*p;}
const int maxn=300003;
int n,opt[maxn],L[maxn],R[maxn],dep[maxn],pre[maxn],nex[maxn];
ll ans;
int main()
{
    /*
    分析:上一次写的用了map
    效率比较低(主要是SPOJ不给开O2所以很慢233) 
    这次又想到一个貌似是O(n)的算法,再来改造一下。。。 

    思考发现,对于一个数肯定要找前驱和后继
    那么我们不妨换个思路,倒着来 
    对于一个初始的链表,我们在其中删除数 
    它原位置的前后指针会随之更新 
    那么在删除前它的前驱和后继肯定也是固定的
    我们只需要倒序删掉所有数,同时记下前驱后继就行了
    最后正着还是按depth[i]=max(depth[pre],depth[nex])+1算就行了 
    最终也是累加贡献
    由于每个元素在链表中只被删除一次,所以复杂度是O(n)的 
    */
    n=read();
    for(int i=1;i<=n;i++)//初始化链表 
        {
            opt[i]=read();
            L[i]=i-1,R[i]=i+1;
        }
    L[1]=0,R[n]=0,dep[0]=-1;
    for(int i=n;i>=1;i--)//逆序删除数字 
        {
            pre[opt[i]]=L[opt[i]];
            nex[opt[i]]=R[opt[i]];
            //记下每个删除的数的前驱和后继 
            R[L[opt[i]]]=R[opt[i]];
            L[R[opt[i]]]=L[opt[i]];
            //在链表中删除这个数 
        }
    for(int i=1;i<=n;i++)
        {
            dep[opt[i]]=max(dep[pre[opt[i]]],dep[nex[opt[i]]])+1;
            ans+=dep[opt[i]];//更新深度,累加贡献 
            printf("%lld\n",ans);
        }
    return 0;
}

code4:

#include<iostream>
#include<cstdio>
#include<cmath>
#define re register
using namespace std;//register吼啊 
typedef long long ll;
const int inf=1e9+7;
char IN[1<<17],*SS=IN,*TT=IN;
inline char gc(){return (SS==TT&&(TT=(SS=IN)+fread(IN,1,1<<17,stdin),SS==TT)?EOF:*SS++);}
inline int read()//fread一发入魂 
{
    int now=0,f=1;re char c=gc();
    for(;!isdigit(c);c=='-'&&(f=-1),c=gc());
    for(;isdigit(c);now=now*10+c-'0',c=gc());
    return now*f;}
const int maxn=300003;
int n,opt[maxn],L[maxn],R[maxn],dep[maxn],pre[maxn],nex[maxn];
ll ans;
int main()
{
    n=read();
    for(re int i=1;i<=n;i++)
        {
            opt[i]=read();
            L[i]=i-1,R[i]=i+1;
        }
    L[1]=0,R[n]=0,dep[0]=-1;
    for(re int i=n;i>=1;i--)
        {
            pre[opt[i]]=L[opt[i]];
            nex[opt[i]]=R[opt[i]];
            R[L[opt[i]]]=R[opt[i]];
            L[R[opt[i]]]=L[opt[i]];
        }
    for(re int i=1;i<=n;i++)
        {
            dep[opt[i]]=max(dep[pre[opt[i]]],dep[nex[opt[i]]])+1;
            ans+=dep[opt[i]];
            printf("%lld\n",ans);
        }
    return 0;
}

或许到这里这题就真的没什么了吧。。。我是真的想不出其他做法了233

最后来对比一下两种做法卡完常前后:

code2:620ms, 12288KB code4:200ms, 9932KB

果然。。。STL要慎用啊。。。真的慢到家了

希望有人把我的code4卡下去,至少到2018年9月28日还是luoguSPOJrk 1

最后,做完发现,这题的脑洞其实还是挺大的。。。希望大家能好好做

还有就是。。。不要让题目迷惑了双眼QAQ

还要注意算法效率的改进和提升!

最后,感谢你的阅读!

最后的最后。。。无耻的推一波我的blog:

https://www.luogu.org/blog/new2zy/

拜拜~~~