当前位置: 首页 > news >正文

信奥赛-刷题笔记-前缀和篇-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 个水壶的水量之和// 注意边界条件:不能超过nfor (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;
}

相关文章:

  • buildroot学习
  • 掌握 npm 核心操作:从安装到管理依赖的完整指南
  • 似然分布与共轭分布,算是补作业吧
  • 《数据结构笔记三》:单链表(创建、插入、遍历、删除、释放内存等核心操作)
  • C语言数据结构
  • 【mindspore系列】- 算子源码分析
  • Spring IoC容器初始化过程
  • Mysql索引的数据结构
  • CMA软件实验室体系建设中的测试方法部分
  • 内网渗透——红日靶场四
  • 【知识点】关于vue3中markRow、shallowRef、shallowReactive的了解
  • 【办公类-18-06】20250523(Python)“口腔检查涂氟信息”批量生成打印(学号、姓名、学校、班级、身份证、户籍、性别、民族)
  • 【AS32X601驱动系列教程】GPIO_按键检测详解
  • DDR DFI 5.2 协议接口学习梳理笔记01
  • 基于SpringBoot+Vue的足球青训俱乐部管理后台系统的设计与开发
  • rac-rac dg 用role自动启动service
  • leetcode hot100:十三、解题思路大全:多维动态规划(不同路径、最小路径和、最长回文子串、 最长公共子序列、编辑距离)
  • NHANES指标推荐:MHR
  • 101 alpha_59
  • LaTeX中所有数字都应该在数学环境中吗?
  • 深圳企业网站建设报价/优化网络的软件
  • 利用CSS修改Wordpress/合肥seo排名公司
  • 深圳网站建设 东莞网站建设/软文营销代理
  • asp.net个人网站怎么做/微信销售平台
  • 上海做电缆桥架的公司网站/今日国际新闻摘抄
  • 如何做网站demo/百度爱采购平台登录