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

646 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 📝 Задача: Система управления событиями
### **Условие:**
Вы разрабатываете систему для управления событиями (конференции, вебинары, встречи). Нужно реализовать сервис регистрации участников с учетом следующих требований:
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 минут
---
# ✅ Решения задач
## 💼 Решение прикладной задачи
<details> <summary>Показать решение</summary>
### **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<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 интерфейсы:**
```java
@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 класс:**
```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<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:**
```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<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:**
```java
@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 тесты:**
```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;
}
}
```
</details>
---
## 🧮 Решение алгоритмической задачи
<details> <summary>Показать решение</summary>
### **Основная идея:**
Это классическая задача о максимальном количестве непересекающихся интервалов. Используем жадный алгоритм:
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:** Можно использовать, но жадный алгоритм проще и эффективнее
### **Почему жадный алгоритм работает:**
Выбирая встречу с самым ранним окончанием, мы всегда оставляем максимально возможное время для размещения остальных встреч. Это оптимальная стратегия для данной задачи.
</details>