1 条题解
-
0
A. 全长度减法 题解
一、题意理解
给定一个长度为 的排列 。对每个 (按顺序),必须恰好选择一个长度为 的子数组,将其中所有元素减 。目标是使最终所有元素变为 。判断是否可行。
二、关键观察
2.1 最大值 的要求
元素 需要被减 次才能变为 。由于总共只有 次操作,这意味着每次操作都必须包含元素 。
因此,所有子数组的公共交集必须包含 的位置。
2.2 次大值 的要求
元素 需要被减 次。它最多被排除在一次操作之外(因为总共有 次操作,它必须参与其中 次)。
- 长度为 的子数组只会包含 (由上一步可知),因此可以排除 。
- 长度为 的子数组必须同时包含 和 ,否则 将少参与一次操作。
因此, 必须与 相邻。
2.3 一般规律
对于任意值 (从 向下考虑),它需要被减 次,因此最多被排除在 次操作之外。
- 长度为 的子数组可以排除 。
- 长度为 的子数组必须包含 。
这意味着所有值 必须包含在长度为 的子数组中,因此它们在原排列中的位置必须构成一个连续段。
2.4 等价条件
排列 可行,当且仅当满足以下条件:
对于每个 从 到 ,值 的位置构成一个连续段。
等价地,排列必须是单峰的(mountain):先递增后递减。
也就是说,从排列的两端交替取最小值,可以依次取出 。
三、算法流程
我们可以模拟以下过程:
- 设置左指针 ,右指针 。
- 对于 :
- 如果 ,说明 在左端,将 右移一位。
- 否则,如果 ,说明 在右端,将 左移一位。
- 否则,排列不是单峰的,输出
NO并返回。
- 如果循环顺利完成,输出
YES。
四、正确性证明
-
充分性:如果排列满足上述条件,我们可以按以下方式构造操作:
- 对于 ,选择整个数组减 。
- 对于 ,选择除去已归零的那一端元素后的子数组。
- 依此类推,每次从两端逐步收缩子数组。
-
必要性:已在上文通过分析每个值需要参与的操作次数证明。
五、标程解析
void solve() { int n; std::cin >> n; std::vector<int> p(n); for (int i = 0; i < n; i++) { std::cin >> p[i]; } int l = 0, r = n - 1; for (int i = 1; i <= n; i++) { if (p[l] == i) { l++; } else if (p[r] == i) { r--; } else { NO; return; } } YES; }步骤:
- 读入排列 。
- 初始化左右指针 ,。
- 从小到大枚举 到 :
- 如果当前左端是 ,则"移除"左端(
l++)。 - 如果当前右端是 ,则"移除"右端(
r--)。 - 如果都不是,说明排列不满足条件,输出
NO。
- 如果当前左端是 ,则"移除"左端(
- 全部成功移除则输出
YES。
时间复杂度 ,总复杂度 。
六、样例验证
样例 1
4 1 3 4 2- : →
- : →
- : →
- : → 完成 →
YES✅
样例 2
5 1 5 2 4 3- : →
- :, →
NO✅
样例 3
5 2 4 5 3 1- : →
- : →
- : →
- : →
- : → 完成 →
YES✅
样例 4
3 3 1 2- :, →
NO✅
七、代码实现(C++)
#include <bits/stdc++.h> using namespace std; void solve() { int n; cin >> n; vector<int> p(n); for (int i = 0; i < n; i++) { cin >> p[i]; } int l = 0, r = n - 1; for (int i = 1; i <= n; i++) { if (p[l] == i) { l++; } else if (p[r] == i) { r--; } else { cout << "NO\n"; return; } } cout << "YES\n"; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
八、复杂度分析
- 每个测试用例仅需 时间扫描排列一次。
- ,,总复杂度极小。
- 空间复杂度 。
- 1
信息
- ID
- 6686
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者