奥林匹克运动中的动态编程

你好

最近,我解决了Timus在线法官档案中的问题,并遇到了一个称为动态编程任务的部分。 我特别感兴趣这种任务,因为这种方法通常可以确保解决方案的速度和优雅。 什么是动态编程?

动态编程是一种解决问题的方法,其中将子任务划分为与原始任务相比“更简单”的子任务。 “动态”一词的含义与“归纳”非常接近:假定答案因某种含义而为人所知 k ,而我想找到答案 k+1 。 在数学中,这称为归纳转换,是动态编程的主要思想。

简单的例子


最引人注目的指示性任务是计算任务 n 斐波那契数列的编号。 已知该序列具有以下属性:

F0=F1=1 Fn=Fn1+Fn2


这立即意味着递归公式:

int Fibonacci(int n) { if(n == 1 || n == 2) return 1; return Fibonacci(n-1) + Fibonacci(n-2); } 

如果递归从“末尾”寻找数字,则以下方法顺序计算位于之间的所有数字 0n

 int dpFibonacci(int n) { int prev1 = 1; int prev2 = 1; int curr = 0; for(int j = 2; j < n; j++) { curr = prev1 + prev2; prev1 = prev2; prev2 = curr; } return curr; } 

显然,对于足够大 n 该算法的运行速度更快:它不会多次计算中间值。 考虑一个稍微复杂的例子。

示例1.您正在收费楼梯上行走。 踩 您必须支付的步骤 ai 硬币。 您可以转到下一步或跳过一个步骤。 任务:通过 n 并花费尽可能少的硬币。

显然,在每一步中,我们将“付款”的数量减至最少,但我们可能会遇到一个非常昂贵的步骤,我们希望避免这样做。 创建一个值数组 d 在其中 j -第一位将是要花费的(最少)硬币数量 j 第一步。 立即清楚的是 d1=a1 d2=a2 。 然后,我们将至少执行前两个步骤,并增加该步骤本身的成本:

di=\最\左di1di2\右+ai



我们稍微改变了问题的条件:假设在某些步骤中您可以获得硬币(这意味着 ak<0 ) 算法中需要更改哪些内容才能给出正确的结果?

解决方案
只需更改动力学的“开始”即可。 如果第一个楼梯没有给我们带来硬币,那么建议跳过它,但是,如果 a1<0 ,最好踩一下并收集硬币。 所以 d2=\最\左0d1\右+a2

考虑另一个使用“二维”动力学的示例。

例子2.在迷宫里有 n\乘m 房间,每个房间都装有金(放在笼子里 ij 谎言 aij 金)。 任务是确定从某个点出发的最佳路线可以收集的最大黄金量 0,0 到这一点 nm 如果您可以向下或向右移动。

因此,我们想知道通往细胞的最佳途径 ij 。 我们可以从两个单元格到达这里- i1jij1 。 假设这两个单元格的最佳路由是已知的(它们存储在某个表中) d ),那么该单元格的答案 ij 获得如下:

dij= max leftdi1jdij1 right+aij


这是另一种经典的动态编程任务,其修改在体育编程任务中非常普遍。 此处将详细解释类似的任务。

更具挑战性的任务

如果需要,可以在任何需要的地方使用动态方法。 考虑Timus在线法官档案中的一项任务

问题的数学公式如下:需要找到将给定数量分解为完整平方所需的最小项数。

和以前一样,假设我们知道所有数字的答案 k1 存储在某个数组中 d 我们想找到 dk

拿这个号码 k 并分析可能的情况:

  1. k 是一个完整的正方形。 在这种情况下 dk=1
  2. 也许以前的数字 k1 是一个完整的正方形。 然后 dk=dk1+1

通常,将单元添加到上一个单元的选项似乎还不错。

我们进行如下操作:我们寻求分解 k=q2+s 这样

dq2+ds<dk1+1

由于 q2 -然后全角 dq2=1

ds<dk1

也就是说,我们发现一个分区比 dk1+1 ,在这种情况下,答案将是

dk=ds+dq2=ds+1



实现此算法的示例Java代码:
 for(int k = 1; k <= n; k++) { int best = d[k - 1] + 1; //      int q = 1; while(k - q*q >= 0) { // k = q*q + s if(k - q*q == 0) { // k -   best = 1; break; } else if(d[k - q*q] < best) best = d[k - q*q] + 1; q++; } d[k] = best; } 


考虑以下问题 。 目标是从 N 根据规则的多维数据集:

  1. 楼梯至少有两个台阶;
  2. 楼梯不能有两个相同的台阶;
  3. 楼梯的台阶按升序排列(即,下一个台阶大于上一个台阶)。

这次我们将建立二维动力学。 建立表格 d 在哪个位置 ij 组成的楼梯数量 高度不超过立方体 j 。 如果解决了,那么我们问题的答案将是总和

 sum limitsnj=1dnj


因此,我们将解决寻找由 高的立方体 j 。 图为落入楼梯 d74

由于我们知道所有楼梯,其中包含较少的立方体,因此我们将“拆分”楼梯 ij 右列。 结果是楼梯c ij 多维数据集。 范例 i=9 j=4

但是对于这样的楼梯,结果是已知的,因此我们将以一个循环对所有这些楼梯进行排序 k 并将所有结果相加。 这样

dij=\总 limitsj1k=1dijk


现在我们将对楼梯的高度进行排序:

dij=\总 limitsj1k=1dijk j=\上线1i

线

最后,改变 来自 1 之前 n 我们得到了答案。

重要提示 :在建立矩阵的过程中,必须考虑到 dii=1 ,因为否则某些类型的楼梯会“丢失”(“断开”时),但是不用说,这样的楼梯不能满足问题的条件,因此答案将是数字 dnn1

实现此算法的示例Java代码:

 dp = new long[n + 1][n+1]; d[1][1] = 1; d[2][1] = 0; d[2][2] = 1; for(int i = 3; i < n + 1; i++) { for(int j = 2; j <i; j++) { long cnt = 0; for(int k = 1; k < j; k++) { cnt += d[i - j][k]; } d[i][j] = cnt; } d[i][i] = 1; //    } long answer = 0L; for(int i = 0; i <= n; i++) { answer += d[n][i]; } answer--; //     


使用一维数组解决下一个任务

所以我们有。 第一个耳语会识2个单词。 每个ent教他自己知道的两个ent的所有单词:Young和Old。 反过来,年轻人被教了许多他已经知道的单词,而老年人只被教了一个单词。 您需要知道多少个实体确切知道 K 词(有必要以模数推论这些实体的数量 P

解决方案非常简单。 创建一个数组 d 在其中 -我们将存储ents的数量(模 P )谁知道 话。 这一切都始于第一个输入项,它知道两个词,因此 d2=1 。 然后一切都很简单:

  • 所有知道奇数单词的ents都是古老的,只能从先前的单词中学习。 因此奇怪 i di=di1;
  • 至于只知道偶数个字词的人,都是从精灵那里得到相同数量字词的人(年轻) + 从先前(旧)中学到的那些人; 也就是说,即使 我们有 di=di\反线2+di1

它仍然需要处理计算模。 为了不存储大量数字,我们将立即记住所有取模值。

实现此算法的示例Java代码:
 int[] d = new int[K + 1]; if(K >= 2) d[2] = 1; if(P != 1) { for(int i = 3; i <= K; i++) { if(i % 2 != 0) { d[i] = d[i - 1]; } else { d[i] = ((d[i/2] % P) + d[i - 1] % P) % P; } } } else d[K] = 0; 


使用的资源:

  1. Timus在线法官;
  2. 关于动态编程的一些知识;
  3. 比较属性取模。

Source: https://habr.com/ru/post/zh-CN418867/


All Articles