算法初探系列14——线性DP进阶之最大子段和与最长上升子序列

Posted 蒟蒻一枚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法初探系列14——线性DP进阶之最大子段和与最长上升子序列相关的知识,希望对你有一定的参考价值。


概述

上节课蒟蒻君和大家一起学习了线性DP初步,相信聪明伶俐的你已经很熟悉啦~这节课蒟蒻君带领大家解决线性DP两个经典问题。


最大子段和


题目

题目描述


给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。


输入格式


第一行是一个整数,表示序列的长度 n。

第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 ai


输出格式


输出一行一个整数表示答案。


输入输出样例


输入 #1

7
2 -4 3 -1 2 -4 3

输出 #1

4

说明/提示


样例 1 解释
选取 [3, 5] 子段 {3, -1, 2},其和为 4。

数据规模与约定


对于40% 的数据,保证 n≤2×103
对于 100% 的数据,保证 1n≤2×105 ,-104 ≤ai ≤104


分析


法1:枚举(T(n3) + O(1))

  • 枚举共有三层。
  • 第一层:枚举子段左端点l。
  • 第二层:枚举子段右端点r。
  • 第三层:累加al至ar

法2:枚举+前缀和优化(T(n3) + O(1))

在法1的基础上,我们可以维护ai加到aj的前缀和,优化掉一层循环。


法3:DP(T(n) + O(n))

  • 状态定义:
    设dpi为a0到ai的最大子段和。
  • 状态转移方程
    在i ≠ 0时,对于dpi有一下两个决策:
    ①继续dpi-1选择的子段。
    ②重新开始一个子段。
    综上,dpi = max(dpi-1, 0) + ai
  • 边界条件
    dp0 = a0

法4:DP + 前缀和优化(T(n) + O(1))

  • 我们可以用sum维护a的前缀和。
  • 对于每次的sum,我们需要用之前的sum求出现在的sum,相当于用dpi-1求出dpi
  • 其余的和法3全部相等。
  • 继续优化,我们可以用x维护ai

法5(拓展):分治法(T(n) + O(n))

  • 设get(l, r)为al至ar的最大子段和。
  • 对于每组(l, r),我们将它分为左右两段分别处理。
  • 对于每组(l, r),我们有以下三种方案:
    ①只考虑左边,即get(l, r) = get(l, mid)
    ②只考虑右边,即get(l, r) = get(mid + 1, r)
    ③考虑左右两段拼接。下面我们来详细说一下③。
  • 我们可以求出所有右端点是amid的子段的和的最大值,再求出所有左端点是mid + 1的子段的最大值。
  • 所有的子问题(即左右两段)的解都是最优,则母问题的解也一定是最优的了,即get(l, r) = 左边的最大值 + 右边的最大值。

实现


法1

#include <bits/stdc++.h>
using namespace std;
const int N = 200005;
int a[N];
int main() {
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	int res = ~0x3f3f3f3f;
	for (int l = 1; l <= n; ++l) {
		for (int r = l; r <= n; ++r) {
			int sum = 0;
			for (int i = l; i <= r; ++i) {
				sum += a[i];
			}
			res = max(res, sum);
		}
	}
	cout << res << '\\n';
	return 0;
}

法2

#include <bits/stdc++.h>
using namespace std;
const int N = 200005;
int a[N];
int main() {
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	int res = ~0x3f3f3f3f;
	for (int l = 1; l <= n; ++l) {
		int sum = 0;
		for (int r = l; r <= n; ++r) {
			sum += a[r];
			res = max(res, sum);
		}
	}
	cout << res << '\\n';
	return 0;
}

法3

#include <bits/stdc++.h>
using namespace std;
const int N = 200000;
int a[N], dp[N];
int main() {
	int n;
	cin >> n;
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
	}
	dp[0] = a[0];
	int res = dp[0];
	for (int i = 1; i < n; ++i) {
		dp[i] = max(dp[i - 1], 0) + a[i];
		res = max(res, dp[i]);
	}
	cout << res << '\\n';
	return 0;
}

法4

#include <bits/stdc++.h>
using namespace std;
int main() {
	int n, x;
	cin >> n >> x;
	int maxn = x, sum = x;
	while (--n) {	// 只执行n - 1次 
	    cin >> x;
		sum = max(sum + x, 0);
		maxn = max(maxn, sum);
	}
	cout << maxn << '\\n';
	return 0;
}

法5

#include <bits/stdc++.h>
using namespace std;
const int inf = ~0x3f3f3f3f;
int n, a[200000];
int get(int l, int r) {
	// 递归终止条件:子段长度为1,解固定 
	if (l == r) {
		return a[l];
	}
	int mid = l + r >> 1;
	// suml维护左子段的前缀和(从mid往l遍历),sumr维护右子段的前缀和(从mid + 1往r遍历)
	int suml = 0, sumr = 0;
	// maxl为左子段右端点为a[mid]的子段的和的最大值,maxr为右子段左端点为a[mid + 1]的子段的和的最大值 
	int maxl = inf, maxr = inf;
	for (int i = mid; i >= l; --i) {
		suml += a[i];
		maxl = max(maxl, suml);
	}
	for (int i = mid + 1; i <= r; ++i) {
		sumr += a[i];
		maxr = max(maxr, sumr);
	}
	return max(max(get(l, mid), get(mid + 1, r)), maxl + maxr);
}
int main () {
	cin >> n;
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
	}
	cout << get(0, n - 1) << '\\n';
	return 0;
}

最长上升子序列

题目


题目描述

给定n个数a0到an-1,从中选出k个数ab[0],ab[1]一直到ab[k-1],使得a与b均为升序排列。这样的k个数就是a的上升子序列,求a的最长上升子序列的长度。


输入格式

共两行。第一行一个正整数n(n ≤ 5000),表示数组元素个数。第二行n个数表示a数组(0 ≤ ai ≤ 1e9)。


输出格式

一个数k表示a的最长上升子序列的长度。

输入样例

13
7 9 16 38 24 37 18 44 19 21 22 63 15

输出样例

8

样例解释

选择{7, 9, 16, 18, 19, 21, 22, 63}即可。


分析

法1:dfs(T(n!) + O(n))

每次从上次位置后搜索,有符合条件的数就放进去并标记,尝试完回溯。


法2:DP(T(n2) + O(n))

  • 状态定义
    定义dpi为a0到a~i并且 包含ai 的最长上升子序列的长度。
  • 状态转移方程
    考虑ai可以继承a0到ai-1的上升子序列,而且继承的子序列越长越好。即:
    dpi = max{a0到ai-1} + 1
  • 边界条件
    一个数肯定可以组成上升子序列,即dp0 = 1。

法3:DP+二分查找(T(nlogn) + O(n) )

  • 状态定义
    定义dpi为长度为i的上升子序列最后一位的最小值。
  • 状态转移方程
    对于每个ai和pos:
    ①若dpj < ai,则ai可以继承这个上升子序列,则dpj = ai
    ②若dpj ≥ ai,则ai可以替换上升子序列的最后一位,因为dp数组中的值越小越可能有后边的值可以继承,但是!!我们不一定要继承长度为j的子序列
    因为在结尾相同的情况下肯定长度越长越好, 所以我们如果确定可以继承长度为j的子序列,那么就有可能可以继承长度 > j的子序列。
    我们可以用二分查找找出最大的符合dpj > ai的j(可以用库函数lower_bound)。
    综上,状态转移方程为:
    ①若dpj < ai,dpj = ai
    ②若dpj ≥ ai,dplower_bound(dp,dp+j+1,a[i])-dp = ai
  • 边界条件
    只由a0构成的上升子序列中的最大值为a0,即dp0 = a0

实现


法1

#include <bits/stdc++.h>
using namespace std;
const int N = 5000;
int n, res = ~0x3f3f3f3f;
int a[N];
// k: 目前上升子序列的长度 
// last: 上次选择的值的下标
void dfs(int k, int last) {
	res = max(res, k);
	for (int i = last + 1; i < n; ++i) {
		if (a[i] > a[last]) {
			dfs(k + 1, i);
		}
	}
} 
int main() {
	cin >> n;
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
	}
	dfs(1, 0);
	cout << res << '\\n';
	return 0;
}


法2

#include <bits/stdc++.h>
using namespace std;
int a[5000], dp[5000];
int main() {
	int n;
	cin >> n;
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
	}
	dp[0] = 1;
	int res = ~0x3f3f3f3f;
	for (int i = 1; i < n; ++i) {
		for (int j = 0; j < i; ++j) {
			if (a[i] > a[j]) {
				dp[i] = max(dp[i], dp[j] + 1);
			}
		}
		res = max(res, dp[i]);
	}
	cout << res << '\\n'; 
	return 0;
}

法3

#include <bits/stdc++.h>
using namespace std;
int a[5000], dp[5000];
int main(){
	int n;
	cin >> n;
	for (int i = 0; i < n; ++i) {
		cin >> a[i];
	}
	dp[0] = a[0];
	int j = 0;
	for (int i = 1; i < n; ++i) {
		if (a[i] > dp[j]) {
			dp[++j] = a[i];
		} else {
			dp[lower_bound(dp, dp + j + 1, a[i]) - dp] = a[i];
		}
	}
	cout << ++j << '\\n';
	return 0;
}

这节课蒟蒻君和大家学习了最大子段和最长上升子序列,下节课蒟蒻君将和大家一起学习最长公共子序列编辑距离

以上是关于算法初探系列14——线性DP进阶之最大子段和与最长上升子序列的主要内容,如果未能解决你的问题,请参考以下文章

最大子段和之环形问题

最大子段和之M子段和

最大连续子段和的两种线性算法

最大连续子段和的两种线性算法

51Nod 1050 循环数组最大子段和 | DP

算法2-1前缀和与差分