H. Gambling - 题解
比赛与标签
比赛: Codeforces Round 799 (Div. 4) 标签: data structures, dp, greedy, math 难度: *1700
题目大意喵~
你好呀,指挥官~!这道题是说,我们来到了一个神奇的赌场,喵。游戏规则是这样的:
- 我们先选定一个数字
a
,还有一个游戏区间[l, r]
。 - 然后从第
l
轮玩到第r
轮,每一轮我们都猜数字a
。 - 赌场会公布每一轮的真实结果
x_i
。 - 如果我们猜对了(
a == x_i
),我们的钱就会翻倍! - 如果我们猜错了(
a != x_i
),我们的钱就会减半...呜。
我们从 1 美元开始,并且我们能预知未来,知道接下来 n
轮的所有结果 x_1, x_2, ..., x_n
。我们的任务就是,选择最优的 a
, l
, r
,让我们在游戏结束时,手里的钱最多最多!然后把这三个数告诉 Marian 就好啦,喵~
解题思路的探索之旅~
这道题看起来有点复杂,但别担心,跟着本喵的思路一步步来,很快就能解开谜题的!
第一步:把乘法变加法喵!
我们最终的钱数是 1 * 2 * 2 / 2 * 2 ...
这样的形式。这其实就是 2
的次幂,对吧? 假设在 [l, r]
这个区间里,我们猜对了 k
次,那么猜错了 (r - l + 1) - k
次。 最后的钱就是 1 * (2^k) * ( (1/2)^(r-l+1-k) )
,也就是 2^(k - (r-l+1-k))
,化简一下就是 2^(2k - (r - l + 1))
。
要想让最后的钱最多,我们其实就是要让指数 2k - (r - l + 1)
最大化!这样问题就从一个复杂的乘法问题,变成了一个我们更熟悉的加法/减法最优化问题了,喵~
第二步:聪明的选择a
我们应该选哪个数字作为 a
呢?如果选一个在数组 x
中根本没出现过的数字,那我们一次都猜不对,k=0
,指数就变成了 -(r-l+1)
,肯定是个负数,钱会变得非常少。所以,明智的 a
一定是数组 x
中出现过的某个数字!
这启发我们,可以遍历数组 x
中所有出现过的不同的数字,把它们都当作 a
的候选。对于每一个候选的 a
,我们都去找到能让 2k - (r - l + 1)
最大的那个区间 [l, r]
。最后再从所有候选 a
的最优结果里,选一个最好的,就是全局最优解啦!
第三步:关键の喵喵拳!(核心洞察)
好,现在我们固定了一个要猜的数字,比如说 v
。怎么为它找到最好的区间 [l, r]
呢?
让我们把问题再变个形!定义一个新的价值数组 b
:
- 如果
x_i == v
,那么b_i = 1
。 - 如果
x_i != v
,那么b_i = -1
。
为什么这么定义呢?因为 sum(b_i for i in [l, r])
等于 (v 在 [l,r] 中的数量) - (非v在 [l,r] 中的数量)
。 而我们的目标 2k - (r-l+1)
,其中 k
是 v
的数量, (r-l+1)
是总数量。 2k - (r-l+1) = k - ((r-l+1) - k) = (v 的数量) - (非v的数量)
。 看!目标得分 2k - (r-l+1)
正好等于 [l, r]
区间内 b
数组的和!
问题就变成了:对于每个 v
,构建一个 b
数组,然后求 b
数组的最大子数组和!
但是,如果每次都为每个不同的 v
构建一个 O(n)
的 b
数组,再用 O(n)
的 Kadane 算法求最大子数组和,总时间复杂度会是 O(不重复数字个数 * n)
,在最坏情况下是 O(n^2)
,这可不行呀,会超时的说!
第四步:究极进化! O(n) 的解法
我们必须找到更快的办法。让我们再深入思考一下。 对于一个固定的 v
,假设它的最优区间是 [l, r]
。
- 如果
x_l != v
,那么b_l = -1
。我们考虑一下新区间[l+1, r]
,它的得分会比原来高1
(-b_l
)!所以,一个最优的区间,它的左端点l
必须满足x_l == v
。 - 同理,如果
x_r != v
,那么b_r = -1
。新区间[l, r-1]
的得分会比原来高1
。所以最优区间的右端点r
也必须满足x_r == v
。
这个发现太重要了!这意味着,我们只需要在那些 v
出现的位置中寻找 l
和 r
就行了!
设 v
在原数组中出现的位置(0-indexed)为 p_0, p_1, ..., p_{m-1}
。我们要找的,就是一个区间 [p_j, p_i]
(其中 j <= i
),使得它的得分最高。
这个区间的得分为: Score = 2 * (v的数量) - (区间长度)
v
的数量是 i - j + 1
。 区间长度是 p_i - p_j + 1
。 Score = 2 * (i - j + 1) - (p_i - p_j + 1)
整理一下这个式子,把和 i
相关的项放一起,和 j
相关的放一起: Score = (2i - p_i) - (2j - p_j) + 1
为了让这个分数最大化,对于一个固定的 i
(右端点),我们需要找到一个 j <= i
(左端点),使得 (2j - p_j)
的值最小。
这就可以用一个类似动态规划的思路来解决啦!
- 用一个
map
把所有数字出现的位置都存起来。 - 遍历
map
中的每一个数字v
和它的位置列表p
。 - 对于
p
列表,我们遍历i
from0
tom-1
:- 计算
H_i = 2i - p_i
。 - 同时维护一个
min_H_j
,表示i
之前见过的所有H_j
的最小值。 - 当前以
p_i
为结尾的最优得分为H_i - min_H_j + 1
。 - 用这个得分更新我们为
v
找到的局部最优解。
- 计算
- 所有
v
都处理完后,我们就得到了全局最优解!
整个过程,我们只遍历了每个数字出现的位置一次。所有位置加起来就是 n
。所以总的时间复杂度是 O(n)
的说!完美解决!
代码实现喵~
下面就是实现这个思路的AC代码啦,本喵加了详细的注释,希望能帮到你!
#include <iostream>
#include <vector>
#include <map>
#include <climits>
using namespace std;
void solve() {
int n;
cin >> n;
// 用一个 map 来存储每个数字出现的所有位置(0-indexed)
// key 是数字本身,value 是一个存储了所有出现位置的 vector
map<int, vector<int>> indices;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
indices[x].push_back(i);
}
long long global_best_score = -1; // 全局最高分
int global_a = -1, global_l = -1, global_r = -1; // 对应的 a, l, r
// 遍历 map 中每一个独特的数字和它的位置列表
for (auto const& [val, positions] : indices) {
int m = positions.size();
long long current_best_score = 0; // 当前数字 val 的最高分
int current_l = -1, current_r = -1; // 当前数字 val 对应的 l, r
// 我们要最大化 (2*i - p_i) - (2*j - p_j) + 1
// 这等价于最大化 H_i - min(H_j) + 1,其中 H_k = 2k - p_k
// 我们用 min_val_prefix 来记录 min(2*j - p_j)
// min_val_prefix_pos 记录是哪个 p_j 产生了最小值
long long min_val_prefix = 2LL * 0 - positions[0];
int min_val_prefix_pos = positions[0];
// 初始情况,只选第一个位置,区间 [p_0, p_0]
current_best_score = 1;
current_l = positions[0];
current_r = positions[0];
// 从第二个位置开始遍历
for (int i = 1; i < m; i++) {
long long current_h = 2LL * i - positions[i];
// 计算以 p_i 为右端点的得分
// H_i - min_H_j + 1
long long score = current_h - min_val_prefix + 1;
// 如果这个得分比我们为 val 找到的最好得分还要高
if (score > current_best_score) {
current_best_score = score;
current_l = min_val_prefix_pos;
current_r = positions[i];
}
// 更新我们见过的 H_j 的最小值
if (current_h < min_val_prefix) {
min_val_prefix = current_h;
min_val_prefix_pos = positions[i];
}
}
// 如果当前这个数字 val 产生的最优解比全局最优解还好,就更新全局最优解
if (current_best_score > global_best_score) {
global_best_score = current_best_score;
global_a = val;
global_l = current_l;
global_r = current_r;
}
}
// 输出结果,注意题目要求是 1-indexed,所以要 +1
cout << global_a << " " << global_l + 1 << " " << global_r + 1 << "\n";
}
int main() {
// 加上这两行可以让 cin/cout 跑得飞快哦~
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
注:这里提供了一份逻辑更清晰的AC代码实现,它严格遵循了解题思路中推导出的 H(i) - H(j) + 1
公式,可能比原题解代码更容易理解。两份代码的核心思想是一致的,都能通过本题。
复杂度分析
- 时间复杂度: O(N) 的说。其中
N
是所有测试用例的n
的总和。我们首先用O(N)
的时间遍历所有输入来填充map
。之后,我们遍历map
中的每个键值对。由于map
中所有positions
向量的长度之和恰好是N
,而我们对每个positions
向量只进行一次线性扫描,所以这部分的总时间也是O(N)
。 - 空间复杂度: O(N) 的说。在最坏的情况下,如果数组中所有数字都不同,
map
需要存储N
个条目,每个条目带一个大小为1的向量,总空间是O(N)
。
知识点与总结
这道题真是一次有趣的冒险呐!它融合了好几种算法思想:
- 问题转化: 核心在于将最大化
2
的幂次,转化为最大化一个线性表达式2k - (r-l+1)
。这是解决许多数学和博弈问题的第一步! - 贪心/剪枝: “最优区间的端点一定是我们要猜的数
v
” 这个发现是解题的关键。它极大地缩小了搜索范围,是典型的通过分析问题性质进行的有效剪枝。 - 动态规划思想:
H_i - min(H_j) + 1
的计算方式,是典型的动态规划模式,和著名的**Kadane算法(最大子数组和)**有着异曲同工之妙。我们通过维护一个前缀最小值,来快速计算以当前点为结尾的最优解。 - 数据结构:
std::map
在这里是分组的好帮手,能高效地把相同值的元素聚合在一起,方便我们后续处理。
希望这篇题解能帮助你理解这道题的精髓!继续加油,你超棒的,喵~!