Number of Provinces
Есть n городов, пронумерованных от 0 до n-1. Вам дана матрица n × n isConnected в виде списка строк: isConnected[i][j] равно 1, если дорога напрямую соединяет город i и город j, и 0, если нет. Дороги двусторонние, поэтому матрица симметрична, и каждый город считается связанным сам с собой.
Провинция — это группа городов, в которой из любого города можно добраться до любого другого напрямую или через другие города, и из которой не ведёт ни одна дорога. Верните количество провинций.
Функция
- isConnectedinteger-2d-array
- матрица n × n, 1 — если дорога напрямую соединяет два города
- Возвращаетinteger
- количество провинций
Ограничения
1 ≤ n ≤ 150, гдеn = isConnected.lengthisConnected[i].length = nisConnected[i][j]равно0или1isConnected[i][i] = 1isConnected[i][j] = isConnected[j][i]
Примеры
- Ввод
- isConnected = [[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 0], [1, 0, 0, 1]]
- Вывод
- 2
- Пояснение
- Из города 0 ведёт дорога в город 3, а из города 1 — в город 2. Между этими двумя парами нет дорог, поэтому всего 2 провинции.
- Ввод
- isConnected = [[1, 1, 0, 0, 0], [1, 1, 1, 0, 0], [0, 1, 1, 0, 0], [0, 0, 0, 1, 0], [0, 0, 0, 0, 1]]
- Вывод
- 3
- Пояснение
- Между городами 0 и 2 нет дороги, но у обоих есть дорога к городу 1, поэтому города 0, 1 и 2 образуют одну провинцию. У городов 3 и 4 вообще нет дорог, и каждый из них образует отдельную провинцию — всего 3.
+15 скрытых тестов при отправке
Дополнительный вопрос
Теперь каждая дорога открывается в определённый день. Сможешь найти первый день, когда все города будут принадлежать одной провинции?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Изобрази каждый город точкой, а каждую
1вне диагонали — линией между двумя точками. Как выглядит провинция на этой картинке?Провинция — это связная компонента: 0 между двумя городами не означает, что они не связаны, потому что их может соединить третий город. Посчитай, сколько раз нужно начать новый поиск из города, до которого не добрался ни один предыдущий поиск.
Другой способ: начни с
nгрупп, по одной на город, и объединяй группыiиjдля каждой единицы над диагональю. Объединение двух разных групп уменьшает их количество на единицу. Структура union-find со сжатием путей делает каждое объединение почти константным по времени.
Решение
Матрица является матрицей смежности неориентированного графа: города — это узлы, а 1 в строке i, столбце j означает ребро. Провинция — это связная компонента, поэтому ответом будет количество компонент. Сложность заключается в том, что связь может проходить через третий город: 0 между двумя городами не означает, что они находятся в разных провинциях. Поиск из каждого ещё не посещённого города или структура непересекающихся множеств, объединяющая концы каждого ребра, подсчитывает компоненты за O(n²) — размер самой матрицы.
Поиск в глубину из каждого непосещённого города
Идея
Пройдитесь по городам по порядку. Если вы встречаете город, который не был отмечен ни одним предыдущим поиском, он не может принадлежать провинции, которую вы уже посчитали, потому что каждый поиск отмечает всю свою провинцию. Поэтому увеличьте счётчик на единицу, а затем отметьте все города, до которых можно добраться из этого города.
Чтобы найти их, используйте стек. Извлеките город, прочитайте его строку матрицы и добавьте в стек каждый город, у которого в этой строке стоит 1 и который ещё не отмечен, отмечая его при добавлении в стек. Во втором примере поиск из города 0 добавляет в стек город 1, а строка города 1 затем добавляет город 2, хотя в строке 0 для города 2 стоит 0. Именно такой переход по строкам позволяет находить города, связанные только через другие города.
Каждый город извлекается один раз, и при его извлечении читаются n элементов его строки, поэтому общее время — O(n²): вы читаете матрицу один раз. Для отметок и стека требуется не более n городов, поэтому дополнительная память — O(n).
Рекурсивный поиск выглядит аккуратнее, но в провинции, вытянутой в длинную линию, вызовы вкладываются друг в друга по одному на каждый город. При n = 150 это безопасно; тот же код на графе с 10^5 узлами переполнит стек вызовов, поэтому стоит придерживаться привычки использовать явный стек.
Алгоритм
- Создай флаг посещения для каждого города и установи счётчик в 0.
- Перебирай города по порядку и пропускай те, которые уже посещены.
- Для непосещённого города увеличь счётчик на 1, отметь его и помести в стек.
- Пока в стеке есть города, извлекай один и помещай в стек каждый город из его строки, для которого указана 1 и который ещё не посещён, отмечая его при добавлении.
- Верни счётчик.
def findCircleNum(isConnected):
n = len(isConnected)
seen = [False] * n
provinces = 0
for start in range(n):
if seen[start]:
continue
# Nobody reached this city from an earlier province, so it starts a new one.
provinces += 1
seen[start] = True
stack = [start]
while stack:
city = stack.pop()
row = isConnected[city]
for other in range(n):
# Mark a city when you push it, so it is never pushed twice.
if row[other] == 1 and not seen[other]:
seen[other] = True
stack.append(other)
return provincesСистема непересекающихся множеств со сжатием путей и объединением по рангу
Идея
Перевернём вопрос. Начнём с n провинций, по одной на город. Каждая 1 в матрице означает, что два города связаны: если они всё ещё в разных группах, объединим группы, и счёт уменьшится на единицу. После последней дороги счёт и будет ответом. Достаточно рассмотреть элементы выше главной диагонали, потому что матрица симметрична, а диагональ связывает город с самим собой. Во втором примере счёт начинается с 5. Единица в (0, 1) объединяет города 0 и 1 (остаётся 4), а единица в (1, 2) обнаруживает, что город 1 относится к группе города 0, и добавляет к ней город 2 (остаётся 3). У городов 3 и 4 выше диагонали нет единиц, поэтому ответ — 3.
Структура «объединение-поиск», также называемая системой непересекающихся множеств, хранит каждую группу в виде дерева. parent[c] указывает на родительский узел на один уровень выше, а город в корне, родителем которого является он сам, — это корень группы. Два города находятся в одной группе тогда и только тогда, когда find поднимается от обоих к одному и тому же корню. Чтобы объединить две группы, укажем один корень как родителя другого.
Два правила помогают сохранять деревья плоскими. Объединение по рангу подвешивает более короткое дерево под более высоким, поэтому дерево высоты h содержит не менее 2^h городов, а длина любого пути не превышает log n. Сжатие пути идёт дальше: как только find находит корень, он направляет каждый пройденный город прямо к этому корню, поэтому при следующем поиске от любого из них потребуется один шаг. Без любого из этих правил объединение городов длинной цепочки в неудачном порядке создаёт дерево в виде единственного пути, и каждый вызов find проходит O(n) шагов.
При использовании обоих правил амортизированная стоимость каждого вызова find составляет O(α(n)), где α — обратная функция Аккермана, которая не превышает 4 для любого n, помещающегося в компьютере. Чтение матрицы всё ещё требует O(n²), поэтому такова общая сложность, а для массивов parent и rank требуется O(n) памяти. Эта структура особенно полезна, когда дороги появляются по одной: она обновляет счёт после каждой новой дороги, не выполняя повторный поиск.
Алгоритм
- Установите
parent[c] = cиrank[c] = 0для каждого города, а счётчик установите вn. - Для каждой пары
i < j, для которойisConnected[i][j] = 1, найдите корниiиj. - В
findподнимайтесь до корня, затем пройдите тот же путь ещё раз и направьте каждый город на этом пути прямо к корню. - Если корни различаются, присоедините корень с меньшим рангом к другому, при равенстве рангов увеличьте ранг на 1 и уменьшите счётчик на 1.
- Верните счётчик.
def findCircleNum(isConnected):
n = len(isConnected)
parent = list(range(n))
rank = [0] * n
def find(city):
root = city
while parent[root] != root:
root = parent[root]
# Path compression: point every city on the way straight at the root.
while parent[city] != root:
up = parent[city]
parent[city] = root
city = up
return root
provinces = n
for i in range(n):
for j in range(i + 1, n): # the matrix is symmetric, so the upper half is enough
if isConnected[i][j] == 1:
a, b = find(i), find(j)
if a == b:
continue
# Union by rank: hang the shorter tree under the taller one.
if rank[a] < rank[b]:
a, b = b, a
parent[b] = a
if rank[a] == rank[b]:
rank[a] += 1
# Two provinces just became one.
provinces -= 1
return provinces
Ловушки и крайние случаи
В большинстве неправильных ответов считают, что 0 доказывает, будто два города не связаны, или считают что-то другое, а не компоненты.
- Проверка только прямых дорог. Города 0 и 2 во втором примере разделены 0, но всё равно входят в одну провинцию через город 1. Любой подсчёт, основанный только на прямых дорогах, этого не учитывает; например, подсчёт различных строк даёт там 5 вместо 3.
- Подсчёт единиц и деление на два. Так считаются дороги, а не провинции: три города, каждый из которых связан с двумя другими, образуют три дороги и одну провинцию.
- В системе union-find уменьшение счётчика при каждой 1, а не только когда корни двух множеств различаются. Дорога внутри уже объединённой группы не должна менять счётчик.
- Сравнение родителей вместо корней.
parent[i] == parent[j]может быть ложным для двух городов в одной группе, если один находится глубже в дереве; всегда сравнивайfind(i)иfind(j). - Присоединение самого города
j, а не его корня, как вparent[j] = find(i). Еслиjуже входил в группу, остальные города этой группы окажутся отрезаны от объединения. - Рекурсия на больших графах. Рекурсивный поиск или рекурсивный
findбез объединения по рангу проходит по одному уровню на каждый город в графе-цепочке. Это нормально для 150 городов, но при 10^5 приведёт к переполнению стека.
Частые вопросы4
Какова временная сложность задачи «Количество провинций»?
O(n²) при поиске в графе или использовании структуры «система непересекающихся множеств», поскольку оба подхода считывают каждую запись матрицы n × n один раз. В структуре «система непересекающихся множеств» появляется множитель α(n) — обратная функция Аккермана, которая не превышает 4 для любого реального входного значения. Дополнительная память составляет O(n) для флагов посещения или массивов родителей и рангов.
Что использовать для задачи «Количество провинций»: DFS, BFS или систему непересекающихся множеств?
Все три метода возвращают одно и то же количество за время O(n²). DFS или BFS проще всего написать, когда вся матрица дана сразу. Union-find — более подходящий инструмент, когда дороги поступают по одной или когда нужно также отвечать на вопрос, входят ли два города в одну провинцию, поскольку он обрабатывает каждую дорогу и каждый запрос за время, близкое к константному, без нового поиска.
Что делают сжатие путей и объединение по рангу в структуре непересекающихся множеств?
Объединение по рангу присоединяет более короткое дерево к более высокому, когда сливаются две группы, благодаря чему высота каждого дерева не превышает log n. Сжатие путей направляет каждый узел, через который проходит find, прямо к корню, поэтому последующие поиски из этих узлов занимают один шаг. При использовании обоих методов любая последовательность из m операций требует O(m α(n)) времени, что практически соответствует линейному времени.
Чем Number of Provinces отличается от Number of Islands?
Оба способа подсчитывают связные компоненты. В задаче Number of Islands граф представляет собой сетку, у каждой клетки не более четырёх соседей, а сложность работы составляет O(rows × cols). Здесь граф задан матрицей смежности: любой город может быть связан с любым другим, и чтобы перечислить соседей одного города, нужно прочитать целую строку из n элементов.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def findCircleNum(isConnected):
# Напишите код здесьСлучай 1
Случай 2
Ввод
isConnected = [[1, 0, 0, 1], [0, 1, 1, 0], [0, 1, 1, 0], [1, 0, 0, 1]]
Ожидается
2