You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

167 lines
6.8 KiB

2 years ago
##[$AcWing$ $100$. 增减序列](https://www.acwing.com/problem/content/102/)
### 一、题目描述
给定一个长度为 $n$ 的数列 $a_1,a_2,…,a_n$,每次可以选择一个区间 $[l,r]$,使下标在这个区间内的数都加一或者都减一。
求至少需要多少次操作才能 **使数列中的所有数都一样**,并求出在保证最少次数的前提下,最终得到的数列可能有多少种。
**输入格式**
第一行输入正整数 $n$。
接下来 $n$ 行,每行输入一个整数,第 $i+1$ 行的整数代表 $a_i$。
**输出格式**
第一行输出最少操作次数。
第二行输出最终能得到多少种结果。
**数据范围**
$0<n10^5,0a_i<2147483648$
**输入样例**
```cpp {.line-numbers}
4
1
1
2
2
```
**输出样例**
```cpp {.line-numbers}
1
2
```
### 二、题目解析
#### 差分复习
对于一个给定序列 $a$, 它的差分序列 $b$ 定义为:
$$ b[1]=a[1] ① \\ b[i]=a[i]a[i1] (2≤i≤n)②$$
可以发现, **前缀和** 、 **差分** 是一对互逆运算,$b$ 序列的前缀和就是 $a$ 序列。
**应用**
如果要把序列 $a$ 的 $[l,r]$ 区间内的所有数加 $d$,差分序列 $b$ 的变化为 $b[l]$ 加 $d$$b[r+1]$ 减 $d$,减法同理。
#### 本题思路
因为题目中提到在原数组 $a[i]$的某个区间上要么加$1$,要么减$1$,也就是一维数组的区间加减问题,符合 **差分** 的应用场景,使用差分,可以使得原来$r-l+1$次修改变成$2$次,可以节约大量的计算时间,所以,本题应该考虑使用差分。
那我们就需要求出 $a$ 的差分序列 $b$。
题目的最终目标是把数组的全部数字变成一样的,那么,对应到差分数组,就是
$b[1],b[n+1]$是什么值都可以,但$b[2],b[3],...b[n]$都需要等于$0$,并且,$b[1]$的值就是最终数组$a[]$的所有数字一样的那个值。
**以数据样例为例进行理解**
```cpp {.line-numbers}
原始数组: 1 1 2 2
差分数组: 1 0 1 0
```
- $A$方案,前两个$+1$:``` 2 2 2 2 ```
差分数组随之变化,$1$位加$1$$2+1=3$位减$1$,得到如下的差分数组:
```cpp {.line-numbers}
2 0 0 0
```
- $B$方案,后两个$-1$:``` 1 1 1 1 ```
差分数组随之变化,$3$位减$1$$4+1=5$位加$1$,得到如下的差分数组:
```cpp {.line-numbers}
1 0 0 0 1
```
**咦?为什么原数组是$4$个数字,你的差分是$5$个数字呢?**
你看一下上面的应用$a[l-r]$加上$d$,是不是$b[l]+=d,b[r+1]-=d$,而$1<=l<=r<=n$
考虑一下边界问题,如果 $r=n$时,上面的
$$b[r+1]-=d \Rightarrow b[n+1]-=d $$
也就是说,$b[]$数组需要考虑到$n+1$项,此时,如果$r=n$时,也就是变化区间最右边到$n$时,就用到了$b[n+1]$,但$b[n+1]$最终是什么值不重要,它的值不影响区间内所有元素的值一样。
**贪心思路求变化最小次数**
> **转化**:题目对 $a$ 序列的 **任何区间加减$1$操作**,等同于对 $b$ 序列中的 **任意两数**,一个加 $1$,一个减 $1$。
> **目标** : 把 $b[2]$ 至 $b[n]$ 全变为 $0$,$b[1],b[n+1]$是多少无所谓。
从 $b[1]$ 至 $b[n+1]$ 中 **任选** 两数的方案进行分类讨论:
由于$b[1],b[n+1]$比较特殊,所以划分分类时,以它们为分类标准进行划分四类:
- **① 选 $b[i]$ 和 $b[j]$**
其中 $2≤i,j≤n$,这种操作会改变 $b[2]$ 至 $b[n]$ 中两个数的值,因为差分操作,无外乎就是:
- 前项$+1$,后项$-1$
- 前项$-1$,后项$+1$
这两种情况,目标是把这段区间所有数字变成$0$,那大于$0$的得$-1$向小了变,小于$0$的得$+1$向大的变。也就是尽量挑 $b[i]$ 和 $b[j]$ **一正一负** 的情况下,这样一下就可以 **变掉两个数**,使得整体向都是$0$的区间逼近。
- **② 选 $b[1]$ 和 $b[j]$**
其中 $2≤j≤n$;这种情况下也是两种可能分支:
- $b[j]>0$,那么$b[j]-=1$,$b[1]+=1$
- $b[j]<0$,那么$b[j]+=1$,$b[1]-=1$
因为我们并不关心$b[1]$的最终值是多少只关心尽快的把b[2 \sim n]变成$0$,所以,这样一下只能改一个数字,效率不如上面的 ① 快。
- **③ 选 $b[i]$ 和 $b[n+1]$**
其中 $2≤j≤n$;这种情况下也是两种可能分支:
- $b[i]>0$,那么$b[i]-=1$,$b[n+1]+=1$
- $b[i]<0$,那么$b[i]+=1$,$b[n+1]-=1$
因为我们并不关心$b[n+1]$的最终值是多少,只关心尽快的把$b[2 \sim n]$变成$0$,所以,这样一下只能改一个数字,效率不如上面的 ① 快。
- **④ 选 $b[1]$ 和 $b[n+1]$**
因为它不会改变 $b[2]$ 至 $b[n]$ 的值,浪费了操作,一定不是最优解,放弃这种可能。
所以,我们的贪心策略就是:尽量的采用方法 ①,如果实在没有办法使用方法 ①时,再考虑使用方法 ② 或者 方法 ③。
那什么情况下可以使用方法 ① 呢? 限制条件是在$b[2 \sim n]$中存在$1$个及$1$个以上大于$0$的数字,并且,存在$1$个及$1$个以上小于$0$的数字,才能进行此操作。
所以,有了如下的神操作:
**设 $b[2]$ 至 $b[n]$ 中正数总和为 $p$,负数总和的绝对值为 $q$。**
首先,我们以正负配对的方式执行操作,可执行 $min(p,q)$ 次,剩余 $|pq|$ 个没有配对。
接下来,剩下的每个与 $b[1]$ 或 $b[n+1]$ 配对,共需 $|pq|$ 次。
**综上**,最少操作次数为 $min(p,q)+|pq|=max(p,q)$ 次。
下面来思考第二个问题:
> **保证最少次数的前提下,最终得到的数列可能有多少种?**
答:在尽量采用方法 ① 的情况下,可以尽快的使得整体区间向全是$0$进行逼近,但最后,随着正数或者负数的消亡,剩下了$|p-q|$个正数值或者负数值,此时,只能通过与$b[1]$或 $b[n+1]$的 **捉对** 操作才能完成整体全是$0$的目标
假设剩下$|p-q|$个:
- $b[1]$消耗掉$0$个,$|p-q|$个交给$b[n+1]$消耗掉
- $b[1]$消耗掉$1$个,$|p-q|-1$个交给$b[n+1]$消耗掉
- $b[1]$消耗掉$2$个,$|p-q|-2$个交给$b[n+1]$消耗掉
...
- $b[1]$消耗掉$|p-q|$个,$0$个交给$b[n+1]$消耗掉
- 共有$|p-q|+1$种情况,此时,$b[1]$的值也随之变化,共$|p-q|+1$种结果。
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int a[N], b[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[i] = a[i] - a[i - 1];
}
LL p = 0, q = 0;
for (int i = 2; i <= n; i++)
if (b[i] > 0)
p += b[i];
else
q -= b[i];
printf("%lld\n%lld\n", max(p, q), abs(p - q) + 1);
return 0;
}
```