MoonBit Pearls Vol.03:01背包问题
01背包问题是算法竞赛中经典的dp题目。文中总共包含五个版本的代码。从最朴素的枚举法开始,在不断的改进下,最终变成了dp解法。
问题定义
有若干个物品,每件物品的有重量weight
和价值value
:
struct Item {
Int
weight : Int
Int
Int
value : Int
Int
}
现在,给定一个物品列表items
,和背包的容量capacity
。从中选出若干件物品,使得这些物品的总重量不超过背包的容量,且物品的总价值最大。
typealias enum @list.List[A] {
Empty
More(A, tail~ : @list.List[A])
}
@list.T as List
let @list.List[Item]
items_1 : enum @list.List[A] {
Empty
More(A, tail~ : @list.List[A])
}
List[struct Item {
weight: Int
value: Int
}
Item] = (arr : FixedArray[Item]) -> @list.List[Item]
Create a list from a FixedArray.
Converts a FixedArray into a list with the same elements in the same order.
Example
let ls = @list.of([1, 2, 3, 4, 5])
assert_eq(ls.to_array(), [1, 2, 3, 4, 5])
@list.of([
{ Int
weight: 7, Int
value: 20 },
{ Int
weight: 4, Int
value: 10 },
{ Int
weight: 5, Int
value: 11 },
])
以上面的items_1
为例,假设背包容量是,那么最优的方案是选取后两个物品,占用的容量,总共有点价值。
注意,由于我们不能把物品切割,因此优先挑选性价比最高的物品并非正解。例如,在上面的例子中,若选取了性价比最高的物品1,则只有点价值,而此时背包已经放不下其他物品了。
问题建模
我们先定义一些基础的对象与操作。
//物品的组合,下文简称组合
struct Combination {
@list.List[Item]
items : enum @list.List[A] {
Empty
More(A, tail~ : @list.List[A])
}
List[struct Item {
weight: Int
value: Int
}
Item]
Int
total_weight : Int
Int
Int
total_value : Int
Int
}
//空的组合
let Combination
empty_combination : struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination = {
@list.List[Item]
items: () -> @list.List[Item]
Creates an empty list.
Example
let ls : List[Int] = @list.empty()
assert_eq(ls.length(), 0)
@list.empty(),
Int
total_weight: 0,
Int
total_value: 0,
}
//往组合中添加物品,得到新的组合
fn struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination::(self : Combination, item : Item) -> Combination
add(Combination
self : struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination, Item
item : struct Item {
weight: Int
value: Int
}
Item) -> struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination {
{
@list.List[Item]
items: Combination
self.@list.List[Item]
items.(self : @list.List[Item], head : Item) -> @list.List[Item]
Add an element to the front of the list.
This is an alias for prepend
- it creates a new list with the given
element added to the beginning.
Example
let ls = @list.of([2, 3, 4]).add(1)
assert_eq(ls, @list.of([1, 2, 3, 4]))
add(Item
item),
Int
total_weight: Combination
self.Int
total_weight (self : Int, other : Int) -> Int
Adds two 32-bit signed integers. Performs two's complement arithmetic, which
means the operation will wrap around if the result exceeds the range of a
32-bit integer.
Parameters:
self
: The first integer operand.
other
: The second integer operand.
Returns a new integer that is the sum of the two operands. If the
mathematical sum exceeds the range of a 32-bit integer (-2,147,483,648 to
2,147,483,647), the result wraps around according to two's complement rules.
Example:
inspect(42 + 1, content="43")
inspect(2147483647 + 1, content="-2147483648") // Overflow wraps around to minimum value
+ Item
item.Int
weight,
Int
total_value: Combination
self.Int
total_value (self : Int, other : Int) -> Int
Adds two 32-bit signed integers. Performs two's complement arithmetic, which
means the operation will wrap around if the result exceeds the range of a
32-bit integer.
Parameters:
self
: The first integer operand.
other
: The second integer operand.
Returns a new integer that is the sum of the two operands. If the
mathematical sum exceeds the range of a 32-bit integer (-2,147,483,648 to
2,147,483,647), the result wraps around according to two's complement rules.
Example:
inspect(42 + 1, content="43")
inspect(2147483647 + 1, content="-2147483648") // Overflow wraps around to minimum value
+ Item
item.Int
value,
}
}
//两个组合等效,意思是它们总价值一样
impl trait Eq {
op_equal(Self, Self) -> Bool
}
Trait for types whose elements can test for equality
Eq for struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination with (self : Combination, other : Combination) -> Bool
op_equal(Combination
self, Combination
other) {
Combination
self.Int
total_value (self : Int, other : Int) -> Bool
Compares two integers for equality.
Parameters:
self
: The first integer to compare.
other
: The second integer to compare.
Returns true
if both integers have the same value, false
otherwise.
Example:
inspect(42 == 42, content="true")
inspect(42 == -42, content="false")
== Combination
other.Int
total_value
}
//比较两个组合的大小,就是比较它们总价值的大小
impl trait Compare {
compare(Self, Self) -> Int
}
Trait for types whose elements are ordered
The return value of [compare] is:
- zero, if the two arguments are equal
- negative, if the first argument is smaller
- positive, if the first argument is greater
Compare for struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination with (self : Combination, other : Combination) -> Int
compare(Combination
self, Combination
other) {
Combination
self.Int
total_value.(self : Int, other : Int) -> Int
Compares two integers and returns their relative order.
Parameters:
self
: The first integer to compare.
other
: The second integer to compare against.
Returns an integer indicating the relative order:
- A negative value if
self
is less than other
- Zero if
self
equals other
- A positive value if
self
is greater than other
Example:
let a = 42
let b = 24
inspect(a.compare(b), content="1") // 42 > 24
inspect(b.compare(a), content="-1") // 24 < 42
inspect(a.compare(a), content="0") // 42 = 42
compare(Combination
other.Int
total_value)
}
然后,我们就可以开始思考如何解决问题了。
一、朴素的枚举
枚举法是最朴素的方案,我们依照问题的定义,一步一步执行,就能得到答案:
- 枚举出所有的组合;
- 过滤出其中有效的组合,也就是那些能装入背包的;
- 答案是其中总价值最大的那个。
得益于标准库提供的两个函数,我们可以将上面三行文字一比一地翻译为MoonBit代码。其中all_combinations
是我们后续需要实现的函数,它的类型是(List[Item]) -> List[Combination]
。
fn (items : @list.List[Item], capacity : Int) -> Combination
solve_v1(@list.List[Item]
items : enum @list.List[A] {
Empty
More(A, tail~ : @list.List[A])
}
List[struct Item {
weight: Int
value: Int
}
Item], Int
capacity : Int
Int) -> struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination {
(items : @list.List[Item]) -> @list.List[Combination]
all_combinations(@list.List[Item]
items)
.(self : @list.List[Combination], f : (Combination) -> Bool) -> @list.List[Combination]
Filter the list.
Example
assert_eq(@list.of([1, 2, 3, 4, 5]).filter(x => x % 2 == 0), @list.of([2, 4]))
filter(fn(Combination
comb) { Combination
comb.Int
total_weight (self_ : Int, other : Int) -> Bool
<= Int
capacity })
.(self : @list.List[Combination]) -> Combination
Get the maximum element of the list.
Warning: This function panics if the list is empty.
Use maximum()
for a safe alternative that returns Option
.
Example
let ls = @list.of([1, 3, 2, 5, 4])
assert_eq(ls.unsafe_maximum(), 5)
Panics
Panics if the list is empty.
unsafe_maximum()
}
注意这里使用的是unsafe_maximum
而不是maximum
。这是因为空列表列表中没有最大值,maximum
在这种情况下会返回一个None
。但我们知道,题目保证答案存在(只要capacity不是负数),所以我们可以改用unsafe_maximum
。它在输入空列表的情况下直接中断程序,其它情况返回列表中的最大值。
接下来我们去实现枚举的过程。函数all_combinations
接受一个物品的列表,返回一个组合的列表,其中包含所有能由这些物品构造出的组合。也许你现在没有头绪,这时我们可以先查看一下列表的定义。它大概长这样:
enum List[A] {
Empty
More(A, tail~ : List[A])
}
也就是说,列表分为两种:
- 第一种是空的列表,叫
Empty
; - 第二种是非空的列表,叫
More
,其中包含了第一个元素(A
)和剩余的部分(tail~ : T[A]
),剩余部分也是一个列表。
这启示我们按物品列表是否为空来分情况讨论:
- 如果物品列表为空,那么唯一的一种组合方式就是空的组合;
- 否则,一定存在第一个物品
item1
和剩余部分items_tail
。这种情况下,我们可以:- 先求出不含
item1
的那些组合。这其实就是items_tail
能凑出的那些组合,可以递归地求出。 - 再求出包含
item1
的那些组合。它们与不含item1
的组合一一对应,只差把item1
加入其中。 - 将这两者合并起来,就是所有
items
能凑出的组合。
- 先求出不含
例如,当物品列表包含a,b,c三个元素时,答案分为以下两个部分:
不含a的部分 | 包含a的部分 |
---|---|
{ } | { a } |
{ b } | { a, b } |
{ c } | { a, c } |
{ b, c } | { a, b, c } |
fn (items : @list.List[Item]) -> @list.List[Combination]
all_combinations(@list.List[Item]
items : enum @list.List[A] {
Empty
More(A, tail~ : @list.List[A])
}
List[struct Item {
weight: Int
value: Int
}
Item]) -> enum @list.List[A] {
Empty
More(A, tail~ : @list.List[A])
}
List[struct Combination {
items: @list.List[Item]
total_weight: Int
total_value: Int
}
Combination] {
match @list.List[Item]
items {
@list.List[Item]
Empty => (x : Combination) -> @list.List[Combination]
Create a list with a single element.
Returns a list containing only the given element.
Example
let ls = @list.singleton(42)
assert_eq(ls, @list.of([42]))
assert_eq(ls.length(), 1)
@list.singleton(Combination
empty_combination)
(Item, @list.List[Item]) -> @list.List[Item]
More(Item
item1, @list.List[Item]
tail=@list.List[Item]
items_tail) => {
let @list.List[Combination]
combs_without_item1 = (items : @list.List[Item]) -> @list.List[Combination]
all_combinations(@list.List[Item]
items_tail)
let @list.List[Combination]
combs_with_item1 = @list.List[Combination]
combs_without_item1.(self : @list.List[Combination], f : (Combination) -> Combination) -> @list.List[Combination]
Maps the list.
Example
assert_eq(@list.of([1, 2, 3, 4, 5]).map(x => x * 2), @list.of([2, 4, 6, 8, 10]))
map(_.(self : Combination, item : Item) -> Combination
add(Item
item1))
@list.List[Combination]
combs_with_item1 (self : @list.List[Combination], other : @list.List[Combination]) -> @list.List[Combination]
Add implementation for List - concatenates two lists.
The +
operator for lists performs concatenation.
a + b
is equivalent to a.concat(b)
.
Example
let a = @list.of([1, 2, 3])
let b = @list.of([4, 5, 6])
let result = a + b
assert_eq(result, @list.of([1, 2, 3, 4, 5, 6]))
+ @list.List[Combination]
combs_without_item1
}
}
}
通过使用模式匹配(match
),我们再一次将上面的五行文字一比一地翻译成了MoonBit代码。