动态规划总结之 LIS ,LCS

LISLongest Increasing subsequence

含义:一列数中(一般是无序才有研究意义),子序列是递增的(当然也有非下降子序列,递减子序列了,这个就是看子序列的元素间的关系了,>,<,=),要求长度最大的这样的子序列的长度是多少。

算法一:O(n*n)

1.思路:

我们把序列的每个元素都当成递增子序列的最后一个数last(即这个递增子序列的最大的数),用一个数组list[i]来保存把每个元素都当成递增子序列的最后一个数这样的递增子序列,所以,我们在初始化list[i]时,不是设置为0,而是1,因为至少有一个数,那就是它本身。然后,我们只要不断地比较它前面的数是否比last大,大就list[i]+1.

因为我们是要取最长的,还要不断地更新这个长度。

2.代码:

 int LIS(int *a, int len)//长度为lena数组的LIS

{

    int i,j,max;//max是存放最大长度,不断更新

    for (i = 0; i < len; i++)//初始化,最长递增子序列长度最小是1,即每个序列都是它们本身

    {

        list[i] = 1;

    }

    for (i = 1,max = 1; i < len; i++)//以数组第二个开始,这里我们存放数以数组下标为0开始存放

    {

       for(j = 0; j < i; j++)//前面的数依次判断,比较大小

       {

           if (a[j] < a[i] && liss[j] + 1 > liss[i])//前面的数比当前数小且保证长度也是比当前数的长度大

           {

              list[i] = list[j]+1;

            }

           if (liss[i] > max)//更新长度

           {

               max = liss[i];

           }

       }

 

    }

    return max;

}

3.拓展

如果我们要把这个LIS打印出来怎么办?我们再设置一个数组,用来表示当前数的前驱(表示的是它在这数组中是位置)

以下是参考别人的代码,我再补充一点我的理解

1. unsigned int LISS(const int array[], size_t length, int result[])  

2. {  

3.     unsigned int i, j, k, max;  

4.   

5.     //变长数组参数,C99新特性,用于记录当前各元素作为最大元素的最长递增序列长度  

6.     unsigned int liss[length];  

7.   

8.     //前驱元素数组,记录当前以该元素作为最大元素的递增序列中该元素的前驱节点,用于打印序列用  

9.     unsigned int pre[length];  

10.   

11.     for(i = 0; i < length; ++i)  

12.     {  

13.         liss[i] = 1;  

14.         pre[i] = i;  //初始化,当前位置就是所在数组的下标位置

15.     }  

16.   

17.     for(i = 1, max = 1, k = 0; i < length; ++i)  

18.     {  

19.         //找到以array[i]为最末元素的最长递增子序列  

20.         for(j = 0; j < i; ++j)  

21.         {  

22.             //如果要求非递减子序列只需将array[j] < array[i]改成<=,  

23.             //如果要求递减子序列只需改为>  

24.             if(array[j] < array[i] && liss[j] + 1> liss[i])  

25.             {  

26.                 liss[i] = liss[j] + 1;  

27.                 pre[i] = j; //记录下前驱的位置 

28.   

29.                 //得到当前最长递增子序列的长度,以及该子序列的最末元素的位置  

30.                 if(max < liss[i])  

31.                 {  

32.                     max = liss[i];  

33.                     k = i; //k表示找到的最长递增子序列的最大数的位置

34.  

35.                 }  

36.             }  

37.         }  

38.     }  

39.   

40.     //输出序列  

41.     i = max - 1;  //数组是从0开始存数,要长度-1

42.   

43.     while(pre[k] != k) //pre[k]表示位置在k的前驱的位置,易知必在k位置的前面,如果pre[k] == k,那么就回到了初始化的位置,也就要打印LIS的最前面一个元素了,从后往前赋值的

44.     {  

45.         result[i--] = array[k]; //先赋值,再移一位 

46.         k = pre[k];  

47.     }  

48.   

49.     result[i] = array[k];  //最前一位元素

50.   

51.     return max;  

52. }  

 

 

算法二:(O(n*logn)

思路:

1.几个符号含义

Maxv[i]——存放长度为i的最小子序列的最大元素(听起来很绕,就是,长度为i时,子序列第一个元素是尽可能小的,如同样是长度同样是3,     1223两个子序列我们要前者,Maxv用来存放这样的子序列的最后一个元素(该子序列最大的一个了)),1,2,33.Maxv可以是vector,array

a[i]——存需要处理的数。

2.详解

(1)第一个元素放入Maxv作为初始化

(2)从第二个元素到第n个元素依次处理

     如当前处理到第i个,若a[i]>Maxv中最大元素(最后一个) put a[i]->Maxv

     否则(a[i] <=Maxv中最大元素 ),用二分在Maxv 中找到第一个大于a[i]的数的位置pos,用a[i]替换它(Maxv[pos] = a[i])。(另一种说法是当前的 maxV[]数组中进行二分搜索,找到位置k,使得maxV[k] < a[i] < maxV[k+1],maxv[k+1]就是我们用二分找的数

(3) 不断处理(2,更新了Maxv的长度,最后Maxv中存放元素的个数就是所求。

3.证明:

为什么这样处理就可以了?我们需要证明2

(1)Maxv[i] < Maxv[i+1](就是长度小的子序列的最大元素 长度大的子序列的最大元素)

  证明:反证法

Maxv[i] > Maxv[i+1],即有Maxv[1],Maxv[2],......Maxv[i+1],Maxv[i],此时i>=i+2,矛盾,故所证成立。

(2)找到k使得maxV[k] < a[i] < maxV[k+1]

  证明只要maxv[k+1] = a[i]即可,也就是这样赋值不会对maxv[k+1]前面没有影响,也不会对maxv[K+2]以及后面的数没有影响。

证明:

Maxv[k+1]前面无变化,易知所证

maxv[K+2]以及后面的数没有影响。

反证法:若maxv[k+2]发生变化(只有maxv[K+2] < a[i]才会变)

有maxV[k+1] < maxV[k+2]<a[i],则二分找到的应该是k+2的位置,与找到为k位置矛盾,故所证成立。

4.代码

int LIS(int *a, int len)

{

    vector<int>mavx;

    mavx.push_back(a[1]);

    for (int i = 1; i <= len; i++)

    {

        if (a[i] > *mavx.rbegin())

        {

            mavx.push_back(a[i]);

        }

        else

        {

            *(lower_bound(mavx.begin(),mavx.end(),a[i])) = a[i];

        }

    }

    return mavx.size();

}

 

LCS(Longest common Subsequence 最长公共子序列)

例如:S1:a,b,d,s,a,d,f,d,w,e,r

      S2:b,a,d,f,,w,a,d,f

      LCS:b,d,f,w

1.思路:

S1S2分别拆成sub1+e1,sub2+e2.,其中,e1,e2分别是两个序列的最后一个元素

则我们可以通过分析得到以下式子:

LCS(s1, s2) =
 { max( LCS(sub1, s2), LCS(s1, sub2) ) , when e1 != e2
 { LCS(sub1, sub2) + e1                , when e1 == e2

也就是我们通过比较两个序列的最后一个元素是否相等,一步一步递归,应该很容易理解。

 

2.代码

int LCS(int sub1_length, int sub2_length)//求两个长度分别为sub1_length,sub2_length序    列的最长公共子序列的长度。

{

    for (int i = 1; i <= sub_length; i++)

    {

        for (int j = 1; j <= sub2_length; j++)

        {

            if (str[i] == str[j])//两个序列的最后一个元素是否相等,这里我们存放两个序列 是从数组下标为1存放的,如果是从0开始的,为str[i-1] == str[j-1].

            {

                a[i][j] = a[i-1][j-1]+1;

             }

            else

            {

                a[i][j] = max{a[i-1][j],a[i][j-1]};

            }

        }

    }

    return a[str1_length][str2_length];

}

以上代码,a[i][j]保存长度分别为i,j的两个序列的最长公共子序列的长度,所以,空间复杂度为str1_length*str2_length,一个二维数组。(排列组合,通过一步一步求出两个序列不同长度的LCS,进而求出我们要求的序列长度的LCS),时间复杂度为O(n*n).

事实上,我们还可以更加节省空间。从上面的代码,我们知道要求的a[i][j]只需要知道a[-1i][j-1](左上方)a[i-1][j](正上方),a[i][j-1](左方)的值即可,所以我们只要用2*str2_length(宽度)的空间即可(当序列的长度很大时,这个优化有很乐观),两行空间不断交错使用

 

所以,继而我们的代码可以改成:

int LCS(int sub1_length, int sub2_length)

{

    for (int i = 1; i <= sub_length; i++)

    {

        for (int j = 1; j <= sub2_length; j++)

        {

            if (str[i] == str[j])

            {

                a[1][j] = a[0][j-1]+1;

            }

            else

            {

                a[1][j] = max{a[0][j],a[1][j-1]};//我们只用a[0][j],a[1][j]两行的空间

            }

            a[0][j]  = a[1][j];//交替空间

        }

    }

    return a[1][sub2_length];//最后需要的值是两行空间的右下角

}

3.拓展

如果我们想把其中一个LCS打印出来怎么办?(LCS可能有多个哦,但是我们只要一个,全部打印还没有研究)很简单,只要再用一个二维数组pre[i][j]将所选择的是从哪个方向来的就可以了,再将其打印就好。

 

代码如下:

int LCS(int sub1_length, int sub2_length)

{

    for (int i = 1; i <= sub_length; i++)

    {

        for (int j = 1; j <= sub2_length; j++)

        {

            if (str[i] == str[j])

            {

                a[i][j] = a[i-1][j-1]+1;

                pre[i][j] = 左上方;

            }

            else if(a[i-1][j] < a[i][j-1])

            {

               a[i][j] = a[i][j-1];

               pre[i][j] = 左方

            }

            else

            {

                a[i][j] = a[i-1][j];

                pre[i][j] = 正上方;

            }

        }

    }

    return a[sub1_length][sub2_length];

}

1. void print_LCS(int i, int j)

2. {

3.     // 第一個或第二個 sequence 為空的的時候就可停止了

4.     if (i!=0 || j!=0return;

5.  

6.     if (prev[i][j] == 左上方)

7.     {

8.         print_LCS(i-1, j-1);

9.         cout << s1[i] << " ";  // 印出 LCS 的元素

10.     }

11.     else if (prev[i][j] == 正上方)

12.         print_LCS(i-1, j);

13.     else if (prev[i][j] == 左方)

14.         print_LCS(i, j-1);

15. }

LIS来求LCS(用于两个序列都各自都没有重复元素,这个方法比较局限,只是告诉我们另一种思路)

1.思路

先比较两个序列的长度,我们把段的设为str2,长的设为str1.

(1)用一个数组把str1中每个数所在位置记录下来。

(2)依次找到str2中与str1相同元素的位置,用LIS,若与str1的元素不同则跳过。

(3)代码(出自算法笔记)

 

1. int LCS(vector<int>& s1, vector<int>& s2)

2. {

3. //  if (s1.size() == 0 || s2.size() == 0) return 0;

4.  

5.     int p[100]; // 假設數值範圍為 0 ~ 99

6.     memset(p, -1sizeof(p));   // 初始化為 -1

7.     for (int i = 0; i < s1.size(); ++i)

8.         p[s1[i]] = i;

9.  

10.     /* LIS: Robinson-Schensted-Knuth Algorithm */

11.  

12.     vector<int> v;

13.     v.push_back(-1);    // 先放入一個數字,免得 v.back() 出錯

14.     

15.     for (int i = 0; i < s2.size(); ++i)

16.     {

17.         int n = p[s2[i]];       // 找出 s2[i] 出現在 s1 中的哪個位置

18.         if (n == -1continue;  // s2[i] 在 s1 中沒出現

19.         

20.         if (n > v.back())

21.             v.push_back(n);

22.         else

23.             *lower_bound(v.begin(), v.end(), n) = n;

24.     }

25.  

26.     return v.size() - 1;

27. }

 

 

 以上纯属个人见解,如有错误,欢迎指出~~

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值