题解:洛谷 P8091 Non-Transitive Dice

【题目来源】

洛谷:P8091 [USACO22JAN] Non-Transitive Dice B - 洛谷

【题目描述】

为了消磨牛棚里的时光,奶牛们喜欢玩简单的骰子游戏。其中一种游戏使用两个骰子 \(X\)\(Y\) 进行。两个骰子均被投掷,获胜的骰子是显示的数字较大的骰子。如果两者显示相同的数字,则重新投掷(只要持续打平,骰子可能会被重新投掷多次)。我们称骰子 \(X\) 击败骰子 \(Y\),如果骰子 \(X\) 比骰子 \(Y\) 更有可能赢得这局游戏。

考虑以下的 \(4\) 面骰子:

骰子 \(A\) 在各面上有数字 \(4\)\(5\)\(6\)\(7\)

骰子 \(B\) 在各面上有数字 \(2\)\(4\)\(5\)\(10\)

骰子 \(C\) 在各面上有数字 \(1\)\(4\)\(8\)\(9\)

这些骰子满足一个相当奇妙的性质:\(A\) 击败 \(B\)\(B\) 击败 \(C\),并且 \(C\) 也击败 \(A\)。特别地,三个骰子都不是「最佳的」,可以击败其他两个。在这种情况下,当没有两个骰子打平,也没有一个骰子是最佳的,我们称这三个骰子的集合为「非传递的」。在非传递的三个骰子的集合中,每个骰子击败一个其他骰子,并输给另一个其他骰子。

给定两个 \(4\) 面骰子 \(A\)\(B\) 各面上的数字,请帮助奶牛们求出是否有方法为第三个骰子 \(C\) 的各面分配数字,使得这个骰子的集合是非传递的。所有骰子面上的数字必须是 \(1\)\(10\) 的整数。

【输入】

每个测试用例包含多个独立的子测试用例,必须全部回答正确才能通过整个测试用例。输入的第一行包含\(T(1\le T\le 10)\),为你需要求解的子测试用例的数量。

以下 \(T\) 行,每行描述了一个子测试用例,包含 \(8\) 个整数:骰子 \(A\)\(4\) 面上的整数,以及骰子 \(B\)\(4\) 面上的整数。所有的数均在 \(1\)\(10\) 之间,不一定排序。可能同一个数会出现多次,即使在同一个骰子上也可能出现多个相同的数。

【输出】

输出 \(T\) 行。如果有可能为骰子 \(C\) 分配数字使得第 \(k\) 个测试用例成为一个非传递的骰子集合,则第k行输出 yes,否则输出 no

【输入样例】

3
4 5 6 7 2 4 5 10
2 2 2 2 1 1 1 1
1 1 1 1 2 2 2 2

【输出样例】

yes
no
no

【核心思想】

  1. 问题分析:给定两个 \(4\) 面骰子 \(A\)\(B\),判断是否存在第三个 \(4\) 面骰子 \(C\)(面值为 \(1\)\(10\) 的整数),使得三个骰子构成非传递关系:\(A\) 击败 \(B\)\(B\) 击败 \(C\)\(C\) 击败 \(A\)(或反向循环)。本质上是暴力枚举 + 胜负判定问题:枚举所有可能的 \(C\) 骰子组合,检查是否满足非传递条件。

  2. 算法选择

    • 胜负函数f(X, Y) 比较骰子 \(X\)\(Y\) 的所有 \(16\) 种面组合,统计 \(X\) 获胜次数是否大于 \(Y\)
    • 四重循环枚举\(C\) 骰子的 \(4\) 个面各从 \(1\)\(10\) 枚举,共 \(10^4 = 10000\) 种可能
  3. 关键步骤

    • 读取数据:读入 \(T\) 组测试数据,每组 \(8\) 个整数(\(A\)\(4\) 面 + \(B\)\(4\) 面)
    • f(x, y) 函数
      • 初始化 \(winx = 0, winy = 0\)
      • 枚举 \(i = 1\)\(4\)\(j = 1\)\(4\)
        • \(x[i] > y[j]\)\(winx++\)
        • \(x[i] < y[j]\)\(winy++\)
      • 返回 \(winx > winy\)
    • check(a, b) 函数
      • 四重循环枚举 \(C\)\(4\) 个面 \((i, j, k, l)\)
      • f(a, b) && f(b, c) && f(c, a),返回 true
      • f(b, a) && f(a, c) && f(c, b),返回 true
      • 否则返回 false
    • 输出 yesno
  4. 时间/空间复杂度

    • 时间复杂度:\(O(T \cdot 10^4 \cdot 16)\),每组数据枚举 \(10000\)\(C\),每种比较 \(16\)
    • 空间复杂度:\(O(1)\),固定大小的数组
  5. 暴力枚举的核心思想

    • 非传递性的两种方向:非传递可以是 \(A \to B \to C \to A\)\(A \to C \to B \to A\)(即 \(B \to A \to C \to B\)),需检查两种循环方向
    • 骰子胜负的定义\(X\) 击败 \(Y\) 当且仅当 \(X\) 的面值大于 \(Y\) 的面值的概率严格大于反向概率,即 \(winx > winy\)(平局不计入)
    • 枚举空间的可控性\(10^4\)\(C\) 组合在 \(T \leq 10\) 时完全可接受,无需剪枝优化
    • 对称性利用:两种循环方向只需改变比较顺序即可,无需重写逻辑
    • 适用于小范围枚举、博弈判定、非传递性验证类问题

【解题思路】

【算法标签】

普及- #模拟

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int t, a[5], b[5];
bool f(int x[], int y[])  // 定义确认获胜的函数
{
    int winx=0, winy=0;  // 定义x和y的获胜次数
    for (int i=1; i<=4; i++) {  // 依次遍历x和y的16个数字
        for (int j=1; j<=4; j++) {
            if (x[i]>y[j]) winx++;  // 两两进行比较,谁大的话,谁的获胜次数自增1
            if (x[i]<y[j]) winy++;
        }
    }
    return winx>winy;  // 如果x获胜次数更多,则返回true
}
bool check(int x[], int y[])  // 定义寻找c的函数,注意这里传递的参数为数组
{
    int z[5];  // 初始化z数组(即骰子C)
    for (int i=1; i<=10; i++) {  // 4个面,每个面都遍历1-10
        for (int j=1; j<=10; j++) {
            for (int k=1; k<=10; k++) {
                for (int l=1; l<=10; l++) {
                    z[1] = i;  // 定义z的4个数字
                    z[2] = j;
                    z[3] = k;
                    z[4] = l;
                    if (f(x, y) && f(y, z) && f(z, x)) return true;  // 如果满足x胜y,y胜z,z胜x,则返回true
                    if (f(y, x) && f(x, z) && f(z, y)) return true;  // 如果满足y胜x,x胜z,z胜y,则返回true
                }
            }
        }
    }
    return false;  // 如果都找不到,返回false
}
int main()
{
    cin >> t;  // 输入t
    while (t--) {  // 遍历t组数据
        for (int i=1; i<=4; i++) {  // 记录a的4个数据
            cin >> a[i];
        }
        for (int i=1; i<=4; i++) {  // 记录b的4个数据
            cin >> b[i];
        }
        if (check(a, b)) cout << "yes" << endl;  // 检查是否可以找到c,满足a胜b,b胜c,c胜a
        else cout << "no" << endl;
    }
    return 0;
}

【运行结果】

3
4 5 6 7 2 4 5 10
yes
2 2 2 2 1 1 1 1
no
1 1 1 1 2 2 2 2
no
posted @ 2026-08-17 22:49  团爸讲算法  阅读(12)  评论(0)    收藏  举报