使用ga算法解决背包问题_我如何使用算法解决现实生活中的手提背包的背包问题

使用ga算法解决背包问题

I’m a nomad and live out of one carry-on bag. This means that the total weight of all my worldly possessions must fall under airline cabin baggage weight limits — usually 10kg. On some smaller airlines, however, this weight limit drops to 7kg. Occasionally, I have to decide not to bring something with me to adjust to the smaller weight limit.

我是一个Nomad民族,只能靠一个手提包生活。 这意味着我所有世俗财产的总重量必须在机舱行李重量限制之内(通常为10公斤)。 但是,在一些较小的航空公司上,此重量限制降至7kg。 有时,我必须决定不带东西以适应较小的体重限制。

As a practical exercise, deciding what to leave behind (or get rid of altogether) entails laying out all my things and choosing which ones to keep. That decision is based on the item’s usefulness to me (its worth) and its weight.

作为一项实践练习,决定留下什么(或完全摆脱掉)需要布置我所有的东西并选择要保留的东西。 该决定基于该商品对我的有用性(价值)和重量。

Being a programmer, I’m aware that decisions like this could be made more efficiently by a computer. It’s done so frequently and so ubiquitously, in fact, that many will recognize this scenario as the classic packing problem or knapsack problem. How do I go about telling a computer to put as many important items in my bag as possible while coming in at or under a weight limit of 7kg? With algorithms! Yay!

作为程序员,我知道可以通过计算机更有效地做出这样的决定。 实际上,这样做是如此频繁且无处不在,以至于许多人都将这种情况视为经典的打包问题背包问题。 在告诉我7公斤或以下的重量时,如何让计算机将尽可能多的重要物品放入包中? 有了算法! 好极了!

I’ll discuss two common approaches to solving the knapsack problem: one called a greedy algorithm, and another called dynamic programming (a little harder, but better, faster, stronger…).

我将讨论两种解决背包问题的常用方法:一种称为贪婪算法 另一种称为动态编程 (难度更高,但更好,更快,更强……)。

Let’s get to it.

让我们开始吧。

设置 (The set up)

I prepared my data in the form of a CSV file with three columns: the item’s name (a string), a representation of its worth (an integer), and its weight in grams (an integer). There are 40 items in total. I represented worth by ranking each item from 40 to 1, with 40 being the most important and 1 equating with something like “why do I even have this again?” (If you’ve never listed out all your possessions and ranked them by order of how useful they are to you, I highly recommend you try it. It can be a very revealing exercise.)

我以CSV文件的形式准备了数据,该文件包括三列:商品名称(字符串),商品价值(整数)和重量(克)(整数)的表示形式。 总共有40个项目。 我通过将每一项从40排名为1来表示价值,其中40是最重要的,而1则等同于“为什么我还要再次拥有它?” (如果您从未列出所有财产,并按对您有多有用的顺序对其进行了排名,我强烈建议您尝试一下。这可能是一个非常有意义的练习。)

Total weight of all items and bag: 9003g

所有物品和袋子的总重量: 9003g

Bag weight: 1415g

包重: 1415g

Airline limit: 7000g

航空公司限制: 7000g

Maximum weight of items I can pack: 5585g

我可以包装的最大物品重量: 5585g

Total possible worth of items: 820

项目的总可能价值: 820

The challenge: Pack as many items as the limit allows while maximizing the total worth.

挑战:在最大限度地增加总价值的同时,包装尽可能多的物品。

数据结构 (Data structures)

读取文件 (Reading in a file)

Before we can begin thinking about how to solve the knapsack problem, we have to solve the problem of reading in and storing our data. Thankfully, the Go standard library’s io/ioutil package makes the first part straightforward.

在开始考虑如何解决背包问题之前,我们必须解决读取和存储数据的问题。 幸运的是,Go标准库的io/ioutil软件包使第一部分变得简单。

package main

import (
    "fmt"
    "io/ioutil"
)

func check(e error) {
    if e != nil {
        panic(e)
    }
}

func readItems(path string) {
	dat, err := ioutil.ReadFile(path)
	check(err)
    fmt.Print(string(dat))
}

The ReadFile() function takes a file path and returns the file’s contents and an error (nil if the call is successful) so we’ve also created a check() function to handle any errors that might be returned. In a real-world application, we probably would want to do something more sophisticated than panic, but that’s not important right now.

ReadFile()函数采用文件路径并返回文件内容和错误(如果调用成功,则返回nil ),因此我们还创建了一个check()函数来处理可能返回的任何错误。 在现实世界中的应用程序中,我们可能想做一些比panic更复杂的事情,但是现在这并不重要。

创建一个结构 (Creating a struct)

Now that we’ve got our data, we should probably do something with it. Since we’re working with real-life items and a real-life bag, let’s create some types to represent them and make it easier to conceptualize our program. A struct in Go is a typed collection of fields. Here are our two types:

现在我们已经有了数据,我们可能应该对它进行一些处理。 由于我们正在处理现实生活中的物品和生活中的包,因此让我们创建一些类型来表示它们,并使概念化我们的程序更加容易。 Go中的struct是字段的类型化集合。 这是我们的两种类型:

type item struct {
	name          string
	worth, weight int
}

type bag struct {
	bagWeight, currItemsWeight, maxItemsWeight, totalWeight int
	items                                                   []item
}

It is helpful to use field names that are very descriptive. You can see that the structs are set up just as we’ve described the things they represent. An item has a name (string), and a worth and weight (integers). A bag has several fields of type int representing its attributes, and also has the ability to hold items, represented in the struct as a slice of item type thingamabobbers.

使用描述性很强的字段名称会有所帮助。 您可以看到这些结构已经建立,就像我们描述它们所代表的事物一样。 item具有name (字符串), worthweight (整数)。 bag具有表示其属性的int类型的几个字段,还具有容纳items的能力,该对象在struct中表示为item类型的东西thingamabobbers。

解析和存储我们的数据 (Parsing and storing our data)

Several comprehensive Go packages exist that we could use to parse our CSV data… but where’s the fun in that? Let’s go basic with some string splitting and a for loop. Here’s our updated readItems() function:

我们可以使用几个全面的Go包来解析CSV数据……但是,这样做的乐趣何在? 让我们开始一些字符串拆分和for循环的基本操作。 这是我们更新的readItems()函数:

func readItems(path string) []item {
	
	dat, err := ioutil.ReadFile(path)
	check(err)

	lines := strings.Split(string(dat), "\n")

	itemList := make([]item, 0)

	for i, v := range lines {
		if i == 0 {
			continue
		}
		s := strings.Split(v, ",")
		newItemWorth, _ := strconv.Atoi(s[1])
		newItemWeight, _ := strconv.Atoi(s[2])
		newItem := item{name: s[0], worth: newItemWorth, weight: newItemWeight}
		itemList = append(itemList, newItem)
	}
	return itemList
}

Using strings.Split, we split our dat on newlines. We then create an empty itemList to hold our items.

使用strings.Split ,我们将dat换行。 然后,我们创建一个空的itemList来保存我们的项目。

In our for loop, we skip the first line of our CSV file (the headers) then iterate over each line. We use strconv.Atoi (read “A to i”) to convert the values for each item’s worth and weight into integers. We then create a newItem with these field values and append it to the itemList. Finally, we return itemList.

在for循环中,我们跳过CSV文件的第一行(标头),然后遍历每行。 我们使用strconv.Atoi (读为“ A to i”)将每个物品的价值和重量的值转换为整数。 然后,我们使用这些字段值创建一个newItem ,并将其附加到itemList 。 最后,我们返回itemList

Here’s what our set up looks like so far:

到目前为止,这是我们的设置:

package main

import (
	"io/ioutil"
	"strconv"
	"strings"
)

type item struct {
	name          string
	worth, weight int
}

type bag struct {
	bagWeight, currItemsWeight, maxItemsWeight, totalWeight, totalWorth int
	items                                                               []item
}

func check(e error) {
	if e != nil {
		panic(e)
	}
}

func readItems(path string) []item {

	dat, err := ioutil.ReadFile(path)
	check(err)

	lines := strings.Split(string(dat), "\n")

	itemList := make([]item, 0)

	for i, v := range lines {
		if i == 0 {
			continue // skip the headers on the first line
		}
		s := strings.Split(v, ",")
		newItemWorth, _ := strconv.Atoi(s[1])
		newItemWeight, _ := strconv.Atoi(s[2])
		newItem := item{name: s[0], worth: newItemWorth, weight: newItemWeight}
		itemList = append(itemList, newItem)
	}
	return itemList
}

Now that we’ve got our data structures set up, let’s get packing (?) on the first approach.

现在我们已经建立了数据结构,让我们在第一种方法上打包(?)。

贪心算法 (Greedy algorithm)

A greedy algorithm is the most straightforward approach to solving the knapsack problem, in that it is a one-pass algorithm that constructs a single final solution. At each stage of the problem, the greedy algorithm picks the option that is locally optimal, meaning it looks like the most suitable option right now. It does not revise its previous choices as it progresses through our data set.

贪心算法是解决背包问题的最直接方法,因为它是构造单个最终解决方案的单次通过算法。 在问题的每个阶段,贪婪算法都会选择局部最优的选项,这意味着它看起来像是目前最合适的选项。 它在处理数据集时不会修改其先前的选择。

建立我们的贪婪算法 (Building our greedy algorithm)

The steps of the algorithm we’ll use to solve our knapsack problem are:

用于解决背包问题的算法步骤如下:

  1. Sort items by worth, in descending order.

    按价值按降序对项目进行排序。
  2. Start with the highest worth item. Put items into the bag until the next item on the list cannot fit.

    从价值最高的物品开始。 将物品放入包中,直到列表中的下一个物品无法容纳为止。
  3. Try to fill any remaining capacity with the next item on the list that can fit.

    尝试用列表中适合的下一项来填充所有剩余容量。

If you read my article about solving problems and making paella, you’ll know that I always start by figuring out what the next most important question is. In this case, there are three main operations we need to figure out how to do:

如果您阅读了有关解决问题和制作西班牙海鲜饭的文章 ,您就会知道,我总是从弄清楚下一个最重要的问题开始。 在这种情况下,我们需要确定三个主要操作:

  • Sort items by worth.

    按价值对项目进行排序。
  • Put an item in the bag.

    在袋子里放一个东西。
  • Check to see if the bag is full.

    检查袋子是否装满。

The first one is just a docs lookup away. Here’s how we sort a slice in Go:

第一个只是文档查找。 这是我们在Go中对切片进行排序的方式:

sort.Slice(is, func(i, j int) bool {
    return is[i].worth > is[j].worth
})

The sort.Slice() function orders our items according to the less function we provide. In this case, it will order the highest worth items before the lowest worth items.

sort.Slice()函数根据我们提供的less函数对项目进行排序。 在这种情况下,它将在价值最低的商品之前订购价值最高的商品。

Given that we don’t want to put an item in the bag if it doesn’t fit, we’ll complete the last two tasks in reverse. First, we’ll check to see if the item fits. If so, it goes in the bag.

鉴于我们不想在不适合的情况下将其放入袋中,我们将反向完成最后两个任务。 首先,我们将检查项目是否适合。 如果是这样,它放在袋子里。

func (b *bag) addItem(i item) error {
	if b.currItemsWeight+i.weight <= b.maxItemsWeight {
		b.currItemsWeight += i.weight
		b.items = append(b.items, i)
		return nil
	}
	return errors.New("could not fit item")
}

Notice the * in our first line there. That indicates that bag is a pointer receiver (as opposed to a value receiver). It’s a concept that can be slightly confusing if you’re new to Go. Here are some things to consider that might help you decide when to use a value receiver and when to use a pointer receiver. For the purposes of our addItem() function, this case applies:

请注意我们第一行中的* 。 这表明bag是指针接收器(与值接收器相对)。 如果您不熟悉Go,那么这个概念可能会使您感到困惑。 这里有一些需要考虑的事情可能会帮助您决定何时使用值接收器以及何时使用指针接收器。 就我们的addItem()函数而言,这种情况适用:

If the method needs to mutate the receiver, the receiver must be a pointer.

如果该方法需要更改接收方,则接收方必须是指针。

Our use of a pointer receiver tells our function we want to operate on this specific bag in particular, not a new bag. It’s important because without it, every item would always fit in a newly created bag! A little detail like this can make the difference between code that works and code that keeps you up until 4am chugging Red Bull and muttering to yourself. (Go to bed on time even if your code doesn’t work — you’ll thank me later.)

我们使用指针接收器告诉我们的功能是我们要在特定的袋子上操作,而不是在新袋子上操作。 这很重要,因为没有它,所有物品都将始终装在新创建的包中! 像这样的一些细节可以使有效的代码与使您一直工作到凌晨4点选择Red Bull并喃喃自语的代码有所不同。 (即使您的代码无效,也要准时上床睡觉-稍后您会感谢我的。)

Now that we’ve got our components, let’s put together our greedy algorithm:

现在我们有了组件,下面将我们的贪婪算法放在一起:

func greedy(is []item, b bag) {
	sort.Slice(is, func(i, j int) bool {
		return is[i].worth > is[j].worth
	})

	for i := range is {
		b.addItem(is[i])
	}

	b.totalWeight = b.bagWeight + b.currItemsWeight

	for _, v := range b.items {
		b.totalWorth += v.worth
	}
}

Then in our main() function, we’ll create our bag, read in our data, and call our greedy algorithm. Here’s what it looks like, all set up and ready to go:

然后,在main()函数中,我们将创建包,读取数据,然后调用贪婪算法。 如下所示,所有设置都已准备就绪,可以开始使用:

func main() {

	minaal := bag{bagWeight: 1415, currItemsWeight: 0, maxItemsWeight: 5585}
	itemList := readItems("objects.csv")

	greedy(itemList, minaal)
}
贪婪算法结果 (Greedy algorithm results)

So how does this algorithm do when it comes to efficiently packing our bag to maximize its total worth? Here’s the result:

那么,在有效包装行李以最大化其总价值时,该算法又如何呢? 结果如下:

Total weight of bag and items: 6987g

包袋总重量: 6987g

Total worth of packed items: 716

包装物品总价值: 716

Here are the items our greedy algorithm chose, sorted by worth:

这是我们的贪婪算法选择的项目,按价值排序:

It’s clear that the greedy algorithm is a straightforward way to quickly find a feasible solution. For small data sets, it will probably be close to the optimal solution. The algorithm packed a total item worth of 716 (104 points less than the maximum possible value), while filling the bag with just 13g left over.

显然,贪婪算法是快速找到可行解决方案的直接方法。 对于小型数据集,它可能接近最佳解决方案。 该算法包装了总价值为716(比最大可能值低104点)的物品,同时仅剩13克装满了袋子。

As we learned earlier, the greedy algorithm doesn’t improve upon the solution it returns. It simply adds the next highest worth item it can to the bag.

如我们先前所知,贪婪算法在返回的解决方案上并没有改善。 它只是将可能的下一个最高价值的物品添加到包中。

Let’s look at another method for solving the knapsack problem that will give us the optimal solution — the highest possible total worth under the weight limit.

让我们看一下解决背包问题的另一种方法,该方法将为我们提供最佳解决方案-在重量限制范围内尽可能多的总价值。

动态编程 (Dynamic programming)

The name “dynamic programming” can be a bit misleading. It’s not a style of programming, as the name might cause you to infer, but simply another approach.

“动态编程”这个名称可能会引起误解。 这不是一种编程风格,因为名称可能会导致您进行推断,而只是另一种方法。

Dynamic programming differs from the straightforward greedy algorithm in a few key ways. Firstly, a dynamic programming bag packing solution enumerates the entire solution space with all possibilities of item combinations that could be used to pack our bag. Where a greedy algorithm chooses the most optimal local solution, dynamic programming algorithms are able to find the most optimal global solution.

动态编程在一些关键方面与直接贪婪算法不同。 首先,动态编程袋包装解决方案列举了整个解决方案空间,其中包括所有可能用于包装我们的袋的项目组合。 当贪婪算法选择最优的本地解决方案,动态编程算法都能够找到最优化的全球性解决方案。

Secondly, dynamic programming uses memoization to store the results of previously computed operations and returns the cached result when the operation occurs again. This allows it to “remember” previous combinations. This takes less time than it would to re-compute the answer again.

其次,动态编程使用记忆存储先前计算的操作的结果,并在操作再次发生时返回缓存的结果。 这使它可以“记住”以前的组合。 与重新计算答案相比,此方法花费的时间更少。

建立我们的动态编程算法 (Building our dynamic programming algorithm)

To use dynamic programming to find the optimal recipe for packing our bag, we’ll need to:

要使用动态编程来找到用于包装袋的最佳配方,我们需要:

  1. Create a matrix representing all subsets of the items (the solution space) with rows representing items and columns representing the bag’s remaining weight capacity

    创建一个矩阵,该矩阵表示项目的所有子集(解决方案空间),其中行表示项目,而列表示袋的剩余重量
  2. Loop through the matrix and calculate the worth that can be obtained by each combination of items at each stage of the bag’s capacity

    遍历矩阵,并计算出在包容量的每个阶段中每种物品组合所能获得的价值
  3. Examine the completed matrix to determine which items to add to the bag in order to produce the maximum possible worth for the bag in total

    检查完成的矩阵,确定要添加到袋子中的项目,以使袋子总价值最大化

It will be most helpful to visualize our solution space. Here’s a representation of what we’re building with our code:

可视化我们的解决方案空间将非常有帮助。 这是我们使用代码构建的表示:

In Go, we can create this matrix as a slice of slices.

在Go中,我们可以将此矩阵创建为切片的一部分。

matrix := make([][]int, numItems+1) // rows representing items
for i := range matrix {
	matrix[i] = make([]int, capacity+1) // columns representing grams of weight
}

We’ve padded the rows and columns by 1 so that the indicies match the item and weight numbers.

我们用1填充了行和列,以便索引与项目和重量数字匹配。

Now that we’ve created our matrix, we’ll fill it by looping over the rows and the columns:

现在我们已经创建了矩阵,我们将通过遍历行和列来填充它:

// loop through table rows
for i := 1; i <= numItems; i++ {
	// loop through table columns
	for w := 1; w <= capacity; w++ {
		// do stuff in each element
	}
}

Then for each element, we’ll calculate the worth value to ascribe to it. We do this with code that represents the following process:

然后,对于每个元素,我们将计算值得的价值。 我们使用代表以下过程的代码来做到这一点:

If the item at the index matching the current row fits within the weight capacity represented by the current column, take the maximum of either:

如果索引处与当前行匹配的项目符合当前列所表示的重量范围,则取最大值之一:

  • The total worth of the items already in the bag or,

    手提袋中已有物品的总价值,或
  • The total worth of all the items in the bag except the item at the previous row index, plus the new item’s worth.

    包中所有项目的总价值(上一行索引中的项目除外)加上新项目的价值。

In other words, as our algorithm considers one of the items, we’re asking it to decide whether this item added to the bag would produce a higher total worth than the last item it added to the bag, at the bag’s current total weight. If this current item is a better choice, put it in — if not, leave it out.

换句话说,当我们的算法考虑其中一个项目时,我们要求它决定以当前袋子的总重量,添加到袋子中的这个项目是否会比添加到袋子中的最后一个项目产生更高的总价值。 如果当前的项目是更好的选择,则将其放入-否则,将其保留。

Here’s the code that accomplishes this:

这是完成此操作的代码:

// if weight of item matching this index can fit at the current capacity column...
if is[i-1].weight <= w {
	// worth of this subset without this item
	valueOne := float64(matrix[i-1][w])
	// worth of this subset without the previous item, and this item instead
	valueTwo := float64(is[i-1].worth + matrix[i-1][w-is[i-1].weight])
	// take maximum of either valueOne or valueTwo
	matrix[i][w] = int(math.Max(valueOne, valueTwo))
// if the new worth is not more, carry over the previous worth
} else {
	matrix[i][w] = matrix[i-1][w]
}

This process of comparing item combinations will continue until every item has been considered at every possible stage of the bag’s increasing total weight. When all the above have been considered, we’ll have enumerated the solution space — filled the matrix — with all possible total worth values.

比较项目组合的过程将继续进行,直到在袋子总重量增加的每个可能阶段都考虑了每个项目为止。 考虑以上所有因素后,我们将枚举所有可能的总价值值(用矩阵填充)的解决方案空间。

We’ll have a big chart of numbers, and in the last column at the last row we’ll have our highest possible value.

我们将有一个很大的数字图表,并且在最后一行的最后一列中,我们将获得可能的最高价值。

That’s great, but how do we find out which combination of items were put in the bag to achieve that worth?

太好了,但是我们如何找出包装中的哪些物品组合才能实现这一目标呢?

获取我们的优化物品清单 (Getting our optimized item list)

To see which items combine to create our optimal packing list, we’ll need to examine our matrix in reverse to the way we created it. Since we know the highest possible value is in the last row in the last column, we’ll start there. To find the items, we:

要查看可以组合哪些物料以创建最佳装箱单,我们需要以与创建矩阵相反的方式检查矩阵。 由于我们知道最高的可能值位于最后一列的最后一行,因此我们将从此处开始。 为了找到物品,我们:

  1. Get the value of the current cell

    获取当前单元格的值
  2. Compare the value of the current cell to the value in the cell directly above it

    将当前单元格的值与其正上方的单元格中的值进行比较
  3. If the values differ, there was a change to the bag items. Find the next cell to examine by moving backwards through the columns according to the current item’s weight (find the value of the bag before this current item was added)

    如果值不同,则说明行李袋有所更改。 根据当前物品的重量,通过在列中向后移动来找到要检查的下一个单元格(在添加当前物品之前,找到袋子的值)
  4. If the values match, there was no change to the bag items. Move up to the cell in the row above and repeat

    如果值匹配,则袋项目没有变化。 向上移动到上一行的单元格,然后重复

The nature of the action we’re trying to achieve lends itself well to a recursive function. If you recall from my previous article about making apple pie, recursive functions are simply functions that call themselves under certain conditions. Here’s what it looks like:

我们尝试实现的操作的性质非常适合于递归函数。 如果您回想起我上一篇有关制作苹果派的文章 ,那么递归函数就是在某些条件下会自行调用的函数。 看起来是这样的:

func checkItem(b *bag, i int, w int, is []item, matrix [][]int) {
	if i <= 0 || w <= 0 {
		return
	}

	pick := matrix[i][w]
	if pick != matrix[i-1][w] {
		b.addItem(is[i-1])
		checkItem(b, i-1, w-is[i-1].weight, is, matrix)
	} else {
		checkItem(b, i-1, w, is, matrix)
	}
}

Our checkItem() function calls itself if the condition we described in step 4 is true. If step 3 is true, it also calls itself, but with different arguments.

如果我们在步骤4中描述的条件为true,则我们的checkItem()函数将自行调用。 如果第3步是正确的,则它也会调用自己,但带有不同的参数。

Recursive functions require a base case. In this example, we want the function to stop once we run out of values of worth to compare. Thus our base case is when either i or w are 0.

递归函数需要一个基本情况。 在此示例中,我们希望函数在值得比较的值用完后停止运行。 因此,我们的基本情况是当iw0

Here’s how the dynamic programming approach looks when it’s all put together:

将所有这些放在一起时,动态编程方法的外观如下:

func checkItem(b *bag, i int, w int, is []item, matrix [][]int) {
	if i <= 0 || w <= 0 {
		return
	}

	pick := matrix[i][w]
	if pick != matrix[i-1][w] {
		b.addItem(is[i-1])
		checkItem(b, i-1, w-is[i-1].weight, is, matrix)
	} else {
		checkItem(b, i-1, w, is, matrix)
	}
}

func dynamic(is []item, b *bag) *bag {
	numItems := len(is)          // number of items in knapsack
	capacity := b.maxItemsWeight // capacity of knapsack

	// create the empty matrix
	matrix := make([][]int, numItems+1) // rows representing items
	for i := range matrix {
		matrix[i] = make([]int, capacity+1) // columns representing grams of weight
	}

	// loop through table rows
	for i := 1; i <= numItems; i++ {
		// loop through table columns
		for w := 1; w <= capacity; w++ {

			// if weight of item matching this index can fit at the current capacity column...
			if is[i-1].weight <= w {
				// worth of this subset without this item
				valueOne := float64(matrix[i-1][w])
				// worth of this subset without the previous item, and this item instead
				valueTwo := float64(is[i-1].worth + matrix[i-1][w-is[i-1].weight])
				// take maximum of either valueOne or valueTwo
				matrix[i][w] = int(math.Max(valueOne, valueTwo))
			// if the new worth is not more, carry over the previous worth
			} else {
				matrix[i][w] = matrix[i-1][w]
			}
		}
	}

	checkItem(b, numItems, capacity, is, matrix)

	// add other statistics to the bag
	b.totalWorth = matrix[numItems][capacity]
	b.totalWeight = b.bagWeight + b.currItemsWeight

	return b
}
动态编程结果 (Dynamic programming results)

We expect that the dynamic programming approach will give us a more optimized solution than the greedy algorithm. So did it? Here are the results:

我们期望动态编程方法将比贪婪算法为我们提供更优化的解决方案。 是吗 结果如下:

Total weight of bag and items: 6982g

包袋总重量: 6982g

Total worth of packed items: 757

包装物品总价值: 757

Here are the items our dynamic programming algorithm chose, sorted by worth:

这是我们的动态编程算法选择的项目,按价值排序:

There’s an obvious improvement to our dynamic programming solution over what the greedy algorithm gave us. Our total worth of 757 is 41 points greater than the greedy algorithm’s solution of 716, and for a few grams less weight too!

与贪婪算法给我们的东西相比,我们的动态编程解决方案有了明显的改进。 我们的总价值757比贪婪算法的解决方案716高41点,而且重量也减少了几克!

输入排序顺序 (Input sort order)

While testing my dynamic programming solution, I implemented the Fisher-Yates shuffle algorithm on the input before passing it into my function, just to ensure that the answer wasn’t somehow dependent on the sort order of the input. Here’s what the shuffle looks like in Go:

在测试我的动态编程解决方案时,我在将输入传递给我的函数之前对输入执行了Fisher-Yates随机播放算法 ,只是为了确保答案不以某种方式取决于输入的排序顺序。 这是Go中洗牌的样子:

rand.Seed(time.Now().UnixNano())

for i := range itemList {
	j := rand.Intn(i + 1)
	itemList[i], itemList[j] = itemList[j], itemList[i]
}

Of course I then realized that Go 1.10 now has a built-in shuffle. It works precisely the same way and looks like this:

当然,然后我意识到Go 1.10现在具有内置的随机播放功能。 它的工作方式完全相同,如下所示:

rand.Shuffle(len(itemList), func(i, j int) {
	itemList[i], itemList[j] = itemList[j], itemList[i]
})

So did the order in which the items were processed affect the outcome? Well…

那么处理项目的顺序会影响结果吗? 好…

突然……流氓重物出现了! (Suddenly… a rogue weight appears!)

As it turns out, in a way, the answer did depend on the order of the input. When I ran my dynamic programming algorithm several times, I sometimes saw a different total weight for the bag, though the total worth remained at 757. I initially thought this was a bug before examining the two sets of items that accompanied the two different total weight values. Everything was the same except for a few changes that collectively added up to a different item subset accounting for 14 of the 757 worth points.

事实证明,答案确实取决于输入的顺序。 当我多次运行动态编程算法时,有时我看到的袋子总重量有所不同,尽管总价值仍然为757。在检查两组带有两种不同总重量的物品之前,我最初认为这是一个错误。价值观。 除少数更改外,其他所有内容都相同,这些更改合计起来构成了757个价值点中的14个不同子集。

In this case, there were two equally optimal solutions based only on the success metric of the highest total possible worth. Shuffling the input seemed to affect the placement of the items in the matrix and thus, the path that the checkItem() function took as it went through the matrix to find the chosen items. Since the success metric of having the highest possible worth was the same in both item sets, we don’t have a single unique solution - there’s two!

在这种情况下,仅基于最大可能总价值的成功指标,就有两个同等最佳的解决方案。 改组输入似乎会影响项目在矩阵中的位置,并因此影响checkItem()函数经过矩阵以查找所选项目时所采用的路径。 由于在两个项目集中具有最高价值的成功指标是相同的,所以我们没有一个唯一的解决方案-有两个!

As an academic exercise, both these sets of items are correct answers. We may choose to optimize further by another metric, say, the total weight of all the items. The highest possible worth at the least possible weight could be seen as an ideal solution.

作为一项学术练习,这两组项目都是正确的答案。 我们可能会选择通过另一个指标(即所有商品的总重量)进一步优化。 以最小的重量获得尽可能高的价值被视为理想的解决方案。

Here’s the second, lighter, dynamic programming result:

这是第二个更轻松的动态编程结果:

Total weight of bag and items: 6955g

包袋总重量: 6955g

Total worth of packed items: 757

包装物品总价值: 757

哪种方法更好? (Which approach is better?)

进行基准测试 (Go benchmarking)

The Go standard library’s testing package makes it straightforward for us to benchmark these two approaches. We can find out how long it takes each algorithm to run, and how much memory each uses. Here’s a simple main_test.go file:

Go标准库的testing包使我们可以轻松地对这两种方法进行基准测试 。 我们可以找出每种算法运行需要多长时间,以及每种使用多少内存。 这是一个简单的main_test.go文件:

package main

import (
	"testing"
)

func Benchmark_greedy(b *testing.B) {
	itemList := readItems("objects.csv")
	for i := 0; i < b.N; i++ {
		minaal := bag{bagWeight: 1415, currItemsWeight: 0, maxItemsWeight: 5585}
		greedy(itemList, minaal)
	}
}

func Benchmark_dynamic(b *testing.B) {
	itemList := readItems("objects.csv")
	for i := 0; i < b.N; i++ {
		minaal := bag{bagWeight: 1415, currItemsWeight: 0, maxItemsWeight: 5585}
		dynamic(itemList, &minaal)
	}
}

We can run go test -bench=. -benchmem to see these results:

我们可以运行go test -bench=. -benchmem go test -bench=. -benchmem查看以下结果:

Benchmark_greedy-4       1000000              1619 ns/op            2128 B/op          9 allocs/op
Benchmark_dynamic-4         1000           1545322 ns/op         2020332 B/op         49 allocs/op
贪婪算法性能 (Greedy algorithm performance)

After running the greedy algorithm 1,000,000 times, the speed of the algorithm was reliably measured to be 0.001619 milliseconds (translation: very fast). It required 2128 Bytes or 2-ish kilobytes of memory and 9 distinct memory allocations per iteration.

在运行贪婪算法1,000,000次之后,该算法的速度被可靠地测量为0.001619毫秒(转换:非常快)。 每次迭代需要2128字节或2千KB的内存,并且需要9种不同的内存分配。

动态编程性能 (Dynamic programming performance)

The dynamic programming algorithm was run 1,000 times. Its speed was measured to be 1.545322 milliseconds or 0.001545322 seconds (translation: still pretty fast). It required 2,020,332 Bytes or 2-ish Megabytes, and 49 distinct memory allocations per iteration.

动态编程算法运行了1000次。 测得其速度为1.545322毫秒或0.001545322秒(转换:仍相当快)。 它需要2,020,332字节或2兆字节,并且每次迭代需要49个不同的内存分配。

判决 (The verdict)

Part of choosing the right approach to solving any programming problem is taking into account the size of the input data set. In this case, it’s a small one. In this scenario, a one-pass greedy algorithm will always be faster and less resource-needy than dynamic programming, simply because it has fewer steps. Our greedy algorithm was almost two orders of magnitude faster and less memory-hungry than our dynamic programming algorithm.

选择正确的方法来解决任何编程问题的一部分,就是要考虑输入数据集的大小。 在这种情况下,它很小。 在这种情况下,单遍贪婪算法总是比动态编程更快,资源占用更少,这仅仅是因为它的步骤更少。 我们的贪婪算法比我们的动态编程算法快了近两个数量级,并且减少了内存消耗。

Not having those extra steps, however, means that getting the best possible solution from the greedy algorithm is unlikely.

但是,没有这些额外的步骤意味着从贪婪算法中获得最佳解决方案的可能性很小。

It’s clear that the dynamic programming algorithm gave us better numbers: a lower weight, and higher overall worth.

显然,动态编程算法为我们提供了更好的数字:更轻的重量和更高的整体价值。

贪心算法 (Greedy algorithm)
  • Total weight: 6987g

    总重量:6987g
  • Total worth: 716

    总价值:716
动态编程 (Dynamic programming)
  • Total weight: 6955g

    总重量:6955g
  • Total worth: 757

    总价值:757

Where dynamic programming on small data sets lacks in performance, it makes up in optimization. The question then becomes whether that additional optimization is worth the performance cost.

小数据集上的动态编程缺乏性能,而优化则由其弥补。 然后,问题就在于,是否进行额外的优化是否值得付出性能成本。

“Better,” of course, is a subjective judgement. If speed and low resource usage is our success metric, then the greedy algorithm is clearly better. If the total worth of items in the bag is our success metric, then dynamic programming is clearly better.

当然,“更好”是一种主观判断。 如果速度和低资源使用率是我们的成功指标,那么贪婪算法显然更好。 如果袋中物品的总价值是我们的成功指标,那么动态编程显然会更好。

However, our scenario is a practical one, and only one of these algorithm designs returned an answer I’d choose. In optimizing for the overall greatest possible total worth of the items in the bag, the dynamic programming algorithm left out my highest-worth, but also heaviest, item: my laptop. The chargers and cables, Roost stand, and keyboard that were included aren’t much use without it.

但是,我们的场景是一个实际的场景,这些算法设计中只有一个返回了我选择的答案。 在优化袋子中所有物品的最大总价值时,动态编程算法忽略了我最有价值但也最重的物品:我的笔记本电脑。 没有它,充电器和电缆,Roost支架和键盘就没什么用了。

更好的算法设计 (Better algorithm design)

There’s a simple way to alter the dynamic programming approach so that the laptop is always included: we can modify the data so that the worth of the laptop is greater than the sum of the worth of all the other items. (Try it out!)

有一种简单的方法可以更改动态编程方法,以便始终包含便携式计算机:我们可以修改数据,以使便携式计算机的价值大于所有其他项目的价值之和。 (试试看!)

Perhaps in re-designing the dynamic programming algorithm to be more practical, we might choose another success metric that better reflects an item’s importance, instead of a subjective worth value. There are many possible metrics we can use to represent the value of an item. Here are a few examples of a good proxy:

也许在重新设计动态编程算法以使其更实用时,我们可能会选择另一个更好地反映项目重要性的成功指标,而不是主观的价值。 我们可以使用许多可能的指标来表示项目的价值。 以下是一些良好代理的示例:

  • Amount of time spent using the item

    使用该物品花费的时间
  • Initial cost of purchasing the item

    购买物品的初始费用
  • Cost of replacement if the item were lost today

    今天丢失的物品的更换费用
  • Dollar value of the product of using the item

    使用该物品的产品的美元价值

By the same token, the greedy algorithm’s results might be improved with the use of one of these alternate metrics.

同样,贪婪算法的结果可能会通过使用这些替代指标之一而得到改善。

On top of choosing an appropriate approach to solving the knapsack problem in general, it is helpful to design our algorithm in a way that translates the practicalities of a scenario into code.

除了选择合适的方法来解决背包问题外,以一种将方案的实际性转化为代码的方式设计我们的算法是有帮助的。

There are many considerations for better algorithm design beyond the scope of this introductory post that I plan to cover (some of) in later articles. A future algorithm may very well decide my bag’s contents on the next trip, but we’re not quite there yet. Stay tuned!

我打算在以后的文章中(其中的一些)介绍这篇介绍性文章之外的内容,因此有很多考虑因素需要更好的算法设计。 未来的算法很可能会在下次旅行中决定我的行李的内容,但我们还没有完全确定。 敬请关注!

Thanks for reading! I hope this article has given you a better idea of how these two common approaches work. If you’d like to learn more about how I live out of one carry-on bag, check out my nomad blog at herOneBag.com.

谢谢阅读! 我希望本文能使您更​​好地了解这两种常见方法的工作方式。 如果您想了解更多关于我如何用一个手提袋生活的信息,请在herOneBag.com上查看我的Nomad博客。

Have a really great day. :)

祝您有美好的一天。 :)

翻译自: https://www.freecodecamp.org/news/how-i-used-algorithms-to-solve-the-knapsack-problem-for-my-real-life-carry-on-knapsack-5f996b0e6895/

使用ga算法解决背包问题

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值