[算法]动态规划(Dynamic programming)

 转载请注明原创:http://www.cnblogs.com/StartoverX/p/4603173.html

   Dynamic Programming的Programming指的不是程序而是一种表格法。我们知道,分治法将问题划分为互不相交的子问题,递归的求解子问题,再将他们组合起来,求出原问题的解。而动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题,在这种情况下,动态规划方法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算。

  动态规划方法通常用来求解最优化问题(optimization problem),也就是找到问题的一个最优解。我们通常按以下四个步骤来设计一个动态规划算法:

    1.刻画一个最优解的结构特征。

    2.递归地定义最优解的值。

    3.计算最优解的值,通常采用自底向上的方法。  

    4.利用计算出的信息构造一个最优解。

  第一步刻画一个最优解的结构特征。如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质。当某问题具有最优子结构性质时,如果这些子问题有重叠的情况,我们就应考虑动态规划方法(具有最优子结构性质也可能意味着适合应用贪心策略)。

  第二步中,由于问题具有子问题,所以我们可以使用递归的方式求解,但是由于反复的求解相同的子问题,朴素的递归算法非常的低效。

  第三步,我们开始真正实现动态规划算法,动态规划算法有两种等价的实现方法:

    1.带备忘的自顶向下法(top-down with memoization):在第二步递归方法的过程中,保存每一个子问题的解,当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值,从而节省了计算时间。否则,按通常方式计算此解后保存。我们称这个递归过程是带备忘的(memoized),因为它“记住”了之前已经计算出的结果。

    2.自底向上法(bottom-up method):将子问题按规模排序,按由小至大的顺序进行求解,当求解某个子问题时,它所依赖的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们第一次遇见它时,它的所有前提子问题都已求解完成。

  第四步中构造最优解,要在第三步中保存每次求解时维护一些额外的信息保存做出的选择,第四步再将每次的选择得出,就可以构造出一个最优解。

  举例:Leetcode word-break:

    Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

    For example, given

      s = "leetcode",
      dict = ["leet", "code"].     Return true because "leetcode" can be segmented as "leet code".

  分析:输入是一个unordered-set<string>& wordDict,一个string& s,输出是一个bool值,判断string s是否能被分解成wordDict中的string值。

  分析该问题我们发现,如果s能够被分解成wordDict中的单词组合,那么,对于其分割答案中的一次分割s->s1,s2,将s分割成s1和s2,s1和s2必也能分割成wordDict中的单词。也就是对s的分割包含了对s1和s2的分割,该问题的最优解包含了其子问题的最优解,该问题具有最优子结构性质。

  现在我们可以尝试通过递归的方法求解该问题,我们对于s中每两个字符间的每一次分割遍历s,如果分割得到的s1和s2都能被分解为wordDict中的单词,我们就说s能够被分解为wordDict中的单词,如果s1和s2不能被分解为wordDict中的单词,则继续遍历s。如果分割点直到s中的最后一个字符都没能得到两个都能被分解的s1和s2,则我们说s不能分解为s1和s2中的单词。

#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
#include <algorithm>
using namespace std; class Solution
{
public:
bool wordBreak(string s,unordered_set<string>& wordDict)
{
if(wordDict.size() == ) //边界条件1
{
return false;
}
if(s.size() == ) //边界条件2
{
return false;
}
if(find(wordDict.begin(),wordDict.end(),s) != wordDict.end()) //递归退出条件
{
return true;
}
     int size = s.size();
if(size == ) //如果wordDict中没找到s,s又不能继续分割,return false
{
return false;
}
for(int i=;i<size;i++) //对s遍历每一个分割点
{
string s1 = s.substr(,i);
string s2 = s.substr(i,size);
if(wordBreak(s1,wordDict) && wordBreak(s2,wordDict))
{
return true;
}
}
return false;
}
};

  分析上述递归的方法,我们发现,对于每一个s,我们都要遍历它的每两个字符间的分割点,而s分割出来的s1,s2,又包含了同样的字符顺序,也就是,我们重复求解了很多次相通的字符串,所以,上面的递归解法非常的低效。由于我们判断出该题的子问题相互重叠,我们使用动态规划的方法。

  上面已经得到了一个递归解法,所以在动态规划解法中我们使用带备忘的自顶向下法对于此问题,我们要求的解是对于一个特定的string,求对应的bool值,所以我们使用一个全局map:map<string,bool> mp来保存每次的结果。在每次递归前检查是否已经取得了解,并在递归后在map中保存解。

#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
#include <algorithm>
#include <map>
using namespace std; class Solution
{
public:
map<string,bool> mp;
bool wordBreak(string s,unordered_set<string>& wordDict)
{
if(wordDict.size() == ) //边界条件1
{
return false;
}
if(s.size() == ) //边界条件2
{
return false;
}
if(find(wordDict.begin(),wordDict.end(),s) != wordDict.end()) //递归退出条件
{
return true;
}
int size = s.size();
if(size == ) //如果wordDict中没找到s,s又不能继续分割,return false
{
return false;
}
for(int i=;i<size;i++) //对s遍历每一个分割点
{
string s1 = s.substr(,i);
string s2 = s.substr(i,size);
bool flag1 = false;
bool flag2 = false;
bool is1 = false;
bool is2 = false;
if(mp.empty() == false && (mp.find(s1) != mp.end()))//如果已经得到了s1的解,直接从mp中取。
{
is1 = true;
flag1 = (*mp.find(s1)).second;
}
if(mp.empty() == false && (mp.find(s2) != mp.end()))//如果已经得到了s2的解,直接从mp中取。
{
is2 = true;
flag2 = (*mp.find(s2)).second;
}
if(is1 == false)//如果没有得到过s1的解,求解并保存在mp中。
{
flag1 = wordBreak(s1,wordDict);
mp[s1] = flag1;
}
if(is2 == false)//如果没有得到过s2的解,求解并保存在mp中。
{
flag2 = wordBreak(s2,wordDict);
mp[s2] = flag2;
}
if(flag1 && flag2)
{
return true;
}
}
return false;
}
};
上一篇:windows下安装和使用scrapy


下一篇:Dynamic Programming