当有人让你用递归算法求解斐波那契数列以及字符串的编辑距离时,所设的陷阱都是一致的(递归调用时的重复计算),解决方案也是一致的(引入备忘录概念)。观察树形的层级调用关系,我们可以发现动态规划隐式地嵌入了一种剪枝机制。
动态规划版求解菲波那切数列
关于朴素递归求解菲波那切数列存在的重复计算的说明:
红色部分均是重复计算项,当递归调用的层次更多时,重复计算的问题更为严重。
unsigned __int64 memo[1000];
unsigned __int64 fib(size_t n)
{
if (n<=1)
return n;
if (memo[n])
return memo[n];
return memo[n] = fib(n-1) + fib(n-2);
// 赋值运算(=)的也存在返回值,返回值是`=`号左侧的值
// 对于第一次计算得到的值进行记录备份,
// 下次如果想得到该值,先进性判断是否已经计算过了
// 如果是,直接返回,见第二个if
}
动态规划版求解字符串编辑距离
我们来看朴素递归来求解字符串编辑距离时可能出现的重复计算的问题(5, 5:表示的是,源字符串和目标字符串各自的长度为5):
现在我们应用动态规划的思想对朴素递归算法进行改造,所谓动态规划,其核心有二:
状态的概念
为递归接口增加状态标识参数 i 和
j :int editDist(char* src, char* dst);
改造为(引入状态的概念):
int editDist(char* src, char* dst, int i, int j);
备忘录概念
通过定义相关的结构体实现
typedef struct tagMemoRecord { int dist; int refCount; }MEMO_RECORD; // 作为全局变量 vector<vector<MEMO_RECORD>> memo(100, vector<MEMO_RECORD>(100));
int editDist(char* src, char* dst, int i, int j)
{
if (memo[i][j].refCount)
{
++memo[i][j].refCount;
return memo[i][j].dist;
}
int dist = 0;
if (strlen(src+i) == 0)
{
dist = strlen(dst+j);
}
else if(strlen(dst+j) == 0)
{
dist = strlen(src+i);
}
else
{
if (src[i] == dst[j])
{
dist = editDist(src, dst, i+1, j+1);
}
else
{
int editIns = editDist(src, dst, i, j+1) + 1;
int editDel = editDist(src, dst, i+1, j) + 1;
int editRep = editDist(src, dst, i+1, j+1) + 1;
dist = std::min({editIns, editDel, editRep});
}
}
memo[i][j].refCount = 1;
return memo[i][j].dist = dist;
}