缩略图

利用C 语言递归算法解决动态规划问题的探索

作者

黄辰臣

武汉晴川学院 湖北省武汉市 430204

1. 引言

计算机领域内有一个解决最优化问题的算法方法叫做动态规划 (DynamicProgramming,DP),动态规划算法的原理是将一个复杂的问题分成若干个简单的问题来求解,从求解出来的子问题得出原问题的一个最优解。而递归算法就是以自己的算法调用自己的算法来描述问题的一种算法。C 语言是有效灵活的算法语言,为递归算法、动态规划的实现提供了环境。研究C 语言递归算法来解决动态规划问题能够很好地体会算法的精髓,提高算法设计和编程的能力,对于解决各种复杂的最优化问题意义重大。

2. 递归算法与动态规划基础概念

2.1 递归算法

递归算法是指函数定义中调用函数自身的函数 。一个递归函数通常由递归终止条件和递归调用两部分构成。递归终止条件是指防止函数无限递归的条件,是递归函数能够结束递归调用的必要条件;递归调用是指函数将原问题分解为新的子问题,直到满足递归终止条件。例如,计算阶乘的递归函数:

int factorial(int n) {

if (n==0 ) {

return 1;

}

return n * factorial(n - 1); }

在上述代码中, n==0 是递归终止条件,n * factorial(n - 1) 是递归调用,通过不断将问题规模减小,最终计算出阶乘结果。

2.2 动态规划

动态规划的核心问题是如何解决重叠子问题和最优子结构性质问题〔2〕。其中最优子结构性质是指问题的最优解由构成该问题的子问题的最优解组成,而重叠子问题是指在问题的解法中会出现大量相同的子问题。动态规划把子问题的解(通常以数组等方式)保存下来,以防止重复的求解,从而降低算法的运行时间。动态规划常用的算法包括两种 :自顶向下的备忘录法(top-down approach with memoization)和自底向上的迭代法。自顶向下的备忘录法和递归算法密切相关。

2.3 递归算法与动态规划的联系与区别

递归算法与动态规划有相同之处,都是将原问题转换为子问题进行解决〔3〕。递归算法侧重于通过函数的调用实现对问题分解过程的描述,而动态规划则考虑如何通过已解决的子问题描述的解去避免大量重复计算,达到优化原问题的目的。通过递归算法实现动态规划(备忘录方法),对已解决的子问题描述的解进行备忘,可以从一定程度避免递归算法中的重复解法。

3. 利用C 语言递归算法解决经典动态规划问题

3.1 斐波那契数列

定义斐波那契数列: F(0)=0 , F(1)=1 , F(n)=F(n-1)+F(n-2) ( )用递归算法直接编写,有很多冗余,其算法的时间复杂度是指数级( )。采用直接递归方式的代码如下:

int fibonacci(int n) {

if (n==0) ) {

return 0;

}

if (n==1) ) {

return 1;

}

return fibonacci(n - 1) + fibonacci(n - 2); }

要避免重复计算,可以用备忘录的方法,用一个数组记录已经计算过的斐波那契数,将时间复杂度降低为 O(n) ,其代码如下:

#define MAX_SIZE 1000

int memo[MAX_SIZE];

int fibonacci_memo(int n) {

if (memo[n] != -1) {

return memo[n];

}

if (n==0) ) {

memo[n] =0 ;

} else if (n==1 ) {

memo[n] μ=1σ ;

} else {

memo[n] Σ=Σ fibonacci_memo(n - 1) + fibonacci_memo(n - 2); }

return memo[n];

}

在上述代码中,memo 数组用于存储已经计算好的斐波那契数。初始化赋值为 -1表示没有计算过,在计算之前先判断 memo[n] 是否已经有值,有值,则直接返回,避免重复计算。

3.2 背包问题

背包问题:假设有 Πn 个物品,每个物品的重量为:w_i,价值为:v_i,背包容量为:

W,如何在背包中装入物品使得背包中物品的总价值最大。背包问题采用递归算法和备忘录法,定义递归函数为:knapsack,递归函数中参数包括物品数量 n,背包容量 W,重量数组 weights,价值数组 values,同时使用二维数组 memo 记录子问题的解。代码如下:

#define N 100 #define W_MAX 1000 int memo[N][W_MAX];

int knapsack(int n, int W, int weights[], int values[]) {

if n==0|W==0 ) {

return 0;

}

if (memo[n][W] != -1) {

return memo[n][W];

}

if (weights[n - 1] > W) {

memo[n][W] Σ=Σ knapsack(n - 1, W, weights, values);

} else {

memo[n][W] Σ=Σ (values[n - 1] + knapsack(n - 1, W - weights[n - 1], weights, values)) > knapsack(n - 1, W, weights, values) ?

(values[n - 1] + knapsack(n - 1, W - weights[n - 1], weights, values)) :

knapsack(n - 1, W, weights, values);

}

return memo[n][W];

}

此段代码中将递归的终止条件设为物品的数目为 0 或者背包容量为 0 若物品的重量超过背包的容量则不将物品装进背包,反之则将装进物品和不装物品时的价值比较,取较大值为该子问题的解,存入memo 数组。

3.3 最长公共子序列问题

最长公共子序列(LongestCommonSubsequence,简称 LCS),对于给定的两个序列,找到这两个序列的最长的公共的子序列的长度。采用递归法 + 备忘录法求解,定义递归函数lcs,参数:两个序列seq1、seq2 及长度:m、n ;使用二维数组memo 存储子问题的解。代码如下:

#define M 100

#define N 100

int memo[M][N];

int lcs(char seq1[], int m, char seq2[], int n) {

if (m == 0 || n == 0) {

return 0;

}

if (memo[m][n] != -1) {

return memo[m][n];

}

if (seq1[m - 1] == seq2[n - 1]) {

memo[m][n] = 1 + lcs(seq1, m - 1, seq2, n - 1);

} else {

memo[m][n] = lcs(seq1, m, seq2, n - 1) > lcs(seq1, m - 1, seq2, n) ?

lcs(seq1, m, seq2, n - 1) : lcs(seq1, m - 1, seq2, n);

}

return memo[m][n];

}

上面的程序中递归停止条件是其中一个序列长度为 0 在两个序列上的当前字符长度若相同,则最长公共子序列的长度为1 加上去掉当前字符后的子序列的最长公共子序列长度,否则取去掉一个序列上的当前字符后的两个子问题中的较大值作为该子问题的解存入memo 数组中。

4. 总结与展望

本文在理论分析与C 语言代码实践的基础上,进一步探讨了采用递归算法实现动态规划问题求解的方法,并通过实现递归算法与备忘录法相结合的方法,达到了有效地解决动态规划问题中子问题重叠性的目的,优化了算法效率。通过采用这种方法来解决斐波那契数列问题、背包问题、最长公共子序列等典型动态规划问题,展示此方法的实现思路。但递归算法在解决动态规划问题也有缺陷,递归深度过大时会出现栈溢出等问题,在后续工作中可以继续研究更加平衡递归算法的简洁性和效率,寻找更多更好解决动态规划的递归算法,并将其运用在更多复杂的工作中。

参考文献

[1] 王英 .C 语言中循环转递归函数策略研究 [J]. 科学技术创新 ,2025,(15):71-74.

[2] 陈艳 , 文晓棠 . 蛮力法 , 分治法和动态规划法求解最大子数组问题的思考 [J].现代计算机 , 2023(18):24-29.

[3] 左正康 , 孙欢 , 王昌晶 , 等 . 命令式动态规划类算法程序推导及机械化验证 [J].软件学报 , 2024, 000(9):24.