3.4 — задача из примера в теме 3.3.2 «Двудольные графы и паросочетания»3.4 — a problem from the example in topic 3.3.2 "Bipartite graphs & matchings"
В 3.3.2 упоминалось: распределение студентов по проектным слотам без предпочтений — прямая задача невзвешенного максимального паросочетания. Здесь — конкретный список "кто в какую группу годится по навыкам" и его решение алгоритмом Куна.3.3.2 mentioned that assigning students to project slots with no preferences is a direct unweighted maximum matching problem. Here's a concrete list of "who qualifies for which group by skill" and its solution via Kuhn's algorithm.
Дано 3 студента (0, 1, 2) и 3 проектные группы (0, 1, 2), каждая рассчитана ровно на одного студента. Список `qualifies[i]` — множество групп, для которых у студента i достаточно навыков: студент 0 годится в группы {0, 1}, студент 1 — только в группу {0}, студент 2 — в группы {1, 2}. Нужно найти максимальное число студентов, которых можно распределить по группам так, чтобы каждый студент попал в группу, для которой он подходит, и каждая группа получила не более одного студента.Given 3 students (0, 1, 2) and 3 project groups (0, 1, 2), each sized for exactly one student. The list `qualifies[i]` is the set of groups student i has sufficient skills for: student 0 qualifies for groups {0, 1}, student 1 only for group {0}, student 2 for groups {1, 2}. Find the maximum number of students that can be assigned to groups such that each student lands in a group they qualify for, and each group gets at most one student.
// Example
qualifies = {
0: [0, 1],
1: [0],
2: [1, 2]
}
// output: max_matched=3, assignment={group0: student1, group1: student0, group2: student2}
Прямое применение определения из 3.3.2: студенты — множество L, группы — множество R, ребро студент-группа существует, если студент годится по навыкам. Задача "максимум распределённых студентов" — это буквально максимальное паросочетание в этом двудольном графе.Direct application of the definition from 3.3.2: students are set L, groups are set R, a student-group edge exists if the student qualifies. "Maximum number of assigned students" is literally a maximum matching in this bipartite graph.
На первый взгляд есть подвох: и студент 0, и студент 1 годятся в группу 0, но студент 1 годится ТОЛЬКО туда — если сразу отдать группу 0 студенту 0 (первому по номеру), студент 1 останется без группы вообще, хотя решение с 3 распределениями существует. Это ровно тот сценарий, ради которого алгоритму Куна нужна увеличивающая цепь из 3.3.2: жадное присвоение "первому пришедшему" не гарантирует максимума, а поиск с возможностью "подвинуть" уже назначенного студента — гарантирует.At first glance there's a catch: both student 0 and student 1 qualify for group 0, but student 1 qualifies ONLY there — if group 0 is greedily handed to student 0 (processed first), student 1 is left with no group at all, even though a 3-assignment solution exists. This is exactly the scenario Kuhn's algorithm needs an augmenting path for, from 3.3.2: greedy "first come" assignment doesn't guarantee the maximum, but a search that can "bump" an already-assigned student does.
студент 0 "подвинут" на группу 1, освобождая единственный вариант студента 1 — это и есть увеличивающая цепь в действииstudent 0 is "bumped" to group 1, freeing up student 1's only option — the augmenting path in action
Пошагово, как это находит Кун: обрабатываем студента 0 первым — группа 0 свободна, назначаем 0→группа 0. Обрабатываем студента 1 — его единственный вариант, группа 0, уже занята студентом 0; вместо отказа алгоритм рекурсивно пытается пристроить студента 0 куда-то ещё — у студента 0 есть вторая опция, группа 1, она свободна — 0 переезжает в группу 1, а группа 0 освобождается для студента 1. Обрабатываем студента 2 — группа 1 занята студентом 0, но у студента 2 есть вторая опция, группа 2, она свободна — назначаем напрямую. Итог: все 3 студента распределены.Step by step, how Kuhn's algorithm finds this: process student 0 first — group 0 is free, assign 0→group 0. Process student 1 — their only option, group 0, is already taken by student 0; instead of failing, the algorithm recursively tries to place student 0 somewhere else — student 0 has a second option, group 1, which is free — 0 moves to group 1, freeing group 0 for student 1. Process student 2 — group 1 is taken by student 0, but student 2 has a second option, group 2, which is free — assign directly. Result: all 3 students are assigned.
class Solution {
bool tryKuhn(int u, const std::vector<std::vector<int>>& adj,
std::vector<bool>& used, std::vector<int>& matchR) {
for (int v : adj[u]) {
if (used[v]) continue;
used[v] = true;
// группа свободна, или её текущего студента можно передвинуть// the group is free, or its current student can be moved
if (matchR[v] == -1 || tryKuhn(matchR[v], adj, used, matchR)) {
matchR[v] = u;
return true;
}
}
return false;
}
public:
std::pair<int, std::vector<int>>
assignGroups(int numStudents, int numGroups,
const std::vector<std::vector<int>>& qualifies) {
std::vector<int> matchR(numGroups, -1); // matchR[group] = студент// matchR[group] = student
int matched = 0;
for (int s = 0; s < numStudents; ++s) {
std::vector<bool> used(numGroups, false);
if (tryKuhn(s, qualifies, used, matchR)) matched++;
}
return {matched, matchR};
}
};
Сложность O(V·E) — базовая гарантия алгоритма Куна из 3.3.2: для каждого из 3 студентов запускается DFS стоимостью O(E). При таком маленьком n разница с более быстрым Хопкрофтом-Карпом (O(E√V)) незаметна — она проявляется на масштабах в сотни-тысячи студентов и групп, о чём явно предупреждал раздел "Грабли" в 3.3.2.Complexity is O(V·E) — Kuhn's algorithm's basic guarantee from 3.3.2: for each of the 3 students, a DFS costing O(E) is run. At this small n, the difference from the faster Hopcroft-Karp (O(E√V)) is invisible — it shows up at scales of hundreds to thousands of students and groups, exactly what 3.3.2's Gotchas section warned about.
Как показано в разборе, если убрать рекурсивный вызов `tryKuhn(matchR[v], ...)` и просто пропускать занятые группы, студент 1 никогда не получит группу, хотя решение на 3 распределения существует — это не крайний случай, а типичная ситуация в любой задаче о назначениях, где у части участников меньше опций, чем у других.As shown in the walkthrough, removing the recursive `tryKuhn(matchR[v], ...)` call and simply skipping taken groups means student 1 never gets a group, even though a 3-assignment solution exists — this isn't an edge case, it's a typical situation in any assignment problem where some participants have fewer options than others.
Массив `used` должен создаваться заново для каждого внешнего вызова `tryKuhn(s, ...)` — он отслеживает, какие группы уже рассмотрены В ТЕКУЩЕМ поиске увеличивающей цепи, а не за всё время работы алгоритма. Если использовать один общий `used` на все вызовы, второй и последующие студенты не смогут даже начать поиск, потому что все группы окажутся помеченными как "уже рассмотренные".The `used` array must be created fresh for each outer call to `tryKuhn(s, ...)` — it tracks which groups have been considered IN THE CURRENT augmenting-path search, not across the algorithm's entire run. Using one shared `used` array across all calls means the second and later students can't even start their search, since every group ends up marked as "already considered."