题解:洛谷 P2098 Team Building

【题目来源】

洛谷:P2098 [USACO16DEC] Team Building P - 洛谷

【题目描述】

每年,Farmer John 都会带着他的 \(N\) 头奶牛参加州展览会的“最佳展示”比赛。他的劲敌 Farmer Paul 也会带着他的 \(M\) 头奶牛参加比赛(\(1 \leq N \leq 1000, 1 \leq M \leq 1000\))。

参加比赛的 \(N + M\) 头奶牛每头都会获得一个单独的整数得分。然而,今年的最终比赛将由 \(K\) 头奶牛组成的团队决定(\(1 \leq K \leq 10\)),规则如下:

Farmer John 和 Farmer Paul 各自选择 \(K\) 头奶牛组成团队进行比赛。这两个团队的奶牛将按得分高低配对:

FJ 团队中得分最高的奶牛与 FP 团队中得分最高的奶牛配对,FJ 团队中得分第二高的奶牛与 FP 团队中得分第二高的奶牛配对,依此类推。如果在每一对中,FJ 的奶牛得分都更高,那么 FJ 获胜。

请帮助 FJ 计算他和 FP 可以选择团队的不同方式的数量,使得 FJ 能够赢得比赛。也就是说,每个不同的(FJ 的 \(K\) 头奶牛集合,FP 的 \(K\) 头奶牛集合)对,只要 FJ 获胜,都应被计入。输出结果对 \(1\,000\,000\,009\) 取模。

【输入】

输入的第一行包含 \(N\)\(M\)\(K\)\(K\) 的值不会超过 \(N\)\(M\)

第二行包含 FJ 的 \(N\) 头奶牛的得分。

第三行包含 FP 的 \(M\) 头奶牛的得分。

【输出】

输出 FJ 和 FP 可以选择团队的方式数量,使得 FJ 获胜,结果对 \(1\,000\,000\,009\) 取模。

【输入样例】

10 10 3
1 2 2 6 6 7 8 9 14 17
1 3 8 10 10 16 16 18 19 19

【输出样例】

382

【核心思想】

  1. 问题分析:FJ 有 \(N\) 头奶牛,FP 有 \(M\) 头奶牛,各自选择 \(K\) 头组成团队。两团队的奶牛按得分从高到低一一配对,若每一对中 FJ 的奶牛得分都严格大于 FP 的奶牛得分,则 FJ 获胜。求 FJ 和 FP 选择团队的不同方式数量(对 \(10^9 + 9\) 取模)。这是一个三维线性DP + 容斥原理问题。

  2. 算法选择

    • 降序排序(Descending Sort):将 FJ 和 FP 的得分分别按降序排序,使得 \(a[i]\)\(b[j]\) 分别是当前考虑范围内的最小元素
    • 三维DP(3D Dynamic Programming)\(dp[i][j][k]\) 表示从 FJ 的前 \(i\) 头奶牛和 FP 的前 \(j\) 头奶牛中,选出 \(k\) 对且 FJ 每对都获胜的方案数
    • 容斥原理(Inclusion-Exclusion Principle):在状态转移时,用容斥原理处理"不选 FJ 的第 \(i\) 头"和"不选 FP 的第 \(j\) 头"两种情况的重复计数问题
  3. 关键步骤

    • 初始化
      • 读入 \(N\)\(M\)\(K\),以及 FJ 的得分数组 \(a[1..N]\) 和 FP 的得分数组 \(b[1..M]\)
      • sort(a + 1, a + n + 1, greater<int>()):FJ 得分降序排序
      • sort(b + 1, b + m + 1, greater<int>()):FP 得分降序排序
      • 边界条件\(dp[i][j][0] = 1\)(对于所有 \(0 \leq i \leq N\)\(0 \leq j \leq M\),选 \(0\) 对的方案数为 \(1\)
    • 状态转移(枚举团队大小 \(k\)\(1\)\(K\)):
      • 枚举 \(i\)\(k\)\(N\)(FJ 至少需要 \(k\) 头)
      • 枚举 \(j\)\(k\)\(M\)(FP 至少需要 \(k\) 头)
      • 容斥转移(先计算不考虑 \(a[i]\)\(b[j]\) 的方案数):
        • \(dp[i][j][k] = dp[i-1][j][k]\)(不选 FJ 的第 \(i\) 头)
        • \(dp[i][j][k] += dp[i][j-1][k]\)(不选 FP 的第 \(j\) 头)
        • \(dp[i][j][k] -= dp[i-1][j-1][k]\)(容斥去重,两者都不选的情况被重复计算)
        • 注意取模:\((x + mod) \% mod\) 防止负数
      • 配对转移(若 \(a[i] > b[j]\),可将它们作为第 \(k\) 对):
        • \(dp[i][j][k] += dp[i-1][j-1][k-1]\)(从前 \(i-1\)\(j-1\) 中选 \(k-1\) 对,再加上当前这对)
    • 输出答案\(dp[N][M][K]\)(从全部 \(N\) 头和 \(M\) 头中选 \(K\) 对的方案数)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \times M \times K)\),三维枚举,\(K \leq 10\)\(N, M \leq 1000\)
    • 空间复杂度:\(O(N \times M \times K)\),三维DP数组,可通过滚动数组优化至 \(O(N \times M)\)
  5. 三维DP + 容斥原理的核心思想

    • 降序排序的妙处:排序后 \(a[i]\)\(b[j]\) 是当前考虑范围内的最小值。若 \(a[i] > b[j]\),则对于任意 \(p \leq i\)\(q \leq j\) 都有 \(a[p] \geq a[i] > b[j] \geq b[q]\),保证配对的单调性
    • 容斥去重技巧:在计算"从 \([1..i]\)\([1..j]\) 中选 \(k\) 对"时,直接考虑"不选 \(a[i]\)"和"不选 \(b[j]\)"会重复计算"两者都不选"的情况,因此需要减去 \(dp[i-1][j-1][k]\)
    • 配对条件的处理:只有在 \(a[i] > b[j]\) 时才允许将 \(a[i]\)\(b[j]\) 配成一对,保证 FJ 获胜。由于降序排列,当前这对的 FJ 奶牛是剩余中最弱的,若能赢过 FP 最弱的,则前面所有配对也一定满足条件
    • 三维状态设计\(dp[i][j][k]\) 同时记录 FJ 前 \(i\) 头、FP 前 \(j\) 头、已选 \(k\) 对三个维度,保证状态无后效性
    • 适用于组合计数、匹配方案数、带约束的选对方案类问题

【算法标签】

普及+ #线性DP-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long           // 将 int 定义为 long long,防止模运算溢出
const int N = 1005, mod = 1e9 + 9;  // N: 最大奶牛数量, mod: 模数
int n, m, K;                    // n: FJ的奶牛数, m: FP的奶牛数, K: 团队大小
int a[N], b[N];                 // a[i]: FJ第i头奶牛的得分, b[i]: FP第i头奶牛的得分
// dp[i][j][k]: 从FJ的前i头奶牛和FP的前j头奶牛中,选出k对使得FJ每对都获胜的方案数
// 注意:a和b已按降序排序,所以a[i]是FJ前i头中最小的,b[j]是FP前j头中最小的
int dp[N][N][15];

signed main()                   // 使用 signed 替代 int,因为 #define int long long
{
    cin >> n >> m >> K;         // 读入FJ奶牛数、FP奶牛数、团队大小

    // 读入FJ的N头奶牛得分
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    // 读入FP的M头奶牛得分
    for (int i = 1; i <= m; i++)
        cin >> b[i];

    // 将FJ和FP的得分按降序排序
    // 排序后,a[i] >= a[i+1],b[i] >= b[i+1]
    // 这样dp[i][j]中,a[i]和b[j]分别是当前考虑的最小元素
    sort(a + 1, a + n + 1, greater<int>());
    sort(b + 1, b + m + 1, greater<int>());

    // 初始化:选0对时,方案数为1(什么都不选也是一种方案)
    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= m; j++)
            dp[i][j][0] = 1;

    // 动态规划:枚举团队大小k,FJ前i头,FP前j头
    for (int k = 1; k <= K; k++)
    {
        for (int i = k; i <= n; i++)      // i至少为k(FJ至少需要k头奶牛)
        {
            for (int j = k; j <= m; j++)  // j至少为k(FP至少需要k头奶牛)
            {
                // 容斥原理:计算不考虑a[i]和b[j]时的方案数
                // dp[i-1][j][k]: 不选FJ的第i头奶牛
                // dp[i][j-1][k]: 不选FP的第j头奶牛
                // 两者相加后,dp[i-1][j-1][k]被重复计算了,需要减去
                dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j][k]) % mod;
                dp[i][j][k] = (dp[i][j][k] + dp[i][j - 1][k]) % mod;
                dp[i][j][k] = (dp[i][j][k] - dp[i - 1][j - 1][k] + mod) % mod;

                // 如果FJ的第i头奶牛得分 > FP的第j头奶牛得分
                // 则可以将它们配成一对(作为第k对,且FJ获胜)
                // 加上从前面i-1和j-1中选k-1对的方案数
                if (a[i] > b[j])
                {
                    dp[i][j][k] = (dp[i][j][k] + dp[i - 1][j - 1][k - 1]) % mod;
                }
            }
        }
    }

    // 输出答案:从FJ的n头和FP的m头中选K对,FJ获胜的方案数
    cout << dp[n][m][K] << endl;

    return 0;
}

【运行结果】

10 10 3
1 2 2 6 6 7 8 9 14 17
1 3 8 10 10 16 16 18 19 19
382
posted @ 2026-06-25 19:14  团爸讲算法  阅读(9)  评论(0)    收藏  举报