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;
}