题目链接
题目描述
小Q和小X是很好的朋友,她们正在玩一个游戏。她们拿到了一个数组,游戏开始时小Q随机选择一个元素作为起点。接着,两人轮流行动,小Q先行动。
每次行动时,当前玩家需要选择当前元素左边比它更小的元素,然后移动到该元素,接下来换另一方从这个元素继续移动。如果某一方无法进行合法的移动,则该方输掉游戏。
小Q想知道,在双方都采取最优策略的情况下,她最终获胜的概率是多少?请输出分数的最简形式,即分子和分母互素。如果小Q必胜,则输出 1/1
;如果小Q必败,则输出 0/1
。
输入格式
- 整数
n
表示数组的大小。 - 数组
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在该位置会输。
然后是动态规划需要的状态转移方程设置:
- 对于每个位置
i
,我们向左检查每个位置j
,前提是a[j] < a[i]
。 - 如果在
i
左边存在一个位置j
,使得a[j] < a[i]
且dp[j] = false
,则dp[i]
为true
,表示小Q在位置i
可以获胜,因为小Q可以把游戏引导到一个小X会输的位置。 - 一旦找到这样的
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)
总体复杂度为
空间复杂度:
使用了大小为 n
的 dp
数组,空间复杂度为 O(n)
。
总结
动态规划,尤其是要考虑好转移方程的设置