CF464E The Classic Problem
- problem
- solution
- code
problem
题目链接
solution
经典题。
本题很特别的是每条边的边权都是 222 的幂,而且数据读入的是指数。
所以可以将路径权值看作一个二进制串,每加一条边就是对应二进制位 +1+1+1,当然会有相应的进位操作。
用一棵线段树维护一个二进制串。
考虑加边的操作。
-
如果该位原先是 000 ,那么直接 +1+1+1 ,即线段树的单点修改。
-
如果该位原先是 111,那么就会导致进位问题,需要从当前位置开始找一段连续最长都为 111 的串,在第一个 000 的位置停下。然后将最终位置 +1+1+1,中间连续一段全都清零。即线段树的区间修改和单点修改。
从当前位置开始找一段连续的串,可以二分长度然后判断,但是这样线段树一次操作就是 O(log2n)O(\log^2n)O(log2n) 的,还有最短路的部分呢?!
所以我们在线段树上二分实现,降为 O(logn)O(\log n)O(logn)。
最短路就是堆优化的 djikstra\text{djikstra}djikstra。
只不过比较两个字符串的大小,需要重载排序规则,所以实现是手打堆。
比较字符串大小肯定是哈希,所以线段树还要顺便维护一下串的哈希值,从高位开始比较,时间是 O(logn)O(\log n)O(logn) 的。
最后是最短路的收缩,一个点可能更新多个点的最短路,但是多个点之间是独立的。
所以最短路径串不能改在原来的上面,每次修改都是产生一个新串。
因此我们采用主席树,动态开点。
总时间复杂度 :O(nlog2n):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 * 20int 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 ) {//初始化串上的每个位置都是数字xint 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) 调半天哭了elsereturn 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位加1int 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>yif( l == r ) return sum[x] <= sum[y];if( same( rson[x], rson[y] ) )return compare( lson[x], lson[y], l, mid );elsereturn 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;
}