动态规划之最长公共子序列问题(LCS)
最长公共子序列问题
设计一个函数输出两个字符串的最长公共子序列
例如:' abc '和' ace '的最长公共子序列为' ac '。
这个问题是分治法的经典问题,具体思路可以这样考虑
比较两个字符串的首位(str1[0], str2[0]),如果相等的话,那么结果则为 str1[0] + f(str1.substr(1), str2.substr(2));
如果首位不相等的话,则分为两种情况:
f(str1, str2.substr(1)) 第一个字符串不变和去掉一个字符的第二个字符串相比
f(str1.substr(1), str2) 第二个字符串不变和去掉第一个字符的第一个字符串相比
然后我们从这两种结果中挑选最长的返回
那么所有的比较都可以大致的用上面的两种情况来递归讨论,所以我们不难写出算法
function LCS(str1, str2) { if(!str1 || !str2) return ""; if(str1[0] == str2[0]) { return str1[0] + LCS(str1.substr(1), str2.substr(1)); } else { var s1 = LCS(str1, str2.substr(1)); var s2 = LCS(str1.substr(1), str2); if(s1.length > s2.length) { return s1; } else { return s2; } } }
当然用分治法是可以解决这个问题,但我们不难发现一次次的递归不仅时间复杂度高,并且其中存在非常多重复的运算,我们是不是可以把已经算过的结果保存在一个数据结构中,下次递归前先看看这个数据结构中是否有我们需要的值,有的话就不用进行重复的递归了
所以出于以上思路,我们可以把这个算法优化一下
function LCS(str1, str2) { var total = []; //使用一个全局变量来存储每次的结果 function _LCS(str1, str2) { if (!str1 || !str2) return ""; //判断如果这次要递归的两个字符串已经在total中出现了,那么直接返回太多结果 total.forEach(item => { if(item.str1 == str1 && item.str2 == str2) { return item.result; } }) var newResult; if (str1[0] == str2[0]) { newResult = str1[0] + _LCS(str1.substr(1), str2.substr(1)); } else { var s1 = _LCS(str1, str2.substr(1)); var s2 = _LCS(str1.substr(1), str2); if (s1.length > s2.length) { newResult = s1; } else { newResult = s2; } } // 每次运算都把结果保存在total中 total.push({ str1, str2, result: newResult }) return newResult; } return _LCS(str1, str2); }
这个算法中,我们牺牲了空间复杂度来换取了时间复杂度,减少了不必要的重复运算,其实这就是动态规划和分治法的一个差异,他们思想是相同的,但是动态规划在分治的基础上加了一层存储,降低了时间复杂度。