2020牛客暑期多校训练营(第四场)题解

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=1x1c×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)1x1x2y2y1S},求 ∣ 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 的切法有两种:

  1. 长度都相同。枚举一下 n n n 的因子判断一下就好。
  2. 长度不同。设最短的长度为 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 2n1 次后就只剩一个数,现在要求出 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 one1,这是为了让剩下的 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 one2 表示三个 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 20S100,即取反的概率很小。如果将 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=1nj=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);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值