3Sum
لديك قائمة من الأعداد الصحيحة nums. أوجد كل ثلاثية [a, b, c] من القيم المأخوذة من ثلاثة مواضع مختلفة في nums بحيث a + b + c = 0. اكتب كل ثلاثية بترتيب غير تنازلي (a ≤ b ≤ c)، وأدرج كل ثلاثية مميزة مرة واحدة، حتى عندما تنتجها اختيارات متعددة للمواضع. أعد الثلاثيات مرتبة حسب قيمتها الأولى، ثم حسب قيمتها الثانية.
الدالة
- numsinteger-array
- قائمة الأعداد الصحيحة، التي تحتوي على ثلاثة عناصر على الأقل
- تُرجعinteger-2d-array
- كل ثلاثية متميزة مجموعها 0، مرتبة كل منها ترتيبًا غير تنازلي، والقائمة مرتبة
القيود
3 ≤ nums.length ≤ 3000-105 ≤ nums[i] ≤ 105- مجموع ثلاثية واحدة على الأقل يساوي 0.
- تكون ثلاثيتان متماثلتين عندما تحتويان على القيم الثلاث نفسها.
أمثلة
- المدخلات
- nums = [-2, 0, 1, 1, -1, 2]
- المخرجات
- [[-2, 0, 2], [-2, 1, 1], [-1, 0, 1]]
- الشرح
- -2 + 0 + 2 و-2 + 1 + 1 و-1 + 0 + 1 جميعها تساوي 0. قد تستخدم
[-2, 1, 1]القيمة 1 مرتين لأن 1 موجودة في موضعين، بينما يمكن تكوين[-1, 0, 1]باستخدام أيٍّ من القيمتين 1، لكنها تظهر مرة واحدة.
- المدخلات
- nums = [0, 0, 0, 0]
- المخرجات
- [[0, 0, 0]]
- الشرح
- أي ثلاثة من الأصفار الأربعة مجموعها 0. هناك أربعة اختيارات للمواضع، لكنها كلها تعطي الثلاثية نفسها، لذا تظهر الإجابة
[0, 0, 0]مرة واحدة.
+15 اختبارات مخفية عند الإرسال
سؤال إضافي
النمط نفسه يحل مسألة 4Sum: ثبّت قيمتين واستخدم مؤشرين على العناصر المتبقية. هل يمكنك كتابته بتعقيد O(n³) مع التعامل الصحيح مع التكرارات في كل مستوى؟
تلميحات
افتحها واحدًا تلو الآخر. كل تلميح يكشف أكثر قليلًا.
رتّب القائمة أولًا. تساعد القائمة المرتّبة بطريقتين: يظهر كل ثلاثي بالترتيب، وتتجاور القيم المتساوية، لذا تأتي القيمة المكررة دائمًا مباشرةً بعد القيمة التي تكررها.
ثبّت أصغر قيمة في الثلاثية،
nums[i]. يجب أن يكون مجموع القيمتين الأخريين-nums[i]، وأن تأتيا من القيم المرتبة الواقعة إلى يمينi. هذا سؤال عن مجموع زوج في قائمة مرتبة.لهذا الزوج، ابدأ بمؤشر واحد مباشرةً بعد
iوبالآخر عند الفهرس الأخير. إذا كان مجموع القيم الثلاث أقل من 0، فحرّك المؤشر الأيسر إلى اليمين؛ وإذا كان أكبر، فحرّك المؤشر الأيمن إلى اليسار. بعد العثور على تطابق، حرّك كليهما واجعل المؤشر الأيسر يتجاوز النسخ المكررة من قيمته. تخطَّ أيiتكون قيمته مساوية للقيمة التي تسبقه.
الحل
هناك أمران يجعلان مسألة 3Sum أصعب مما تبدو عليه. ففحص كل ثلاثية يستغرق O(n³)، ويجب أن تتضمن الإجابة كل ثلاثية مرة واحدة حتى عند تكرار القيم. يحل الفرز الأمرين: إذ تصبح القيم المتساوية متجاورة، فتتجاوز التكرارات بمقارنة العناصر المجاورة، وبمجرد تثبيت أصغر قيمة، يصبح إيجاد القيمتين الأخريين مسألة مجموع زوج في قائمة مرتبة، يحلها مؤشّران في مسح واحد.
جرّب كل ثلاثية
صحيحة، لكنها لا تنتهي في أكبر الاختبارات
الفكرة
رتّب القائمة أولًا. بعد ذلك، تعطي أي ثلاثة مواضع i < j < k قيمًا مرتبة بالفعل، nums[i] ≤ nums[j] ≤ nums[k]، لذا تكون الثلاثية مكتوبة بالترتيب الصحيح بمجرد العثور عليها. تمرّ ثلاث حلقات متداخلة على كل اختيار ممكن للمواضع، لذا لا يمكن أن تفوتنا أي ثلاثية.
بعد ذلك تأتي القيم المكررة. بعد الترتيب، يصبح المثال الأول [-2, -1, 0, 1, 1, 2]، ويمكن أن تأخذ [-1, 0, 1] القيمة 1 من الفهرس 3 أو الفهرس 4. لذلك تتخطى كل حلقة موضعًا تكون قيمته مساوية للقيمة التي جرّبتها الحلقة نفسها قبله. وهكذا تجرّب كل حلقة كل قيمة مميزة مرة واحدة، وتظهر كل ثلاثية مميزة مرة واحدة، وبترتيب تصاعدي من البداية. لا تقارن عملية التخطي إلا بالموضع السابق داخل الحلقة نفسها، لذا تظل [-2, 1, 1] تستخدم الواحدين كليهما.
المشكلة هي التكلفة. هناك نحو n³/6 ثلاثيات: وهذا يعني 4.5 × 10^9 مجموعًا لـ 3000 عدد، وهو ما يتجاوز بكثير أي حد زمني.
الخوارزمية
- رتّب
nums. - كرّر
iعلى المواضع وتخطَّiعندما تكونnums[i]مساوية لـnums[i-1]. - داخلها، كرّر
jبدءًا منi+1وتخطَّjعندما يكونj > i+1وتكونnums[j]مساوية لـnums[j-1]. - داخل ذلك، كرّر
kبدءًا منj+1مع قاعدة التخطي نفسها، وسجّل[nums[i], nums[j], nums[k]]عندما يكون مجموع القيم الثلاث 0. - أعِد الثلاثيات بالترتيب الذي عثرت عليها به. فهي مرتبة بالفعل.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # same first value as the round before
for j in range(i + 1, n - 1):
if j > i + 1 and nums[j] == nums[j - 1]:
continue # same second value as the round before
for k in range(j + 1, n):
if k > j + 1 and nums[k] == nums[k - 1]:
continue # same third value as the round before
if nums[i] + nums[j] + nums[k] == 0:
triplets.append([nums[i], nums[j], nums[k]])
return tripletsثبّت قيمة واحدة، واعثر على الزوج باستخدام مجموعة تجزئة
الفكرة
بعد تثبيت القيمة الأولى nums[i]، تحتاج إلى قيمتين لاحقتين يكون مجموعهما -nums[i]. هذه هي مسألة Two Sum. حرّك j إلى يمين i واحتفظ بمجموعة من القيم التي مررت بها. عند كل j، تكون القيمة الناقصة هي need = -nums[i] - nums[j]. إذا كانت need في المجموعة، فإن [nums[i], need, nums[j]] يكون مجموعها 0. يستغرق البحث في المجموعة O(1) في المتوسط، لذا تكون كلفة قيمة واحدة لـ i هي O(n)، وكلفة البحث بالكامل O(n²).
ما زال الترتيب يتولى تنظيم الأمور. تخطَّ أي i تكون قيمته مساوية للقيمة التي قبله. بعد العثور على تطابق، حرّك j متجاوزًا كل نسخة من nums[j]: فمع تثبيت القيمتين الأولى والثالثة، تكون القيمة الوسطى ثابتة أيضًا، لذا لا يمكن لنسخة أخرى إلا أن تكرر الثلاثية نفسها. وبما أن need تأتي من موضع أسبق في القائمة المرتبة، فإن need ≤ nums[j] وتكون الثلاثية مرتبة. يمكنك أيضًا التوقف بمجرد أن يصبح nums[i] > 0: فالقيمتان التاليتان لا تقلان عنه، لذا لا يمكن أن يصل المجموع إلى 0.
ملاحظة: كلما تحرك j إلى اليمين، تزداد nums[j] وتتناقص need، لذا تخرج الثلاثيات لقيمة i واحدة بترتيب تنازلي للقيمة الوسطى. في [-2, -1, 0, 1, 1, 2] مع i = 0، تجد [-2, 1, 1] عند نسخة 1 الثانية، ثم [-2, 0, 2] عند 2. اعكس ترتيب كل مجموعة قبل إضافتها إلى الإجابة. تستخدم نسختا C وR مصفوفة مفهرسة بالقيم لتحديد القيم التي تمت رؤيتها بدلًا من مجموعة تجزئة، وهذا ممكن لأن كل قيمة تقع ضمن ±10^5.
الخوارزمية
- رتّب
nums. - لكل
i، توقّف عندما يكونnums[i] > 0وتجاوزiعندما تكونnums[i]مساوية لـnums[i-1]. - ابدأ مجموعة فارغة. ولكل
jبدءًا منi+1، احسبneed = -nums[i] - nums[j]. إذا كانتneedضمن المجموعة، فسجّل[nums[i], need, nums[j]]وحرّكjمتجاوزًا النسخ المتكررة منnums[j]. - أضف
nums[j]إلى المجموعة وانتقل إلىjالتالي. - اعكس الثلاثيات التي عُثر عليها لهذا
iوألحِقها بالإجابة.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if nums[i] > 0:
break # the two values after it are at least as large
if i > 0 and nums[i] == nums[i - 1]:
continue # this first value was already handled
group = []
seen = set() # values between position i and position j
j = i + 1
while j < n:
need = -nums[i] - nums[j]
if need in seen:
group.append([nums[i], need, nums[j]])
while j + 1 < n and nums[j + 1] == nums[j]:
j += 1
seen.add(nums[j])
j += 1
# need shrinks as nums[j] grows, so this group came out backwards
group.reverse()
triplets.extend(group)
return tripletsالفرز واستخدام مؤشّرين
الفكرة
يمكن للترتيب التصاعدي أن يحل محل المجموعة. ثبّت nums[i]، وضع lo عند i+1 وhi عند الفهرس الأخير، وانظر إلى nums[i] + nums[lo] + nums[hi]. إذا كان المجموع أقل من 0، فأنت بحاجة إلى قيمة أكبر، لذا يتحرك lo إلى اليمين. وإذا كان أكبر من 0، فأنت بحاجة إلى قيمة أصغر، لذا يتحرك hi إلى اليسار. وعندما يساوي المجموع 0 تمامًا، سجّل الثلاثية وحرّك المؤشرين معًا.
لا تضيع أي ثلاثية. عندما يكون المجموع أقل من 0، تكون nums[lo] صغيرة أكثر من اللازم حتى مع أكبر قيمة متبقية، وهي nums[hi]، لذا لا يمكنها أن تقترن بأي قيمة ما زالت ضمن النطاق، ولن يؤدي استبعادها إلى فقدان أي شيء. والحالة التي يكون فيها المجموع أكبر من 0 هي العكس: تكون nums[hi] كبيرة أكثر من اللازم حتى مع أصغر قيمة متبقية. في كل خطوة، يُستبعد عنصر واحد نهائيًا، لذا تستغرق كل قيمة من قيم i عددًا من الخطوات لا يتجاوز n، ويستغرق البحث كله O(n²)، من دون ذاكرة إضافية تتجاوز الترتيب والناتج.
خذ القائمة المرتبة [-2, -1, 0, 1, 1, 2]. عندما تكون i = 0 (القيمة -2)، يبدأ lo عند -1 وhi عند 2: المجموع هو -1، لذا يتحرك lo إلى 0. الآن -2 + 0 + 2 = 0، لذا تسجّل [-2, 0, 2] ويصل المؤشران إلى قيمتي 1، فتكون النتيجة [-2, 1, 1]. عندما تكون i = 1 (القيمة -1)، يعطي 0 و2 مجموعًا يساوي 1، لذا يتحرك hi إلى قيمة 1 الثانية، ويكون -1 + 0 + 1 = 0، فتُسجّل [-1, 0, 1]. لا تجد القيمة 0 عند i = 2 أي شيء، وعند i = 3 تكون القيمة موجبة، لذا يتوقف البحث.
للقيم المكررة قاعدتان. تخطَّ قيمة i إذا كانت تساوي القيمة التي قبلها. بعد العثور على تطابق، حرّك lo متجاوزًا النسخ المطابقة للقيمة التي استخدمها. لا يحتاج hi إلى قاعدة خاصة به: عندما يكون lo عند قيمة أكبر، تؤدي نسخة من nums[hi] القديمة الآن إلى مجموع أكبر من 0، فتتحرك بعيدًا تلقائيًا. وبما أن i يمر على القيم المميزة بترتيب تصاعدي، وأن lo يتحرك إلى اليمين فقط، تخرج الثلاثيات مرتبة.
الخوارزمية
- رتّب
nums. - لكل
i، توقّف عندما يكونnums[i] > 0، وتجاوزiعندما يكونnums[i]مساويًا لـnums[i-1]. - عيّن
lo = i+1وhi = n-1. ما دامlo < hi، اجمعnums[i]وnums[lo]وnums[hi]. - إذا كان المجموع أقل من 0، حرّك
loإلى اليمين. وإذا كان أكبر من 0، حرّكhiإلى اليسار. - إذا كان يساوي 0، سجّل الثلاثية، وحرّك المؤشرين، ثم حرّك
loمتجاوزًا النسخ المكررة من القيمة التي استخدمها. - أعِد الثلاثيات. فهي مرتبة بالفعل.
def threeSum(nums):
nums = sorted(nums)
n = len(nums)
triplets = []
for i in range(n - 2):
if nums[i] > 0:
break # the two values after it are at least as large
if i > 0 and nums[i] == nums[i - 1]:
continue # this first value was already handled
lo, hi = i + 1, n - 1
while lo < hi:
total = nums[i] + nums[lo] + nums[hi]
if total < 0:
lo += 1
elif total > 0:
hi -= 1
else:
triplets.append([nums[i], nums[lo], nums[hi]])
lo += 1
hi -= 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1
return triplets
أخطاء شائعة وحالات حدّية
تأتي معظم الإجابات الخاطئة من القيم المكررة، لذا اختبر باستخدام مُدخلات تحتوي عليها.
- تخطي
iعندما تكونnums[i]مساوية لـnums[i+1]يُبقي النسخة الأخيرة من كل قيمة بوصفها العنصر الأول، ويزيل النسخ التي تسبقها. في[-1, -1, 2]يؤدي ذلك إلى فقدان[-1, -1, 2]. قارن مع الموضع السابق،nums[i-1]. - التوقف عندما تكون
nums[i] ≥ 0بدلًا منnums[i] > 0يفوّت[0, 0, 0]. - إزالة القيم المكررة في النهاية بدلًا من تخطيها. عند وجود 3000 صفر، تسجّل حلقة المؤشرين نسخًا بالملايين من
[0, 0, 0]قبل إجراء أي تنظيف، وفي عدة لغات تقارن المجموعة التي تحتوي على قوائم بين القوائم حسب الهوية، لذا تبقى النسخ على أي حال. - استخدام موضع واحد مرتين. إصدار يستخدم مجموعة تجزئة ويملأها بالقائمة بأكملها مسبقًا يحوّل
[-2, 1, 3]إلى[-2, 1, 1]باستخدام القيمة 1 الوحيدة مرتين. ابحث فقط عن القيم في المواضع التي تجاوزتها بالفعل. - إرجاع الثلاثيات بترتيب غير صحيح. المقارنة دقيقة، لذا يجب أن يعكس إصدار مجموعة التجزئة ترتيب كل مجموعة، كما يجب أن يرتب الحل الذي يجمع الثلاثيات في مجموعة إياها في النهاية.
أسئلة شائعة4
ما هو التعقيد الزمني لمسألة 3Sum؟
يعمل حل الفرز والمؤشرين في زمن O(n²). يستغرق الفرز O(n log n)، ويحتاج كل اختيار من الاختيارات n للقيمة الأولى إلى مسح واحد يستغرق O(n). يحتاج الحل إلى مساحة إضافية O(1)، باستثناء مساحة الفرز والناتج. أما التحقق من كل ثلاثية فيستغرق O(n³).
كيف تتجنب 3Sum الثلاثيات المكررة؟
يرتّب القائمة، بحيث تصبح القيم المتساوية متجاورة. ثم يتجاوز أي قيمة أولى تساوي القيمة التي قبلها، وبعد كل تطابق يحرّك المؤشر الأيسر إلى ما بعد النسخ من القيمة التي استخدمها. يُعثر على كل ثلاثية مرة واحدة، بدءًا من النسخ الأولى لقيمها، لذا لا حاجة إلى مجموعة للنتائج.
هل أستخدم مؤشرين أم مجموعة تجزئة لمسألة 3Sum؟
كلاهما يعمل في زمن O(n²). لا تحتاج المؤشرات الاثنتان إلى ذاكرة إضافية، ويمنحك الترتيب المصنّف الثلاثيات مرتبةً مسبقًا. تستهلك مجموعة التجزئة ذاكرة O(n)، وتتطلب عناية للحفاظ على تميّز المواضع وترتيب الناتج. تبرز أهمية فكرة مجموعة التجزئة عندما لا يمكنك الفرز، كما في Two Sum حيث تعيد الفهارس الأصلية.
هل يمكن حل مسألة 3Sum بزمن أسرع من O(n²)؟
ليس بفارق كبير. فأفضل الخوارزميات المعروفة تتفوق على n² بعوامل لوغاريتمية قليلة فحسب، وتفترض كثير من نتائج الصعوبة في الهندسة الحاسوبية عدم وجود خوارزمية تصل إلى قوة لـ n أقل من 2. هذه الخوارزميات الأسرع هي نتائج بحثية، لذا فإن O(n²) هي الإجابة التي يتوقعها المحاورون.
مسائل مشابهة
مسائل تعتمد على الأفكار نفسها. حلّ اثنتين أو ثلاث منها يثبّت النمط.
Python
def threeSum(nums):
# اكتب الكود هناالحالة 1
الحالة 2
المدخلات
nums = [-2, 0, 1, 1, -1, 2]
المتوقع
[[-2, 0, 2], [-2, 1, 1], [-1, 0, 1]]