On this page
ข้อ 13 · LC1679 Max Number of K-Sum Pairs 🟡
จับคู่เลขที่บวกกันได้ k แล้วเอาออกจาก array — จับได้มากสุดกี่คู่
ให้ array (ลิสต์) nums และเลข k ในหนึ่ง operation (การกระทำ) เราเลือกเลขสองตัวที่บวกกันได้ k แล้วนำทั้งคู่ ออกจาก array จง return จำนวน operation มากที่สุดที่ทำได้
จุดที่ต้องระวัง: เลขแต่ละตัวใช้ได้ครั้งเดียว เพราะถูกเอาออกไปแล้ว
- Input:
- nums = [1,2,3,4], k = 5
- Output:
- 2
- Explanation:
- จับ (1,4) ได้ 5 → เหลือ [2,3] แล้วจับ (2,3) ได้ 5 → เหลือ [] รวม 2 คู่
- Input:
- nums = [3,1,3,4,3], k = 6
- Output:
- 1
- Explanation:
- จับ 3 กับ 3 ได้ 6 → เหลือ [1,4,3] ซึ่งไม่มีคู่ไหนบวกได้ 6 อีก รวม 1 คู่
- Input:
- nums = [2,2,2,3], k = 4
- Output:
- 1
- Explanation:
- มีเลข 2 อยู่ 3 ตัว จับได้แค่คู่เดียว (เหลือ 2 ตัวเดียวโดด) — เลขเหลือเศษจับไม่ได้
- 1 <= nums.length <= 10^5
- 1 <= nums[i] <= 10^9
- 1 <= k <= 10^9
- nums ไม่ได้เรียงมาให้ (ต่างจาก LC167 Two Sum II)
"จับคู่ให้ได้มากที่สุด" ฟังดูซับซ้อน แต่โครงสร้างของโจทย์ง่ายกว่านั้นมาก เพราะตัวเลข x จับได้กับ k − x เท่านั้น ไม่มีทางเลือกอื่น ดังนั้นแค่ นับ ว่าแต่ละค่ามีกี่ตัว ก็คำนวณคำตอบได้ตรง ๆ
ลองเองก่อน 10–15 นาที
def max_operations(nums: list[int], k: int) -> int:
# เขียนโค้ดของคุณที่นี่
pass
print(max_operations([1, 2, 3, 4], 5)) # ควรได้ 2
print(max_operations([3, 1, 3, 4, 3], 6)) # ควรได้ 1
print(max_operations([2, 2, 2, 3], 4)) # ควรได้ 1💡 ใบ้ขั้น 1 — ตั้งคำถามให้ตัวเองก่อน
- ถ้าคุณกำลังถือเลข x อยู่ในมือ มีเลขอีกกี่ค่า (ไม่ใช่กี่ตัว) ที่จับกับ x ได้?
- ถ้าคำตอบคือ "ค่าเดียว" แปลว่าปัญหานี้ไม่ต้องเลือกอะไรเลยใช่ไหม — แค่จับให้ครบเท่าที่มี
- ถ้า array ถูก sorted (เรียง) แล้ว การหาคู่ที่บวกกันได้ k จากหัวและท้าย ทำได้เร็วขึ้นไหม
- กรณี x = k − x (เช่น k = 6, x = 3) ต่างจากกรณีอื่นยังไง
💡 ใบ้ขั้น 2 — เทคนิคที่ต้องใช้ (มี 2 ทางที่ถูกทั้งคู่)
ทางที่ 1 — Two Pointers (ต้อง sort ก่อน): sort แล้วใช้ left/right วิ่งเข้าหากัน ถ้า nums[left] + nums[right] == k นับหนึ่งคู่แล้วขยับทั้งสองตัว (เท่ากับ "เอาออก") ถ้าน้อยไปขยับ left ถ้ามากไปขยับ right
ทางที่ 2 — นับด้วย dict (จำคู่ค่า-จำนวน): ไล่ทีละตัว เก็บว่าค่าไหนยังไม่มีคู่อยู่บ้าง (เหมือนสมุดจดชื่อ-จำนวน) พอเจอ x ที่มี k − x ค้างอยู่ในสมุด ก็จับคู่ทันทีแล้วลดจำนวนในสมุดลงหนึ่ง
| Two Pointers | นับด้วย dict | |
|---|---|---|
| ต้องวนกี่รอบ | รวม sort + วนหนึ่งรอบ | วนรอบเดียว ✅ เร็วกว่า |
| ต้องใช้พื้นที่พิเศษเพิ่มไหม | ไม่ต้องมีโครงสร้างใหม่ แต่ sort ไปแก้ list ของคนเรียกด้วย ⚠️ (อธิบายด้านล่าง) | ต้องมี dict เก็บจำนวน |
| ข้อดี | ไม่ต้องสร้าง dict เห็นภาพชัด | ไม่ต้อง sort เร็วกว่าตอน n ใหญ่มาก และไม่ไปแก้ข้อมูลเดิม |
ตอนเจอคู่แล้ว ต้องขยับ ทั้งสอง pointer (left += 1 และ right -= 1) ถ้าขยับตัวเดียว เลขตัวที่ไม่ขยับจะถูกใช้ซ้ำ — ผิดกติกาเพราะมันถูก "เอาออก" ไปแล้ว
💡 ใบ้ขั้น 3 — โครงโค้ด (pseudocode) มีช่องว่างให้เติม
# ทางที่ 1: two pointers
nums.sort()
left, right = 0, len(nums) - 1
ops = 0
while left < right:
total = nums[left] + nums[right]
if total == k:
ops += 1
______ # (1) ขยับอะไรบ้าง (คำใบ้: สองบรรทัด)
elif total < k:
______ # (2)
else:
______ # (3)
return ops# ทางที่ 2: นับด้วย dict ธรรมดา
need = {} # ค่าที่ "รอคู่" -> เหลือกี่ตัว
ops = 0
for x in nums:
if need.get(______, 0) > 0: # (4) มีตัวที่รอจับกับ x อยู่ไหม
need[______] -= 1 # (5) ใช้มันไปหนึ่งตัว
ops += 1
else:
need[x] = need.get(x, 0) + 1 # x เข้าคิวรอคู่
return opsneed.get(key, 0) แปลว่า "ไปอ่านค่าที่ key นี้ ถ้ายังไม่เคยมี key นี้เลยให้ถือว่าเป็น 0" กันไม่ให้โปรแกรม error ตอนไปอ่าน key ที่ยังไม่เคยสร้าง
ไล่ทีละสเต็ปด้วยมือ (dry run)
แบบ two pointers บน nums = [3, 1, 3, 4, 3], k = 6 — sort ก่อนได้ [1, 3, 3, 3, 4]
| left (ค่า) | right (ค่า) | ผลรวม | เทียบ k = 6 | ทำอะไร | ops |
|---|---|---|---|---|---|
| 0 (1) | 4 (4) | 5 | น้อยไป | left → (อยากได้ค่าซ้ายใหญ่ขึ้น) | 0 |
| 1 (3) | 4 (4) | 7 | มากไป | right ← (อยากได้ค่าขวาเล็กลง) | 0 |
| 1 (3) | 3 (3) | 6 | เท่ากัน ✅ | นับ 1 คู่ แล้วขยับทั้งสองตัว | 1 |
| 2 (3) | 2 (3) | — | — | left == right → ออกจาก loop | 1 |
ตอบ 1 ตรงกับ Example 2 — และเห็นชัดว่าเลข 3 ที่เหลือตัวเดียวจับกับใครไม่ได้
แบบนับด้วย dict บนชุดเดิม [3, 1, 3, 4, 3], k = 6 (ไม่ต้อง sort ไล่ตามลำดับที่ให้มาเลย)
| x | หา need[6 − x] | มีไหม | ทำอะไร | need หลังรอบ | ops |
|---|---|---|---|---|---|
| 3 | need[3] | ไม่มี | 3 เข้าคิวรอคู่ | {3: 1} | 0 |
| 1 | need[5] | ไม่มี | 1 เข้าคิวรอคู่ | {3: 1, 1: 1} | 0 |
| 3 | need[3] | มี ✅ | จับคู่ (3,3) ลด need[3] | {3: 0, 1: 1} | 1 |
| 4 | need[2] | ไม่มี | 4 เข้าคิวรอคู่ | {3: 0, 1: 1, 4: 1} | 1 |
| 3 | need[3] | ไม่มี (เหลือ 0) | 3 เข้าคิวรอคู่ | {3: 1, 1: 1, 4: 1} | 1 |
ทำไมมันถูกต้อง: โจทย์นี้ไม่มีอะไรให้เลือกเลย
คำว่า "ให้ได้มากที่สุด" ในโจทย์ทำให้ดูเหมือนเป็นปัญหาที่ต้องเลือกให้ฉลาด — เลือกผิดคู่ตอนต้นแล้วอาจเสียคู่ตอนท้าย เหมือนโจทย์จับคู่ทั่วไป แต่ข้อนี้ไม่ใช่ และเข้าใจข้อนี้ได้แล้วโจทย์จะยุบทั้งข้อ
เหตุผล: เลข x จับได้กับ k − x เท่านั้น ค่าเดียว ไม่มีตัวเลือกที่สอง ดังนั้นตัวเลขทั้งหมดจึงถูกแบ่งเป็น กลุ่มที่ไม่เกี่ยวข้องกันเลย โดยแต่ละกลุ่มมีแค่สองค่าคือ x กับ k − x การตัดสินใจในกลุ่มหนึ่งไม่กระทบอีกกลุ่มแม้แต่นิดเดียว
nums = [1, 5, 2, 4, 2, 4, 4, 3] k = 6
แบ่งเป็นกลุ่มที่ไม่เกี่ยวกันเลย:
กลุ่ม (1, 5) : มี 1 อยู่ 1 ตัว , มี 5 อยู่ 1 ตัว -> จับได้ min(1, 1) = 1 คู่
กลุ่ม (2, 4) : มี 2 อยู่ 2 ตัว , มี 4 อยู่ 3 ตัว -> จับได้ min(2, 3) = 2 คู่
กลุ่ม (3, 3) : มี 3 อยู่ 1 ตัว (x = k - x) -> จับได้ 1 // 2 = 0 คู่
รวม = 1 + 2 + 0 = 3 คู่ <- ไม่มีการเลือกใดที่ทำให้ได้มากกว่านี้ดูบรรทัด "กลุ่ม (2, 4)" ให้ดี: มีเลข 2 อยู่สองตัวและเลข 4 อยู่สามตัว จับได้มากสุด 2 คู่ ไม่ว่าจะจับตัวไหนกับตัวไหน เพราะ 2 หมดก่อน เลข 4 ที่เหลืออีกตัวจับกับใครไม่ได้เลยเนื่องจากคู่ของมันคือ 2 ซึ่งหมดแล้ว จำนวนคู่ของแต่ละกลุ่มจึงเป็น min(จำนวนของ x, จำนวนของ k − x) ตายตัว ไม่ใช่ผลของการเลือกที่ดี
กรณีพิเศษคือกลุ่มที่ x = k − x (คู่ของมันคือตัวมันเอง เช่น k = 6 กับ x = 3) กลุ่มนี้ต้องใช้ของสองตัวจากกองเดียวกัน จึงได้จำนวนคู่เท่ากับ จำนวนของ x หารสองปัดลง เช่นมี 3 อยู่ห้าตัวก็ได้ 2 คู่ เหลือเศษหนึ่งตัว — ตรงกับ Example 3 ในโจทย์ที่มีเลข 2 สามตัวแล้วจับได้แค่คู่เดียว
เมื่อคำตอบเป็นผลรวมของ min ของแต่ละกลุ่มแบบตายตัว วิธีใดก็ตามที่จับคู่ให้ครบทุกกลุ่มจะได้คำตอบเดียวกันหมด นั่นคือเหตุผลว่าทำไม two pointers บน array ที่เรียงแล้ว และการนับด้วย dict ซึ่งเดินคนละทางเลย จึงตอบตรงกันทุกเคส — ไม่ใช่เรื่องบังเอิญ
แล้วข้อนี้ sort ได้ ทั้งที่หน้าแนวคิดบอกว่าบางข้อ sort ไม่ได้
หน้าแนวคิดของหมวดนี้ให้คำถามคัดกรองไว้ว่า ก่อน sort ต้องเช็คว่าโจทย์ขอ index เดิมหรือลำดับเดิมหรือเปล่า ข้อนี้ผ่านคำถามนั้นสบาย เพราะโจทย์ขอแค่ จำนวนคู่ ซึ่งเป็นตัวเลขตัวเดียว ไม่ได้ขอว่าคู่ไหนอยู่ตำแหน่งไหน และไม่ได้ขอให้คงลำดับอะไรไว้ ลำดับเดิมของ nums จึงไม่มีความหมายกับคำตอบเลย เราจึงเรียงมันได้อย่างอิสระ
เทียบกับ LC1 Two Sum ที่หน้าแนวคิดยกเป็นตัวอย่างว่า sort ไม่ได้ — ข้อนั้นต้อง return index ของตัวเลขในแถวเดิม การเรียงจึงทำลายคำตอบทิ้ง ความต่างของสองข้ออยู่ที่สิ่งที่โจทย์ขอให้ return เท่านั้น ไม่ได้อยู่ที่ตัวอัลกอริทึม
nums.sort() ไม่ได้สร้างแถวใหม่ มันเรียง แถวเดิมของคนที่เรียกฟังก์ชัน ทิ้งเลย แปลว่าหลังเรียกฟังก์ชันนี้ ตัวแปรของผู้เรียกจะถูกสลับลำดับไปแล้วโดยที่ชื่อฟังก์ชันไม่ได้บอกใบ้ไว้เลย ในโจทย์ LeetCode ไม่มีใครเดือดร้อน แต่ในโค้ดจริงถือเป็นพฤติกรรมที่ควรเลี่ยง เพราะฟังก์ชันที่ชื่อว่า "นับจำนวนคู่" ไม่ควรไปแก้ข้อมูลของคนอื่น ถ้าต้องการเลี่ยงให้เขียน nums = sorted(nums) แทน sort() ซึ่งทำสำเนาให้ก่อนเรียง (แลกกับพื้นที่เพิ่มเท่ากับขนาดแถว) หรือถ้าอ่านโค้ดคนอื่นแล้วเห็นการส่ง nums[:] เข้าไป ก็ให้รู้ว่าเขากำลังส่งสำเนาเพื่อป้องกันเรื่องนี้อยู่ — เดี๋ยวจะเห็นในโค้ดทดสอบของหน้านี้เอง
🔓 เปิดเฉลยเต็ม (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู
ไอเดียหนึ่งบรรทัด: เลข x จับได้กับ k − x เท่านั้น จึงไม่มีอะไรต้องเลือก — แค่จับให้ครบทุกคู่ที่มีอยู่ ทำได้ทั้งด้วย two pointers บน array ที่ sort แล้ว หรือด้วยการนับด้วย dict
class Solution:
def maxOperations(self, nums: List[int], k: int) -> int:
nums.sort() # (1) แบบที่ 1 ต้องการข้อมูลที่เรียงแล้ว
left, right = 0, len(nums) - 1
ops = 0
while left < right: # (2) ยังเหลือเลขให้จับอย่างน้อย 2 ตัว
total = nums[left] + nums[right]
if total == k: # (3) เจอคู่
ops += 1
left += 1 # (4) เอาออกทั้งสองตัว
right -= 1
elif total < k: # (5) น้อยไป ต้องการค่าซ้ายที่ใหญ่ขึ้น
left += 1
else: # (6) มากไป ต้องการค่าขวาที่เล็กลง
right -= 1
return opsข้อนี้เรียงข้อมูลก่อนได้เพราะโจทย์ขอแค่ จำนวนคู่ ไม่ได้ขอตำแหน่งเดิม ซึ่งตรงกับคำถามคัดกรองข้อแรกในส่วนที่ 6 ของหน้าแนวคิดพอดี · ถ้าโจทย์เปลี่ยนเป็นขอตำแหน่งของคู่ ท่านี้จะใช้ไม่ได้ทันที และต้องเปลี่ยนไปใช้ hash map
| บรรทัด | โค้ด | ทำอะไร / ทำไมต้องมี |
|---|---|---|
| (1) | nums.sort() | หัวใจที่ทำให้การตัดสินใจในบรรทัด (5)/(6) ถูกต้อง — ถ้าไม่เรียง การรู้ว่าผลรวมน้อยไปจะไม่ได้บอกอะไรเลยว่าควรขยับตัวไหน |
| (2) | while left < right | < ไม่ใช่ <= เพราะเลขตัวเดียวจับคู่กับตัวเองไม่ได้ (มันมีอยู่ตัวเดียวใน array) |
| (3) | if total == k | เจอคู่ที่ใช้ได้ — และเพราะ x จับได้กับ k − x เท่านั้น เราจับเลยได้ ไม่ต้องกลัวเสียโอกาสให้คู่อื่น |
| (4) | left += 1 และ right -= 1 | แทนการ "เอาออกจาก array" ทั้งสองตัว จุดพลาดอันดับหนึ่งคือขยับแค่ตัวเดียว → นับเกิน |
| (5) | elif total < k: left += 1 | nums[left] เล็กเกินไป — มันจับกับตัวที่ใหญ่สุดที่เหลืออยู่ยังไม่ถึง k ก็ไม่มีตัวไหนช่วยได้อีก ตัดทิ้ง |
| (6) | else: right -= 1 | ผลรวมเกิน k → nums[right] ใหญ่เกินไปแม้จับกับตัวเล็กสุดที่เหลือ จึงไม่มีคู่ให้มันเลย ตัดทิ้ง |
ทำไม greedy นี้ให้จำนวนคู่มากที่สุดจริง: เพราะ "ใครจับกับใครได้" ในโจทย์นี้แยกเป็นกลุ่มเล็ก ๆ ที่ไม่เกี่ยวกันเลย — ค่า x เกี่ยวกับ k − x เท่านั้น ถ้าค่า x มี a ตัวและ k − x มี b ตัว จำนวนคู่ในกลุ่มนั้นคือ min(a, b) ตายตัว ไม่มีการเลือกที่ทำให้ดีกว่านี้ได้ (กรณีพิเศษ x = k − x ได้ a หาร 2 แบบปัดลง คู่)
class Solution:
def maxOperations(self, nums: List[int], k: int) -> int:
need = {} # ค่าที่ยัง "รอคู่" -> เหลือกี่ตัว
ops = 0
for x in nums:
if need.get(k - x, 0) > 0: # มีตัวที่รอจับกับ x อยู่ไหม
need[k - x] -= 1 # ใช้มันไปหนึ่งตัว เท่ากับจับคู่สำเร็จ
ops += 1
else:
need[x] = need.get(x, 0) + 1 # ยังไม่มีคู่ ให้ x เข้าคิวรอ
return opsคำตอบที่ 2 เร็วกว่าเพราะไม่ต้องเรียงข้อมูล จึงเป็น O(n) แทน O(n log n) แต่มัน ไม่ใช่ท่าของหมวดนี้ · ถ้ากำลังฝึกหมวด Two Pointers ให้ส่งคำตอบที่ 1 เพราะเป้าหมายคือฝึกท่าให้ติดมือ ไม่ใช่หาโค้ดที่เร็วที่สุด · เก็บคำตอบที่ 2 ไว้เป็นตัวเทียบ แล้วจะเจอท่านี้เต็ม ๆ อีกครั้งในหมวด Hash Map / Set
สองวิธีให้คำตอบตรงกันทุกเคส เลือกใช้ตามข้อจำกัด: ถ้าห่วง memory หรือ array เรียงมาแล้วให้ใช้ two pointers ถ้าห่วงความเร็วและ n ใหญ่มากให้ใช้การนับด้วย dict (ไม่ต้องเสียเวลาไปกับการ sort ก่อน)
ขั้นสูง (ทางเลือก): ย่อโค้ดด้วย defaultdict
แบบ need.get(x, 0) ข้างบนเขียนได้ยาวไปหน่อยเวลาต้องอ่านและเขียนค่าซ้ำ ๆ ในไลบรารีมาตรฐานของ Python มี defaultdict ที่ช่วยให้ไม่ต้องเขียน .get(..., 0) ทุกครั้ง — พอเราอ่าน key ที่ไม่เคยมี มันจะสร้าง key นั้นให้อัตโนมัติด้วยค่าเริ่มต้น 0 ให้เลย
from collections import defaultdict
def max_operations_hash_v2(nums: list[int], k: int) -> int:
need = defaultdict(int) # เหมือน dict ธรรมดา แต่อ่าน key ใหม่แล้วได้ 0 อัตโนมัติ
ops = 0
for x in nums:
if need[k - x] > 0:
need[k - x] -= 1
ops += 1
else:
need[x] += 1
return ops
print(max_operations_hash_v2([3, 1, 3, 4, 3], 6))1ข้อควรระวังเล็กน้อย: การอ่าน need[k - x] จะสร้าง key นั้นขึ้นมาด้วยค่า 0 ทันทีแม้แค่ "ดู" เฉย ๆ ทำให้ dict โตกว่าที่จำเป็นเล็กน้อย แต่ไม่กระทบความถูกต้องของคำตอบ
"หาคู่ที่บวกกันได้ค่าเป้าหมาย" มีสองท่ามาตรฐานที่ต้องมีติดตัว: sort + two pointers (ประหยัด memory) และนับด้วย dict (เร็วกว่า) เจอโจทย์ pair sum ให้นึกถึงสองท่านี้ทันที
LC1 Two Sum (นับด้วย dict), LC167 Two Sum II (two pointers บน array ที่เรียงแล้ว), LC15 3Sum (fix ตัวหนึ่งแล้ว two pointers), LC532 K-diff Pairs