动态规划

前言

有些东西,你不鄙视它,别人就鄙视你。 --- BloodD

动态规划问题在很多牛B的IT公司面试中几乎总能碰到(例如hulu),搞不懂,不能忍。本篇博客记录相关的知识,应用和资源,持续更新。

背景介绍

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。
20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)
的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,
利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法 —— 动态规划。 ---百度百科

多阶段决策问题

请输入图片描述

为了找到由A至E的最短线路,可以将该问题分成A—B—C—D—E 4个阶段,在每个阶段都需要作出决策,即在A点需决策下一步到B1还是到B2或B3;同样,若到达第二阶段某个状态,比如B1 ,需决定走向C1还是C2 ;依次类推,可以看出:各个阶段的决策不同,由A至E的路线就不同,当从某个阶段的某个状态出发作出一个决策,则这个决策不仅影响到下一个阶段的距离,而且直接影响后面各阶段的行进线路。所以这类问题要求在各个阶段选择一个恰当的决策,使这些决策序列所决定的一条路线对应的总路程最短【2】。

动态规划

动态规划同分治算法一样,是通过组合子问题的解而解决整个问题的,动态规划中的子问题是不独立的,若采用分治算法会重复的求解公共的子子问题,而动态规划对每个子子问题只求解一次,将其结果保存在一张表中,避免重复的求解公共的子子问题。动态规划通常用于最优化问题,此类问题可能存在多种最优解。

如果问题是由交叠的子问题所构成,那么就可以用动态规划技术来解决它,一般来说,这样的子问题出现在对给定问题求解的递推关系中,这个递推关系包含了相同问题的更小子问题的解。动态规划法建议,与其对交叠子问题一次又一次的求解,不如把每个较小子问题只求解一次并把结果记录在表中(空间换时间),这样就可以从表中得到原始问题的解。

核心思想: 记录子问题的解,空间换时间,不重复求解,由交叠子问题从较小问题解逐步决策,构造较大问题的解。

思考

那么遇到问题如何用动态规划去解决呢?根据上面的分析我们可以按照下面的步骤去考虑:
1、构造问题所对应的过程。
2、思考过程的最后一个步骤,看看有哪些选择情况。
3、找到最后一步的子问题,确保符合“子问题重叠”,把子问题中不相同的地方设置为参数。
4、使得子问题符合“最优子结构”。
5、找到边界,考虑边界的各种处理方式。
6、确保满足“子问题独立”,一般而言,如果我们是在多个子问题中选择一个作为实施方案,而不会同时实施多个方案,那么子问题就是独立的。
7、考虑如何做备忘录。
8、分析所需时间是否满足要求。
9、写出转移方程式。

应用动态规划思想解决问题的例子

最长公共子串,最长公共子序列(Longest Common Subsequence, LCS),最长递增子序列, 字符串编辑距离(Longest Increase Subsequence,LIS)
Note: 1.字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列
Note:2.公共子串要求子序列是连续的


最长公共子序列:
str1: adbcef
str2: defb
lcs : def

分析:将问题转化为2个子问题,i.当两个字符串在各自位置上的字符相同时,最长公共子序列为各自同时回退一位处的最长公共子序列加1。ii.当两个字符串在各自位置上的字符不相同时,最长公共子序列保持为字符串各自分别回退一位处最长公共子序列的值。引入矩阵C[i,j]记录字符串各自位置之前的最长公共序列的长度。

转移方程式见下图:
请输入图片描述

矩阵填写过程如下示例图 :
请输入图片描述

代码:

using namespace std;
const unsigned int M = 256;
int C_matrix[M][M] = {0};

int max_num(int a, int b){
    return (a >= b) ? a : b;
}

int LCS(char *source, char *target){
    if (source == NULL || target == NULL)
        return -1;
    int len_src = strlen(source);
    int len_tgt = strlen(target);
    int i = 0, j = 0;

    for (i; i <= len_src; ++i)
    {
        C_matrix[i][0] = 0;
    }

    for (j; j <= len_tgt; ++j)
    {
        C_matrix[0][j] = 0;
    }

    for (i = 1; i <= len_src; ++i)
    {
        for (j = 1; j <= len_tgt; ++j)
        {
            if (source[i-1] == target[j-1])
            {
                C_matrix[i][j] = C_matrix[i - 1][j - 1] + 1;
            }
            else
            {
                C_matrix[i][j] = max_num(C_matrix[i - 1][j], C_matrix[i][j - 1]);           
            }
        }
    }

    cout << "LCS number is :" << C_matrix[len_src][len_tgt] << endl;
    return C_matrix[len_src][len_tgt];
}

最长公共子串
str1: adbcef
str2: defb
lcs : ef

分析:由于要求子串是连续的,填写矩阵时,用到策略不同,当前位置字符匹配则值为左上角位置值加1;反之,赋值为0,将原本连续的字符串截断,从下一个位置开始重新计算。

代码:

using namespace std;
const unsigned int M = 256;
int C_matrix[M][M] = { 0 };

int max_num(int a, int b){
    return (a >= b) ? a : b;
}

int LCSeq(char *source, char *target){
    if (source == NULL || target == NULL)
        return -1;
    int len_src = strlen(source);
    int len_tgt = strlen(target);
    int i = 0, j = 0;
    int max_len = 0;

    for (i = 0; i < len_src; ++i)
    {
        for (j = 0; j < len_tgt; ++j)
        {
            if (source[i] == target[j])
            {
                if (i == 0 || j == 0)
                {
                    C_matrix[i][j] = 1;
                    max_len = max_num(C_matrix[i][j], max_len);
                }
                else
                {
                    C_matrix[i][j] = C_matrix[i - 1][j - 1] + 1;
                    max_len = max_num(C_matrix[i][j], max_len);
                }
            }
            else
            {
                C_matrix[i][j] = 0;
            }
        }
    }

    cout << "LCSeq number is :" << max_len << endl;
    return max_len;
}

最长递增子序列
sequence: 1 4 2 6 7 3 8
result: 5(1 2 6 7 8)

分析:
1.可先对原始序列进行排序,转换成最长公共子序列,用动态规划求解;
2.在序列i位置处的最长递增子序列L[i]等于max{L[j]}+1,在满足 j<i 并且 L[j]<L[i]条件条件下;
3.还有一种O(nlogn)的解,见【4】

using namespace std;
const unsigned int M = 256;
unsigned int L[M] = { 1 };
int max_num(int a, int b){
return (a >= b) ? a : b;
}
int LIS(char *source){
    if (source == NULL)
        return -1;
    int len_src = strlen(source);
    int i = 0, j = 0;
    int max_len = 0;
    for (i = 0; i < len_src; ++i)
    {
        for (j = 0; j < i; ++j)
        {
            if (source[j] < source[i] && L[i]<L[j]+1)
            {
                L[i] = L[j] + 1;
                max_len = max_num(L[i],max_len);
            }
        }
    }
    cout << "LIS number is :" << max_len << endl;
    return max_len;
}

字符串编辑距离

[引用]
1. http://baike.baidu.com/view/28146.htm?fr=aladdin
2. http://dec3.jlu.edu.cn/webcourse/t000048/yun/ch5_01.htm
3. http://www.cnblogs.com/sdjl/articles/1274312.html
4. http://qiemengdao.iteye.com/blog/1660229

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值