算法讲解与题解分析
Topic: Simulation, Prefix/Suffix Sums, Ad-hoc
暴力解法?
尝试修改每一个位置 $i$ 为其他两种指令,然后重新模拟。
复杂度:$O(C^2)$。由于 $C \le 10^5$,这会超时。
优化思路:
我们需要 $O(C)$ 或 $O(C \log C)$ 的解法。
关键观察:牵一发而动全身。
修改指令主要导致后面的路径发生整体平移。
如果在位置 $i$ 本来是向左走(-1),现在改成向右走(+1),那么 $i$ 之后的所有位置都比原计划大了 2。
我们要预处理出这 5 种偏移下,后缀 (Suffix) 能击中多少目标。
| 原指令 | 新指令 | 位置变化 | 后续整体偏移 | Array Index |
|---|---|---|---|---|
| R (+1) | L (-1) | $-2$ | Left 2 | 0 |
| R (+1) / F (0) | F (0) / L (-1) | $-1$ | Left 1 | 1 |
| Any | Same | $0$ | No Change | 2 |
| L (-1) / F (0) | F (0) / R (+1) | $+1$ | Right 1 | 3 |
| L (-1) | R (+1) | $+2$ | Right 2 | 4 |
1. 离线预处理 (Suffix):
我们需要知道:“如果从现在开始,所有位置都平移 $K$,后面还能打中几个靶子?”
map<int, int> right[5]: 存储 5 种偏移情况下的击中统计。2. 模拟扫描 (Prefix):
set<int> left: 当前时刻之前已经击中的目标(去重)。left.size() + right[shift_index].size() (注意去重)。如果一个靶子在左边被击中了,它在右边就算再次被击中也不能重复得分。
// 如果当前是 'F' 指令,且真的击中了靶子
if (targets.count(p) && left[p] == 0) {
// 1. 将其加入左边已完成集合
left[p]++;
// 2. 从所有右边(未来)的可能列表中彻底移除这个靶子
// 这样后续计算 right.size() 时,就不会包含这个靶子了
for (int j = 0; j < 5; j++) right[j].erase(p);
}
这种“贪心移除”策略是 AC 的关键:一旦前面打中了,后面无论怎么移,这个靶子的贡献已经被计算在 res (即 left.size) 里了,所以从所有 future maps 中删掉。
#include <bits/stdc++.h>
using namespace std;
int t, c;
char s[100005];
set<int> targets;
// left: 记录左边(过去)实际击中的目标
map<int,int> left_hits;
// right[5]: 记录右边(未来)在不同偏移下击中的目标及次数
// 0:偏移-2, 1:偏移-1, 2:不变, 3:偏移+1, 4:偏移+2
map<int,int> right_hits[5];
int ans;
int main() {
scanf("%d %d", &t, &c);
for (int i = 0; i < t; i++) {
int p; scanf("%d", &p); targets.insert(p);
}
scanf("%s", s);
int p = 0;
// --- Step 1: 预处理所有未来的可能性 (Fill Suffixes) ---
for (int i = 0; i < c; i++) {
if (s[i] == 'L') p--;
if (s[i] == 'R') p++;
if (s[i] == 'F') {
// 记录如果当前位置偏移 k,能否击中目标
if (targets.count(p-2)) right_hits[0][p-2]++;
if (targets.count(p-1)) right_hits[1][p-1]++;
if (targets.count(p)) right_hits[2][p]++;
if (targets.count(p+1)) right_hits[3][p+1]++;
if (targets.count(p+2)) right_hits[4][p+2]++;
}
}
// --- Step 2: 模拟过程 (Sweep) ---
p = 0;
for (int i = 0; i < c; i++) {
int current_score = left_hits.size();
// 如果把当前指令改成F,能否击中新目标?(前提是左边没击中过)
int hit_if_change_to_F = (left_hits.count(p) == 0 && targets.count(p)) ? 1 : 0;
if (s[i] == 'L') {
// Case 1: L -> F (Shift +1)
// 分数 = 左边得分 + (如果F击中新目标) + 右边偏移+1后的得分
// 注意: right_hits[3] 可能会包含当前位置 p,需要小心处理(代码中使用 max 简化了覆盖逻辑)
// 这里使用题目特性的简化逻辑:如果right包含p且我们现在击中p,不重复算?
// 实际上 map.size() 统计的是独特的。如果 change to F 击中 p,
// 且 p 在 right[3] 中,那么 p 会被算两次吗?
// 你的代码逻辑:如果是F,如果right[3]有p,ans加的时候不加newhit?
// 实际上:如果 right[3] 有 p,说明未来也会击中 p。
if (right_hits[3].count(p))
ans = max(ans, current_score + (int)right_hits[3].size());
else
ans = max(ans, current_score + hit_if_change_to_F + (int)right_hits[3].size());
// Case 2: L -> R (Shift +2)
ans = max(ans, current_score + (int)right_hits[4].size());
p--; // Execute actual move L
}
else if (s[i] == 'R') {
// Case 1: R -> F (Shift -1)
if (right_hits[1].count(p))
ans = max(ans, current_score + (int)right_hits[1].size());
else
ans = max(ans, current_score + hit_if_change_to_F + (int)right_hits[1].size());
// Case 2: R -> L (Shift -2)
ans = max(ans, current_score + (int)right_hits[0].size());
p++; // Execute actual move R
}
else { // s[i] == 'F'
// 既然我们要走过这个 F 了,我们需要把这个 F 对未来的贡献从 right_hits 中移除
// 因为这些 hits 变成了 "过去式"
for (int j = 0; j < 5; j++)
if (right_hits[j].count(p+j-2)) {
if (--right_hits[j][p+j-2] == 0) right_hits[j].erase(p+j-2);
}
// 尝试修改 F
ans = max(ans, current_score + (int)right_hits[1].size()); // F -> L (Shift -1)
ans = max(ans, current_score + (int)right_hits[3].size()); // F -> R (Shift +1)
// 执行实际的 F 操作:如果击中,记录到 left_hits,并从所有 right maps 永久删除
if (targets.count(p) && left_hits.count(p) == 0) {
left_hits[p]++;
for (int j = 0; j < 5; j++) right_hits[j].erase(p);
}
}
}
// 最后考虑完全不改的情况
ans = max(ans, (int)left_hits.size());
printf("%d\n", ans);
return 0;
}