题目信息
- 平台:LeetCode
- 题目:2211. 统计道路上的碰撞次数
- 难度:Medium
- 题目链接:2211. Count Collisions on a Road
题目描述
在一条无限长的公路上,有
n辆车,给你一个下标从0开始的字符串directions,长度为n。directions[i]可以是'L'、'R'或'S',分别表示第i辆车是向左、向右或静止。所有移动的车辆都以相同速度行驶。碰撞规则如下:
'R'和'L'相撞:碰撞次数增加 2,两车都变为'S'。- 移动车辆和
'S'相撞:碰撞次数增加 1,移动车辆变为'S'。计算所有碰撞发生后,总的碰撞次数。
算法分析
-
核心思想:识别并排除那些永远不会发生碰撞的车辆。
- 最左侧连续向左行驶的车辆(
L)永远不会与任何车辆碰撞,因为它们前方没有车。 - 最右侧连续向右行驶的车辆(
R)也永远不会与任何车辆碰撞,因为它们后方没有车。
- 最左侧连续向左行驶的车辆(
-
解题步骤:
- "修剪"字符串 :我们可以逻辑上(或实际上)移除
directions字符串开头所有的'L'和末尾所有的'R'。 - 统计碰撞 :在"修剪"后的中间部分,任何移动的车辆(即非
'S'的车辆)最终都将参与至少一次碰撞并变为静止。- 一个向右的
'R'会一直向右行驶,直到遇到一个'S'或一个向左的'L',然后停下来。 - 一个向左的
'L'会一直向左行驶,直到遇到一个'S'或一个已经停下的'R',然后停下来。
- 一个向右的
- 计算结果 :因此,中间部分中每个
'L'或'R'都会贡献 1 次碰撞。我们只需要统计这个中间区域中非'S'车辆的数量即可。
- "修剪"字符串 :我们可以逻辑上(或实际上)移除
-
时间复杂度:O(n),其中 n 是字符串的长度。我们需要遍历字符串来找到中间区域并进行计数。
-
空间复杂度:O(1)(如果使用指针/索引)或 O(n)(如果创建了新的子字符串)。
代码实现
方案一:指针/索引法 (C++)
cpp
class Solution {
public:
int countCollisions(string directions) {
int n = directions.size();
// 找到第一个不为 'L' 的车
int l = 0;
while (l < n && directions[l] == 'L')
++l;
// 找到最后一个不为 'R' 的车
int r = n - 1;
while (r >= l && directions[r] == 'R')
--r;
int ans = 0;
// 统计中间区域非 'S' 的车辆数
for (int i = l; i <= r; ++i) {
if (directions[i] != 'S')
++ans;
}
return ans;
}
};
方案二:字符串处理 (Python & C++20)
这种方法更简洁,直接利用语言的内置或库函数功能。
Python:
python
class Solution:
def countCollisions(self, directions: str) -> int:
# 移除前导 'L' 和后导 'R'
trimmed_directions = directions.lstrip("L").rstrip("R")
# 统计剩余部分中 'L' 和 'R' 的总数
return len(trimmed_directions) - trimmed_directions.count("S")
C++20 Ranges:
cpp
#include <string>
#include <algorithm>
#include <ranges>
class Solution {
public:
int countCollisions(std::string directions) {
// C++20 Ranges a bit verbose for this, but demonstrates the concept
auto view = directions | std::views::drop_while([](char c){ return c == 'L'; });
std::string temp;
std::ranges::copy(view, std::back_inserter(temp));
auto rview = temp | std::views::reverse | std::views::drop_while([](char c){ return c == 'R'; });
int count = 0;
for (char c : rview) {
if (c != 'S') {
count++;
}
}
return count;
}
};
方案三:查找边界 (Rust)
rust
impl Solution {
pub fn count_collisions(directions: String) -> i32 {
let s = directions.as_bytes();
let n = s.len();
let mut l = 0;
while l < n && s[l] == b'L' {
l += 1;
}
let mut r = n;
while r > l && s[r - 1] == b'R' {
r -= 1;
}
let mut count = 0;
for i in l..r {
if s[i] != b'S' {
count += 1;
}
}
count
}
}
方案四:单次遍历模拟 (Go)
这种方法不先"修剪"字符串,而是在一次遍历中通过状态机或计数器来模拟碰撞过程。
go
// 模拟: flag 记录右行车的数量
func countCollisions(directions string) int {
ans := 0
flag := -1 // -1: 没有遇到R; 0: 遇到S或碰撞后的静止车队; >0: 连续R的数量
for _, c := range directions {
if c == 'L' {
if flag >= 0 { // 如果左边有R车或S车
ans += flag + 1 // L与R车队碰撞(flag辆R+1辆L)或与S碰撞(1辆L)
flag = 0 // 碰撞后形成静止车队
}
} else if c == 'S' {
if flag > 0 { // R车队撞上S
ans += flag
}
flag = 0 // 形成或加入静止车队
} else { // c == 'R'
if flag >= 0 {
flag++
} else {
flag = 1
}
}
}
return ans
}
总结与反思
- 问题简化:此题的关键在于正确地简化问题模型。通过识别问题的"边界条件"(即永不碰撞的车辆),可以将一个看似复杂的模拟问题转化为一个简单的计数问题(如方案一、二、三)。这是解决此类问题的常用技巧。
- 多种实现:同样的核心逻辑可以通过不同的编程范式实现。Python 的字符串处理函数提供了非常简洁的写法。C++ 和 Rust 的指针/索引法则提供了更底层的控制。C++20 的 Ranges 库也提供了声明式的处理方式,尽管在本例中可能比传统循环更冗长。
- 模拟法对比 :直接的单次遍历模拟(如方案四的 Go 实现)是另一种有效的思路。它不依赖于先"修剪"掉永不碰撞的车辆,而是在遍历过程中动态计算碰撞。这种方法通常需要维护更复杂的状态(如此处的
flag变量),但避免了多次遍历或创建子字符串。对于此题,"修剪"法在逻辑上更直观、代码更简单。