Files
second-mind-aep/💼 Работа/Собеседования/Лето 2025/Задачи/🧮 Алгоритмическая задача для Middle разработчика.md
2025-08-13 14:53:57 +04:00

7.3 KiB
Raw Blame History

🧮 Алгоритмическая задача для Middle разработчика

📝 Задача: Планировщик встреч

Условие:

У вас есть список встреч, каждая встреча представлена как массив [start, end], где start и end - время начала и окончания в минутах от начала дня.

Нужно найти максимальное количество непересекающихся встреч, которые можно провести в одной переговорной комнате.

Примеры:

Пример 1:

Ввод: meetings = [[0,30],[5,10],[15,20]]
Вывод: 2
Объяснение: Можно выбрать встречи [5,10] и [15,20]

Пример 2:

Ввод: meetings = [[7,10],[2,4]]
Вывод: 2
Объяснение: Обе встречи не пересекаются

Пример 3:

Ввод: meetings = [[1,5],[8,9],[8,9],[5,9],[9,15]]
Вывод: 3
Объяснение: Можно выбрать [1,5], [8,9], [9,15]

Ограничения:

  • 1 <= meetings.length <= 10^4
  • meetings[i].length == 2
  • 0 <= starti < endi <= 10^6

Задание:

  1. Реализуйте метод public int maxMeetings(int[][] meetings)
  2. Объясните алгоритм и его сложность
  3. Напишите unit-тесты

Время на выполнение: 15 минут


Решение алгоритмической задачи

Показать решение

Основная идея:

Это классическая задача о максимальном количестве непересекающихся интервалов. Используем жадный алгоритм:

  1. Сортируем встречи по времени окончания
  2. Выбираем встречу с самым ранним окончанием
  3. Исключаем все пересекающиеся с ней встречи
  4. Повторяем для оставшихся встреч

Решение:

import java.util.*;

public class MeetingScheduler {
    
    public int maxMeetings(int[][] meetings) {
        if (meetings == null || meetings.length == 0) {
            return 0;
        }
        
        // Сортируем по времени окончания
        Arrays.sort(meetings, (a, b) -> Integer.compare(a[1], b[1]));
        
        int count = 0;
        int lastEndTime = -1;
        
        for (int[] meeting : meetings) {
            int startTime = meeting[0];
            int endTime = meeting[1];
            
            // Если текущая встреча не пересекается с предыдущей выбранной
            if (startTime >= lastEndTime) {
                count++;
                lastEndTime = endTime;
            }
        }
        
        return count;
    }
}

Объяснение алгоритма:

  1. Сортировка: Сортируем все встречи по времени окончания. Это ключевая идея - выбирая встречи с самым ранним окончанием, мы оставляем максимум времени для последующих встреч.

  2. Жадный выбор: Проходим по отсортированным встречам и выбираем те, которые не пересекаются с уже выбранными.

  3. Условие непересечения: Встреча не пересекается с предыдущей, если её время начала >= времени окончания предыдущей.

Временная сложность: O(n log n) - из-за сортировки

Пространственная сложность: O(1) - если не считать пространство для сортировки

Пошаговый пример:

Для meetings = [[0,30],[5,10],[15,20]]:

  1. После сортировки по времени окончания: [[5,10], [15,20], [0,30]]
  2. Выбираем [5,10], lastEndTime = 10
  3. Проверяем [15,20]: 15 >= 10 ✓, выбираем, lastEndTime = 20
  4. Проверяем [0,30]: 0 < 20 ✗, пропускаем
  5. Результат: 2 встречи

Для meetings = [[1,5],[8,9],[8,9],[5,9],[9,15]]:

  1. После сортировки по времени окончания: [[1,5], [8,9], [8,9], [5,9], [9,15]]
  2. Выбираем [1,5], lastEndTime = 5
  3. Проверяем [8,9]: 8 >= 5 ✓, выбираем, lastEndTime = 9
  4. Проверяем [8,9]: 8 < 9 ✗, пропускаем
  5. Проверяем [5,9]: 5 < 9 ✗, пропускаем
  6. Проверяем [9,15]: 9 >= 9 ✓, выбираем
  7. Результат: 3 встречи

Unit тесты:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class MeetingSchedulerTest {
    
    private final MeetingScheduler scheduler = new MeetingScheduler();
    
    @Test
    void testExample1() {
        int[][] meetings = {{0,30},{5,10},{15,20}};
        assertEquals(2, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testExample2() {
        int[][] meetings = {{7,10},{2,4}};
        assertEquals(2, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testExample3() {
        int[][] meetings = {{1,5},{8,9},{8,9},{5,9},{9,15}};
        assertEquals(3, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testEmptyInput() {
        int[][] meetings = {};
        assertEquals(0, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testNullInput() {
        assertEquals(0, scheduler.maxMeetings(null));
    }
    
    @Test
    void testSingleMeeting() {
        int[][] meetings = {{1,3}};
        assertEquals(1, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testAllOverlapping() {
        int[][] meetings = {{1,4},{2,5},{3,6}};
        assertEquals(1, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testNoOverlapping() {
        int[][] meetings = {{1,2},{3,4},{5,6}};
        assertEquals(3, scheduler.maxMeetings(meetings));
    }
    
    @Test
    void testTouchingMeetings() {
        int[][] meetings = {{1,3},{3,5},{5,7}};
        assertEquals(3, scheduler.maxMeetings(meetings));
    }
}

Почему жадный алгоритм работает:

Выбирая встречу с самым ранним окончанием, мы всегда оставляем максимально возможное время для размещения остальных встреч. Это оптимальная стратегия для данной задачи.

Доказательство корректности: Пусть OPT - оптимальное решение, а GREEDY - решение жадного алгоритма. Можно показать, что GREEDY не хуже OPT путем замены встреч в OPT на встречи из GREEDY без ухудшения результата.