题目:
样例输入:
2
5
4 3 5 1 2
10
4 7 3 8 6 1 9 10 5 2
样例输出:
7
24
题意:给出一个排列p,把每个位置视为点,建一个无向图,i,j之间的边权为|i-j|*|pi-pj|。求这个图的最小生成树。
先来给出最小生成树的一些性质:
(1)一个图的最小生成树不一定唯一,但是其对应第k大边权一定是唯一的
(2)最小生成树的最大边权一定是所有生成树的最大边权的最小值
分析:首先我们尝试着把相邻的点之间连一条边,这样每两个相邻点之间的边权都是|pi-pj|<n,那么我们就可以知道有一棵生成树的边的最大值是小于n的,那么就等价于我们可以构造一棵最小生成树而其中所有边权均小于n,因为最小生成树的最大边权一定是所有生成树的最大边权的最小值。所以最小生成树中所有边权均小于n。那么也就是说我们只需要把所有边权小于n的边记录一下然后跑最小生成树就行。现在问题是我怎么找到边权小于n的边,根据|i-j|*|pi-pj|<n我们可以知道|i-j|和|pi-pj|至少有一个小于sqrt(n),那么我们就可以用mp数组记录点权pi对应的位置i,然后通过2n*sqrt(n)的复杂度把所有的边权小于n的边全部找到,kruscal跑最小生成树需要先对边按照边权从小到大进行排序,但是快速排序的复杂度是(nlogn)级别的,肯定会超时,所以我们这道题只能用桶排,用链式前向星写一个h[i]代表权值为i的边的初始编号,记录一下每个边权出现的编号,最后直接跑一个最小生成树就可以了。注意在桶排的时候不要用vector,容易超时,而用链式前向星写会比vector快一倍多。
下面是代码:
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<map>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
const int N=5e4+10;
int fu[N],a[N],mp[N];
int h[N],ne[N*600],idx,u[N*600],v[N*600];
struct node{
int u,v,w;
}p[N*600];
int find(int x)
{
if(x!=fu[x]) fu[x]=find(fu[x]);
return fu[x];
}
void add(int x,int y,int z)
{
u[idx]=x;
v[idx]=y;
ne[idx]=h[z];
h[z]=idx++;
}
int main()
{
int T;
cin>>T;
while(T--)
{
int n;
scanf("%d",&n);
idx=0;
for(int i=1;i<=n;i++)
{
fu[i]=i;h[i]=-1;
scanf("%d",&a[i]);
mp[a[i]]=i;
}
int cnt=0;
for(int i=1;i<=n;i++)
{
int t=(int)sqrt(n);
int e=min(i+t,n);
for(int j=i+1;j<=e;j++)
{
int tt=1ll*(j-i)*abs(a[j]-a[i]);
if(tt<n)
add(i,j,tt);
tt=1ll*abs(mp[j]-mp[i])*(j-i);
if(tt<n)
add(mp[i],mp[j],tt);
}
}
long long ans=0;
int count=0;
for(int i=1;i<n;i++)
{
for(int j=h[i];j!=-1;j=ne[j])
{
int fx=find(u[j]),fy=find(v[j]);
if(fx==fy) continue;
fu[fx]=fy;
count++;ans+=i;
if(count==n-1) break;
}
if(count==n-1) break;
}
printf("%lld\n",ans);
}
return 0;
}