跳转至

J - Triangle City

基本信息

题目出处2019 山东省大学生程序设计竞赛
队伍通过率2/307 (0.7%)

题解

假设我们选择了一条从 \((1, 1)\)\((n, n)\) 且不经过重复边的路径 \(p\)。从图上去掉 \(p\) 的所有边后,剩下的边就是我们“丢弃”的边,我们来观察这些边的特征。

首先注意到原图中所有节点的度数都是偶数,而去掉 \(p\) 之后,只有 \((1, 1)\)\((n, n)\) 两个节点的度数变为奇数,剩下的节点度数仍然为偶数。由于每个连通块的度数总和都是偶数,因此去掉 \(p\) 之后,\((1, 1)\)\((n, n)\) 不能分别处于不同的连通块,一定处于同一个连通块。也就是说,我们“丢弃”的边,至少包含一条从 \((1, 1)\)\((n, n)\) 的路径,可能还要加上一些额外的边。

为了让 \(p\) 尽量长,我们“丢弃”的边权之和要尽量小。那么从 \((1, 1)\)\((n, n)\) 边权之和最小的路径是什么呢?当然就是从 \((1, 1)\)\((n, n)\) 的最短路。因此我们求出一条从 \((1, 1)\)\((n, n)\) 的最短路,并把它从图中去掉。剩下的图中,只有 \((1, 1)\)\((n, n)\) 两个节点的度数变为奇数,剩下的节点度数仍然为偶数,因此存在一条从 \((1, 1)\)\((n, n)\) 的欧拉路径,刚好符合题目的要求。求出这条欧拉路径就是答案。复杂度 \(\mathcal{O}(n^2\log n)\)

有的读者可能会有疑问:如果去掉最短路之后,图变得不连通了,那么欧拉路径就不存在了。然而,由于题目保证了边权符合三角形不等式,因此去掉最短路之后,图一定仍然是连通的。

证明

首先证明以下引理。

引理:对于任意 \((i, j)\)\((i + 1, j)\)\((i + 1, j + 1)\) 构成的三角形,从 \((1, 1)\)\((n, n)\) 的最短路最多经过三角形的一条边。

这一引理容易证明。假设最短路首先经过了 \((i, j) \to (i + 1, j)\),又经过了 \((i + 1, j) \to (i + 1, j + 1)\)。由于边权满足三角形不等式,因此直接走 \((i, j) \to (i + 1, j + 1)\) 距离更短。

\(\blacksquare\)

接下来利用反证法证明去掉最短路之后,图仍然是连通的。

假设共享顶点 \(A\) 的两个三角形 \(\Delta ABC\)\(\Delta ADE\) 去掉最短路之后不连通了,不妨假设点 \(B\) 和点 \(D\) 不连通了。讨论 \(AB\)\(AD\) 是否在最短路上。

  • \(AB\)\(AD\) 都不在最短路上:直接 \(B \to A \to D\),与假设矛盾。
  • \(AB\)\(AD\) 都在最短路上:此时两个三角形都有一条边在最短路上,而 \(B \to C \to A \to E \to D\) 仍然是连通的,必须再从 \(BC\)\(AC\)\(AE\)\(DE\) 中去掉至少一条边。但这样最短路就会经过其中一个三角形的两条边,与引理矛盾。
  • \(AB\) 在最短路上,\(AD\) 不在:此时必须再从 \(BC\)\(AC\) 中去掉至少一条边,否则 \(B \to C \to A \to D\) 是连通的。但这样最短路就会经过 \(\Delta ABC\) 的两条边,与引理矛盾。
  • \(AD\) 在最短路上,\(AB\) 不在:同上。

\(\blacksquare\)

参考代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <bits/stdc++.h>
#define MAXN 300
using namespace std;
typedef pair<int, int> pii;
typedef pair<long long, pii> plii;

int n;
vector<int> ans;

int etot, e[MAXN * MAXN * 3 * 2 + 10][3], p[MAXN * MAXN + 10];
long long dis[MAXN * MAXN + 10];
int from[MAXN * MAXN + 10];
bool ban[MAXN * MAXN * 3 * 2 + 10];

int gao(int i, int j) { return i * n + j; }

void adde(int sn, int fn, int val) {
    etot++; e[etot][0] = fn; e[etot][1] = val; e[etot][2] = p[sn]; p[sn] = etot;
    etot++; e[etot][0] = sn; e[etot][1] = val; e[etot][2] = p[fn]; p[fn] = etot;
}

// dijkstra 求从 (1, 1) 到 (n, n) 的最短路
void dijkstra() {
    memset(dis, -1, sizeof(long long) * (n * n + 3));
    priority_queue<plii> pq;
    pq.push(plii(0, pii(0, 0)));
    while (!pq.empty()) {
        plii tp = pq.top(); pq.pop();
        int sn = tp.second.first;
        if (dis[sn] >= 0) continue;
        dis[sn] = -tp.first;
        from[sn] = tp.second.second;
        for (int i = p[sn]; i > 0; i = e[i][2]) {
            int fn = e[i][0];
            if (dis[fn] >= 0) continue;
            pq.push(plii(-dis[sn] - e[i][1], pii(fn, i)));
        }
    }

    memset(ban, 0, sizeof(bool) * (etot + 3));
    for (int sn = gao(n - 1, n - 1); sn > 0; sn = e[from[sn] ^ 1][0]) ban[from[sn]] = ban[from[sn] ^ 1] = true;
}

// hierholzer 求欧拉路径
void dfs(int sn) {
    for (int i = p[sn]; i > 0; i = p[sn]) {
        if (ban[i]) { p[sn] = e[i][2]; continue; }
        ban[i] = ban[i ^ 1] = true;
        dfs(e[i][0]);
        ans.push_back(e[i][0]);
    }
}

short dir[3][4] = {0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1};

void solve() {
    scanf("%d", &n);
    etot = 1;
    memset(p, 0, sizeof(int) * (n * n + 3));
    long long sm = 0;
    for (int k = 0; k < 3; k++) for (int i = 0; i < n - 1; i++) for (int j = 0; j <= i; j++) {
        int x = gao(i + dir[k][0], j + dir[k][1]), y = gao(i + dir[k][2], j + dir[k][3]);
        int z; scanf("%d", &z);
        adde(x, y, z);
        sm += z;
    }

    // 求最短路
    dijkstra();
    // 最长路径就是边权之和减去最短路的长度
    printf("%lld\n", sm - dis[gao(n - 1, n - 1)]);

    // 求欧拉路径
    ans.clear(); dfs(0); reverse(ans.begin(), ans.end());
    printf("%d\n1 1", ans.size() + 1);
    for (int i = 0; i < ans.size(); i++) printf(" %d %d", ans[i] / n + 1, ans[i] % n + 1);
    printf("\n");
}

int main() {
    int tcase; scanf("%d", &tcase);
    while (tcase--) solve();
    return 0;
}