Skip to content

Commit 00429f7

Browse files
committed
feat(day 131): implement pairwise index sum finder with used-tracking to avoid double counting
1 parent ad010f6 commit 00429f7

1 file changed

Lines changed: 288 additions & 0 deletions

File tree

FreeCodeCamp/CodingQ/Pairwise.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
"""
2+
3+
Pairwise
4+
Given an array of integers and a target number, find all pairs of elements in the array whose values add up to the target and return the sum of their indices.
5+
6+
For example, given [2, 3, 4, 6, 8] and 10, you will find two valid pairs:
7+
8+
2 and 8 (2 + 8 = 10), whose indices are 0 and 4
9+
4 and 6 (4 + 6 = 10), whose indices are 2 and 3
10+
Add all the indices together to get a return value of 9.
11+
"""
12+
13+
import unittest
14+
15+
class PairwiseTest(unittest.TestCase):
16+
17+
def test1(self):
18+
self.assertEqual(pairwise([2, 3, 4, 6, 8], 10), 9)
19+
20+
def test2(self):
21+
self.assertEqual(pairwise([4, 1, 5, 2, 6, 3], 7), 15)
22+
23+
def test3(self):
24+
self.assertEqual(pairwise([-30, -15, 5, 10, 15, -5 , 20, -40], -20), 22)
25+
26+
def test4(self):
27+
self.assertEqual(pairwise([7, 9, 13, 19, 21, 6, 3, 1, 4, 8, 12, 22], 24), 10)
28+
29+
30+
def pairwise_bruteforce(arr, target):
31+
32+
freq = dict()
33+
summ = 0
34+
35+
for i in range(len(arr)):
36+
for j in range(i+1, len(arr)):
37+
if arr[i] + arr[j] == target :
38+
freq[i] = j
39+
40+
print(freq)
41+
42+
for key, value in freq.items():
43+
summ += key + value
44+
45+
return summ
46+
47+
"""
48+
49+
This brute force solution has some issues
50+
51+
=> this solution overwrite freq[i] each, time, so if multiple pairs exist for the same i, only the last one is kept.
52+
=> Here no marking of elements as "used", so technically the same element could be reused in another pair if you extended this.
53+
=> The above test cases will pass but the solution itself is fragile.
54+
55+
The real time example:
56+
arr = [1, 9, 9, 1]
57+
target = 10
58+
59+
i = 0, j = 1 -> 1 + 9 = 10 -> freq[0] = 1
60+
i = 0, j = 2 -> 1 + 9 = 10 -> overwrites freq[0] = 2 (previous pair lost)
61+
i = 3, j = 1 -> 1 + 9 = 10 -> freq[3] = 1
62+
i = 3, j = 2 -> 1 + 9 = 10 -> overwrites freq[3] = 2
63+
64+
Final freq = {0: 2, 3: 2} -> Only last matches kept, earlier valid paris discarded.
65+
Flaw: You lose information because dictioanary keys must be unique. Multiple valid pairs for the same i overwrite each other.
66+
"""
67+
68+
69+
70+
71+
def pairwise_optimal(arr, target):
72+
73+
freq = {}
74+
for i in range(len(arr)):
75+
freq[arr[i]] = i # maps value -> last index
76+
77+
78+
summ = 0
79+
res = {}
80+
81+
for i in range(len(arr)):
82+
compliment = target - arr[i]
83+
if compliment in freq and freq[compliment] != i:
84+
summ += i + freq[compliment]
85+
86+
"""
87+
This solution is partial but it has issues
88+
=> Overwritting indices:
89+
freq[arr[i]] = i stores only the last index of each value. if a value appears multiple times, earlier indices are lost.
90+
=> Double counting:
91+
when you loop over i, you find both (i, j) and (j, i) because the dictionary still contains the complement,
92+
That's why you get twice the sum.
93+
=> No "used" tracking:
94+
Once a pair is found, you don't mark those indices as consumed, so they can be reused.
95+
96+
The real time example:
97+
Build freq:
98+
{2 : 0, 8 : 1, 4 : 2, 6 : 3}
99+
Loop:
100+
=> i = 0 (val = 2), compliment = 8 -> found at index 1 -> summ += 0 + 1 = 1
101+
=> i = 1 (val = 8), compliment = 2 -> found at index 0 -> summ += 1 + 0 = 1 (double counted!)
102+
=> i = 2 (val = 4), compliment = 6 -> found at index 3 -> summ += 2 + 3 = 5
103+
=> i = 3 (val = 6), compliment = 4 -> found at index 2 -> summ += 3 + 2 = 5(double counte!)
104+
105+
Total = 12, but correct answer should be 6 (pairs (2, 8) and (4, 6)).
106+
Flaws:
107+
1. Double counting: Both(i, j) and (j, i) are added.
108+
2. Overwriting indices: if a value appears multile times, only the last index is stored.
109+
"""
110+
111+
def pairwise_optimal_corrected(arr, target):
112+
used = [False] * len(arr)
113+
114+
summ = 0
115+
index_map = {}
116+
117+
for i, val in enumerate(arr):
118+
index_map.setdefault(val, []).append(i)
119+
120+
for i, val in enumerate(arr):
121+
if used[i]:
122+
continue
123+
compliment = target - val
124+
if compliment in index_map:
125+
for j in index_map[compliment]:
126+
if j != i and not used[j]:
127+
summ += i + j
128+
used[i] = True
129+
used[j] = True
130+
break
131+
132+
return summ
133+
134+
"""
135+
The real time example:
136+
arr = [2, 8 , 4, 6]
137+
target = 10
138+
139+
=> i = 0 (val = 2), compliment = 8 -> j = 1 -> summ = 1, mark used[0] = True, used[1] = True
140+
=> i = 1 -> continue because used[1] = True -> skip
141+
=> i = 2 (val = 4), compliment = 6 -> j = 3 -> summ = 1 + 5 = 6, mark used[2] = True, used[3] = True
142+
143+
=> i = 3 -> continue because used[3] = True -> skip
144+
145+
Final sum = 6
146+
147+
The use of j!=i
148+
149+
Here, index_map[compliment] is a list of all indices where the complement value occurs.
150+
That list can include the current index i itself if arr[i] equals its own complement.
151+
152+
When does i == j happen
153+
154+
It happens when the element can pair with itself to reach the target.
155+
156+
Example 1: self-pair case
157+
158+
arr = [5]
159+
target = 10
160+
161+
=> i = 0, val = 5
162+
=> compliment = 10 - 5 = 5
163+
=> index_map[5] = [0]
164+
=> So j = 0 -> same as i.
165+
166+
If we didn't check j != i, we'd incorrectly count the element pairing with itself.
167+
168+
Example 2: Multiple identical values
169+
170+
arr = [5, 5]
171+
target = 10
172+
173+
=> i = 0, val = 5, compliment = 5
174+
=> index_map[5] = [0, 1]
175+
=> loop over j:
176+
=> j = 0 -> same as i -> skip because of j!=i
177+
=> j = 1 -> valid pair -> summ += 0 + 1
178+
=> Without j!=i, the first iteration would try to pair index 0 with itself
179+
180+
181+
* Why it matters:
182+
==> Correctness: Prevents self-paring unless there are actually two distinct elements.
183+
==> Safety: Avoids double counting when the complement equals the value itself.
184+
==> Logic: We only want pairs of two different indices.
185+
186+
* Key Takeaway:
187+
==> i == j happens when the value equals its own complement (e.g., target = 10, value = 5).
188+
==> The check j!=i ensures we don't pair an element with itself unless another copy exists.
189+
==> Yes, i always moves forward, but the inner loop iterates over all indices of the complement, which can include i itself. That's why the guard is necessary.
190+
191+
192+
The real time example:
193+
194+
arr = [5, 5, 10]
195+
target = 10
196+
197+
we want pairs that sum to 10.
198+
199+
Step 1: Build index_map
200+
201+
index_map = {
202+
5: [0, 1],
203+
10: [2]
204+
}
205+
206+
So value 5 occurs at indices 0 and 1, and value 10 occurs at index 2.
207+
208+
Step 2: Iterate over array
209+
210+
i = 0, val = 5
211+
=> compliment = 10 - 5 = 5
212+
=> index_map[5] = [0, 1]
213+
=> Loop over j:
214+
=> j = 0 -> same as i -> skip because of j!=i
215+
=> j = 1 -> valid pair -> summ += 0 + 1
216+
mark used[0] = True, used[1] = True
217+
218+
i = 1, val = 5
219+
=> used[1] = True -> continue (skip this index)
220+
i = 2, val = 10
221+
=> compliment = 10 - 10 = 0
222+
=> 0 not in index_map -> no pair
223+
224+
Step 3: Result
225+
=> Only one valid pair: indices (0, 1) -> sum = 1
226+
=> Without j!=i, the algorithm would have tried to pair index 0 with itself (0 + 0), which is invalid.
227+
228+
Which j!=i is essential
229+
=> It prevents self-pairing when the value equals its own complement (like 5 + 5 = 10).
230+
=> Ensures we only use two distinct indices.
231+
=> Works correctly even when duplicates exist.
232+
233+
234+
REAL-TIME ANALOGY:
235+
236+
Imagine you're pairing for a dance:
237+
238+
=> Each person has a "number" (array index).
239+
=> The target is the total score tey must reach together.
240+
=> If you allow someone to pair with themselves, they'd be dancing alone
241+
- which breaks the rule.
242+
That's why we check j!=i : to ensure two different people form the pair.
243+
244+
245+
"continue" means skip this person if they're already paired, and j!=i means don't let some pair with themselves.
246+
"""
247+
248+
249+
def pairwise(arr, target):
250+
251+
used = [False] * len(arr) # Tracking used elements
252+
total = 0
253+
254+
for i in range(len(arr)):
255+
if used[i]:
256+
continue
257+
for j in range(i + 1, len(arr)):
258+
if not used[j] and arr[i] + arr[j] == target:
259+
total += i + j
260+
used[i] = True
261+
used[j] = True
262+
break # move to next i after finding a pair
263+
264+
return total
265+
266+
"""
267+
This solution
268+
269+
=> Avoid double counting: Once an element is used in a pair, mark it as used so it doesn't get reused.
270+
=> Efficiency: This is O(n^2) in worst case, but fine for moderate input sizes.
271+
=> Correctness: Matches the example:
272+
pair(2, 8) -> indices 0 + 4 = 4
273+
pair(4, 6) -> indices 2 + 3 = 5
274+
Total = 9
275+
"""
276+
277+
"""
278+
Key Takeaways
279+
=> Brute force flaw: dictionary overwrites earlier pairs for same i.
280+
=> Optimal flaw: dictionary stores only last index per value, and double counts pairs.
281+
=> Correct fix: store all indices, and use a used array.
282+
continue means: if this index is already paired, skip the rest of the loop body and move to the next iteration.
283+
"""
284+
285+
if __name__ == "__main__":
286+
print(pairwise_optimal_corrected([2, 3, 4, 6, 8], 10))
287+
# unittest.main()
288+

0 commit comments

Comments
 (0)