题目解析 小Q和小X的游戏 | 豆包MarsCode AI 刷题

5 阅读6分钟

题目链接

小Q和小X的游戏

题目描述

小Q和小X是很好的朋友,她们正在玩一个游戏。她们拿到了一个数组,游戏开始时小Q随机选择一个元素作为起点。接着,两人轮流行动,小Q先行动。

每次行动时,当前玩家需要选择当前元素左边比它更小的元素,然后移动到该元素,接下来换另一方从这个元素继续移动。如果某一方无法进行合法的移动,则该方输掉游戏。

小Q想知道,在双方都采取最优策略的情况下,她最终获胜的概率是多少?请输出分数的最简形式,即分子和分母互素。如果小Q必胜,则输出 1/1;如果小Q必败,则输出 0/1


输入格式

  1. 整数 n 表示数组的大小。
  2. 数组 a,包含 n 个整数。

输出格式

  • 结果为一个分数,表示小Q获胜的概率,分子和分母互素。

测试样例

  • 示例 1

输入

n=5 a=[3, 1, 5, 4, 3]

输出

3/5

  • 示例 2

输入

n=6

a=[6, 2, 9, 7, 4, 3]

输出

2/3

  • 示例 3

输入 `

n=4 a=[8, 5, 6, 3]

输出

1/4


算法种类

  • 动态规划(DP) :通过递推计算每个状态是否为胜利状态。

思路分析

在理解这道题目时,首先关注到了它的游戏规则和获胜条件——小Q和小X轮流行动,每次只能向左移动到一个比当前位置数值小的元素。如果无法找到这样一个位置,当前玩家即为失败者。小Q作为先手,她的目标是尽可能找到路径,将小X逼入无法行动的状态。

这个游戏规则带有明显的博弈性质,双方会采取最优策略,这意味着在每个决策点,双方都会选择最有利于自己的行动——这里可以联想到动态规划的应用,因为每个位置的状态不仅影响当前位置的胜负,还直接依赖于更左边的状态。也就是说,当前位置的状态可以通过前面位置的状态递推得出。

进一步分析游戏规则,可以发现问题的本质是“胜负状态”的传递性——某个位置的状态依赖于其左侧比它小的元素的状态。如果小Q在当前位置能找到一个左侧状态为“小X必输”的位置,她就可以通过移动到这个位置让自己获胜。因此,这个问题可以看作是一个有序的决策过程,适合用动态规划的思想来解决。在这种情境下,我们可以通过递推法来逐步得出每个位置的胜负状态,直到整个数组的状态被填满。

于是——组来存储每个位置的状态,即小Q在从该位置开始是否能够获胜。这个布尔数组被初始化为全false,假设小Q在所有位置上都会失败。

然后,我们从左到右依次处理数组中的每个位置,对于每个位置,我们向左检查所有比当前元素小的位置。如果找到一个位置能让小X必输,则当前的位置状态可标记为小Q获胜。这样,当我们逐步填满数组后,最终就可以确定所有起点的状态。

统计结果时,我们只需遍历布尔数组,统计所有小Q获胜的起点数,再除以总的起点数,这样就可以得到小Q获胜的概率。为了确保输出为最简形式的分数,我们使用最大公约数将胜利起点数和总起点数化简。通过这个方法,我们将博弈问题转化为动态规划问题,并最终得出了最优策略下小Q的获胜概率。

每个位置的胜负情况取决于左侧元素的状态,也就是说,i 位置的状态可以根据左边的状态推导出。

如果我们知道小Q在某个左边位置 j 会输,并且可以从位置i移动到位置 j,则小Q可以利用这一点获胜。

我们可以使用一个数组存储每个位置的胜负状态,从而避免重复计算,这样可以有效提高效率。

动态规划详细步骤

我们用一个布尔数组 dp 来表示每个位置的状态。 dp[i] = true 表示小Q在位置 i 可以获胜 dp[i] = false 表示小Q在位置 i 会输

而且,确定好初始值的问题:默认情况下,dp[i] 初始化为 false,假设小Q在该位置会输。

然后是动态规划需要的状态转移方程设置:

  1. 对于每个位置 i,我们向左检查每个位置 j,前提是 a[j] < a[i]
  2. 如果在 i 左边存在一个位置 j,使得 a[j] < a[i]dp[j] = false,则 dp[i]true,表示小Q在位置 i 可以获胜,因为小Q可以把游戏引导到一个小X会输的位置。
  3. 一旦找到这样的 j,我们可以跳出循环,因为小Q在 i 位置已经确定能获胜,不需要继续查找其他左侧位置。

最终结果计算

遍历 dp 数组,统计 dp[i] = true 的位置总数,这个数表示小Q获胜的起点数。胜利的概率就是获胜起点数 winCount 除以总起点数 n

额外需要注意的是输出形式gcd 函数来计算化简分数


代码实现


import java.util.*;
import java.io.*;

public class Main {
    public static String solution(int n, int[] a) {

        boolean[] dp = new boolean[n];
        

        for (int i = 0; i < n; i++) {
            dp[i] = false; 
            for (int j = i - 1; j >= 0; j--) {
                if (a[j] < a[i] && !dp[j]) {
                    dp[i] = true;
                    break;
                }
            }
        }


        int winCount = 0;
        for (boolean win : dp) {
            if (win) winCount++;
        }


        int gcd = gcd(winCount, n);
        return (winCount / gcd) + "/" + (n / gcd);
    }

    private static int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
    public static void main(String[] args) {
        System.out.println(solution(5, new int[]{3, 1, 5, 4, 3}));
        System.out.println(solution(6, new int[]{6, 2, 9, 7, 4, 3}));
        System.out.println(solution(4, new int[]{8, 5, 6, 3})); 
    }
}

示例运行结果

对于输入 n=5, a=[3, 1, 5, 4, 3],程序输出 3/5
对于输入 n=6, a=[6, 2, 9, 7, 4, 3],程序输出 2/3
对于输入 n=4, a=[8, 5, 6, 3],程序输出 1/4


复杂度分析

时间复杂度

外层循环遍历数组,复杂度为$O(n) 内层循环检查左边的合法位置,最坏复杂度为O(n)

总体复杂度为 O(n2)O(n^2)



空间复杂度

使用了大小为 ndp 数组,空间复杂度为 O(n)


总结

动态规划,尤其是要考虑好转移方程的设置