## 📝 Задача: Система управления событиями ### **Условие:** Вы разрабатываете систему для управления событиями (конференции, вебинары, встречи). Нужно реализовать сервис регистрации участников с учетом следующих требований: 1. **Событие** имеет: - ID, название, описание - Дату начала и окончания - Максимальное количество участников - Статус (DRAFT, PUBLISHED, CANCELLED) 2. **Участник** имеет: - ID, имя, email - Дату регистрации 3. **Бизнес-правила:** - Регистрация возможна только на опубликованные события - Нельзя зарегистрироваться на событие, которое уже началось - Нельзя превысить лимит участников - Один участник может зарегистрироваться на событие только один раз - При отмене события нужно уведомить всех участников ### **Техническое задание:** Реализуйте следующие компоненты: 1. **Entity классы** (Event, Participant, Registration) 2. **Repository интерфейсы** 3. **Service класс EventRegistrationService** с методами: - `registerParticipant(Long eventId, Long participantId)` - `cancelRegistration(Long eventId, Long participantId)` - `cancelEvent(Long eventId)` - `getEventParticipants(Long eventId)` 4. **REST Controller** с endpoint'ами для регистрации 5. **Обработка исключений** для всех бизнес-правил ### **Дополнительные требования:** - Использовать Spring Boot, JPA/Hibernate - Добавить валидацию данных - Написать unit-тесты для сервиса - Подумать о транзакциях и concurrency **Время на выполнение:** 25 минут --- # 🧮 Алгоритмическая задача для Middle разработчика ## 📝 Задача: Планировщик встреч ### **Условие:** У вас есть список встреч, каждая встреча представлена как массив `[start, end]`, где `start` и `end` - время начала и окончания в минутах от начала дня. Нужно найти максимальное количество непересекающихся встреч, которые можно провести в одной переговорной комнате. ### **Примеры:** **Пример 1:** ``` Ввод: meetings = [[0,30],[5,10],[15,20]] Вывод: 2 Объяснение: Можно выбрать встречи [0,30] и [15,20] ИЛИ [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. Entity классы:** ```java @Entity @Table(name = "events") public class Event { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; private String description; @Column(name = "start_date", nullable = false) private LocalDateTime startDate; @Column(name = "end_date", nullable = false) private LocalDateTime endDate; @Column(name = "max_participants", nullable = false) private Integer maxParticipants; @Enumerated(EnumType.STRING) private EventStatus status; @OneToMany(mappedBy = "event", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List registrations = new ArrayList<>(); // конструкторы, геттеры, сеттеры } @Entity @Table(name = "participants") public class Participant { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; @Column(nullable = false, unique = true) private String email; // конструкторы, геттеры, сеттеры } @Entity @Table(name = "registrations") public class Registration { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "event_id", nullable = false) private Event event; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "participant_id", nullable = false) private Participant participant; @Column(name = "registration_date", nullable = false) private LocalDateTime registrationDate; // конструкторы, геттеры, сеттеры } public enum EventStatus { DRAFT, PUBLISHED, CANCELLED } ``` ### **2. Repository интерфейсы:** ```java @Repository public interface EventRepository extends JpaRepository { List findByStatus(EventStatus status); } @Repository public interface ParticipantRepository extends JpaRepository { Optional findByEmail(String email); } @Repository public interface RegistrationRepository extends JpaRepository { boolean existsByEventIdAndParticipantId(Long eventId, Long participantId); Optional findByEventIdAndParticipantId(Long eventId, Long participantId); @Query("SELECT r.participant FROM Registration r WHERE r.event.id = :eventId") List findParticipantsByEventId(@Param("eventId") Long eventId); long countByEventId(Long eventId); void deleteByEventId(Long eventId); } ``` ### **3. Service класс:** ```java @Service @Transactional public class EventRegistrationService { private final EventRepository eventRepository; private final ParticipantRepository participantRepository; private final RegistrationRepository registrationRepository; private final NotificationService notificationService; public EventRegistrationService(EventRepository eventRepository, ParticipantRepository participantRepository, RegistrationRepository registrationRepository, NotificationService notificationService) { this.eventRepository = eventRepository; this.participantRepository = participantRepository; this.registrationRepository = registrationRepository; this.notificationService = notificationService; } public void registerParticipant(Long eventId, Long participantId) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new EventNotFoundException("Event not found: " + eventId)); Participant participant = participantRepository.findById(participantId) .orElseThrow(() -> new ParticipantNotFoundException("Participant not found: " + participantId)); validateRegistration(event, participant); Registration registration = new Registration(); registration.setEvent(event); registration.setParticipant(participant); registration.setRegistrationDate(LocalDateTime.now()); registrationRepository.save(registration); } private void validateRegistration(Event event, Participant participant) { // Проверка статуса события if (event.getStatus() != EventStatus.PUBLISHED) { throw new RegistrationException("Event is not published"); } // Проверка даты начала if (event.getStartDate().isBefore(LocalDateTime.now())) { throw new RegistrationException("Event has already started"); } // Проверка лимита участников long currentParticipants = registrationRepository.countByEventId(event.getId()); if (currentParticipants >= event.getMaxParticipants()) { throw new RegistrationException("Event is full"); } // Проверка дублирования регистрации if (registrationRepository.existsByEventIdAndParticipantId(event.getId(), participant.getId())) { throw new RegistrationException("Participant already registered for this event"); } } public void cancelRegistration(Long eventId, Long participantId) { Registration registration = registrationRepository.findByEventIdAndParticipantId(eventId, participantId) .orElseThrow(() -> new RegistrationNotFoundException("Registration not found")); registrationRepository.delete(registration); } public void cancelEvent(Long eventId) { Event event = eventRepository.findById(eventId) .orElseThrow(() -> new EventNotFoundException("Event not found: " + eventId)); List participants = registrationRepository.findParticipantsByEventId(eventId); // Отмена события event.setStatus(EventStatus.CANCELLED); eventRepository.save(event); // Уведомление участников participants.forEach(participant -> notificationService.notifyEventCancellation(participant, event)); } @Transactional(readOnly = true) public List getEventParticipants(Long eventId) { if (!eventRepository.existsById(eventId)) { throw new EventNotFoundException("Event not found: " + eventId); } return registrationRepository.findParticipantsByEventId(eventId); } } ``` ### **4. REST Controller:** ```java @RestController @RequestMapping("/api/events") @Validated public class EventRegistrationController { private final EventRegistrationService registrationService; public EventRegistrationController(EventRegistrationService registrationService) { this.registrationService = registrationService; } @PostMapping("/{eventId}/participants/{participantId}") public ResponseEntity registerParticipant( @PathVariable @Positive Long eventId, @PathVariable @Positive Long participantId) { registrationService.registerParticipant(eventId, participantId); return ResponseEntity.ok().build(); } @DeleteMapping("/{eventId}/participants/{participantId}") public ResponseEntity cancelRegistration( @PathVariable @Positive Long eventId, @PathVariable @Positive Long participantId) { registrationService.cancelRegistration(eventId, participantId); return ResponseEntity.ok().build(); } @DeleteMapping("/{eventId}") public ResponseEntity cancelEvent(@PathVariable @Positive Long eventId) { registrationService.cancelEvent(eventId); return ResponseEntity.ok().build(); } @GetMapping("/{eventId}/participants") public ResponseEntity> getEventParticipants( @PathVariable @Positive Long eventId) { List participants = registrationService.getEventParticipants(eventId); List dtos = participants.stream() .map(this::toDto) .collect(Collectors.toList()); return ResponseEntity.ok(dtos); } private ParticipantDto toDto(Participant participant) { return new ParticipantDto(participant.getId(), participant.getName(), participant.getEmail()); } } ``` ### **5. Exception Handling:** ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(EventNotFoundException.class) public ResponseEntity handleEventNotFound(EventNotFoundException ex) { ErrorResponse error = new ErrorResponse("EVENT_NOT_FOUND", ex.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); } @ExceptionHandler(RegistrationException.class) public ResponseEntity handleRegistrationError(RegistrationException ex) { ErrorResponse error = new ErrorResponse("REGISTRATION_ERROR", ex.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity handleValidation(ConstraintViolationException ex) { ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", "Invalid input parameters"); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } } ``` ### **6. Unit тесты:** ```java @ExtendWith(MockitoExtension.class) class EventRegistrationServiceTest { @Mock private EventRepository eventRepository; @Mock private ParticipantRepository participantRepository; @Mock private RegistrationRepository registrationRepository; @Mock private NotificationService notificationService; @InjectMocks private EventRegistrationService service; @Test void registerParticipant_Success() { // Given Long eventId = 1L; Long participantId = 1L; Event event = createPublishedEvent(); Participant participant = createParticipant(); when(eventRepository.findById(eventId)).thenReturn(Optional.of(event)); when(participantRepository.findById(participantId)).thenReturn(Optional.of(participant)); when(registrationRepository.countByEventId(eventId)).thenReturn(5L); when(registrationRepository.existsByEventIdAndParticipantId(eventId, participantId)).thenReturn(false); // When service.registerParticipant(eventId, participantId); // Then verify(registrationRepository).save(any(Registration.class)); } @Test void registerParticipant_EventNotPublished_ThrowsException() { // Given Event event = createDraftEvent(); Participant participant = createParticipant(); when(eventRepository.findById(1L)).thenReturn(Optional.of(event)); when(participantRepository.findById(1L)).thenReturn(Optional.of(participant)); // When & Then assertThrows(RegistrationException.class, () -> service.registerParticipant(1L, 1L)); } @Test void registerParticipant_EventFull_ThrowsException() { // Given Event event = createPublishedEvent(); event.setMaxParticipants(10); Participant participant = createParticipant(); when(eventRepository.findById(1L)).thenReturn(Optional.of(event)); when(participantRepository.findById(1L)).thenReturn(Optional.of(participant)); when(registrationRepository.countByEventId(1L)).thenReturn(10L); // When & Then assertThrows(RegistrationException.class, () -> service.registerParticipant(1L, 1L)); } private Event createPublishedEvent() { Event event = new Event(); event.setId(1L); event.setStatus(EventStatus.PUBLISHED); event.setStartDate(LocalDateTime.now().plusDays(1)); event.setMaxParticipants(10); return event; } private Event createDraftEvent() { Event event = new Event(); event.setId(1L); event.setStatus(EventStatus.DRAFT); event.setStartDate(LocalDateTime.now().plusDays(1)); event.setMaxParticipants(10); return event; } private Participant createParticipant() { Participant participant = new Participant(); participant.setId(1L); participant.setName("John Doe"); participant.setEmail("john@example.com"); return participant; } } ```
--- ## 🧮 Решение алгоритмической задачи
Показать решение ### **Основная идея:** Это классическая задача о максимальном количестве непересекающихся интервалов. Используем жадный алгоритм: 1. Сортируем встречи по времени окончания 2. Выбираем встречу с самым ранним окончанием 3. Исключаем все пересекающиеся с ней встречи 4. Повторяем для оставшихся встреч ### **Решение:** ```java 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 = [[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 тесты:** ```java 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)); } } ``` ### **Альтернативные подходы:** 1. **Dynamic Programming:** Сложность O(n²), не оптимально для этой задачи 2. **Recursive with memoization:** Также O(n²), избыточно 3. **Priority Queue:** Можно использовать, но жадный алгоритм проще и эффективнее ### **Почему жадный алгоритм работает:** Выбирая встречу с самым ранним окончанием, мы всегда оставляем максимально возможное время для размещения остальных встреч. Это оптимальная стратегия для данной задачи.