CF464E The Classic Problem(主席树+哈希+最短路)

CF464E The Classic Problem

problem

题目链接

solution

经典题。

本题很特别的是每条边的边权都是 2 2 2 的幂,而且数据读入的是指数。

所以可以将路径权值看作一个二进制串,每加一条边就是对应二进制位 + 1 +1 +1,当然会有相应的进位操作。

用一棵线段树维护一个二进制串。

考虑加边的操作。

  • 如果该位原先是 0 0 0 ,那么直接 + 1 +1 +1 ,即线段树的单点修改。

  • 如果该位原先是 1 1 1,那么就会导致进位问题,需要从当前位置开始找一段连续最长都为 1 1 1 的串,在第一个 0 0 0 的位置停下。然后将最终位置 + 1 +1 +1,中间连续一段全都清零。即线段树的区间修改和单点修改。

    从当前位置开始找一段连续的串,可以二分长度然后判断,但是这样线段树一次操作就是 O ( log ⁡ 2 n ) O(\log^2n) O(log2n) 的,还有最短路的部分呢?!

    所以我们在线段树上二分实现,降为 O ( log ⁡ n ) O(\log n) O(logn)

最短路就是堆优化的 djikstra \text{djikstra} djikstra

只不过比较两个字符串的大小,需要重载排序规则,所以实现是手打堆。

比较字符串大小肯定是哈希,所以线段树还要顺便维护一下串的哈希值,从高位开始比较,时间是 O ( log ⁡ n ) O(\log n) O(logn) 的。

最后是最短路的收缩,一个点可能更新多个点的最短路,但是多个点之间是独立的。

所以最短路径串不能改在原来的上面,每次修改都是产生一个新串。

因此我们采用主席树,动态开点。

总时间复杂度 : O ( n log ⁡ 2 n ) :O(n\log^2n) :O(nlog2n)

部分具体实现含义可见代码注释。

code

#include <bits/stdc++.h>
using namespace std;
#define ull unsigned long long //自然溢出
#define mod 1000000007
#define maxn 400005
#define MAX 100040 //2^1e5叠加还要进位 开1e5+5不够
struct node { int to, nxt, w; }E[maxn];
int S, T, n, m, cnt = 1;
ull base2[maxn];
int head[maxn], lst[maxn], root[maxn], base1[maxn];
bool vis[maxn];
stack < int > ans;

void addedge( int u, int v, int w ) {
    E[cnt] = { v, head[u], w };
    head[u] = cnt ++;
}

namespace sgt { 
/*
[0,X]就表示[0,X]的二进制位 进位是往右边进的 所以rson二进制级别高于lson
因为堆优化的dijkstra u点可能会导致若干个v点的最短路 而每一个v之间是独立的
所以得用主席树
一棵线段树其实本质是一个二进制串 主席树相当于是在改串
*/
    #define mid ( ( l + r ) >> 1 )
    #define maxm maxn * 20
    int cnt;
    int hash1[maxm], sum[maxm], lson[maxm], rson[maxm]; //sum[now]:区间中二进制位为1的个数
    ull hash2[maxm];

    void pushup( int now ) {
        sum[now] = sum[lson[now]] + sum[rson[now]];
        hash1[now] = ( hash1[lson[now]] + hash1[rson[now]] ) % mod;
        hash2[now] = hash2[lson[now]] + hash2[rson[now]];
    }

    int build( int x, int l = 0, int r = MAX ) {
    //初始化串上的每个位置都是数字x
        int now = ++ cnt;
        if( l == r ) { 
            sum[now] = x;
            hash1[now] = base1[l] * x;
            hash2[now] = base2[l] * x;
            return now;
        }
        lson[now] = build( x, l, mid );
        rson[now] = build( x, mid + 1, r );
        pushup( now );
        return now;
    }

    int query( int now, int l, int r, int L, int R ) {
        if( R < l or r < L ) return 0;
        if( L <= l and r <= R ) return sum[now];
        return query( lson[now], l, mid, L, R ) + query( rson[now], mid + 1, r, L, R );
    }

    int query( int now, int pos, int l = 0, int r = MAX ) {
    //在pos位+1 查询从pos开始的最长连续的1 即从pos开始的第一个0位置 对应的是区间查询
        if( l == r ) return l;
        if( pos > mid ) return query( rson[now], pos, mid + 1, r ); 
        if( query( lson[now], l, mid, pos, mid ) == mid - pos + 1 ) 
            return query( rson[now], mid + 1, mid + 1, r );
        //打成了 query(rson[now],pos,mid+1,r) 调半天哭了
        else
            return query( lson[now], pos, l, mid );
    }

    int modify( int lst, int pos, int l = 0, int r = MAX ) {
    //进位 前面的若干连续1导致了向pos进位(pos位一定为0)的结果 对应的是单点修改
        int now = ++ cnt;
        lson[now] = lson[lst], rson[now] = rson[lst];
        if( l == r ) { 
            sum[now] = 1;
            hash1[now] = base1[l];
            hash2[now] = base2[l];
            return now;
        }
        if( pos <= mid ) lson[now] = modify( lson[lst], pos, l, mid );
        else rson[now] = modify( rson[lst], pos, mid + 1, r );
        pushup( now );
        return now;
    }

    int modify( int x, int y, int L, int R, int l, int r ) {
    //巧妙运用root[0](最开始的线段树)全是0 所以在修改区间内就直接指向root[0] 指针即可
    //没必要做无谓的动态开点 浪费空间
        if( R < l or r < L ) return x;
        if( L <= l and r <= R ) return y;
        int now = ++ cnt;
        lson[now] = modify( lson[x], lson[y], L, R, l, mid );
        rson[now] = modify( rson[x], rson[y], L, R, mid + 1, r );
        pushup( now );
        return now;
    }

    int add( int rt, int w ) { //这条边权值为2^w 对应操作为在第w位加1
        int pos = query( rt, w ); //找到可能触发进位后的第一个为0位置
        int now = modify( rt, pos );
        if( pos == w ) return now; //没有触发进位 就在原本的位置+1 就不会有后面的清除
        else return modify( now, root[0], w, pos - 1, 0, MAX ); //一旦进位到pos意味着[w,pos)全都是1 需要将这一段清0
    }

    bool same( int x, int y ) { 
        return sum[x] == sum[y] and hash1[x] == hash1[y] and hash2[x] == hash2[y];
    }

    bool compare( int x, int y, int l = 0, int r = MAX ) { 
    //比较x这棵树代表的二进制串和y代表的二进制串的大小 优先从高位(右儿子)开始i比较
    //1代表x<=y 0代表x>y
        if( l == r ) return sum[x] <= sum[y];
        if( same( rson[x], rson[y] ) )
            return compare( lson[x], lson[y], l, mid );
        else
            return compare( rson[x], rson[y], mid + 1, r );
    }

}

struct heap { 

    int siz, cnt, root;
    int id[maxn], rt[maxn], dis[maxn], lson[maxn], rson[maxn];

    int merge( int x, int y ) {
        if( ! x or ! y ) return x + y;
        if( sgt :: compare( rt[y], rt[x] ) ) swap( x, y );
        rson[x] = merge( rson[x], y );
        if( dis[rson[x]] > dis[lson[x]] ) swap( lson[x], rson[x] ); //启发式合并
        dis[x] = dis[rson[x]] + 1;
        return x;
    }   

    void push( int x, int t ) { 
        siz ++, cnt ++;
        id[cnt] = x;
        rt[cnt] = t;
        root = merge( root, cnt ); 
    }

    void pop() { siz --; root = merge( lson[root], rson[root] ); }

    int top() { return id[root]; }

    bool empty() { return siz == 0; }

}q;

void dijkstra() {
    int ori = sgt :: build( 1 );
    for( int i = 1;i <= n;i ++ ) root[i] = ori;
    root[0] = root[S] = sgt :: build( 0 );
    q.push( S, root[S] );
    while( ! q.empty() ) {
        int u = q.top(); q.pop();
        if( vis[u] ) continue;
        vis[u] = 1;
        for( int i = head[u];i;i = E[i].nxt ) {
            int v = E[i].to, w = E[i].w;
            if( vis[v] ) continue;
            int New = sgt :: add( root[u], w );
            if( sgt :: compare( root[v], New ) ) continue;
            root[v] = New, lst[v] = u;
            q.push( v, root[v] );
        }
    }
    if( root[T] == ori ) { printf( "-1\n" ); return; }
    printf( "%d\n", sgt :: hash1[root[T]] ); //hash1即为最后结果 顺便可以起hash作用
    for( int i = T;i ^ lst[S];i = lst[i] ) ans.push( i );
    printf( "%d\n", (int)ans.size() );
    while( ! ans.empty() ) printf( "%d ", ans.top() ), ans.pop();
}

int main() {
    base1[0] = base2[0] = 1;
    for( int i = 1;i <= MAX;i ++ ) //单纯2^i最后答案hash比较很容易冲撞 再来一个自然溢出hash加强
        base1[i] = base1[i - 1] * 2 % mod, base2[i] = base2[i - 1] * 17;
    scanf( "%d %d", &n, &m );
    for( int i = 1, u, v, w;i <= m;i ++ ) {
        scanf( "%d %d %d", &u, &v, &w );
        addedge( u, v, w );
        addedge( v, u, w );
    }
    scanf( "%d %d", &S, &T );
    dijkstra();
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值