1 changed files with 236 additions and 0 deletions
@ -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"]) |
Loading…
Reference in new issue