[luogu p6628] [省选联考 2020 B 卷] 丁香之路

\(\mathtt{Link}\)

P6628 [省选联考 2020 B 卷] 丁香之路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

\(\mathtt{Description}\)

给定一个 \(n\) 个顶点(编号 \(1 \ldots n\))构成的完全无向图,边 \((u ,v)\) 的边权为 \(| u - v|\),其中给定 \(m\) 条边,构成边集 \(S\),问从点 \(s\) 的单源最短路径,并且路径必须包含 \(S\).

\(\mathtt{Data} \text{ } \mathtt{Range} \text{ } \mathtt{\&} \text{ } \mathtt{Restrictions}\)

  • \(1 \le s \le n \le 2500\)

  • \(0 \le m \le \dfrac{n(n - 1)}{2}\)

\(\mathtt{Solution}\)

丁香是哈尔滨的市花,因此给我做这个题加了顺畅buff(?

转化问题

首先观察到题目中给出的是无向完全图,也就是任意两对点之间有一条无向边。考虑转化一下问题:

可重边集 \(S\) 进行添加边的操作,使边集 \(S\) 构成一条从 \(s\) 通向 \(i\) 的欧拉路径。可以有很多种加边方案,最后求的是加边之后 \(S\) 中边权和的最小值。

那么你可能就会问了,为什么是欧拉路径啊?欧拉路径明明是仅通过一次,你这个道不是随便走吗。其实我们思考一下,欧拉路径的算法在重边环境下也可以跑,一个道路你走三遍,那边集 \(S\) 里有三个这个道路就行了。因此 \(S\) 要求可重。

转化问题之后,我们观察到欧拉路径的性质:起点和终点是奇点,其他的点都是偶点,并且连通。

有奇有偶,处理不方便,因此我们可以do一个小trick:

建一条从 \(i \rightarrow s\) 的虚边。

于是,\(s\)\(i\) 的欧拉路径可以转化为 \(s\)\(i\) 的欧拉回路。欧拉回路的性质:所有点都是偶点,并且连通。那么我们干掉所有奇点,处理一下连通性就好了,当然还需要注意维护边权和最小。

处理奇点

其实对于一张图,奇点只能是偶数个(不在这里证明了),我们将奇点两两配对连边,就可以解决问题。

但是注意维护边权和最小,怎么做呢?

观察到题目中的边权比较特殊,是 \(|u - v|\) 的形式,因此为了让连上的边权和最小,我们将奇点按照编号排序……举个例子吧。

现在我们有 \(7\) 个点 \(n = 7\),其中第2,3,5,7号点是奇点。

为了让所连边权和最小,很显然是从小到大两两配对连,也就是2和3连,5和7连。很容易证明出来这样连一定边权和最小(你可以想一下2和5连,3和7连,边权和显然会更大,其他的也一样)

但是在这里我们再次注意,虽然我们是处理奇点,但最后图还需要连通,为了连通我们应该尽量多找几个点连在一起,也就是尽可能多加边。现在我们想一下这样一个事:

5和6连,6和7连,边权和是几?2,5和7连边权和是几?也是2,边权和是一样的。然而,5 - 6 - 7 这样的连法就多带了个 6 进来,图就更容易连通了。

因此我们应该是从小到大两两配对,对于一对奇点 \((u, v)\),我们应该连上\((u, u + 1), (u +1, u + 2), \ldots , (v - 1, v)\)也就是 \(u\)\(v\) 之间相邻的点连成一条链。

于是奇点我们就处理完了,将加的边产生的代价统计到最终答案中。

处理连通性

这么处理完之后,图还有可能不连通,怎么办呢?

在经过上述操作之后,图已经形成了一些连通块,现在我们要做的就是在这些连通块之间加边,使得整张图连通,还要让加的这些边权和最小……

让图连通的最小代价,那么最小生成树已经呼之欲出了,具体做法就是把每个连通块看成一个点,跑最小生成树即可!

最小生成树边权和的二倍统计到最终答案中。为什么是二倍呢,两种角度理解:

  • 加边会让边的两个端点变成奇点,因此这条边添加两次,维护奇偶性;

  • 欧拉回路,都回路了,你去另外一个连通块的时候总得回来吧?

最后的最后,再将原先那些必走的丁香道路边权和(初始的 \(S\) 集合边权和)统计到最终答案,那么最终答案就华丽的变成了最终答案(?

于是思路我们讲完了,再说一些事:

  • 第一:我们可以发现,上述所有用到的操作,其实只用到了点的度数和并查集查询点所在连通块,因此你根本就不用建图;

  • 第二:处理连通性跑最小生成树之前,提供给最小生成树选择的边也是有讲究的,为了维护边权和最小的性质,提供选择的边显然两个端点是“不在同一个连通块中的两个点”分别的并查集祖先(也就是连通块编号);但是边权需要做到应该是两个连通块中端点编号差最小值(对应最小的边权),实际我们可以这样做:

  •         e.clear();
            lst = 0;
            for (int j = 1; j <= n; ++j) {
                if (deg[j] == 0)
                    continue;
                if (lst == 0 || d.get(a.get(lst)) == d.get(a.get(j))) {
                    lst = j;
                    continue;
                }
                edge now;
                now.u = a.get(lst);
                now.v = a.get(j);
                now.dis = j - lst;
                e += now;
                lst = j;
            }
    
  • 代码中的 e 是一个 basic_string <edge> 类型的变量,存的就是供最小生成树选择的边。观察这段代码,你会发现 lst 只要不是 deg[j] == 0 就会跟随 j,也就是存储 j 的上一个度数不为 \(0\) (不是一个孤岛)的顶点编号。当我们发现 j 和 上一个不是孤岛的点不在一个连通块,就连。你是否想问这样是否会漏掉?比如1和3之间本来不在一个并查集但是被2淹没了于是他们对应的连通块没有连。确实是这样的,但是,什么都不影响!原因如下(设 \(d_i\)\(i\) 所在连通块号,有 \(d_1 \neq d_3\)):

  • 如果2是孤岛点,显然1和3会正常处理;

  • 如果 \(d_2 = d_1\),于是 \(d_3\)\(d_2\) 连,代价为 \(1\),是比你连 \(d_3\)\(d_1\),代价 \(2\) 更优的;

  • 如果 \(d_2 = d_3\),于是 \(d_2\)\(d_1\) 连,代价为 \(1\),同样更优;

  • 如果 \(d_2 \neq d_1\)\(d_2 \neq d_3\),于是 \(d_2\)\(d_1\) 连,\(d_3\)\(d_2\) 连,代价分别为 \(1\),等效于连了一条 \(d_3\)\(d_1\) 的代价为 \(2\) 的边。

  • 所以这样是不会影响的。另外,这样你还会发现,给最小生成树的边最多只有 \(n - 1\) 条。

  • 第三:要把 \(m\) 个丁香道路用并查集统一处理好,不需要每个循环都处理一遍。

  • 第四:思考一下刚刚我们连奇点的时候,不直接连这对奇点而是,将这对奇点之间的所有点用链连起来。那如果我们直接连,会不会影响算法的正确性呢?会。原因:假设本来5和7是两个奇点,如果你的代码连了5和7,而6和5,7不连通,那么后面最小生成树我们还会跑一个5连6,6连7的结果,于是我们就花费了将5,6,7连成一个三角的代价(\(3\)),但事实上需要5 - 7这条边吗?不需要,你去掉它,5,6,7之间通向任意一个点的代价是不变的,因此你多花费了 \(1\) 的代价,答案不最优。

  • ps:这里是我们将连奇点方式改成直连的测评结果,果不其然WA60,鼠标移到WA的数据点,会发现我们得到的答案一直比正确答案要大。

\(\mathtt{Time} \text{ } \mathtt{Complexity}\)

\(\operatorname{O}(n^2\log n)\)

最外层 \(n\) 循环,里层最大的是边数为 \(n - 1\) 的最小生成树。

\(\mathtt{Code}\)

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2022-07-03 23:56:12 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2022-07-04 02:14:54
 */
#include <bits/stdc++.h>
#define int long long

inline int read() {
    int x = 0;
    bool flag = true;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        if (ch == '-')
            flag = false;
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 1) + (x << 3) + ch - '0';
        ch = getchar();
    }
    if(flag) return x;
    return ~(x - 1);
}


struct edge {
    int u, v, dis;
};

std :: basic_string <edge> e;

bool cmp(edge a, edge b) {
    return a.dis < b.dis;
}

const int maxn = 2505;

int deg[maxn];

struct query_set {
    int fa[maxn];
    void init(int n) {
        for (int i = 1; i <= n; ++i)
            fa[i] = i;
    }

    int get(int x) {
        while (x != fa[x])
            x = fa[x] = fa[fa[x]];
        return x;
    }

    void merge(int x, int y) {
        fa[get(x)] = get(y);
    }
}a, d;

inline int abs(int x) {
    return x > 0 ? x : -x;
}

signed main() {
    int n = read(), m = read(), s = read(), flower = 0;
    a.init(n);
    for (int _ = 1; _ <= m; ++_) {
        int u = read(), v = read();
        ++deg[u];
        ++deg[v];
        a.merge(u, v);
        flower += abs(u - v);
    }

    
    for (int i = 1; i <= n; ++i) {
        d.init(n);
        ++deg[s];
        ++deg[i]; // 在 s 和 i 中连边 i -> s,变为构造欧拉回路

        d.merge(a.get(s), a.get(i));

        // 将奇点两两配对连边,同时考虑到此处边权的性质,u 和 v 连,为了让他最能有连通性
        // 不连 u 和 v,而是把 u 和 v 中的所有点连成一条链
        int lst = -1LL, free = 0;
        for (int j = 1; j <= n; ++j) {
            if ((deg[j] & 1) == 0)
                continue;
            if (lst == -1) {
                lst = j;
                continue;
            }
            for (int k = lst; k < j; ++k)
                d.merge(a.get(k), a.get(k + 1));
            free += j - lst;
            lst = -1;
        }

        // 连通块连边
        e.clear();
        lst = 0;
        for (int j = 1; j <= n; ++j) {
            if (deg[j] == 0)
                continue;
            if (lst == 0 || d.get(a.get(lst)) == d.get(a.get(j))) {
                lst = j;
                continue;
            }
            edge now;
            now.u = a.get(lst);
            now.v = a.get(j);
            now.dis = j - lst;
            e += now;
            lst = j;
        }

        // 在连通块连的边中跑 MST,获得最少代价联通方案
        std :: sort(e.begin(), e.end(), cmp);
        for (int i = 0; i < e.length(); ++i) {
            edge now = e[i];
            if (d.get(now.u) != d.get(now.v)) {
                d.merge(now.u, now.v);
                free += now.dis * 2; // 为了连通时不破坏奇偶性,只能连两次咯
            }
        }

        // 把加来的 i -> s 这条边还回去
        --deg[s];
        --deg[i];
        printf("%lld ", flower + free);
    }
    puts("");
    return 0;
}

/*
4 3 1
1 2
4 2
3 1
*/

非常神奇的一道题,可以看出出题人有着非常强大的功底。

就算你不知道这题怎么做,翻看了题解之后,也不会有那么大的失落感,而是完全被这道题的绝妙思路而惊叹。(虽然我自己做出来了)

另外,这个题不需要建图,导致代码特别的诗意……

不夸了,反正很适合码代码的一道题。

posted @ 2022-07-05 00:38  dbxxx  阅读(109)  评论(2编辑  收藏  举报