题目链接: AtCoder Regular Contest 077 E - guruguru
题目大意
两个按钮, 一个可以使计数器+1(计数器数字从1-m), 当前值为m时, 再+1就变成了1
另一个按钮储存了一个值x, 按一下就从任意值会变成x
n-1次操作, 由一个数组a[n]描述, 第i次操作: 将计数器从a[i]调到a[i+1]
将x设置为某个值, 使得所有操作需要按按钮的次数总和最小, 输出这个最小值
范围:
2≤n,m≤105
2
≤
n
,
m
≤
10
5
1≤ai≤m
1
≤
a
i
≤
m
ai≠ai+1
a
i
≠
a
i
+
1
n,m,ai都是整数
n
,
m
,
a
i
都
是
整
数
思路
设置一个数组p, p[i] := 如果x在i位置, 对于所有操作, 使用第二个按钮能够减少的操作次数(相对于只使用第一个按钮)
设l = a[i], r = a[i+1]
, 如果l>r
, 令r = r+m
, 这样就不用考虑上界的问题了
那么对于每一对l, r, 如果r-l<=1
, 那么无论x在什么位置, 第二个按钮都不能减少操作次数
如果r-l>1
, 那么
x在l+2
位置使用第二个按钮能够减少1次操作, 在l+3
位置能减少2次… 在r位置能够减少r-(l+2)+1
次操作
所以p[l+2] += 1, p[l+3] += 2 ... p[r] += r-(l+2)+1
对每一对l, r如此处理, 得到最后的p数组
设all为只使用第一个按钮所需要的操作总次数
那么`最少操作次数 = all - max{p[i]+p[i+m]}, 1<=i<=m
以上就是基本思路, 如果不加其他优化, 直接写的话, 复杂度
O(mn)
O
(
m
n
)
能优化的地方是p[l+2] += 1, p[l+3] += 2 ... p[r] += r-(l+2)+1
这就是复杂度里m的来源, 可以用查分标记然后求两次前缀和, 将它优化到
O(1)
O
(
1
)
用差分标记将一个区间加上一个等差数列:
如果要将p[l]到p[r]依次加上1, 2, … r-l+1
我们可以这样: p[i] += 1, p[r+1] -= r-l+1 + 1, p[r+2] += r-l+1
, 每次只更新这三个值, 最后再从头到尾p[i] += p[i-1]
具体的一组例子, 比如l=2, r=5, 要将区间[l, r]中的元素依次加上{1, 2, 3, 4}:
更新操作 | 复杂度 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
更新三个值 | O(1) O ( 1 ) | 0 | 1 | 0 | 0 | 0 | -5 | 4 |
p[i]+=p[i-1] | O(n) O ( n ) | 0 | 1 | 1 | 1 | 1 | -4 | 0 |
p[i]+=p[i-1] | O(n) O ( n ) | 0 | 1 | 2 | 3 | 4 | 0 | 0 |
所以最后总复杂度 O(n+m) O ( n + m )
参考自: 来源
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 3E5 + 100;
typedef long long ll;
int n, m, a[MAXN];
ll p[MAXN];
int main()
{
ll all = 0;
scanf("%d%d", &n, &m);
for(int i=0; i<n; ++i) scanf("%d", a+i);
for(int i=1; i<n; ++i)
{
int l = a[i-1], r = a[i];
if(l>r) r += m;
all += r-l;
if(r-l>1)
{
p[l+2] += 1;
p[r+1] -= (r-(l+2)+1) + 1;
p[r+2] += (r-(l+2)+1);
}
}
for(int i=1; i<=2*m; ++i) p[i] += p[i-1];
for(int i=1; i<=2*m; ++i) p[i] += p[i-1];
ll ans = -1;
for(int i=1; i<=m; ++i) ans = max(ans, p[i]+p[i+m]);
cout << all - ans << endl;
return 0;
}