将 n n n个集合进行合并,最后合并为1个集合
假设一次合并的时间复杂度为 O ( o p ) O(op) O(op)
合并过程中的复杂度为 O ( 1 + 2 + 3 ⋯ + n ) = O ( n 2 ) O(1 + 2 + 3 \dots+n) = O(n^2) O(1+2+3⋯+n)=O(n2)
总的时间复杂度为 O ( o p n 2 ) O(opn^2) O(opn2)
需要优化!
如果每一次将小的集合合并到大的集合里面
复杂度为: O ( o p × n l o g n ) O(op \times nlogn) O(op×nlogn)
观察每一个元素对最终计算量的贡献
它取决于每一次所在集合合并的次数
假设最小的集合,集合内的元素个数为 x x x
合并一次之后,两个集合合并之后元素个数为 2 ∗ x 2*x 2∗x
题意:
n个布丁,有m个颜色
操作:
op1:要把一个颜色的布丁全部变为另一种颜色
op2:询问连续的颜色段个数
每一次将 x x x的颜色变成 y y y,就代表 x x x和 y y y的珠子合并成同一类
计算一个珠子的贡献,如果它变化前和左边不一样,则贡献加一,如果和右边不一样,则贡献为2。
合并之后,贡献减2。
存储时候,需要将每种颜色对应珠子的编号进行存储。
每一次合并的时候,把小的集合链合并到大的集合链,并且维护颜色之间的包含关系
#include
#include
#include
#include
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int n, m, ans;
int color[N], h[M], ne[N], e[N], id;
int fa[M], sz[M];
void add(int a, int b)
{
e[id] = b, ne[id] = h[a], h[a] = id++;
sz[a]++;
}
void merge(int &a, int &b)
{
if(a == b) return;
if(sz[a] > sz[b]) swap(a, b);
for(int i = h[a]; i != -1; i = ne[i])
{
int j = e[i];
ans -= (color[j - 1] == b) + (color[j + 1] == b);
}
for(int i = h[a]; i != -1; i = ne[i])
{
int j = e[i];
color[j] = b;
if(ne[i] == -1)
{
ne[i] = h[b];
h[b] = h[a];
break;
}
}
sz[b] += sz[a];
sz[a] = 0;
h[a] = -1;
}
int main()
{
memset(h, -1, sizeof(h));
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
scanf("%d", &color[i]);
add(color[i], i);
}
ans = 1;
for(int i = 2; i <= n; i++)
{
if(color[i] != color[i - 1]) ans++;
}
for(int i = 1; i < M; i++) fa[i] = i;
while(m --)
{
int op;
scanf("%d", &op);
if(op == 1)
{
int x, y;
scanf("%d%d", &x, &y);
merge(fa[x], fa[y]);
}
else
{
printf("%d\n", ans);
}
}
return 0;
}
Problem
树的节点有颜色,一种颜色占领了一个子树,当且仅当没有其他颜色在这个子树中出现得比它多。求占领每个子树的所有颜色之和 CF600E
solution
算法思路
这道题我们可以遍历整棵树,并用一个 c n t cnt cnt数组记录每种颜色出现几次
但是每做完一棵子树就需要清空 c n t cnt cnt,以免对其兄弟造成影响。
而这样做它的祖先时就要把它重新搜一遍,浪费时间
但是我们发现,对于每个节点x,最后一棵子树是不用清空的,因为做完那棵子树后可以把其结果直接加入x的答案中。
选哪棵子树呢?当然是所含节点最多的一棵咯,我们称之为“重儿子”
考虑暴力怎么写:遍历每个节点—>把子树中的所有颜色暴力统计出来更新答案—>消除该节点的贡献—继续递归这肯定是 O ( n 2 ) O(n^2) O(n2)的。
d s u o n t r e e dsu on tree dsuontree巧妙的利用了轻重链剖分的性质,把复杂度降到了 O ( n l o g n ) O(nlogn) O(nlogn)。你不知道啥叫轻重链剖分?一句话:对于树上的一个点,与其相连的边中,连向的节点子树大小最大的边叫做重边,其他的边叫轻边
算法证明
设根到该节点有x条轻边,该节点的大小为y,根据轻重边的定义,轻边所连向的点的大小不会成为该节点总大小的一半。这样每经过一条轻边,y的上限就会/2,因此 y < n / 2 x y
算法实现,对于节点i:
遍历每一个节点
递归解决所有的轻儿子,同时消除递归产生的影响
递归重儿子,不消除递归的影响
统计所有轻儿子对答案的影响
更新该节点的答案
删除所有轻儿子对答案的影响
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N = 1e5 + 50, M = N * 2;
int n;
int h[N], ne[M], e[M], id;
int son[N], mx, cnt[N], sz[N], color[N];
ll ans[N], sum;
void addedge(int u, int v)
{
e[id] = v, ne[id] = h[u], h[u] = id++;
}
int dfs_son(int u, int father)
{
sz[u] = 1;
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(j == father) continue;
sz[u] += dfs_son(j, u);
if(sz[j] > sz[son[u]]) son[u] = j;
}
return sz[u];
}
void update(int u, int father, int sign, int pson)
{
// 计算每个结点的sum
int c = color[u];
cnt[c] += sign;
if(cnt[c] > mx) mx = cnt[c], sum = c;
else if(cnt[c] == mx) sum += c;
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(j == father || j == pson) continue; // 更新的时候避开轻儿子
update(j, u, sign, pson);
}
}
void dfs(int u, int father, int op) // 重儿子op为1, 轻儿子op为2
{
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(j == father || j == son[u]) continue; // 避开重儿子
dfs(j, u, 0);
}
if(son[u]) dfs(son[u], u, 1);
update(u, father, 1, son[u]); // 重儿子不需要清空
ans[u] = sum;
if(!op)
{
update(u, father, -1, 0); // 轻儿子需要清空
sum = mx = 0;
}
}
int main()
{
memset(h ,-1, sizeof(h));
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &color[i]);
for(int i = 1; i < n; i++)
{
int a, b;
scanf("%d%d", &a, &b);
addedge(a, b), addedge(b, a);
}
dfs_son(1, -1);
dfs(1, -1, 1);
for(int i = 1; i <= n; i++) printf("%lld ", ans[i]);
return 0;
}