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

22 KiB
Raw Blame History

📝 Задача: Система управления событиями

Условие:

Вы разрабатываете систему для управления событиями (конференции, вебинары, встречи). Нужно реализовать сервис регистрации участников с учетом следующих требований:

  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 классы:

@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<Registration> 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 интерфейсы:

@Repository
public interface EventRepository extends JpaRepository<Event, Long> {
    List<Event> findByStatus(EventStatus status);
}

@Repository
public interface ParticipantRepository extends JpaRepository<Participant, Long> {
    Optional<Participant> findByEmail(String email);
}

@Repository
public interface RegistrationRepository extends JpaRepository<Registration, Long> {
    boolean existsByEventIdAndParticipantId(Long eventId, Long participantId);
    
    Optional<Registration> findByEventIdAndParticipantId(Long eventId, Long participantId);
    
    @Query("SELECT r.participant FROM Registration r WHERE r.event.id = :eventId")
    List<Participant> findParticipantsByEventId(@Param("eventId") Long eventId);
    
    long countByEventId(Long eventId);
    
    void deleteByEventId(Long eventId);
}

3. Service класс:

@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<Participant> participants = registrationRepository.findParticipantsByEventId(eventId);
        
        // Отмена события
        event.setStatus(EventStatus.CANCELLED);
        eventRepository.save(event);
        
        // Уведомление участников
        participants.forEach(participant -> 
            notificationService.notifyEventCancellation(participant, event));
    }
    
    @Transactional(readOnly = true)
    public List<Participant> getEventParticipants(Long eventId) {
        if (!eventRepository.existsById(eventId)) {
            throw new EventNotFoundException("Event not found: " + eventId);
        }
        
        return registrationRepository.findParticipantsByEventId(eventId);
    }
}

4. REST Controller:

@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<Void> registerParticipant(
            @PathVariable @Positive Long eventId,
            @PathVariable @Positive Long participantId) {
        
        registrationService.registerParticipant(eventId, participantId);
        return ResponseEntity.ok().build();
    }
    
    @DeleteMapping("/{eventId}/participants/{participantId}")
    public ResponseEntity<Void> cancelRegistration(
            @PathVariable @Positive Long eventId,
            @PathVariable @Positive Long participantId) {
        
        registrationService.cancelRegistration(eventId, participantId);
        return ResponseEntity.ok().build();
    }
    
    @DeleteMapping("/{eventId}")
    public ResponseEntity<Void> cancelEvent(@PathVariable @Positive Long eventId) {
        registrationService.cancelEvent(eventId);
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/{eventId}/participants")
    public ResponseEntity<List<ParticipantDto>> getEventParticipants(
            @PathVariable @Positive Long eventId) {
        
        List<Participant> participants = registrationService.getEventParticipants(eventId);
        List<ParticipantDto> 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:

@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(EventNotFoundException.class)
    public ResponseEntity<ErrorResponse> 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<ErrorResponse> handleRegistrationError(RegistrationException ex) {
        ErrorResponse error = new ErrorResponse("REGISTRATION_ERROR", ex.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
    
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(ConstraintViolationException ex) {
        ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", "Invalid input parameters");
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
}

6. Unit тесты:

@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. Повторяем для оставшихся встреч

Решение:

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 тесты:

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: Можно использовать, но жадный алгоритм проще и эффективнее

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

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