2020CCPC长春补题

好难,只过了两题

F - Strange Memory

比赛时我写了个点分治233

写完才发现是有根树,然后我就把点分治改了下,变成了一个O(n^2)的暴力,在第三个点T了

然后我就想这是不是要轻重链剖分,然后剖了一下,发现完全写不来。。

赛后看了下dsu on tree,就感觉dsu on tree就是这题的正解了

今天思考了下这个算法,终于理解了dsu on tree了

然后就试试自己写下F题

我做出来的第一道dsu on tree

思路:

先考虑暴力的O(n^2)做法,计算每个节点为LCA时的贡献,要把这个节点当子树的根节点时,把整颗子树遍历一遍,然后还要清空

复杂度 = Σsize[node] = O(n^2)

然后考虑优化的做法,计算一个节点的贡献,我们先计算完其儿子的贡献,然后再计算它的贡献

在计算第二个儿子的贡献时,要把它第一个儿子的记录清空。计算第三个儿子的贡献时,要把它第二个儿子的记录清空,以此类推,

但是,在计算最后一个儿子的贡献时,就不必把记录清空了,这个记录在计算其父节点的时候也要用到,

所以,我们把重儿子留到最后一个计算,并不对重儿子做清空处理

这就是dsu on tree了

复杂度分析:

对于计算每个节点,要遍历的点为除重儿子为根节点的子树以外的所有子树 ( 即所有轻儿子为根节点的子树)

复杂度 = Σsize[node] - size[son[node]] 

这个复杂度怎么算呢,我们对于某个节点node,沿着一条重链计算一下,size[node] - siz[son[node]] + siz[son[node]] - siz[son[son[node]]] + siz[son[son[node]]] - ...

可以发现这是一个相消的式子,累加的结果为size[node]

我们先从根节点沿着重链计算一下,size[st] = n,复杂度 += n

然后对于那些没有计算过,但父亲计算过的节点vi,从这些节点沿重链计算一下,Σsize[vi] < n,复杂度 += n

再次这样计算几轮,直到所有节点都被计算过为止

可以发现轮数一定是<=log2 n的

所以复杂度为O(n log2 n)

对于本题,还有一个难点,就是在清除的时候该怎么做

显然不能在一个vector数组里指定清除哪个数

其实直接pop掉就行(想一想,为什么)

最后,为什么我写的dsu on tree的函数DSU_ON_TREEEEEEEEEEEEE,函数名这么长,还用了个very_heavyyyyyyyy?

因为我第一次理解了dsu on tree,比较兴奋(

我从5月份的时候就想学dsu on tree了,然而现在12月份了,才理解dsu on tree。。。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
const int MAXN = 1e5+7;
const int MAXP = 1e6+7;
int a[MAXN];
vector<int> dt[MAXP];
int n;
struct EDGE{
	int to,next;
}edge[MAXN*2];
int head[MAXN],tot;
void add(int u,int v){
	tot++;
	edge[tot].to = v;
	edge[tot].next = head[u];
	head[u] = tot;
}
int fa[MAXN],son[MAXN],siz[MAXN];
void dfs(int s,int f){//求重儿子 
	siz[s] = 1;
	int maxsize = -1;
	for(int i = head[s];i;i=edge[i].next ){
		int po = edge[i].to;
		if(po == f ) continue;
		fa[po] = s;
		dfs(po,s);
		siz[s] += siz[po];
		if(siz[po]>maxsize){
			maxsize = siz[po];
			son[s] = po;
		}
	}
}
long long ans = 0;

struct Pa{
	int node,qq;
}tmp[MAXN];
int cnt = 0;

void cal(int s,int st){//记录整颗子树 
	int ff = a[s] ^ a[st];
	if(ff<=1000000){
		for(int i = 0;i < dt[ff].size();i++){
			ans += (long long)(dt[ff][i] ^ s);
		}
	}
	cnt++;
	tmp[cnt].node = s;
	tmp[cnt].qq = a[s];
	for(int i = head[s];i;i=edge[i].next ){
		int po = edge[i].to;
		if(po == fa[s]) continue;
		cal(po,st);
	}
}
void clear(int st){//清空整颗子树的记录 
	dt[a[st]].pop_back();
	for(int i = head[st];i;i=edge[i].next){
		int po = edge[i].to;
		if(po == fa[st]) continue;
		clear(po);
	}
}

void DSU_ON_TREEEEEEEEEEEEE(int st,bool very_heavyyyyyyyy){
	for(int i = head[st]; i ;i = edge[i].next){
		int po = edge[i].to;
		if(po == fa[st] || po == son[st]) continue;
		DSU_ON_TREEEEEEEEEEEEE(po,0);//先求轻儿子 
	}
	if(son[st]) DSU_ON_TREEEEEEEEEEEEE(son[st],1); //求重儿子 
	for(int i = head[st]; i ;i = edge[i].next){//计录轻儿子,此时重儿子已经计录过了 
		int po = edge[i].to;
		if(po == fa[st] || po == son[st]) continue;
		cnt = 0;
		cal(po,st);
		for(int j = 1;j <= cnt;j++){
			int node = tmp[j].node;
			int qq = tmp[j].qq;
			dt[qq].push_back(node);
		}
	}
	for(int i = 0;i < dt[0].size();i++){//计算自己 
		ans += (long long)(dt[0][i]^st); 
	}
	dt[a[st]].push_back(st);//记录子树根节点 
	if(!very_heavyyyyyyyy) clear(st);//子树根节点是轻儿子,清空整颗子树的记录 
}

int main()
{
	cin>>n;
	tot = 0;
	for(int i = 1;i<=n;i++){
		scanf("%d",&a[i]);
		head[i] = 0;
	}
	int u,v;
	for(int i = 1;i <= n-1;i++){
		scanf("%d%d",&u,&v);
		add(u,v);
		add(v,u);
	}
	dfs(1,0); 
	DSU_ON_TREEEEEEEEEEEEE(1,1);
	cout<<ans<<endl;
	return 0;
}

K - Ragdoll

这题看起来很难

思路:

先从x⊕y = gcd(x,y)下手

打了个表,发现所有符合条件的对,x和y互不为倍数,也就是gcd(x,y)!=x&&gcd(x,y)!=y

为什么?因为如果满足gcd(x,y)==x且x⊕y == x,那么y==0(因为x ^ y==x),显然这样gcd(x,y)!=x

受此启发,我们可以先假设gcd(x,y)为x的某个因数z,满足x⊕y == z,那么 y = x⊕z,最后再检验gcd(x,y)是否为z

这样就能把2e5内的所有对求出来了

然后是怎么处理合并时的计算

我们记录每棵树上各个权值的个数,这样合并的时候,遍历一遍其中一颗树就能计算了

先考虑这样做的时间复杂度

受上面F题的启发,我们每次合并的时候只搜索轻的树,把轻的树合并到重的树上,这样时间复杂度和上面那题应该是一样的

再考虑空间复杂度

最多有3e5棵树,每棵树开2e5的cnt数组是不行的,怎么办?

我想到了动态开点,但感觉这样好麻烦,后来突然想到了map,好!这样就能做了

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<vector>
#include<map>
using namespace std;
const int MAXN = 3e5+7;
vector<int>graph[MAXN];
int gcd(int a,int b){
	if(a<b)swap(a,b);
	while(b){
		a = a%b;
		swap(a,b);
	}
	return a;
}
void pre(int n){
	for(int i = 1;i <= n;i++){
		for(int j = 1;j * j <= i;j++){
			if(i % j == 0){
				if(gcd(i^j, i) == j) graph[i].push_back(i^j);
				if(i / j != j){
					if(gcd((i^(i/j)),i ) == i / j) graph[i].push_back(i^(i/j));
				}
			}			 
		}
	}
}
int n,m;
long long ans = 0;
struct NODE{
	int siz = 0,top, id, val;
	map <int,int> cnt;
}node[MAXN];
int par[MAXN];
int find(int x){
	if(par[x] == x) return x;
	return par[x] = find(par[x]);
}
void merge(int a,int b){
	par[b] = a;
	for(map<int,int>::iterator it = node[b].cnt.begin();it!=node[b].cnt.end();++it){
		if(!it->second) continue;
		int x = it->first, nx = it->second;
		for(int i = 0;i < graph[x].size();i++){
			int y = graph[x][i];
			ans += (long long)nx * (long long)node[a].cnt[y];
		} 
	}
	for(map<int,int>::iterator it = node[b].cnt.begin();it!=node[b].cnt.end();++it){
		if(!it->second) continue;
		int x = it->first;
		node[a].cnt[x] += it->second;
	}
	node[a].siz += node[b].siz;
}
int main()
{

	pre(2e5);
	cin>>n>>m;
	int vv;
	for(int i = 1;i <= n;i++) {
		node[i].id = i;
		node[i].top = i;
		scanf("%d",&vv);
		node[i].cnt[vv]++;
		node[i].val = vv;
		node[i].siz++;
		par[i] = i;
	}
	int t, x, y, v;
	for(int i = 1;i <= m;i++){
		scanf("%d",&t);
		if(t==1){
			scanf("%d%d",&x,&v);
			node[x].id = x;
			node[x].top = x;
			node[x].cnt[v]++;
			node[x].siz++;
			node[x].val = v;
			par[x] = x;
		}
		if(t==2){
			scanf("%d%d",&x,&y);
			x = find(x),y = find(y); 
			if(node[x].siz < node[y].siz) swap(x,y);			
			if(x!=y)merge(x,y);
		}
		if(t==3){
			scanf("%d%d",&x,&v);
			int fx = find(x);
			for(int j = 0;j < graph[node[x].val].size();j++){
				int y = graph[node[x].val][j];
				ans -= (long long)node[fx].cnt[y];
			}
			node[fx].cnt[node[x].val]--;
			node[x].val = v;
			for(int j = 0;j < graph[v].size();j++){
				int y = graph[v][j];
				ans += (long long)node[fx].cnt[y]; 
			}
			node[fx].cnt[v]++;
		}
		printf("%lld
",ans);
	}
	return 0;
}