Loading

[题解] 2025 ICPC 武汉邀请赛 2025 ICPC Wuhan Invitational Contest (The 3rd Universal Cup. Stage 37 Wuhan)

补题链接:Dashboard - 2025 ICPC Wuhan Invitational Contest (The 3rd Universal Cup. Stage 37: Wuhan) - Codeforces

中文题面:wuhan-contest-zh.pdf

赛时榜单:第 50 届 ICPC 国际大学生程序设计竞赛邀请赛武汉站 - 正式赛 | Board - XCPCIO

官方题解:2025 ICPC 国际大学生程序设计竞赛 全国邀请赛(武汉)

题号按照字典序排序,鼠标悬停至题号,可快速跳转。

A

依据题意模拟,每个属性都可以分开计算。对于每条建议,我们可以缩小属性的范围,最终检查是否 l <= r 判断合法性。

\(O(n)\)

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

void solve() {
    int n, Q;
    cin >> n >> Q;
    vector<int> a(n + 1);
    vector<pair<int, int>> b(n + 1, {1, 1e9});
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    while (Q--) {
        int p, l, r;
        cin >> p >> l >> r;
        b[p].first = max(b[p].first, l);
        b[p].second = min(b[p].second, r);
    }

    i64 ans = 0;
    for (int i = 1; i <= n; i++) {
        if (b[i].first > b[i].second) {
            cout << -1 << '\n';
            return;
        }
        if (b[i].first <= a[i] && a[i] <= b[i].second) {
            continue;
        } else {
            ans += min(abs(b[i].first - a[i]), abs(a[i] - b[i].second));
        }
    }
    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

C

官方题解的思路。

分类讨论,可以在打草纸上画出折线分析。因为草生长的时间是固定的,所以收割的时候的移动方向可能会变,也可能不会变。

第一次收割的时候肯定都是在 1 -> n时收割的,我们不讨论这个。

从收割之后开始计算时间:

  1. 如果下一次草是还是在 1 -> n 时收割的,那说明每次都会在 1 -> n时收割。

  2. 如果下一次草是在 n -> 1 时收割的:

    ​ (1) 如果再下一次草还是保持 n -> 1,那么说明以后一直保持 n -> 1

    ​ (2) 如果再下一次是 1 -> n,说明方向一直是变换的,1 -> nn -> 11 -> nn -> 1...

这一共是三种情况。我们需要分别考虑 收割时所在的周期。

k为常数,根据 a[i]计算得来,计算细节见代码。

  1. 一直是 1 -> n1, k + 1, 2k + 1....每次收割间隔的周期是一样的。
  2. 一直是 n -> 11, k, 2k, 3k....从第二次开始,都提前一个周期。
  3. 两种方向交替:1, k, 2k, 3k - 1, 4k - 1, 5k - 2... 间隔的周期一次长(k),一次短(k - 1)

代码实现时,分类讨论需要注意细节。

\(O(n + m\log m)\),计数、调和级数累加。

#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;

void solve() {
    int n, m;
    cin >> n >> m;
    int T = 2 * n - 1;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    // g1 g2 g3 用于统计
    vector<int> g1(m + 10), g2(m + 10), g3(m + 10);
    // 这里算 1 ~ n - 1 ,n 单独处理
    for (int i = 1; i < n; i++) {
        int k = a[i] / T;
        // 相隔超过 m 个周期,只有第一次能割。
        if (k >= m) {
            continue;
        }
        a[i] %= T;
        int f1 = a[i] / ((n - i) * 2);
        int f2 = a[i] / ((i - 1) * 2 + 1);
        if (f1) { // 来 => 来 => 来 => 来 => ...
            // 从 来 开始,跨越了 k 个周期,还能再跨到 下个周期前
            // 所以一直是 来 时收割,每 k + 1 个周期 收割一次
            g1[k + 1]++;
        } else if (f2) { // 来 => 回 => 回 => 回 => ...
            // 从 来 开始,因为 跨越 k 个周期后,不能跨到下个周期,
            // 所以转成 回 时收割。并且能保持这个收割状态,
            // 这样的话,第一次 k 个周期就能收割,后面都是 k + 1 收割一次。
            g2[k + 1]++;
        } else { // 来 => 回 => 来 => 回 => ...
            // 原理同上,
            // 来 => 回 是 k 个周期, 回 => 来 是 k + 1 个周期。
            g3[k + 1]++;
        }
    }
    // 对于 a_n,跨越 a_n 个周期收割一次,跟着 g1 走
    if (a[n] / T < m) {
        g1[a[n] / T + 1]++;
    }

    // 计算答案
    vector<int> ans(m + 1);
    // 首先所有草 一开始就能割一次
    ans[1] = n;
    for (int j = 1; j <= m; j++) {
        // g1,每 j 次能 割一次
        for (int i = 1 + j; i <= m; i += j) {
            ans[i] += g1[j];
        }
        // g2,从 0 开始,
        for (int i = j; i <= m; i += j) {
            ans[i] += g2[j];
        }
        // g3,交替
        int fl = 1;
        for (int i = j; i <= m; i += j - (fl ^= 1)) {
            ans[i] += g3[j];
        }
    }
    for (int i = 1; i <= m; i++) {
        cout << ans[i] << " \n"[i == m];
    }
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

E

官方题解的思路。

对于每个点,我们可以做到最优策略:

  1. 偶数度的点:每个颜色都出现两次。
  2. 奇数度的点:只有一个颜色出现一次,其他的颜色出现两次。

我们可以构造环来解决这个问题,把所有奇数度的点连成偶数度。然后对每个环着同一个颜色。

边取环边删边,\(O(n + m)\)。Codeforces, 296ms通过。

#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;

const int maxn = 2e5 + 5;
vector<pair<int, int>> g[maxn];
vector<int> vis, mp, lu, del;
vector<pair<int, int>> ed;
int co = 0;

void dfs(int x, int fe) {
    if (vis[x]) {
        vis[ed[fe].first] = vis[ed[fe].second] = 0;
        del[fe] = 1;
        mp[fe] = ++co;
        while (ed[lu.back()].first != x && ed[lu.back()].second != x) {
            int e = lu.back();
            auto [u, v] = ed[e];
            vis[u] = vis[v] = 0;
            lu.pop_back();
            mp[e] = co;
        }
        vis[ed[lu.back()].first] = vis[ed[lu.back()].second] = 0;
        mp[lu.back()] = co;
        lu.pop_back();
    } else {
        lu.push_back(fe);
    }
    vis[x] = 1;
    while (g[x].size()) {
        auto [v, id] = g[x].back();
        g[x].pop_back();
        if (id == fe || del[id]) {
            continue;
        }
        del[fe] = 1;
        dfs(v, id);
        return;
    }
}

void solve() {
    co = 0;
    int n, m;
    cin >> n >> m;
    lu.clear();
    vis.assign(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        g[i].clear();
    }
    vector<int> in(n + 1);
    ed.assign(m + 1, {0, 0});
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        ed[i] = {u, v};
        g[u].emplace_back(v, i);
        g[v].emplace_back(u, i);
        in[v]++;
        in[u]++;
    }
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        if (in[i] % 2) {
            q.push(i);
        }
    }
    int idx = m;
    while (q.size()) {
        int u = q.front();
        q.pop();
        int v = q.front();
        q.pop();
        g[u].emplace_back(v, ++idx);
        g[v].emplace_back(u, idx);
        ed.emplace_back(u, v);
    }
    del.assign(ed.size(), 0);
    mp.assign(ed.size(), 0);

    for (int x = 1; x <= n; x++) {
        for (auto &[v, id] : g[x]) {
            if (!del[id]) {
                vis[x] = 1;
                dfs(v, id);
            }
        }
    }

    for (int i = 1; i <= m; i++) {
        cout << mp[i] << " \n"[i == m];
    }
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

G

根号分治,根据每种颜色出现的次数,对每种颜色选择不同的做法,把复杂度控制在 \(O(nm \sqrt{nm})\) 内。

首先必须知道 从(i, j)(n, m)的路径数量怎么算。一个方向可以走n - i步,另一个方向可以走m - j步。从总步数里面选一部分走 n - i,则剩下的就是 m - j。即:\(C_{n - i + m - j} ^ {n - i}\)

两种做法:设当前计算的颜色为 o

  1. 遍历 \(n*m\) 个格子,状态只能从颜色不是o的格子转移而来。然后统计是o的格子上的状态就可以了。
  2. 记录下每个颜色是o的格子的坐标,然后排序。算到第 i 个坐标的时候,我们要减去被 前面的格子挡住的路径数,用总路径减去。总路径是(1, 1)(x_i, y_i)。减去 \(\sum\)(x_j, y_j)(x_i, y_i)。要求x_j <= x_i && y_j <= y_i

cnt为颜色为o的格子的数量。

显然第一种适合在 cnt大的情况下使用。极端情况,\(cnt = \sqrt{nm}\) 时,每次\(O(nm)\),一共\(\sqrt{nm}\)次,\(O(nm\sqrt{nm})\)

第二种在 cnt小的情况下使用,\(cnt = \sqrt{nm}\),也是 \(O(nm\sqrt{nm})\)

所以根据cnt大小选择做法。

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

const int maxn = 2e5 + 5;
const int mod = 998244353;

i64 f[maxn], g[maxn];
i64 ksm(i64 a, i64 n) {
    i64 ans = 1;
    a %= mod;
    while (n) {
        if (n & 1) {
            ans = ans * a % mod;
        }
        a = a * a % mod;
        n >>= 1;
    }
    return ans;
}

void init() {
    f[0] = 1;
    g[0] = 1;
    for (int i = 1; i < maxn; i++) {
        f[i] = f[i - 1] * i % mod;
        g[i] = g[i - 1] * ksm(i, mod - 2) % mod;
    }
}

inline i64 C(i64 n, i64 m) {
    if (m > n || n < 0 || m < 0) {
        return 0;
    }
    if (m == 0) {
        return 1;
    }
    return f[n] * g[m] % mod * g[n - m] % mod;
}

vector<pair<int, int>> coo[maxn];

void solve() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> a(n + 1, vector<int>(m + 1));
    vector<int> cnt(n * m + 1);
    int B = sqrtl(n * m);
    for (int i = 1; i <= n * m; i++) {
        // coo[i].swap(vector<pair<int, int>>());
        vector<pair<int, int>>().swap(coo[i]);
    }
    // vector<vector<pair<int, int>>> coo(n * m + 1);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
            cnt[a[i][j]]++;
            if (cnt[a[i][j]] <= B) {
                coo[a[i][j]].emplace_back(i, j);
            }
        }
    }

    i64 ans = 0;
    for (int o = 1; o <= n * m; o++) {
        if (cnt[o] > B) {
            vector<vector<i64>> dp(n + 1, vector<i64>(m + 1));
            dp[1][0] = 1;
            i64 res = 0;
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= m; j++) {
                    dp[i][j] = (dp[i - 1][j] * (a[i - 1][j] != o) + 
                        dp[i][j - 1] * (a[i][j - 1] != o)) % mod;
                    if (a[i][j] == o) {
                        res = (res + dp[i][j] * C(n - i + m - j, n - i)) % mod;
                    }
                }
            }
            ans = (ans + res) % mod;
        } else {
            auto &v = coo[o];
            sort(v.begin(), v.end());
            vector<i64> dp(v.size());
            for (int i = 0; i < v.size(); i++) {
                dp[i] = C(v[i].first + v[i].second - 2, v[i].first - 1);
                for (int j = 0; j < i; j++) {
                    if (v[j].first <= v[i].first && v[j].second <= v[i].second) {
                        dp[i] = (dp[i] - dp[j] * C(
                            v[i].first - v[j].first + v[i].second - v[j].second, 
                            v[i].first - v[j].first
                        ) % mod + mod) % mod;
                    }
                }
            }
            i64 res = 0;
            for (int i = 0; i < dp.size(); i++) {
                res = (res + dp[i] * C(
                    n - v[i].first + m - v[i].second, 
                    n - v[i].first
                )) % mod;
            }
            ans = (ans + res) % mod;
        }
    }
    cout << ans << '\n';
}

int main() {
    init();
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

I

一种容易想到的构造方式是,把大数往对角线里面填,最右下角不填,然后最右边一列最下边一行尽可能的小,然后控制最右下角的数控制答案

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

void solve() {
    int n, k;
    cin >> n >> k;
    if (k < n || k > n * n - n + 1) {
        cout << "No\n";
        return;
    }

    cout << "Yes\n";
    vector<int> vis(n * n + 1);
    vector<vector<int>> a(n + 1, vector<int>(n + 1));
    for (int i = 1; i <= n - 1; i++) {
        int v = n * n - i + 1;
        a[i][i] = v;
        vis[v] = 1;
    }
    a[n][n] = k;
    vis[k] = 1;

    int cur = n * n;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (a[i][j]) {
                continue;
            }
            while (vis[cur]) {
                cur--;
            }
            a[i][j] = cur;
            vis[cur] = 1;
        }
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cout << a[i][j] << " \n"[j == n];
        }
    }
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

J

显然使用后缀数组解决,对于选定的 [l_i, r_i],我们根据l_i寻找这个后缀的 rk,在rk[l_i]两侧二分,找到一个最大的区间,包含l_i并且 LCP大于r_i - l_i + 1

区间内的每个后缀,贡献是 n - sa[i] + 1 - ht[i]n - sa[i] + 1是原本后缀的长度,减去 ht[i],重复的部分。

对于区间最开头的那个后缀,贡献是 max(0, len - 1 - ht[区间左端点]),就是把长度 >=给定的缀长度的部分都拿掉,但是可能会有残留。

对于区间的其他部分(区间左端点,区间右端点],贡献全部拿走,并清零。

我们可以用线段树维护这一操作,另外修改的时候注意一下,没有负贡献。

\(O((n + q)\log n)\)。处理SA -> ST表预处理LCP->计算贡献线段树->对于每次查询二分出最大区间->区间修改,单点修改

#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;

const int maxn = 2e5 + 5;

// n 字符串长度,m 桶大小
int n, m;
// od[] 作为辅助数组,作为 oldsa[] / oldrk[]
// ct[] 计数,用于排序
int od[2 * maxn], ct[maxn];
int rk[2 * maxn], sa[maxn], ht[maxn];

void sor(int k) {
    for (int i = 0; i <= m; i++) ct[i] = 0;
    for (int i = 1; i <= n; i++) od[i] = sa[i];
    for (int i = 1; i <= n; i++) ct[rk[od[i] + k]]++;
    for (int i = 1; i <= m; i++) ct[i] += ct[i - 1];
    for (int i = n; i >= 1; i--) sa[ct[rk[od[i] + k]]--] = od[i];
}

// 1-indexed string, max ascii in string
void initSA(string &s, int _m) {
    m = _m;
    // 按第一个字母排序
    for (int i = 1; i <= n; i++) rk[i] = s[i];
    for (int i = n + 1; i <= 2 * n; i++) rk[i] = od[i] = 0;
    for (int i = 1; i <= n; i++) sa[i] = i;
    sor(0);
    // 倍增
    for (int k = 1; k <= n; k *= 2) {
        // 第二关键字排序
        sor(k);
        // 第一关键字排序
        sor(0);
        // 把已经能分辨出的 放进不同的桶
        for (int i = 1; i <= n; i++) od[i] = rk[i];
        m = 0;
        for (int i = 1; i <= n; i++) {
            // 如果分辨不出和前一个的差距,
            // 即 这一位 和 后k位 都一样,就和前一个放在一起。
            if (od[sa[i]] == od[sa[i - 1]] && od[sa[i] + k] == od[sa[i - 1] + k]) {
                rk[sa[i]] = m;
            } else {
                rk[sa[i]] = ++m;
            }
        }
        // 全部都能分开,已排好。
        if (m == n) break;
    }
    // 计算 ht[i] 代表 lcp(sa[i - 1], sa[i])
    for (int i = 1, k = 0; i <= n; i++) {
        // 第一名 ht 是 0
        if (rk[i] == 1) continue;
        // 如果上一个 ht 不是 0,按照引理这里可能会倒退 1
        if (k) k--;
        // 计算 lcp(i, j),和 前一个计算,用 rk[i] - 1 找前一个
        int j = sa[rk[i] - 1];
        while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
        ht[rk[i]] = k;
    }
}

struct SparseTable {
    int mi[maxn][25], lg[maxn];
    int a[maxn];

    void build() {
        lg[0] = -1;
        for (int i = 1; i <= n; i++) {
            mi[i][0] = a[i];
            lg[i] = lg[i / 2] + 1;
        }
        for (int i = 1; i <= lg[n]; i++) {
            for (int j = 1; j + (1 << i) - 1 <= n; j++) {
                mi[j][i] = min(mi[j][i - 1], mi[j + (1 << (i - 1))][i - 1]);
            }
        }
    }

    int query(int l, int r) {
        int len = lg[r - l + 1];
        return min(mi[l][len], mi[r - (1 << len) + 1][len]);
    }
} ST;

inline int LCP(int l, int r) {
    return ST.query(l + 1, r);
}

// 答案
i64 res = 0;

struct SegTree {
    struct Node {
        int l, r, laz;
        i64 v;
    } t[maxn * 4];

    int a[maxn];

    inline void pushup(int p) {
        auto &me = t[p];
        auto &lc = t[p << 1];
        auto &rc = t[p << 1 | 1];
        me.v = lc.v + rc.v;
    }

    inline void pushdown(int p) {
        auto &me = t[p];
        auto &lc = t[p << 1];
        auto &rc = t[p << 1 | 1];
        if (me.laz) {
            lc.v = 0;
            rc.v = 0;
            lc.laz = 1;
            rc.laz = 1;
            me.laz = 0;
        }
    }

    void build(int p, int l, int r) {
        auto &me = t[p];
        me.l = l, me.r = r;
        me.laz = 0;
        if (l == r) {
            me.v = a[l];
            return;
        }
        int mid = l + r >> 1;
        build(p << 1, l, mid);
        build(p << 1 | 1, mid + 1, r);
        pushup(p);
    }

    void modify(int p, int l, int r) {
        auto &me = t[p];
        if (l <= me.l && me.r <= r) {
            res += me.v;
            me.v = 0;
            me.laz = 1;
            return;
        }
        pushdown(p);
        int mid = t[p].l + t[p].r >> 1;
        if (l <= mid)
            modify(p << 1, l, r);
        if (r > mid)
            modify(p << 1 | 1, l, r);
        pushup(p);
    }

    void remain(int p, int i, i64 d) {
        auto &me = t[p];
        if (i <= me.l && me.r <= i) {
            res += max(0LL, me.v - d);
            me.v = min(me.v, d);
            return;
        }
        pushdown(p);
        int mid = t[p].l + t[p].r >> 1;
        if (i <= mid)
            remain(p << 1, i, d);
        if (i > mid)
            remain(p << 1 | 1, i, d);
        pushup(p);
    }
} T;

void solve() {
    res = 0;
    string s;
    cin >> s;
    n = s.size();
    s = " " + s;
    // 处理出后缀数组
    initSA(s, 128);

    // 快速求 LCP,基于 ST 表
    for (int i = 1; i <= n; i++) {
        ST.a[i] = ht[i];
    }
    ST.build();

    // 按照 后缀排序 建树
    for (int i = 1; i <= n; i++) {
        T.a[i] = n - sa[i] + 1 - ht[i];
    }
    T.build(1, 1, n);

    int Q;
    cin >> Q;
    while (Q--) {
        int li, ri;
        cin >> li >> ri;
        int len = ri - li + 1;
        // 寻找区间左端点
        int l = 0, r = rk[li];
        while (l + 1 < r) {
            int mid = l + r >> 1;
            if (LCP(mid, rk[li]) >= len) {
                r = mid;
            } else {
                l = mid;
            }
        }
        int L = r;
        // 寻找右端点
        l = rk[li], r = n + 1;
        while (l + 1 < r) {
            int mid = l + r >> 1;
            if (LCP(rk[li], mid) >= len) {
                l = mid;
            } else {
                r = mid;
            }
        }
        int R = l;

        // cout << "L R: " << L << ' ' << R << '\n';

        // [L + 1, R] 区间全部置 0
        if (L + 1 <= R) {
            T.modify(1, L + 1, R);
        }
        int sta = len - 1 - ht[L];
        sta = max(0, sta);
        T.remain(1, L, sta);
        
        cout << res << ' ';
    }
    cout << '\n';
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

L

注意到对数组排序不影响答案,但是更好计算。

注意到范围比较小,可以\(O(n^2)\)求解。

对于每个数作为中位数,从两侧往中心收lr。因为排好序了,根据a[l] + a[r]和中位数的大小动态调整lr并 check ,达到最优的效果。

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

void solve() {
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    sort(a.begin() + 1, a.end());

    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int l = 1, r = n;
        while (l <= i && i <= r) {
            int h = a[l] + a[r];
            if (h % 2 == 0 && h / 2 == a[i]) {
                if (i - l <= r - i - 1) {
                    ans = max(ans, (i - l + 1) * 2);
                } else {
                    ans = max(ans, (r - i) * 2 + 1);
                }
            }
            if (h / 2 >= a[i]) {
                r--;
            } else {
                l++;
            }
        }
    }
    cout << ans << '\n';
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}

M

官方题解第一个做法挺好的,讲的也很通透,这里直接写了个代码贴上了。

理解时注意外积方向。注意 asinacos必须控制定义域,要不然会丢失精度 Rejected

#include <bits/stdc++.h>
using ld = long double;
using i64 = long long;
using namespace std;

struct Pt {
    ld x, y, z;
};

Pt operator^(const Pt &a, const Pt &b) {
    return {
        a.y * b.z - a.z * b.y,
        a.z * b.x - a.x * b.z,
        a.x * b.y - a.y * b.x
    };
}

ld operator*(const Pt &a, const Pt &b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

ld mo(const Pt &a) {
    return sqrtl(a.x * a.x + a.y * a.y + a.z * a.z);
}

void solve() {
    ld r;
    Pt p, s, t;
    cin >> r >> p.x >> p.y >> p.z >> s.x >> s.y >> s.z >> t.x >> t.y >> t.z;
    Pt n = s ^ t;
    // 由于叉乘特性 (x轴方向 ^ y轴方向 = z轴方向),
    // 此时 n ^ s 指向 平面之间的区域(劣弧段),n ^ t 向外。
    // 如果 p 在 s, n 构成的平面与 t, n 构成的平面之间 (劣弧段)
    // 那么(充要), (n ^ s) * p > 0 ,(n ^ t) * p < 0。
    if ((n ^ s) * p > 0 && (n ^ t) * p < 0) {
        // 这个时候 s, n 构成的平面 与 p 的夹角 乘上半径 就是答案
        ld d = fabsl(n * p / mo(n)) / mo(p);
        d = max(-1.0L, d);
        d = min(1.0L, d);
        cout << asinl(d) * r << '\n';
    } else {
        // 否则就是在端点处取最小,s, t 分别与 p 取夹角,小角乘半径。
        ld d = s * p / mo(s) / mo(p);
        d = max(-1.0L, d);
        d = min(1.0L, d);
        ld j1 = acosl(d);

        d = t * p / mo(t) / mo(p);
        d = max(-1.0L, d);
        d = min(1.0L, d);
        ld j2 = acosl(d);
        cout << min(j1, j2) * r << '\n';
    }

}

int main() {
    cout << fixed << setprecision(10);
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int tt = 1;
    cin >> tt;
    while (tt--) {
        solve();
    }
    return 0;
}
posted @ 2025-06-05 19:28  Music163  阅读(1486)  评论(0)    收藏  举报