最大子段和 最长上升子序列 最长公共子序列 最长公共上升子序列 编辑距离(dp思路及例题)

最大子段和

1(最大子段和)
题目描述 :
给出一段序列,选出其中连续且非空的一段使得这段和最大。
输入格式:
第一行是一个正整数N,表示了序列的长度。
第二行包含N个绝对值不大于1e9的整数Ai​,描述了这段序列。
输出格式:
一个整数,为最大的子段和是多少。

思路:
对于全部为负数的序列 ,我们只需输出一个最大值即可。
对于不全为负数的序列 ,我们利用前缀和sum处理,每次更新一个最大值max,当sun<0时,说明后面的数加上此前缀无法变得更大,和则令sun=0,继续处理。

代码:

#include 
long long n,a[1000005],sum,max,MAX=-1e9;
int main()
{
	scanf("%lld",&n);
	for(int i=0;i<n;i++)
    {
        scanf("%lld",&a[i]);
        if(a[i]>MAX)
            MAX=a[i];
    }
    if(MAX<=0)	
    {
        printf("%lld",MAX);
        return 0;
    }
    max=a[0];
	for(int i=0;i<n;i++)
	{
		sum+=a[i];
		if(sum<0)
			sum=0;
		if(sum>max)
			max=sum;
	}
	printf("%lld",max);
	return 0;
}

2 (最大子矩阵和)
题目描述 :
给出一个矩阵,求最大非空子矩阵是多少。
输入格式:
第一行是正整数N ,M,表示矩阵的行数 列数。
接下来N行,每行M个数,表示-1e9<=a[i][j]<=1e9。
输出格式:
一个整数,为最大的非空子矩阵和是多少。

思路:我们可以把二维的矩阵,转变为一维的子段和问题处理,所以我们要储存a[i][j]第j列前i行和。

代码:

#include 
long long n,m,a[444][444],b[444][444],s,ans=-1e9;
int main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            scanf("%lld",&a[i][j]);
            b[i][j]=b[i-1][j]+a[i][j];
            if(ans<a[i][j])
                ans=a[i][j];
        }
    }
    if(ans<=0)
    {
        printf("%lld",ans);
        return 0;
    }
    else
    {
        for(int i=1;i<=n;i++)
        {
            for(int j=i;j<=n;j++)
            {
                s=0;
                for(int k=1;k<=m;k++)
                {
                    s+=b[j][k]-b[i][k];
                    if(s<0)
                        s=0;
                    else if(s>ans)
                        ans=s;
                }
            }
        }
    }
    printf("%lld",ans);
    return 0;
}

最长上升子序列

1.( 跳木桩)
面前有一排 n 个木桩,木桩的高度分别是h1,h2,h3⋯hn​。第一步可以跳到任意一个木桩,接下来的每一步不能往回跳只能往前跳,并且跳下一个木桩的高度 不大于 当前木桩。希望能踩到尽量多的木桩,请你计算,最多能踩到多少个木桩。

输入格式
第一行输入一个整数 n代表木桩个数。第二行输入 n个整数 分别代表 n 个木桩的高度。(1≤n≤1000,1≤hi​≤100000)

输出格式
输出一个整数,代表最多能踩到的木桩个数,占一行。

样例输入
6
3 6 4 1 4 2
样例输出
4

思路:本题的模版是最长不上升子序列,dp[i]表示以第i号元素为结尾的子序列,最长是多少。我们只需要枚举i号之前的元素,有多少不小于h[i]即可。最后输出最大的一个dp[i].

代码:

#include 
int n,h[100005],dp[100005],ans=1;
int max(int x,int y)
{
    return x>y?x:y;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&h[i]);
    }
    for(int i=1;i<=n;i++)
    {
        dp[i]=1;
        for(int j=1;j<i;j++)
        {
            if(h[j]>=h[i])
            {
                dp[i]=max(dp[j]+1,dp[i]);
                ans=max(dp[i],ans);
            }
        }
    }
    printf("%d",ans);
    return 0;
}

2(删除最少元素)
给定n个数 ,满足a[i]>=a[j]···>=a[k]<=···a[p]<=a[q] (其中 i

思路:两个dp

代码:

#include 
int n,a[1005],dp1[1005],dp2[1005],ans;
int max(int x,int y)
{
    return x>y?x:y;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
    }
    for(int i=1;i<=n;i++)
    {
        dp1[i]=1;
        for(int j=1;j<i;j++)
        {
            if(a[j]>=a[i])
            {
                dp1[i]=max(dp1[i],dp1[j]+1);
            }
        }
    }
    for(int i=n;i>=1;i--)
    {
        dp2[i]=1;
        for(int j=n;j>i;j--)
        {
            if(a[j]>=a[i])
            {
                dp2[i]=max(dp2[i],dp2[j]+1);
            }
        }
    }
    for(int i=1;i<=n;i++)
    {
        ans=max(dp1[i]+dp2[i]-1,ans);
    }
    printf("%d",n-ans);
    return 0;
}

3(优化版本的最长上升子序列)
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式
第一行包含整数N。

第二行包含N个整数,表示完整序列。

输出格式
输出一个整数,表示最大长度。

数据范围
1≤N≤100000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

思路:考虑到上述最长子序列算法时间复杂度为n方,而本题n的范围为1e5,所以必须进行优化。
首先数组a中存输入的数(原本的数),开辟一个数组f用来存结果,最终数组f的长度就是最终的答案;假如数组f现在存了数,当到了数组a的第i个位置时,首先判断a[i] > f[cnt] ? 若是大于则直接将这个数添加到数组f中,即f[++cnt] = a[i];这个操作时显然的。
当a[i] <= f[cnt] 的时,我们就用a[i]去替代数组f中的第一个大于等于a[i]的数,因为在整个过程中我们维护的数组f 是一个递增的数组,所以我们可以用二分查找在 logn 的时间复杂的的情况下直接找到对应的位置,然后替换,即f[l] = a[i]。
我们用a[i]去替代f[i]的含义是:以a[i]为最后一个数的严格单调递增序列,这个序列中数的个数为l个。

这样当我们遍历完整个数组a后就可以得到最终的结果。

时间复杂度分析:O(nlogn)

代码:

#include 
long long a[100005],f[100005],n,ans=0;
void er(long long target) 
{
    int left = 0;
    int right = ans; 
    while (left < right)
    { 
        int mid = (left + right) / 2;
        if (f[mid] == target) 
        {
            right = mid;
        } 
        else if (f[mid] < target)
        {
            left = mid + 1;
        } 
        else if (f[mid] > target)
        {
            right = mid; 
        }
    }
    f[left]= target;
}
int main()
{
    scanf("%lld",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%lld",&a[i]);
    }
    f[ans++]=a[0];
    for(int i=1;i<n;i++)
    {
        if(a[i]>f[ans-1]) f[ans++]=a[i];
        else if(a[i]<f[ans-1])
        {
            er(a[i]);
        }
    }
    printf("%lld",ans);
    return 0;
}


最长公共子序列

1(最长公共子序列)
最大子段和 最长上升子序列 最长公共子序列 最长公共上升子序列 编辑距离(dp思路及例题)_第1张图片
思路:
设X=x1 x2…xm和Y=y1 y2…yn是两个序列,Z=z1 z2…zk是这两个序列的一个最长公共子序列。

  1. 如果xm=yn,那么zk=xm=yn,且Zk-1是Xm-1,Yn-1的一个最长公共子序列;
  2. 如果xm≠yn,那么zk≠xm,意味着Z是Xm-1,Y的一个最长公共子序列;
  3. 如果xm≠yn,那么zk≠yn,意味着Z是X,Yn-1的一个最长公共子序列。

我们使用dp[i][j]来表示第一个串的前i位和第二个串的前j位中的最长公共子序列,我们很容易能发现当两个串的任意一个串的当前长度为0时,它的最长公共子序列的长度为0,所以先对dp数组的边界进行初始化。然后我们发现,如果a[i]=b[j],dp[i][j]=dp[i-1][j-1]+1,很显然,当比对的位字符一样时,能得到该状态转移方程。如果a[i]≠b[j],dp[i][j]=max(dp[i-1][j],dp[i][j-1]),该状态转移方程是由上面的2,3条取最大值得到的。

代码:

#include 
#include 
int len1,len2,dp[1005][1005]; //dp[i][j]表示a前i位和b前j位的最长公共子序列长度
char a[1005],b[1005];
int max(int x,int y)
{
	return x>y?x:y;
}
int main()
{
    scanf("%s%s",a+1,b+1);
    len1=strlen(a+1);
    len2=strlen(b+1);
    for(int i=1;i<=len1;i++)
    {
        for(int j=1;j<=len2;j++)
        {
            if(a[i]==b[j])
            {
                dp[i][j]=dp[i-1][j-1]+1;
            }
            else
            {
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
    }
    printf("%d",dp[len1][len2]);
	return 0;
}

2
问题描述
一个字符串如果从左往右读和从右往左读都一样,那么这个字符串是一个回文串。例如:”abcba”,”abccba”。
蒜头君想通过添加字符把一个非回文字符串变成回文串。例如:”trit”,可以添加一个’i’ 变成回文串”tirit”。请你用程序计算出,对于一个给定的字符串,最少需要添加几个字符,才能变成回文串。
输入格式
输入一个长度为n(1≤n≤3000) 的字符串。(字符串只包含字母)
输出格式
输出最少需要添加的字符个数,占一行。

思路:
简单分析一些就可以发现:既然是回文串,那么将原串s1倒置得到一个新的字符串s2,求出s1和s2的最长公共子序列长度,用s1的长度减去所求长度就是最少添加字符数量。

代码:

#include 
#include 
int len,dp[3010][3010];
char a[3010],b[3010];
int max(int x,int y)
{
    return x>y?x:y;
}
int main()
{
    scanf("%s",a);
    len=strlen(a);
    int j=0;
    for(int i=len-1;i>=0;i--)
    {
        b[j++]=a[i];
    }
    b[len]='\0';
    for(int i=len-1;i>=0;i--){
		for(int j=len-1;j>=0;j--){
			if(a[i]==b[j]){
				dp[i][j]=dp[i+1][j+1]+1;
			}else{
				dp[i][j]=max(dp[i+1][j],dp[i][j+1]);
			}
		}
	}
    printf("%d",len-dp[0][0]);
    return 0;
}

最长公共上升子序列

1
熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列A和B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列A和B的长度均不超过3000。

输入格式
第一行包含一个整数N,表示数列A,B的长度。

第二行包含N个整数,表示数列A。

第三行包含N个整数,表示数列B。

输出格式
输出一个整数,表示最长公共子序列的长度。

数据范围
1≤N≤3000,序列中的数字均不超过231−1
输入样例:
4
2 2 1 3
2 1 2 3
输出样例:
2

思路:dp[i][j]表示a中前i位和b中第j位的最长公共上升子序列。对于a[i],我们遍历b中每个元素,判断a[i]和b[j]的关系,如果a[i]>b[j],那么为了方便后面找到a[i]=b[k]时,dp[i][k]的最长公共子序列,我们记录最大的max=dp[i-1][j](a[i]>b[j])。如果a[i]=b[j],那么dp[i][j]=max+1.

代码:

#include 
int n,m,a[3005],b[3005],dp[3005][3005],ans=-1e9,MAX;   //dp[i][j]代表a中前i位和b中第j位匹配时最长公共上升子序列
int max(int x,int y)                                  //MAX代表小于b[j]的b[k]所具有的最大子序列长度
{
    return x>y?x:y;
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)
        scanf("%d",&b[i]);
    for(int i=1;i<=n;i++)
    {
        MAX=0;       
        for(int j=1;j<=n;j++)
        {
            dp[i][j]=dp[i-1][j];  //如果第i位与j位不相同 则前i位与第j位最大上升子序列要去前i-1位找
            if(a[i]>b[j])   MAX=max(MAX,dp[i-1][j]);  //优化 减小后面遍历寻找b[k]
            else if(a[i]==b[j]) dp[i][j]=MAX+1;  
        }
    }
     for(int j=1;j<=n;j++)
    {
         ans=max(ans,dp[n][j]);  //n j 代表前n位 第j位
    }
    printf("%d",ans);
    return 0;
}

2
蒜头君喜欢把做过的事情记录下来,写在日志里,为了安全起见,它还有一份备份放在另外的地方,不过很不幸的是,最近他的两份日志都受到了破坏,有增加删除修改,但没有改变任何原来的顺序,两份受到的破坏不一定一样,蒜头君记录事情都是按时间顺序的,记录的也都是时间戳,所以正确的记录上时间戳肯定是严格递增的,并且只有两份记录上都出现了的时间戳他才认为有可能自己做了,现在他想知道他最多可能做了多少事情。

输入格式
第一行输入两个整数n,m代表两份记录的长度。(1≤n,m≤103)

接下来一行输入n个整数,a1,a2,a3⋯an,代表第一份记录的n个时间戳。(1≤ai≤103)

接下来一行输入m个整数,b1,b2,b3⋯bm,代表第二份记录的m个时间戳。(1≤bi≤103)

输出格式
输出一个整数,代表蒜头君最多可能做了多少事情。

输入样例
3 2
1 3 2
1 2
输出样例
2

代码:

#include 
int n,m,a[1005],b[1005],dp[1005][1005],max;
int main()//dp[i][j]代表a数组中前i项和b数组中以j项结尾的最长公共上升子序列
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
        scanf("%d",&b[i]);
    for(int i=1;i<=n;i++)
    {
        max=0;
        for(int j=1;j<=m;j++)
        {
            dp[i][j]=dp[i-1][j];//a[i]!=b[i]时
            if(a[i]>b[j]&&max<dp[i-1][j])	max=dp[i-1][j];//记录小于a[i]的最长的长度 后面要用
            else if(a[i]==b[j])	dp[i][j]=max+1;
        }
    }
    int ans=0;
    for(int i=1;i<=m;i++)
    {
        if(ans<dp[n][i])
        {
            ans=dp[n][i];
        }
    }
    printf("%d\n",ans);
    return 0;
}

编辑距离

编辑距离参考思路

每个人都有点秘密,蒜头君也不例外,他把秘密记在一个小本上,并且留有备份,不过第一个本的内容被人破坏掉了,跟原来不一定相同了,他现在想要照着第二个本把第一个本的内容还原,每一次做一个操作,一个操作可以是在某位置增加一个字符,删掉某个字符,或者把某个位置的字符改成另一个字符,他想知道他最少需要进行多少次操作才能把第一个本的内容还原。

输入格式
第一行一个字符串A,表示第一个本被破坏之后的字符串。

第二行一个字符串B,表示第二个本上面的字符串。

字符串均仅有小写字母组成且长度均不超过1000。

输出格式
输出一个整数,为蒜头君最少要做的操作数。

样例输入
aa
ab
样例输出
1

代码:

#include 
int min(int x,int y,int z)
{
    if(x>y)
        x=y;
    if(x>z)
        x=z;
    return x;
}
char a[1000],b[1000];
int  dp[1000][1000]; //dp[i][j]代表a数组前i元素和b数组前j元素匹配所进行的操作数
int main()
{	
    scanf("%s%s",a,b);
    int len1=strlen(a);
    int len2=strlen(b);
    for(int i=1;i<=len1;i++)
        dp[i][0]=i;     //处理边界情况
    for(int j=1;j<=len2;j++)
        dp[0][j]=j;
    for(int i=1;i<=len1;i++)  //防止数组下标出现负数 故i、j从1开始处理
    {
        for(int j=1;j<=len2;j++)
        {
            if(a[i-1]==b[j-1])
            {
                dp[i][j]=dp[i-1][j-1];
            }
            else
            {
                dp[i][j]=min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1;
            }    //分别对应修改 a删除 a增加
        }
    }
    printf("%d\n",dp[len1][len2]);
    return 0;
}

你可能感兴趣的:(算法初探)