算法初探系列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进阶之最大子段和与最长上升子序列的主要内容,如果未能解决你的问题,请参考以下文章