最大子段和部分相关问题整理和总结

朴素问题

先来一道朴素最大子段和
https://www.luogu.com.cn/problem/P1115
很简单就是给你一个序列,在这里面找到连续且非空的一段,让这一段和最大

  • d p [ i ] dp[i] dp[i]表示包含第 i i i个数的最大子段和,考虑如何转移,很简单,因为如果 d p [ i − 1 ] < 0 dp[i-1]<0 dp[i1]<0,那么如果第 i i i个数归类到前面,总不如自成一派;反之就归类到前面,这样可以使前面一派更大
#include <bits/stdc++.h>

using namespace std;

int main(){
    
    
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n + 1);
    for(int i=1;i<=n;i++){
    
    
        cin >> a[i];
    }
    vector<int> dp(n + 1);
    for(int i=1;i<=n;i++){
    
    
        if(dp[i - 1] < 0){
    
    
            dp[i] = a[i];
        }else{
    
    
            dp[i] = a[i] + dp[i - 1];
        }
    }
    int ans = -2e9;
    for(int i=1;i<=n;i++){
    
    
        ans = max(ans, dp[i]);
    }
    cout << ans; 
    return 0;
}
  • 这里可以省掉这个 d p dp dp数组,边读边处理,因为满足无后效性,后面不会对前面产生影响

m段连续最大子段和

接下来是一道最大子段和的加强版
http://acm.hdu.edu.cn/showproblem.php?pid=1024
意思是从一个序列中找 m m m个连续子段,要求所有的总和最大的值是多少,这 m m m个子段随便找只要没有重叠部分即可

  • 和第一个问题相比,这个题要求找的子段确定了数量,所以考虑设 d p [ i ] [ j ] dp[i][j] dp[i][j]为前 i i i个数划分成以 i i i为结尾的 j j j个子段得到的最大总和,现在考虑第 i i i个数,如果说这个数划分到第 j j j段,那么应该有 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + a [ i ] dp[i][j]=dp[i-1][j]+a[i] dp[i][j]=dp[i1][j]+a[i];为什么是从 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]转移过来的?这是因为 d p [ i − 1 ] [ j − 1 ] dp[i-1][j-1] dp[i1][j1]是表示以 a [ i − 1 ] a[i-1] a[i1]为结尾的划分成 j − 1 j-1 j1组的最大值, d p [ i ] [ j ] dp[i][j] dp[i][j]不可能从它转移过来,只能从 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j]转移过来;那么如果这个数没有被划分到第 j j j段,那么前面的 i − 1 i-1 i1个数应该被划分为 j − 1 j-1 j1组,哪些数被划分?不知道,但是至少要有 j − 1 j-1 j1个数,最多 i − 1 i-1 i1个数,所以现在状态转移方程应该是 d p [ i ] [ j ] = d p [ k ] [ j − 1 ] + a [ i ] , j − 1 ≤ k ≤ i − 1 dp[i][j]=dp[k][j-1]+a[i],j-1\leq k\leq i-1 dp[i][j]=dp[k][j1]+a[i],j1ki1,所以总的状态转移方程就是
    d p [ i ] [ j ] = m a x { d p [ i ] [ j ] , d p [ i − 1 ] [ j ] + a [ i ] , d p [ k ] [ j − 1 ] + a [ i ] } , j − 1 ≤ k ≤ i − 1 dp[i][j]=max\{dp[i][j],dp[i-1][j]+a[i],dp[k][j-1]+a[i]\},j-1\leq k\leq i-1 dp[i][j]=max{ dp[i][j],dp[i1][j]+a[i],dp[k][j1]+a[i]},j1ki1
    程序如下
while(cin >> m >> n){
    
    
	vector<int> a(n + 1);
	for(int i=1;i<=n;i++) cin >> a[i];
	vector<vector<int> > dp(n + 1, vector<int> (m + 1));
	for(int i=1;i<=n;i++){
    
    
		for(int j=1;j<=m;j++){
    
    
			for(int k=j-1;k<i;k++){
    
    
				dp[i][j] = max({
    
    dp[i][j], dp[i - 1][j] + a[i], dp[k][j - 1] + a[i]});
			}
		}
	}
	int ans = INT_MIN;
	for(int i=m;i<=n;i++){
    
    
		ans = max(ans, dp[i][m]);
	}
	cout << ans << '\n';
}
  • 但是这样时间复杂度是 O ( n 2 m ) O(n^2m) O(n2m)的,对于 1 e 6 1e6 1e6 n n n显然不行

压时间

考虑优化,式子中的 d p [ k ] [ j − 1 ] dp[k][j-1] dp[k][j1]是在找上一层的最大值,我们可以在上层遍历的时候就把它找到并用一个数组记录下来,这样可以压掉这一层 O ( n ) O(n) O(n)的遍历,优化如下

#include <bits/stdc++.h>

using namespace std;

int main(){
    
    
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int m, n;
	while(cin >> m >> n){
    
    
		vector<int> a(n + 1);
		for(int i=1;i<=n;i++) cin >> a[i];
		vector<vector<int> > dp(n + 1, vector<int> (m + 1));
		vector<int> dp2(n + 1);
		int val = INT_MIN;
		for(int j=1;j<=m;j++){
    
    		
			val = INT_MIN;
			for(int i=j;i<=n;i++){
    
    
				dp[i][j] = max({
    
    dp[i][j], dp[i - 1][j] + a[i], dp2[i - 1] + a[i]});
				dp2[i - 1] = val;
				val = max(dp[i][j], val);
			}
		}
		cout << val << '\n';
	}
	return 0;
}
  • 注意这里因为没有存储中间状态,不能保证答案的合法性,所以必须在中间更新答案,最后一次循环由于 j j j达到 m m m,这一次循环出来就是最终答案,如果最后更新, i i i需要从 m m m开始,因为最少需要 m m m个数才能构成 m m m
  • 但是没有给出 m m m的范围,所以这样超空间也不行

压空间

  • 我们或许能够发现,方程中 j j j并没有什么作用,因为都是从前一个状态转移过来的,正序更新即可
#include <bits/stdc++.h>

using namespace std;

int main(){
    
    
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, m;
    while(cin >> m >> n){
    
    
        vector<int> a(n + 1);
        vector<int> dp1(n + 1);
        vector<int> dp2(n + 1);
        for(int i=1;i<=n;i++) cin >> a[i];
        int val = INT_MIN;
        for(int i=1;i<=m;i++){
    
    
            val = INT_MIN;
            for(int j=i;j<=n;j++){
    
    
                dp1[j] = max(dp1[j - 1] + a[j], dp2[j - 1] + a[j]);
                dp2[j - 1] = val;
                val = max(val, dp1[j]);
            }
        }
		// int ans = INT_MIN;
        // for(int i=m;i<=n;i++){//如果如此统计答案,需要从m开始
		// 	ans = max(ans, dp1[i]);
		// }//也可,因为dp1[i]含义是前i个数以i结尾分成m组的最大和
		cout << val << '\n';
    }
    return 0;
}

这样时间复杂度是 O ( n m ) O(nm) O(nm),空间复杂度是 O ( n ) O(n) O(n),这样就没什么问题了

最大双子段和

https://www.luogu.com.cn/problem/P2642
在一个序列里面找到两段连续的子段,让它们加和最大,这两个子段不能连成一个子段

  • 这个问题和上一个问题有相似的地方,它可以拆解成单个子问题,也就是分别找两个最大子段和,让它们加和最大,所以一个比较明显的思路是,我哦们找一个前缀最大子段和,再找一个后缀最大子段和,从前到后枚举中间点,更新最大值,前缀和后缀是独立的,因为它们不相交,所以能够容易的写出程序
  • 但是需要注意的是最好单独考虑首尾,或者更新前后缀最大子段和的时候从2开始更新
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
int main(){
    
    
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<ll> a(n + 1);
    for(int i=1;i<=n;i++){
    
    
        cin >> a[i];
    }
    vector<ll> pre(n + 1), suff(n + 2);
    for(int i=1;i<=n;i++){
    
    
        if(pre[i - 1] > 0){
    
    
            pre[i] = pre[i - 1] + a[i];
        }else{
    
    
            pre[i] = a[i];
        }
    }
    for(int i=2;i<=n;i++) pre[i] = max(pre[i - 1], pre[i]);
    for(int i=n;i>=1;i--){
    
    
        if(suff[i + 1] > 0){
    
    
            suff[i] = suff[i + 1] + a[i];
        }else{
    
    
            suff[i] = a[i];
        }
    }
    for(int i=n-1;i>=1;i--) suff[i] = max(suff[i + 1], suff[i]);
    ll ans = pre[1] + suff[3];
    for(int i=2;i<n;i++){
    
    
        ans = max(ans, pre[i - 1] + suff[i + 1]);
    }
    cout << ans;
    return 0;
}

这是一种比较巧妙的方法,但是如果考虑到分成的段数增多,那就不行了,所以这种方法不具有普适性;但是这题和上一个分成 m m m段的又不一样,因为那个是随便分没有重叠即可,这个问题要求子段之间不能连成一个区域

  • 还有一种方法,设 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示前 i i i个数分成 j j j组, k k k表示选不选第 i i i个数, k ∈ [ 0 , 1 ] k\in[0,1] k[0,1],那么状态转移方程应该是 d p [ i ] [ j ] [ 0 ] = m a x ( d p [ i − 1 ] [ j ] [ 0 ] , d p [ i − 1 ] [ j ] [ 1 ] ) dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]) dp[i][j][0]=max(dp[i1][j][0],dp[i1][j][1]) d p [ i ] [ j ] [ 1 ] = m a x ( d p [ i − 1 ] [ j − 1 ] [ 0 ] , d p [ i − 1 ] [ j ] [ 1 ] ) + a [ i ] dp[i][j][1]=max(dp[i-1][j-1][0],dp[i-1][j][1])+a[i] dp[i][j][1]=max(dp[i1][j1][0],dp[i1][j][1])+a[i]
  • 关于初始状态,显然首先需要全初始化为 − ∞ -\infty ,只有一个数的时候,如果分成一组,那么有 d p [ 1 ] [ 1 ] [ 1 ] = a [ 1 ] dp[1][1][1]=a[1] dp[1][1][1]=a[1],如果不分组,那么有 d p [ 1 ] [ 0 ] [ 0 ] = 0 dp[1][0][0]=0 dp[1][0][0]=0,所以程序如下
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int MAXN = 1e6 + 5;
ll dp[MAXN][3][3];
int main(){
    
    
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n;
	cin >> n;
	vector<ll> a(n + 1);
	for(int i=1;i<=n;i++) cin >> a[i];
	memset(dp, -0x3f, sizeof dp);
	dp[1][1][1] = a[1];
	dp[1][0][0] = 0;
	for(int i=2;i<=n;i++){
    
    
		for(int j=0;j<=2;j++){
    
    
			dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1]);
			if(j > 0)
			dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0]) + a[i];
		}
	}
	ll ans = max(dp[n][2][0], dp[n][2][1]);
	cout << ans;
	return 0;
}
  • 用上述方法,我们就能够求出 m m m段的最大子段和了

环状最大子段和

https://nanti.jisuanke.com/t/A2232
最大子段和的环状版本

  • 对于一个环状序列,通常的做法是数组翻倍,但是对于这个问题,并不应该这样做,因为环状最大子段和要么越过边界,要么不越过边界,如果没有越过边界,那么求一个最大子段和即可,如果越过了边界,我们需要看什么时候最大,那么这显然求原序列的最小子段和即可,因为我们找到了最小子段和,只要计算出序列总和,那么剩下的部分加和肯定是最大的,打印方案都搞得定
  • 最小子段和和最大子段和是一个道理,所以很容易写出程序
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int main(){
    
    
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n;
	cin >> n;
	vector<ll> a(n + 1);
	ll sum = 0;
	for(int i=1;i<=n;i++){
    
    
		cin >> a[i];
		sum += a[i];
	}
	vector<ll> dp1(n + 1), dp2(n + 1);
	for(int i=1;i<=n;i++) dp1[i] = max(dp1[i - 1] + a[i], a[i]);
	for(int i=2;i<=n;i++) dp1[i] = max(dp1[i - 1], dp1[i]);
	for(int i=1;i<=n;i++) dp2[i] = min(dp2[i - 1] + a[i], a[i]);
	for(int i=2;i<=n;i++) dp2[i] = min(dp2[i - 1], dp2[i]);
	cout << max(dp1[n], sum - dp2[n]);
	return 0;
}

环状最大双子段和

https://www.luogu.com.cn/problem/P1121
有下面这两种情况
1 ◯ ∗ ∗ ∗ ∗ 1111 ∗ ∗ ∗ ∗ 22222 ∗ ∗ ∗ ∗ \text{\textcircled 1}****1111****22222**** 1111122222
2 ◯ 111 ∗ ∗ ∗ ∗ 2222 ∗ ∗ ∗ ∗ ∗ ∗ 11111 \text{\textcircled 2}111****2222******11111 2111222211111

  • 第一种情况只需要跑一次前缀最大子段和和后缀最大子段和枚举断点即可;第二种情况求最小前缀子段和和最小后缀子段和,然后用总和减掉两段最小。这样的思路和环状最大子段和是一样的,但是这样有可能出现下面的情况
    1111 ∗ 222222111111 1111*222222111111 1111222222111111或者这样 111222222222221111 111222222222221111 111222222222221111
  • 在这两种情况下我们如果按照 2 ◯ \text{\textcircled 2} 2来进行,那么就有可能只取一个数或者不取数,这样肯定是不对的,那为什么会出现这两种情况呢?容易想到是因为只有一个正数或者没有正数,所以在这两种情况之下我们不讨论 2 ◯ \text{\textcircled 2} 2,只选择 1 ◯ \text{\textcircled 1} 1即可
#include <bits/stdc++.h>

using namespace std;

int main(){
    
    
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin >> n;
    vector<int> a(n + 1);
    int num = 0;
    int sum = 0;
    for(int i=1;i<=n;i++){
    
    
        cin >> a[i];
        sum += a[i];
        if(a[i] > 0) num += 1;
    }
    function<int()> ask = [&](){
    
    
        int res = INT_MIN;
        vector<int> pre(n + 1), suff(n + 2);
        for(int i=1;i<=n;i++) pre[i] = max(pre[i - 1] + a[i], a[i]);
        for(int i=2;i<=n;i++) pre[i] = max(pre[i - 1], pre[i]);
        for(int i=n;i>=1;i--) suff[i] = max(suff[i + 1] + a[i], a[i]);
        for(int i=n-1;i>=1;i--) suff[i] = max(suff[i + 1], suff[i]);
        for(int i=1;i<n;i++){
    
    
            res = max(res, pre[i] + suff[i + 1]);
        }
        return res;
    };
    int ans = ask();
    if(num == 1 || num == 0){
    
    
        cout << ans;
        return 0;
    }
    for(int i=1;i<=n;i++) a[i] *= -1;
    ans = max(ans, ask() + sum);
    cout << ans;
    return 0;
}

猜你喜欢

转载自blog.csdn.net/roadtohacker/article/details/121196386