1 条题解

  • 0
    @ 2025-10-28 9:37:07

    题解:「USACO 2024 Jan Platinum P3」Mooball Teams III(基于提供代码)

    核心思路总览

    本题是 计算几何 + 线段树组合问题,核心通过“总合法方案 - 不合法方案”的容斥思想求解。先计算所有可能被直线分隔的方案总数,再减去无法被水平/垂直直线分隔的方案,最终复杂度 (O(N \log N)),适配 (N=2 \times 10^5) 的规模。

    关键背景与容斥逻辑

    1. 合法方案的定义

    两支非空队伍(红队、蓝队)能被一条水平或垂直的非整数坐标直线分隔。例如:

    • 垂直直线 (x=k+0.5) 分隔:红队全在左侧((x \leq k)),蓝队全在右侧((x \geq k+1)),或反之。
    • 水平直线 (y=k+0.5) 分隔:红队全在下方((y \leq k)),蓝队全在上方((y \geq k+1)),或反之。

    2. 容斥原理

    • 总合法方案:所有可能被水平或垂直直线分隔的方案数。
    • 不合法方案:只能被水平或垂直直线分隔,但实际无法被任何直线分隔的方案(需减去重复计算的部分)。
    • 代码通过“总可能方案 - 不合法方案”计算最终答案,避免直接枚举的高复杂度。

    核心状态与数组定义

    数组/变量 含义
    x[i], y[i] 奶牛 (i) 的坐标,其中 (x_i) 和 (y_i) 均为 (1\ldots N) 的排列。
    ord[x] = y 映射:(x) 坐标对应的 (y) 坐标(因 (x_i) 是排列,可直接索引)。
    pw2[i] 预处理 (2^i \mod \text{MOD}),用于快速计算子集数。
    sum 总可能方案数(初始包含所有可被水平或垂直分隔的方案)。
    ans 不合法方案数(需从 sum 中减去)。
    sgt[p][k] 线段树节点 (p) 的状态:k=0(全0)、1(全1)、2(0→1)、3(1→0)、4(无限制),表示区间内队伍分配的约束。
    typ[i] 线段树叶子节点的类型(初始为0,动态更新为1或2)。

    完整代码(带关键注释)

    #include <bits/stdc++.h>
    using namespace std;
    
    int n, x[205000], y[205000], ord[205000], typ[205000];
    const long long mod = 1000000007;
    long long pw2[205000], sum, ans;
    // 线段树状态:0(全0), 1(全1), 2(0→1), 3(1→0), 4(无限制)
    long long sgt[805000][5];
    
    // 线段树合并:计算父节点状态 from 左右子节点
    void pushup(int p) {
        // 状态0:全0(左0且右0,或左0右无限制,或左无限制右0)
        sgt[p][0] = (sgt[p*2][0] * sgt[p*2+1][0] + 
                     sgt[p*2][0] * sgt[p*2+1][4] + 
                     sgt[p*2][4] * sgt[p*2+1][0]) % mod;
        // 状态1:全1(类似状态0)
        sgt[p][1] = (sgt[p*2][1] * sgt[p*2+1][1] + 
                     sgt[p*2][1] * sgt[p*2+1][4] + 
                     sgt[p*2][4] * sgt[p*2+1][1]) % mod;
        // 状态2:0→1(左0→1且右1/无限制,或左0/无限制且右0→1,或左0且右1)
        sgt[p][2] = (sgt[p*2][2] * (sgt[p*2+1][1] + sgt[p*2+1][4]) + 
                     (sgt[p*2][0] + sgt[p*2][4]) * sgt[p*2+1][2] + 
                     sgt[p*2][0] * sgt[p*2+1][1]) % mod;
        // 状态3:1→0(类似状态2)
        sgt[p][3] = (sgt[p*2][3] * (sgt[p*2+1][0] + sgt[p*2+1][4]) + 
                     (sgt[p*2][1] + sgt[p*2][4]) * sgt[p*2+1][3] + 
                     sgt[p*2][1] * sgt[p*2+1][0]) % mod;
        // 状态4:无限制(左右均无限制)
        sgt[p][4] = sgt[p*2][4] * sgt[p*2+1][4] % mod;
    }
    
    // 构建线段树:初始所有叶子为状态0(未限制)
    void build(int p, int s, int t) {
        if (s == t) {
            sgt[p][0] = sgt[p][1] = sgt[p][2] = sgt[p][3] = 0;
            sgt[p][typ[s]] = 1;  // typ初始为0,故sgt[p][0] = 1
            sgt[p][4] = 1;       // 无限制状态初始为1
            return;
        }
        int mid = (s + t) / 2;
        build(p*2, s, mid);
        build(p*2+1, mid+1, t);
        pushup(p);
    }
    
    // 更新线段树:将位置x的类型改为y(0/1/2)
    void settyp(int p, int s, int t, int x, int y) {
        if (s == t) {
            typ[s] = y;
            // 重置所有状态
            sgt[p][0] = sgt[p][1] = sgt[p][2] = sgt[p][3] = 0;
            if (typ[s] <= 1) {
                sgt[p][typ[s]] = 1;  // 类型0或1:对应状态直接置1
                sgt[p][4] = 1;       // 保留无限制状态
            } else {
                sgt[p][1] = 1;       // 类型2:强制状态1
                sgt[p][4] = 0;       // 取消无限制
            }
            return;
        }
        int mid = (s + t) / 2;
        if (x <= mid) settyp(p*2, s, mid, x, y);
        else settyp(p*2+1, mid+1, t, x, y);
        pushup(p);
    }
    
    int main() {
        // 预处理2的幂次模mod
        pw2[0] = 1;
        for (int i = 1; i <= 200050; i++) {
            pw2[i] = pw2[i-1] * 2 % mod;
        }
    
        scanf("%d", &n);
        for (int i = 1; i <= n; i++) {
            scanf("%d%d", &x[i], &y[i]);
            ord[x[i]] = y[i];  // x是排列,建立x→y的映射
        }
    
        // 计算总可能方案sum:所有可被水平或垂直直线分隔的方案
        for (int i = 1; i < n; i++) {
            // 垂直分隔:左侧i个,右侧n-i个,左右各选非空子集,红左蓝右或红右蓝左(×2)
            // 水平分隔同理,但此处先按垂直计算,后续×4(垂直2种+水平2种)
            sum = (sum + pw2[i-1] * (pw2[n - i] - 1)) % mod;
        }
        sum = sum * 4 % mod;  // ×4:垂直2种方向 + 水平2种方向
    
        // 初始化线段树:所有节点类型为0
        build(1, 1, n);
    
        // 计算不合法方案ans:无法被垂直直线分隔的方案
        for (int i = 1; i < n; i++) {
            // 第i步:将x≤i的点(y=ord[1..i])设为类型2(强制为1)
            settyp(1, 1, n, ord[i], 2);
            // 累加状态2(0→1)和状态3(1→0):这些是无法被垂直分隔的方案
            ans = (ans + sgt[1][2] + sgt[1][3]) % mod;
            // 恢复为类型1(后续步骤继续累加)
            settyp(1, 1, n, ord[i], 1);
        }
    
        // 不合法方案×2(水平方向同理),从总和中减去
        ans = ans * 2 % mod;
        ans = (sum + mod - ans) % mod;  // 确保非负
        printf("%lld\n", ans);
    
        return 0;
    }
    

    核心流程拆解

    1. 预处理与总方案计算

    • 幂次预处理pw2[i] 存储 (2^i \mod \text{MOD}),用于快速计算子集数量(每个奶牛有“选/不选”两种状态)。
    • 总方案 sum:计算所有可被垂直或水平直线分隔的方案:
      • 垂直分隔:对每个 (i)((1 \leq i < N)),左侧 (i) 个奶牛和右侧 (N-i) 个奶牛各选非空子集,红左蓝右或红右蓝左(共 (2 \times (2^i - 1) \times (2^{N-i} - 1)) 种)。
      • 水平分隔同理,故总方案为垂直方案 × 2(水平方向)。

    2. 线段树状态设计

    线段树用于追踪区间内奶牛的队伍分配约束,核心状态:

    • 0:区间内所有奶牛均未被选中(或全为0)。
    • 1:区间内所有奶牛均被选中(或全为1)。
    • 2:区间内奶牛从0过渡到1(存在0→1的分界)。
    • 3:区间内奶牛从1过渡到0(存在1→0的分界)。
    • 4:无限制(可任意分配)。

    通过 pushup 函数合并子区间状态,快速计算整体约束。

    3. 不合法方案计算

    • 对于垂直分隔:当奶牛的 (x) 坐标与 (y) 坐标不完全单调时,存在无法被垂直直线分隔的方案。
    • 代码通过动态更新线段树,逐步将 (x \leq i) 的奶牛标记为“强制选中”(类型2),计算此时的不合法方案(状态2和3)。
    • 水平方向的不合法方案与垂直方向对称,故最终不合法方案 × 2。

    4. 最终答案

    通过容斥原理:答案 = 总方案 - 不合法方案,确保结果模 (10^9+7)。

    关键难点解析

    1. 容斥思想的应用

    直接计算合法方案复杂度过高,通过“总可能方案 - 不合法方案”简化问题,将难点转化为计算不合法方案。

    2. 线段树状态的高效合并

    线段树的5种状态设计精准捕捉了队伍分配的约束,pushup 函数通过组合子区间状态,在 (O(1)) 时间内完成合并,确保整体复杂度 (O(N \log N))。

    3. 对称性处理

    水平和垂直方向的不合法方案具有对称性,代码通过一次计算并 × 2 处理,避免重复实现。

    复杂度分析

    • 时间复杂度:(O(N \log N)),线段树构建 (O(N)),每次更新和查询 (O(\log N)),共 (O(N \log N))。
    • 空间复杂度:(O(N)),线段树大小为 (4 \times N),适配 (N=2 \times 10^5)。
    • 1

    信息

    ID
    4404
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者