洛谷P3201 序列启发式合并,对于启发式合并的一些更深理解

题意:

n 个布丁摆成一行,进行 m 次操作。每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色。

思路:
启发式合并

先学的树上启发式合并,对树上启发式合并的理解还停留在保留规模最大的最省时间这一观点上,但其实这并不全面,启发式合并的根本是在于合并两个集合时把小的往大的合并更省时间。在树上启发式合并,保留规模最大的重儿子 等价于在保留规模最大的重儿子后回到根节点暴力合并轻儿子,这里的轻儿子就是启发式合并的根本,小的往大的合并

这一题可以用 s e t set set记录 i i i颜色的所有节点。把一段往另一个集合里合并的影响应该是首尾是否相接,比如说现在加入的一段是 [ l , r ] [l,r] [l,r],如果合并到的集合里出现了 l − 1 l-1 l1,那么总段数 − 1 -1 1,同理对 r + 1 r+1 r+1也是如此,(我还在lowerbound判断是否有前驱后继,这样其实是更简单的)。有这个结论之后我们再来看这里的合并的段数影响, s e t 1 [ i ] set1[i] set1[i]储存的是所有 i i i颜色的,不止一段区间,但是对于一段区间 [ l , r ] [l,r] [l,r] [ l + 1 , r − 1 ] [l+1,r-1] [l+1,r1]的前驱后继显然是不可能出现在需要合并到 s e t 1 [ j ] set1[j] set1[j]的,因此, [ l + 1 , r − 1 ] [l+1,r-1] [l+1,r1]这一段在 s e t 1 [ j ] set1[j] set1[j]一定没有前驱后继,所以判断了也不会有影响,那么就直接 c h e c k   s e t 1 [ i ] check\ set1[i] check set1[i]的元素在 s e t 1 [ j ] set1[j] set1[j]的前驱后继状况就可以,起作用的一定是区间端点。

如果要合并 s e t 1 [ i ] set1[i] set1[i] s e t 1 [ j ] set1[j] set1[j],如果 s i z e i < s i z e j size_{i}<size_{j} sizei<sizej的,直接合并,满足了小合并到大的要求。如果 s i z e i > s i z e j size_{i}>size_{j} sizei>sizej,先 s w a p ( s e t 1 [ i ] , s e t 1 [ j ] ) swap(set1[i],set1[j]) swap(set1[i],set1[j]),在把 s e t 1 [ i ] set1[i] set1[i]合并到 s e t 1 [ j ] set1[j] set1[j]后面一定不要再 s w a p swap swap一次! 因为第一次swap就是先把 i i i自己大的给了 j j j,然后再把 j j j自己小的合并回去。最终两个集合都是到 s e t 1 [ j ] set1[j] set1[j]的。

复杂度证明:

更深一步理解了启发式合并,现在来证明一下时间复杂度,每一次都是小合并到大,设小集合和大集合的大小分别是 s i z e 1 , s i z e 2 size_{1},size_{2} size1size2,合并之后是 s i z e 1 + s i z e 2 > s i z e 1 + s i z e 1 = 2 ∗ s i z e 1 size_{1}+size_{2}>size_{1}+size_{1}=2*size_{1} size1+size2>size1+size1=2size1,即对小集合来说,合并完的集合大小至少翻倍,假设一开始最小的集合大小是1,那么最多翻 l o g 2 n log_{2}n log2n次倍,也就是最多合并 l o g 2 n log_{2}n log2n次,每一次最多合并 n n n个元素(达不到),因此,最差时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),理论上是跑不满的。

#include<bits/stdc++.h>
#define ll long long
using namespace std;

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

int n,m,tot;
set<int>set1[1000005];

int operator<(set<int>&x,set<int>&y){return (int)x.size()<(int)y.size();}

void solve(int x,int y)
{
    if(x==y) return;
    if(set1[y].empty()) swap(set1[x],set1[y]);
    else
    {
        if(set1[x]<set1[y])
        {
            for(auto i:set1[x]) tot-=set1[y].count(i-1)+set1[y].count(i+1);
            for(auto i:set1[x]) set1[y].insert(i);
            set1[x].clear();
        }
        else
        {
            swap(set1[x],set1[y]);
            for(auto i:set1[x]) tot-=set1[y].count(i-1)+set1[y].count(i+1);
            for(auto i:set1[x]) set1[y].insert(i);
            set1[x].clear();
//            swap(set1[x],set1[y]);
        }
    }
}

int main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)
    {
        int x=read();
        if(!set1[x].count(i-1)) tot++;
        set1[x].insert(i);
    }
    while(m--)
    {
        if(read()==1)
        {
            int x=read(),y=read();
            solve(x,y);
        }
        else printf("%d\n",tot);
    }
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值