信奥赛-刷题笔记-前缀和篇-T2-P6568[NOI Online #3 提高组] 水壶0523

​# 总题单

本部分总题单如下

【腾讯文档】副本-CSP-JS+NOI 题单 (未完待续)

https://docs.qq.com/sheet/DSmJuVXR4RUNVWWhW?tab=BB08J2

前缀和篇题单

在这里插入图片描述

P6568 [NOI Online #3 提高组] 水壶

题目描述

n n n 个容量无穷大的水壶,它们从 1 ∼ n 1\sim n 1n 编号,初始时 i i i 号水壶中装有 A i A_i Ai 单位的水。

你可以进行不超过 k k k 次操作,每次操作需要选择一个满足 1 ≤ x ≤ n − 1 1\le x\le n-1 1xn1 的编号 x x x,然后把 x x x 号水壶中的水全部倒入 x + 1 x+1 x+1 号水壶中。

最后你可以任意选择恰好一个水壶,并喝掉水壶中所有的水。现在请你求出,你最多能喝到多少单位的水。

输入格式

第一行一个正整数 n n n,表示水壶的个数。

第二行一个非负整数 k k k,表示操作次数上限。

第三行 n n n 个非负整数,相邻两个数用空格隔开,表示水壶的初始装水量 A 1 A_1 A1, A 2 A_2 A2, ⋯ \cdots , A n A_n An

输出格式

一行,仅一个非负整数,表示答案。

输入输出样例 #1

输入 #1

10
5
890 965 256 419 296 987 45 676 976 742

输出 #1

3813

说明/提示

数据规模与约定
  • 对于 10 % 10\% 10% 的数据,保证 n ≤ 10 n \leq 10 n10
  • 对于 30 % 30\% 30% 的数据,保证 n ≤ 100 n \leq 100 n100
  • 对于 50 % 50\% 50% 的数据,保证 n ≤ 10 3 n \leq 10^3 n103
  • 对于 70 % 70\% 70% 的数据,保证 n ≤ 10 5 n \leq 10^5 n105
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 10 6 1\leq n\leq 10^6 1n106 0 ≤ k ≤ n − 1 0\leq k \leq n-1 0kn1 0 ≤ A i ≤ 10 3 0\le A_i\le 10^3 0Ai103

代码1-贪心模拟-是错误的答案啊

#include<bits/stdc++.h>

using namespace std;

int n,k,sh[1000010];
int main(){
	// 输入水壶初始值 
	cin>>n>>k; 
	for(int i=1;i<=n;i++){
		cin>>sh[i];
	}
//	for(int i=1;i<=n;i++){
//		cout<<sh[i]<<" ";
//	}	
	// 每次找出连续两个和最大的值,累加起来
	for(int i=1;i<=k;i++){
		int temp=-1;
		int x1,x2;
		// 找出一个最合适的操作 ,即当前的值与下一个值的和最大
		for(int j=1;j<=n-1;j++){
			if(temp<sh[j]+sh[j+1]){
				temp=sh[j]+sh[j+1];
				x1=j,x2=j+1;
			}
		} 
		// 找到最合适的下表后,累加修改水壶数组 
		sh[x2] += sh[x1];
		sh[x1] = 0;
		
	} 
	int temp=-1;
	// 输出最大值
	for(int i=1;i<=n;i++){
		if(temp<sh[i]) temp=sh[i];
	} 
	cout<<temp;
	
	return 0;
}

你的代码思路是:每次操作选择相邻两个水壶,把前一个倒入后一个中,然后重复这个贪心策略 k k k 次。最后取最大值作为答案。

乍一看似乎合理,但其实 贪心策略并不总是最优的。我们来详细分析你为什么错了。


🚫 你的做法的问题

1. 贪心局部最优 ≠ 全局最优

你每一步都试图选择当前相邻两个数之和最大的位置进行合并,以为这样最终能得到最大值。

但问题在于:

  • 合并顺序会影响后续的选择。
  • 某些“暂时看起来小”的合并,可能在后面带来更大的收益。
✅ 示例说明:

比如:

n = 3
k = 2
A = [1, 100, 1]
如果你第一次选 (1+100),变成 [0,101,1],第二次选 (101+1),得到 [0,0,102],最终结果是 102。

✅ 正确做法是可行的。

但如果数据是:
n = 4
k = 2
A = [1, 99, 100, 1]

你第一步可能会合并 99+100=199,变成 [1, 0, 199, 1]

第二步合并 199+1=200,最终最大是 200。

但是如果我们先合并 1+99=100,变成 [0, 100, 100, 1],再合并 100+100=200,也能达到 200。

所以这组数据没问题。


❌ 然而下面这个例子会直接击溃你的贪心逻辑:
n = 5
k = 2
A = [100, 1, 1, 1, 100]

你第一次会合并中间的 1+1=2,变成 [100,1,0,2,100]

第二次合并 2+100=102,变成 [100,1,0,0,102],最终最大是 102。

但实际上,我们可以这样做:

  1. 合并第一个 100 → 得到 [0,101,1,1,100]
  2. 再合并 101 → 得到 [0,0,102,1,100]

最大是 102。

✅ 但还有更优解法吗?

当然有!

更优路径:
  1. 合并第四个 1 → 第五个 100(即右边)[100,1,1,0,101]
  2. 合并第二个 1 → 第三个 1(左边)[100,0,2,0,101]
    → 最终最大是 101 和 100,不如前面。

那如果我们从两端往中间推呢?

最优解:
  1. 合并第一个 100 → 第二个 1:[0,101,1,1,100]
  2. 合并第五个 100 → 第四个 1:[0,101,1,101,0]
    → 最终最大值为 101 + 1 = 102

结论:无论怎么组合,最多只能做到 102。

但是注意:

如果你的贪心每次都选当前最大和的位置,就有可能错过某些全局更优的合并路径。


代码2-前缀和

使用前缀和的方法可以有效地解决这个问题。前缀和数组可以帮助我们快速计算任意子数组的和,从而在常数时间内完成子数组和的计算。以下是使用前缀和方法来实现该题目的完整代码,并附有详细注释。

使用前缀和的方法

  1. 构建前缀和数组prefix_sum[i] 表示从第一个水壶到第 i 个水壶的总水量。
  2. 计算子数组和:对于长度为 k+1 的子数组 [j, j+k],其和可以通过 prefix_sum[j+k+1] - prefix_sum[j] 快速得到。
  3. 遍历所有可能的子数组:寻找最大值。

完整代码实现

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

const int MAXN = 1e6 + 10;
long long A[MAXN], prefix_sum[MAXN];

int main() {
    int n, k;
    cin >> n >> k;  // 输入水壶数量n和最多操作次数k

    // 初始化前缀和数组
    prefix_sum[0] = 0;

    // 读取每个水壶的初始水量并构建前缀和数组
    for (int i = 1; i <= n; ++i) {
        cin >> A[i];
        prefix_sum[i] = prefix_sum[i - 1] + A[i];
    }

    /*
     * 示例输入:
     * 10
     * 5
     * 890 965 256 419 296 987 45 676 976 742
     */

    long long max_sum = 0;

    // 遍历所有可能的长度为 k+1 的子数组,找到其中的最大和
    for (int i = 0; i + k + 1 <= n; ++i) {
        long long current_sum = prefix_sum[i + k + 1] - prefix_sum[i];
        if (current_sum > max_sum) {
            max_sum = current_sum;
        }
    }

    // 输出最终能够喝到的最大水量
    cout << max_sum << endl;

    return 0;
}

程序将执行以下步骤:

  1. 构建前缀和数组:

    prefix_sum = [0, 890, 1855, 2111, 2530, 2826, 3813, 3858, 4534, 5510, 6252]
    
  2. 遍历所有可能的长度为 k+1=6 的子数组,计算它们的和,并更新最大值:

    • 子数组 [1, 6] 的和:prefix_sum[6] - prefix_sum[0] = 3813 - 0 = 3813
    • 子数组 [2, 7] 的和:prefix_sum[7] - prefix_sum[1] = 3858 - 890 = 2968
    • 最终最大值为 3813

因此,输出结果为 3813

这种方法的时间复杂度为 O(n),因为我们只需遍历数组两次(一次构建前缀和数组,一次遍历查找最大子数组和)。空间复杂度也是 O(n),用于存储前缀和数组。

代码3-双指针

双指针方法解析

双指针方法非常适合解决这类需要遍历数组并计算子数组和的问题。其核心思想是通过两个指针(窗口的左右边界)来维护一个动态的窗口,并随着指针的移动更新窗口内的元素总和。

算法思想:
  1. 初始化:定义两个指针 leftright,分别表示当前窗口的左边界和右边界。
  2. 滑动窗口
    • 开始时,leftright 都指向数组的第一个元素。
    • 每次将 right 向右移动一格,同时增加窗口内的元素总和。
    • 当窗口大小超过 k+1 时,将 left 向右移动一格,同时减少窗口内移除的元素值。
  3. 更新最大值:在每次调整完窗口后,检查当前窗口内的元素总和是否为最大值。
  4. 终止条件:当 right 到达数组末尾时,算法结束。

这种方法可以确保我们以线性时间复杂度 O(n) 完成任务,因为每个元素只会被访问两次(一次由 right 指针访问,一次由 left 指针访问)。


双指针实现代码

以下是基于上述思想的完整代码实现,并附有详细注释。

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

const int MAXN = 1e6 + 10;
int A[MAXN];  // 存储每个水壶初始水量

int main() {
    int n, k;
    cin >> n >> k;  // 输入水壶数量n和最多操作次数k

    // 读取每个水壶的初始水量
    for (int i = 1; i <= n; ++i) {
        cin >> A[i];
    }

    long long sum = 0;  // 当前窗口内的水量总和
    int left = 1, right = 1;  // 初始化双指针

    long long max_sum = 0;  // 记录最大水量

    while (right <= n) {
        // 将当前元素加入窗口
        sum += A[right];

        // 如果窗口大小超过了 k+1,则移动左指针缩小窗口
        while (right - left + 1 > k + 1 && left <= right) {
            sum -= A[left];
            left++;
        }

        // 更新最大水量
        if (right - left + 1 == k + 1) {  // 只有当窗口大小正好为 k+1 时才更新最大值
            max_sum = max(max_sum, sum);
        }

        // 移动右指针扩大窗口
        right++;
    }

    // 输出最终能够喝到的最大水量
    cout << max_sum << endl;

    return 0;
}

代码说明

  • 输入部分:读入水壶的数量 n 和最大操作次数 k,以及每个水壶的初始水量。
  • 初始化sum 用于存储当前窗口内的元素总和,leftright 分别作为窗口的左右边界。
  • 循环体
    • 使用 right 指针扩展窗口,直到遍历完整个数组。
    • 每次扩展窗口时,如果窗口大小超过了 k+1,则通过移动 left 指针缩小窗口,直到窗口大小不超过 k+1
    • 在每次窗口大小正好为 k+1 时,更新最大水量 max_sum
  • 输出:输出找到的最大水量。

示例运行

假设输入如下:

10
5
890 965 256 419 296 987 45 676 976 742

程序将执行以下步骤:

  1. 初始化 sum=0, left=1, right=1, max_sum=0
  2. 开始滑动窗口:
    • right=1: sum=890
    • right=2: sum=1855
    • right=6 时,sum=3813,此时窗口大小为 k+1=6,更新 max_sum=3813
    • 继续移动 right,直到遍历完整个数组。
  3. 最终输出结果为 3813

这种方法的时间复杂度为 O(n),因为我们只遍历了数组一次。空间复杂度为 O(1),因为我们只使用了常数级别的额外空间。

代码4-滑动窗口/双指针

✅ 正确做法是什么?

这个问题是一个经典的 滑动窗口/双指针 类型的题目。

正确思路如下:
  • 我们可以将最多连续 $k+1$ 个元素合并成一个水壶(因为最多能通过 $k$ 次操作把 $k$ 个相邻水壶的内容合并到一个水壶里)。
  • 所以我们要找出长度为 $k+1$ 的连续子数组,使得它们的和最大。

举个例子:

如果 $k = 3$,那么最多可以把 3 次操作用来把 4 个相邻水壶的水全部倒进某一个里面。

所以我们只需要找所有长度为 $k+1$ 的子数组的最大和即可。

✅ 时间复杂度:
  • 使用滑动窗口法可以在 O ( n ) O(n) O(n) 时间内完成。

✅ AC 版本代码如下:

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

// 最大数组容量,防止越界
const int MAXN = 1e6 + 10;
int A[MAXN];  // 存储每个水壶初始水量,下标从1开始

int main() {
    int n, k;
    cin >> n >> k;  // 输入水壶数量n和最多操作次数k

    // 读取每个水壶的初始水量,注意我们从下标1开始存储
    for (int i = 1; i <= n; ++i) {
        cin >> A[i];
    }

    /*
     * 示例输入:
     * 10
     * 5
     * 890 965 256 419 296 987 45 676 976 742
     *
     * 我们要找的是:最多能通过 k 次操作把连续的 k+1 个水壶合并成一个的最大值。
     * 因此我们需要找出长度为 k+1 的最大子数组和。
     */

    long long sum = 0;  // 当前窗口内的水量总和

    // 初始时,计算第一个长度为 k+1 的窗口的水量总和
    // 即前 k+1 个水壶的水量之和
    // 注意边界条件:不能超过n
    for (int i = 1; i <= k + 1 && i <= n; ++i) {
        sum += A[i];
    }

    long long max_sum = sum;  // 初始化最大水量为当前窗口的水量

    // 使用滑动窗口向右移动
    // 每次将窗口右边的一个元素加入窗口,并移除最左边的一个元素
    for (int i = k + 2; i <= n; ++i) {
        // 窗口滑动:加入右边新元素 A[i],移除左边旧元素 A[i - k - 1]
        sum += A[i] - A[i - k - 1];

        // 更新最大水量
        if (sum > max_sum) {
            max_sum = sum;
        }
    }

    // 输出最终能够喝到的最大水量
    cout << max_sum << endl;

    return 0;
}

🔁 总结

你的做法错误原因
贪心地每次合并相邻两数之和最大的位置局部最优不等于全局最优,无法保证最终得到最大值
正确做法原理
滑动窗口法,找出长度为 $k+1$ 的最大子数组和因为最多能通过 $k$ 次操作把 $k+1$ 个水壶合并成一个

现场真题注意事项

https://cspoj.com/contest.php?cid=1002Fus5yz4x3EcSJH1Z

注意事项

文件名(程序名和输入输出文件名)必须使用英文小写。(提交必须使用freopen()进行提交)

C/C++ 中函数 main() 的返回值类型必须是 int,程序正常结束时的返回值必须是0。

提交的程序代码文件的放置位置请参考各省的具体要求。

因违反以上三点而出现的错误或问题,申述时一律不予受理。

若无特殊说明,结果的比较方式为全文比较(过滤行末空格及文末回车)。

程序可使用的栈空间内存限制与题目的内存限制一致。

全国统一评测时采用的机器配置为:Inter® Core™ i7-8700K CPU @3.70GHz,内存 32GB。上述时限以此配置为准。

只提供 Linux 格式附加样例文件。

评测在当前最新公布的 NOI Linux 下进行,各语言的编译器版本以此为准

假设输入样例数据存在文件test.in中,输出样例数据存在文件test.out中,

则在CSP、NOI等比赛的代码中,需添加freopen、fclose语句,

内容详见模板代码如下。

#include <bits/stdc++.h>
#include<cstdio>//必须包含cstdio头文件
#include<iostream>
using namespace std;
 
int main(){
	freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);
	
	cout<<"Hello NOI"<<endl;
	
	fclose(stdin);
	fclose(stdout);
	
	return 0;
}

复制

下面为函数的简介,详细可参见 http://www.cplusplus.com/reference/clibrary/cstdio/freopen.html

函数名:freopen

声明:

FILE _freopen( const char_ path, const char _mode, FILE_ stream );

所在文件: stdio.h

参数说明:

path: 文件名,用于存储输入输出的自定义文件名。

mode: 文件打开的模式。和fopen中的模式(如r-只读, w-写)相同。

stream: 一个文件,通常使用标准流文件。

返回值:成功,则返回一个path所指定文件的指针;失败,返回NULL。(一般可以不使用它的返回值)

功能:实现重定向,把预定义的标准流文件定向到由path指定的文件中。标准流文件具体是指stdin、stdout和stderr。其中stdin是标准输入流,默认为键盘;stdout是标准输出流,默认为屏幕;stderr是标准错误流,一般把屏幕设为默认。通过调用freopen,就可以修改标准流文件的默认值,实现重定向。

#include<iostream>
#include<cstdio>
using namespace std;
int main(){
    freopen("7532.in", "r", stdin);
    freopen("7532.out", "w", stdout);
    //原来的代码保持不变
    double a, b, r;
    int k;
    cin >> a >> b;
    k = int(a/b);
    r = a - b * k;
    printf("%g", r);
    //-------------
    fclose(stdin);
    fclose(stdout);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT从业者张某某

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值