摘要:延续首题优良传统进一步探讨和训练一下动规的基本套路.

1.正文:其实,动态规划作为运筹学的一个分支,经过前辈的理论沉淀,已经形成一套科学可行的解决思路,在实践中借用前辈的智慧,结合实际问题我可以总结我在解决动规过程中常有的实现套路:

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

  (1)题意分析;

  (2)基于分析数学建模;

  (3)判定是否可以符合使用动规的两大前置条件(最优子结构和无后效性),是则下一步,否则终止(非动规可以解决的问题,另寻他法);

  (4)动规基本三步曲:

    1)结合题意根据模型选择计算出比较合适的状态转移方程,归约初始的状态值,推导出终止(最终收敛)条件;

    2)迭代验证;

    3)选择合适的迭代次序实现状态转移方程的迭代和收敛;

  (5)编程实现。

  对待常规的动规问题,这样的做法足矣。虽然没有理论上那么严谨,表现在没有过多的校验或者数学推演,但须知我们本来就是为了解决现实问题而非理论问题,所以在面向工业实践上的问题来看这样的套路基本属于严谨的范畴了。以下我逐步说明一下为何要做这样的步骤:

  步骤(1):题意分析是大前提,如果这一步都做错了,映射到解决业务需求的现实工作模型中你就是没有跟产品对齐需求,理解错了需求了,这样做出来的产品还会有业务价值吗?

  步骤(2):基于分析来建立数学模型,一是确保分析过后的正确理解作为建立数学模型的输入参数,这样一环扣一环,才会确保问题解决的正确性;二是必然,所有信息化相关的实际问题终究是归结于数学模型的建立,信息的输入输出模型本质上就是可以映射成数学中自变量与因变量的函数变换,当然还有诸多特质注定它可以被数学建模解决。如果不分析你很难建模;如果不建模,你很难理解该如何推导出合适状态转移方程,所以这一步是衔接;

  步骤(3):但凡想用任何一种技巧或方法去解决某类问题,都先校验清楚该技巧或方法的适用范围(无论是本次讨论的动态规划还是以后二分查找、分治法等等方法)是否符合题意场景,不然即便做出来也不能确保它的正确性;

  步骤(4):按着这三步曲走只是博主总结的个人实例,如上有问题,还请多多指教!

      1)即便是一道题适用动态规划,他也可以有种形式的状态转移方程(后面会讨论),至于什么是合适的,除了具体问题具体分析外,还有结合方程是否可以转化为方便编程实现的要素考虑;

      2)验证就不多说了,好比测试驱动开发中的测试步骤,必不可少的验证其正确性的步骤。

      3)至于选择合适的迭代次序,博主踩过坑,有一次推导出一个个性化的动规转移方程上是需要按列逆向遍历的二重遍历,但没理解到位,按常规的行正向遍历次序去遍历动规的存储数组,导致了迭代产生的数据都是不对的,通过调试才发现,所以迭代计算的顺序也是需要关注的。另外,确保动规的状态转移方程是可以收敛的,程序才不会死循环,可收敛表现在可以根据参数推导出迭代终止条件的。

  步骤(5)不再赘述,关键在于组织好代码即可。

  不多说,看看今日有趣的例题。

2.题目:

某蛋糕店推出一种礼品卡,使用该卡可以用于购买店内的蛋糕,
每种蛋糕限购1个。
该卡片面值为R元,仅可以使用1次,结账后卡内剩下的余额作废。
已知店内的全部种类的蛋糕价格都为整数,
求出怎么购买,可使结账后卡内剩下的余额最少?

3.输入输出示例:

输入:

1)所有蛋糕的价值:
  2, 3, 5, 11, 6
2)面值R:
  15

输出最少的余额:  

  1

4.例程: 

public class Asolution {

/**
某蛋糕店推出一种礼品卡,使用该卡可以用于购买店内的蛋糕,
每种蛋糕限购1个。
该卡片面值为R元,仅可以使用1次,结账后卡内剩下的余额作废。
已知店内的全部种类的蛋糕价格都为整数,
求出怎么购买,可使结账后卡内剩下的余额最少
*/
/**
* F[i][j]表示用j元面额买[0 .. i]内的蛋糕的最大金额
* F[i][j] = max{F[i - 1][j - a[i]] + a[i] , F[i - 1][j]}
*/

private static int getMaxValues(int[] prices, int r) {
int[][] maxs = new int[prices.length][r + 1];
int max = 0;
for (int j = 0; j <= r; j++) {
if (j >= prices[0]) {
maxs[0][j] = prices[0];
        }
}
for (int i = 1; i < maxs.length; i++) {
for (int j = 0; j <= r; j++) {
maxs[i][j] = maxs[i - 1][j];
if (j >= prices[i]) {
maxs[i][j] = Math.max(maxs[i][j] , maxs[i - 1][j - prices[i]] + prices[i]);
          }
if (max < maxs[i][j]) {
max = maxs[i][j];
          }
}
}
return max;
}

public static void main(String[] strings) {
int[] prices = {2, 3, 5, 11, 6};
    System.out.println(15 - getMaxValues(prices , 15));
  }
}


现在按上述我自行总结的套路捋一遍:


该题比较浅显,算是出题人已经帮你建好模了,不需要再抽象出来数学模型(变量都直接标注在题干里)。
1.基于分析建模:问题最明显:剩下余额最少,面值R固定,花费才是主动发生变化的变量,故我们可以转化问题为求最大花费,即一个最优化问题,
1)它的未来状态不会影响过去状态(你将来花了多少钱都不会影响你过去实际已经花掉的最大值(是过去已经花掉的,不包含当前选择花费多少的动态考虑)),无后效;
2)在子范围内(n - 1个蛋糕)解决类似的问题得到的答案可以用于父范围(n个蛋糕),属于最优子结构。

2.三步走:
1) 显然,状态转移方程很容易可以得出,分类讨论一下即可:

  (1)买了价值a[i]的蛋糕则:F[i - 1][j - a[i]] + a[i];

  (2)不买价值a[i]的蛋糕则:F[i - 1][j]

  两者比较最大者胜出,这就”打擂台“得到了最大花费了。

  * F[i][j]表示用j元面额买[0 .. i]内的蛋糕的最大金额
  * F[i][j] = max{F[i - 1][j - a[i]] + a[i] , F[i - 1][j]}

  显然,初始值F[0][j] =a[0],终止条件是遍历完成;

  2)验证:举例如:求F[1][15],此时i=1,j=15

    F[i - 1][j - a[i]] + a[i] = F[0][15 - 3] + 3 = a[0] + 3 = 5

    F[i - 1][j] = F[0][15] = 2;

  所以显然是对的,当然还可以再验证多几个,不再赘述。

  3)这里只要捋清楚遍历次序就是了,因为初始值比较常规,就是首行,正向行遍历即可。

5.总结:

 1.虽然只是简单的一道线性动规,但是捋清楚整个套路下来,我们就很容易聚焦到重点难点上:1)对于未曾建模的题型(后面会遇到),建模将会是难点,因为它是现实模型到数理模型的一个映射,焦点抓得好,建模才有效果;2)推导选择出合适的状态转移方程,无他,唯训练尔。当然,数学知识将会是很大的帮助。

 

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄