摘要:本题要求在单位网格上不抬笔连续绘制所有给定线段,求最少墨水量,本质是图论中的开放中国邮递员问题。解法先将每条线段按整数点拆分为单位边,构建无向图并检查连通性,再用 Floyd‑Warshall\texttt{Floyd‑Warshall}Floyd‑Warshall 求任意两点最短距离,最后通过状压 DP 对奇度顶点做最小权完美匹配(枚举欧拉路径端点对),答案为总边长加上额外重复路径长度。坐标范围极小,算法高效可行。

题目描述

给定若干条单位网格上的线段,线段端点均为整数坐标。要求从某条线段的一个端点开始,笔尖不离开纸面,经过所有给定线段至少一次,且不得绘制额外线段。允许重复经过已有线段,仅能在整数坐标点处改变方向。求完成绘制所需的最少墨水量(单位长度墨水为 111 个单位)。若无法在不抬笔的情况下完成所有线段,则输出特殊标记 ~x(。

输入格式

第一行包含一个整数 TTT(T<20T < 20T<20),表示测试用例总数。
每个测试用例第一行包含一个整数 NNN(N<10N < 10N<10),表示线段数量。
接下来 NNN 行,每行描述一条线段,格式为两个点 Y1X1 Y2X2Y_1X_1\ Y_2X_2Y1​X1​ Y2​X2​,其中 YYY 取值为 A\texttt{A}A 到 E\texttt{E}E(对应纵坐标 111 到 555),XXX 取值为 1\texttt{1}1 到 5\texttt{5}5(对应横坐标 111 到 555)。

输出格式

对于每个测试用例,输出 Case x: 后接所需墨水量,保留两位小数。若无法完成,则输出 ~x(。

样例

输入

2
4
A1 C2
B3 C3
C4 C2
C2 D2
2
A1 A5
E1 E5

输出

Case 1: 8.24
Case 2: ~x(

题目分析

本题本质是图论中的中国邮递员问题(开放版)。给定线段作为无向带权图的边,端点作为顶点,边权为欧氏距离。绘制要求等价于:从某个顶点出发,沿图的边行走,覆盖所有边至少一次,允许重复边,求最短行走距离。

由于允许重复经过边,问题转化为:在原图上添加若干重复边(即重复行走的部分),使得新图中存在一条欧拉路径(即从某起点出发,恰好经过所有边一次,不要求回起点)。根据欧拉路径存在条件,新图必须连通且奇度顶点数目为 000(欧拉回路)或 222(欧拉路径)。原图可能已有一些奇度顶点。由于添加重复边会改变顶点的奇偶性(重复一条边会使两端点度数的奇偶性均翻转),因此需要在奇度顶点之间通过重复路径配对连接,使得最终奇度顶点数不超过 222。

本题额外约束:所有顶点坐标都是整数,且线段端点均在 [1,5]×[1,5][1,5]\times[1,5][1,5]×[1,5] 的网格上,但线段可能穿过中间点。然而题目给出的每条线段仅由两个端点描述,而规则 5\texttt{5}5 要求只能在整数坐标点改变方向。因此实际绘制时,必须沿线段经过所有整数点,且方向只能在这些整数点处改变。故对于每一条原始线段,必须将其拆分为连接相邻整数点的若干小段,这些小段才是真正可走的基本单元。拆分后形成新的无向无权(但边长均为 111)图,原问题等价于在该新图上求最短的连续覆盖所有边的路线。

若原图不连通,则不可能不抬笔经过所有边,直接输出特殊标记。

解题思路

处理每个测试用例的步骤如下:

步骤 1\texttt{1}1. 解析线段并拆分为单位线段
对于每条给定线段,其两个端点均为整数坐标。由于只能沿整数点转向,必须把线段上的所有整数点(包括端点)依次列出,然后将相邻整数点之间连成一条单位长度的边。这些单位边构成新图的基本边集。所有顶点为所有出现的整数坐标点。

步骤 2\texttt{2}2. 构建无向图并计算度数
对每条单位边,连接两个顶点,并记录该边长度为 111。同时累加所有单位边的总长度 totalLentotalLentotalLen,并更新两端点的度数。

步骤 3\texttt{3}3. 检查连通性
使用深度优先搜索遍历图,若存在顶点未被访问,则图不连通,输出特殊标记并跳过后续计算。

步骤 4\texttt{4}4. 计算所有顶点对之间的最短路径
由于顶点数量很小(最多 252525 个整数点,实际出现的更少),使用 Floyd‑Warshall 算法计算任意两点间的最短距离(以单位边长度 111 为权值)。该距离代表在图上重复走某段路径所需的最小额外墨水量。

步骤 5\texttt{5}5. 处理奇度顶点匹配
设当前图中度数为奇数的顶点集合为 SSS,其大小为 mmm(必为偶数)。若 m=0m=0m=0,则图已是欧拉图,不需要额外重复,答案为 totalLentotalLentotalLen。
若 m>0m>0m>0,需要添加重复路径使奇度顶点数变为 222(即选择一对奇度顶点作为欧拉路径的起点和终点),其余 m−2m-2m−2 个奇度顶点两两配对,每对之间走一条最短路径作为重复边。重复路径的长度之和即为需要额外增加的墨水量。
枚举所有可能的端点对 (i,j)(i,j)(i,j),对剩余顶点做最小权完美匹配(状压 DP)。匹配权重为两点间的最短距离。取所有端点对中匹配权值的最小值作为额外重复长度 repeatrepeatrepeat。最终答案为 totalLen+repeattotalLen + repeattotalLen+repeat。

步骤 6\texttt{6}6. 输出结果
保留两位小数输出答案。

代码实现

// Continuous Drawing
// UVa ID: 11156
// Verdict: Accepted
// Submission Date: 2026-07-17
// UVa Run Time: 0.000s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

struct Point {
    int x, y;
};

bool onSegment(const Point& p, const Point& a, const Point& b) {
    long long cross = 1LL * (p.x - a.x) * (b.y - a.y) - 1LL * (p.y - a.y) * (b.x - a.x);
    if (cross != 0) return false;
    long long dot = 1LL * (p.x - a.x) * (b.x - a.x) + 1LL * (p.y - a.y) * (b.y - a.y);
    long long len2 = 1LL * (b.x - a.x) * (b.x - a.x) + 1LL * (b.y - a.y) * (b.y - a.y);
    return 0 <= dot && dot <= len2;
}

double distPoint(const Point& a, const Point& b) {
    return sqrt(1.0 * (a.x - b.x) * (a.x - b.x) + 1.0 * (a.y - b.y) * (a.y - b.y));
}

Point parsePoint(const string& s) {
    int x = s[1] - '0';
    int y = s[0] - 'A' + 1;
    return {x, y};
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin >> T;
    for (int tc = 1; tc <= T; ++tc) {
        int N;
        cin >> N;
        vector<pair<Point, Point>> segs;
        for (int i = 0; i < N; ++i) {
            string p1s, p2s;
            cin >> p1s >> p2s;
            Point p1 = parsePoint(p1s);
            Point p2 = parsePoint(p2s);
            segs.push_back({p1, p2});
        }
        map<pair<int, int>, int> id;
        auto getIdx = [&](const Point& p) -> int {
            pair<int, int> key = {p.x, p.y};
            if (id.count(key)) return id[key];
            int idx = (int)id.size();
            id[key] = idx;
            return idx;
        };
        vector<tuple<int, int, double>> edges;
        vector<int> deg;
        double totalLen = 0.0;
        for (auto& seg : segs) {
            Point a = seg.first, b = seg.second;
            vector<Point> pts;
            for (int x = 1; x <= 5; ++x)
                for (int y = 1; y <= 5; ++y) {
                    Point p{x, y};
                    if (onSegment(p, a, b)) pts.push_back(p);
                }
            auto distSq = [&](const Point& p) -> long long {
                long long dx = p.x - a.x, dy = p.y - a.y;
                return dx * dx + dy * dy;
            };
            sort(pts.begin(), pts.end(), [&](const Point& p, const Point& q) {
                return distSq(p) < distSq(q);
            });
            for (size_t k = 0; k + 1 < pts.size(); ++k) {
                Point p = pts[k], q = pts[k + 1];
                int u = getIdx(p), v = getIdx(q);
                double w = distPoint(p, q);
                edges.emplace_back(u, v, w);
                totalLen += w;
                if ((int)deg.size() <= u) deg.resize(u + 1, 0);
                if ((int)deg.size() <= v) deg.resize(v + 1, 0);
                deg[u]++, deg[v]++;
            }
        }
        int V = (int)id.size();
        if (V == 0) {
            cout << "Case " << tc << ": 0.00\n";
            continue;
        }
        // 连通性检查
        vector<vector<int>> adj(V);
        for (auto& e : edges) {
            int u = get<0>(e), v = get<1>(e);
            adj[u].push_back(v);
            adj[v].push_back(u);
        }
        vector<bool> vis(V, false);
        function<void(int)> dfs = [&](int u) {
            vis[u] = true;
            for (int v : adj[u])
                if (!vis[v]) dfs(v);
        };
        dfs(0);
        bool connected = true;
        for (int i = 0; i < V; ++i)
            if (!vis[i]) { connected = false; break; }
        if (!connected) {
            cout << "Case " << tc << ": ~x(\n";
            continue;
        }
        // Floyd-Warshall算法求所有点对最短路径
        const double INF = 1e100;
        vector<vector<double>> dist(V, vector<double>(V, INF));
        for (int i = 0; i < V; ++i) dist[i][i] = 0.0;
        for (auto& e : edges) {
            int u = get<0>(e), v = get<1>(e);
            double w = get<2>(e);
            if (w < dist[u][v]) dist[u][v] = dist[v][u] = w;
        }
        for (int k = 0; k < V; ++k)
            for (int i = 0; i < V; ++i)
                for (int j = 0; j < V; ++j)
                    if (dist[i][k] + dist[k][j] < dist[i][j])
                        dist[i][j] = dist[i][k] + dist[k][j];
        // 奇度顶点
        vector<int> odd;
        for (int i = 0; i < V; ++i)
            if (deg[i] % 2 == 1) odd.push_back(i);
        int m = (int)odd.size();
        double repeat = 0.0;
        if (m > 0) {
            // 奇度点之间的距离矩阵
            vector<vector<double>> d(m, vector<double>(m, 0.0));
            for (int i = 0; i < m; ++i)
                for (int j = 0; j < m; ++j)
                    d[i][j] = dist[odd[i]][odd[j]];
            int fullMask = (1 << m) - 1;
            vector<double> dp(1 << m, INF);
            dp[0] = 0.0;
            for (int mask = 1; mask <= fullMask; ++mask) {
                if (__builtin_popcount((unsigned)mask) % 2) continue;
                int p = -1;
                for (int i = 0; i < m; ++i)
                    if (mask & (1 << i)) { p = i; break; }
                for (int q = p + 1; q < m; ++q) {
                    if (!(mask & (1 << q))) continue;
                    int prev = mask ^ (1 << p) ^ (1 << q);
                    dp[mask] = min(dp[mask], dp[prev] + d[p][q]);
                }
            }
            double best = INF;
            for (int i = 0; i < m; ++i)
                for (int j = i + 1; j < m; ++j) {
                    int sub = fullMask ^ (1 << i) ^ (1 << j);
                    best = min(best, dp[sub]);
                }
            repeat = best;
        }
        double ans = totalLen + repeat;
        cout << "Case " << tc << ": " << fixed << setprecision(2) << ans << "\n";
    }
    return 0;
}

总结

本题的关键在于将原始线段拆分为单位边,将绘制问题转化为图论中的开放中国邮递员问题。核心技巧是利用欧拉路径的度数条件,将重复行走的优化转化为奇度顶点之间的最小权完美匹配。由于坐标范围极小,Floyd‑Warshall\texttt{Floyd‑Warshall}Floyd‑Warshall 和状压 DP\texttt{DP}DP 均能高效求解。注意对不连通图的特殊处理,以及严格按题意在整数点处分段处理线段,否则会忽略中间可转向点导致错误。本题展示了图论模型转换与动态规划结合的经典思路。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐