This commit is contained in:
Zengtudor 2024-11-20 20:52:05 +08:00
parent 1042f508dd
commit 1c0c534c5a
2 changed files with 165 additions and 208 deletions

View File

@ -38,6 +38,10 @@ int main(){
}
// pa(d,p,s);
for(ll i{1};i<=n;i++){
/*
k是一个山顶
*/
ans=min(ans,max(p[i],s[i+1]));
}
cout<<ans<<'\n';

View File

@ -1,261 +1,214 @@
要解决这道题我们需要通过对数组进行子数组加1操作使其最终形成一个“山形”序列即存在一个峰值位置 $ k $,使得数组在 $ k $ 之前严格递增,在 $ k $ 之后严格递减。我们的目标是最小化所需的操作次数。
# 题目
# 调整数组
### 问题分析
## 题目描述
**操作定义:**
- 每次操作选择一个连续子数组将其中每个元素同时加1。
给定一个长为 $N$ 的正整数数组 $A$,下标从 $1$ 开始,你可以进行 $0$ 次或任意正整数次下面这种操作:
**目标**
- 通过最少次数的操作,使得最终数组满足存在一个峰值 $ k $,满足 $ A_1 < A_2 < \dots < A_k > A_{k+1} > \dots > A_N $。
- 选取一个子段 $[A_L, ..., A_R]$ ,让这个子段中的每个数同时加 $1$。
### 思路概述
求让数组 $A$ 能够满足下面要求的最小操作次数:
1. **明确最终数组的要求**
- **递增部分**:对于 $ i \leq k $,需要 $ A_i < A_{i+1} $。
- **递减部分**:对于 $ i > k $,需要 $ A_i > A_{i+1} $。
- 存在一个整数 $k \in [1,N]$,满足 $A_1 < ... < A_k > A_{k + 1} > ... > A_N$
2. **确定需要的增量**
- 为满足递增和递减的要求,可能需要对某些元素进行增量操作。这些增量可以记录为 $ c_i $,其中 $ c_i $ 表示对位置 $ i $ 进行操作的次数每次操作增加1
特殊地:
- 当 $N = 1$ 时,$A$ 数组一定会满足要求
- 如果 $k = 1$,则该要求为 $A_1 > ... > A_N$
- 如果 $k = N$,则该要求为 $A_1 < ... < A_N$
3. **计算每个位置所需的最小增量**
- **递增部分**从左到右确保每个位置的值比前一个位置大至少1。
- **递减部分**从右到左确保每个位置的值比后一个位置大至少1。
## 输入格式
4. **确定最小操作次数**
- 将所有位置的增量需求转换为最少的连续子数组操作次数。实际上,这等同于统计增量需求的“层数”,即覆盖所有需要增加的部分所需的最少水平切片数量。
第一行,包含一个正整数 $N$
### 详细步骤
第二行,包含 $N$ 个正整数 $A_i$
#### 1. 预处理递增和递减部分的增量需求
## 输出格式
为了高效地计算每个可能的峰值位置 $ k $ 对应的增量需求,我们可以预先计算两个辅助数组:
输出一行,包含一个非负整数,表示答案
- **递增部分的增量数组**(前缀方式):
- 定义 `pre[i]` 表示为了使前 $ i $ 个元素严格递增所需的最小增量。
- 计算方法:
```cpp
pre[1] = 0;
for (int i = 2; i <= N; ++i) {
pre[i] = max(pre[i-1] + 1 + (A[i-1] - A[i]), 0);
}
```
这里,`pre[i]` 的意思是为了让 $ A_i + c_i > A_{i-1} + c_{i-1} $,需要至少增加多少。
## 样例 #1
- **递减部分的增量数组**(后缀方式):
- 定义 `suf[i]` 表示为了使后 $ N - i + 1 $ 个元素严格递减所需的最小增量。
- 计算方法:
```cpp
suf[N] = 0;
for (int i = N-1; i >= 1; --i) {
suf[i] = max(suf[i+1] + 1 + (A[i+1] - A[i]), 0);
}
```
这里,`suf[i]` 的意思是为了让 $ A_i + c_i > A_{i+1} + c_{i+1} $,需要至少增加多少。
#### 2. 计算最小操作次数
为了找到最小的操作次数,我们需要为每个可能的峰值位置 $ k $ 计算所需的增量,并确定覆盖这些增量所需的最少操作次数。
**关键观察:**
- 每个操作可以覆盖一个连续的子数组将其中所有元素加1。
- 要覆盖所需的增量 `c[i]`,可以将增量视为柱状图的高度,操作次数即为“绘制这些柱状图”所需的水平切片数。
**操作次数计算方法:**
- 对于一个特定的增量数组 `c`,最小的操作次数等于 `c[i]``c[i-1]` 的差值的正部分之和,具体表达为:
$$
\text{操作次数} = \sum_{i=1}^{N} \max(c[i] - c[i-1], 0)
$$
其中,定义 $ c[0] = 0 $ 以便处理第一个元素。
**具体步骤:**
1. **枚举所有可能的峰值位置 $ k $**
- 对于每一个 $ k $,设定数组前 $ k $ 个元素的增量为 `pre[k]`,后 $ N - k $ 个元素的增量为 `suf[k+1]`
2. **计算每个 $ k $ 对应的总操作次数**
- 合并前缀和后缀增量需求,得到整体的增量数组 `c`
- 对于 $ i \leq k $`c[i] = pre[i]`
- 对于 $ i > k $`c[i] = suf[i]`
- 计算操作次数:
```cpp
long long operations = 0;
long long prev = 0;
for (int i = 1; i <= N; ++i) {
long long current = (i <= k) ? pre[i] : suf[i];
if (current > prev)
operations += (current - prev);
prev = current;
}
```
3. **选择最小的操作次数**
- 遍历所有 $ k $,记录最小的 `operations` 作为答案。
#### 3. 优化计算
尽管上述方法直观,但在 $ N $ 高达 $ 2 \times 10^5 $ 时,枚举所有 $ k $ 并对每个 $ k $ 进行 $ O(N) $ 的计算会导致总时间复杂度 $ O(N^2) $,超出限制。为了优化,我们需要更高效的方法:
**优化策略:**
- 观察到对于不同的 $ k $,前缀和后缀增量需求具有重叠和单调性,可以通过预处理和动态规划的方式来减少重复计算。
- **核心思想**:最小操作次数等于前缀和后缀增量需求的联合上升序列的步数。
**具体实现:**
1. **预计算前缀操作次数和后缀操作次数**
- 定义 `pre_ops[i]` 表示前 $ i $ 个元素严格递增所需的操作次数。
- 定义 `suf_ops[i]` 表示后 $ N - i + 1 $ 个元素严格递减所需的操作次数。
2. **计算 `pre_ops``suf_ops`**
```cpp
vector<long long> pre(N+1, 0);
for(int i=2;i<=N;i++) {
pre[i] = max(pre[i-1] + 1 + (A[i-1] - A[i]), 0LL);
}
vector<long long> suf(N+1, 0);
for(int i=N-1;i>=1;i--) {
suf[i] = max(suf[i+1] + 1 + (A[i+1] - A[i]), 0LL);
}
vector<long long> pre_ops(N+1, 0);
for(int i=1;i<=N;i++) {
pre_ops[i] = pre_ops[i-1] + max(pre[i] - (i > 1 ? pre[i-1] : 0LL), 0LL);
}
vector<long long> suf_ops(N+1, 0);
for(int i=N;i>=1;i--) {
suf_ops[i] = suf_ops[i+1] + max(suf[i] - (i < N ? suf[i+1] : 0LL), 0LL);
}
```
3. **结合前缀和后缀操作次数**
- 对于每个 $ k $,总操作次数为 `pre_ops[k] + suf_ops[k+1]`
- 需要遍历所有 $ k $ 找到最小值。
4. **找到最小操作次数**
```cpp
long long min_operations = LLONG_MAX;
for(int k=1;k<=N;k++) {
long long total_ops = pre_ops[k] + suf_ops[k+1];
min_operations = min(min_operations, total_ops);
}
```
这样,整个过程的时间复杂度降为 $ O(N) $,适用于题目给定的约束。
### 示例解析
以样例输入1为例
### 样例输入 #1
```
5
3 2 2 3 1
```
**递增部分的增量 (`pre` 数组):**
- `pre[1] = 0`
- `pre[2] = max(0 + 1 + (3 - 2), 0) = 2`
- `pre[3] = max(2 + 1 + (2 - 2), 0) = 3`
- `pre[4] = max(3 + 1 + (2 - 3), 0) = 3`
- `pre[5] = max(3 + 1 + (3 - 1), 0) = 5`
### 样例输出 #1
**递减部分的增量 (`suf` 数组):**
- `suf[5] = 0`
- `suf[4] = max(0 + 1 + (1 - 3), 0) = 0`
- `suf[3] = max(0 + 1 + (3 - 2), 0) = 2`
- `suf[2] = max(2 + 1 + (2 - 2), 0) = 3`
- `suf[1] = max(3 + 1 + (2 - 3), 0) = 4`
```
3
```
**前缀操作次数 (`pre_ops`):**
- `pre_ops[1] = 0`
- `pre_ops[2] = 0 + max(2 - 0, 0) = 2`
- `pre_ops[3] = 2 + max(3 - 2, 0) = 3`
- `pre_ops[4] = 3 + max(3 - 3, 0) = 3`
- `pre_ops[5] = 3 + max(5 - 3, 0) = 5`
## 样例 #2
**后缀操作次数 (`suf_ops`):**
- `suf_ops[5] = 0`
- `suf_ops[4] = 0 + max(0 - 0, 0) = 0`
- `suf_ops[3] = 0 + max(2 - 0, 0) = 2`
- `suf_ops[2] = 2 + max(3 - 2, 0) = 3`
- `suf_ops[1] = 3 + max(4 - 3, 0) = 4`
### 样例输入 #2
**计算每个 $ k $ 的总操作次数:**
- $ k = 1 $: `pre_ops[1] + suf_ops[2] = 0 + 3 = 3`
- $ k = 2 $: `pre_ops[2] + suf_ops[3] = 2 + 2 = 4`
- $ k = 3 $: `pre_ops[3] + suf_ops[4] = 3 + 0 = 3`
- $ k = 4 $: `pre_ops[4] + suf_ops[5] = 3 + 0 = 3`
- $ k = 5 $: `pre_ops[5] + suf_ops[6] = 5 + 0 = 5`
```
5
9 7 5 3 1
```
最小操作次数为3。
### 样例输出 #2
### 实现代码
```
0
```
以下是基于上述思路的C++实现:
## 样例 #3
### 样例输入 #3
```
2
2024 2024
```
### 样例输出 #3
```
1
```
## 样例 #4
### 样例输入 #4
```
8
12 2 34 85 4 91 29 85
```
### 样例输出 #4
```
93
```
## 提示
### 数据范围
子任务 $1$$40$ 分):$1 \leq N \leq 2000$
子任务 $2$$60$ 分):$1 \leq N \leq 2 \times 10^5$
对于全部数据,$1 \leq N \leq 2 \times 10^5, 1 \leq A_i \leq 10^9$
# 题解
>1. 作者在前面的话:这题是很普及组的题,就是稍微考验一点思维,但是作者可能是写题少了的缘故挂分了
>2. 代码是我写的文章没空写给AI(chat-o1-mini)写了
>3. [博客食用更佳](https://blog.zziyu.cn/archives/diao-zheng-shu-zu-chai-fen-qian-zhui-he-si-wei-xing-ti-mu-ti-jie)
## 解题思路
题目要求通过对数组进行特定的加1操作使得数组中存在一个转折点 `k`,满足 `A_1 < A_2 < ... < A_k > A_{k+1} > ... > A_N`。为了达到这一目标,关键在于合理地选择操作的子段,并最小化操作次数。
### 操作解析
每次操作可以选择一个子段 `[A_L, ..., A_R]`让该子段中的所有元素同时加1。要使得最终数组满足单调递增到 `k`,再单调递减的要求,可以转化为以下步骤:
1. **差分数组**:构造差分数组 `d[i] = A[i] - A[i-1]`(对于 `i = 1``A[0]` 可视为0。为了实现递增到 `k`,需保证 `d[2], d[3], ..., d[k] > 0`;为了实现从 `k` 开始递减,需保证 `d[k+1], d[k+2], ..., d[N] < 0`
2. **前缀调整**:对于每个位置 `i`,计算将 `d[1]``d[i]` 调整为正所需的最小操作次数 `p[i]`
3. **后缀调整**:对于每个位置 `i`,计算将 `d[i]``d[N]` 调整为负所需的最小操作次数 `s[i]`
4. **综合考虑**:对于每个可能的转折点 `k`,需要 `p[k]``s[k+1]` 都满足,故对于每个 `k`,操作次数为 `max(p[k], s[k+1])`。最终的答案是所有可能 `k` 的最小值。
### 代码注释
```cpp
#include <bits/stdc++.h>
using ll = int64_t;
using namespace std;
typedef long long ll;
const ll maxn = 2e5 + 5, inf = (1LL << 60);
ll n, a[maxn], d[maxn], p[maxn], s[maxn];
ll ans = inf;
// 辅助函数,用于输出数组(调试用)
template<class T>
void pa(T *t){
for(ll i = 1; i <= n; i++) cout << t[i] << ' ';
cout << '\n';
}
template<class T, class ...Args>
void pa(T&&t, Args&&...args){
pa(t);
pa(args...);
}
int main(){
// 加速输入输出
ios::sync_with_stdio(false);
cin.tie(0);
int N;
cin >> N;
vector<ll> A(N+1, 0);
for(int i=1;i<=N;i++) cin >> A[i];
cout.tie(0);
// Compute pre[i]
vector<ll> pre(N+1, 0);
pre[1] = 0;
for(int i=2;i<=N;i++){
pre[i] = max(pre[i-1] + 1 + (A[i-1] - A[i]), 0LL);
}
cin >> n;
for(ll i = 1; i <= n; i++) cin >> a[i];
// Compute suf[i]
vector<ll> suf(N+2, 0); // suf[N+1] = 0
for(int i=N;i>=1;i--){
if(i == N){
suf[i] = 0;
// 计算差分数组 d[i] = A[i] - A[i-1]
for(ll i = 1; i <= n; i++){
d[i] = a[i] - a[i-1];
if(d[i] <= 0){
// 如果差分非正需要调整为至少1
// 调整次数为 (1 - d[i])
p[i] = p[i-1] + 1 - d[i];
}
else{
suf[i] = max(suf[i+1] + 1 + (A[i+1] - A[i]), 0LL);
// 差分已经为正,无需调整
p[i] = p[i-1];
}
}
// Compute pre_ops[i]
vector<ll> pre_ops(N+1, 0);
pre_ops[0] = 0;
pre_ops[1] = max(pre[1] - 0, 0LL);
for(int i=2;i<=N;i++){
pre_ops[i] = pre_ops[i-1] + max(pre[i] - pre[i-1], 0LL);
// 从后往前计算将差分调整为负所需的操作次数
for(ll i = n; i >= 1; i--){
if(d[i] >= 0){
// 如果差分非负,需要调整为至少-1
// 调整次数为 (d[i] + 1)
s[i] = s[i+1] + d[i] + 1;
}
else{
// 差分已经为负,无需调整
s[i] = s[i+1];
}
}
// Compute suf_ops[i]
vector<ll> suf_ops(N+2, 0); // suf_ops[N+1] = 0
suf_ops[N+1] = 0;
suf_ops[N] = max(suf[N] - 0, 0LL);
for(int i=N-1;i>=1;i--){
suf_ops[i] = suf_ops[i+1] + max(suf[i] - suf[i+1], 0LL);
// 遍历所有可能的转折点 k计算最小的操作次数
for(ll i = 1; i <= n; i++){
/*
对于每个转折点 k = i
需要前缀 [1, k] 调整为递增,后缀 [k+1, N] 调整为递减
因为操作可以重叠,因此取 p[k] 和 s[k+1] 的最大值
*/
ans = min(ans, max(p[i], s[i+1]));
}
// Find minimum operations over all k
ll min_operations = LLONG_MAX;
for(int k=1;k<=N;k++){
ll total_ops = pre_ops[k] + suf_ops[k+1];
min_operations = min(min_operations, total_ops);
}
// Edge case when k=N
// When k=N, suf_ops[k+1] = suf_ops[N+1] =0
cout << min_operations;
cout << ans << '\n';
}
```
### 复杂度分析
### 详细解释
- **时间复杂度**: $ O(N) $,每个预处理步骤都是线性扫描。
- **空间复杂度**: $ O(N) $,需要存储前缀和后缀的增量及操作次数。
1. **差分数组的构造**
- 差分数组 `d[i] = A[i] - A[i-1]` 表示相邻元素之间的差值。为了使数组在某个点 `k` 前单调递增,需要 `d[2] > 0, d[3] > 0, ..., d[k] > 0`;在 `k` 后单调递减,需要 `d[k+1] < 0, d[k+2] < 0, ..., d[N] < 0`
2. **前缀操作次数 `p[i]`**
- 遍历数组,从左到右计算前缀中每个差分需要调整为正的最小操作次数。
- 若 `d[i] > 0`,不需要调整,`p[i] = p[i-1]`。
- 若 `d[i] <= 0`需要将其至少调整为1操作次数增加 `1 - d[i]`
3. **后缀操作次数 `s[i]`**
- 从右向左遍历数组,计算后缀中每个差分需要调整为负的最小操作次数。
- 若 `d[i] < 0`,不需要调整,`s[i] = s[i+1]`。
- 若 `d[i] >= 0`,需要将其至少调整为-1操作次数增加 `d[i] + 1`
4. **选择转折点 `k`**
- 对于每个可能的转折点 `k`,所需的总操作次数为 `max(p[k], s[k+1])`
- 因为操作可以在前缀和后缀之间重叠,取两者的最大值即可。
- 最终答案为所有可能 `k` 的最小值。
### 总结
通过预先计算递增和递减部分的增量需求,并结合这些需求计算最少的连续子数组操作次数,我们能够高效地解决这道题。关键在于将问题分解为前缀和后缀两个部分,并利用动态规划的方法优化计算过程,确保算法在大规模数据下依然高效。
该解法通过构造差分数组,将问题转化为调整差分为正和负的最小操作次数问题。利用前缀和后缀的方法,分别计算在不同转折点 `k` 下所需的操作次数,最终取最小值作为答案。这种方法的时间复杂度为 O(N),适用于较大的数据规模(最大 `N = 2e5`