2.3. Демонстрация - Разбор задачи валидации скобочных выражений
Шаг 1
На примере этой задачи можно понять, насколько правильный подход к подготовке данных для задачи имеет вес в производительности и простоте решения. Офис X работает по правилам коворкинга: каждый сотрудник может работать из дома или приходить в офис. В спокойные дни в офисе тихо, приходят пара-тройка людей. А ближе к релизам/отчетным периодам/другим завалам людей гораздо больше, но все сотрудники всё равно никогда не выходят. Начальство хочет какое-то время понаблюдать, сколько же человек максимум приходят в офис, чтобы сократить количество «посадочных мест» и меньше платить за аренду. Для этого они наладили систему, которая фиксирует время захода и выхода людей из офиса, и планируют из этих данных получить максимальное количество присутствующих сотрудников в день. Система эта сохраняет нужные данные в формате [времяВхода, времяВыхода] для каждого сотрудника. Если люди входят и выходят одновременно, считается, что выход происходит раньше (уходящий человек уже встал с места и освободил его, а входящий ещё не успел занять). Ваша задача — написать функцию, получающую из информации в системе максимальное количество одновременно находящихся сотрудников в офисе. Если вы ещё не решали эту задачу, остановитесь и попробуйте сами.
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function count(input) {
return 0;
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
Шаг 2
Если оперировать таким набором данных, то решения будут довольно сложными как алгоритмически, так и в целом для понимания. Например, можно создать объект, в котором ключи будут конкретным моментом времени, а значения — количеством людей в этот момент. Заполнять этот объект довольно просто: будем брать каждый интервал и в цикле от его начала до его конца станем прибавлять 1 к каждому моменту времени, а потом просто найдём ключ с максимальным значением. Но! Если время хранится в миллисекундах, как это чаще всего и делается, то на 12 часов открытого офиса будет приходиться 43200000 ключей! Расходы памяти просто огромнейшие, да и по времени заполнения это решение очень медленное.
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function count(input) {
const logs = {};
for (const [startTime, endTime] of input) {
for (let i = startTime; i < endTime; i++) {
logs[i] = logs[i] ? logs[i] + 1 : 1;
}
}
return Math.max(...Object.values(logs), 0);
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
Шаг 3
А если взглянуть на эту задачу под немного другим углом? Например, условный охранник, который хочет потягаться с новой системой учёта, решал бы эту задачу совершенно другим образом. По мере пропуска людей через турникеты на входе можно просто вести учёт людей, находящихся в офисе в каждый конкретный момент времени. Если человек заходит, прибавляем к текущему числу единицу, выходит — отнимаем. Охраннику всё равно, входил в этот момент Паша, Игорь или Женя. Ему важно, что кто-то когда-то вошёл, а кто-то когда-то вышел. Он не связывает эти события в пары, как это делает система. Тогда и максимум можно обновлять по мере продвижения людей: если вошло больше людей, чем было до этого, обновляем, если нет — не трогаем.
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function count(input) {
return 0;
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
Шаг 4
Если перекладывать это решение в код, то для начала нам нужно преобразовать данные в такой формат событий «прохода через турникеты». Например, возьмём следующую структуру объекта:
{
time: 0,
isEntering: true, // если в этот момент человек входил — true, выходил — false
}
И перегоним наблюдения системы в него. Это займет у нас O(n) времени.
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function count(input) {
const entries = [];
for (const [enteringTime, leavingTime] of input) {
entries.push({
time: enteringTime,
isEntering: true,
});
entries.push({
time: leavingTime,
isEntering: false,
})
}
return 0;
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
Шаг 5
Так как данные в системе у нас никак не сортированы по времени, отсортируем их. Сортировка, как мы помним (но пока не понимаем), заняла у нас O(nlogn).
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function compareEntries(left, right) {
// Выход раньше входа в то же время
if (left.time === right.time) {
return left.isEntering ? 1 : -1;
}
return left.time - right.time;
}
function count(input) {
const entries = [];
for (const [enteringTime, leavingTime] of input) {
entries.push({
time: enteringTime,
isEntering: true,
});
entries.push({
time: leavingTime,
isEntering: false,
})
}
entries.sort(compareEntries);
return 0;
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
Шаг 6
Теперь просто пройдёмся «глазами охранника» по этому массиву и посчитаем максимальное количество людей. И это тоже заняло у нас O(n) времени. Попробуйте посчитать общую сложность алгоритма сами. Если у вас получилось O(nlogn), то вы абсолютно правы! Если нет — ничего страшного, просто повторите правила оценивания алгоритмов из первого раздела.
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function compareEntries(left, right) {
// Выход раньше входа в то же время
if (left.time === right.time) {
return left.isEntering ? 1 : -1;
}
return left.time - right.time;
}
function count(input) {
const entries = [];
for (const [enteringTime, leavingTime] of input) {
entries.push({
time: enteringTime,
isEntering: true,
});
entries.push({
time: leavingTime,
isEntering: false,
})
}
entries.sort(compareEntries);
let currentCount = 0;
let maximumCount = 0;
for (const {isEntering} of entries) {
currentCount += isEntering ? 1 : -1;
maximumCount = Math.max(currentCount, maximumCount);
}
return maximumCount;
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
Шаг 7
Эта задача очень хорошо показывает, что правильно выбранная структура для хранения данных — ключ к простому решению задачи. Если у вас возникают трудности при решении той или иной рабочей задачи — попробуйте взглянуть на неё с другой стороны. Возможно, данные в том формате, в котором они даны, не совсем подходят для какой-то конкретной задачи. Попробуйте абстрагироваться от них и подумать, какое представление данных позволило бы прийти к решению максимально просто.
import {test} from "./tester.js"
/*
Примеры:
count([]) // 0
count([[1, 5], [5, 10]]) // 1
count([[1, 5], [0, 1], [4, 5]]) // 2
count([[1, 10], [5, 6], [2, 3], [7, 8]]) // 2
count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]) // 4
*/
function compareEntries(left, right) {
// Выход раньше входа в то же время
if (left.time === right.time) {
return left.isEntering ? 1 : -1;
}
return left.time - right.time;
}
function count(input) {
const entries = [];
for (const [enteringTime, leavingTime] of input) {
entries.push({
time: enteringTime,
isEntering: true,
});
entries.push({
time: leavingTime,
isEntering: false,
})
}
entries.sort(compareEntries);
let currentCount = 0;
let maximumCount = 0;
for (const {isEntering} of entries) {
currentCount += isEntering ? 1 : -1;
maximumCount = Math.max(currentCount, maximumCount);
}
return maximumCount;
}
test(count([]), 0)
test(count([[1, 5], [5, 10]]), 1)
test(count([[1, 5], [0, 1], [4, 5]]), 2)
test(count([[1, 10], [5, 6], [2, 3], [7, 8]]), 2)
test(count([[1, 2], [1, 10], [4, 9], [8, 15], [5, 6], [8, 16]]), 4)
File tester.js
const results = document.getElementById('results')
export function test(left, right) {
const result = document.createElement('li');
if (left === right) {
result.innerHTML = "Тест пройден!";
result.style.color = "green";
} else {
result.innerHTML = `Тест не пройден! ${left} не равно ${right}`;
result.style.color = "red";
}
results.appendChild(result);
}