• Codeforces Educational Round 92 赛后解题报告(AG)


    Codeforces Educational Round 92 赛后解题报告

    惨 huayucaiji 惨

    A. LCM Problem

    赛前:A题嘛,总归简单的咯
    赛后:A题这种**题居然想了20min

    我说这句话,肯定是有原因的,我们看到 \(\operatorname{lcm}\) 会想到之前一些题:题目,我的题解

    自然就往 \(a\times b=\operatorname{gcd}(a,b)\times \operatorname{lcm}(a,b)\) 的方向思考,但是,这太麻烦了,我们可以这样想一个问题:

    如果我们现在有两个数 \(x,y\)。其中 \(x<y\)。那么我们一定有:

    \[\operatorname{lcm}(x,y)\geq 2x \]

    我们可以简略地证明一下,首先因为 \(x<y\),所以他们的 \(\operatorname{lcm}\) 一定大于 \(x\)。其次我们令 \(g=\operatorname{gcd}(x,y),x=q\cdot g,y=p\cdot g\)。所以 \(\operatorname{gcd}(p,q)=1\),即 \(p,q\) 互质,那么我们知道 \(\operatorname{lcm}(x,y)=g\cdot p\cdot q=x\cdot p\)。因为 \(p\) 是整数,又因为 \(x\neq y\)。所以 \(p\) 最小取 \(2\)。故 \(\operatorname{lcm}(x,y)\geq 2x\)

    我们只需要判断是否 \(2\times l\leq r\) 即可

    //Don't act like a loser.
    //You can only use the sode for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    int l,r,t;
    
    signed main() {
    	
    	t=read();
    	while(t--) {
    		l=read();r=read();
    		bool flag=0;
    		
    		if(l*2<=r) {
    			printf("%d %d\n",l,l*2);
    		}
    		else {
    			printf("-1 -1\n");
    		}
    		
    	}
    	return 0;
    }
    
    

    B. Array Walk

    我们先来分析题目中一个重要的条件:

    Also, you can't perform two or more moves to the left in a row.

    我们不能一次向左移动两格。所以这代表一次左后必定是右。那么我们很容易可以想到一个贪心的策略,我们无论如何,先走到 \(k+1\)。我们再来反悔,由于 \(z\) 很小,我们可以枚举往后走几步,并且维护前面相邻两个数的和的最大值,选择在这里后退前进。于是我们可以得到下面的程序:

    //Don't act like a loser.
    //You can only use the sode for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    
    const int maxn=1e5+10;
    
    int n,a[maxn],sum[maxn],maxx[maxn],k,z;
    
    signed main() {
    	int t=read();
    	while(t--) {
    		n=read();k=read();z=read();
    		for(int i=1;i<=n;i++) {
    			a[i]=read();
    			sum[i]=sum[i-1]+a[i];
    		}
    		fill(maxx,maxx+n+1,0);
    		
    		for(int i=1;i<=n;i++) {
    			maxx[i]=max(maxx[i-1],a[i]+a[i-1]);
    		}
    		int ans=sum[k+1];
    		for(int i=1;i<=z;i++) {
    			if(k+1-2*i>=2) {
    				ans=max(ans,maxx[k+1-2*i]*i+sum[k+1-2*i]);
    			}
    		}
    		
    		printf("%d\n",ans);
    	}
    	return 0;
    }
    
    

    呵呵,WA。

    我们再来仔细想想比如对于:

    1
    5 4 2
    50 40 30 20 10
    

    我们的最优方案是 \(50->40->50->40->50\) 答案为 \(230\)。但是我们上面的程序给出的答案是 \(50->40->50->40->30\)。答案为 \(210\)。为什么呢?原因就是我们珂以退回去后就不行再往前走了,留在后面的格子里。所以我们还要加上这一段代码:

    if(k+2-2*i>=2) {
    	ans=max(ans,sum[k+1-2*i]+i*(a[k-i*2+1]+a[k-i*2+2]));
    }
    

    注意一些下标的问题哦~

    C. Good String

    这个题比B简单吧

    我们可以通过题目得出以下结论:

    \[s_2\ \ s_3\ \ s_4.....s_{n-1}\ \ s_n\ \ s_1\\=s_n\ \ s_1\ \ s_2.....s_{n-3}\ \ s_{n-2}\ \ s_{n-1} \]

    所以我们有 \(s_1=s_3=s_5=....s_{n-1}=q\)\(s_2=s_4=s_6=.....s_{n}=p\)

    我们只需要枚举 \(p,q\) 即可,每次枚举出来后,顺次统计以下最少要删多少字符,用贪心一个一个扫就是最优的,这里就不再证明了。但是我们还有一些特殊情况要处理。

    如果我们最后删减版序列是 \(pqpqp\)。那么我们得出的两序列是 \(ppqpq\)\(qpqpp\)。显然在 \(p\neq q\) 时不等。所以当 \(p\neq q\)\(len\) 是奇数时,删去的数还要在加一。

    注意特判 \(len=1\)

    //Don't act like a loser.
    //You can only use the sode for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    
    int n;
    string s;
    
    signed main() {
    	
    	int t=read();
    	while(t--) {
    		cin>>s;
    		n=s.size();
    		
    		if(n==1) {
    			cout<<0<<endl;//特判 n=1
    			continue;
    		}
    		
    		int ans=n;
    		for(int i=0;i<=9;i++) {
    			for(int j=0;j<=9;j++) {
    				int num=0,sum=0;//num记录当前应该匹配哪个数
    				
    				for(int k=0;k<n;k++) {
    					if(!num) {
    						if(s[k]-'0'!=i) {
    							sum++;
    						}
    						else {
    							num=1-num;
    						}
    					}//分类
    					else {
    						if(s[k]-'0'!=j) {
    							sum++;
    						}
    						else {
    							num=1-num;
    						}
    					}
    				}
    				if(i!=j&&num) {
    					sum++;//删去的数+1
    				}
    				ans=min(ans,sum);
    			}
    		}
    		
    		printf("%d\n",ans);
    	}
    	return 0;
    }
    

    D. Segment Intersections

    我们先打开单词小本本记录一下 intersection 这个词(虽然我会)。它的意思是交叉,十字路口。

    我们切入正题。

    首先我们会发现问题分成两个类别。第一类是 \([l_1,r_1],[l_2,r_2]\) 有交集。即 \(\min(r_1,r_2)-\max(l_1,l_2)\geq 0\)。交集的长度(即题目中的 \(\operatorname{intersection\ length}\))也就为 \(\min(r_1,r_2)-\max(l_1,l_2)\)。如下图:

    那么此时我们选择这种做法(也就是官方解法)。我们每次将 \([l_1,r_1]\) 扩展为 \([l_1,r_1+1]\),或者同理将 \([l_2,r_2]\) 扩展为 \([l_2-1,r_2]\),知道两个区间完全相同为止,在这样扩充时,我们每用一步,就可以让交集长度增加 \(1\)。是最划算的。但是一旦两个区间完全相同,即 \(l_1=l_2,r_1=r_2\) 时,我们不能在这样操作了,我们必须让两个区间的同一边长度都增加 \(1\),才能让交集长度增加 \(1\)。这是唯一的选择了。那么第一类我们讨论完了。

    再来看第二类,也就是一开始没有交集。那么我们可以让它有交集在用第一种情况的处理方法来处理。我们要枚举一个 \(cnt\),代表要让多少对区间有交集。我们完全没有必要让所有的区间成双成对地有交集,毕竟有的用不上嘛~。我们让一堆区间有交集的最小步数就为 \(\max(l_1,l_2)-\min(r_1,r_2)\)。注意还要乘以 \(cnt\)。最后用第一种情况的处理方法来处理即可。时间复杂度为 \(O(n)\)

    注意细节!比如开 long long

    //Don't act like a loser.
    //You can only use the code for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    #define int long long//不开long long 见祖宗!
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    
    int calc(int n,int k,int l1,int r1,int l2,int r2) {
    	if(k<=0) {
    		return 0;
    	}
    	
    	if((max(r1,r2)-min(l1,l2))*n>=k+(min(r1,r2)-max(l1,l2))*n)//注意细节 {
    		return k;
    	}
    	else {
    		int tmp=(max(l1,l2)-min(l1,l2)+max(r1,r2)-min(r1,r2))*n;//细节
    		return tmp+(k-tmp)*2;
    	}
    }
    
    signed main() {
    	int t=read();
    	
    	while(t--) {
    		int n,k,l1,l2,r1,r2;
    		n=k=l1=l2=r1=r2=0;
    		cin>>n>>k>>l1>>r1>>l2>>r2;
    		
    		int ans=0x7fffffffffffffff;
    		if(min(r1,r2)<max(l1,l2)) {
    			for(int i=1;i<=n;i++) {
    				ans=min(ans,(max(l1,l2)-min(r1,r2))*i+calc(i,k,min(l1,l2),min(r1,r2),min(r1,r2),max(r1,r2)));//细节,我们要进行计算的是我们处理完的区间,是有交集的,保证有交集即可。
    			}
    		}
    		else {
    			ans=calc(n,k-(min(r1,r2)-max(l1,l2))*n,l1,r1,l2,r2);
    		}
    		
    		printf("%lld\n",ans);//lld的细节
    	}
    	return 0;
    }
    

    E. Calendar Ambiguity

    我们会发现这个题目只有一个可以入手的点,其他都是定义类条件。

    A pair (x,y) such that x<y is ambiguous if day x of month y is the same day of the week as day y of month x.

    用数学方法表示也就是

    \[xd+y\equiv yd+x\ (\operatorname{mod}\ w)\\xd+y-(yd+x)\equiv 0\ (\operatorname{mod}\ w)\\(d-1)(x-y)\equiv 0\ (\operatorname{mod}\ w) \]

    \(d-1\) 是一个定值。我们只需要关注 \(x-y\)。但是注意了,我们的 \(d-1\)\(w\) 可能有相同的质因子,这些就没有必要再在 \(x-y\) 包含了。所以我们令 \(w'=\frac{w}{\gcd(d-1,w)}\)。我们要求的就是有多少对 \((x,y)\) 满足 \(x-y\equiv 0\ (\operatorname{mod}\ w')\)

    为了求解这个问题,我们可以考虑弱化条件。假设我们现在求解有多少对 \((x,y)\) 满足 \(x-y=k\cdot w'\)。那么我们可以用数学方法来解决,就有 \(\min(m,d)-k\cdot w'\) 对。这个应该十分好理解。 我们再把条件还原,我们就可以枚举 \(k\),在累加答案。由于 \(x-y\leq \min(m,d)\)所以最后的答案就是:

    \[\sum\limits_{k=1}^{\lfloor\frac{\min(m,d)}{w'}\rfloor} \min(m,d)-k\cdot w' \]

    聪明的小伙伴可能会发现,这样的时间复杂度实在太大,到了 \(O(t\cdot\lfloor\frac{\min(m,d)}{w'}\rfloor)\) 应该是过不了的,大家可以试一试。那么我们就要想如何优化。我们再次观察这个式子,发现是一个等差数列,公差是 \(-w'\)。我们用高斯公式就可以解决这个问题。首项为 \(\min(d,m)-w'\),末项为 \(\min(d,m)-\lfloor\frac{\min(m,d)}{w'}\rfloor\cdot w'\),项数为 \(\lfloor\frac{\min(m,d)}{w'}\rfloor\)。所以最终答案为:

    \[\frac{(2\min(d,m)-\lfloor\frac{\min(m,d)}{w'}\rfloor\cdot w'-w')\times\lfloor\frac{\min(m,d)}{w'}\rfloor}2 \]

    这样时间复杂度为 \(O(\log(m+d))\)

    时间复杂度的大户竟是计算 \(\gcd\) 你敢相信?

    //Don't act like a loser.
    //You can only use the code for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    
    int m,d,w;
    
    int gcd(int x,int y) {
    	return y==0? x:gcd(y,x%y);
    }
    
    signed main() {
    	int t=read();
    	while(t--) {
    		m=read();d=read();w=read();
    		
    		w=w/gcd(w,d-1);
    		int n=min(m,d);
    		int num=n/w;
    		int ans=(2*n-w*num-w)*num/2;
    		
    		cout<<ans<<endl; 
    	}
    	return 0;
    }
    
    

    F. Bicolored Segments

    拿出单词小本本,我们再来记一个单词:
    embed:嵌入,本题中意思就是一个区间被另一个区间所包含。

    回归正题,我在写这篇题解时看了洛谷上一篇和我思路差不多的大佬写的题解,他的线段树优化DP写的不错的,大家可以看看,但是我觉得他的解法二不太清晰,特别是那个“对抗抵消”。可能蒟蒻不太能理解qwq,这里换一个角度在重新讲一下。

    我们这样想,既然有两种不同颜色的区间,就代表我们可以理解把它们为一个二分图的节点。如果你愿意,我们再把所有互为“不好的”区间连边。我们就得到了一个看着不错二分图。举个例子,我们题目中的样例二构图:

    样例二

    5
    5 8 1
    1 3 2
    3 4 2
    6 6 1
    2 10 2
    

    我们构成的二分图

    F

    我们再看这个图,是不是有一点感觉了,我们要求的正是这个图的最大点独立集。求最大点独立集,我们就要求最大匹配。这两个概念不知道的同学可以上网搜。我们令最大点独立集的答案为 \(x\),最大匹配的答案为 \(y\)。我们有 \(x+y=n\)\(n\) 就是点的个数。证明略我们的问题成功转化为这个二分图求最大匹配。

    我相信不会有人想着真的把图建出来用网络流求解吧,不会吧,不会吧。
    咳咳,我一开始就这么想的

    不扯远了,但确实我们的第一反应应该是网络瘤,毕竟是它的好伙伴二分图嘛。但是我们建图的时间复杂度就是 \(O(n^2)\),Dinic 做二分图的时间复杂度 \(O(m\sqrt{n})\),显然爆炸。我们要像一种高效的方法才行。

    我们先和上面的那篇 blog 一样,按每个区间的右端点为第一关键字排序,搞两个 multiset 记录我们选择了哪些区间。接下来的操作还是一样,但是我们的解释不太一样。我们要求的是最大匹配,如果我们在异色的 multiset 中,存在与我们现在考虑的这个区间形成“不好区间”的区间的话,我们就选择这两个点之间的这条边作为最大匹配中的一条边,把那个 muliset 中的点给抹掉,并且现在的这个区间也不加入到 multiset 中,因为我们这两个点不能再出现在最大匹配的边里了,这是最大匹配的定义要求的。否则这个区间我们可以加入到 multiset 中。这样解释应该更清楚一些吧(自认为)。也就不用去理解“对抗抵消”了。

    //Don't act like a loser.
    //You can only use the code for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    
    const int maxn=2e5+10; 
    
    int n=0,ans=0;
    struct segment {
    	int l,r,col;
    	
    	const operator < (const segment& y) {
    		return r<y.r;
    	}
    }s[maxn];
    
    multiset<int> sel[2];
    
    signed main() {
    	n=read();
    	for(int i=1;i<=n;i++) {
    		s[i].l=read();
    		s[i].r=read();
    		s[i].col=read()-1;
    	}
    	
    	sort(s+1,s+n+1);
    	
    	ans=n;
    	for(int i=1;i<=n;i++) {
    		if(sel[1-s[i].col].lower_bound(s[i].l)!=sel[1-s[i].col].end()) {
    			ans--;
    			sel[1-s[i].col].erase(sel[1-s[i].col].lower_bound(s[i].l));
    		}
    		else {
    			sel[s[i].col].insert(s[i].r);
    		}
    	}
    	
    	cout<<ans<<endl;
    	return 0;
    }
    
    

    G. Directing Edges

    有点意思的一道题

    首先推荐阅读一下DP之神UM的一篇关于换根DP的blog。阅读完后再来看本题解效果更佳。

    好,我们切回正题。首先我们来简化条件。如果现在给我们的是一颗有 \(n\) 个点的树。让我们来做这个问题,我们该怎么做呢?比如我们考虑下图这棵树:

    G1

    我们容易想到我们要做的是 DP。我们定义:\(f_u\) 表示以 \(u\) 为根节点的子树里,保证 \(u\) 是饱和的情况下,我们所能获得的最大收益为多少。假设我们已经知道了所有 \(u\) 的子节点的值,即 \(f_{v_1},f_{v_2},f_{v_3}....f_{v_d}\) 的值(其中 \(v_1,v_2....v_d\)\(u\) 的子节点)那么我们现在需要考虑的就是连接 \(u,v_i\) 的边的方向到底是什么了。现在我们就来考虑一下 \(3\)\(f_1\) 的贡献。

    如果我们的特殊点是 \(3,9,11\),现在我们考虑蓝色的边的方向。我们发现所有的特殊点都在以 \(3\) 为根的子树中。为了让 \(1\) 饱和,那么蓝色的边的方向就应该是由 \(3\)\(1\),从而让所有的特殊点都指向 \(1\),同时 \(f_1\) 也得到了 \(f_3\) 的全部收益。这种情况只有在所有特殊点都在一颗子树中时成立,我们可以决定这课子树到其父节点这条边的方向。 如下图:

    G2

    如果我们的特殊点是 \(1,2,6,8\),现在我们考虑蓝色的边的方向。我们发现所有的特殊点都不在以 \(3\) 为根的子树中。为了获得 \(f_3\) 的收益,蓝色的边的方向就应该是由 \(1\)\(3\)这种情况只有在所有特殊点都在一颗子树外时成立,我们可以决定这课子树到其父节点这条边的方向。 如下图:

    G3

    还有一种情况就是在某一子树外和子树中都有特殊点,那么为了获得 \(f_3\) 的收益,同时让 \(1\) 饱和,我们必须要让这条边成为双向边。订单式如果这条边成为双向边的代价 \(w_i>f_3\)。我们宁愿抛弃 \(f_3\)。因为此时它带来的增益是负的。

    因此我们总结一下:对于一个节点 \(u\),已知他的儿子 \(f_{v_1},f_{v_2},f_{v_3}....f_{v_d}\) 的值。我们令连接 \(u,v_i\) 的边编号为 \(j\)\(size_u\) 为以 \(u\) 为根节点的子树中有特殊点个数,那么我们有:

    \[f_i=\sum\limits_{i=1}^d (f_{v_i}+calc(v_i)) \]

    \[clac(v)=\max \begin{cases}f_v&size_{v}=0,size_v=k\\f_v-e_j&f_v>e_j\\0&f_v\leq e_j\end{cases} \]

    显然,我们想要算 \(u\) 一定为饱和点的时候最大收益就是 \(u\) 为根的时候 \(f_u\) 的值。但是我们要把每个点的答案都算出来,时间复杂度就上升到了 \(O(n^2)\)。实在太大,无法接受。所以我们要采用换根DP的方式来优化复杂度。我们再来定义 \(dp_u\) 为满足 \(u\) 一定饱和的情况下,在以 \(u\) 的根节点的子树外的最大收益。我们先以 \(1\) 为根节点做一遍DFS求出 \(f\) 数组的值。接着再做一遍DFS,算出 \(dp\) 数组。具体的方法可以参照UM的blog,读者自己思考完成。实在不会可以看看代码。最后 \(ans_u=f_u+dp_u-c_u\)。因为 \(dp_u,f_u\) 中都包含 \(c_u\),也就是这个节点本身的收益。

    提示一下初值:\(dp_u=f_u=c_u\) 哦~

    但是我们上面讨论的是树的情况下。但如果是一张图呢?我们就要来进行一下问题的转化。

    我们一般把一个无向图转化为树的方法就是双联通分量缩点。我们来证明一下一个缩完点的双连通分量等同于树上的一个点。首先,我们知道一个双联通分量内部的边有向化,一定有至少一种方案使其成为强连通分量。所以这个双联通分量内部的点,要么全部饱和,要么全不饱和,其其内部的遍全有向化,代价为 \(0\)。因此,我们只需要缩个点,在进行换根DP即可解决此题。

    代码至少150+,大家自便(不压行)~

    //Don't act like a loser.
    //You can only use the code for studying or finding mistakes
    //Or,you'll be punished by Sakyamuni!!!
    #include<bits/stdc++.h>
    #define int long long
    using namespace std;
    
    int read() {
    	char ch=getchar();
    	int f=1,x=0;
    	while(ch<'0'||ch>'9') {
    		if(ch=='-')
    			f=-1;
    		ch=getchar();
    	}
    	while(ch>='0'&&ch<='9') {
    		x=x*10+ch-'0';
    		ch=getchar();
    	}
    	return f*x;
    }
    
    const int maxn=3e5+10;
    
    int n=0,m=0,k=0,h[maxn]={},cnt=0,low[maxn]={},dfn[maxn]={},col[maxn]={},num[maxn]={},size[maxn]={},d[maxn]={},tot=0;
    int col_cnt=0,time_cnt=0,w[maxn]={},c[maxn]={},val[maxn]={},spe[maxn]={},f[maxn]={},dp[maxn]={};
    vector<pair<int,int> > g[maxn];
    bool vis[maxn]={},spec[maxn];
    
    struct edge {
    	int v,w,next;
    }e[maxn<<1];
    
    void addedge(int u,int v,int w) {
    	e[++cnt].v=v;
    	e[cnt].w=w;
    	e[cnt].next=h[u];
    	h[u]=cnt; 
    }
    void insert(int u,int v,int w) {
    	addedge(u,v,w);
    	addedge(v,u,w);
    }
    
    void dfs1(int u,int fa) {
    	vis[u]=1;
    	low[u]=dfn[u]=++time_cnt;
    	int sz=g[u].size();
    	for(int i=0;i<sz;i++) {
    		int v=g[u][i].first;
    		
    		if(v!=fa) {
    			if(!vis[v]) {
    				dfs1(v,u);
    				low[u]=min(low[u],low[v]);
    			}
    			else {
    				low[u]=min(low[u],dfn[v]);
    			}
    		}
    	}
    	d[++tot]=u;
    }
    
    void dfs2(int u,int fa) {
    	col[u]=col_cnt;
    	spe[col_cnt]+=spec[u]? 1:0;
    	val[col_cnt]+=c[u];
    	
    	int sz=g[u].size();
    	for(int i=0;i<sz;i++) {
    		int v=g[u][i].first;
    		
    		if(dfn[u]>dfn[v]||low[v]>dfn[u]) {//判断是否是桥。
    			continue;
    		}
    		if(!col[v]) {
    			dfs2(v,u);
    		}
    	}
    }
    
    void tarjan() {
    	dfs1(1,0);
    	
    	for(int i=n;i;i--) {//反向循环
    		if(!col[d[i]]) {
    			col_cnt++;
    			dfs2(d[i],0);
    		}
    	}
    	
    	for(int u=1;u<=n;u++) {//缩点后的建图
    		int sz=g[u].size();
    		
    		for(int i=0;i<sz;i++) {
    			int v=g[u][i].first;
    			
    			if(u<v&&col[u]!=col[v]) {//注意一条边不要建两次~
    				insert(col[u],col[v],w[g[u][i].second]);
    			}
    		}
    	}
    }
    
    void dfs3(int u,int fa) {//以1为根的DFS
    	f[u]=val[u];
    	size[u]=spe[u];
    	for(int i=h[u];i;i=e[i].next) {
    		int v=e[i].v;
    		
    		if(v!=fa) {
    			dfs3(v,u);
    			size[u]+=size[v];
    			
    			if(size[v]==0||size[v]==k) {
    				f[u]+=f[v];
    			}
    			else {
    				f[u]+=max(0LL,f[v]-e[i].w);
    			}
    		}
    	}
    }
    
    void dfs4(int u,int fa) {//换根的部分
    	int sum=0;
    	
    	for(int i=h[u];i;i=e[i].next) {
    		int v=e[i].v;
    		
    		if(v!=fa) {
    			if(size[v]==k||size[v]==0) {//注意分类
    				sum+=f[v];
    			}
    			else {
    				sum+=max(0LL,f[v]-e[i].w);
    			}
    		}
    	}
    	
    	for(int i=h[u];i;i=e[i].next) {
    		int v=e[i].v;
    		
    		if(v!=fa) {
    			dp[v]=val[v];
    			if(size[v]==0||size[v]==k) {//分类,由上而下更新dp数组
    				dp[v]+=dp[u]+sum-f[v];
    			}
    			else {
    				dp[v]+=max(0LL,dp[u]+sum-max(0LL,f[v]-e[i].w)-e[i].w);
    			}
    			dfs4(v,u);
    		}
    	}
    }
    
    signed main() {
    	n=read();
    	m=read();
    	k=read();
    	
    	for(int i=1;i<=k;i++) {
    		spec[read()]=1;
    	}
    	for(int i=1;i<=n;i++) {
    		c[i]=read();
    	}
    	for(int i=1;i<=m;i++) {
    		w[i]=read();
    	}
    	for(int i=1;i<=m;i++) {
    		int a=read(),b=read();
    		g[a].push_back(make_pair(b,i));
    		g[b].push_back(make_pair(a,i));//第一次建图
    	} 
    	
    	tarjan();
    	
    	dfs3(1,0);
    	dp[1]=val[1];
    	dfs4(1,0);
    	
    	for(int i=1;i<=n;i++) {
    		printf("%lld ",dp[col[i]]+f[col[i]]-val[col[i]]);//注意-val[i]
    	}
    	
    	return 0;
    }
    
  • 相关阅读:
    mysql之优化器、执行计划、简单优化
    一条查询sql的执行流程和底层原理
    mysql建立索引,实际工作中建立索引的示例
    explain命令---查看mysql执行计划
    mysql 一些知识点
    开发中一些快捷键的使用
    simple-rpc
    maven
    数组合并排序
    SpringMVC配制全局的日期格式
  • 原文地址:https://www.cnblogs.com/huayucaiji/p/CF1389.html
Copyright © 2020-2023  润新知