HDU 3065 2017 Multi-University Training Contest - Team 1 1003 Corlorful Tree:计数+树上分块

题意:给定一棵树(n<=2e5),每个点都有一个颜色C(1..n)。现在定义道路u-v的value为u-v简单道路上不同颜色的数量。现在要求整棵树上所有的n*(n-1)/2条道路的value和(u-v和v-u算同一条,i-i不算)。

题解:换一种方式求总和,把每种颜色的贡献单独计算。再换一个角度,先假定所有的n*(n-1)/2条道路的value都是最大值n,然后对于1..n每种颜色,把不该算的道路数量找出来,在总答案上剪掉即可。

如果某种颜色未出现过,那么该颜色在所有道路上都被非法计算了一次,答案减掉n*(n-1)/2

如果某种颜色出现果过,那么非法统计的答案是:没有出现过该颜色的道路数量。想要找到这些道路是简单的:把所有该颜色的点删掉,此时图中剩下的所有的道路总数即是所求。我们紧接着对这个思路进行优化:删点之后,树被拆成了一个森林。而新森林的每个小树必定有一个性质:叶子节点要么是老树的叶子,要么在老树中有一条边连像删掉的点,所以每个小树,必定有是原图中某个该颜色的点的一条树枝的一部分。因此我们就是要找到所有这样的块的大小size,答案减掉size*(size-1)/2。

那么对于颜色i,我们枚举每个颜色为i的点x,x的出度(去掉指向父亲的边)就等于树枝个数。那么继续枚举x的每个儿子节点y,把y看成根,就是一个树枝,也是我们所说的一个块的初始形态,我们要在y为根的树枝上,找到所有最接近y的同样颜色为i的点p,把p这个树枝全部砍掉,这样我们就限制好了一个块。下面问题转变成了如何找到树枝y上,离y最近的几个颜色为i的点(所有y到叶子的道路上,最接近y的颜色为i的点)。

DFS序列可以满足我们的需求:我们需要构造一个虚根0,他指向树根1。DFS序本身类似一个括号序列,他一定是合法的。而每一个节点a会出现两次。第一次是遍历到a的时候,出现的位置叫做L[a],第二次是回溯的时候,出现的位置叫做R[a],那么L[a]和R[a]之间出现过的所有的点就是树枝a上的所有的点。同时,既然这是一个合法的括号序列,那么假如说a有三个树枝,括号序列(a出现的地方改成花括号表示)必然呈现出{ ()()()}的样子(小括号里面有更小的括号,省略不写)。那么要找到所有以树枝上颜色为i的点,我们可以这样进行:在颜色为i的点集合中,首先找到L中第一个比La大于等于的Lx,如果Lx<Ra说明x位于a树枝上,那么去掉所有x以下的点,然后下一次,要考虑去掉x树枝的剩下的a树枝,我们找第一个比Rx+1(这个位置是x树枝括号序列结束的下一个位置,也就是下一条树枝开始的地方或者a括号结束的地方)大于等于的Lxx,再把这条树枝去掉,然后在找到第一个比Rxx+1大于等于的Lxxx,把他去掉………………..这样我们就限制得到了一个块,同时得到他的size,这个块中的所有道路,在颜色i的贡献上都做了非法贡献,减掉size*(size-1)/2。

总结一句话就是emmmm考虑每个颜色i,找到被所有i颜色点隔开的所有的块大小size就是了。

然后体验了一波auto。。。。以及vector等用lower_bound查找到it=end指针的时候,不要输出*it,会爆炸。。。我还以为代码写错了呢。。。另外auto & 可以修改相应内存单元的值。这么简洁的一个树上块剖(姑且这么叫他)的算法被我描述的。。。。这么啰嗦orz。。。太弱辣

注意:代码里有一条注释,他在任何情况下都会导致程序崩掉。就是因为上面说的问题。

Code:

#include<bits/stdc++.h>
using namespace std;
#define MAX 200006
vector<int> E[MAX],C[MAX];
int R[MAX],L[MAX],S[MAX],F[MAX];
int n;
void dfs(int nod,int father,int &&count){
	L[nod]=++count;
	S[nod]=1;
	F[nod]=father;
	for (auto a:E[nod]){
		if ((a)==father){
			continue;
		}
		dfs(a,nod,move(count));
		S[nod]+=S[a];
	}
	R[nod]=count;
}
bool cmp(const int& x,const int& y){
	return L[x]<L[y];
}
int main(){
	int num=1;
	E[0].push_back(1);
	while (scanf("%d",&n)!=EOF){
		for (int i=1;i<=n;i++){
			E[i].clear();
			C[i].clear();
		}
		for (int i=1;i<=n;i++){
			int temp;
			scanf("%d",&temp);
			C[temp].push_back(i);
		}

		for (int i=0;i<n-1;i++){
			int a,b;
			scanf("%d%d",&a,&b);
			E[a].push_back(b);
			E[b].push_back(a);
		}
		dfs(0,0,0);
		long long ans = 1LL*n*n*(n-1)/2;
		for (int i = 1;i<=n;i++){
			if (C[i].empty()){
				ans-=1LL*n*(n-1)/2;
			}else{
				C[i].push_back(0);
				sort(C[i].begin(),C[i].end(),cmp);
				for (auto &x:C[i]){
					for (auto &y:E[x]){
						if (y==F[x]){
							continue;
						}
						int size = S[y];
						int k = L[y];
						while (true){
							L[n+1] = k;
							auto it = lower_bound(C[i].begin(),C[i].end(),n+1,cmp);
//							cout<<x<<" "<<y<<" "<<*it<<" "<<R[*it]<<endl;
							if (it==C[i].end()||L[(*it)]>R[y]){
								break;
							}
							size-=S[*it];
							k = R[*it]+1;
						}
						ans -= 1LL*size*(size-1)/2;
					}
				}
			}
		}
		cout<<"Case #"<<num++<<": "<<ans<<endl;
	}
	return 0; 
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值