[EC Final 2022] Map

GitHub跳转原题关系图返回列表

最优路线可化为对 s、t 各连续做若干次「公园→地图」收缩传送后走直线,枚举两个次数使 a+b≤n 即可,O(n²)。

OJ: luogu

题目 ID: P9720

难度:省选/NOI-

标签:计算几何相似变换枚举数学

创建: 2026-10-02 15:23

更新: 2026-10-03 12:51

形式化题目

平面上给定两个矩形:公园 PP 与地图 MM,MM 是 PP 的相似图形且完全落在 PP 内。第 ii 个地图角点对应第 ii 个公园角点,因此存在相似变换 f:M→Pf:M\to P,其相似比 r=∣f(a)−f(b)∣∣a−b∣⩾1r=\dfrac{|f(a)-f(b)|}{|a-b|}\geqslant 1(地图不小,故 ff 是放大)。令 g=f−1g=f^{-1} 为反向收缩。

Mio 位于公园内的点 xx 时,可以选择:不动,或传送到 g(x)∈Mg(x)\in M(公园 →\to 地图)。若 xx 同时落在地图上,还可以传送到 f(x)∈Pf(x)\in P(地图 →\to 公园)。每传送一次耗时 kk 秒,最多传送 nn 次;步行速度为 11 单位每秒,可在平面内任意行走。

给定 s,ts,t(均在 PP 内),求从 ss 到 tt 所需的最小总时间。注意地图可能被翻转,故 ff 允许带反射。

正解

思路

先看传送能到达哪里。一次传送把点沿相似变换 ff 或 gg 搬一次,其中 gg 把公园收缩成地图,是个「变小」的映射。走路则不改变位置关系,只产生欧氏距离。

关键观察一:连续的 ff 与 gg 相邻出现时,必定浪费。

ff 与 gg 互为逆映射,所以对合法位置有 g(f(x))=xg(f(x))=x、f(g(y))=yf(g(y))=y。若路线中出现了 ff 紧接着 gg(或反过来),这一对会回到原处,白白花掉 2k2k 秒。把它们删掉,路线其余部分不变,只会更优。所以最优路线的传送段中,gg 与 ff 不会交替,一定是「先若干个 gg、再若干个 ff」,或相反的「先若干个 ff、再若干个 gg」。

关键观察二:走路应当尽量晚走。

gg 是收缩映射,对任意两点都有 ∣g(a)−g(b)∣=∣a−b∣r⩽∣a−b∣|g(a)-g(b)|=\dfrac{|a-b|}{r}\leqslant |a-b|。因此把一段步行挪到某个 gg 之后,这段位移只会变短或不变,不可能变长。

把两条观察合起来:最优路线形如

s→ ga sa→ 步行 u→ fc t, s \xrightarrow{\ g^{a}\ } s_a \xrightarrow{\ \text{步行}\ } u \xrightarrow{\ f^{c}\ } t,

即先对 ss 做 aa 次收缩,再走一段直线,最后做 cc 次放大到达 tt,其中 a+c⩽na+c\leqslant n。

最后的放大段可以直接反过来理解:既然 fc(u)=tf^{c}(u)=t,就有 u=gc(t)u=g^{c}(t)。于是整条路线等价于

对 s 做 a 次 g,对 t 做 b 次 g,再走直线, \text{对 } s \text{ 做 } a \text{ 次 } g,\quad \text{对 } t \text{ 做 } b \text{ 次 } g,\quad \text{再走直线},

其中 a+b⩽na+b\leqslant n。由对称性,先放大后收缩的情况完全一样(aa 与 bb 互换,距离对称)。

于是答案就是

min⁡a+b⩽n (∣ga(s)−gb(t)∣+k(a+b)). \min_{a+b\leqslant n}\ \Bigl(\bigl|g^{a}(s)-g^{b}(t)\bigr| + k(a+b)\Bigr).

因为 n⩽100n\leqslant 100,直接枚 a,ba,b 两层循环即可,每层把点做一次 gg。

如何实现一次 gg(公园 →\to 地图)。

两个矩形都是矩形,所以从同一个角点出发的两条邻边互相垂直,可以当正交基用。设公园角点依次为 A1…A4A_1\dots A_4、地图角点为 B1…B4B_1\dots B_4,且 AiA_i 对应 BiB_i。取

A1,u=A4−A1,v=A2−A1, A_1,\quad u=A_4-A_1,\quad v=A_2-A_1,

对公园内任意点 pp,把它在这组基下分解:

α=(p−A1)⋅uu⋅u,β=(p−A1)⋅vv⋅v. \alpha=\frac{(p-A_1)\cdot u}{u\cdot u},\qquad \beta=\frac{(p-A_1)\cdot v}{v\cdot v}.

由于地图与公园相似且对应边一一对应,同样的系数配在地图的正交基上就得到像点:

g(p)=B1+α (B4−B1)+β (B2−B1). g(p)=B_1+\alpha\,(B_4-B_1)+\beta\,(B_2-B_1).

这组点积写法自动处理了旋转与翻转,不需要单独讨论地图是否倒放;也不需求出相似比 rr。

另外,每一步枚举都让 a+b⩽na+b\leqslant n,所以传送次数约束天然满足;a=b=0a=b=0 的项就是「完全不传送、直接走过去」。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-02 15:23
 * update_at: 2026-10-03 12:45
 */
// main.cpp:P9720 [EC Final 2022] Map 正解。
//
// 核心结论:最优路线一定可以写成
//   从 s 连续做 a 次“公园 -> 地图”传送到达 s';
//   从 t 连续做 b 次“公园 -> 地图”传送到达 t';
//   再从 s' 沿直线走到 t',其中 a + b <= n。
//
// 为什么只用“公园 -> 地图”这一个方向:
//   1) 走路放在传送中间不如挪到最后。地图比公园小,把中间那段路挪到
//      最后走只会更短(题解中有完整证明)。
//   2) “地图 -> 公园”之后再“公园 -> 地图”会原样回到出发点,白花 2k 秒,
//      所以最优序列里不会出现这种反复。
//
// 于是只要枚举 a、b,每次“公园 -> 地图”就是这个相似变换的逆,
// 直接把点按固定比例收缩,复杂度 O(n^2)。
#include <bits/stdc++.h>
using namespace std;

const long double INF = 1e30L;

// 二维点 / 向量
struct Point {
    long double x, y;
};

Point operator+(Point a, Point b) { Point c; c.x = a.x + b.x; c.y = a.y + b.y; return c; }
Point operator-(Point a, Point b) { Point c; c.x = a.x - b.x; c.y = a.y - b.y; return c; }
Point operator*(long double t, Point a) { Point c; c.x = t * a.x; c.y = t * a.y; return c; }

// 点积
long double dot(Point a, Point b) { return a.x * b.x + a.y * b.y; }

Point park[5]; // 公园的四个角,下标 1..4
Point mp[5];   // 地图的四个角,下标 1..4
Point s, t;    // 起点、终点,均在公园内
int k, n;      // 每次传送耗时、传送次数上限

long double dist(Point a, Point b) {
    long double dx = a.x - b.x;
    long double dy = a.y - b.y;
    return sqrtl(dx * dx + dy * dy);
}

// 公园中的点 p 传送到地图上的对应点,即题面中的 f^{-1}。
// 公园、地图都是矩形,所以 park[1] 出发的两条边 park[4]-park[1] 与
// park[2]-park[1] 相互垂直:先把 p 在这组正交基下分解,再用地图的
// 对应两条边还原即可。第 i 个地图角对应第 i 个公园角,故基向量也一一对应。
Point park_to_map(Point p) {
    Point o1 = park[1], e1 = park[4] - park[1], e2 = park[2] - park[1];
    Point o2 = mp[1],   f1 = mp[4] - mp[1],     f2 = mp[2] - mp[1];
    long double c1 = dot(p - o1, e1) / dot(e1, e1);
    long double c2 = dot(p - o1, e2) / dot(e2, e2);
    return o2 + c1 * f1 + c2 * f2;
}

void solve() {
    for (int i = 1; i <= 4; i++) {
        cin >> park[i].x >> park[i].y;
    }
    for (int i = 1; i <= 4; i++) {
        cin >> mp[i].x >> mp[i].y;
    }
    cin >> s.x >> s.y >> t.x >> t.y;
    cin >> k >> n;

    long double ans = INF;
    Point u = s;                                    // 对 s 做 a 次传送
    for (int a = 0; a <= n; a++) {
        Point v = t;                                // 对 t 做 b 次传送
        for (int b = 0; a + b <= n; b++) {
            long double cost = dist(u, v) + (long double)k * (a + b);
            if (cost < ans) ans = cost;
            v = park_to_map(v);
        }
        u = park_to_map(u);
    }
    cout << fixed << setprecision(10) << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int T;
    cin >> T;
    while (T--) solve();

    return 0;
}

复杂度

每个测试点枚举 O(n2)O(n^2) 个 (a,b)(a,b) 组合,每次做 O(1)O(1) 次点积与开方;总时间复杂度 O(Tn2)O(Tn^{2}),空间 O(1)O(1)。

总结

本题的难点不在计算几何的实现,而在把连续的最优路线压缩成有限种结构:

  • 相邻的 ff 与 gg 会互相抵消,最优序列不会让两个方向交替;
  • gg 是收缩映射,任何步行段挪到收缩之后都只会更短。

两条一合并,路线就被固定成「对 ss 收缩若干次、对 tt 收缩若干次、然后走直线」,用 O(n2)O(n^2) 枚举即可。实现上只需用矩形邻边构造正交基做点积分解,就能一次性处理旋转和翻转。