这道来自洛谷P3929的题目属于信奥竞赛中的经典序列操作题型,主要考察选手对循环控制和条件判断的掌握程度。题目要求我们对于一个给定的整数序列,通过特定操作使其变成严格单调递增序列,并计算最小操作次数。
题目描述:给定一个长度为n的整数序列a,每次操作可以选择一个位置i(1≤i≤n),将a_i加上或减去1。求使序列变成严格递增序列所需的最小操作次数。
输入格式:第一行一个整数n,第二行n个整数表示序列a。
输出格式:一个整数表示最小操作次数。
这道题的核心在于理解"严格递增"的定义和操作规则。严格递增意味着对于序列中的每一个元素a[i],都必须满足a[i] < a[i+1]。而每次操作只能对单个元素进行±1的调整,这实际上是在计算将序列调整为满足条件所需的最小步数。
关键点在于:
最直观的解法是从左到右遍历序列,确保每个元素都比前一个元素至少大1。具体来说:
这种贪心算法的正确性基于局部最优可以导致全局最优的特性。因为每个元素的调整只影响它和下一个元素的关系,不会对更前面的元素产生影响。
这个算法只需要一次线性遍历,时间复杂度是O(n),空间复杂度是O(1)(除了存储输入序列外只需要几个临时变量),对于题目给定的n≤10^5的限制完全足够。
需要特别注意几种特殊情况:
cpp复制#include <iostream>
#include <vector>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
long long operations = 0; // 使用long long防止溢出
for (int i = 1; i < n; ++i) {
if (a[i] <= a[i-1]) {
int target = a[i-1] + 1;
operations += target - a[i];
a[i] = target;
}
}
cout << operations << endl;
return 0;
}
虽然这个解法已经足够高效,但还可以做以下优化:
plaintext复制输入1:
5
1 2 2 3 4
输出1:
2
解释:将第三个元素从2调整为3(+1),第四个元素从3调整为4(+1)
输入2:
3
5 4 3
输出2:
3
解释:第二个元素4→6(+2),第三个元素3→7(+4),共6次操作
plaintext复制输入3:
1
100
输出3:
0
解释:单元素序列总是严格递增
输入4:
3
1 1 1
输出4:
3
解释:第二个元素1→2(+1),第三个元素2→3(+1)
对于n=1e5的序列,如全为相同元素的情况,验证程序是否能快速处理且不溢出:
plaintext复制输入5:
100000
1 1 1 ... 1 (共1e5个1)
输出5:
4999950000
我们的算法在每个步骤都做出局部最优选择,即用最少的操作使当前元素满足严格大于前一个元素的条件。这种选择不会影响之前已经处理好的部分,也不会使后续的选择变差。
问题的最优解包含子问题的最优解。对于序列a[1..n],如果我们已经用最优方式处理了a[1..n-1],那么处理a[n]时只需要考虑它与a[n-1]的关系即可。
基础情况:对于n=1,显然成立。
归纳假设:假设对于长度为k的序列,算法正确。
归纳步骤:对于长度为k+1的序列,前k个元素已经按算法处理,第k+1个元素的处理不会影响前k个元素的关系,且保证了a[k] < a[k+1],因此整个序列严格递增。
如果题目改为允许相等(非严格递增),解法只需将判断条件改为a[i] < a[i-1]时调整到a[i] = a[i-1]。
如果限制只能进行加法或减法操作中的一种,问题会变得复杂,可能需要不同的贪心策略或动态规划。
对于矩阵或高维数组要求每行/列都满足单调性,这类问题通常需要更复杂的全局考虑。
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
这类序列操作题在竞赛中常见特征:
在实际竞赛中遇到这类题目时,我通常会采取以下步骤:
对于这道题,我最初犯的一个错误是忽略了操作次数的累加方式,错误地认为只需要计算相邻元素的差值。后来通过测试用例发现了这个问题,修正为计算差值+1的情况。这也提醒我在竞赛中一定要自己构造各种边界测试用例来验证程序的正确性。