子段和問題

carboxylBase發表於2024-07-31

Abstract

本文主要介紹各種序列子段和問題。


P1 最大子段和

傳送門

Introduction

首先來看一道經典例題,求一段序列的最大子段和

Idea

考慮動態規劃,令 dp[i] 表示在取第 i 個數的情況下,前 i 個數所能得到的最大子段和,那麼顯然有 dp[i] = max( dp[i-1] + a[i] , a[i]),其中 a[i] 表示序列的第 i 個數。

Code

#include <bits/stdc++.h>
using namespace std;

int dp[300000];
int ans = -INT_MAX;
int main()
{
    int n;
    cin >> n;
    memset(dp, -0x3f3f3f, sizeof dp);
    for (int i = 1; i < n + 1; i++)
    {
        int m;
        cin >> m; 
        dp[i] = max(dp[i - 1] + m, m);
        ans = max(ans, dp[i]); // 注意答案每次都要更新
    }
    cout << ans;
    return 0;
}

P2 雙子序列最大和

傳送門

Introduction

再看一道升級版的,這次我們需要找出兩個不重疊的子段,使他們的各自的子段和之和達到最大值。

Idea

一個顯然的想法是,我們可以依次列舉序列中的每一項,然後分別找出這一項右側和左側的最大子段和,然後將他們相加即可。這樣一來,問題就變得和 P1 極其相似了。

我們不妨以 l[i] 表示第 i 項左邊的數可以得到的最大子段和,r[i] 表示第 i 項右邊的數可以得到的最大子段和,答案就是 max( l[i] + r[i] ), i = 2,3...n-1。

下面考慮如何計算 l[i] 和 r[i] , 和 P1 類似的,我們可以寫出以下轉移方程 l[i] = max( l[i-1] + a[i-1], a[i-1]), r[i] = max ( r[i+1] + a[i+1] , a[i+1]),這樣一來,我們就成功得到了選取第 i-1/i+1 個數時,第 i 個數左側/右側的數所能取得的最大子段和,然後,我們對 l,r 陣列做以下處理:l[i] = max( l[i] , l[i-1] ),r[i] = max( r[i] , r[i+1] ),這樣一來,就將 l,r 陣列轉變為我們需要的東西了。

Code

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 1100000;
int l[maxn];
int r[maxn];
int a[maxn];

signed main()
{
    int n;
    scanf("%lld", &n);
    memset(l, -0x3f3f3f3f, sizeof l);
    memset(r, -0x3f3f3f3f, sizeof r);
    l[1] = l[0] = 0;
    r[n] = r[n + 1] = 0;
    scanf("%lld ", &a[1]);
    for (int i = 2; i < n + 1; i++)
    {
        scanf("%lld", &a[i]);
        l[i] = max(l[i - 1] + a[i - 1], a[i - 1]);
    }

    for (int i = n - 1; i > 0; i--)
    {
        r[i] = max(r[i + 1] + a[i + 1], a[i + 1]);
    }

    for (int i = n - 1; i > 0; i--)
    {
        r[i - 1] = max(r[i - 1], r[i]);
    }

    for (int i = 2; i < n + 1; i++)
    {
        l[i + 1] = max(l[i + 1], l[i]);
    }

    int ans = -0x3f3f3f3f;
    for (int i = 2; i < n; i++)
    {
        ans = max(l[i] + r[i], ans);
    }
    cout << ans;
    return 0;
}

P3 環狀最大兩段子段和

傳送門

Introduction

這一題是 P2 的進階版本,怎樣在環形序列中找到子段和最大的兩端不重疊序列呢?

Idea

實際上,可能的情況只有兩種(0表示不選,@表示選):

  1. 000@@@000@@@000
  2. @@@000@@@000@@@

第一種情況和 P2 是完全一致的,第二種情況看似有些變化,但仔細一想,選出和最大的 @ 和選出和最小的 0 好像是等價的啊!那麼我們只需要把序列中的數全部都取相反數,然後再重複 P2 的操作就可以了,此題得解。

Code

#include <bits/stdc++.h>
using namespace std;

int n;
int a[300000];
int dp_l[300000];
int dp_r[300000];
int sum;
bool ok;
int main()
{
    scanf("%d", &n);
    for (int i = 1; i < n + 1; i++)
    {
        scanf("%d", a + i);
        if (a[i] > 0)
        {
            ok = 1;
        }
        sum += a[i];
    }
    // 如果全是負數的話,我們只需要把最大的兩個負數之和輸出就可以了
    // 我不知道為啥程式會在這地方出問題所以加了特判qwq
    if (!ok)
    {
        sort(a + 1, a + 1 + n);
        cout << a[n] + a[n - 1] << endl;
        return 0;
    }

    // 階段一
    for (int i = 1; i <= n; i++)
    {
        dp_l[i] = max(dp_l[i - 1] + a[i], a[i]);
    }
    for (int j = n; j > 0; j--)
    {
        dp_r[j] = max(dp_r[j + 1] + a[j], a[j]);
    }
    // 階段二
    for (int i = 1; i <= n; i++)
    {
        dp_l[i] = max(dp_l[i], dp_l[i - 1]);
    }
    for (int i = 1; i <= n; i++)
    {
        dp_r[i] = max(dp_r[i], dp_r[i + 1]);
    }
    // 更新答案
    int ans1 = -INT_MAX;
    for (int i = 2; i <= n; i++)
    {
        ans1 = max(ans1, dp_l[i - 1] + dp_r[i]);
    }

    // 全部取反
    for (int i = 1; i <= n; i++)
    {
        a[i] *= -1;
    }
    // 階段一
    for (int i = 1; i <= n; i++)
    {
        dp_l[i] = max(dp_l[i - 1] + a[i], a[i]);
    }
    for (int j = n; j > 0; j--)
    {
        dp_r[j] = max(dp_r[j + 1] + a[j], a[j]);
    }
    // 階段二
    for (int i = 1; i <= n; i++)
    {
        dp_l[i] = max(dp_l[i], dp_l[i - 1]);
    }
    for (int i = 1; i <= n; i++)
    {
        dp_r[i] = max(dp_r[i], dp_r[i + 1]);
    }
    int ans2 = -INT_MAX;
    for (int i = 2; i <= n; i++)
    {
        ans2 = max(ans2, dp_l[i - 1] + dp_r[i]);
    }
    // 把 ans2 變換為我們需要的東西
    ans2 *= -1;
    ans2 = sum - ans2;

    // 輸出 ans1,ans2 中較大的
    cout << max(ans1, ans2);
    return 0;
}

Postscript

一些類似的題目:

LeeCode 918. 環形子陣列的最大和

相關文章