出现字迹模糊迹象_改变迹象:如何使用动态编程解决竞争性编程问题

出现字迹模糊迹象

by Sachin Malhotra

由Sachin Malhotra

改变迹象:如何使用动态编程解决竞争性编程问题 (Change the signs: how to use dynamic programming to solve a competitive programming question)

If you’re a competitive programmer like I am, one of the best feelings in the world is seeing your program getting accepted on first try on one of the most famous programming platforms, CodeChef.

如果您是像我这样的有竞争力的程序员,那么世界上最好的感觉之一就是看到您的程序在最著名的编程平台之一CodeChef上首次尝试就被接受。

I was an avid competitive programmer during undergrad, and then lost touch with it when working as a developer @Hike. However, I recently started out into this adventurous world of programming again, all thanks to my friend Divya Godayal.

在本科期间,我曾是一名狂热的竞争性程序员,然后在作为开发人员@Hike时失去了与它的联系。 但是,最近我再次开始进入这个冒险的编程世界,这一切都要归功于我的朋友Divya Godayal

The CodeChef May 2018 Long Challenge ended about an hour ago, and I decided to write this article as a post describing one of the questions in the competition.

CodeChef 2018年5月的长期挑战赛大约一个小时前结束,我决定将这篇文章写为一篇文章,描述比赛中的一个问题。

Without wasting any more time, let’s get to it.

不要浪费更多的时间,让我们开始吧。

解开问题陈述 (Unravelling the Problem Statement)

Let’s look at some examples to better understand what the problem statement is asking for.

让我们看一些例子,以更好地理解问题陈述的要求。

Consider the following number sequence.

考虑以下数字顺序。

4 3 1 2

Now the question asks us to perform a certain operation (possibly 0 times, leaving the sequence unchanged). We can negate a certain subsequence of numbers and get a new sequence.

现在,问题要求我们执行某些操作(可能为0次,序列保持不变)。 我们可以否定数字的某个子序列并获得新的序列。

-4 3 1 24 -3 1 -24 3 -1 24 3 1 -2-4 -3 1 2 etc.

The question says that the resulting sequence should satisfy the following constraint:

问题说,结果序列应满足以下约束:

The sum of elements of any substring with length greater than 1 is strictly positive.

长度大于1的任何子字符串的元素之和严格为正。

Clearly, the following sequences are not valid:

显然,以下序列无效:

-4 3 1 24 -3 1 -2 4 3 1 -2 -4 -3 1 2 -4 -3 -1 -24 3 -1 -2

We only have 2 valid subsequences that can be obtained by performing the operation mentioned above. Note: we haven’t written down all the possible subsequences. That would be 2^n, that is 16 in this case, because for every number we have two options. Either to negate it, or not.

通过执行上述操作,我们只有2个有效的子序列。 注意:我们还没有写下所有可能的子序列。 那将是2 ^ n,在这种情况下就是16,因为对于每个数字,我们都有两个选择。 是否取消它。

So the two valid sequences are:

因此,两个有效序列为:

4 3 1 2

and

4 3 -1 2

The original sequence would always be one of the valid sequences as all the numbers in it are positive.

原始序列始终是有效序列之一,因为其中的所有数字都是正数。

Now the question asks us to find the sequence with the minimum sum. So for the example we have considered, the sequence required would be 4 3 -1 2 .

现在,问题要求我们找到具有最小总和的序列。 因此,对于我们考虑的示例,所需的序列将为4 3 -1 2

贪婪会工作吗? (Would Greedy Work?)

A greedy approach in this question would be that if it is possible to negate a number while satisfying the given constraints, then we should negate that number. This approach however, would not always give the right results. Consider the following example.

这个问题中的一种贪婪方法是,如果可以在满足给定约束的情况下否定一个数字,那么我们应该否定那个数字。 但是,这种方法并不总是能给出正确的结果。 考虑以下示例。

4 1 3 2

Here, it is possible to have these three valid sets of numbers:

在这里,可以具有以下三个有效数字集:

4 1 3 2           4 -1 3 2           4 1 3 -2

Clearly, both the numbers 2 and 1 can be negated. But not both of them at the same time. If we negate a number greedily — that is, if a number can be negated, then we negate it — then it is possible that we might end up negating the number 1. Then you won’t be able to negate the number 2. This would give us a suboptimal solution.

显然,数字2和1都可以取反。 但不是两个都同时出现。 如果我们贪婪地否定一个数字(也就是说,如果一个数字可以取反,那么我们将其取反),那么我们最终可能会否定一个数字1。那么您将无法否定这个数字2。会给我们一个次优的解决方案。

So this Greedy approach would not work here. We have to “try out a specific choice of whether to negate or not for a number and see what choice gives us the optimal solution”.

因此,这种贪婪方法在这里行不通。 我们必须“尝试确定是否对某个数字求反的特定选择,并查看哪种选择可以为我们提供最佳解决方案”

This smells like Dynamic Programming.

这闻起来像动态编程。

好的动态编程 (Good ol’ Dynamic Programming)

One of the most interesting algorithmic techniques out there, and possibly one of the most dreaded, is dynamic programming. This is the technique we are going to use to solve this particular problem.

动态编程是目前最有趣的算法技术之一,而且可能也是最令人恐惧的算法之一。 这是我们将用来解决此特定问题的技术。

Two of the most important steps in any dynamic programming problem are:

任何动态编程问题中最重要的两个步骤是:

  1. Identifying the recurrent relation.

    确定递归关系。
  2. Figuring out what to memoize. (not memoRize :P)

    找出要记住的内容 (不是memoRize:P)

The DP-based approach here is divided into two basic parts.

这里基于DP的方法分为两个基本部分。

  • One is the main recursion that we use to find out the minimum sum of the final set. Note, the dynamic programming is not directly used to obtain the final set, just the sum of the final set of numbers. So our dynamic programming approach would correctly find out the sum for the example given above as 8. 4 + 3 + (-1) + 2 = 8 .

    一种是主要的递归,我们用它来找出最终集合最小和 。 注意,动态编程不是直接用于获得最终集合,而只是直接获得最终数字集的总和。 因此,我们的动态编程方法将正确找到上述示例的总和为8。4 4 + 3 + (-1) + 2 = 8

  • What we actually need is the final modified set of numbers where some (possibly none) of the numbers are negated. We use the concept of a parent pointer and backtracking to find out the actual set of numbers.

    我们真正需要的是最终的一组修改的数字,其中一些(可能没有)数字被取反了。 我们使用父指针回溯的概念来找出实际的数字集。

Let’s move onto our recursion relation for our dynamic programming approach.

让我们进入我们的动态编程方法的递归关系。

Before describing the recursive relation an important observation to make here is that if a number has been negated, then any adjacent number to it can not be negative. That is, two adjacent numbers cannot be negative as that would give a substring of length 2 whose sum is negative, and that is not allowed according to the question.

在描述递归关系之前,这里需要做的一个重要观察是,如果一个数字被取反, 那么任何与其相邻的数字都不能为负 。 那是, 两个相邻的数字不能为负,因为那样会产生长度为2的子串,其总和为负,并且根据问题不允许这样做。

For the recurrence relation, we need two variables. One is the index number of where we are in the array, and one is a boolean value that tells us if the previous number (one left to the previous number) is negated or not. So if the current index is i, then the boolean value would tell us if the number at i — 2 was negated or not. You will know the importance of this boolean variable in the next paragraph.

对于递归关系,我们需要两个变量。 一个是我们在数组中的位置的索引号,另一个是一个布尔值,它告诉我们前一个数字(前一个数字的左边)是否被取反。 因此,如果当前索引是i ,那么布尔值会告诉我们,如果在号码i — 2被否定或不。 在下一段中,您将知道此布尔变量的重要性。

We need to know in O(1) if a number can be negated or not. Since we are following a recursion with memoization-based solution, whenever we are at an index i in the recursion, we are sure that the numbers to the right (i+ 1 onwards) have not been processed up to this point. This means that all of them are still positive.

我们需要在O(1)知道数字是否可以取反。 由于我们正在使用基于记忆的解决方案进行递归,因此只要我们在递归中位于索引i ,就可以确保到目前为止,尚未处理右边的数字(从i+ 1开始)。 这意味着他们所有人仍然是积极的。

The choice of whether the number at index i can be negated is dependent upon the right hand side (if there is one) and the left hand side (if there is one). The right hand side is easy. All we need to check is if

索引i的数字是否可以取反的选择取决于右侧(如果有一个)和左侧(如果有一个)。 右侧很容易。 我们需要检查的是

number[i] < number[i + 1]

because if this is not true, then adding these two would give a negative value for the substring [i, i + 1] thus making it an invalid operation.

因为如果这不是真的,则将这两个值相加会给子字符串[i, i + 1]赋予负值[i, i + 1]从而使其无效。

Now comes the tricky part. We need to see if negating the number at i will cause a substring of negative sum to the left or not. When we reach the index i in our recursion, we have already processed the numbers before it, and some might have been negated as well.

现在是棘手的部分。 我们需要查看是否对i取反会导致左边的和为负数。 当我们在递归中达到索引i时,我们已经处理了它之前的数字,并且有些数字也可能被取反。

So say we have this set of numbers 4 1 2 1 and we had negated the first 1 and we are now processing the last number ( 1 ).

假设我们有这组数字4 1 2 1而我们否定了前一个数字1 ,现在正在处理最后一个数字( 1 )。

4 -1 2 [1]

The last number in square brackets is the one we are processing right now. As far as the right hand side is concerned, since there is none, we can negate it. We need to check if negating this 1 at index 3 (0 based indexing) would cause any substring to the left of ≤ 0 sum. As you can see, it will produce such a substring.

方括号中的最后一个数字是我们现在正在处理的数字。 就右侧而言,既然没有,我们可以否定它。 我们需要检查在索引3处否定此1(基于0的索引)是否会导致任何子串在≤0 sum的左边。 如您所见,它将产生这样的子字符串。

-1 2 -1

This substring would have a 0 sum, and that is invalid according to the question. After negating a subsequence of numbers, the substrings in the final set should have a sum which is strictly positive. All the substrings of length > 1.

该子字符串的总和为0,根据问题,该值无效。 排除数字的子序列后,最终集中的子字符串的总和应严格为正。 所有长度> 1的子串

We cannot apply the following approach here directly:

我们无法在此处直接应用以下方法:

if number[i] < number[i - 1], then it is good to go on negation.

because, although 1 <; 2 , if we negate that last 1 as well we will have an invalid set of numbers as seen above. So this simple approach or check won’t work here.

因为,尽管1 < ; 2,如果我们也否定最后一个1,我们将得到无效的数字集,如上所示。 因此,这种简单的方法或检查在这里行不通。

Here comes the boolean variable which tells us if, given an index i, the number at i — 2 was negated or not. Consider the two scenarios.

这里谈到的布尔变量,如果它告诉我们,给定一个指标i ,在数字i — 2是否定或没有。 考虑这两种情况。

  • Yes, the number at index i — 2 was negated like in the example just showcased. In that case, negation of the number at i — 2 would have a capacity reduction for number at i — 1. In the example 4 1 2 1 , negating the 1 at index 1(0 based indexing) would reduce the capacity of the number 2 (at index 2) by 1. We refer to remaining values of numbers as capacities here. We need to consider this reduced capacity when performing the check to see if a number can be negated or not.

    是的,索引i — 2被否定,就像刚刚展示的示例一样。 在这种情况下,在号码否定i — 2将具有在对数的能力减少i — 1 。 在示例4 1 2 1 ,在索引1(基于0的索引)处取反1将使数字2(在索引2)的容量减少1。我们在此将数字的剩余值称为容量。 在执行检查以查看数字是否可以取反时,我们需要考虑这种减少的容量。

number[i] < reducedCapacityOfNumberAt(i - 1)
  • In case the number at index i — 2 wasn’t negated, the number at i — 1 is at it’s full capacity. The simple check

    如果在索引数量i — 2并没有否定,人数为i — 1是它的满负荷生产。 简单检查

number[i] < number[i - 1]

would be enough to see if we can negate the number at index i .

足以确定我们是否可以否定索引i的数字。

Let’s look at the code for the recursion containing all the ideas discussed above.

让我们看一下包含上面讨论的所有想法的递归代码。

That’s all nice and dandy. But, this is just recursion, and the heading says dynamic programming. That means there would be overlapping subproblems. Let us look at the recursion tree to see if there are any.

很好,花花公子。 但是,这只是递归,标题说的是动态编程。 这意味着将存在重叠的子问题。 让我们看一下递归树,看是否有递归树。

As you can see, there are overlapping subproblems in the recursion tree. That is why we can use memoization.

如您所见,递归树中有重叠的子问题。 这就是为什么我们可以使用记忆。

The memoization is as simple as:

备注很简单:

""" This comes at the top. We check if the state represented by the tuple of the index and the boolean variable is already cached """
if(memo[i][is_prev_negated] != INF) {    return memo[i][is_prev_negated];}
...... CODE
# Cache the minimum sum from this index onwards.memo[i][is_prev_negated] = min(pos, neg);
# The parent pointer is used for finding out the final set of #sparent[i][is_prev_negated] = min(pos, neg) == pos ? 1 : -1;

As pointed out earlier, this recursive approach would return the minimum sum of the set of numbers possible after making the valid set of modifications to them.

如前所述,这种递归方法在对数字进行有效修改后将返回可能的最小数字总和。

The question, however, asks us to actually print the final set of numbers that gives the minimum sum after making such modifications. For that, we need to use a parent pointer that would tell us at every index and boolean variable is_prev_negated ’s value as to what optimal action was taken.

但是,这个问题要求我们在进行此类修改后实际打印出给出最小总和的最终数字集。 为此,我们需要使用一个父指针,该指针将告诉我们每个索引和布尔变量is_prev_negated的值,以了解采取了什么最佳操作。

parent[i][is_prev_negated] = min(pos, neg) == pos ? 1 : -1;

So we simply store 1 or -1 depending upon if negating the number at index i (if possible!) gave us the minimum sum or if choosing to ignore it gave the minimum sum.

因此,我们简单地存储1或-1,具体取决于是否对索引i的数字取反(如果可能!)给我们最小的总和,或者选择忽略它给我们最小的总和。

回溯 (Backtracking)

Now comes the part where we backtrack to find the solution to our original problem. Note that the decision for the very first number is what propagates the recursion further. If the first number was negated, the second number would be positive and the third number’s decision can be found using parent[2][true]. Similarly, if the first number wasn’t negated, then we move onto the second number and it’s decision can be found using parent[1][false] and so on. Let’s look at the code.

现在是我们回溯以找到原始问题的解决方案的部分。 请注意,第一个数字的决定是进一步传播递归的原因。 如果第一个数字取反,则第二个数字为正,可以使用parent[2][true]找到第三个数字的决定。 同样,如果第一个数字未取反,那么我们移到第二个数字,可以使用parent[1][false]等找到它的决定。 让我们看一下代码。

更好的方法 (A Better Approach)

If you take a look at the space complexity of the solution suggested, you will see that it’s a 2 dimensional dynamic programming solution because the state of the recursion is represented by two variables i.e. the index i representing what number of the array we are considering and then the boolean variable is_prev_negated . So the space complexity and the time complexity would be O(n*2) which is essentially O(n).

如果您看一下所建议解决方案的空间复杂性,您会发现它是一种二维动态规划解决方案,因为递归的状态由两个变量表示,即索引i表示我们正在考虑的数组数量以及然后是布尔变量is_prev_negated 。 因此,空间复杂度和时间复杂度将为O(n * 2),本质上为O(n)。

However, there is a slightly better approach as well to solving this problem as suggested by Divya Godayal. This problem can even be solved by 1 dimensional dynamic programming based solution.

但是, Divya Godayal提出了一种更好的方法来解决此问题。 这个问题甚至可以通过基于一维动态编程的解决方案来解决。

Essentially, the boolean variable is_prev_negated is helping us to decide if we can negate a given number at index i or not as far as the left hand side of the array is concerned i.e. all the numbers from 0 .. i-1 because the right hand side is anyways safe as all the numbers on that side are positive (as the recursion hasn’t reached them yet). So for the right hand side we simply checked the number at i+1 but for the left hand side of index i we had to make use of the boolean variable is_prev_negated .

本质上讲,布尔变量is_prev_negated可以帮助我们确定是否可以对索引i处给定的数字取反,就数组的左侧而言,即all the numbers from 0 .. i-1因为右手无论如何,这边都是安全的,因为该边的所有数字都是正数(因为递归尚未到达它们)。 因此,对于右侧,我们仅检查i+1处的数字,但对于索引i的左侧,我们必须使用布尔变量is_prev_negated

It turns out, that we can simply skip this boolean variable altogether and simply look ahead to decide if a number can be negated or not. Which simply means if you are at an index i, you check if that element along with the element at i+2 have the capacity to swallow the element at i+1 i.e.

事实证明,我们可以简单地完全跳过此布尔变量,并简单地向前看以确定是否可以取反数字。 这只是意味着如果您在索引i ,您将检查该元素以及i+2处的元素是否具有吞下i+1处的元素的能力,即

numbers[i] + numbers[i+2] >= numbers[i+1  (SWALLOW)

If there is a such a possibility, then we directly jump to i+3if we negate element at i because element at i+1 and i+2 both can’t be negative in such a scenario.

如果有这种可能性,那么如果我们否定i处的元素,则我们直接跳到i+3处,因为在这种情况下, i+1i+2处的元素都不能为负。

In case the swallow condition is not satisfied and we end up negating the number at index i , then we would jump to index i+2 because in any case, two consecutive numbers cannot be negated. So if the number at i was negated, then the number at i+1 has to be positive. The swallow check is to see if the number at i+2 would definitely have to be positive or if we can exercise the choice of whether to negate or not there.

如果不满足吞咽条件并且我们最终否定了索引i处的数字,那么我们将跳转到索引i+2因为在任何情况下,两个连续的数字都不能取反。 因此,如果i处的数字为负数,则i+1处的数字必须为正。 吞下检查的目的是确定i+2处的数字是否一定一定是正数,或者我们是否可以选择是否取反。

Have a look at the code for a better understanding.

查看代码以更好地理解。

Hence, just a single variable i.e. the index is used to define the state of the recursion. So the time and space complexity, both got reduced to half of what they were in the previous solution.

因此,仅使用一个变量(即索引)来定义递归的状态。 因此,时间和空间的复杂性都降低到了以前解决方案的一半。

I hope you were able to grasp the working of the algorithm described above and how the dynamic programming technique fits into this problem. I think it’s an interesting problem, because you not only have to use dynamic programming but also the concept of parent pointer to retrace the steps through the optimal solution and get the answer required in the question.

我希望您能够掌握上述算法的工作以及动态编程技术如何解决此问题。 我认为这是一个有趣的问题,因为您不仅必须使用动态编程,而且还必须使用父指针的概念来通过最佳解决方案追溯步骤并获得问题中所需的答案。

翻译自: https://www.freecodecamp.org/news/just-change-the-signs-how-to-solve-a-competitive-programming-question-f9730e8f04a9/

出现字迹模糊迹象

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值