BZOJ-2049 2049: [Sdoi2008]Cave 洞穴勘测 LCT||并查集||可撤销并查集+时间分治

9 篇文章 0 订阅
2 篇文章 0 订阅

大家都很强, 可与之共勉 。

辉辉热衷于洞穴勘测。某天,他按照地图来到了一片被标记为JSZX的洞穴群地区。经过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,并且每条通道连接了恰好两个洞穴。假如两个洞穴可以通过一条或者多条通道按一定顺序连接起来,那么这两个洞穴就是连通的,按顺序连接在一起的这些通道则被称之为这两个洞穴之间的一条路径。洞穴都十分坚固无法破坏,然而通道不太稳定,时常因为外界影响而发生改变,比如,根据有关仪器的监测结果,123号洞穴和127号洞穴之间有时会出现一条通道,有时这条通道又会因为某种稀奇古怪的原因被毁。辉辉有一台监测仪器可以实时将通道的每一次改变状况在辉辉手边的终端机上显示:如果监测到洞穴u和洞穴v之间出现了一条通道,终端机上会显示一条指令 Connect u v 如果监测到洞穴u和洞穴v之间的通道被毁,终端机上会显示一条指令 Destroy u v 经过长期的艰苦卓绝的手工推算,辉辉发现一个奇怪的现象:无论通道怎么改变,任意时刻任意两个洞穴之间至多只有一条路径。因而,辉辉坚信这是由于某种本质规律的支配导致的。因而,辉辉更加夜以继日地坚守在终端机之前,试图通过通道的改变情况来研究这条本质规律。然而,终于有一天,辉辉在堆积成山的演算纸中崩溃了……他把终端机往地面一砸(终端机也足够坚固无法破坏),转而求助于你,说道:“你老兄把这程序写写吧”。辉辉希望能随时通过终端机发出指令 Query u v,向监测仪询问此时洞穴u和洞穴v是否连通。现在你要为他编写程序回答每一次询问。已知在第一条指令显示之前,JSZX洞穴群中没有任何通道存在。

Input

第一行为两个正整数n和m,分别表示洞穴的个数和终端机上出现过的指令的个数。以下m行,依次表示终端机上出现的各条指令。每行开头是一个表示指令种类的字符串s(”Connect”、”Destroy”或者”Query”,区分大小写),之后有两个整数u和v (1≤u, v≤n且u≠v) 分别表示两个洞穴的编号。

Output

对每个Query指令,输出洞穴u和洞穴v是否互相连通:是输出”Yes”,否则输出”No”。(不含双引号)

题解1 : LCT

裸的LCT。

/**************************************************************
    Problem: 2049
    User: Lazer2001
    Language: C++
    Result: Accepted
    Time:968 ms
    Memory:2064 kb
****************************************************************/

# include <bits/stdc++.h>

char buf [1 << 16], *ss, *tt ;

inline char pick ( )  {
    return ( ss == tt ) ? ( tt = buf + fread ( ss = buf, 1, 1 << 16, stdin ), *ss ++ ) : * ss ++ ;
}

inline void readIn ( int& x )  {
    register char c ;
    bool opt = 1 ;
    while ( ! isdigit ( c = pick ( ) ) && ( c ^ 45 ) ) ;
    if ( c == 45 )  {
        opt = 0 ; c = pick ( ) ;
    }
    for ( x = -48 + c ; isdigit ( c = pick ( ) ) ; x = x * 10 + c - 48 ) ;
    if ( ! opt )    x = -x ;
}

inline int readStr ( char* ss )  {
    int len ( 0 ) ;
    register char c ; 
    while ( ! isalpha ( c = pick ( ) ) ) ;
    for ( ss [len ++] = c ; ! isspace ( c = pick ( ) ) ; ss [len ++] = c ) ;
    return len ;    
}

const int N = 30005 ;

struct node  {
    node *ch [2], *fa ;
    bool rev ;
    void update ( ) ;
    void pushdown ( ) ;
} pool [N], *null, *tail = pool ;

void node :: pushdown ( )  {
    if ( this == null ) return ;
    if ( rev )  {
        ch [0] -> rev ^= 1 ;
        ch [1] -> rev ^= 1 ;
        std :: swap ( ch [0], ch [1] ) ;
        rev = 0 ;
    }
}

node *root [N], *st [N] ;

struct Link_Cut_Tree  {

    void Init ( int n )  {
        for ( int i = 1 ; i <= n ; ++ i )  {
            root [i] = ++ tail ;
            root [i] -> fa = root [i] -> ch [0] = root [i] -> ch [1] = null ;
            root [i] -> rev = 0 ;
        }
    }
    bool isroot ( node*& p )  {
        return ( p -> fa == null ) || ( p -> fa -> ch [0] != p && p -> fa -> ch [1] != p ) ;
    }
    bool isrs ( node*& p )  {
        return p == p -> fa -> ch [1] ;
    }
    void rotate ( node* p )  {
        if ( isroot ( p ) || p == null )    return ;
        int d = isrs ( p ) ;
        node* par = p -> fa ;
        par -> ch [d] = p -> ch [! d] ;
        if ( p -> ch [! d] != null )    p -> ch [! d] -> fa = par ;
        if ( ! isroot ( par ) )  par -> fa -> ch [isrs ( par )] = p ;
        p -> fa = par -> fa ;
        p -> ch [! d] = par ;
        par -> fa = p ;
    } 
    void splay ( node* p )  {
        if ( p == null )    return ;
        int tp ;
        st [tp = 1] = p ;
        for ( node* t = p ; ! isroot ( t ) ; t = t -> fa )
            st [++ tp] = t -> fa ;
        while ( tp )    st [tp --] -> pushdown ( ) ;
        for (  ; ! isroot ( p ) ; rotate ( p ) )
            if ( ! isroot ( p -> fa ) )  rotate ( ( isrs ( p ) == isrs ( p -> fa ) ) ? p -> fa : p ) ;
    } 
    void access ( node* p )  {
        for ( node* pre = null ; p != null ; pre = p, p = p -> fa )
            splay ( p ) , p -> ch [1] = pre ;
    }
    void makeroot ( node* p )  {
        access ( p ) ;  splay ( p ) ;  p -> rev ^= 1 ;
    }
    void link ( node* p, node* q )  {
        makeroot ( p ) ;  p -> fa = q ;
    }
    void cut ( node* p, node* q )  {
        makeroot ( p ) ;    access ( q ) ;   splay ( q ) ;
        p -> fa = q -> ch [0] = null ;
    }
    bool isLink ( node* p, node* q )  {
        while ( p -> fa != null )    p = p -> fa ;
        while ( q -> fa != null )    q = q -> fa ;
        return p == q ; 
    }
} Lct ;

int n, m ;

void Init ( )  {
    null = pool ;
    null -> ch [0] = null -> ch [1] = null -> fa = null ;
    Lct.Init ( n ) ;    
}

int main ( )  {
    readIn ( n ) ; readIn ( m ) ;
    Init ( ) ;
    while ( m -- )  {
        char ss [10] ;
        int u, v ;
        readStr ( ss ) ; readIn ( u ) ; readIn ( v ) ;
        switch ( *ss )  {
            case 'C' : Lct.link ( root [u], root [v] ) ;  break ;
            case 'Q' : puts ( Lct.isLink ( root [u], root [v] ) ? "Yes" : "No" ) ;   break ;
            case 'D' : Lct.cut ( root [u], root [v] ) ; break ;
        }
    }
}

题解2 : 并查集

link u v 将u提根,然后设置u的父亲为v

cut u v 将u提根,然后将v的父亲设为0(反向了所以是设v的父亲而不是u的父亲)

query u v u提根后直接暴力并查集查找v的父亲是不是u

提根: 将u的父亲及其边全部反向。

/**************************************************************
    Problem: 2049
    User: Lazer2001
    Language: C++
    Result: Accepted
    Time:356 ms
    Memory:924 kb
****************************************************************/

# include <cctype>
# include <cstdio>

static const size_t Size = 1 << 16 | 1 ;
static char buf [Size], *ss, *tt ;

# define pick( ) ( ( ss == tt ) ? ( tt = buf + fread ( ss = buf, 1, Size, stdin ), *ss ++ ) : *ss ++ )

inline int read ( )  {
    register int x, c ;
    while ( ( c = pick ( ) ) > '9' || c < '0' ) ;
    for ( x = -48 + c ; isdigit ( c = pick ( ) ) ; x = x * 10 + c - 48 ) ;
    return x ;
}

int n, m, u, v, f [10005] ;

inline void sroot( int x )  { for ( register int c = 0, fa = f [x] ; x ; fa = f [x] ) { f [x] = c, c = x, x = fa ; } }

int main ( )  {
    n = read ( ), m = read ( ) ;
    for ( register int i = 0, u, v, c ; i < m ; ++ i )  {
        while ( ( c = pick ( ) ) > 'Z' || c < 'A' ) ;
        u = read ( ), v = read ( ) ;
        sroot ( u ) ;
        if ( c == 'C' ) {
            f [u] = v ;
            continue ;
        }
        if ( c == 'D' )  {
            f [v] = 0 ;
            continue ;
        }
        while ( v != u && v ) v = f [v] ;
        puts ( v == u ? "Yes" : "No" ) ;
    }
}

题解3 :可撤销并查集+时间分治

时间分治的话就是以操作序号为时间,对应对一颗线段树上去,然后在线段树上分治找到操作时间对应的节点,进行操作。
对于一条边,它存活的时间就是【加边的时间,删边的时间】这样一个区间,然后我们这条边push到线段树上去,分散的存在各个节点,注意不能push_up,这个过程其实就是一个不push_up也不push_down的线段树区间更新,而push_up不进行的原因就是线段树节点只能存放在该节点对应整个时间内存在的边,就是把一段时间分散到不同的时间段里去了,不push_down的原因是让防止边重复出现,也没有必要。这样的话由于不会一直跑到线段树底部,最多是log(4*m)的复杂度。所以这个插入操作的时间复杂度最多是m*log(4*m)。

然后就是对查询进行处理了。
这时候需要对线段树dfs一下,递归到底层,也就是区间就是一个点x的时候,从这个点一直到根节点所经历的所有区间的边,都是在这个时间x存在的边了,因为x都且只包含在这些区间里,注意区间对应时间。所以假如我们根节点到当前节点存储的所有的边都用并查集连接的话,那么当前时间的询问(如果是询问的话)边是否连通就很好做了。这个时候就需要一个带撤销的并查集了,并查集需要是按秩合并的并查集,路径压缩的并查集我么是没有办法分裂的,这样的话不可撤销,如果没有优化的并查集的话,就可能超时,按秩合并即可以分裂,有保证了logn的复杂度,撤销的具体操作就看代码吧。

然后我们dfs的时候,进入一个节点就先将这个时间存在的边都先merg,每次退出节点前再讲原来的merg撤销,这样就做到了每次到叶子节点都维护了一个从根节点到当前节点对应的边所组成的一个并查集。然后就做完了。

线段树空间要开4倍

/**************************************************************
    Problem: 2049
    User: Lazer2001
    Language: C++
    Result: Accepted
    Time:2544 ms
    Memory:27352 kb
****************************************************************/

# include <bits/stdc++.h>

static const size_t Size = 1 << 16 | 1 ;
static char buf [Size], *ss, *tt ;
# define pick( ) ( ( ss == tt ) ? ( tt = buf + fread ( ss = buf, 1, Size, stdin ), *ss ++ ) : *ss ++ )

inline int read ( )  {
    register int x, c ;
    while ( ( c = pick ( ) ) > '9' || c < '0' ) ;
    for ( x = -48 + c ; isdigit ( c = pick ( ) ) ; x = x * 10 + c - 48 ) ;
    return x ;
}

const int M = 200010 ;

std :: map < std :: pair < int, int >, int > map ;
std :: vector < std :: pair < int, int > > v [M << 2] ;

struct edge  {  int fr, to ;    } eg [M] ;

bool ans [M], isq [M] ;

class  UFS  {
private :
    int *fa, *rank ;
    std :: stack < std :: pair < int*, int > > stk ;    
public :
    UFS ( ) {   }
    UFS ( int n )  {
        fa = new int [( const int ) n + 1] ;
        rank = new int [( const int ) n + 1] ;
        memset ( rank, 0, sizeof rank ) ;   
        for ( int i = 1 ; i <= n ; ++ i )    fa [i] = i ;
    }
    inline int find ( int x )  {
        while ( x ^ fa [x] )    x = fa [x] ;
        return x ;
    }
    inline int Join ( int x, int y )  {
        x = find ( x ), y = find ( y ) ;
        if ( x == y )   return 0 ;
        if ( rank [x] <= rank [y] )  {
            stk.push ( std :: make_pair ( fa + x, fa [x] ) ) ;
            fa [x] = y ;
            if ( rank [x] == rank [y] )  {
                stk.push ( std :: make_pair ( rank + y, rank [y] ) ) ;
                ++ rank [y] ;
                return 2 ;
            }
            return 1 ;
        }
        stk.push ( std :: make_pair ( fa + y, fa [y] ) ) ;
        return fa [y] = x, 1 ;
    }
    inline void Undo ( )  {
        *stk.top ( ).first = stk.top ( ).second ;
        stk.pop ( ) ;
    }
} T ;

inline void Insert ( int o, int lf, int rg, const int& L, const int& R, const std :: pair < int, int >& t )  {
    if ( L <= lf && rg <= R )  {
        return ( void ) v [o].push_back ( t ) ;
    }
    int mid = ( lf + rg ) >> 1 ;
    if ( L <= mid )  Insert ( o << 1, lf, mid, L, R, t ) ;
    if ( R > mid )   Insert ( o << 1 | 1, mid + 1, rg, L, R, t ) ;
}

inline void Solve ( int o, int lf, int rg )  {
    int cnt ( 0 ) ;
    for ( std :: vector < std :: pair < int, int > > :: iterator it = v [o].begin ( ), end = v [o].end ( ) ; it != end ; ++ it )  {
        cnt += T.Join ( it -> first, it -> second ) ;
    }
    if ( lf == rg )  {
        if ( isq [lf] )  {
            ans [lf] = ( T.find ( eg [lf].fr ) == T.find ( eg [lf].to ) ) ;
        }
        while ( cnt -- )    T.Undo ( ) ;
        return ;
    }
    int mid = ( lf + rg ) >> 1 ;
    Solve ( o << 1, lf, mid ) ;   Solve ( o << 1 | 1, mid + 1, rg ) ;
    while ( cnt -- )    T.Undo ( ) ;
}

int main ( )  {
    int n = read ( ), m = read ( ) ;
    T = UFS ( n ) ;
    for ( register int i = 1, u, v, c ; i <= m ; ++ i )  {
        while ( ( c = pick ( ) ) > 'Z' || c < 'A' ) ;
        u = read ( ), v = read ( ) ;
        if ( u > v ) u ^= v^= u ^= v ;   // !!!!!!!!
        eg [i] = ( edge ) { u, v } ;
        switch ( c )  {
            case 'C' :  {
                map [std :: make_pair ( u, v )] = i ;
                break ;
            }
            case 'D' :  {
                std :: pair < int, int > t = std :: make_pair ( u, v ) ;
                Insert ( 1, 1, m, map [t], i, t ) ;
                map.erase ( t ) ;
                break ;
            }
            case 'Q' :  {
                isq [i] = 1 ;
                break ;
            }
        }
    }
    for ( std :: map < std :: pair < int, int >, int > :: iterator it = map.begin ( ), end = map.end ( ) ; it != end ; ++ it )  {
        Insert ( 1, 1, m, it -> second, m, it -> first ) ;
    }
    Solve ( 1, 1, m ) ;
    for ( int i = 1 ; i <= m ; ++ i )  {
        if ( isq [i] )  {
            puts ( ans [i] ? "Yes" : "No" ) ;
        }
    }
}

各有好处

时间上并查集 < LCT < 可撤销并查集+时间分治

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值