0Pricing
DSA Interview Prep · درس

دمج الحسابات والمكوّنات المتصلة

جمّع الحسابات التي تتشارك بريدًا إلكترونيًا عبر اعتبار عناوين البريد عقد DSU، ثم اجمع جميع العناوين في كل مكوّن لإعادة بناء الحسابات المدمجة.

دمج الحسابات والمكوّنات المتصلة درس مجاني في DSA Interview Prep على CoddyKit. هذا هو الدرس 4 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DSA Interview Prep، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

المسألة: دمج الحسابات

تمنحكم مسألة Accounts Merge (LeetCode 721) قائمةً بالحسابات، ويكون كل حساب منها قائمةً من السلاسل، حيث يكون العنصر الأول اسم الحساب، وتكون العناصر المتبقية عناوين بريد إلكتروني. ينتمي حسابان إلى الشخص نفسه إذا اشتركا في بريد إلكتروني واحد على الأقل. ادمجوا جميع الحسابات التي تنتمي إلى الشخص نفسه، وأعيدوا قوائم البريد الإلكتروني مرتبة.

هذه في جوهرها مسألة المكوّنات المتصلة، حيث تكون رسائل البريد الإلكتروني عقدًا، ويربط وجودها في حساب مشترك بينها. وDSU هو الأداة المثالية: أجروا union لجميع رسائل البريد الإلكتروني داخل الحساب نفسه، ثم اجمعوا الرسائل لكل مكوّن.

# Example input
accounts = [
    ['John', 'john@mail.com', 'john1@mail.com'],
    ['John', 'john2@mail.com'],
    ['Mary', 'mary@mail.com'],
    ['John', 'john1@mail.com', 'john2@mail.com'],
]
# john@mail.com and john1@mail.com are in account[0]
# john1@mail.com and john2@mail.com are in account[3]
# => john@, john1@, john2@ are all the same person
# Expected output:
# ['John', 'john1@mail.com', 'john2@mail.com', 'john@mail.com']
# ['Mary', 'mary@mail.com']
print('Goal: merge accounts sharing any email into one account')

ربط رسائل البريد الإلكتروني بمعرّفات صحيحة

يعمل DSU على فهارس صحيحة، لكن عقدنا هي سلاسل تمثل عناوين البريد الإلكتروني. لذلك نحتاج إلى ربط كل بريد إلكتروني فريد بمعرّف صحيح. ونحتاج أيضًا إلى تذكّر اسم الحساب الذي يملك كل بريد إلكتروني. استخدموا قاموس email_to_id لتعيين معرّفات متزايدة، وقاموس email_to_name لتتبع اسم الحساب المرتبط بكل بريد إلكتروني.

يحصل كل بريد إلكتروني فريد على معرّف واحد. وإذا ظهر البريد الإلكتروني نفسه في حسابات متعددة، فإنه يُربط بالمعرّف نفسه — ويؤدي إجراء union لمعرّفات رسائل البريد الإلكتروني داخل حساب واحد إلى ربطها في مكوّن واحد. ويكون الاسم المرتبط بمعرّف البريد الإلكتروني الموجود في الجذر هو اسم الحساب المدمج.

accounts = [
    ['John', 'john@mail.com', 'john1@mail.com'],
    ['John', 'john2@mail.com'],
    ['Mary', 'mary@mail.com'],
    ['John', 'john1@mail.com', 'john2@mail.com'],
]

email_to_id = {}
email_to_name = {}
next_id = [0]

for account in accounts:
    name = account[0]
    for email in account[1:]:
        if email not in email_to_id:
            email_to_id[email] = next_id[0]
            next_id[0] += 1
        email_to_name[email] = name

print('Total unique emails:', len(email_to_id))
for email, eid in email_to_id.items():
    print(f'  {email} => id {eid} (owner: {email_to_name[email]})')

ضمّ رسائل البريد الإلكتروني ضمن كل حساب

بالنسبة إلى كل حساب، نُجري union لمعرّفات جميع رسائل البريد الإلكتروني المدرجة فيه معًا. ونختار أول بريد إلكتروني في الحساب ممثّلًا، ثم نُجري union لمعرّف كل بريد إلكتروني آخر مع معرّفه. وبذلك نربط جميع رسائل البريد الإلكتروني في الحساب ضمن مكوّن واحد.

بعد معالجة جميع الحسابات، تشترك رسائل البريد الإلكتروني التي ظهرت معًا، مباشرةً أو انتقاليًا عبر الرسائل المشتركة بين الحسابات، في جذر DSU نفسه. وهذه هي الخطوة الأساسية التي تنشر الاتصال عبر حسابات متعددة.

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    def union(self, x, y):
        self.parent[self.find(x)] = self.find(y)

# After building email_to_id (from previous step)
# email_to_id = {'john@mail.com':0, 'john1@mail.com':1,
#                'john2@mail.com':2, 'mary@mail.com':3}

dsu = DSU(5)  # 4 unique emails

# For account ['John', 'john@mail.com', 'john1@mail.com']:
dsu.union(0, 1)    # john@ and john1@ share account => same component

# For account ['John', 'john1@mail.com', 'john2@mail.com']:
dsu.union(1, 2)    # john1@ and john2@ share account => same component

# Now 0,1,2 all share a root; 3 (mary) is separate
print('find(0)==find(2)?', dsu.find(0) == dsu.find(2))  # True
print('find(0)==find(3)?', dsu.find(0) == dsu.find(3))  # False

تجميع رسائل البريد الإلكتروني حسب المكوّن

بعد إتمام جميع عمليات union، نمرّ على كل بريد إلكتروني، ونعثر على جذر DSU الخاص به، ثم نجمع رسائل البريد الإلكتروني حسب ذلك الجذر باستخدام قاموس من القوائم. ويصبح معرّف الجذر هو المفتاح. وأخيرًا، نسترجع اسم الحساب لكل مجموعة، ونرتّب قائمة البريد الإلكتروني، ثم نضيف الاسم في بدايتها.

يفرض تنسيق رسائل البريد الإلكتروني ترتيبًا معجميًا بحسب المسألة — إذ يجب أن تكون الرسائل داخل الحساب المدمج مرتبة معجميًا. ويمكن استرجاع الاسم من أي بريد إلكتروني في المجموعة، لأن جميع الرسائل في المكوّن نفسه تنتمي إلى الشخص نفسه.

from collections import defaultdict

# After DSU unions, group by root
def collect_components(email_to_id, email_to_name, dsu):
    root_to_emails = defaultdict(list)
    for email, eid in email_to_id.items():
        root = dsu.find(eid)
        root_to_emails[root].append(email)

    result = []
    for root, emails in root_to_emails.items():
        # Find the name from any email in this group
        name = email_to_name[emails[0]]
        result.append([name] + sorted(emails))
    return result

# Mock data for illustration
email_to_id = {'john@m.com':0,'john1@m.com':1,'john2@m.com':2,'mary@m.com':3}
email_to_name = {e:'John' for e in list(email_to_id)[:3]}
email_to_name['mary@m.com'] = 'Mary'

class DSU:
    def __init__(self,n): self.p=list(range(n))
    def find(self,x): self.p[x]=self.p[self.p[x]] if self.p[x]!=x else x; return self.p[x] if self.p[x]==x else self.find(self.p[x])
    def union(self,x,y): self.p[self.find(x)]=self.find(y)

dsu=DSU(4); dsu.union(0,1); dsu.union(1,2)
for row in collect_components(email_to_id, email_to_name, dsu):
    print(row)

الحل الكامل لدمج الحسابات

إليكم الحل الكامل الذي يجمع الخطوات الثلاث: إنشاء الربط بين البريد الإلكتروني والمعرّف، وإجراء union لرسائل البريد الإلكتروني داخل كل حساب، وتجميع الرسائل حسب جذر DSU. التعقيد الزمني الإجمالي هو O(n × m × alpha(n × m))، حيث تمثل n عدد الحسابات، وتمثل m الحد الأقصى لعدد رسائل البريد الإلكتروني في الحساب، وهو عمليًا O(n × m).

تعقيد المساحة هو O(n × m) لخرائط البريد الإلكتروني ومصفوفات DSU. ويتعامل هذا الحل بصورة صحيحة مع الدمج الانتقالي: فإذا شارك الحساب A البريد الإلكتروني X مع الحساب B، وشارك الحساب B البريد الإلكتروني Y مع الحساب C، فستُدمج الحسابات A وB وC جميعًا في مجموعة واحدة.

from collections import defaultdict

def accounts_merge(accounts):
    email_to_id = {}
    email_to_name = {}
    eid = 0

    for account in accounts:
        name = account[0]
        for email in account[1:]:
            if email not in email_to_id:
                email_to_id[email] = eid
                eid += 1
            email_to_name[email] = name

    parent = list(range(eid))

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        parent[find(x)] = find(y)

    for account in accounts:
        first_id = email_to_id[account[1]]
        for email in account[2:]:
            union(first_id, email_to_id[email])

    root_to_emails = defaultdict(list)
    for email, i in email_to_id.items():
        root_to_emails[find(i)].append(email)

    return [[email_to_name[emails[0]]] + sorted(emails)
            for emails in root_to_emails.values()]

accounts = [['John','a@m.com','b@m.com'],['John','c@m.com'],
            ['Mary','d@m.com'],['John','b@m.com','c@m.com']]
for row in accounts_merge(accounts):
    print(row)

بديل BFS/DFS لدمج الحسابات

يبني نهج بديل رسمًا بيانيًا يربط عناوين البريد الإلكتروني بالحسابات، بحيث تكون عناوين البريد عقدًا، وتصل الحواف بين عناوين البريد التي تظهر في الحساب نفسه. بعد ذلك، يعثر BFS/DFS على كل مكوّن متصل. وعلى الرغم من صحة هذا النهج، فإنه يتطلب بناء الرسم البياني صراحةً وتشغيل BFS انطلاقًا من كل عنوان بريد لم تتم زيارته — وهذا يعني كتابة شيفرة أكثر وصعوبة أكبر في فهمها مقارنةً بـ DSU.

يُعدّ DSU أنظف لأن بنية الاتحاد والعثور تمثّل عضوية المكوّنات طبيعيًا، من دون الحاجة إلى قائمة تجاور صريحة. وتكون أفضلية BFS هنا فقط عندما تحتاجون إلى إعادة بناء المسار الفعلي أو سلسلة عناوين البريد المشتركة بين حسابين.

# BFS alternative (for comparison)
from collections import defaultdict, deque

def accounts_merge_bfs(accounts):
    email_to_accounts = defaultdict(set)
    for i, account in enumerate(accounts):
        for email in account[1:]:
            email_to_accounts[email].add(i)

    visited_accounts = set()
    result = []

    for i, account in enumerate(accounts):
        if i in visited_accounts:
            continue
        queue = deque([i])
        emails_in_group = set()
        while queue:
            acc_idx = queue.popleft()
            if acc_idx in visited_accounts:
                continue
            visited_accounts.add(acc_idx)
            for email in accounts[acc_idx][1:]:
                emails_in_group.add(email)
                for j in email_to_accounts[email]:
                    queue.append(j)
        result.append([account[0]] + sorted(emails_in_group))
    return result

accounts = [['John','a@m.com','b@m.com'],['John','b@m.com','c@m.com'],['Mary','d@m.com']]
for row in accounts_merge_bfs(accounts):
    print(row)

التعميم: المكوّنات المتصلة في الرسم البياني

يمكن تعميم نمط دمج الحسابات على أي مشكلة من نوع المكوّنات المتصلة ذات التسميات: لديكم مجموعة من العناصر، وقد أُعلن أن بعض العناصر متكافئة (متصلة)، وتريدون تجميع جميع العناصر المتكافئة انتقاليًا معًا. وتشمل الأمثلة مسائل التجميع، ومجموعات الأصدقاء في الشبكات الاجتماعية، واكتشاف السجلات المكررة.

تكون الخوارزمية العامة دائمًا كما يلي: (1) تعيين معرّف صحيح لكل عنصر، (2) إجراء union لمعرّفات العناصر المعلَن تكافؤها، (3) تجميع العناصر وفق جذرها في DSU. يُعدّ DSU أساسًا محركًا للتجميع لعلاقات التكافؤ.

# Generalised grouping template
def group_equivalents(items, equivalences):
    item_to_id = {item: i for i, item in enumerate(items)}
    n = len(items)
    parent = list(range(n))

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        parent[find(x)] = find(y)

    for a, b in equivalences:
        if a in item_to_id and b in item_to_id:
            union(item_to_id[a], item_to_id[b])

    groups = {}
    for item in items:
        root = find(item_to_id[item])
        groups.setdefault(root, []).append(item)
    return list(groups.values())

# Example: merging duplicate customer records
customers = ['Alice-NY','Alice-LA','Bob','Alice-TX','Carol']
links = [('Alice-NY','Alice-LA'),('Alice-LA','Alice-TX')]
print(group_equivalents(customers, links))

التعامل مع الحالات الحدّية

من الحالات الحدّية المهمة في دمج الحسابات:

  • الحسابات ذات البريد الإلكتروني الواحد: يشكّل الحساب الذي يحتوي على عنوان بريد إلكتروني واحد فقط مكوّنًا مستقلًا، ما لم يشارك حساب آخر عنوان البريد نفسه.
  • الاسم نفسه لأشخاص مختلفين: ظهور 'John' في حسابين لا يعني أنهما يعودان إلى الشخص نفسه — فعناوين البريد المشتركة فقط هي التي تدمج الحسابات. ويُخزَّن الاسم لكل عنوان بريد، لا لكل مكوّن.
  • الحسابات الفارغة: يجب تخطي الحساب الذي لا يحتوي على عناوين بريد لتجنب أخطاء الفهرسة.

تحققوا دائمًا من أن الحل يتعامل مع الحسابات التي لا ينبغي دمجها لمجرد أنها تشترك في الاسم نفسه. إذ تعتمد روابط DSU حصريًا على عناوين البريد الإلكتروني المشتركة.

# Edge case: two Johns with no shared email => separate output
accounts = [
    ['John', 'john_a@m.com'],
    ['John', 'john_b@m.com'],   # different email => different component
    ['Mary'],                    # no emails => skip
]

def accounts_merge_safe(accounts):
    email_to_id = {}; email_to_name = {}; eid = 0
    for account in accounts:
        name = account[0]
        for email in account[1:]:
            if email not in email_to_id:
                email_to_id[email] = eid; eid += 1
            email_to_name[email] = name

    parent = list(range(eid))
    def find(x):
        while parent[x]!=x: parent[x]=parent[parent[x]]; x=parent[x]
        return x
    def union(x,y): parent[find(x)]=find(y)

    for account in accounts:
        if len(account) < 2: continue          # skip no-email accounts
        first = email_to_id[account[1]]
        for email in account[2:]:
            union(first, email_to_id[email])

    from collections import defaultdict
    groups = defaultdict(list)
    for email, i in email_to_id.items():
        groups[find(i)].append(email)
    return [[email_to_name[e[0]]] + sorted(e) for e in groups.values()]

for row in accounts_merge_safe(accounts):
    print(row)

عدد المكوّنات المتصلة في رسم بياني

تطرح مسألة مرتبطة (LeetCode 323) سؤالًا عن عدد المكوّنات المتصلة في رسم بياني غير موجّه. وهذه المسألة أبسط من دمج الحسابات: ابدؤوا بتهيئة DSU مع n عقد، ثم عالجوا جميع الحواف باستخدام union، وبعد ذلك احسبوا الجذور المختلفة.

أوجز طريقة لعدّ المكوّنات هي الاحتفاظ بالمتغير count بدءًا من n، وإنقاصه في كل مرة يدمج فيها union ناجح مكوّنين مختلفين. وبديلًا عن ذلك، احسبوا في النهاية عدد العقد i التي تحقق find(i) == i.

def count_components(n, edges):
    parent = list(range(n))

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    count = n
    for u, v in edges:
        pu, pv = find(u), find(v)
        if pu != pv:
            parent[pu] = pv
            count -= 1
    return count

print(count_components(5, [[0,1],[1,2],[3,4]]))  # 2: {0,1,2} and {3,4}
print(count_components(5, [[0,1],[1,2],[2,3],[3,4]]))  # 1: all connected
print(count_components(5, []))   # 5: no edges, all isolated

أصغر مكوّن وأكبر مكوّن

بعد أن يصبح لديكم DSU مع تتبع الأحجام، يمكنكم الإجابة عن أسئلة مثل «ما حجم أكبر مكوّن متصل؟» أو «كم مكوّنًا يحتوي بالضبط على 3 عقد؟» خلال O(n)، وذلك بفحص مصفوفة الأحجام عند العقد الجذرية.

تظهر هذه الأسئلة في مسائل مثل «العثور على أكبر جزيرة متصلة» في شبكة، أو «تحديد أصغر تقسيم للشبكة». بعد اكتمال جميع عمليات union، افحصوا العقد i التي تحقق find(i) == i (فهذه هي الجذور)، ثم افحصوا أحجامها.

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.size = [1] * n
    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    def union(self, x, y):
        px, py = self.find(x), self.find(y)
        if px == py: return
        if self.size[px] < self.size[py]: px, py = py, px
        self.parent[py] = px
        self.size[px] += self.size[py]

def component_stats(n, edges):
    dsu = DSU(n)
    for u, v in edges:
        dsu.union(u, v)
    sizes = [dsu.size[i] for i in range(n) if dsu.find(i) == i]
    print('Component sizes:', sizes)
    print('Largest component:', max(sizes))
    print('Smallest component:', min(sizes))
    print('Number of components:', len(sizes))

component_stats(8, [(0,1),(1,2),(3,4),(5,6),(6,7)])

نصائح للمقابلات حول مسائل DSU

عندما تواجهون مسألة تتضمن دمج المجموعات أو استعلامات الاتصال أو العثور على الحافة الزائدة، فكّروا فورًا في DSU. خلال المقابلات، اذكروا كلا التحسينين (ضغط المسار + الاتحاد حسب الرتبة/الحجم) لإظهار عمق معرفتكم، حتى لو كان DSU البدائي الأبسط كافيًا ضمن القيود المعطاة.

من الأخطاء الشائعة التي يجب تجنبها: نسيان التعامل مع الحالة التي يكون فيها الطرفان متصلين بالفعل (فيصبح union بلا تأثير)، والخطأ في استخدام الفهرسة بدءًا من 0 أو بدءًا من 1، وعدم ترتيب مخرجات accounts-merge (إذ تتطلب المسألة قوائم بريد إلكتروني مرتبة). احرصوا دائمًا على توضيح قيود الإدخال قبل كتابة الشيفرة.

# Interview checklist for DSU problems
checklist = [
    '1. Identify: is this a grouping/connectivity/cycle problem?',
    '2. Map problem entities to integer node IDs if needed',
    '3. Implement DSU with path compression + union by rank/size',
    '4. Process all relationships (edges/pairs) with union()',
    '5. Answer queries using find() and size/count tracking',
    '6. Handle edge cases: already connected, single nodes, no edges',
    '7. Check output format: sorted? 1-indexed? Name included?',
    '8. State time complexity: O(n * alpha(n)) ~ O(n)',
]
for item in checklist:
    print(item)

اختبار سريع

اختبروا فهمكم لمفاهيم Data Structures & Algorithms — Coding Interview Prep الواردة في هذا الدرس.

مراجعة الدرس

تعلّمتم في هذا الدرس أن: accounts-merge هي مسألة مكوّنات متصلة، حيث تكون عناوين البريد عقدًا وتربط الحسابات بين عناوين البريد، وأن DSU يحلّها عبر ربط عناوين البريد بمعرّفات صحيحة، وإجراء union للمعرّفات داخل كل حساب، ثم التجميع حسب الجذر، وأن نمط تجميع DSU نفسه ينطبق على أي مسألة من مسائل فئات التكافؤ أو التجميع. ننتقل بعد ذلك إلى معالجة البتات، بدءًا من العوامل الأساسية AND وOR وXOR وNOT وعوامل الإزاحة.

الأسئلة الشائعة

هل درس «دمج الحسابات والمكوّنات المتصلة» مجاني؟

نعم — نص درس «دمج الحسابات والمكوّنات المتصلة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DSA Interview Prep، انتقل إلى CoddyKit PRO. تتضمن دورة DSA Interview Prep 4 دروس في المجموع.

ماذا ستتعلم في «دمج الحسابات والمكوّنات المتصلة»؟

جمّع الحسابات التي تتشارك بريدًا إلكترونيًا عبر اعتبار عناوين البريد عقد DSU، ثم اجمع جميع العناوين في كل مكوّن لإعادة بناء الحسابات المدمجة. تتمرن على DSA Interview Prep مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DSA Interview Prep؟

لا تُشترط خبرة سابقة. DSA Interview Prep على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 4 من أصل 4.

كم من الوقت يستغرق درس «دمج الحسابات والمكوّنات المتصلة»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DSA Interview Prep هذا؟

نعم. كل درس في DSA Interview Prep يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. DSU مع ضغط المسار
  2. الدمج حسب الرتبة وحدّ دالة Ackermann العكسية
  3. الحافة الزائدة واكتشاف الدورات
  4. دمج الحسابات والمكوّنات المتصلة
← العودة إلى DSA Interview Prep