文章目录
A. Ancient Distance
给出一棵树,选定 k k k 个关键点,每个点往根方向走到的第一个关键点为最近关键点,令 f ( k ) = max i = 1 n ( i f(k)=\max_{i=1}^n(i f(k)=maxi=1n(i 到最近关键点的距离 ) ) ),求 ∑ k = 1 n f ( k ) \sum_{k=1}^n f(k) ∑k=1nf(k)。
他要的是枚举关键点数量,容易转化成枚举最大距离,然后求出需要的最少关键点数。
枚举出最大距离 x x x 后,就是个比较简单的贪心了,每次取深度最大的点,往上跳 x x x 步,将这个点标记为关键点即可,正确性显然,因为要覆盖整个深度最大的点,肯定要在往上跳 p ∈ [ 0 , x ] p\in[0,x] p∈[0,x] 次的祖先中选一个当关键点,而选最上面那个能覆盖的点最多,肯定贪心选
代码如下:
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 200010
int n;
struct edge{int y,next;}e[maxn];
int first[maxn],len;
void buildroad(int x,int y){e[++len]=(edge){y,first[x]};first[x]=len;}
int f[maxn][20],deep[maxn];
int dfn[maxn],con[maxn],old[maxn],id;
void dfs(int x){
dfn[x]=++id;old[id]=x;
for(int i=first[x];i;i=e[i].next)
f[e[i].y][0]=x,deep[e[i].y]=deep[x]+1,dfs(e[i].y);
con[x]=id;
}
struct par{
int id,deep;par(int x=0,int y=0):id(x),deep(y){}
bool operator <(const par &B)const{return deep<B.deep;}
};
struct node{
int l,r,mid;par ma,ma_;node *zuo,*you;
node(int x,int y):l(x),r(y),mid(l+r>>1){
if(x<y){
zuo=new node(l,mid);
you=new node(mid+1,r);
ma_=ma=max(zuo->ma,you->ma);
}else zuo=you=NULL,ma_=ma=par(old[x],deep[old[x]]);
}
void change(int x,int y){
if(l==x&&r==y){ma=par(0,0);return;}
if(y<=mid)zuo->change(x,y);
else if(x>=mid+1)you->change(x,y);
else zuo->change(x,mid),you->change(mid+1,y);
ma=max(zuo->ma,you->ma);
}
void ch_back(int x,int y){
ma=ma_;
if(l==x&&r==y)return;
if(y<=mid)zuo->ch_back(x,y);
else if(x>=mid+1)you->ch_back(x,y);
else zuo->ch_back(x,mid),you->ch_back(mid+1,y);
}
}*root;
int jump(int x,int h){
if(deep[x]<=h)return 1;
for(int i=18;i>=0;i--)if((1<<i)<=h)h-=(1<<i),x=f[x][i];
return x;
}
int need[maxn];
vector<par>ch;
int main()
{
while(~scanf("%d",&n))
{
len=id=0;
for(int i=0;i<=n;i++){
first[i]=need[i]=0;
memset(f[i],0,20<<2);
}
for(int i=2,fa;i<=n;i++)scanf("%d",&fa),buildroad(fa,i);
deep[1]=1;dfs(1);
for(int j=1;j<=18;j++)
for(int i=1;i<=n;i++)
f[i][j]=f[f[i][j-1]][j-1];
root=new node(1,n);
for(int i=0;i<n;i++){
need[i]=0;ch.clear();
while(root->ma.deep>0){
need[i]++;
int x=root->ma.id;
x=jump(x,i);
root->change(dfn[x],con[x]);
ch.push_back(par(dfn[x],con[x]));
}
for(int j=0;j<ch.size();j++)root->ch_back(ch[j].id,ch[j].deep);
if(need[i]==1)break;
}
int now=n-1,ans=0;
for(int i=1;i<=n;i++){
while(now&&need[now-1]<=i)now--;
ans+=now;
}
printf("%d\n",ans);
}
}
B. Basic Gcd Problem
定义
f c ( x ) = { max i = 1 x − 1 c × f c ( gcd ( x , i ) ) ( x > 1 ) 1 ( x = 1 ) f_c(x)=\begin{cases}\max_{i=1}^{x-1} c\times f_c(\gcd(x,i))~~~~(x>1)\\1~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~(x=1)\end{cases} fc(x)={maxi=1x−1c×fc(gcd(x,i)) (x>1)1 (x=1)
现在给你若干组询问,每次询问给出 n i , c i n_i,c_i ni,ci,求 f c i ( n i ) f_{c_i}(n_i) fci(ni)。
要使得 f c ( x ) f_c(x) fc(x) 最大,显然要从 x x x 的次大因子转移过来,每次转移会少一个最小的质因子,所以可以转移质因子个数次,以这个次数为指数求以 c c c 为底的幂就是答案。
代码如下:
#include <cstdio>
#define maxn 1000010
#define mod 1000000007
int T,n,m;
int ksm(int x,int y){int re=1;for(;(y&1?re=1ll*re*x%mod:0),y;y>>=1,x=1ll*x*x%mod);return re;}
int mindiv[maxn],f[maxn];
void work()
{
for(int i=2;i<=maxn-10;i++)
for(int j=i;j<=maxn-10;j+=i)
if(!mindiv[j])mindiv[j]=i;
for(int i=2;i<=maxn-10;i++)f[i]=f[i/mindiv[i]]+1;
}
int main()
{
scanf("%d",&T);work();
while(T--)scanf("%d %d",&n,&m),printf("%d\n",ksm(m,f[n]));
}
C. Count New String
给出一个字符串 s s s,定义 f ( S , x , y ) [ i ] = max j = x i S j f(S,x,y)[i]=\max_{j=x}^i S_j f(S,x,y)[i]=maxj=xiSj,令 A = { f ( f ( S , x 1 , y 1 ) , x 2 , y 2 ) ∣ 1 ≤ x 1 ≤ x 2 ≤ y 2 ≤ y 1 ≤ ∣ S ∣ } A=\{f(f(S,x_1,y_1),x_2,y_2)|1\leq x_1\leq x_2\leq y_2\leq y_1\leq |S|\} A={f(f(S,x1,y1),x2,y2)∣1≤x1≤x2≤y2≤y1≤∣S∣},求 ∣ A ∣ |A| ∣A∣。
容易发现,当 f f f 嵌套在一起时,外层的 f f f 相当于取内层 f f f 的一个子串,如 f ( f ( S , x 1 , y 1 ) , x 2 , y 2 ) = f ( S , x 1 , y 1 ) [ x 2 f(f(S,x_1,y_1),x_2,y_2)=f(S,x_1,y_1)[x_2 f(f(S,x1,y1),x2,y2)=f(S,x1,y1)[x2 ~ y 2 ] y_2] y2],所以实际上就是要求所有 f ( S , x , y ) f(S,x,y) f(S,x,y) 有多少个不同的子串。
观察一下就能知道, f ( S , x , y ) ( y ∈ [ x , n ] ) f(S,x,y)(y\in[x,n]) f(S,x,y)(y∈[x,n]) 一定是 f ( S , x , n ) f(S,x,n) f(S,x,n) 的一个前缀,所以我们只需要统计所有 f ( S , x , n ) f(S,x,n) f(S,x,n) 含有多少个不同的子串即可。
从后往前考虑所有 f ( S , x , n ) f(S,x,n) f(S,x,n),设 n e [ x ] ne[x] ne[x] 表示下一个最早的大于 x x x 的字符的位置,那么可以发现, f ( S , n e [ x ] , n ) f(S,ne[x],n) f(S,ne[x],n) 一定是 f ( S , x , n ) f(S,x,n) f(S,x,n) 的一个后缀,也就是说,如果我们将所有 f ( S , x , n ) f(S,x,n) f(S,x,n) 拿来建字典树,那么 f ( S , x , n ) f(S,x,n) f(S,x,n) 就会使树上多 n e [ x ] − x ne[x]-x ne[x]−x 个节点,则总结点数的上限为 10 n 10n 10n。
然后对着这棵字典树造后缀自动机即可统计出不同子串数量,代码如下:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010
int n,ne[maxn],la[maxn];
char s[maxn];
struct state{int len,link,next[10];}st[maxn*20];
int last=0,now,p,q,id=0;
void extend(int x)
{
now=++id;st[now].len=st[last].len+1;
for(p=last;p!=-1&&!st[p].next[x];p=st[p].link)st[p].next[x]=now;
if(p!=-1)
{
q=st[p].next[x];
if(st[p].len+1==st[q].len)st[now].link=q;
else
{
int clone=++id;
st[clone]=st[q];st[clone].len=st[p].len+1;
for(;st[p].next[x]==q;p=st[p].link)st[p].next[x]=clone;
st[q].link=st[now].link=clone;
}
}
last=now;
}
int main()
{
scanf("%s",s+1);n=strlen(s+1);
memset(la,63,26<<2);
for(int i=n;i>=1;i--){
ne[i]=n+1;
for(int j=s[i]-'a';j<10;j++)ne[i]=min(ne[i],la[j]);
la[s[i]-'a']=i;
}
memset(la,0,sizeof(la));st[0].link=-1;
for(int i=n;i>=1;i--){
last=la[ne[i]];
for(int j=ne[i]-1;j>=i;j--)extend(s[i]-'a');
la[i]=last;
}
long long ans=0;
for(int i=1;i<=id;i++)
ans+=st[i].len-st[st[i].link].len;
printf("%lld",ans);
}
D. Dividing Strings
给出一个只含数字的字符串,你可将它切开成若干份,然后将每一份看成一个十进制数(不能含有前导零),要求最大值和最小值之差最小。
容易发现,将每一份的大小切成 1 1 1,那么答案一定不超过 9 9 9。在此基础上,考虑使答案更优。
长度大于 1 1 1 的切法有两种:
- 长度都相同。枚举一下 n n n 的因子判断一下就好。
- 长度不同。设最短的长度为 k k k,那么最长的长度肯定为 k + 1 k+1 k+1,容易发现,长度为 k k k 的一定形如 99...9 x 99...9x 99...9x,长的一定形如 100...0 x 100...0x 100...0x,所以也很容易判断。
具体细节看代码吧:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 100010
int T,n;
char s[maxn];
int go(int block){
static int ma[maxn],mi[maxn];
for(int i=1;i<=block;i++)ma[i]=0,mi[i]=9;
for(int i=1;i<=n/block;i++){
if(s[(i-1)*block+1]=='0')return 9;
int bigger=0,smaller=0;
for(int j=1;j<=block;j++){
int c=s[(i-1)*block+j]-'0';
if(!bigger&&c!=ma[j])bigger=c>ma[j]?1:-1;
if(!smaller&&c!=mi[j])smaller=c<mi[j]?1:-1;
}
for(int j=1;j<=block;j++){
if(bigger==1)ma[j]=s[(i-1)*block+j]-'0';
if(smaller==1)mi[j]=s[(i-1)*block+j]-'0';
}
}
int last=0;
for(int i=1;i<=block;i++){
last=last*10+ma[i]-mi[i];
if(i<block&&last>1)return 9;
}
return last;
}
int ma,mi,ans;
void init(){ma=-100;mi=100;}
void go2(int k){
if(k==n-1)return;
init();
for(int i=1;i<=n;i++){
if(s[i]=='1'){
if(i+k>n)return;
for(int j=i+1;j<i+k;j++)if(s[j]!='0')return;
ma=max(ma,s[i+k]-'0');
i+=k;
}else if(s[i]=='9'){
if(i+k-1>n)return;
for(int j=i;j<i+k-1;j++)if(s[j]!='9')return;
mi=min(mi,s[i+k-1]-'0');
i+=k-1;
}else return;
}
if(ma!=-100&&mi!=100)ans=min(ans,ma-mi+10);
}
int main()
{
scanf("%d",&T);while(T--)
{
scanf("%d %s",&n,s+1);
init();
for(int i=1;i<=n;i++){
ma=max(ma,s[i]-'0');
mi=min(mi,s[i]-'0');
}
ans=ma-mi;
for(int i=2;i*i<=n;i++)if(n%i==0){
ans=min(ans,go(i));
if(i*i!=n)ans=min(ans,go(n/i));
}
int k=0;
for(int i=1;i<=n;i++){
if(s[i]=='1'){
int j=i+1;k=1;
while(j<=n&&s[j]=='0')k++,j++;
break;
}
}
//由于不确定最后的0是100...0x中的0还是x,所以要尝试k和k-1两种情况
if(k>1)go2(k);
if(k>2)go2(k-1);
else if(n>2){//特判k=1,此时99...9x中没有前导9,只能看100...0x中的1
init();
for(int i=1;i<=n;){
if(i<n&&s[i]=='1'){
int c=10+(s[i+1]-'0');
ma=max(ma,c);
mi=min(mi,c);
i+=2;
}else{
ma=max(ma,s[i]-'0');
mi=min(mi,s[i]-'0');
i++;
}
}
ans=min(ans,ma-mi);
}
printf("%d\n",ans);
}
}
E. Eliminate++
给出一个 1 1 1 ~ n n n 的排列 a a a( n n n 是奇数),一次操作可以选定三个连续的数,然后保留中位数并删掉另外两个,操作 n − 1 2 \frac {n-1} 2 2n−1 次后就只剩一个数,现在要求出 a n s ans ans 数组, a n s i = 1 ans_i=1 ansi=1 表示 a i a_i ai 可能是留到最后的数。
先考虑暴力,考虑每个数是否可能留到最后。
考虑一个数 x x x,一个显然的转化是将所有大于 x x x 的数变成 1 1 1,小于 a i a_i ai 的数变成 0 0 0。那么就只有两种消除方式:1、三个一样的,消除两个;2、存在 01 01 01 对,那么随便再和旁边一个组合起来就能消掉这个 01 01 01 对。
有一个性质,如果原问题有解,那么一定存在一种方案,先对 x x x 左右两边的数进行操作,最后再进行跨 x x x 的操作(跨 x x x 的操作是指 x x x 在中间的操作),证明显然。
另一个性质,假如左右进行若干次消除后 0 0 0 和 1 1 1 的数量相同,那么必然有解。证明很简单,消去所有 01 01 01 对——此时 0 0 0 和 1 1 1 数量依然相同,那么两边就分别只剩下数量相同的 0 0 0 和 1 1 1 了。
那么怎么判断是否能做到 01 01 01 数量相同呢?考虑贪心,假设 0 0 0 比 1 1 1 少,那么就要尽量凑出 111 111 111 然后就可以删掉两个 1 1 1。记 o n e one one 表示此时 1 1 1 的数量,假如新增了一个 0 0 0,那么就配对出 10 10 10,使 o n e − 1 one-1 one−1,这是为了让剩下的 1 1 1 可以和后面的 1 1 1 配对,假如新增了一个 1 1 1,那么就让 o n e + 1 one+1 one+1,若此时 o n e = 3 one=3 one=3,那么让 o n e − 2 one-2 one−2 表示三个 1 1 1 配对成功于是消掉两个。
这个贪心左右两侧分别做一次,看看做完之后 1 1 1 的数量是否不超过 0 0 0 的数量,是就有解,因为就算 1 1 1 少于 0 0 0,也可以通过贪心时少删几对使得数量相等。
但是现在直接做是 O ( n 2 ) O(n^2) O(n2) 的,考虑优化。
考虑从小到大求解 a i a_i ai,那么每次就是让序列中一个 1 1 1 变成 0 0 0。可以发现上面贪心的过程可以用线段树优化,那么就做完了。
具体来说,设线段树上管理区间 l l l ~ r r r 的节点包含的 n u m [ i ] num[i] num[i] 表示, o n e = i one=i one=i 时贪心经过区间 l l l ~ r r r 后最多能配对多少对 1 1 1, r e s [ i ] res[i] res[i] 表示 o n e = i one=i one=i 在贪心后的 o n e one one 是多少,然后实现看代码就能懂。
代码如下:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 1000010
int T,n,a[maxn],p[maxn],ans[maxn];
struct par{
int num[3],res[3];
void init(){
for(int i=0;i<3;i++)//一开始为1
num[i]=i>=2,res[i]=i%2+1;
}
void change(){
for(int i=0;i<3;i++)//变成了0
num[i]=0,res[i]=max(i-1,0);
}
};
par merge(par x,par y){
par re;
for(int i=0;i<3;i++){
re.num[i]=x.num[i]+y.num[x.res[i]];
re.res[i]=y.res[x.res[i]];
}
return re;
}
struct node *s[maxn*2],*root;int id=0;
struct node{
int l,r,mid;par z;node *zuo,*you;
void change(int x){
if(l==r)return z.change();
if(x<=mid)zuo->change(x);
else you->change(x);
z=merge(zuo->z,you->z);
}
void ask(int x,int y,int &rs,int &tot){
if(l==x&&r==y){
tot+=z.num[rs];
rs=z.res[rs];
return;
}
if(y<=mid)zuo->ask(x,y,rs,tot);
else if(x>=mid+1)you->ask(x,y,rs,tot);
else zuo->ask(x,mid,rs,tot),you->ask(mid+1,y,rs,tot);
}
};
void build(node *&now,int x,int y){
now=s[id++];
now->l=x,now->r=y,now->mid=(x+y>>1);
if(x<y){
build(now->zuo,x,now->mid);
build(now->you,now->mid+1,y);
now->z=merge(now->zuo->z,now->you->z);
}else now->zuo=now->you=NULL,now->z.init();
}
int main()
{
for(int i=0;i<maxn*2;i++)s[i]=new node();
scanf("%d",&T);while(T--)
{
scanf("%d",&n);int mid=(n+1)>>1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]),p[a[i]]=i;
id=0;build(root,1,n);
for(int i=1;i<mid;i++){//1比0多
root->change(p[i]);
int tot=0,rs;
if(p[i]>1)rs=0,root->ask(1,p[i]-1,rs,tot);
if(p[i]<n)rs=0,root->ask(p[i]+1,n,rs,tot);
ans[p[i]]=(i-1>=n-i-2*tot);//删掉若干对1后0是否比1多
}
ans[p[mid]]=1;id=0;build(root,1,n);
for(int i=n;i>mid;i--){//0比1多,此时线段树内维护的是能配对的0的数量
root->change(p[i]);
int tot=0,rs;
if(p[i]>1)rs=0,root->ask(1,p[i]-1,rs,tot);
if(p[i]<n)rs=0,root->ask(p[i]+1,n,rs,tot);
ans[p[i]]=(n-i>=i-1-2*tot);
}
for(int i=1;i<=n;i++)printf("%d",ans[i]);printf("\n");
}
}
F. Finding the Order
给出 A C , A D , B C , B D AC,AD,BC,BD AC,AD,BC,BD,问是 A B / / C D AB//CD AB//CD 还是 A B / / D C AB//DC AB//DC。
分类讨论一下,假如 C , D C,D C,D 两点都离 A A A 比较近,那么判断一下 B B B 到两点距离即可,否则判断 A A A 到两点距离。
代码如下:
#include <cstdio>
#include <algorithm>
using namespace std;
int T,a,b,c,d;
int main()
{
scanf("%d",&T);while(T--)
{
scanf("%d %d %d %d",&a,&b,&c,&d);
if(a<c&&b<d){
if(c>d)printf("AB//CD\n");
else printf("AB//DC\n");
}else{
if(a<b)printf("AB//CD\n");
else printf("AB//DC\n");
}
}
}
H. Harder Gcd Problem
你需要将 1 1 1 ~ n n n 中的数进行尽可能多的配对,要求每一对的 gcd > 1 \gcd>1 gcd>1。
容易发现, 1 1 1 和大于 n 2 \dfrac n 2 2n 的质数都是不可能配对的。
剩下的数中,从大到小考虑每个质数和它的没有配对的倍数们,假如有偶数个,那么相互配对即可,如果有奇数个,那么保留这个质数的 2 2 2 倍,其他相互配对,然后最后 2 2 2 与 2 2 2 的倍数们尽可能多的配对即可,显然这样配对数最多。
代码如下:
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 200010
int T,n,m;
int prime[maxn],t=0;
bool v[maxn];
void work(){
for(int i=2;i<=maxn-10;i++){
if(!v[i])prime[++t]=i;
for(int j=1;j<=t&&i*prime[j]<=maxn-10;j++){
v[i*prime[j]]=true;
if(i%prime[j]==0)break;
}
}
}
int zhan[maxn],top=0,mat[maxn],tot;
vector<int>vec;
void match(int x,int y){mat[x]=y;mat[y]=x;tot++;}
int main()
{
work();scanf("%d",&T);while(T--)
{
scanf("%d",&n);
memset(mat,0,(n+1)<<2);tot=0;
for(int i=1;prime[i]<=n/2;i++)zhan[++top]=prime[i];
while(top){
vec.clear();
for(int i=zhan[top];i<=n;i+=zhan[top])
if(!mat[i])vec.push_back(i);
if(vec.size()%2){
match(vec[0],vec[2]);
for(int i=3;i<vec.size();i+=2)
match(vec[i],vec[i+1]);
}else{
for(int i=0;i<vec.size();i+=2)
match(vec[i],vec[i+1]);
}
top--;
}
printf("%d\n",tot);
for(int i=1;i<=n;i++)if(mat[i]>i)printf("%d %d\n",i,mat[i]);
}
}
I. Investigating Legions
有 n n n 个人,每个人属于一个团队,有 m m m 个团队( m m m 未知),有一个 a a a 数组, a i , j = 1 / 0 a_{i,j}=1/0 ai,j=1/0 表示第 i i i 个人和第 j j j 个人是否在同一个团队内,但是现在有一个 S S S, a a a 中的每一位有 1 S \dfrac 1 S S1 的概率取反,现在给出被搞过的 a a a,求出原来每个人属于哪个团队。
注意到 20 ≤ S ≤ 100 20\leq S \leq 100 20≤S≤100,即取反的概率很小。如果将 a i , j a_{i,j} ai,j 看成 i , j i,j i,j 之间是否有边,那么一个团队原来就是一张完全图, S S S 搞过之后,会少一点点边,又会连出去一点点边,但是大部分原来的边都还在,所以图的大概形状是保留下来了的。
于是就可以乱搞了,代码如下:
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 1210
int T,n,S,f[maxn][maxn],be[maxn];
char s[maxn*maxn];
vector<int>vec;
int main()
{
scanf("%d",&T);while(T--)
{
scanf("%d %d",&n,&S);
scanf("%s",s);int now=0;
for(int i=1;i<=n;i++){
f[i][i]=1;
for(int j=i+1;j<=n;j++)
f[i][j]=f[j][i]=s[now++]-'0';
}
int cnt=0;
memset(be,0,sizeof(be));
for(int i=1;i<=n;i++)if(!be[i]){
cnt++;vec.clear();
for(int j=1;j<=n;j++)if(!be[j]&&f[i][j])vec.push_back(j);
for(int j=1;j<=n;j++)if(!be[j]){
int tot=0;
for(int k=0;k<vec.size();k++)if(f[j][vec[k]])tot++;
if(tot>=vec.size()/2)be[j]=cnt;
}
}
for(int i=1;i<=n;i++)printf("%d ",be[i]-1);printf("\n");
}
}
J.Jumping on the Graph
给出一张图,定义一条路径的权值为路径上边权的次大值,令 D ( i , j ) D(i,j) D(i,j) 表示 i i i 到 j j j 的权值最小的路径的权值,求 ∑ i = 1 n ∑ j = i + 1 n D ( i , j ) \sum_{i=1}^n \sum_{j=i+1}^nD(i,j) ∑i=1n∑j=i+1nD(i,j)。
先考虑暴力,枚举作为次大值的边(设为 x x x),将边权大于 x x x 的边权值设为 1 1 1,权值小于 x x x 的边权值设为 0 0 0,那么一条以 x x x 作为次大值的路径一定经过了恰好一条 1 1 1 边和 x x x 边。
假如将 0 0 0 边连接的点看成一个连通块,那么我们需要统计的路径一定形如:连通块A → \to → 经过 1 1 1 边 → \to → 连通块B → \to → 经过 x x x 边 → \to → 连通块C,并且不存在 1 1 1 边连接连通块A与连通块C。
设 X , Y X,Y X,Y 为 x x x 边连接的两个连通块,如果能维护出 X X X 和 Y Y Y 能到达的连通块,那么就可以通过这条柿子计算贡献: s i z e X × s i z e p 1 + s i z e Y × s i z e p 2 size_X\times size_{p_1}+size_Y\times size_{p_2} sizeX×sizep1+sizeY×sizep2,其中 p 1 p_1 p1 是 Y Y Y 能到达而 X X X 不能到达的连通块们, p 2 p_2 p2 类似。
那么就尝试维护每个连通块能到达的连通块集合,设 n b r [ i ] nbr[i] nbr[i](即neighbor)为连通块 i i i 能到达的连通块们,当统计完一条 x x x 边的答案之后,这条边就要变成 0 0 0 边,连接 X , Y X,Y X,Y 两个连通块(设 s i z e n b r [ X ] < s i z e n b r [ Y ] size_{nbr[X]}<size_{nbr[Y]} sizenbr[X]<sizenbr[Y]),用启发式合并将 n b r [ X ] nbr[X] nbr[X] 合并到 n b r [ Y ] nbr[Y] nbr[Y] 中,时间复杂度就是 O ( n log n ) O(n\log n) O(nlogn) 的。
但是统计答案时,我们不能直接遍历 n b r [ X ] nbr[X] nbr[X] 和 n b r [ Y ] nbr[Y] nbr[Y] 来求 p 1 , p 2 p_1,p_2 p1,p2,否则会TLE,于是需要维护另一个东西: n b r s i z e nbrsize nbrsize, n b r s i z e [ x ] nbrsize[x] nbrsize[x] 表示 x x x 的neighbor们的 s i z e size size 之和,那么只需要遍历 n b r [ X ] nbr[X] nbr[X] 求出 n b r [ X ] nbr[X] nbr[X] 和 n b r [ Y ] nbr[Y] nbr[Y] 的交集,然后用 n b r s i z e [ x ] − nbrsize[x]- nbrsize[x]−交集大小 就可以得到 p 2 p_2 p2, p 1 p_1 p1 类似。
但是问题又来了,合并 X , Y X,Y X,Y 时,我们不单单需要修改 X X X 的neighbor的 n b r s i z e nbrsize nbrsize,还需要修改 Y Y Y 的neighbor的 n b r s i z e nbrsize nbrsize,这不是还要遍历 n b r [ Y ] nbr[Y] nbr[Y] 吗?
于是这里可以考虑分块算法,记 n b r [ x ] ≥ 2 m nbr[x]\geq \sqrt{2m} nbr[x]≥2m 的 x x x 为big neighbor,合并 X , Y X,Y X,Y 时,假如 Y Y Y 不是big neighbor,那么就遍历 n b r [ Y ] nbr[Y] nbr[Y] 更新 n b r s i z e nbrsize nbrsize,假如 Y Y Y 是big neighbor,那么就不遍历了,但是当 X X X 的neighbor合并过来时,不将 Y Y Y 记录到他们的 n b r nbr nbr 里,而是记录到另一个集合—— b i g n b r bignbr bignbr 里。
由于 b i g n b r bignbr bignbr 的数量不超过 2 m \sqrt {2m} 2m,所以求解时直接遍历 b i g n b r bignbr bignbr 来求 p 1 , p 2 p_1,p_2 p1,p2 即可,时间复杂度 O ( n log n + m m ) O(n\log n+m\sqrt m) O(nlogn+mm)。
还要注意,当一个连通块 x x x 从neighbor变成big neighbor时,需要遍历一下他的 n b r [ x ] nbr[x] nbr[x],让neighbor们将 x x x 从 n b r nbr nbr 移到 b i g n b r bignbr bignbr 内。
代码如下:
#include <cstdio>
#include <cstring>
#include <cmath>
#include <unordered_map>
#include <algorithm>
using namespace std;
#define maxn 100010
#define to j.first
#define pr pair<int,bool>
int n,m;
struct edge{int x,y,z;}e[maxn<<1];
int fa[maxn];
int findfa(int x){return x==fa[x]?x:fa[x]=findfa(fa[x]);}
unordered_map<int,bool>nbr[maxn],bignbr[maxn];
int size[maxn],nbrsize[maxn];
bool small[maxn];
bool cmp(edge x,edge y){return x.z<y.z;}
long long ans=0;
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d %d %d",&e[i].x,&e[i].y,&e[i].z);
if(e[i].x==e[i].y)continue;
nbrsize[e[i].x]++;nbrsize[e[i].y]++;
}
int sqm=sqrt(m<<1);
for(int i=1;i<=n;i++)if(nbrsize[i]<=sqm)small[i]=true;
for(int i=1;i<=m;i++){
int x=e[i].x,y=e[i].y; if(x==y)continue;
if(small[x])nbr[y][x]=1; else bignbr[y][x]=1;
if(small[y])nbr[x][y]=1; else bignbr[x][y]=1;
}
for(int i=1;i<=n;i++)nbrsize[i]=nbr[i].size(),fa[i]=i,size[i]=1;
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++)if(findfa(e[i].x)!=findfa(e[i].y)){
int x=findfa(e[i].x),y=findfa(e[i].y);
if(nbr[x].size()>nbr[y].size())swap(x,y);
//断开 x,y 中间的 1 边
if(small[x])nbr[y].erase(x),nbrsize[y]-=size[x];
else bignbr[y].erase(x);
if(small[y])nbr[x].erase(y),nbrsize[x]-=size[y];
else bignbr[x].erase(y);
//更新答案
int nbrszx=nbrsize[x],nbrszy=nbrsize[y];
for(pr j:nbr[x])if(nbr[y].count(to))nbrszx-=size[to],nbrszy-=size[to];
for(pr j:bignbr[x])if(!bignbr[y].count(to))nbrszx+=size[to];
for(pr j:bignbr[y])if(!bignbr[x].count(to))nbrszy+=size[to];
ans+=(1ll*size[x]*nbrszy+1ll*size[y]*nbrszx)*e[i].z;
//断开 x 额 neighbor
if(small[x]){
for(pr j:nbr[x])nbr[to].erase(x),nbrsize[to]-=size[x];
for(pr j:bignbr[x])nbr[to].erase(x),nbrsize[to]-=size[x];
}else{
for(pr j:nbr[x])bignbr[to].erase(x);
for(pr j:bignbr[x])bignbr[to].erase(x);
}
//将 x 的 neighbor 连向 y
if(small[y]&&nbr[y].size()>sqm){// y 从 neighbor 变成 big neighbor
small[y]=false;
for(pr j:nbr[y])nbr[to].erase(y),nbrsize[to]-=size[y] , bignbr[to][y]=1;
for(pr j:bignbr[y])nbr[to].erase(y),nbrsize[to]-=size[y] , bignbr[to][y]=1;
for(pr j:nbr[x])bignbr[to][y]=1;
for(pr j:bignbr[x])bignbr[to][y]=1;
}else{
if(small[y]){
for(pr j:nbr[y])nbrsize[to]+=size[x];
for(pr j:bignbr[y])nbrsize[to]+=size[x];
for(pr j:nbr[x])if(!nbr[to].count(y))nbr[to][y]=1,nbrsize[to]+=size[x]+size[y];
for(pr j:bignbr[x])if(!nbr[to].count(y))nbr[to][y]=1,nbrsize[to]+=size[x]+size[y];
}else{
for(pr j:nbr[x])bignbr[to][y]=1;
for(pr j:bignbr[x])bignbr[to][y]=1;
}
}
//将 y 连向 x 的 neighbor
for(pr j:nbr[x])if(!nbr[y].count(to))nbr[y][to]=1,nbrsize[y]+=size[to];
for(pr j:bignbr[x])bignbr[y][to]=1;
nbr[x].clear();bignbr[x].clear();
fa[x]=y;size[y]+=size[x];
}
printf("%lld",ans);
}