最大上升子序列和 (一文消化dp解法)【从小白到进阶的各种解法】

😊😊 😊😊
不求点赞,只求耐心看完,指出您的疑惑和写的不好的地方,谢谢您。本人会及时更正感谢。希望看完后能帮助您理解算法的本质
😊😊 😊😊

题目描述:

一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …,aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中序列和最大为18,为子序列(1, 3, 5, 9)的和. 你的任务,就是对于给定的序列,求出最大上升子序列和。注意,最长的上升子序列的和不一定是最大的,比如序列(100, 1, 2, 3)的最大上升子序列和为100,而最长上升子序列为(1, 2, 3)。

输入描述:
输入包含多组测试数据。
每组测试数据由两行组成。第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出描述:
对于每组测试数据,输出其最大上升子序列和。

输入:
7
1 7 3 5 9 4 8

输出:
18

小白到进阶各种解法:

一、两重循环暴力枚举:😊

思路一:

  1. 枚举所有的上升子序列。
  2. 求出所有的上升子序列的和值。
  3. 从所有上升子序列中取一个和值最大的上升子序列,即为答案!

思考:怎样枚举才能做到不重不漏地枚举呢?
这里采用的是:从左往右枚举,每次枚举以第 i i i 个数为结尾的所有上升子序列。从而做到不重不漏枚举。因为每个数都会作为子序列的终点!

具体实现:
外层循环枚举子序列的终点,即以第 i i i 个数为作为当前子序列的终点,然后内层循环枚举区间 [ 1 , i ] [1, i] [1,i] 的上升子序列,并计算上升子序列的和。然后一个一个地取最大值即可!

代码一:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 1e5 + 10;
long long a[N], f[N];

int main()
{
	int n, res=0;
	cin >> n;
	for (int i=1; i <= n; i ++)
		cin >> a[i];
	int ans=0;
	for (int i=1; i <= n; i ++)
	{
		int sum=0;	//计算以第i个数为结尾的上升子序列的和,总之就是求出所有的上升子序列的和!
		int last = 0; 
		for (int j=1; j <= i; j ++)
		{
			if (a[j] > last)
			{
				sum += a[j];
				last = a[j];
			}
		}
		ans = max(ans, sum);
	}
	
	cout << ans << endl;
	return 0;
}

相信有同学很困惑我是如何保证序列严格单调上升的呢?
其实下面的思路二,甚至是暴搜,我都用了同样的技巧!下面思路二进行讲解更为合适!

思路二:与思路一恰好相反!

思路一的枚举方式是枚举所有以第 i i i 个数为结尾的上升子序列的和,思路二也可以 到不重不漏地枚举所有的上升子序列。
即外层循环子序列的起点,内层循环枚举子序列的终点 (i ~ n)。
请先看下面的代码段:代码后面有详细解释 l a s t last last 变量!

	for (int i=1; i <= n; i ++)	//枚举所有子序列的起点! 
	{
		int sum=a[i];	//记录以第i个数为起点的子序列的总和! 
		int last=a[i];
		for (int j=i+1; j <= n; j ++)
		{
			if (a[j] > last)	//保证子序列严格单调上升!
			{
				sum += a[j];
				last = a[j];
			}
		}
		ans = max(ans, sum);
	}

如何保证序列严格单调上升?

关键在于我们的 l a s t last last 变量!
l a s t last last 变量的作用是:记录上升子序列的末尾的元素,既然是上升子序列的末尾元素,那么这个元素必然是整个子序列最大的值。那么每次加入一个元素就要进行迭代,迭代 l a s t last last ,想想一个元素加入到上升子序列的条件是什么,就是这个元素要大于 这个上升子序列的末尾元素啊,即大于 l a s t last last 才可以加入啊!所以说,每次加入一个新的元素,都要将 l a s t last last 变量进行迭代传递交付!!

代码二:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int N = 1e5 + 10;
long long a[N], f[N];

int main()
{
	int n, res=0;
	cin >> n;
	for (int i=1; i <= n; i ++)
		cin >> a[i];
	int ans=0;
	for (int i=1; i <= n; i ++)	//枚举所有子序列的起点! 
	{
		int sum=a[i];	//记录以第i个数为起点的子序列的总和! 
		int last=a[i];
		for (int j=i+1; j <= n; j ++)
		{
			if (a[j] > last)	//保证子序列严格单调上升!
			{
				sum += a[j];
				last = a[j];
			}
		}
		ans = max(ans, sum);
	}
	
	cout << ans << endl;
	return 0;
}

二、暴搜:😊

在这里插入图片描述

思路1:

  1. 枚举序列中的每个元素,作为起点去进行搜索。
  2. 搜索以该点为起点的上升子序列。
  3. 递归的出口:当整个序列搜索完毕后,即为 u > n u>n u>n时,表示以该点为起点的序列搜索完毕!
  4. 递归的参数:搜索到序列中的第 u u u 个数了,当前上升子序列的和值 s u m sum sum
  5. 递归计算:保证严格单调递增,就需要保证添加到序列末尾的数必须大于当前末尾元素!
for (int i=u+1; i <= n; i ++)
        if(a[u] < a[i]) //先保证严格单调上升
            dfs(i, sum + a[i]);
ans = max(ans, sum);

代码1:

#include<iostream>

using namespace std;

const int N = 1e5 +  10;
typedef long long LL;
int n;
int a[N];
int ans;

void dfs(int u, int sum)
{
    for (int i=u+1; i <= n; i ++)
    {
        if(a[u] < a[i]) //先保证严格单调上升
        {
            dfs(i, sum + a[i]);
        }
    }
    ans = max(ans, sum);
    return;
}

int main()
{
    scanf("%d", &n);
    for (int i=1; i <= n; i ++)
        scanf("%d", &a[i]);
    
    for (int i=1; i <= n; i ++)
        dfs(i, a[i]);
    cout << ans << endl;
    return 0;
}

思路2:

代码2:

#include <iostream>
#include <vector>
using namespace std;

int n, ans;
vector<int> a;

void dfs(int i, int sum) {
    if (i == n) {  // 达到序列末尾,更新答案
        ans = max(ans, sum);
        return;
    }
    dfs(i + 1, sum);  // 不选当前元素
    if (a[i] > a[i - 1]) {  // 选当前元素
        dfs(i + 1, sum + a[i]);	
    }
}

int main() {
    cin >> n;
    a.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> a[i];
    }
    dfs(0, 0);  // 从第一个元素开始搜索
    cout << ans << endl;
    return 0;
}

在这里插入图片描述

三、记忆化搜索:待更新😊

在这里插入图片描述

思路一:

  1. 记忆化数组: f [ u ] : f[u]: f[u]表示以第u个数为起点的最大和上升子序列!
  2. 那么则枚举每一个元素作为上升子序列的起点。往后搜索它的上升子序列。
  3. 记住: f [ u ] : f[u]: f[u]记录的是从当前节点到末尾节点的答案,并不包含它前面的元素的情况在内!即局部答案!
    在这里插入图片描述

代码一:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL f[N];
int a[N];
int n;

LL dfs(int u)
{
    if (f[u] != 0)
        return f[u];
    
    LL res = a[u];
    for (int i=u+1; i <= n; i ++)
        if (a[i] > a[u])
            res = max(res, dfs(i) + a[u]);
    f[u] = res;
    return res;
}

int main()
{
    cin >> n;
    for (int i=1; i <= n; i ++)
        cin >> a[i];
    LL ans=0;
    for (int i=1; i <= n; i ++)
        ans = max(ans, dfs(i));
    cout << ans;
    return 0;
}

思路二:

其中,f[u]表示以第u个数为结尾的最大上升子序列和,初始化为-1,表示未被访问过。在递归中,如果f[u]已经计算过,则直接返回f[u]。否则,枚举所有比a[u]小的数a[i],递归计算以第i个数为结尾的最大上升子序列和,并更新f[u]的值。最后,枚举所有可能的结尾位置,求出所有的最大上升子序列和,并取其中最大的一个作为答案。

代码二:

#include<iostream>
#include<cstring>

using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
//f[u]:表示以第u个数结尾的上升子序列的最大和值。如果该值已经计算过了,则无需再次计算.因为再次计算结果相同!
LL f[N];
LL a[N];
int n;
int dfs(int u)
{
    if (f[u] != 0)  //记忆化!
        return f[u];
    f[u] = a[u];
    for (int i=1; i < u; i ++)
    //a[u]表示子序列末尾的数,a[i]表示子序列内部的数!既然是以a[u]结尾,则a[u]>a[i]
        if (a[u] > a[i])
    //取max的原因是:从所有分支中取一个最值。因为以第u个数结尾的上升子序列存在多个分支。
    //枚举取最大值!
            f[u] = max(f[u], dfs(i) + a[u]);
    return f[u];
}

int main()
{
    cin >> n;
    for (int i=1; i <= n; i ++)
        cin >> a[i];
    
    int ans=0;
    for (int i=1; i <= n; i ++)
        ans = max(ans, dfs(i));
    
    cout << ans << endl;
    // cout << dfs(1);
    return 0;
}

在这里插入图片描述

四、线性DP动态规划 – O(n2)😊

思路:

  1. 首先思考答案的所有子集情况:比如样例为:1, 9, 7, 10;
    在这里插入图片描述
    由上可知,子集数量众多,小到单独的一个数都可以作为上升子序列的和。
    采用暴力循环解法的话:上面也看到了暴力循环的解法是无法AC的。
    原因就是因为答案子集的数量太多,那么我们不妨将答案进行分类。保证各个类别的子集能够组合起来做到不重不漏!观察上图可知,子集可以以自己作为一个单调上升子序列,所以说为了包含这一类的话,我们不妨设 f [ u ] f[u] f[u]表示的是:以第 u u u 个数为结尾的最大上升子序列的和!从而进行划分:
    比如这里以 10 结尾的子序列有:
    在这里插入图片描述
    所以说以第 i i i 个数为结尾的上升子序列,我们可以将其划分为两类!包含10的子序列和不包含10的子序列。
  2. 如何计算每一类呢?
    在这里插入图片描述

那我们不妨思考下,就是因为需要枚举的答案

总结:

动态规划思路:
1.状态定义:设dp[i]为以a[i]结尾的最大上升子序列和,则最终的结果就是max(dp[i]),其中0≤i<n。
2.状态转移:考虑以a[i]结尾的所有上升子序列,它们的结尾都是a[i],那么这些子序列中的最大值再加上a[i],就是以a[i]结尾的最大上升子序列和。
3.因此,对于每个i,需要枚举0到i-1的所有j,满足a[j]<a[i],然后计算dp[i]=max(dp[j])+a[i]。
4.边界条件:dp[0]=a[0]。
5.最终答案:max(dp[i])。

代码:

#include<iostream>

using namespace std;
const int N = 1e5 + 10;
int a[N];
long long f[N];

int main()
{
	int n;
	long long res=0;
	cin >> n;
	for (int i=1; i <= n; i ++)
		cin >> a[i];
	for (int i=1; i <= n; i ++)
	{
		f[i] = a[i];
		for (int j=1; j < i; j ++)
		{
			if (a[j] < a[i])
				f[i] = max(f[i], f[j] + a[i]);
		}
		res = max(res, f[i]);
	}
	
	cout << res << endl;
	return 0;
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值