题目:
思路:
发现同余与商无关,再将其模到0~M-1之间,再将其放到一个环上,放到环上后发现,对于任意x,穿过x做一条对称轴,对称轴对面的两个点之间一定是不会有任何路径穿过,可以从对面的这条边断开,将二维变成经典一维问题。
所以可以反过来思考,只要枚举所有能断开的位置,再把所有能断开的位置的x枚举一遍,则一定可以把最优解枚举到.
怎么枚举所有能断开的位置?
先将所有a[i]排序后(因为能断开的位置一定是相邻两个a[i]之间的位置)放在圆周上,枚举所有相邻a[i]的边,假设其断开,断开后根据一维情况最优解求答案,即求中位数,然后求所有点到中位数的距离和,并将所有情况的中位数对比求最小值,就是最少操作数。
但是,在圆上断开时最坏情况下有n个位置,再排序后求每点到中位数的总距离和的时间复杂度为n^2logn,超时,需要优化:
枚举换环上从每个位置断开,有一个通用的优化方式:
破环成链:将环直接展开,展开后再复制一遍放在右边成一个2n的数组,并且递增排序,如下图
每一个从环上断开后所形成的链都对应着这个数组上一个长度为n的区间,因此如果要枚举所有断开位置所形成的链的话,相当于枚举这个长度为2n的数组中所有长度为n的区间。然后对于每一个长度为n的区间都求其中位数p,再都分别求每个数到中位数的距离和,如下图
但是这里如果直接求的话时间复杂度还是n^2,需要优化:
通过公式变化+前缀和来进行优化,时间复杂度变为n,如下图
代码:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N=400010;//因为要把数组乘2,N多开一倍
int n,m;
int a[N];
LL s[N];//前缀和
int main()
{
int T;
cin>>T;
while(T--)
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[i]%=m;
}
sort(a+1,a+n+1);//排序,保证后面枚举的是相邻边
//破环成链,将数组复制一遍放在后面
for(int i=1;i<=n;i++) a[i+n]=a[i]+m;
//求前缀和
for(int i=1;i<=n*2;i++) s[i]=s[i-1]+a[i];
//定义答案,先定义为正无穷
LL res=1e18;
//枚举所有长度为n的区间,枚举左端点,右端点等于左端点+n-1,共同构成长度为n的区间
for(int l=1;l<=n;l++)
{
int r=l+n-1,p=(l+r)/2;//右端点,中点(中位数)
//左、右半边的和,a[p]要强制转换为LL,否则会爆int
LL left=(p-l+1)*(LL)a[p] - (s[p]-s[l-1]);
LL right=(s[r]-s[p]) - (r-p)*(LL)a[p];
//更新最小值
res=min(res,left+right);
}
cout<<res<<endl;
}
return 0;
}