【论题选编】稠密图最短路

有的最短路题目中,可能边的数目很大,朴素建图边的数目为 O ( V 2 ) O(|V|^2) 。这样很难直接应用 Dijkstra 算法等。

对付这种图,一般采用的策略是:

  1. 去除无用选项。有可能两点之间直接连接的路径长一定不是最短路径,这样就可以排除大多数路径,只构造那些有用的边。
  2. 构造等价选项。有可能两点之间的路径长完全等价于按照某种简单模式形成的路径长,这样就可以考虑这种简单的模式。
  3. 优化建图方式。例如建立虚拟点,以代替完全子图中各个点之间的相互连边。

例 1:最短路 1

题意:有 n n 个点, i i 号点和 j j 号点之间无向边的边权为 i xor j i \operatorname{xor} j ,求 1 1 号点到 n n 号点的最短路。(原题:HDU 6713

由于 i xor j xor j xor k = i xor k i xor j + j xor k i\operatorname{xor}j\operatorname{xor}j\operatorname{xor}k = i\operatorname{xor}k \le i \operatorname{xor} j + j\operatorname{xor}k ,因此直接从 i i k k 一定比 i i j j 再到 k k 更优。所以答案就是 1 xor n 1 \operatorname{xor} n

例 2:最短路

题意:给定一张边带权有向图,除了已有的边外,两个点之间还可以走异或边,权重为一个常数乘以两点编号的异或值。求指定两点的最短路。(原题:LOJ 6354

由于两个点之间的异或可以看作是一个点删掉某几位的 1、加上某几位的 1 得到,因此可以按位这种简单的模式建边。然后跑最短路即可。

priority_queue<pair<int, int> > pq;
int n, m, C, S, T;
int to[2200005], at[100005] = {0}, cnt = 0, nxt[2200005], w[2200005];
int dis[100005];
void init(){
    n = read(), m = read(), C = read();
    for (int i = 1; i <= m; ++i){
        int u = read(), v = read(), ww = read();
        w[++cnt] = ww;
        to[cnt] = v, nxt[cnt] = at[u], at[u] = cnt;
    }
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; j <<= 1)
            if ((i ^ j) <= n && (i ^ j) > 0)
                w[++cnt] = C * j,
                to[cnt] = (i ^ j), nxt[cnt] = at[i], at[i] = cnt;
    memset(dis, 0x3f, sizeof(dis));
    S = read(), T = read();
}
void solve(){
    dis[S] = 0;
    pq.push(make_pair(0, S));
    for (int i = 1; i <= n; ++i){
        while (!pq.empty()){
            if (dis[pq.top().second] < -pq.top().first)
                pq.pop();
            else break;
        }
        int minp = pq.top().second, dd = -pq.top().first;
        pq.pop();
        for (int i = at[minp]; i; i = nxt[i]){
            int v = to[i];
            int ww = min(w[i], C * (minp ^ v));
            if (dis[v] > dd + ww)
                dis[v] = dd + ww, pq.push(make_pair(-dis[v], v));
        }
    }
    printf("%d\n", dis[T]);
}

例 3:The Captain

题意:给定 n n 个平面直角坐标系上的点,两个点之间的距离定义为横坐标之差的绝对值、纵坐标之差的绝对值,二者取较小,即 min ( x 1 x 2 , y 1 y 2 ) \min(| x_1-x_2|, |y_1-y_2|) 。求 1 1 号点到 n n 号点的最短路。(原题:BZOJ 4152)

一般这种题都要考虑点和(在某种量度下)周围点之间的关系,比方说只和周围的点建边。

事实上确实如此。我们只需要将一个点和在横坐标意义上离得最近的 2 个点以及纵坐标意义上离得最近的 2 个点连边即可。原因在于:设当前点为 i i ,对于其他点 j j ,直接从 i i j j 不会比从 i i 到这 4 个邻居中的一个再到 j j 更短。

对横坐标来说,前者是一个 x i x j |x_i-x_j| 的形式,后者是 x i x k + min ( x k x j , y k y j ) |x_i - x_k| + \min(|x_k - x_j|, |y_k - y_j|) 的形式, k k 是邻居之一。显然后者的优化空间更大。注意,这里可以通过选取恰当的邻居使得绝对值不等式取等号。

然后跑堆优化 Dijkstra 即可。

例 4:牛妹游历城市

题意:给定 n n 个点,第 i i 个点有点权 a i a_i 。如果对于 i , j i, j a i and a j a_i \operatorname{and} a_j 不为 0 0 ,那么 i , j i, j 间有无向边,边权为 lowbit ( a i and a j ) \operatorname{lowbit}(a_i \operatorname{and} a_j) 。问从 1 1 n n 的最短路。(原题:牛客练习赛 67 E

方法一

我们可以依次考虑每一个位,然后把点权有相同位的点互相连一条边。但这样边的数目可能是 O ( n 2 ) O(n^2)

更好的做法是对当前位建立一个虚拟点,设为 u u 。遍历所有点,如果 i i 的点权满足这一位上为 1 1 ,那么连一条从 i i u u 的边,边权为这一位对应的二进制数;再连一条从 u u i i 的边,边权为 0 0 。这样边数就下降到了至多 64 n 64n 。跑 Dijkstra 可以通过。

int n;
unsigned a[100005];
ll dis[100005 + 50];
priority_queue<pair<int, ll>, vector<pair<int, ll> >, 
    greater<pair<int, ll> > > pq;
int to[6400005], nxt[6400006], at[100005 + 50], cnt;
unsigned w[6400005];
void init(){
    n = read();
    for (int i = 1; i <= n; ++i) 
        scanf("%u", &a[i]);
    // build graph
    memset(at, 0, sizeof(at));
    cnt = 0;
    int nn = n;
    for (unsigned t = 1; t > 0; t <<= 1){
        ++n;
        for (int i = 1; i <= nn; ++i) {
            if (a[i] & t){
                to[++cnt] = n, nxt[cnt] = at[i], w[cnt] = t, at[i] = cnt;
                to[++cnt] = i, nxt[cnt] = at[n], w[cnt] = 0, at[n] = cnt;
            }
        }
    }
}
void solve(){
    memset(dis, 0x3f, sizeof(dis));
    ll lim = dis[1];
    dis[1] = 0;
    pq.push(make_pair(1, 0));
    for (; ; ){
        while (!pq.empty()){
            if (pq.top().second > dis[pq.top().first])
                pq.pop();
            else break;
        }
        if (pq.empty()) break;
        int h = pq.top().first;
        ll dd = pq.top().second;
        pq.pop();
        for (int i = at[h]; i; i = nxt[i]){
            if (dd + w[i] < dis[to[i]])
                dis[to[i]] = dd + w[i], 
                pq.push(make_pair(to[i], dis[to[i]]));
        }
    }

    if (dis[n - 32] == lim){
        printf("Impossible\n");
    }else printf("%lld\n", dis[n - 32]);
}

方法二(std)

本题标程给的做法是:先设定一个阈值 B = 256 B = 256 ,将每一个 a i a_i 拆成多个长为 log B \log B 的位域,试图将每条边分到不同的位域中。

对于每一个位域,我们构建 2 B 2B 个虚拟点,分为 B B 个“起点”和 B B 个“终点”。我们枚举 0 u , v < B 0 \le u, v < B ,如果 u and v 0 u \operatorname{and} v \neq 0 ,那么每一个位域内,都连一条边,从编号为 u u 的“起点”到编号为 v v 的“终点”,边权为 lowbit ( u and v ) × 2 r \operatorname{lowbit}(u \operatorname{and} v) \times 2^r ,其中 2 r 2^r 表示这个位域相对于最低位的偏移量。这部分建立的边数至多为 B 2 log B B^2 \log B

然后考虑每一个原图的点 i i ,对于 a i a_i 的每一个位域,将 i i 连向该位域的“起点”,而将该位域的“终点”连向 i i

这么说非常抽象,可以参考出题人给的题解

猜你喜欢

转载自blog.csdn.net/zqy1018/article/details/108161824