Сегодня мы рассмотрим решение задачи 2116. Check if a Parentheses String Can Be Valid.
🧩 Описание задачи
Дана строка s
, состоящая только из символов '('
и ')'
, и строка locked
длины n
, состоящая из символов '0'
и '1'
. Каждая позиция в locked
указывает, можно ли изменить символ в s на этой позиции:
- Если
locked[i] == '1'
, символ s[i]
нельзя менять.
- Если
locked[i] == '0'
, символ s[i]
можно изменить на '('
или ')'
.
Нужно определить, можно ли сделать строку s
корректной скобочной записью.
💡 Идея
Решение основывается на двух проходах по строке:
- Прямой проход: Проверяем баланс открывающих и закрывающих скобок слева направо, учитывая символы, которые можно менять.
- Обратный проход: Инвертируем строку (меняем '('
на ')'
и наоборот) и проверяем баланс справа налево.
При каждом проходе используем два счётчика:
- balance
для отслеживания текущего баланса скобок.
- free
для отслеживания количества символов, которые можно изменить.
Если на любом этапе баланс становится отрицательным, и его нельзя компенсировать изменением свободных символов, строка не может быть сделана валидной.
🔧 Подробности подхода
-
Прямой проход:
- Проходим по строке слева направо, обновляя баланс и количество свободных символов.
- Если баланс становится отрицательным, используем свободные символы для его восстановления. Если свободных символов недостаточно, строка невалидна.
-
Обратный проход:
- Инвертируем строку: меняем
'('
на ')'
и наоборот.
- Проходим по строке справа налево с тем же алгоритмом.
-
Итераторы:
- Для упрощения логики используем итераторы для обработки строк. Прямой и обратный проходы оформлены как отдельные итераторы, которые генерируют пары (символ, locked).
-
Крайний случай:
- Строки нечётной длины нельзя сделать корректными ни при каких условиях.
⏱️ Асимптотика
- Временная сложность:
O(n)
– мы проходим по строке дважды (прямой и обратный проходы).
- Пространственная сложность:
O(1
– итераторы и счётчики используют постоянное количество памяти.
📝 Исходный код
impl Solution {
pub fn can_be_valid(s: String, locked: String) -> bool {
if s.len() % 2 != 0 {
return false;
}
let s_bytes = s.as_bytes();
let locked_bytes = locked.as_bytes();
// Forward pass iterator
let forward_iter = s_bytes.iter().zip(locked_bytes.iter());
// Forward pass
if !Self::check_balance(forward_iter) {
return false;
}
// Backward pass iterator
let backward_iter = s_bytes.iter().rev()
.zip(locked_bytes.iter().rev())
.map(|(char, lock)| {
let swapped_char = if *char == b'(' { &b')' } else { &b'(' };
(swapped_char, lock)
});
// Backward pass
if !Self::check_balance(backward_iter) {
return false;
}
true
}
fn check_balance<'a>(iter: impl Iterator<Item = (&'a u8, &'a u8)>) -> bool {
let mut balance = 0;
let mut free = 0;
for (&char, &lock) in iter {
if lock == b'0' {
free += 1; // Free to change
} else if char == b'(' {
balance += 1;
} else {
balance -= 1;
}
// Adjust balance using free parentheses
if balance < 0 {
if free == 0 {
return false;
}
balance += 1;
free -= 1;
}
}
balance <= free
}
}
Небезынтересно выглядит реализация этого же подхода на Go
– у них весьма своеобразно реализуются итераторы в неожиданном для Go функциональном стиле. В реальной же практике такое редко гошники практикуют, как я понимаю, — гораздо проще байты строки in-place инвертировать.
// CharLockPair represents a character and its corresponding lock state.
type CharLockPair struct {
char, lock rune
}
// Seq defines a functional iterator for yielding elements of type T.
type Seq[T any] func(func(T) bool)
// canBeValid checks if the given string can be a valid parentheses string.
func canBeValid(s, locked string) bool {
if len(s)%2 != 0 {
return false
}
// Forward pass
if !checkBalance(forwardIter(s, locked)) {
return false
}
// Backward pass
if !checkBalance(backwardIter(s, locked)) {
return false
}
return true
}
// checkBalance processes the iterator and determines if the sequence is balanced.
func checkBalance(seq Seq[CharLockPair]) bool {
balance, free := 0, 0
for pair := range seq {
char, lock := pair.char, pair.lock
if lock == '0' {
free++
} else if char == '(' {
balance++
} else {
balance--
}
if balance < 0 {
if free == 0 {
return false // Stop iteration early
}
balance++
free--
}
}
return balance <= free
}
// forwardIter generates an iterator for the forward pass.
func forwardIter(s, locked string) Seq[CharLockPair] {
return func(yield func(CharLockPair) bool) {
for i := 0; i < len(s); i++ {
if !yield(CharLockPair{char: rune(s[i]), lock: rune(locked[i])}) {
return
}
}
}
}
// backwardIter generates an iterator for the backward pass with swapped characters.
func backwardIter(s, locked string) Seq[CharLockPair] {
return func(yield func(CharLockPair) bool) {
for i := len(s) - 1; i >= 0; i-- {
char := rune(s[i])
if char == '(' {
char = ')'
} else {
char = '('
}
if !yield(CharLockPair{char: char, lock: rune(locked[i])}) {
return
}
}
}
}
ответить