题意:给定一个长度为n的排列,每次向右循环移位,求的最小值,并求出是在第几次移位得到的。
思路:开始O(N^2)模拟了一下,果然超时。参考了一下网上代码,知道了一个O(N)的思路。用cnt数组记录当前num[i]所在位置 到 下标为num[i]的位置 需要向右循环移位几次,两个变量add、sub表示下一次移位会使sum值+1/-1的个数,每次更新均在上一排列的基础上进行。
先注意题意, 1 <= num[i] <= n。
再注意一点,对于每个位置的数,设为num[i],先不看num[i] = 0或者num[i] = n的情况,对于其他情况,从1到num[i]这段区间,num[i]右移使|num[i] - loc|单减,从num[i]到n这段区间,num[i]右移使|num[i] - loc|单增;对于num[i] = 0,从1到n,|num[i] - loc|一直单增;对于num[i] = n,num[i]右移使|num[i] - loc|单减;当num[i]从n移动到1,增减不一定。所以对于每个数num[i],位置num[i]是个转折点,决定下一步|num[i] - loc|如何变化(每次+1还是-1)。具体思路见注释。
// CodeForces 820D Mister B and PR Shifts 运行/限制:264ms/2000ms
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <cmath>
using namespace std;
#define INF 0x3f3f3f3f
#define LL long long
int num[1000005],cnt[1000005];
int main(){
int n;
int add, sub, index;
LL sum, re;
while (scanf("%d", &n) != EOF) {
sum = 0; add = 0; sub = 0;
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i <= n; i++) {
scanf("%d", &num[i]);
sum += fabs(num[i] - i);
cnt[(num[i] - i + n) % n]++;//cnt下标为:当前num[i]所在位置 到 num[i]位置 需要移位几次
if (num[i] > i) sub++;//下一次移位会使值-1的个数
else add++;//下一次移位会使值+1的个数
}
re = sum; index = 0;
for (int i = 1; i < n; i++) {//每次在前一次移位的基础上处理,i为第几次移动
sum += add - sub - 1;//当前最后一位肯定满足数值小于等于n(数据范围最大是n),所以之前计算在add里了;但因为要移动到首位,所以不一定是加、减
sum += (num[n - i + 1] - 1) - (n - num[n - i + 1]);//计算当前最后一位移动到首位sum值的变化;
//每次并没有真正进行移位,而是每次计算最后一位应该是未移位序列的哪个位置了
//求完sum,相当于该次移动了
//根据此次移动后的结果,更新下一步add、sub会出现的值
//-1,+1是因为这一次移动后到第一个位置的数,由于单调性反转,使add比正常情况-1、sub+1。
//num[i] = 1时,|num[i] - loc|全程单调递增,从n位置移动到1位置时单调性的变化是个特例,可以分析一下num[n] = 1也满足下面式子
add = add + cnt[i] - 1;
sub = sub - cnt[i] + 1;
if (sum < re) {
re = sum;
index = i;
}
}
printf("%lld %d\n", re, index);
}
return 0;
}