diff --git a/config.json b/config.json index e4c24e87..27265e5e 100644 --- a/config.json +++ b/config.json @@ -889,6 +889,14 @@ "practices": [], "prerequisites": [], "difficulty": 3 + }, + { + "slug": "knapsack", + "name": "Knapsack", + "uuid": "254d5f19-d6fc-4c56-a8e2-f6d4e90ad4ec", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ] }, diff --git a/exercises/practice/knapsack/.docs/instructions.md b/exercises/practice/knapsack/.docs/instructions.md new file mode 100644 index 00000000..0ebf7914 --- /dev/null +++ b/exercises/practice/knapsack/.docs/instructions.md @@ -0,0 +1,25 @@ +# Instructions + +Your task is to determine which items to take so that the total value of her selection is maximized, taking into account the knapsack's carrying capacity. + +Items will be represented as a list of items. +Each item will have a weight and value. +All values given will be strictly positive. +Lhakpa can take only one of each item. + +For example: + +```text +Items: [ + { "weight": 5, "value": 10 }, + { "weight": 4, "value": 40 }, + { "weight": 6, "value": 30 }, + { "weight": 4, "value": 50 } +] + +Knapsack Maximum Weight: 10 +``` + +For the above, the first item has weight 5 and value 10, the second item has weight 4 and value 40, and so on. +In this example, Lhakpa should take the second and fourth item to maximize her value, which, in this case, is 90. +She cannot get more than 90 as her knapsack has a weight limit of 10. diff --git a/exercises/practice/knapsack/.docs/introduction.md b/exercises/practice/knapsack/.docs/introduction.md new file mode 100644 index 00000000..9ac9df59 --- /dev/null +++ b/exercises/practice/knapsack/.docs/introduction.md @@ -0,0 +1,10 @@ +# Introduction + +Lhakpa is a [Sherpa][sherpa] mountain guide and porter. +After months of careful planning, the expedition Lhakpa works for is about to leave. +She will be paid the value she carried to the base camp. + +In front of her are many items, each with a value and weight. +Lhakpa would gladly take all of the items, but her knapsack can only hold so much weight. + +[sherpa]: https://en.wikipedia.org/wiki/Sherpa_people#Mountaineering diff --git a/exercises/practice/knapsack/.meta/config.json b/exercises/practice/knapsack/.meta/config.json new file mode 100644 index 00000000..17df6e42 --- /dev/null +++ b/exercises/practice/knapsack/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "JohnMertz" + ], + "files": { + "solution": [ + "lib/Knapsack.pm" + ], + "test": [ + "t/knapsack.t" + ], + "example": [ + ".meta/solutions/lib/Knapsack.pm" + ] + }, + "blurb": "Given a knapsack that can only carry a certain weight, determine which items to put in the knapsack in order to maximize their combined value.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Knapsack_problem" +} diff --git a/exercises/practice/knapsack/.meta/solutions/lib/Knapsack.pm b/exercises/practice/knapsack/.meta/solutions/lib/Knapsack.pm new file mode 100644 index 00000000..b1c82bc4 --- /dev/null +++ b/exercises/practice/knapsack/.meta/solutions/lib/Knapsack.pm @@ -0,0 +1,53 @@ +package Knapsack; + +use strict; +use warnings; +use experimental qw; + +use Exporter qw; +our @EXPORT_OK = qw; + +sub maximum_value ( $items, $weight, $offset = 0, $best_score = 0, @indexes ) { + + # Loop through all unchecked combinations of @indexes (the list of + # items passed from the last level of recursion), plus each of the + # items which appear later in the array (from the $offset index). + foreach my $index ( $offset .. $#$items ) { + + # Reset local array of indexes from last loop iteration to that + # which was passed at the start of this level of recursion. + my @local_indexes = @indexes; + + # Skip if this item exceeds remaining weight. + next if ( $items->[$index]->{'weight'} > $weight ); + + # Otherwise add item to knapsack + push( @local_indexes, $index ); + + # Calculate sum for new array of indexes + my $score = 0; + $score += $items->[$_]->{'value'} foreach @local_indexes; + + # Overwrite $best_score if it has been beaten + $best_score = $score if ( $score > $best_score ); + + # Optimizations: Skip recursion if there is no capacity left + # or if there would be no additional items to check. + next if $weight == $items->[$index]->{'weight'}; + next if ( $index == $#$items ); + + # Recursively call this function to check the remaining items + # in combination with the current @local_indexes. Offset the + # starting index to be the item after this one. + $best_score = maximum_value( + $items, + $weight - $items->[$index]->{'weight'}, + ++$index, + $best_score, + @local_indexes + ); + } + return $best_score; +} + +1; diff --git a/exercises/practice/knapsack/.meta/solutions/t/knapsack.t b/exercises/practice/knapsack/.meta/solutions/t/knapsack.t new file mode 120000 index 00000000..1f742843 --- /dev/null +++ b/exercises/practice/knapsack/.meta/solutions/t/knapsack.t @@ -0,0 +1 @@ +../../../t/knapsack.t \ No newline at end of file diff --git a/exercises/practice/knapsack/.meta/template-data.yaml b/exercises/practice/knapsack/.meta/template-data.yaml new file mode 100644 index 00000000..14de5b6f --- /dev/null +++ b/exercises/practice/knapsack/.meta/template-data.yaml @@ -0,0 +1,60 @@ +subs: maximum_value + +properties: + maximumValue: + test: |- + use Data::Dmp; + sprintf(<<'END', dmp($case->{input}{items}), $case->{input}{maximumWeight}, $case->{expected}, dmp($case->{description})); + is( + maximum_value(%s, %s), + %s, + %s, + ); + END + +stub: |- + sub maximum_value ($items, $weight) { + return undef; + } + +example: |- + sub maximum_value ($items, $weight, $offset=0, $best_score=0, @indexes) { + + # Loop through all unchecked combinations of @indexes (the list of + # items passed from the last level of recursion), plus each of the + # items which appear later in the array (from the $offset index). + foreach my $index ($offset..$#$items) { + # Reset local array of indexes from last loop iteration to that + # which was passed at the start of this level of recursion. + my @local_indexes = @indexes; + + # Skip if this item exceeds remaining weight. + next if ($items->[$index]->{'weight'} > $weight); + + # Otherwise add item to knapsack + push (@local_indexes, $index); + + # Calculate sum for new array of indexes + my $score = 0; + $score += $items->[$_]->{'value'} foreach @local_indexes; + # Overwrite $best_score if it has been beaten + $best_score = $score if ($score > $best_score); + + # Optimizations: Skip recursion if there is no capacity left + # or if there would be no additional items to check. + next if $weight == $items->[$index]->{'weight'}; + next if ($index == $#$items); + + # Recursively call this function to check the remaining items + # in combination with the current @local_indexes. Offset the + # starting index to be the item after this one. + $best_score = maximum_value( + $items, + $weight - $items->[$index]->{'weight'}, + ++$index, + $best_score, + @local_indexes + ); + } + return $best_score; + } diff --git a/exercises/practice/knapsack/.meta/tests.toml b/exercises/practice/knapsack/.meta/tests.toml new file mode 100644 index 00000000..8e013ef1 --- /dev/null +++ b/exercises/practice/knapsack/.meta/tests.toml @@ -0,0 +1,36 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[a4d7d2f0-ad8a-460c-86f3-88ba709d41a7] +description = "no items" +include = false + +[3993a824-c20e-493d-b3c9-ee8a7753ee59] +description = "no items" +reimplements = "a4d7d2f0-ad8a-460c-86f3-88ba709d41a7" + +[1d39e98c-6249-4a8b-912f-87cb12e506b0] +description = "one item, too heavy" + +[833ea310-6323-44f2-9d27-a278740ffbd8] +description = "five items (cannot be greedy by weight)" + +[277cdc52-f835-4c7d-872b-bff17bab2456] +description = "five items (cannot be greedy by value)" + +[81d8e679-442b-4f7a-8a59-7278083916c9] +description = "example knapsack" + +[f23a2449-d67c-4c26-bf3e-cde020f27ecc] +description = "8 items" + +[7c682ae9-c385-4241-a197-d2fa02c81a11] +description = "15 items" diff --git a/exercises/practice/knapsack/lib/Knapsack.pm b/exercises/practice/knapsack/lib/Knapsack.pm new file mode 100644 index 00000000..c2104eda --- /dev/null +++ b/exercises/practice/knapsack/lib/Knapsack.pm @@ -0,0 +1,12 @@ +package Knapsack; + +use v5.40; + +use Exporter qw; +our @EXPORT_OK = qw; + +sub maximum_value ( $items, $weight ) { + return undef; +} + +1; diff --git a/exercises/practice/knapsack/t/knapsack.t b/exercises/practice/knapsack/t/knapsack.t new file mode 100755 index 00000000..82d442bd --- /dev/null +++ b/exercises/practice/knapsack/t/knapsack.t @@ -0,0 +1,51 @@ +#!/usr/bin/env perl +use Test2::V0; + +use FindBin qw<$Bin>; +use lib "$Bin/../lib", "$Bin/../local/lib/perl5"; + +use Knapsack qw; + +is( # begin: 3993a824-c20e-493d-b3c9-ee8a7753ee59 + maximum_value( [], 100 ), + 0, + "no items", +); # end: 3993a824-c20e-493d-b3c9-ee8a7753ee59 + +is( # begin: 1d39e98c-6249-4a8b-912f-87cb12e506b0 + maximum_value( [ { value => 1, weight => 100 } ], 10 ), + 0, + "one item, too heavy", +); # end: 1d39e98c-6249-4a8b-912f-87cb12e506b0 + +is( # begin: 833ea310-6323-44f2-9d27-a278740ffbd8 + maximum_value( [ { value => 5, weight => 2 }, { value => 5, weight => 2 }, { value => 5, weight => 2 }, { value => 5, weight => 2 }, { value => 21, weight => 10 } ], 10 ), + 21, + "five items (cannot be greedy by weight)", +); # end: 833ea310-6323-44f2-9d27-a278740ffbd8 + +is( # begin: 277cdc52-f835-4c7d-872b-bff17bab2456 + maximum_value( [ { value => 20, weight => 2 }, { value => 20, weight => 2 }, { value => 20, weight => 2 }, { value => 20, weight => 2 }, { value => 50, weight => 10 } ], 10 ), + 80, + "five items (cannot be greedy by value)", +); # end: 277cdc52-f835-4c7d-872b-bff17bab2456 + +is( # begin: 81d8e679-442b-4f7a-8a59-7278083916c9 + maximum_value( [ { value => 10, weight => 5 }, { value => 40, weight => 4 }, { value => 30, weight => 6 }, { value => 50, weight => 4 } ], 10 ), + 90, + "example knapsack", +); # end: 81d8e679-442b-4f7a-8a59-7278083916c9 + +is( # begin: f23a2449-d67c-4c26-bf3e-cde020f27ecc + maximum_value( [ { value => 350, weight => 25 }, { value => 400, weight => 35 }, { value => 450, weight => 45 }, { value => 20, weight => 5 }, { value => 70, weight => 25 }, { value => 8, weight => 3 }, { value => 5, weight => 2 }, { value => 5, weight => 2 } ], 104 ), + 900, + "8 items", +); # end: f23a2449-d67c-4c26-bf3e-cde020f27ecc + +is( # begin: 7c682ae9-c385-4241-a197-d2fa02c81a11 + maximum_value( [ { value => 135, weight => 70 }, { value => 139, weight => 73 }, { value => 149, weight => 77 }, { value => 150, weight => 80 }, { value => 156, weight => 82 }, { value => 163, weight => 87 }, { value => 173, weight => 90 }, { value => 184, weight => 94 }, { value => 192, weight => 98 }, { value => 201, weight => 106 }, { value => 210, weight => 110 }, { value => 214, weight => 113 }, { value => 221, weight => 115 }, { value => 229, weight => 118 }, { value => 240, weight => 120 } ], 750 ), + 1458, + "15 items", +); # end: 7c682ae9-c385-4241-a197-d2fa02c81a11 + +done_testing;