大致题意:给你很多条边,每条边有边权,现在让你找到一个方差最小的生成树,即最小方差生成树。
看到这题,我们首先回忆一下很久之前做过的一道,求最小极差生成树的题目 CSU 1845 。这套题目里面用到的主要思路就是,最小极差肯定是连续的一段,然后我就按照边的权值大小排序,每次看边是否可以直接加入,如果可以则直接加入,否则在加入这条边之后的环中寻找一条权值最小的边删掉。这样所有的边遍历一遍,中间过程中的极差最小就是我们的解。
接着,我们考虑这道题目,显然,这题的方差和极差是有关系的,因为方差越小意味着样本之间越接近,相应的极差可能也会更小,所以很自然而然的我们可以想到,最小方差生成树也一定是连续的一段。然后我们考虑一下暴力的做法如何去做。
方差当然也是与均值的取值相关的。对于两条相互冲突的边,其权值分别是wi和wj且满足wi 然后考虑优化。我们枚举M^2个均值显然还是太多了,有没有什么方法可以不枚举均值呢?我们发现,我们之前说的,最小方差生成树边权肯定是连续的一段,然后还要冲突的两条边的选取依据我们还没有怎么利用。其实,如果这么考虑,冲突的两条边只会是在按照边权kruskal过程中,冲突的边,同样我们删掉环中边权最小的边,这个删除的过程不正是相当于做了一次选择的过程吗?也即,在删除的时候,我是默认把均值调整到大于两边权值和的一半了,也就是说,我们在做kruskal的过程中,其实也是在枚举这个均值。 但是,这里会出现一个问题,如果说之前有一对边(i,j)矛盾我把它们删除了,当前又有边(o,p)矛盾, 具体来说,对于每一个一开始直接加入的边,我们把它的优先级设置为负无穷大,权值设置为边权。然后对于每一个与前面边矛盾的边,把它的边设置为 这样总共m条边,就会有2m个操作,对一个每条边的加入和删除。按照优先级排序之和,这个顺序就恰好对应了我们的均值从小到大枚举的过程。我们要做的就是依次处理这些操作,每次当发现加入的边条数为n-1的时候,就比较更新一下最后的结果。 关于这个最后结果的比较,我们用到了概率论里面给出的公式 q;
int con[N],n,m;
struct Link_Cut_Tree
{
int son[N][2],fa[N],mn[N],val[N];
bool rev[N];
void init(int n)
{
for(int i=0;i<=n;i++)
{
son[i][0]=son[i][1]=0;
fa[i]=0;
}
}
inline bool which(int x){return son[fa[x]][1]==x;}
bool isroot(int x){return !fa[x]||son[fa[x]][which(x)]!=x;}
inline void push_up(int x)
{
if (!x) return; mn[x]=val[x];
if (son[x][0]) mn[x]=min(mn[x],mn[son[x][0]]);
if (son[x][1]) mn[x]=min(mn[x],mn[son[x][1]]);
}
inline void Reverse(int x)
{
if (!x) return;
swap(son[x][0],son[x][1]);
rev[x]^=1;
}
inline void push_down(int x)
{
if (!x||!rev[x]) return;
Reverse(son[x][0]);
Reverse(son[x][1]);
rev[x]=0;
}
inline void Rotate(int x)
{
int y=fa[x]; bool ch=which(x);
son[y][ch]=son[x][ch^1];son[x][ch^1]=y;
if (!isroot(y)) son[fa[y]][which(y)]=x;
fa[x]=fa[y]; fa[y]=x; fa[son[y][ch]]=y;
push_up(y); push_up(x);
}
inline void splay(int x)
{
int i=x;
for(;!isroot(i);i=fa[i])
sta.push(i); sta.push(i);
while (!sta.empty())
{
push_down(sta.top());
sta.pop();
}
while (!isroot(x))
{
int y=fa[x];
if (!isroot(y))
{
if (which(x)^which(y)) Rotate(x);
else Rotate(y);
}
Rotate(x);
}
}
inline void access(int x)
{
int y=0;
while (x)
{
splay(x); son[x][1]=y;
push_up(x); y=x; x=fa[x];
}
}
void beroot(int x){access(x);splay(x);Reverse(x);}
inline int getroot(int x)
{
access(x); splay(x);
while (son[x][0]) x=son[x][0];
return x;
}
inline void link(int x,int y)
{
beroot(x);
fa[x]=y;
}
inline void del(int x)
{
splay(x);
for(int i=0;i<2;i++)
{
if (!son[x][i]) continue;
fa[son[x][i]]=fa[x];
fa[x]=son[x][i]=0;
}
}
} LCT;
int qpow(int x,int n)
{
int res=1;
while(n)
{
if (n&1) res=(LL)res*x%mod;
x=(LL)x*x%mod; n>>=1;
}
return res;
}
int main()
{
IO;
file(1007);
int T; cin>>T;
while(T--)
{
cin>>n>>m;
q.clear(); LCT.init(n+m+1);
for(int i=1;i<=m;i++)
{
con[i]=0; int x,y,w;
cin>>x>>y>>w; g[i]=Edge{x,y,w};
}
sort(g+1,g+1+m);
for(int i=1;i<=m;i++)
{
int x=g[i].x,y=g[i].y,w=g[i].w;
if (LCT.getroot(x)==LCT.getroot(y))
{
LCT.beroot(x);
LCT.access(y);
LCT.splay(y);
int id=LCT.mn[y];
LCT.del(id+n); con[id]=i;
q.push_back(P(w+g[id].w,w));
} else q.push_back(P(-INF,w));
LCT.val[x]=LCT.val[y]=INF;
int e=i+n; LCT.val[e]=i;
LCT.link(x,e); LCT.link(y,e);
}
for(int i=1;i<=m;i++)
{
int w=g[i].w;
if (con[i]) q.push_back(P(w+g[con[i]].w,~w));
else q.push_back(P(INF,~w));
}
int e=0;
LL sum=0,sqsum=0;
LL ex2=1e18,e2x=1e18;
sort(q.begin(), q.end());
for(auto i:q)
{
LL b,c; b=i.second;
if (b>=0) e++,c=b*b; else e--,b=-~b,c=-(b*b);
sum+=b; sqsum+=c;
if (e==n-1)
{
LL x=sqsum-sum/(n-1)*sum-sum%(n-1)*sum/(n-1);
LL y=-sum%(n-1)*sum%(n-1); if (y<0) y+=n-1,x--;
if (ex2+(e2x>y)>x) ex2=x,e2x=y;
}
}
int inv=qpow(n-1,mod-2);
cout<<(ex2+e2x*inv%mod)%mod*inv%mod< ,我也要删除,但有可能
。就是说在删掉(i,j)后我就默认把均值调整到大于这两条边权之和的一半了,那么此时o和p这一对应该早就已经选择p,但是在删除之前还是选择了o,这里就会出现错误。所以说,我们不妨按照这个两条矛盾边的边权和的一半排一个序。
,这里i和j表示小的边和大的边,权值设置为大边的权值。同时对应要把前一个边删除的操作也要保存,优先级还是
,权值设置为前面小边权值的取反。最后,还要存下把所有的没有矛盾的边也删掉的操作,对应优先级为正无穷大,权值为对应边的权值的取反。
即平方和的均值减去和的均值的平方。所以我们比较的是分为两部分,一个是平方和,一个是和。最后是平方和除以(n-1),和的平方除以(n-1)的平方。我们通分一下,就是 (平方和-和的平方/n-1)) /(n-1)。为了比较最小我们选择比较分子,取最小的分子,而这个分子还有一个除法,这个除法也是不能利用逆元的,所以我学习标称利用了一种很骚的操作。具体见代码:
#include