Связные записи по телефону/email

Senior Python Fintech

Условие задания

**Задание по мотивам реального тестового в Сбере.**

**Данные:** Записи клиентов в формате `key;id;phone;mail`:
[см. код в задании]

Два клиента считаются **одним лицом**, если у них совпадает хотя бы один телефон ИЛИ email. Транзитивность: если A связан с B, а B связан с C, то A, B, C — одно лицо.

**Задание:**
1. Реализуйте алгоритм поиска связных компонент (Union-Find или BFS/DFS)
2. Для каждой группы выведите все записи + объединённый набор телефонов и email
3. Оцените сложность алгоритма
4. Обработайте edge cases: пустые поля, дубли id

Пример данных

Структура для ориентира — реальные значения из эталонного решения.

from collections import defaultdict

# --- Данные ---
raw_data = """key;id;phone;mail
1;user1;+79001234567;user1@mail.ru
2;user2;+79007654321;user1@mail.ru
3;user3;+79001234567;user3@gmail.com
4;user4;+79119876543;user4@ya.ru
5;user5;+79119876543;user2@mail.ru
6;user2;+79007654321;user6@gmail.com"""

lines = raw_data.strip().split('\n')
header = lines[0].split(';')
records = []
for line in lines[1:]:
    parts = line.split(';')
    records.append({
        'key': int(parts[0]),
        'id': parts[1],
        'phone': parts[2] if len(parts) > 2 else '',
        'mail': parts[3] if len(parts) > 3 else '',
    })


# =============================================
# Подход 1: Union-Find (Disjoint Set Union)
# =============================================
class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        # Path compression
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return
        # Union by rank
        if self.rank[rx] < self.rank[ry]:
            rx, ry = ry, rx
        self.parent[ry] = rx
        if self.rank[rx] == self.rank[ry]:
            self.rank[rx] += 1


def deduplicate_uf(records):
    n = len(records)
    uf = UnionFind(n)

    # Индекс: значение → первый индекс записи с таким значением
    phone_index = {}
    mail_index = {}

    for i, rec in enumerate(records):
        phone = rec['phone'].strip()
        mail = rec['mail'].strip().lower()

        if phone:
            if phone in phone_index:
                uf.union(i, phone_index[phone])
            else:
                phone_index[phone] = i

        if mail:
            if mail in mail_index:
                uf.union(i, mail_index[mail])
            else:
                mail_index[mail] = i

    # Собираем группы
    groups = defaultdict(list)
    for i in range(n):
        groups[uf.find(i)].append(i)

    return groups


# =============================================
# Подход 2: BFS на графе (альтернатива)
# =============================================
def deduplicate_bfs(records):
    from collections import deque

    n = len(records)
    # Строим граф: запись → список смежных записей
    adj = defaultdict(set)
    phone_map = defaultdict(list)
    mail_map = defaultdict(list)

    for i, rec in enumerate(records):
        phone = rec['phone'].strip()
        mail = rec['mail'].strip().lower()
        if phone:
            phone_map[phone].append(i)
        if mail:
            mail_map[mail].append(i)

    # Рёбра: все записи с одинаковым phone/mail связаны
    for indices in list(phone_map.values()) + list(mail_map.values()):
        for j in range(1, len(indices)):
            adj[indices[0]].add(indices[j])
            adj[indices[j]].add(indices[0])

    # BFS по компонентам
    visited = set()
    groups = {}
    group_id = 0

    for start in range(n):
        if start in visited:
            continue
        queue = deque([start])
        visited.add(start)
        component = []

        while queue:
            node = queue.popleft()
            component.append(node)
            for neighbor in adj[node]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)

        groups[group_id] = component
        group_id += 1

    return groups


# --- Запуск ---
print("=== Union-Find ===")
uf_groups = deduplicate_uf(records)
for root, indices in sorted(uf_groups.items()):
    phones = set()
    mails = set()
    ids = set()
    for i in indices:
        r = records[i]
        ids.add(r['id'])
        if r['phone']:
            phones.add(r['phone'])
        if r['mail']:
            mails.add(r['mail'])

    print(f"\nГруппа (записи: {[records[i]['key'] for i in indices]}):")
    print(f"  IDs: {ids}")
    print(f"  Телефоны: {phones}")
    print(f"  Email: {mails}")

print("\n=== BFS ===")
bfs_groups = deduplicate_bfs(records)
for gid, indices in sorted(bfs_groups.items()):
    keys = [records[i]['key'] for i in indices]
    print(f"Группа {gid}: записи {keys}")

# --- Сложность ---
print("\n=== Сложность ===")
print("Union-Find: O(N × α(N)) ≈ O(N) — почти линейная")
print("  α(N) — обратная функция Аккермана, < 5 для любых реальных N")
print("BFS: O(N + E), где E — число рёбер (общих phone/email)")
print("  Для плотных данных E может быть O(N²) → Union-Find лучше")

Темы

python Union-Find графы дедупликация BFS Сбер

Подсказки

Все тестовые задания →

Частые вопросы

Какой уровень знаний нужен для задачи "Связные записи по телефону/email"?

Это задание для уровня Senior. Senior-уровень — глубокое понимание темы, опыт решения нестандартных задач, обсуждение trade-off на собеседовании.

На каких собеседованиях встречается такая задача?

Подобные задания в категории «Python» регулярно дают на собеседованиях аналитика данных в Яндекс, Сбер, Ozon, Авито, Тинькофф, Wildberries, T-Bank, X5, ВТБ и других крупных IT-компаниях. Тематика: python, Union-Find, графы, дедупликация, BFS.

Сколько времени даётся на решение?

На реальном собеседовании на подобную задачу отводится 30-60 минут с обсуждением подходов, оптимизаций и trade-off. Для тренировки рекомендуем сначала решить самостоятельно, потом сверить с эталонным решением и подсказками.

Где ещё потренироваться по теме «Python»?

На zasqlpython.ru есть 482 Python задачи с проверкой через Pyodide, конспекты Python и pandas, AI мок-собеседование с разбором ваших ответов.

← Все задания