diff --git a/src/20241120/T541792.cpp b/src/20241120/T541792.cpp index 99babc5..ab3f69f 100644 --- a/src/20241120/T541792.cpp +++ b/src/20241120/T541792.cpp @@ -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< 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 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 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 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 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 + +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 +void pa(T *t){ + for(ll i = 1; i <= n; i++) cout << t[i] << ' '; + cout << '\n'; +} +template +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 A(N+1, 0); - for(int i=1;i<=N;i++) cin >> A[i]; + cout.tie(0); - // Compute pre[i] - vector 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 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 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 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` 的最小值。 ### 总结 -通过预先计算递增和递减部分的增量需求,并结合这些需求计算最少的连续子数组操作次数,我们能够高效地解决这道题。关键在于将问题分解为前缀和后缀两个部分,并利用动态规划的方法优化计算过程,确保算法在大规模数据下依然高效。 \ No newline at end of file +该解法通过构造差分数组,将问题转化为调整差分为正和负的最小操作次数问题。利用前缀和后缀的方法,分别计算在不同转折点 `k` 下所需的操作次数,最终取最小值作为答案。这种方法的时间复杂度为 O(N),适用于较大的数据规模(最大 `N = 2e5`)。 \ No newline at end of file