update
This commit is contained in:
parent
1042f508dd
commit
1c0c534c5a
@ -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';
|
||||
|
@ -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`)。
|
Loading…
Reference in New Issue
Block a user