diff --git a/💼 Работа/Собеседования/Лето 2025/Задачи/💼 Прикладная задача для Middle разработчика.md b/💼 Работа/Собеседования/Лето 2025/Задачи/💼 Прикладная задача для Middle разработчика.md new file mode 100644 index 0000000..48ddd62 --- /dev/null +++ b/💼 Работа/Собеседования/Лето 2025/Задачи/💼 Прикладная задача для Middle разработчика.md @@ -0,0 +1,646 @@ + + +## 📝 Задача: Система управления событиями + +### **Условие:** + +Вы разрабатываете систему для управления событиями (конференции, вебинары, встречи). Нужно реализовать сервис регистрации участников с учетом следующих требований: + +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:** Можно использовать, но жадный алгоритм проще и эффективнее + +### **Почему жадный алгоритм работает:** + +Выбирая встречу с самым ранним окончанием, мы всегда оставляем максимально возможное время для размещения остальных встреч. Это оптимальная стратегия для данной задачи. + +
\ No newline at end of file