LCS,LIS等模板

42 篇文章 1 订阅

咳咳,有一部分是根据CillyB总结的

1.最大子序和

最大子序列和一定是连续的,如果不连续,就没有意义了,因为我们只需要扫描一遍数组,输出其中所有正整数即可,他们的和一定是最大
的。

很容易理解时间界O(N) 是正确的,但是要是弄明白为什么正确就比较费力了。其实这个是算法二的一个改进。分析的时候也是i代表当前序列的起点,j代表当前序列的终点。
如果我们不需要知道最佳子序列的位置,那么i就可以优化掉。


    重点的一个思想是:如果a[i]是负数那么它不可能代表最有序列的起点,因为任何包含a[i]的作为起点的子序列都可以通过用a[i+1]作为起点来改进。类似的有,任何的负的子序列
不可能是最优子序列的前缀。例如说,循环中我们检测到从a[i]到a[j]的子序列是负数,那么我们就可以推进i。关键的结论是我们不仅可以把i推进到i+1,而且我们实际可以把它一直
推进到j+1。


    举例来说,令p是i+1到j之间的任何一个下标,由于前面假设了a[i]+…+a[j]是负数,则开始于下标p的任意子序列都不会大于在下标i并且包含从a[i]到a[p-1]的子序列对应的子序列(
j是使得从下标i开始成为负数的第一个下标)。因此,把i推进到j+1是安全的,不会错过最优解。注意的是:虽然,如果有以a[j]结尾的某序列和是负数就表明了这个序列中的任何一个数
不可能是与a[j]后面的数形成的最大子序列的开头,但是并不表明a[j]前面的某个序列就不是最大序列,也就是说不能确定最大子序列在a[j]前还是a[j]后,即最大子序列位置不能求出。
但是能确保maxSum的值是当前最大的子序列和。这个算法还有一个有点就是,它只对数据进行一次扫描,一旦a[j]被读入处理就不需要再记忆。它是一个联机算法。


 
联机算法:在任意时刻算法都能够对它已读入的数据给出当前数据的解。 
 


常量空间线性时间的联机算法几乎是完美的算法。

#include <iostream>
#include <algorithm>
using namespace std;
int s ,S , e , maxsum , cursum , len;
int maxsubsum (int a[])
{
    S = 0;
    maxsum  = 0;
    cursum = 0;
    for(int i = 0 ; i < len ; i++)
    {
        cursum += a[i];
        if(cursum < 0)   {cursum = 0;  S = i + 1;}      //  如果小于0  起始点就在他的后面一位
        if(maxsum < cursum)  {maxsum = cursum;  s = S ; e = i;}   //  s e用来记录起始点与末尾点

    }
    return maxsum;
}
int main()
{
    int a[10];
    cin >> len ;
    for(int i = 0 ; i < len ; i++)
        cin >> a[i];
    int k  = maxsubsum(a);
    cout << k << endl ;
    cout << s << ' ' << e << endl ;
    return 0;
}

2.最长公共子序列(可不连续)
//最长公共子序列(可不连续)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int dp[105][105];  //记录当前字母“前面”的最长子序列的长度
char a[100], b[100];
int path[150];
int main()
{
    while(cin >> a >> b)
    {
        int len1 = strlen(a);
        int len2 = strlen(b);
        for(int i = 1; i <= len1; i++)  //i,j从一开始
            for(int j = 1; j <= len2; j++)
        {
            if(a[i-1] == b[j-1])        //前一个相同,当前的就是前面dp+1;
                dp[i][j] = dp[i-1][j-1] + 1;
            else
                dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
        }
        cout << dp[len1][len2] << endl;
        //输出路径,只会输其中一条;从a里面输出,可以在加上从b里面的
        int i = len1, j = len2, k = dp[len1][len2];  //k代表最长的数量;ij都是从后往前找;
        while(i > 0)
        {
            if(dp[i][j] == dp[i-1][j]) i--;  //看a当前字母与前一个字母的dp是否相同,相同说明i-1那个字母
            else        //并不是最长子序列中的一个,不相同说明是其中的一个
            {
                path[k--] = i - 1;   //然后用一个path记录i-1,即属于最长子序列的字母的位置
                i--;  //可以想成,字符串0-i(前i个),跟b的dp相比,判断每段字符最后一个字母是否属于子序列
            }
        }
        for(int i = 1; i <= dp[len1][len2]; i++)  //从前往后输出
            {
                printf("%c",a[path[i]]);
            }
            cout << endl;
        /*while(j > 0)
        {
            if(dp[i][j] == dp[i][j-1]) j--;
            else
            {
                path[k--] = j - 1;
                j--;
            }
        }
        for(int i = 1; i <= dp[len1][len2]; i++)
            {
                printf("%c",b[path[i]]);
            }
            cout << endl;*/
    }
}
3.最长公共子串(连续)

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int dp[105][105];  //记录当前字母“前面”的最长子序列的长度
char a[100], b[100];
int main()
{
    while(cin >> a >> b)
    {
        int max1 = 0, temp;
        memset(dp,0,sizeof(dp));
        int len1 = strlen(a), len2 = strlen(b);
        for(int i = 1; i <= len1; i++)
            for(int j = 1; j <= len2; j++)
            {
                 if(a[i-1] == b[j-1])  dp[i][j] = dp[i-1][j-1] + 1;
                 else dp[i][j] = 0;
                 if(max1 < dp[i][j])
                 {
                    max1 = dp[i][j]; //纪录dp[][]中的最大值
                    temp = i;//纪录最长公共子串的末端在str1中的位置(也可以纪录在str2中的位置)
                 }
            }
        for(int i = temp - max1; i < temp; i++)
            cout << a[i];
            cout << endl;
    }

}

4.LIS的三种写法

方法一: 转换成LCS问题 空间时间复杂度都比较大, 空间n^2,时间n^2+nlogn
将原数组与排好序的数组进行求LCS,得到的最长公共子序列的就是最长上子序列.

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 10005;
int a[maxn], b[maxn], dp[maxn][maxn];
int main(void)
{
    int n;
    while(cin >> n)
    {
        memset(dp, 0, sizeof(dp));
        for(int i = 0; i < n; i++) scanf("%d", &a[i]), b[i] = a[i];
        sort(b, b+n);
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
            {
                if(a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
                else dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        printf("%d\n", dp[n][n]);   //非严格递增,如要严格递增要对b数组去重
    }
    return 0;
}
方法二:动态规划  时间复杂度n^2,空间复杂度n
设f(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:
这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面
且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末
元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,
即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;
如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], ans;
int main(void)
{
    int n;
    while(cin >> n)
    {
        for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
        ans = 1;
        for(int i = 1; i < n; i++)
            for(int j = 0; j < i; j++)
                if(a[j] < a[i] && dp[j]+1 > dp[i])
                    dp[i] = dp[j]+1, ans = max(ans, dp[i]);
        printf("%d\n", ans);
    }
    return 0;
}

方法三:方法二+二分 时间复杂度nlongn
在第二种算法中,在计算每一个f(i)时,都要找出最大的f(j)(j<i)来,
由于f(j)没有顺序,只能顺序查找满足aj<ai最大的f(j),如果能将让f(j)有序,
就可以使用二分查找,这样算法的时间复杂度就可能降到O(nlogn)。于是想到用一个数组
B来存储“子序列的”最大增子序列的最末元素,即有B[f(j)] = aj在计算f(i)时,在数组B中
用二分查找法找到满足j<i且B[f(j)]=aj<ai的最大的j,并将B[f[j]+1]置为ai。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1005;
int a[maxn], b[maxn], dp[maxn];
int main(void)
{
    int n;
    while(cin >> n)
    {
        for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
        int Len = 1, l, m, r;
        b[1] = a[0];
        for(int i = 1; i < n; i++)
        {
            l = 1, r = Len;
            while(l <= r)
            {
                m = (l+r)/2;
                if(b[m] < a[i]) l = m+1;
                else r = m-1;
            }
            b[l] = a[i];
            if(l > Len) Len++;
        }
        printf("%d\n", Len);
    }
    return 0;
}

上面的代码都是只求了最长的长度,但没有求路径。路径可能有多条,题目不同要求输出的不同,贴一下输出第一条
路径和最后一条路径的代码。输出第一条符合的路径:

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], pre[maxn], path[maxn],  ans, e;
int main(void)
{
    int n;
    while(cin >> n)
    {
        for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1, pre[i] = -1;
        ans = 1;    //初始最大长度为1,结尾在a[0]
        e = 0;
        for(int i = 1; i < n; i++)
            for(int j = 0; j < i; j++)
                if(a[j] < a[i] && dp[j]+1 > dp[i])
                {
                    dp[i] = dp[j]+1;
                    pre[i] = j;     //记录每个点i的上一个最长序列,存在已他自己为下标的pre里
                    if(dp[i] > ans) ans = dp[i], e = i;    //因为是>,而不是>=所以最后一个元素肯定是所有里面最前面的
                }
        printf("%d\n", ans);
        //路径要逆推回去
        for(int i = 0, k = ans; i < ans; i++)
        {
            path[k--] = a[e];   //从后往前推,最后一个节点为e;把他的值a【e】输出了
            e = pre[e];     //pre【e】存了上一个他的节点,也就是他之前最长的长度的最后一个字母
        }
        for(int i = 1; i <= ans; i++)
        {
            if(i-1) printf(" ");
            printf("%d", path[i]);
        }
        printf("\n");
    }
    return 0;
}
输出最后一条路径
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], path[maxn];
int main(void)
{
    int n, ans;
    while(cin >> n)
    {
        ans = 0;
        for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
        for(int i = 1; i < n; i++)
            for(int j = 0; j < i; j++)
                if(a[j] < a[i] && dp[j]+1 > dp[i])
                    dp[i] = dp[j]+1, ans = max(ans, dp[i]);
        printf("%d\n", ans);
        for(int i = n-1, j = ans; i > -1; i--)   //从后往前找,第一个找的肯定是最后面那个最长的最后一个元素
            if(dp[i] == j) path[j--] = a[i];
        for(int i = 1; i <= ans; i++)
        {
            if(i != 1) printf(" ");
            printf("%d", path[i]);
        }
        printf("\n");
    }
    return 0;
}
//求原串与逆序串的最大公共子序列(滚动数组)
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn = 10005;
int a[maxn], dp[maxn], path[maxn];
int main(void)
{
    int n, ans;
    while(cin >> n)
    {
        ans = 0;
        for(int i = 0; i < n; i++) scanf("%d", &a[i]), dp[i] = 1;
        for(int i = 1; i < n; i++)
            for(int j = 0; j < i; j++)
                if(a[j] < a[i] && dp[j]+1 > dp[i])
                    dp[i] = dp[j]+1, ans = max(ans, dp[i]);
        printf("%d\n", ans);
        for(int i = n-1, j = ans; i > -1; i--)   //从后往前找,第一个找的肯定是最后面那个最长的最后一个元素
            if(dp[i] == j) path[j--] = a[i];
        for(int i = 1; i <= ans; i++)
        {
            if(i != 1) printf(" ");
            printf("%d", path[i]);
        }
        printf("\n");
    }
    return 0;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值