跳转至

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) 的欧拉路径,刚好符合题目的要求。求出这条欧拉路径就是答案。复杂度 O(n2logn)

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

证明

首先证明以下引理。

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

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

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

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

  • ABAD 都不在最短路上:直接 BAD,与假设矛盾。
  • ABAD 都在最短路上:此时两个三角形都有一条边在最短路上,而 BCAED 仍然是连通的,必须再从 BCACAEDE 中去掉至少一条边。但这样最短路就会经过其中一个三角形的两条边,与引理矛盾。
  • AB 在最短路上,AD 不在:此时必须再从 BCAC 中去掉至少一条边,否则 BCAD 是连通的。但这样最短路就会经过 ΔABC 的两条边,与引理矛盾。
  • AD 在最短路上,AB 不在:同上。

参考代码

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