当前位置: 首页>编程语言>正文

Python 工程经济分析 python工程预算

python视觉编程



I was given an interesting coding problem recently. The problem is Project Euler Problem 31:

最近给了我一个有趣的编码问题。 问题是欧拉计划问题31 :

In the United Kingdom the currency is made up of pound (£) and pence (p). There are eight coins in general circulation:

在英国,货币由英镑(£)和便士(p)组成。 一般发行的硬币有八种:

1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), and £2 (200p).

1p,2p,5p,10p,20p,50p,£1(100p)和£2(200p)。

It is possible to make £2 in the following way:

可以通过以下方式赚取£2:

1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p

1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p

How many different ways can £2 be made using any number of coins?

使用任意数量的硬币,£2可以有多少种不同的制作方法?

I’ll go though the problem solving process I went through when approaching this problem. You can see my solution at the bottom of this page.

解决这个问题时,我将经历解决问题的过程。 您可以在本页底部看到我的解决方案。

(Start by considering a simple case)

With problems it’s good to start with simpler cases and build up from there. For example how many ways can you can 2p from the available coins?

遇到问题时,最好从简单的案例开始,然后从那里开始。 例如,您可以使用几种方法从可用硬币中赚2p?

The answer is 2 ways: 2 x 1p, 1 x 2p

答案是2种方式:2 x 1p,1 x 2p

But how could we arrive at this result systematically? A clear way to solve this is to consider each coin in turn and see if they can be subtracted from the target amount, i.e. consider taking away 1p from 2p, then separately consider taking away 2p from 2p.

但是我们如何系统地得出这个结果呢? 解决此问题的一种清晰方法是依次考虑每个硬币,看看是否可以从目标数量中减去它们,即考虑从2p中减去1p,然后分别考虑从2p中减去2p。

(Visualization helps build our intuition)

Visualizations are usually a good tool. In this case we can use a tree to visualize the 2p solution:

可视化通常是一个很好的工具。 在这种情况下,我们可以使用树来可视化2p解决方案:





Tree showing all ways to make change for 2p 该树显示了为2p进行更改的所有方法

Let me explain this visualization:

让我解释一下这个可视化:

  • Each node (the ovals) represents the current target that we want to make from the coins on the edges below it
  • The root node (the top oval) shows the target value we want to make from change
  • The number next to each edge (the arrows) represents the coin used to go from one target to the next
  • Each branch of the tree terminates when the current target is 0
  • Each way of reaching the root target value can be gotten by going back up the tree from each leaf node (nodes with 0) to the top and making a note of the coins on the edges

(Visualization helps for more complex cases)

The same process to produce the 2p tree can then be used to produce a 3p tree:

然后可以使用相同的过程来生成2p树,以生成3p树:


Tree showing all ways to make change for 3p including permutations

该树显示了为3p进行更改(包括排列)的所有方法

However we notice an issue that this tree also counts all permutations of coins to reach 3p: 3 x 1p, 1 x 1p + 1 x 2p, 1 x 2p + 1 x 1p; even though the correct answer is two ways: 3 x 1p, 1 x 1p + 1 x 2p, since 1 x 1p + 1 x 2p and 1 x 2p + 1 x 1p are equivalent.

但是,我们注意到一个问题,该树还计算硬币的所有排列以达到3p:3 x 1p,1 x 1p + 1 x 2p,1 x 2p + 1 x 1p; 即使正确的答案有两种:3 x 1p,1 x 1p +1 x 2p,因为1 x 1p +1 x 2p和1 x 2p +1 x 1p是等效的。

We could just keep the tree and then remove any permutations, however this would be inefficient. Instead we should think, is there a way of modifying the tree to naturally remove permutations?

我们可以只保留树,然后删除所有排列,但是这样效率低下。 相反,我们应该考虑,是否有一种修改树以自然消除排列的方法?

(The visualization must ignore permutations)

It turns out there is a way to naturally not include permutations.

事实证明,有一种方法自然不包括置换。

Looking at the 3p tree, let’s go up from each leaf node to the root and noting the edges as we go. We get: (1p, 1p, 1p), (1p, 2p), and (2p, 1p).

看一下3p树,让我们从每个叶节点向上到根,并注意边沿。 我们得到:(1p,1p,1p),(1p,2p)和(2p,1p)。

By sorting each sequence we would see that there are only two unique ways to total 3p. After sorting we have: (1p, 1p, 1p), (1p, 2p), and (1p, 2p). The unique elements of this list are simply (1p, 1p, 1p), (1p, 2p), and the length of this list is 2, which equals the number of ways we can make change for 3p.

通过对每个序列进行排序,我们将看到只有两种独特的方法可以累加3p。 排序后,我们得到:(1p,1p,1p),(1p,2p)和(1p,2p)。 该列表的唯一元素只是(1p,1p,1p),(1p,2p),此列表的长度为2,这等于我们对3p进行更改的方式数量。

So how could we enforce that the sequence of edges for each leaf node in the tree will be in order?

那么我们如何强制树中每个叶节点的边缘顺序是有序的呢?

When we produce the tree (from top to bottom) we should only allow an edge if it is less than or equal to the edge above it. This would remove the edge (2)→(1) in the 3p tree, resulting in 2 leaf nodes.

当我们生成树时(从上到下),我们仅应允许边缘小于或等于其上方的边缘。 这将删除3p树中的边缘(2)→(1),从而产生2个叶节点。

The resultant trees are shown below.

生成的树如下所示。


Trees without permutations for 1p, 2p, 3p, 4p

1p,2p,3p,4p无排列的树

(It’s useful if trees are well defined by a function)

Now is there any way we can use these trees to efficiently calculate the ways of making change?

现在有什么方法可以使用这些树来有效地计算进行更改的方式?

A useful thing to see is that this question has a purely functional answer i.e. we can solve this if we can find a well defined function f such that f(target) = (number ways of making change).

有用的看到的是,这个问题有一个纯粹的功能性答案,即,如果我们找到一个定义明确的函数f ,使得f (目标)=(做出改变的多种方式),我们可以解决这个问题。

Since a function returns the same answer for the same inputs, we need to see if all (sub)trees with a root node x have the same number of leaf nodes n.

由于函数针对相同的输入返回相同的答案,因此我们需要查看所有具有根节点x的 (子)树是否具有相同数量的叶节点n

We reach an issue when we check 2p (sub)trees, shown in the boxes below.

当我们检查2p个(子)树时,我们遇到了一个问题,如下框所示。


Trees with 2p (sub)trees in boxes 盒子中有2p(子)树的树


We see there are two types of 2p (sub)tree: ones with 1 leaf node, and ones with 2 leaf nodes. The first set has an edge with 1 pointing to its root node, whereas the second set has 2 pointing to its root node (or no edge pointing to the root node). This means the 1-leaf trees correspond to making 2p from 1p whereas the 2-leaf trees correspond to making 2p from 1p or 2p.

我们看到2p(子)树有两种类型:一种具有1个叶节点,另一种具有2个叶节点。 第一组具有1的边缘指向其根节点,而第二组具有2的边缘指向其根节点(或没有边缘指向根节点)。 这意味着1叶树对应于从1p生成2p,而2叶树对应于从1p或2p生成2p。

What does this mean for our function f?

这对我们的函数f意味着什么?

This means for our function to only depend on its inputs we also need a second input argument for the maximum allowed coin y i.e. f(x, y) = n

这意味着对于我们的函数来说,仅取决于其输入,对于最大允许的硬币y,我们还需要第二个输入参数,即f(x,y)= n

Rewriting out the trees with this new way of labeling nodes we get¹:

用这种标记节点的新方法重写树,我们得到¹:


Trees with each node showing the target value and maximum allowed coin 每个节点显示目标值和允许的最大硬币的树

We can now check again that all (sub)trees with a root node x and max coin y have the same number of leaf nodes n. This time the check passes.

现在我们可以再次检查所有具有根节点x和最大硬币y的 (子)树具有相同数量的叶节点n 。 这次检查通过了。

(Recursion makes the problem more manageable)

Now that f(x, y) is well defined we should consider if we can use one of the common tools in functional programming: recursion. For recursion to work, there needs to be examples of smaller solutions inside larger solutions.

既然f(x,y)定义明确,我们应该考虑是否可以在函数式编程中使用一种常用工具: 递归 。 为了使递归起作用,在大型解决方案中需要一些小型解决方案的示例。

One example in the above trees is f(4, 200) = f(3, 1) + f(2, 2). In words this says “the number of ways of making change for 4p using at most £2 coins is equal to the sum of: the number of ways of making change for 3p using at most 1p coins, and the number of ways of making change for 2p using at most 2p coins”.

上述树的一个示例是f(4,200)= f(3,1)+ f(2,2) 。 换句话说, “最多使用2英镑2英镑硬币进行4p兑换的方法的数量等于以下总和:使用最多1p硬币进行3p硬币进行兑换的方法的数量,以及最多使用2p硬币进行2p硬币进行兑换的方法的数量。大多数2p硬币”。

More importantly there needs to be a systematic way to relate smaller solutions to larger solutions. We can find this by remembering how the tree is generated in the first place: at each node we make a branch for each possible coin that is less than or equal to the max allowed coin. Therefore f(x, y) is the sum of f(x-b, b) where b is a coin ≤ y.

更重要的是,需要一种系统的方式将较小的解决方案与较大的解决方案相关联。 我们可以通过首先记住树是如何生成的来找到的:在每个节点处,我们为每个可能的硬币创建一个分支,该分支小于或等于允许的最大硬币。 因此f(x,y)f(xb,b)的总和,其中b是硬币≤y

For recursion we also need a base case, otherwise we will just end up in a loop of defining f(x, y) in terms of another f(x’, y’). The simplest base case is that f(0, y) = 1 for any y.

对于递归,我们还需要一个基本情况,否则我们将最终陷入根据另一个f(x',y')定义f(x,y)的循环中。 最简单的基本情况是任何y的 f(0,y)= 1

We can now write a solution using recursion:

现在,我们可以使用递归编写解决方案:

coins = (1, 2, 5, 10, 20, 50, 1_00, 2_00)def n_ways(target, max_coin):
# returns number of ways of producing change to reach target,
    # using change up to and including the max_coin
    # raises error for negative targets
if target == 0:
        return 1
    if target > 0:
# don't count cases that would lead to a negative target
return sum(
            n_ways(target - coin, coin)
            for coin in coins
            if coin <= max_coin
            if target - coin >= 0
        )
    raise ValueError# 73682
print(n_ways(2_00, 2_00))

If we run this code it works well and gives the correct answer of 73,682.

如果我们运行此代码,它将运行良好,并给出73682的正确答案。

(Adding caching speeds up the code)

Note that sometimes we will need to evaluate f(x, y) when we have already previously calculated f(x, y) e.g. f(1, 1) gets calculated twice when we calculate f(4, 200). These cases will actually occur more and more often as we increase x, therefore we should try and store previous values of the function so we can just recall the answer rather than recalculate it.

请注意,有时我们需要预先计算f(x,y)时才需要评估f(x,y) 例如当我们计算f(4,200),f(1,1)被计算了两次。 随着x的增加,这些情况实际上会越来越多地发生,因此我们应该尝试存储该函数的先前值,以便我们可以回忆起答案而不是重新计算它。

In Python this can be done in just two lines with the lru_cache. This turns any function into a cached function: a function that stores previously computed values and retrieves the stored value if the function is given the same arguments.

在Python中,只需两行即可使用lru_cache完成。 这会将任何函数转换为缓存的函数:一个存储以前计算的值并在给定相同参数的情况下检索存储值的函数。

from functools import lru_cachecoins = (1, 2, 5, 10, 20, 50, 1_00, 2_00)@lru_cache(maxsize=None)
def n_ways(target, max_coin):
# returns number of ways of producing change to reach target, 
    # using change up to and including the max_coin
    # raises error for negative targets
if target == 0:
        return 1
    if target > 0:
# don't count cases that would lead to a negative target
return sum(n_ways(target - coin, coin)
                   for coin in coins
                   if coin <= max_coin
                   if target - coin >= 0)  
    raise ValueError# 73682
print(n_ways(2_00, 2_00))
# CacheInfo(hits=414, misses=482, maxsize=None, currsize=482)
print(n_ways.cache_info())

This doubles the speed of computing f(200, 200). Not bad for two additional lines. We can also see by inspecting the cache_info that we actually stored 482 values for f in our cache² and that by using the cache we didn’t need to recalculate f 414 times.

这使计算f(200,200)的速度提高了一倍。 对于另外两行来说还不错。 通过检查cache_info,我们还可以看到我们实际上在高速缓存²中存储了482个f值,并且通过使用高速缓存,我们不需要重新计算f 414次。

(Conclusion)

This is the final form of our code³. It if flexible enough to be extended to a larger number of coins and higher totals. For example if we add £5 and £10 notes then the code tells us there are 327,631,322 ways to make £10.

这是我们的代码³的最终形式。 如果足够灵活,可以扩展到更多的硬币和更高的总数。 例如,如果我们添加5英镑和10英镑的钞票,则代码告诉我们,有327,631,322种方法可以使10英镑变成英镑。

The combination of recursion and caching in this solution means this is an example of dynamic programming, which you can read more about here.

此解决方案中递归和缓存的结合意味着这是动态编程的示例,您可以在此处阅读更多内容。

(Footnotes)

[1] By looking at the trees we can see cases of f(x, y) where y > x. These cases will have the same answer as f(x, y*) where y* is the largest coin smaller than y e.g. f(2, 200) = f(2,2) and f(1, 2) = f(1,1). This is because adding coins higher value than the target can’t change how many ways you can make change. Taking this into account could make our code more efficient at the expense of added complexity. However in practice these very rarely occur, and even if f(x, y) is not cached, all nodes below it will have been cached if f(x, y*) is in the cache.

[1]通过查看树,我们可以看到f(x,y)的情况 ,其中y> x 。 这些情况 将具有与f(x,y *)相同的答案其中y *是小于y的最大硬币,例如f(2,200)= f(2,2)f(1,2)= f(1,1 )。 这是因为添加比目标价更高的硬币无法改变您进行更改的方式。 考虑到这一点,可以使我们的代码更高效,但会增加复杂性。 但是实际上,这种情况很少发生,即使f(x,y)没有被缓存,如果f(x,y *)在缓存中,它下面的所有节点也会被缓存。

[2] I found other solutions online helpful when thinking how I would solve this problem. However these solutions require calculating f(x, y) for all values of x and y less than or equal to 200. This means f needs to be calculated 200 * 8 = 1600 times. As we’ve seen in comparison the method above only calculates f 482 times, meaning it reduces computation by a factor of 4.

[2]在考虑如何解决此问题时,我发现其他在线 解决方案很有帮助。 但是,这些解决方案需要为所有小于或等于200的xy值计算f(x,y) 。这意味着需要计算f * 200 * 8 = 1600次。 正如我们在比较中所看到的,上述方法仅计算f 482次,这意味着它将计算量减少了4倍。

[3] An interesting alternative solution is found if you plug the first 10 values of the sequence into OEIS which gives sequence A057537 and the corresponding generating function for the “number of ways of making change for n Euro-cents using the Euro currency”.

[3]如果将序列的前10个值插入OEIS , 则会得到一个有趣的替代解决方案,该序列给出序列A057537和相应的生成函数,即“使用欧元对n欧分进行更改的方式数量”。


翻译自: https://medium.com/@robert.swan/visual-problem-solving-and-dynamic-programming-in-python-dfa5e028b3f

python视觉编程


https://www.xamrdz.com/lan/54g1960734.html

相关文章: