2107C - Maximum Subarray Sum

2107C - Maximum Subarray Sum

一、问题描述

​ 给定一个数组,其中某些位置的值可以替换为任意整数。要求确定是否存在一种替换方式,使得替 换后的数组的最大子数组和恰好等于给定的整数 k。如果存在,输出 "YES",否则输出 "NO"

输入:
int n,k;               cin>>n>>k;			    //第一行输入  数组长度n   需要的最大值数组和k
string s;              cin>>s;				    //第二行输入  长度为n的字符串s 
vector<int> a(n);      for(int i=0;i<n;i++) cin>>a[i];      //第三行输入数组
//字符串s由'0'和'1'组成,s[i]='0'意思为a[i]未知且可变,s[i]='1'意思为a[i]已知且不可变。
输出:
cout<<"NO"<<endl;
//或者
cout<<"YES"<<endl;
for(int i:a) cout<<a<<" ";
cout<<endl;

二、测试样例

输入1:
10


3 5
011
0 0 1


5 6
11011
4 -3 0 -2 1


4 4
0011
0 0 -4 -5


6 12
110111
1 2 0 5 -1 9


5 19
00000
0 0 0 0 0


5 19
11001
-8 6 0 0 -5


5 10
10101
10 0 10 0 10


1 1
1
0


3 5
111
3 -1 3


4 5
1011
-2 0 1 -5

输出1:
Yes
4 0 1
Yes
4 -3 5 -2 1
Yes
2 2 -4 -5
No
Yes
5 1 9 2 2
Yes
-8 6 6 7 -5
Yes
10 -20 10 -20 10
No
Yes
3 -1 3
Yes
-2 4 1 -5

三、解题思路

  1. 初步判断

    • 若所有位置都是固定的,那么直接计算最大子数组和,判断其是否为k,若不为k则无解,输出NO

    • 否则,则计算每片固定的区域的最大子数组和是否超过k。若超过k则无解,输出NO

      点击查看例子
      比如`s`输入的是`111001111000111`
        很明显可以察觉到固定区域为开头的`111`,中间的`1111`,结尾的`111`,那么我们现在需要计算的就是这三片区域对应的最大子数组和是否超过`k`了
      • 那么,我们应该如何巧妙地计算最大子数组和呢?我们可以将数组a中所有未知(非固定)的数字设置为-1e13。从而防止最大子数组和中的“子数组”会跨过非固定点位。之后再对整个数组进行最大子数组计算即可。
      点击查看操作例子
      比如`s`输入的是`1110111`
        `a`输入的是`5 -5 7 0 13 -12 14`
        那么我们这一步就是将a设置为`5 -5 7 -10000000000000 13 -12 14`
        从而将数组`a`分成两块,此时再计算数组`a`的最大子数组和   就等价于 
        计算`max(数组{5,-5,7}的最大字数组和,数组{13,-12,14}的最大子数组和)`
  2. 可行情况分析

    • 如果上述初步判断通过,则答案一定为 "YES"。之后开始计算如何凑出k来。而因为有多个非固定点位,同时考虑较难,所以我们可以简化为只考虑最后一个非固定点位,之后只需要计算最后这个非固定点位及其附近的固定点位的最大子数组和即可。而对于这步操作,原题解有两种解法,这里提供其中一种:线性解法。

    • 点击查看操作例子
      比如a输入的是{10 ? 20 ? 30},那么我们现在只看最后一个点位以及其周围的最大子数组和
        也就是{20 ? 30}这三项的最大子数组和。
  3. 最大子数组和计算方法(重点!!!)

    1. 代码如下:

      int calculate_max_prefix_sum(int start,int end){
          int curr = 0 ;
      	for (int i = start; i <= end ; i++){
      		curr = max(curr + a[i], a[i]);
               mx = max(mx, curr);
          }
          return curr;
      }
      //原理:用例子来说明吧:
      //比如输入 1 3 -5 2 4 6 -5 3 -9 10
      //curr会分别为:1 4 -1 2 6 12 7 10 1 11 (可以动手验算一下,会有更深刻的理解)
      //在取a[3]=2时,因curr+a[i]=-1+2=1<a[i]=2,因此就舍去前面的内容,而取新的开始,则后面就以“2”为新的开始。
      //后面都没有以上这种抛弃前面的情况,但为什么最大的仍不是最后的那个数字呢?因为如果后面的数字取了,但和仍比前面的小,那么就没必要取了,所以无需从尾到头重新跑一遍,因为就算跑一遍,也会因为后面只会给予负贡献,所以把后面的给抛弃掉,像刚刚抛弃{1,3,-5}一样,抛弃后缀{-5,3,-9,10}。
      //所以最大为12
      
  4. 线性解法

    1. 我们现在将最后一个非固定点位命名为:pos,它左侧的最大前缀和 以及 最大后缀和分别为 bc

    2. 计算 pos 后侧的最大前缀和 b 和前侧的最大后缀和 c(注意,是后侧的前缀和 & 前侧的后缀和!)

    3. pos 处的值设为 x=k−b−c,确保包含 pos 的最大和为 k。(那么这一步又是为什么呢?咱们继续看例子)

      例如输入刚刚例子的
      1  3  -5  2  4  6  -5  3  -9  10
      
      curr分别为:
      1  4  -1  2  6  12  7  10  1  11
      
      之后我们抠去一个,比如抠去4

      那么我们得到的是

      1  3  -5  2  ?  6  -5  3  -9  10
      
      之后计算前侧的后缀和
      1  0  -3  2  ?  x   x   x   x   x
      

      很明显前侧的后缀和最大值为2

      之后计算后侧的前缀和
      x   x   x   x   ?  6  1  4  -5  5
      

      很明显后侧的前缀和最大值为6

      那么这两个值分别有什么意义呢?我们拿前侧的后缀和来举例子:

      如果我取比2靠近的值,那么我们可以发现我们其实舍弃了更大的子数组可能值。因为继续远处走可能会有对最大子数组和造成正贡献的数字。

      如果我取比2远离的值,那么我们可能就取多了子数组,导致其并非最大值。因为往太远走会导致对最大子数组和造成负贡献。

      那么后侧的前缀和同理。

      其实这一步做的就是:在“?”的前后侧,从近往远找对最大子数组和贡献最大的子数组和。因为我们要确保这段数字的最大子数组和的“子数组”需要包括这个。那么很明显,我们现在的2和6分别都是左右两侧最大的贡献值了,所以对应26的那个点就是“子数组”的起点和终点了,而在其中间。(省流:找左右两侧的最大贡献值)

      下一步就是计“?

      ?= k - b - c = 12 - 2 - 6 = 4
      

      恰好补上了抠掉的4。

四、最终代码

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

int main(){
    int t; cin >> t;
    while (t--){
        int n; 
        long long k; 
        cin >> n >> k;
        string s; cin >> s;
        vector <long long> a(n);
        for (auto &x : a) cin >> x;
        
        int pos = -1;
        for (int i = 0; i < n; i++){
            if (s[i] == '0'){
                pos = i;
                a[i] = -1e13;
            }
        }
        //第一步:先将所有都设置为-1e13,并且记录最后一个“?”的点
        //设置成-1e13能保证在计算最大子数组和的时候,能够把不连续的固定点给区分开。保证计算的最大子数组的“子数组”不会跨越任何一个“?”。
        long long mx = 0;
        long long curr = 0;
        for (int i = 0; i < n; i++){
            curr = max(curr + a[i], a[i]);
            mx = max(mx, curr);
        }
        //第二步:计算当前已固定的点中是否存在本身子数组和就大于k的情况,如果固定的都大于k了,那么填入什么都于事无补了
        if (mx > k || (mx != k && pos == -1)){
            cout << "No\n";
            continue;
        }
        //如果能够设置点位
        //第三步:计算包含“?”的最大子数组和的“子数组”。并根据k推算出“?”的值。为了简化,我们只计算最后一个非固定点“?”。那么我们需要计算这个“?”附近,能对最大子数组和作出的最大的贡献值。
        if (pos != -1){
            mx = 0, curr = 0;//清零是在考虑比如{ -1 ? -1 }的情况时,最大子数组和不看x而看x-2的情况(x是“?”的值)
            long long L, R;
            for (int i = pos + 1; i < n; i++){
                curr += a[i];
                mx = max(mx, curr);
            }
            L = mx;
            //3-1:计算pos右边的最大贡献值,从而找到“?”附近的最大子数组的起点。
            mx = 0;
            curr = 0;
            for (int i = pos - 1; i >= 0; i--){
                curr += a[i];
                mx = max(mx, curr);
            }
            R = mx;
            //3-2:计算pos左边的最大贡献值,从而找到“?”附近的最大子数组的终点。
            a[pos] = k - L - R;
            //3-3:那么需要设置的点答案就是k-L-R
        }
        
        cout << "Yes\n";
        for (int i = 0; i < n; i++){
            cout << a[i] << " \n"[i + 1 == n];
        }
    }
    return 0;
}
posted @ 2025-05-08 21:31  lynx_loong  阅读(45)  评论(1)    收藏  举报