揭开均线系统的神秘面纱_揭开动态规划的神秘面纱

揭开均线系统的神秘面纱

by Prajwal M

通过Prajwal M

There are many quality articles on how to become a software developer. They teach you to program, develop, and use libraries. But little has been done to educate in Algorithms and DataStructures. No matter how good you are at development, without knowledge of Algorithms and Data Structures, you can’t get hired.

关于如何成为软件开发人员,有很多高质量的文章。 他们教您编程,开发和使用库。 但是,几乎没有做什么算法数据结构来教育 无论您的开发水平如何,如果没有算法数据结构的知识就不会被雇用

Learning popular algorithms like Matrix Chain Multiplication, Knapsack or Travelling Salesman Algorithms is not sufficient. Interviewers ask problems like the ones you find on competitive programming sites. To solve such problems, you need to have a good and firm understanding of the concepts.

学习流行的算法,例如矩阵链乘法,背包或旅行推销员 算法是不够的。 采访者会问一些问题,例如您在竞争性编程网站上发现的问题。 为了解决这些问题,您需要对概念有一个良好而坚定的理解。

什么是动态编程? (What is Dynamic Programming?)

According to Wikipedia, dynamic programming is simplifying a complicated problem by breaking it down into simpler sub-problems in a recursive manner. This article will teach you to:

根据Wikipedia的说法,动态编程通过以递归的方式将其分解为更简单的子问题,从而简化了一个复杂的问题。 本文将教您:

-> Identify subproblems
-> Learn how to solve subproblems
-> Identify that subproblems are repetitive 
-> Identify that subproblems have optimal substructure property
-> Learn to cache/store results of sub problems
-> Develop a recursive relation to solve the problem
-> Use top-down and bottom-up approach to solve the problem
我将使用哪种语言? (Which language will I use?)

I know that most people are proficient or have experience coding in JavaScript. Also, once you learn in JavaScript, it is very easy to transform it into Java code. The same can be said of Python or C++. The trick is to understand the problems in the language you like the most. Hence I have chosen to use JavaScript.

我知道大多数人精通JavaScript或具有使用JavaScript进行编码的经验。 另外,一旦您学习了JavaScript,就很容易将其转换为Java代码。 对于Python或C ++也可以这样说。 诀窍是用您最喜欢的语言来理解问题。 因此,我选择使用JavaScript。

This post is about algorithms and more specifically about dynamic programming. It is generally perceived as a tough topic. If you make it to the end of the post, I am sure you can tackle many dynamic programming problems on your own ?.

这篇文章是关于算法的,更具体地说是关于动态编程的。 通常认为这是一个艰难的话题。 如果您到文章末尾,我相信您可以自己解决许多动态编程问题

问题陈述 (Problem Statement)

Problem: Given an integer n, find the minimum number of steps to reach integer 1.

At each step, you can:

Subtract 1,

Divide by 2, if it is divisible by 2

Divide by 3, if it is divisible by 3

All Dynamic programming problems have a start state. You have to reach the goal by transitioning through a number of intermediate states. In a typical textbook, you will often hear the term subproblem. It is the same as a state. The terms can be used interchangeably. In this article, I will use the term state instead of the term subproblem.

所有动态编程问题都有一个开始状态。 Ÿ欧有通过一些中间状态的 转换以达到目标 。 在典型的教科书中,您经常会听到“ 子问题 ”一词。 它与状态相同。 这些术语可以互换使用。 在本文中,我将使用状态一词来代替子问题。

What is a subproblem or state ? A subproblem/state is a smaller instance of the original problem. The methods used to solve the original problem and the subproblem are the same.

什么是子问题或状态? 子问题/状态是原始问题的较小实例。 解决原始问题和子问题的方法相同。

Some problems will give you the rules that specify the state transitions. This is one such problem. This problem says you can move to n-1, n/2 or n/3 starting from n. On the flip side, there are problems that will not specify the state transitions. You will have to figure them out by yourself. I will talk about these types of problems in another post.

一些问题将为您提供指定状态转换的规则。 这就是这样一个问题。 此问题表明您可以从n开始移至n-1,n / 2或n / 3。 另一方面,有些问题不会指定状态转换。 您将不得不自己弄清楚它们。 我将在另一篇文章中讨论这些类型的问题。

Here,

这里,

Start state -> n
Goal -> 1
Intermediate states -> any integer number between 1 and n

Given a state (either start or intermediate), you can always move to a fixed number of states.

给定一个状态(开始或中间状态), 您始终可以移至固定数量的状态。

from n you can move to :

n -> n-1 

if n % 2 == 0:
   n -> n/2
   
if n % 3 == 0:
   n -> n/3
   
example:

from 3 you can move to,
3 -> 3-1 = 2
3 -> 3/3 = 1

from 4 you can move to,
4 -> 4-1 = 3
4 -> 4/2 = 2

In a dynamic programming optimization problem, you have to determine moving though which states from start to goal will give you an optimal solution.

在动态编程优化问题中您必须确定从开始到到达哪个状态将为您提供最佳解决方案。

For n = 4:

approach one:
4 -> 3 -> 2 -> 1

approach two:
4 -> 2 -> 1 

approach three:
4 -> 3 -> 1

Here, of the three approaches, approaches two and three are optimal, as they require smallest amount of moves/transitions. Approach one is the worst, as it requires more moves.

在这三种方法中,方法二和方法三是最佳的,因为它们需要最少的移动/过渡。 方法一是最糟糕的,因为它需要更多的动作。

教科书术语解释 (Textbook terminologies explained)
Repetitive subproblems : You will end up solving the same problem more than once.

for n = 5
example:
5 -> 4 -> 3 -> 1
5 -> 4 -> 2 -> 1
5 -> 4 -> 3 -> 2 -> 1

observe here that 2 -> 1 occurs two times. 
also observe that 5 -> 4 occurs three times.

Optimal Substructure : Optimal solutions to subproblems give optimal solution to the entire problem

example:
2 -> 1 is optimal 
3 -> 1 is optimal 

when I am at 4,
4 -> 3 -> 2 -> 1 and 4 -> 3 -> 1 is possible
but the optimal solution of 4 is 4 -> 3 -> 1. The optimal solution of four comes from optimal solution of three (3 -> 1).

similarly,
4 -> 3 -> 2 -> 1 and 4 -> 2 -> 1 is possible
but the optimal solution of 4 is 4 -> 2 -> 1. The optimal solution of four comes from optimal solution of two (2 -> 1).

now 5,
The optimal solution of 5 depends on optimal solution to 4.
5 -> 4 -> 2 -> 1 and 5 -> 4 -> 3 -> 1 are optimal.

How should you use Repetitive subproblems and Optimal Substructure to our advantage ?

您应如何利用重复子问题和最优子结构来发挥我们的优势?

We will solve the subproblems only once and solve each subproblem optimally.

我们将只解决一次子问题,并以最佳方式解决每个子问题。

we will solve the subproblems 3 -> 1 and 2 -> 1 only once and optimally.

Now for 4 we will solve only once by 4 -> 3 -> 1 and optimally. You can also solve as 4 -> 2 -> 1 but that is left to you. 

Finally for 5 we will solve only once by 5 - > 4 -> 3 -> 1 and optimally.

In practice you will use an array to store the optimal result of a subproblem. This way when you have to solve the subproblem again, you can get the value from the array rather than solving it again. Essentially you are now solving a subproblem only once.

实际上,您将使用数组来存储子问题的最佳结果。 这样,当您必须再次解决子问题时,可以从数组中获取值,而不必再次解决。 本质上,您现在只解决一次子问题。

如何衡量最优性 (How to measure Optimality)

By using something called cost. There is always a cost associated with moving from one state to another state. Cost may be zero or a finite number. The set of moves/transitions that give the optimal cost is the optimal solution.

通过使用称为成本的东西。 从一种状态转移到另一种状态始终存在成本。 成本可以为零或有限的数字。 给出最佳成本的一组移动/过渡是最佳解决方案。

In 5 -> 4 -> 3 -> 1 
for 5 -> 4 cost is 1 
for 4 -> 3 cost is 1
for 3 -> 1 cost is 1

The total cost of 5 -> 4 -> 3 -> 1 is the total sum of 3.

In In 5 -> 4 -> 3 -> 2 -> 1
for 5 -> 4 cost is 1
for 4 -> 3 cost is 1 
for 3 -> 2 cost is 1
for 2 -> 1 cost is 1
The total cost of 5 -> 3 -> 2 -> 1 is the total sum of 4.

The optimal solution of 5 -> 4 -> 3 -> 1 has a cost of three which is the minimum. Hence we can see that optimal solutions have optimal costs

Recursive Relation: All dynamic programming problems have recursive relations. Once you define a recursive relation, the solution is merely translating it into code.

递归关系:所有动态编程问题都具有递归关系。 一旦定义了递归关系,解决方案就是将其转换为代码。

For the above problem, let us define minOne as the function that we will use to solve the problem and the cost of moving from one state to another as 1.

if n = 5,
solution to 5 is cost + solution to 4
recursive formulae/relation is 
minOne(5) = 1 + minOne(4) 

Similarly,
if n = 6,
recursive formulae/relation is
minOne(6) = min(             
              1 + minOne(5),
              1 + minOne(3),
              1 + minOne(2) )

(Code)

Dynamic programming problems can be solved by a top down approach or a bottom up approach.

动态编程问题可以通过自上而下的方法或自下而上的方法解决。

Top Down : Solve problems recursively. 
for n = 5, you will solve/start from 5, that is from the top of the problem.
It is a relatively easy approach provided you have a firm grasp on recursion. I say that this approach is easy as this method is as simple as transforming your recursive relation into code.

Bottom Up : Solve problems iteratively.
for n = 5, you will solve/start from 1, that is from the bottom of the problem.
This approach uses a for loop. It does not lead to stack overflow as in recursion. This approach is also slightly more optimal.
哪种方法更好? (Which approach is better?)

It is up to your comfort. Both give the same solutions. In very large problems, bottom up is beneficial as it does not lead to stack overflow. If you choose a input of 10000, the top-down approach will give maximum call stack size exceeded, but a bottom-up approach will give you the solution.

这取决于您的舒适度。 两者给出相同的解决方案。 在非常大的问题中,自下而上是有益的,因为它不会导致堆栈溢出。 如果您选择输入10000,则自上而下的方法将提供最大的调用堆栈大小,但自下而上的方法将为您提供解决方案。

But do remember that you cannot eliminate recursive thinking completely. You will always have to define a recursive relation irrespective of the approach you use.

但是请记住,您不能完全消除递归思维。 无论使用哪种方法,您都将必须定义一个递归关系。

自下而上的方法 (Bottom-Up approach)
/*
Problem: Given an integer n, find the minimum number of steps to reach integer 1.
At each step, you can:
Subtract 1,
Divide by 2, if it is divisible by 2 
Divide by 3, if it is divisible by 2 
*/


// bottom-up
function minOneBottomUp(n) {

    const cache = [];
    // base condition
    cache[1] = 0;

    for (i = 2; i <= n; i++) {

        // initialize a , b and c to some very large numbers
        let a = 1000, b = 1000, c = 1000;

        // one step from i -> i-1
        a = 1 + cache[i - 1];

        // one step from i -> i/2 if i is divisible by 2
        if (i % 2 === 0) {
            b = 1 + cache[i / 2];
        }

        // one step from i -> i/3 if i is divisible by 3
        if (i % 3 === 0) {
            c = 1 + cache[i / 3];
        }

        // Store the minimum number of steps to reach i
        cache[i] = Math.min(a, b, c);
    }

    // return the number minimum number of steps to reach n
    return cache[n];
}

console.log(minOneBottomUp(1000));
Line 11 : The function that will solve the problem is named as minOneBottomUp. It takes n as the input.

Line 13 : The array that will be used to store results of every solved state so that there is no repeated computation is named cache. Some people like to call the array dp instead of cache. In general, cache[i] is interpreted as the minimum number of steps to reach 1 starting from i.

Line 15 : cache[1] = 0 This is the base condition. It says that minimum number of steps to reach 1 starting from 1 is 0.

Line 17 - 37 : For loop to fill up the cache with all states from 1 to n inclusive.

Line 20 : Initialize variables a, b and c to some large number. Here a represents minimum number of steps. If I did the operation n-1, b represents the minimum number of steps. If I did the operation n/2, c represents the minimum number of steps. If I did the operation n/3. The initial values of a, b and c depends upon the size of the problem.

Line 23 : a = 1 + cache[i-1]. This follows from the recursive relation we defined earlier.

Line 26 - 28: if(i % 2 == 0){
                  b = 1 + cache[i/2];
              }
              
This follows from the recursive relation we defined earlier.

Line 31 - 33: if(i % 3== 0){
                  c= 1 + cache[i/3];
              }
This follows from the recursive relation we defined earlier.

Line 36 : This the most important step.
cache[i] = Math.min(a, b, c). This essentially determines and stores which of a, b and c gave the minimum number of steps.

Line 40 : All the computations are completed. Minimum steps for all states from 1 to n is calculated. I return cache[n](minimum number of steps to reach 1 starting from n) which is the answer we wanted.

Line 43 : Testing code. It returns a value of 9
自上而下的方法 (Top-Down approach)
/*
Problem: Given an integer n, find the minimum number of steps to reach integer 1.
At each step, you can:
Subtract 1,
Divide by 2, if it is divisible by 2 
Divide by 3, if it is divisible by 2 
*/


// top-down
function minOne(n, cache) {

    // if the array value at n is not undefined, return the value at that index
    // This is the heart of dynamic programming 
    if (typeof (cache[n]) !== 'undefined') {
        return cache[n];
    }

    // if n has reached 1 return 0
    // terminating/base condition
    if (n <= 1) {
        return 0;
    }

    // initialize a , b and c to some very large numbers
    let a = 1000, b = 1000, c = 1000;

    // one step from n -> n-1
    a = 1 + minOne(n - 1, cache);

    // one step from n -> n/2 if n is divisible by 2
    if (n % 2 === 0) {
        b = 1 + minOne(n / 2, cache);
    }

    // one step from n -> n/3 if n is divisible by 3
    if (n % 3 === 0) {
        c = 1 + minOne(n / 3, cache);
    }

    // Store the minimum number of steps to reach n 
    return cache[n] = Math.min(a, b, c);

}



const cache = [];
console.log(minOne(1000, cache));
Line 11 : The function that will solve the problem is named as minOne. It takes n and cache as the inputs.

Line 15 - 16 : It checks if for a particular state the solution has been computed or not. If it is computed it returns the previously computed value. This is the top-down way of not doing repeated computation.

Line 21 - 23 : It is the base condition. It says that if n is 1 , the minimum number of steps is 0.

Line 26 :  Initialize variables a, b and c to some large number. Here a represents minimum number of steps if I did the operation n-1, b represents the minimum number of steps if I did the operation n/2 and c represents the minimum number of steps if I did the operation n/3. The initial values of a, b and c depends upon the size of the problem.

Line 29 : a = 1 + minOne(n-1, cache). This follows from the recursive relation we defined earlier.

Line 32 - 34 : if(n % 2 == 0){
                  b = 1 + minOne(n/2, cache);
              }
This follows from the recursive relation we defined earlier.

Line 37 - 39 : if(n % 3== 0){
                  c = 1 + minOne(n/3, cache);
              }
This follows from the recursive relation we defined earlier.

Line 42 : return cache[n] = Math.min(a, b, c) . This essentially determines and stores which of a, b and c gave the minimum number of steps.

Line 48 - 49 : Testing code. It returns a value of 9
时间复杂度 (Time Complexity)

In Dynamic programming problems, Time Complexity is the number of unique states/subproblems * time taken per state.

在动态编程问题中,时间复杂度是唯一状态/子问题的数量*每个状态花费的时间

In this problem, for a given n, there are n unique states/subproblems. For convenience, each state is said to be solved in a constant time. Hence the time complexity is O(n * 1).

在这个问题中,对于给定的n,存在n个唯一状态/子问题。 为方便起见,据说每个状态都在固定时间内解决。 因此,时间复杂度为O(n * 1)。

This can be easily cross verified by the for loop we used in the bottom-up approach. We see that we use only one for loop to solve the problem. Hence the time complexity is O(n ) or linear.

通过自下而上方法中使用的for循环,可以轻松地对此进行交叉验证。 我们看到我们只使用一个for循环来解决问题。 因此,时间复杂度为O(n)或线性。

This is the power of dynamic programming. It allows such complex problems to be solved efficiently.

这就是动态编程的力量。 这样可以有效地解决这些复杂的问题。

空间复杂度 (Space Complexity)

We use one array called cache to store the results of n states. Hence the size of the array is n. Therefore the space complexity is O(n).

我们使用一个称为缓存的数组来存储n个状态的结果。 因此,数组的大小为n。 因此,空间复杂度为O(n)

DP作为时空权衡 (DP as Space-Time tradeoff)

Dynamic programming makes use of space to solve a problem faster. In this problem, we are using O(n) space to solve the problem in O(n) time. Hence we trade space for speed/time. Therefore it’s aptly called the Space-Time tradeoff.

动态编程利用空间来更快地解决问题。 在此问题中,我们使用O(n)空间来解决O(n)时间内的问题。 因此,我们以空间换取速度/时间。 因此,它被恰当地称为“ 时空权衡”。

结语 (Wrapping up)

I hope this post demystifies dynamic programming. I understand that reading through the entire post might’ve been painful and tough, but dynamic programming is a tough topic. Mastering it requires a lot of practice.

我希望这篇文章能揭开动态编程的神秘面纱。 我了解通读整篇文章可能会很痛苦且艰辛,但是动态编程是一个艰巨的话题。 掌握它需要大量练习。

I will publish more articles on demystifying different types of dynamic programming problems. I will also publish a article on how to transform a backtracking solution into a dynamic programming solution.

我将发表更多文章以揭开不同类型的动态编程问题的神秘面纱。 我还将发表有关如何将回溯解决方案转换为动态编程解决方案的文章。

If you like this post, please support by clapping ?(you could go up to 50) and follow me here on Medium ✌️. You can connect with me on LinkedIn . You can also follow me on Github.

如果您喜欢这篇文章,请通过鼓掌来支持(您最多可以支持50位),并在Medium✌️上关注我。 您可以在LinkedIn上与我联系。 您也可以在Github上关注我。

翻译自: https://www.freecodecamp.org/news/demystifying-dynamic-programming-24fbdb831d3a/

揭开均线系统的神秘面纱

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值