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\n",ans); } return 0; }