一、动态规划
动态规划思想:通过子问题求解原问题,应用于子问题重叠的情况。
动态规划特点:动态规划法通常用来求解最优化问题,该问题具备以下两个要素:
1.最优子结构:一个问题的最优解包含其子问题的最优解。我们必须小心确保考察了最优解中用到的所有子问题。
2.重叠子问题:问题的递归算法会反复地求解相同的子问题,而不是一直生成新的子问题。
动态规划步骤
1.刻画一个最优解的结构特征;
2.递归地定义最优解的值;
3.计算最优解的值,通常采用自底向上的方法;
4.利用计算出的信息构造一个最优解。
动态规划实现方法(等价)
1.带备忘的自顶向下法(top-down with memoization):递归地求解问题,但保存每个子问题的解,若解已保存则返回该解,否则递归求解并保存。
2.自底向上法(bottom-up method):若子问题的求解只依赖于更小的子问题的解,则可以将子问题按规模排序,按由小至大顺序进行求解。
3.两者比较:1.具有相同的渐进运行时间。2.在某些特殊情况下,自顶向下方法并未真正递归地考察所有可能的子问题。3.由于没有频繁的递归函数调用的开销,自底向上方法的时间复杂度通常具有更小的系数。
子问题图
用于分析动态规划问题中子问题及子问题之间的依赖关系,可以用来分析子问题个数(顶点数)和运算时间(顶点的度)。(《算法导论》P209)
分治法与动态规划法比较:
相同:将原问题划分为子问题递归求解;
区别:分治法将问题划分为为互不相交的子问题,这些子问题是全新的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解;动态规划法应用于子问题重叠的情况,即不同的子问题具有公共的子子问题,对每个子子问题只求解一次(此时若采用分治法将会导致重复求解相同的子子问题)。
二、钢铁切割问题
问题描述:给定一段长度为n英寸的钢条和一个价格表Pi,求切割方案,使得销售收益Rn最大。
例子:
输入:
n = 10
i = 1 -- 10下:
长度i 1 2 3 4 5 6 7 8 9 10
价格Pi 1 5 8 9 10 17 17 20 24 30
输出(i = 1 -- 10情况下):
R1 = 1,切割方案1 = 1(无切割)
R2 = 5,切割方案2 = 2(无切割)
R3 = 8, 切割方案3 = 3(无切割)
R4 = 10, 切割方案4 = 2 + 2
R5 = 13, 切割方案5 = 2 + 3
R6 = 17, 切割方案6 = 6(无切割)
R7 = 18, 切割方案7 = 1 + 6或7 = 2 + 2 + 3
R8 = 22, 切割方案8 = 2 + 6
R9 = 25, 切割方案9 = 3 + 6
R10 = 30,切割方案10 = 10(无切割)
分析:对于长度为n的钢条,由于在距离钢条左端长度为i(i=1--n-1)时我们可以选择切割或不切割,所以得出共有2^(n-1)种切割方案。
版本一:递归法(自顶向下)
原理:把一个规模为n的问题缩小为若干规模小于n的子问题,通过合并这些子问题的解来求得该问题的解。钢条切割的思路是:这里把长度为n的钢条切去一段长度i(i = 0 -- n-1),该段成为子问题,这些子问题的最优解和切出的段i价格相加,收益最大的解为当前问题的最优解。
时间复杂度:算法反复求解相同的子问题,当问题规模扩大时,运算时间呈指数增长,T(n)=O(2^n)(可用代入法结合递归方程证明)。递归方程:T(n) = 1 + T(0) + T(1) + T(2) + ... + T(n-1), n > 1; T(n) = 1, n = 0;
体会:利用这种递归实现方式,发现了一种现象,那就是具有重叠子问题。递归法采用了分治的思想,利用规模更小的子问题来解决当前问题,但在递归的过程中出现了重复解决相同子问题这种现象,后面的动态规划法版本就是为了解决这种重复计算问题的。
C语言实现核心代码:
/** * @file cut_rod_by_recursion.c * @brief get maximum price of cut rod by recursion. * @author chenxilinsidney * @version 1.0 * @date 2015-01-04 */ #include <stdio.h> #include <stdlib.h> #include <limits.h> #define NDEBUG #include <assert.h> #include "memory.h" #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define ROD_PRICE_LENGTH 10 TYPE rod_price[ROD_PRICE_LENGTH] = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30}; /** * @brief get max rod price by cutting rod by recursion method. * * @param rod_price rod price list for each type of different length * @param rod_price_length rod price list length * @param rod_length given rod length * * @return max rod price */ TYPE rod_cut_by_recursion(TYPE* rod_price, TYPE rod_length) { if (rod_length == 0) return 0; TYPE i; TYPE max = INT_MIN; for (i = 0; i < rod_length; i++) { TYPE temp = rod_cut_by_recursion(rod_price, rod_length - i - 1) + rod_price[i]; if (temp > max) max = temp; } return max; } int main(void) { // set rod length TYPE rod_length = 8; assert(rod_length <= ROD_PRICE_LENGTH); // get maximum rod price by cutting rod TYPE max_total_price = rod_cut_by_recursion(rod_price, rod_length); // output result printf("max total price = %d\n", max_total_price); return EXIT_SUCCESS; }
版本二:动态规划——带备忘的自顶向下法Top-down with memoization
原理:开辟一个空间或一片内存,递归的过程中把每次解决的子问题的存储下来,每次解决子问题前检查是否已经保存过此解(加入备忘机制),有则使用,无则加入保存。
时间复杂度:T(n)=O(n^2),递归调用的中执行for循环的迭代次数是一个等差数列。
体会:采用一个空间来存储子问题的解,并在递归函数中使用了一个静态变量来存储已有的子问题解的个数。这里的动态规划方法的思想仔细安排求解顺序,存储已有的子问题的解,防止相同的子问题重复解决。动态规划法通过付出额外的空间来节省计算时间,是典型的时空权衡的例子。
C语言实现核心代码:
/** * @file cut_rod_by_memoized_v1.c * @brief get maximum price of cut rod by dynamic programming with * top-down with memoization method. * @author chenxilinsidney * @version 1.0 * @date 2015-01-04 */ #include <stdio.h> #include <stdlib.h> #include <limits.h> #define NDEBUG #include <assert.h> #include "memory.h" #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define ROD_PRICE_LENGTH 10 TYPE rod_price[ROD_PRICE_LENGTH] = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30}; TYPE memoized_cache[ROD_PRICE_LENGTH + 1] = {0}; /** * @brief get max rod price by cutting rod by top-down with memoization method. * * @param rod_price rod price list for each type of different length * @param rod_length given rod length * * @return max rod price */ TYPE rod_cut_by_memoized(TYPE* rod_price, TYPE rod_length) { DEBUG_PRINT_STRING("In recursion now.\n"); DEBUG_PRINT_VALUE("%d", rod_length); static TYPE count = 0; if (count >= rod_length) { DEBUG_PRINT_STRING("get value from cache.\n"); DEBUG_PRINT_VALUE("%d", memoized_cache[rod_length]); DEBUG_PRINT_VALUE("%d", rod_length); DEBUG_PRINT_STRING("Out recursion now.\n"); return memoized_cache[rod_length]; } TYPE i; TYPE max = INT_MIN; for (i = 0; i < rod_length; i++) { TYPE temp = rod_cut_by_memoized(rod_price, rod_length - i - 1) + rod_price[i]; if (temp > max) max = temp; } memoized_cache[rod_length] = max; count++; DEBUG_PRINT_STRING("set value to cache.\n"); DEBUG_PRINT_VALUE("%d", memoized_cache[rod_length]); DEBUG_PRINT_VALUE("%d", rod_length); DEBUG_PRINT_STRING("Out recursion now.\n"); return max; } int main(void) { // set rod length TYPE rod_length = 8; assert(rod_length >= 1 && rod_length <= ROD_PRICE_LENGTH); // get maximum rod price by cutting rod TYPE max_total_price = rod_cut_by_memoized(rod_price, rod_length); // output result printf("max total price = %d\n", max_total_price); return EXIT_SUCCESS; }
版本三:动态规划——带备忘的自顶向下法(优化)Top-down with memoization
原理:在判定子问题的解是否被保存过时,上一个版本在,这个版本把子问题的每个解的都初始化为一个特殊值(这里采用整型最小值),通过判断是否等于这个特殊值就可以知道相应子问题有没有被保存下来,不需要使用静态变量。
时间复杂度:T(n)=O(n^2),内存for循环的迭代次数是一个等差数列。
体会:可利用特殊值(负无穷大,正无穷大,零,负数,正数等)来保存未知值表示未知。
C语言实现核心代码:
/** * @file cut_rod_by_memoized_v2.c * @brief get maximum price of cut rod by dynamic programming with * top-down with memoization method. * @author chenxilinsidney * @version 1.0 * @date 2015-01-04 */ #include <stdio.h> #include <stdlib.h> #include <limits.h> #define NDEBUG #include <assert.h> #include "memory.h" #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define ROD_PRICE_LENGTH 10 TYPE rod_price[ROD_PRICE_LENGTH] = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30}; /** * @brief get max rod price by cutting rod by top-down with memoization method. * * @param rod_price rod price list for each type of different length * @param rod_length given rod length * * @return max rod price */ TYPE rod_cut_by_memoized_aux(TYPE* rod_price, TYPE rod_length, TYPE* memoized_cache); TYPE rod_cut_by_memoized(TYPE* rod_price, TYPE rod_length) { // initialize cache first TYPE memoized_cache[ROD_PRICE_LENGTH + 1]; TYPE i; for (i = 0; i <= rod_length; i++) memoized_cache[i] = INT_MIN; // get result with cache return rod_cut_by_memoized_aux(rod_price, rod_length, memoized_cache); } TYPE rod_cut_by_memoized_aux(TYPE* rod_price, TYPE rod_length, TYPE* memoized_cache) { DEBUG_PRINT_STRING("In recursion now.\n"); DEBUG_PRINT_VALUE("%d", rod_length); if (memoized_cache[rod_length] > INT_MIN) { DEBUG_PRINT_STRING("get value from cache.\n"); DEBUG_PRINT_VALUE("%d", memoized_cache[rod_length]); DEBUG_PRINT_VALUE("%d", rod_length); DEBUG_PRINT_STRING("Out recursion now.\n"); return memoized_cache[rod_length]; } if (rod_length == 0) { memoized_cache[rod_length] = 0; DEBUG_PRINT_STRING("set value to cache.\n"); DEBUG_PRINT_VALUE("%d", memoized_cache[rod_length]); DEBUG_PRINT_VALUE("%d", rod_length); DEBUG_PRINT_STRING("Out recursion now.\n"); return 0; } TYPE i; TYPE max = INT_MIN; for (i = 0; i < rod_length; i++) { TYPE temp = rod_cut_by_memoized(rod_price, rod_length - i - 1) + rod_price[i]; if (temp > max) max = temp; } memoized_cache[rod_length] = max; DEBUG_PRINT_STRING("set value to cache.\n"); DEBUG_PRINT_VALUE("%d", memoized_cache[rod_length]); DEBUG_PRINT_VALUE("%d", rod_length); DEBUG_PRINT_STRING("Out recursion now.\n"); return max; } int main(void) { // set rod length TYPE rod_length = 8; assert(rod_length >= 1 && rod_length <= ROD_PRICE_LENGTH); // get maximum rod price by cutting rod TYPE max_total_price = rod_cut_by_memoized(rod_price, rod_length); // output result printf("max total price = %d\n", max_total_price); return EXIT_SUCCESS; }
版本四:动态规划——自底向上法bottom-up method
原理:这里要求求解一个子问题时,它的解只依赖于“更小的”子问题的解。这种情况下我们可以将子问题按规模由小到大顺序求解。
时间复杂度:T(n)=O(n^2)
体会:这里最主要思考的是,对于一个子问题,当规模增大时,如何通过这些子问题的解获得规模增大后的子问题的解,可以先采用带备忘的自顶向下的方法去思考问题,再逆过来进行自底向上的实现。
C语言实现核心代码:
/** * @file cut_rod_by_bottom_up.c * @brief get maximum price of cut rod by dynamic programming with * bottom-up method. * @author chenxilinsidney * @version 1.0 * @date 2015-01-04 */ #include <stdio.h> #include <stdlib.h> #include <limits.h> #define NDEBUG #include <assert.h> #include "memory.h" #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define ROD_PRICE_LENGTH 10 TYPE rod_price[ROD_PRICE_LENGTH] = {1, 5, 8, 9, 10, 17, 17, 20, 24, 30}; /** * @brief get max rod price by cutting rod by bottom-up method. * * @param rod_price rod price list for each type of different length * @param rod_length given rod length * * @return max rod price */ TYPE rod_cut_by_memoized(TYPE* rod_price, TYPE rod_length) { TYPE memoized_cache[ROD_PRICE_LENGTH + 1] = {0}; TYPE i, j; for (i = 1; i <= rod_length; i++) { memoized_cache[i] = INT_MIN; for (j = 0; j <= i - 1; j++) { TYPE temp = memoized_cache[i - j - 1] + rod_price[j]; if (memoized_cache[i] < temp) memoized_cache[i] = temp; } } return memoized_cache[rod_length]; } int main(void) { // set rod length TYPE rod_length = 8; assert(rod_length >= 1 && rod_length <= ROD_PRICE_LENGTH); // get maximum rod price by cutting rod TYPE max_total_price = rod_cut_by_memoized(rod_price, rod_length); // output result printf("max total price = %d\n", max_total_price); return EXIT_SUCCESS; }
体会:分析自顶向下法和自底向上法,想到一个共同的好处,由于子问题的解都被记录在一个空间中,那么当程序下一次再次求解这个问题(但可能不是同样的n值)时,只要这个空间对新的问题仍是可见的,那么空间中的这些已经求得的解将会被重复再利用,加快了第二次调用此算法的运算时间;自顶向下法的缺点是频繁的递归调用也会消耗运算时间;另外,《算法导论》中说到,某些特殊情况下,自顶向下的方法并未真正递归地考察所有可能的子问题(这句话待实践思考)。
此问题源码均开源(在UBUNTU + GCC4.8.2下编译并测试通过),下载或查看地址:
https://github.com/chenxilinsidney/funnycprogram/tree/master/introduction_to_algorithms/cut_rod
三、斐波那契数列问题
问题描述:求解斐波那契数列中第n个值(从零开始索引)
斐波那契数列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
例子:
输入n = 8时,输出34。
分析:寻找规律:斐波那契数列中第n项的值为前两项的和(n>=2)。
版本一 递归法
原理:根据斐波那契数列的定义求解,递归地把规模为n的问题,分解为规模为n-1和n-2两个子问题,并进行合并(求和)。这里采用了分治的思想。
时间复杂度:根据递归方程:T(n) = T(n-1)+T(n-2) + 1,n >= 2; T(n) = 1,n = 0,1; 利用递归方程结合等比数列公式可以证明T(n)=2^n - 1(代入法),求得时间复杂度T(n) = O(2^n)。
体会:这种递归方法,出现了相同子问题重复求解的情况。在分析递归函数时,可以采用递归树法,通过画出递归树来分析递归情况;也可以在算法实现时加入调式代码,输出递归函数的状态,辅助分析递归函数。
C语言实现核心代码:
/** * @file get_fibonacci_polynomial_by_recursion.c * @brief get the nth member of the fibonacci sequence by recursion. * the fibonacci sequence index begin from 0th: * 1, 1, 2, 3, 5, 8, 13, 21, 34.... * @author chenxilinsidney * @version 1.0 * @date 2014-12-31 */ #include <stdlib.h> #include <stdio.h> #define NDEBUG #include <assert.h> #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define NUM 8 TYPE fib(TYPE index) { assert(index >= 0); DEBUG_PRINT_STRING("In recursion now.\n"); DEBUG_PRINT_VALUE("%d", index); if(index == 0 || index == 1) { DEBUG_PRINT_STRING("get first and second index 0/1.\n"); DEBUG_PRINT_VALUE("%d", index); DEBUG_PRINT_STRING("Out recursion now.\n"); return 1; } else { TYPE temp = fib(index - 1) + fib(index - 2); DEBUG_PRINT_VALUE("%d", temp); DEBUG_PRINT_VALUE("%d", index); DEBUG_PRINT_STRING("Out recursion now.\n"); return temp; } } int main(void) { printf("index = %d\n", NUM); printf("fib = %d\n", fib(NUM)); return EXIT_SUCCESS; }
版本二 动态规划——带备忘的自顶向下法Top-down with memoization
原理:与问题二一致。
时间复杂度:T(n) = O(n),n个子问题各求解1次。
体会:对新开辟的空间的初始化很重要(需要用一个值来表示未知值)。
C语言实现核心代码:
/** * @file get_fibonacci_polynomial_by_dynamic_programming.c * @brief get the nth member of the fibonacci sequence by dynamic programming. * with top-down with memoization method. * the fibonacci sequence index begin from 0th: * 1, 1, 2, 3, 5, 8, 13, 21, 34.... * @author chenxilinsidney * @version 1.0 * @date 2014-12-31 */ #include <stdlib.h> #include <stdio.h> #include <limits.h> #define NDEBUG #include <assert.h> #include "memory.h" #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define NUM 9 /** * @brief get nth number of the fibonacci sequence. * * @param index index of the number. begin from 0. * * @return the nth number */ TYPE fib_aux(TYPE index, TYPE* cache); TYPE fib(TYPE index) { /// initialize cache first TYPE* cache = SMALLOC(NUM + 1, TYPE); TYPE i; for (i = 0; i <= NUM; i++) cache[i] = INT_MIN; /// get result with cache TYPE result = fib_aux(index, cache); SFREE(&cache); return result; } TYPE fib_aux(TYPE index, TYPE* cache) { assert(index >= 0); if (cache[index] > INT_MIN) { return cache[index]; } if (index == 0 || index == 1) { cache[index] = 1; } else { cache[index] = fib(index - 1) + fib(index - 2); } return cache[index]; } int main(void) { printf("index = %d\n", NUM); printf("fib = %d\n", fib(NUM)); return EXIT_SUCCESS; }
版本三
动态规划——自底向上法bottom-up method
原理:与问题二一致。
时间复杂度:T(n) = O(n),n个子问题各求解1次。
体会:首先,初始状态赋值很重要。其次,存储子问题时,如果可以,可以只存储需要用到的子问题的解,避免开辟更多空间储存每个子问题的解。针对此问题,这里只储存前两项的数列值。
C语言实现核心代码:
/** * @file get_fibonacci_polynomial_by_dynamic_programming.c * @brief get the nth member of the fibonacci sequence by dynamic programming. * with bottom-up method. * the fibonacci sequence index begin from 0th: * 1, 1, 2, 3, 5, 8, 13, 21, 34.... * @author chenxilinsidney * @version 1.0 * @date 2014-12-31 */ #include <stdlib.h> #include <stdio.h> // #define NDEBUG #include <assert.h> // #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define NUM 9 TYPE fib(TYPE index) { assert(index >= 0); TYPE cachs_prevous[2] = {1, 1}; TYPE i; if(index == 0 || index == 1) return 1; for(i = 2; i <= index; i++) { cachs_prevous[i & 1] = cachs_prevous[0] + cachs_prevous[1]; } return cachs_prevous[(--i) & 1]; } int main(void) { printf("index = %d\n", NUM); printf("fib = %d\n", fib(NUM)); return EXIT_SUCCESS; }
此问题源码均开源(在UBUNTU + GCC4.8.2下编译并测试通过),下载或查看地址:
版本四
矩阵法
注意:这里斐波那契数列从0值开始:
斐波那契数列: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
原理:可用数学归纳法证明:
这样原问题就转化为求解上述等式右边矩阵n-1次幂的问题。
我们可以利用分治的思想进行求解,这里A为2*2阶矩阵:
时间复杂度:求解矩阵n-1次幂的递归方程:T(n) = T([n / 2]) + 1,n > 1; T(n) = 1,n = 1,计算得时间复杂度T(n) = O(lgn)。
体会:换了一种思路把问题转化为2*2矩阵的n-1次幂求解问题,将时间复杂度减少到O(lgn),神奇!
C语言实现核心代码:
/** * @file get_fibonacci_polynomial_by_matrix.c * @brief get the nth member of the fibonacci sequence by dynamic programming. * with bottom-up method. * the fibonacci sequence index begin from 0th: * 0, 1, 1, 2, 3, 5, 8, 13, 21, 34.... * @author chenxilinsidney * @version 1.0 * @date 2014-12-31 */ #include <stdlib.h> #include <stdio.h> // #define NDEBUG #include <assert.h> // #define NDBG_PRINT #include "debug_print.h" typedef int TYPE; #define NUM 9 struct matrix_2by2 { TYPE m11; TYPE m12; TYPE m21; TYPE m22; }; struct matrix_2by2 matrix_multiply_2by2( struct matrix_2by2 matrix_a, struct matrix_2by2 matrix_b) { struct matrix_2by2 matrix_c; matrix_c.m11 = matrix_a.m11 * matrix_b.m11 + matrix_a.m12 * matrix_b.m21; matrix_c.m12 = matrix_a.m11 * matrix_b.m12 + matrix_a.m12 * matrix_b.m22; matrix_c.m21 = matrix_a.m21 * matrix_b.m11 + matrix_a.m22 * matrix_b.m21; matrix_c.m22 = matrix_a.m21 * matrix_b.m12 + matrix_a.m22 * matrix_b.m22; return matrix_c; } struct matrix_2by2 matrix_power_2by2(TYPE n) { assert(n > 0); struct matrix_2by2 matrix_source = {1, 1, 1, 0}; if (n == 1) { return matrix_source; } else { if ((n & 0x1) == 0) { struct matrix_2by2 matrix_temp = matrix_power_2by2((unsigned)n >> 1); struct matrix_2by2 matrix_return = matrix_multiply_2by2(matrix_temp, matrix_temp); return matrix_return; } else { struct matrix_2by2 matrix_temp = matrix_power_2by2((unsigned)(n - 1) >> 1); struct matrix_2by2 matrix_return = matrix_multiply_2by2(matrix_temp, matrix_temp); matrix_return = matrix_multiply_2by2(matrix_return, matrix_source); return matrix_return; } } } TYPE fib(TYPE index) { assert(index >= 0); if (index == 0) return 0; else if (index == 1) return 1; struct matrix_2by2 matrix_return = matrix_power_2by2(index - 1); return matrix_return.m11; } int main(void) { printf("index = %d\n", NUM); printf("fib = %d\n", fib(NUM)); return EXIT_SUCCESS; }