天下第一 txdy (LCT+双指针+线段树)

这篇博客探讨了一种图论问题,即如何找出一个无向图中满足特定条件的链状子图。通过双指针法和动态树LCT(Link-Cut Tree)算法,解决了一个区间内点数减边数等于1且无环的子图计数问题。文章详细解释了算法思路并提供了C++代码实现。
摘要由CSDN通过智能技术生成

天下第一 txdy

description

djq_cpp 是天下第一的。
djq_cpp 给了你一个 n 个点 m 条边的无向图(无重边自环),点标号为 1 ∼n。祂想要考考你,
有多少对整数对 (l, r) 满足:
• 1 ≤l ≤r ≤n
• 如果把区间 [l, r] 内的点以及它们之间的边保留下来,其他的点和边都扔掉,那么留下来的这张
图恰好是一条链。
注:链必须是连通的图。特别地,一个点的图也是链。

Input
第一行两个非负整数 n, m。
接下来 m 行每行两个正整数 u, v(1 ≤u, v ≤n, u ̸= v),表示 u, v 之间有一条边。
保证图无重边自环。

Output
输出一个整数表示答案。

Sample
Input
3 3
1 2
2 3
3 1
Output
5
Explanation
(1, 1), (1, 2), (2, 2), (2, 3), (3, 3) 是符合条件的整数对。

Constraint
对于 10% 的数据,n, m ≤100。
对于 20% 的数据,n, m ≤1000。
对于 60% 的数据,n, m ≤50000。
对于另外 10% 的数据,保证每个点度数 ≤2。
对于 100% 的数据,1 ≤n ≤250000, 0 ≤m ≤250000。

solution

首先清楚为链的必要条件

  • 点数减边数 = 1 =1 =1
  • 无环
  • 每个点度数 ≤ 2 \le 2 2

正解就是寻找辅助工具来判断点 [ l , r ] [l,r] [l,r]内的所有边是否满足上面所有条件

实际上,三个条件可以分开保证后再合并

双指针法

考虑枚举点区间的右端点 r r r

然后找到满足 [ l , r ] [l,r] [l,r]内每个点度数都 ≤ 2 \le 2 2的最小的 l l l,记为 f [ r ] f[r] f[r]

显然随着 r r r的右移, l l l只会变大不会变小

  • 具体而言,利用度数 d i d_i di,每次右端点 + 1 +1 +1后,加入新右端点连接的所有在 [ l , r + 1 ] [l,r+1] [l,r+1]的边,并判断边连接两点是否度数超过 2 2 2,如果超过就选择右移左端点,断掉原来左端点的所有已连边,直到度数不超过 2 2 2才停止左端点右移

同理,双指针法

考虑枚举右端点 r r r

然后找到满足 [ l , r ] [l,r] [l,r]内所有边加入后无环的最小的 l l l,记为 g [ r ] g[r] g[r]

显然随着 r r r的右移, l l l只会变大不会变小

  • 具体而言,与维护度数本质上是完全一样的,但是这里涉及到了边的动态变化,那就不得不使用动态树 LCT \text{LCT} LCT了,判断新右端点的边连接的两个点原本已经连接,这条边加入就会形成环,右移左端点,断掉原来左端点的所有已连边。这些完完全全就是 LCT \text{LCT} LCT的模板专场了

为了满足以上两个条件,则真正的左端点是 l = max ⁡ ( g [ r ] , f [ r ] ) l=\max(g[r],f[r]) l=max(g[r],f[r])

最后就是必须是同一条链的连通性问题

要求点数减边数 = 1 =1 =1,就可以用线段树维护点数减边数最小值,再记录最小值的个数

  • 具体而言,对于每一个右端点 r r r,线段树叶子结点 l l l表示的意思是 [ l , r ] [l,r] [l,r]区间内所有边都加入后,点数减边数的最小值。写法上,是将线段树与无环的判断放在一起写的

考场上硬刚LCT的人真的是强者

code

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define maxn 250005
int n, m, top, sta[maxn], pos[maxn], deg[maxn];
vector < int > G[maxn];

namespace LCT {
    struct node { int son[2], fa, tag; }t[maxn];
    bool root( int x ) { return t[t[x].fa].son[0] ^ x and t[t[x].fa].son[1] ^ x; }
    void reverse( int x ) { swap( t[x].son[0], t[x].son[1] ); t[x].tag ^= 1; }
    void pushdown( int x ) {
        if( ! t[x].tag ) return;
        if( t[x].son[0] ) reverse( t[x].son[0] );
        if( t[x].son[1] ) reverse( t[x].son[1] );
        t[x].tag ^= 1;
    }
    void rotate( int x ) {
        int fa = t[x].fa;
        int Gfa = t[fa].fa;
        int d = t[fa].son[1] == x;
        if( ! root( fa ) ) t[Gfa].son[t[Gfa].son[1] == fa] = x;
        t[x].fa = Gfa;
        if( t[x].son[d ^ 1] ) t[t[x].son[d ^ 1]].fa = fa;
        t[fa].son[d] = t[x].son[d ^ 1];
        t[x].son[d ^ 1] = fa;
        t[fa].fa = x;
    }
    void splay( int x ) {
        sta[++ top] = x; int y = x;
        while( ! root( y ) ) sta[++ top] = y = t[y].fa;
        while( top ) pushdown( sta[top --] );
        while( ! root( x ) ) {
            int fa = t[x].fa, Gfa = t[fa].fa;
            if( ! root( fa ) ) (t[Gfa].son[0] == fa) ^ (t[fa].son[0] == x) ? rotate( x ) : rotate( fa );
            rotate( x ); 
        }
    }
    void access( int x ) { for( int son = 0;x;son = x, x = t[x].fa ) splay( x ), t[x].son[1] = son; }
    void makeroot( int x ) { access( x ); splay( x ); reverse( x ); }
    void split( int x, int y ) { makeroot( x ); access( y ); splay( y ); }
    void link( int x, int y ) { makeroot( x ); t[x].fa = y; }
    void cut( int x, int y ) { split( x, y ); t[x].fa = t[y].son[0] = 0; }
    int findroot( int x ) { access( x ); splay( x ); while( t[x].son[0] ) pushdown( x ), x = t[x].son[0]; splay( x ); return x; }
    bool check( int x, int y ) { makeroot( x ); return findroot( y ) == x; }
}

struct node { int ans, cnt, tag; }t[maxn << 2];
namespace SGT {
    #define lson now << 1
    #define rson now << 1 | 1
    #define mid  (l + r >> 1)
    node operator + ( node x, node y ) {
        if( x.ans < y.ans ) return x;
        else if( x.ans > y.ans ) return y;
        else { x.cnt += y.cnt; return x; }
    }
    void pushdown( int now ) {
        if( ! t[now].tag ) return;
        t[lson].ans += t[now].tag;
        t[lson].tag += t[now].tag;
        t[rson].ans += t[now].tag;
        t[rson].tag += t[now].tag;
        t[now].tag = 0;
    }
    void build( int now, int l, int r ) {
        t[now] = { 0, 1, 0 };
        if( l == r ) return;
        build( lson, l, mid );
        build( rson, mid + 1, r );
        t[now] = t[lson] + t[rson];
    }
    void modify( int now, int l, int r, int L, int R, int v ) {
        if( R < l or r < L ) return;
        if( L <= l and r <= R ) { t[now].ans += v; t[now].tag += v; return; }
        pushdown( now );
        modify( lson, l, mid, L, R, v );
        modify( rson, mid + 1, r, L, R, v );
        t[now] = t[lson] + t[rson]; t[now].tag = 0; //因为重载的写法问题 t[now]=t[lson]/t[rson] 会把儿子的懒标记也赋过来 但实际上now这里是不该存在懒标记的
    }
    node query( int now, int l, int r, int L, int R ) { 
        if( r < L or R < l ) return { inf, 0, 0 };
        if( L <= l and r <= R ) return t[now];
        pushdown( now );
        return query( lson, l, mid, L, R ) + query( rson, mid + 1, r, L, R );
    }
}

int main() {
    scanf( "%d %d", &n, &m );
    for( int i = 1, u, v;i <= m;i ++ ) {
        scanf( "%d %d", &u, &v );
        G[u].push_back( v );
        G[v].push_back( u );
    }
    for( int r = 1, l = 1;r <= n;r ++ ) {//枚举位置r作为右端点 
		//双指针l求出最远的满足度数<=2的条件
        sort( G[r].begin(), G[r].end() );
        for( int i : G[r] ) { //把每条r相连的属于[l,r]的边加进来 
            if( i > r ) break;
            while( l <= i and ( deg[i] == 2 or deg[r] == 2 ) ) {
				//在加这条边之前 两点度数就已经等于2 加了过后肯定不满足度数<=2的条件
				//这个时候说明l左端点应当右移
				//知道两点度数都<2为止 
                for( int j : G[l] )
                    if( l < j and ( j < r or ( j == r and l < i ) ) )
                        deg[l] --, deg[j] --;
                l ++;
            }
            if( l <= i ) deg[i] ++, deg[r] ++;//这条边还在调整后新区间内[l',r]内才真的加入
        }
        pos[r] = l;
    }
    SGT :: build( 1, 1, n );
    long long ans = 0;
    for( int r = 1, l = 1;r <= n;r ++ ) {
        SGT :: modify( 1, 1, n, 1, r, 1 );
        for( int i : G[r] ) {
            if( i > r ) break;
            while( l <= i and LCT :: check( i, r ) ) {
                for( int j : G[l] )
                    if( l < j and ( j < r or ( j == r and l < i ) ) )
                        LCT :: cut( l, j );
                    /*
					不能写成if(l<j and j<= r) 上面同理
					因为新加一条边是i-r
					有可能i就恰好是l
					发现i和r已经联通就必须去除掉l连接的边
					l里面就会访问到l-r这条边 但是这里还没有加
					错误写法就会删去一条根本没加过的边导致错误 
					*/
                l ++;
            }
            if( l <= i ) LCT :: link( i, r );
            SGT :: modify( 1, 1, n, 1, i, -1 );
        }
        pos[r] = max( pos[r], l );
        node now = SGT :: query( 1, 1, n, pos[r], r );
        if( now.ans == 1 ) ans += now.cnt;
    }
    printf( "%lld\n", ans );
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值