diff --git a/closest_2sum.nim b/closest_2sum.nim new file mode 100644 index 0000000..ef3d254 --- /dev/null +++ b/closest_2sum.nim @@ -0,0 +1,236 @@ +import os, system, strutils, strformat, sequtils, algorithm, memfiles, parseopt, tables + +type + Gift = tuple + name: string + price: int + +type + Solution = tuple + first: Gift + second: Gift + +proc `<`(a, b: Gift): bool = + a.price < b.price + +proc parseGifts(fileName : string) : seq[Gift] = + var gifts : seq[Gift] + + for gift in fileName.readFile.splitLines: + let splitted = gift.split(",") + if splitted.len == 2: + gifts &= (splitted[0].strip, splitted[1].strip.parseInt) + gifts + +proc chooseGifts3(fileName : string, targetPrice : int) : Solution = + # version that uses two indexes into the array and progressively moves them towards each other + var gifts : seq[Gift] = fileName.parseGifts + + var low : int + var high = gifts.len - 1 + var currentMax : int + + while low < high: + var sum = gifts[low].price + gifts[high].price + if sum == targetPrice: + # handle the case where they're exact, and break early + result = (gifts[low], gifts[high]) + break + elif sum < targetPrice: + # the sum was less than the target price + # keep it as a candidate, + # but keep moving the low index up to try and find a better one + if sum > currentMax: + currentMax = sum + result = (gifts[low], gifts[high]) + # the logic here is that it's impossible for anything lower + # to sum to something higher than what we already have + # since it would be a + b vs a + c where c > b and a < c + # so we can safely ignore entries lower while still moving the high index downwards + low += 1 + else: + # skip ones that are too large to fit + high -= 1 + +proc chooseGifts2(fileName : string, targetPrice : int) : Solution = + # iterate over the gifts and then use binary search + # with a custom comparator to find the closest element that sums to <= target + # keep track of the current max sum and replace if we find a greater one + + var gifts : seq[Gift] = fileName.parseGifts + let buckets = gifts.zip(gifts[1..^1]).reversed + var currentMax : int = 0 + + for gift in gifts: + proc compareGifts(giftPair : tuple[a : Gift, b : Gift], target : int) : int = + if giftPair.a == gift: + # ignore the gift that we are already looking at + # this avoids the problem of duplicates + if target >= giftPair.b.price: + return 1 + else: + return -1 + + if target >= giftPair.b.price: + return 1 + + if target < giftPair.a.price: + return -1 + + if (target >= giftPair.a.price) and (target < giftPair.b.price): + return 0 + + if gift.price >= targetPrice: + continue + + let candidateIndex = buckets.binarySearch(targetPrice - gift.price, compareGifts) + + if (candidateIndex == -1): + continue + + if currentMax < (gift.price + buckets[candidateIndex].a.price): + currentMax = gift.price + buckets[candidateIndex].a.price + result = (gift, buckets[candidateIndex].a) + + assert(currentMax <= targetPrice) + +proc chooseGifts1(fileName : string, targetPrice : int) : Solution = + # solution based on repeatedly scanning the sequence of gifts + # not the most optimal solution + var currentMax : int = 0 + var currentGift : Gift + var gifts : seq[Gift] = fileName.parseGifts.reversed + + while gifts.len > 0: + currentGift = gifts[0] + + # if the current price is greater than the target price + # then it's impossible it fits, move to the next smallest + if currentGift.price > targetPrice: + gifts = gifts[1..^1] + continue + + # loop over all gifts after the current one + for gift in gifts[1..^1]: + + # if the currentGift plus the one after it is greater than our current max + # then there's no need to look at other pairs, move currentGift to the next one + # and keep trying + if (currentGift.price + gift.price) < currentMax: + break + + # if the currentGift plus the one we're looking at equals the target exactly + # then we're done, break + if currentGift.price + gift.price == targetPrice: + result = (currentGift, gift) + break + + # if the currentGift plus the one we're looking at is less than the target + # and greater than the currentMax, then it becomes our new max + if ((currentGift.price + gift.price) < targetPrice) and ((currentGift.price + gift.price) > currentMax): + result = (currentGift, gift) + currentMax = currentGift.price + gift.price + + # re-assign gifts to a smaller slice of gifts each time we've processed one + gifts = gifts[1..^1] + +proc choose3Gifts(fileName : string, targetPrice : int) : tuple[a : Gift, b: Gift, c: Gift] = + # Implementation of the 3 gift sum challenge + # The algorithm is quadratic + # The basic idea is similar to the chooseGifts3 proc but instead there is an outer for loop + # And we use the rest of the gifts as our search space + + var gifts : seq[Gift] = fileName.parseGifts + var sum : int + var currentMax : int + var start, ending : int + + for i in countup(0, gifts.len - 2): + # test the rest of them using the same algorithm as before + start = i + 1 + ending = gifts.len - 1 + + while start < ending: + sum = gifts[start].price + gifts[ending].price + gifts[i].price + if sum == targetPrice: + # handle the case where they're exact, and break early + return (gifts[i], gifts[start], gifts[ending]) + elif sum < targetPrice: + # the sum was less than the target price + # keep it as a candidate, + # but keep moving the low index up to try and find a better one + if sum > currentMax: + currentMax = sum + result = (gifts[i], gifts[start], gifts[ending]) + start += 1 + else: + # skip ones that are too large to fit + ending -= 1 + assert(currentMax <= targetPrice) + +proc outputSolution(version : string, + filename : string, + target : string) : string = + var solution : Solution + + case version: + of "1": + solution = filename.chooseGifts1(target.parseInt) + of "2": + solution = filename.chooseGifts2(target.parseInt) + of "3": + solution = filename.chooseGifts3(target.parseInt) + else: + echo fmt"There is no version {version}" + quit(1) + + # if both are set to 0 it means no solution was found + # these are just the default initialized values + if solution.first.price <= 0 or solution.second.price <= 0: + "Not possible" + else: + let gifts = [solution.first, solution.second].sorted + fmt"{gifts[0].name} {gifts[0].price}, {gifts[1].name} {gifts[1].price}" + +when isMainModule: + var args = initOptParser(commandLineParams().join(" ")) + var params = initTable[string, string]() + let validArgs = @["v", "version", "f", "filename", "t", "target"] + var currentKey : string + + while true: + args.next() + case args.kind + of cmdEnd: break + of cmdShortOption, cmdLongOption: + if args.val == "": + continue + else: + if validArgs.contains(args.key): + params[args.key] = args.val + of cmdArgument: + if validArgs.contains(currentKey): + params[currentKey] = args.val + + if params.hasKey("v"): + params["version"] = params["v"] + if params.hasKey("f"): + params["filename"] = params["f"] + if params.hasKey("t"): + params["target"] = params["t"] + + if params.hasKey("version") and params["version"] == "4": + let gifts = params["filename"].choose3Gifts(params["target"].parseInt) + if [gifts.a.price, gifts.b.price, gifts.c.price].all(proc (p : int) : bool = p == 0): + echo "Not possible" + else: + echo fmt"{gifts.a.name} {gifts.a.price}, {gifts.b.name} {gifts.b.price}, {gifts.c.name} {gifts.c.price}" + quit(0) + + if not (params.hasKey("version") and + params.hasKey("filename") and + params.hasKey("target")): + echo "Invalid parameters" + quit(1) + + echo outputSolution(params["version"], params["filename"], params["target"])