This commit is contained in:
Zengtudor 2024-11-20 10:19:54 +08:00
parent f663b768a0
commit 29fcb3c5de
13 changed files with 329 additions and 2 deletions

51
src/20241120/T541792.cpp Normal file
View File

@ -0,0 +1,51 @@
#include <bits/stdc++.h>
using ll = int64_t;
using std::cin,std::cout;
const ll maxn{ll(2e5+5)},inf{ll(1)<<60};
ll n,a[maxn],fl[maxn],fr[maxn],ans{inf},gl[maxn],gr[maxn];
template<class T>
void pa(T *t){
for(ll i{1};i<=n;i++)cout<<t[i]<<' ';
cout<<'\n';
}
int main(){
std::iostream::sync_with_stdio(false),std::cin.tie(nullptr),std::cout.tie(nullptr);
cin>>n;
for(ll i{1};i<=n;i++){
cin>>a[i];
}
for(ll i{2};i<=n;i++){
if(a[i-1]+fl[i-1]>=a[i]){
fl[i]=fl[i-1]+(a[i-1]-a[i])+1;
}
gl[i]=gl[i-1];
if(fl[i-1]<fl[i]){
gl[i]+=fl[i]-fl[i-1];
}
}
for(ll i{n-1};i>=1;i--){
if(a[i+1]+fr[i+1]>=a[i]){
fr[i]=fr[i+1]+(a[i+1]-a[i])+1;
}
gr[i]=gr[i+1];
if(fr[i+1]<fr[i]){
gr[i]+=fr[i]-fr[i+1];
}
}
// pa(fl);
// pa(fr);
// pa(gl);
// pa(gr);
for(ll i{1};i<=n;i++){
ans=std::min(
ans,
(gl[i]+gr[i])
);
}
cout<<ans<<'\n';
}

261
src/20241120/T541792.md Normal file
View File

@ -0,0 +1,261 @@
要解决这道题我们需要通过对数组进行子数组加1操作使其最终形成一个“山形”序列即存在一个峰值位置 $ k $,使得数组在 $ k $ 之前严格递增,在 $ k $ 之后严格递减。我们的目标是最小化所需的操作次数。
### 问题分析
**操作定义:**
- 每次操作选择一个连续子数组将其中每个元素同时加1。
**目标**
- 通过最少次数的操作,使得最终数组满足存在一个峰值 $ k $,满足 $ A_1 < A_2 < \dots < A_k > A_{k+1} > \dots > A_N $。
### 思路概述
1. **明确最终数组的要求**
- **递增部分**:对于 $ i \leq k $,需要 $ A_i < A_{i+1} $。
- **递减部分**:对于 $ i > k $,需要 $ A_i > A_{i+1} $。
2. **确定需要的增量**
- 为满足递增和递减的要求,可能需要对某些元素进行增量操作。这些增量可以记录为 $ c_i $,其中 $ c_i $ 表示对位置 $ i $ 进行操作的次数每次操作增加1
3. **计算每个位置所需的最小增量**
- **递增部分**从左到右确保每个位置的值比前一个位置大至少1。
- **递减部分**从右到左确保每个位置的值比后一个位置大至少1。
4. **确定最小操作次数**
- 将所有位置的增量需求转换为最少的连续子数组操作次数。实际上,这等同于统计增量需求的“层数”,即覆盖所有需要增加的部分所需的最少水平切片数量。
### 详细步骤
#### 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} $,需要至少增加多少。
- **递减部分的增量数组**(后缀方式):
- 定义 `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为例
```
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`
**递减部分的增量 (`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`
**前缀操作次数 (`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`
**后缀操作次数 (`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`
**计算每个 $ 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`
最小操作次数为3。
### 实现代码
以下是基于上述思路的C++实现:
```cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
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];
// 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);
}
// 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;
}
else{
suf[i] = max(suf[i+1] + 1 + (A[i+1] - A[i]), 0LL);
}
}
// 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);
}
// 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);
}
// 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;
}
```
### 复杂度分析
- **时间复杂度**: $ O(N) $,每个预处理步骤都是线性扫描。
- **空间复杂度**: $ O(N) $,需要存储前缀和后缀的增量及操作次数。
### 总结
通过预先计算递增和递减部分的增量需求,并结合这些需求计算最少的连续子数组操作次数,我们能够高效地解决这道题。关键在于将问题分解为前缀和后缀两个部分,并利用动态规划的方法优化计算过程,确保算法在大规模数据下依然高效。

View File

@ -0,0 +1 @@
3

View File

@ -0,0 +1,2 @@
5
3 2 2 3 1

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,2 @@
5
9 7 5 3 1

View File

@ -0,0 +1 @@
1

View File

@ -0,0 +1,2 @@
2
2024 2024

View File

@ -0,0 +1 @@
93

View File

@ -0,0 +1,2 @@
8
12 2 34 85 4 91 29 85

View File

@ -0,0 +1 @@
16618723651088

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,9 @@
# NOIP2018 T3 题解 # NOIP2018提高组 T3 题解
>关于使用AI >关于使用AI
> >
>1. 代码由我书写注释和解释由ai书写 >1. 代码由我书写注释和解释由ai书写
>2. 文章经过了我的审核 >2. 文章经过了我的审核
>3. [博客食用更佳](https://blog.zziyu.cn/archives/noip2018-t3-ti-jie) >3. [博客食用更佳](https://blog.zziyu.cn/archives/noip2018-s-t3-ti-jie)
## 问题描述 ## 问题描述