动态规划&最长公共子序列

一个字符串的子串是字符串中连续的一个序列,而一个字符串的子序列是字符串中保持相对位置的字符序列,譬如,"adi"可以使字符串"abcdefghi"的子序列但不是子串。这也就决定了在解这两种"LCS"问题上的一些区别。
Longest-Common-Substring和Longest-Common-Subsequence是不一样的。

参考:
wiki-动态规划
何海涛微博

动态规划DP

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有[重叠子问题]和[最优子结构]性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其[记忆化]存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈[指数增长]时特别有用。

DP问题有几个典型应用:
解整数背包问题: 设有n件物品,每件价值记为Pi,每件体积记为Vi,用一个最大容积为Vmax的背包,求装入物品的最大价值。 用一个数组f[i,j]表示取i件商品填充一个容积为j的背包的最大价值,显然问题的解就是f[n,Vmax].
f[i,j]=

  f[i-1,j] {j<Vi}
  max{f[i-1,j],f[i,j-Vi]+Pi} {j>=Vi}
  0 {i=0 OR j=0}

对于特例01背包问题(即每件物品最多放1件,否则不放入)的问题,状态转移方程:

f[i,j]=

  f[i-1,j] {j<Vi}
  max{f[i-1,j],f[i-1,j-Vi]+Pi} {j>=Vi}
  0 {i=0 OR j=0}

WIKI上举的第一个例子是Fibonacci数列,普通的递归求法会重复计算很多次前面的值因而效率很低,所以我们可以从低位算起。这样就可以利用前面的值。

到目前为止我对DP的理解就是,每一步的结果都与上一步有关。

LCS(longest common subsequence)

我们有两个字符串,现在求两个字符串的最长公共子序列(注:这里的要求不包括字符必须连续)
例:abdhgf和dadchgm的LCS就是adhg
这种问题确实不好做,一般的思路解决太复杂了。如果用遍历的方式,不想等的时候往前移动,相等的话,看后面的是否相等,后面的一个也存在之前的情况。。

但是我们也可以假设我们已经有一段字符串满足相同的子序列了,那么我们关心当前的这一个就可以了。

我们假设两个字符串的长度为m,n;LCS的长度为k;并且假设LCS里面的所有字符都是满足条件的
并且假设前k个都满足情况了,我们讨论第k个:

设Xm={x0,x1,…xm-1}和Yn={y0,y1,…,yn-1}为两个字符串,而Zk={z0,z1,…zk-1}是它们的LCS,则:

  1. 如果xm-1=yn-1,那么zk-1=xm-1=yn-1,并且Zk-1是Xm-1和Yn-1的LCS;
  2. 如果xm-1≠yn-1,那么当zk-1≠xm-1时Z是Xm-1和Y的LCS;
  3. 如果xm-1≠yn-1,那么当zk-1≠yn-1时Z是Yn-1和X的LCS

注:这个关系大家多想一想,是最重要的部分。
如果觉得上面的文字有干扰的话,可以自己去理解。

上面的关系式实际上是逆推,即我们假设已经有了LCS,我们去找LCS在两个字符串里面的位置。

  • 我觉得很重要的一点就是,我们只关心这一步(和上一步的关系),至于上一步也不满足的话,那就是递归的事情了。这样想能简化思路!

**性质大家可以反证法证明一下,如果不能理解,可以举个例子试一下,LCS里面的字符肯定会出现在x,y里面,如果x,y的最后面是无关的字符,后面的两个条件就可以逐步把无关的删除掉;
其实后面两个条件就是去掉无关字符的过程,讨论的都是当x!=y的情况。
**

由上面的三种情况:

我们可以得出如下的思路:求两字符串Xm={x0, x1,…xm-1}和Yn={y0,y1,…,yn-1}的LCS,

  • 如果xm-1=yn-1, 那么只需求得Xm-1和Yn-1的LCS, 并在其后添加xm-1( yn-1) 即可;
  • 如果xm-1≠yn-1, 我们分别求得Xm-1和Y的LCS和Yn-1和X的LCS,并且这两个LCS中较长的一个为X和Y的LCS

这就是DP的特点吧,每一步的情况都有两种(多种),你看着办吧。

如果我们记字符串Xi和Yj的LCS的长度为c[i,j],我们可以递归地求c[i,j]:

  • 0, if i<0 or j<0

  • c[i-1,j-1]+1 ,if i,j>=0 and xi=xj

  • max(c[i,j-1],c[i-1,j] if i,j>=0 and xi≠xj

根据这个思路,我们创建一个矩阵lcs_length来记录对应的i,j的值,之所以这样是为了避免类似于Fibonacci里面的重复求值的问题,以及方便输出。
在下面的代码里面,还创建了一个lcs_dir的矩阵,这个也是为了保存每一次值的来源,方便我们打印的时候知道取哪个值。

  • 这个程序的主干部分是这样的:
    矩阵的横列是str2,竖列是str1
    结合上面的实例,我们得到的矩阵是这样的:

str| a |b|d|h|g|f
----|------|----|---|----|
d | 0|0|1|0|0|0
a | 1|1 |1 |1 |1 | 1
d |1| 1|2 |2 |2 |2
c|0| 1| 2|2 |2 |2
h|0| 1|2 |3 | 3|3
g| 0| 1| 2| 3| 4|4

下面是代码:

//c[i,k]=
// 0, if i<0 or j<0
// c[i - 1, j - 1] + 1, if i, j >= 0 and xi = xj
// max(c[i, j - 1], c[i - 1, j] if i, j >= 0 and xi≠xj
#include <iostream>

enum dir {kinit=0,kup,kleftup,kleft};//c[i,j] comes from 3 directions

//we have a matrix holding value,another holding the direction
int lcs(char* str1,char* str2)
{
    if (!str1 || !str2)
        return;
    int len1 = strlen(str1);
    int len2 = strlen(str2);
    if (!len1 || !len2)
        return 0;

    unsigned int i, j;
    int** lcs_len = (int**)(new int[len1]);
    for (i = 0; i < len1; i++)
        lcs_len[i] = (int*)new int[len2];
    for (i = 0; i < len1; i++)
        for (j = 0; j < len2; j++)
            lcs_len[i][j] = 0;

    int** lcs_dir = (int**)(new int[len1]);
    for (i = 0; i < len1; i++)
        lcs_dir[i] = (int*)new int[len2];
    for (i = 0; i < len1; i++)
        for (j = 0; j < len2; j++)
            lcs_dir[i][j] =kinit ;

    //core: detect every unit
    for (i = 0; i < len1; i++)
        for (j = 0; j < len2; j++)
        {
            if (i == 0 || j == 0)//the begin of common string
            {
                if (str1[i] == str2[j])
                {
                    lcs_len[i][j] = 1;
                    lcs_dir[i][j] = kleftup;
                }
                else
                    lcs_len[i][j] = 0;

            }
            else if (str1[i] == str2[j])
            {
                lcs_len[i][j] = lcs_len[i - 1][j - 1] + 1;//case 1
                lcs_dir[i][j] = kleftup;
            }
            else if (lcs_len[i - 1][j] > lcs_len[i][j - 1])
            {
                lcs_len[i][j] = lcs_len[i - 1][j] ;
                lcs_dir[i][j] = kup;
            }
            else
            {
                lcs_len[i][j] = lcs_len[i][j-1] ;
                lcs_dir[i][j] = kleft;
            }
        }
    return lcs_len[len1 - 1][len2 - 1];
}

然后我们根据得到的矩阵打印:
只需要打印方向矩阵里面,方向标识为leftup的字符,其它的根据方向标识来移动。
既然这样的话,我们可以用递归的方式打印,简化代码量:

//only lcs_dir=kleftup are to be printed
void print_path(int**lcs_dir, char* str1, char* str2, size_t row, size_t col)
{
    if (!str1 || !str2)
        return;
    size_t len1 = strlen(str1);
    size_t len2 = strlen(str2);

    if (len1 == 0 || len2 == 0 || !(row < len1&&col < len2))
        return;

    if (lcs_dir[row][col] == kleftup)
    {
        if (row > 0 && col > 0)
            print_path(lcs_dir, str1, str2, row - 1, col - 1);
        std::cout << str1[row];
    }
    if (lcs_dir[row][col]==kleft)
        print_path(lcs_dir, str1, str2, row , col - 1);
    if (lcs_dir[row][col] == kup)
        print_path(lcs_dir, str1, str2, row-1, col );
}

测试了一下没问题的。


总结一下吧,关于动态规划这个概念大家不要太纠结,其实重心在于如何找出规律!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容