zoukankan      html  css  js  c++  java
  • 「dsu on tree」学习笔记(优雅的暴力)

    「dsu on tree」学习笔记(优雅的暴力)

    前置芝士

    • 半熟练剖分

    基本概念

    • (dsu):并查集

    • (on tree):在树上

    即在树上的并查集

    行吧,跟名字没多大关系。

    可以通俗的理解为暴力,一般用来处理以下性质的问题:

    • 一个根的答案由子树来贡献

    • 不含任何修改操作

    有同学可能就问了,这用点分治不就可以了吗?

    两者有点差别:

    • 点分治一般处理的是无根树,而 (dsu) 处理的是有根树。

    • 点分治的效率是 (nlog_n^2),而 (dsu) 的效率是 (nlog_n)

    基本思路

    拿一个例题:CF600E Lomsat gelral


    首先考虑暴力如何去做:

    • 枚举每一个根,直接暴搜它的子树所有节点,统计答案即可,时间负责度 (Theta (n^2))

    • 预处理出来每个子树的颜色序列,存起来,直接上传到其父亲即可,空间复杂度 (Theta (n^2))

    显然两种暴力都不可取,但是两种暴力都各有所长,我们就将其长处融合在一起:(dsu)


    • 根据暴力一的思路,我们可以将其每个轻儿子的子树暴力统计,每次统计完后直接清除即可。

    • 根据暴力二的思路,我们可以不用将重儿子的答案清除,直接用于统计答案即可。

    所以这样,我们既保证了时间效率的较优,又保证了空间负责度的合理,就可以很好的处理这个问题了。

    • 小细节(决定成败):当你暴力统计轻儿子的子树答案时,将重儿子的子树跳过即可。

    (u1s1),通俗((dsu))易懂。

    板板题代码

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    
    #define int long long
    
    using namespace std;
    
    const int maxn = 1e5 + 50, INF = 0x3f3f3f3f;
    
    inline int read () {
    	int x = 0, w = 1;
    	char ch = getchar ();
    	for (; ch < '0' || ch > '9'; ch = getchar ()) if (ch == '-') w = -1;
    	for (; ch >= '0' && ch <= '9'; ch = getchar ()) x = x * 10 + ch - '0';
    	return x * w;
    }
    
    int n;
    int col[maxn], ans[maxn];
    
    struct Edge {
    	int to, next;
    }e[maxn << 1];
    
    int tot, head[maxn];
    
    inline void Add (register int u, register int v) {
    	e[++ tot].to = v;
    	e[tot].next = head[u];
    	head[u] = tot;
    }
    
    int size[maxn], son[maxn];
    
    inline void DFS1 (register int u, register int fa) { // 熟练剖分,统计重儿子
    	size[u] = 1;
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa) continue;
    		DFS1 (v, u);
    		size[u] += size[v];
    		if (size[son[u]] < size[v]) son[u] = v;
    	}
    }
    
    int cnt[maxn], sum, maxx, Son;
    
    inline void Query (register int u, register int fa, register int w) {
    	cnt[col[u]] += w;
    	if (cnt[col[u]] > maxx) maxx = cnt[col[u]], sum = col[u]; // 更新答案
    	else if (cnt[col[u]] == maxx) sum += col[u];
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa || v == Son) continue; // 跳过记录的重儿子
    		Query (v, u, w);
    	}
    }
    
    inline void DFS2 (register int u, register int fa, register bool opt) { // opt = 1,表示不用清除,即重儿子;opt = 0,清除,即轻儿子
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa || v == son[u]) continue; // 重儿子要跳过
    		DFS2 (v, u, 0); // 轻儿子向下递归
    	}
    	if (son[u]) DFS2 (son[u], u, 1), Son = son[u]; // 重儿子向下递归
    	Query (u, fa, 1), Son = 0; // 统计轻儿子的答案
    	ans[u] = sum; // 记录答案
    	if (opt == 0) Query (u, fa, -1), sum = 0, maxx = 0; // 轻儿子清除
    }
    
    signed main () {
    	n = read();
    	for (register int i = 1; i <= n; i ++) {
    		col[i] = read();
    	}
    	for (register int i = 1; i < n; i ++) {
    		register int u = read(), v = read();
    		Add (u, v), Add (v, u);
    	}
    	DFS1 (1, 0);
    	DFS2 (1, 0, 1);
    	for (register int i = 1; i <= n; i ++) {
    		printf ("%lld ", ans[i]);
    	}
    	puts ("");
    	return 0;
    }
    

    例题

    射手座之日

    其实这个题用线段树可做,但是在某谷上交会暴内存,但是好像还是有人卡过了。

    在这儿冲一发 (dsu) 的题解~~~


    首先我们考虑这个题的性质:

    • 一个子树里的若干个结点,其 (LCA) 一定在这个子树内。

    • 若在 (a) 序列中,有一段区间的点,都在同一个子树内,则有 (frac {len imes (len - 1)}{2}) 的区间数,即方案为 (LCA) 在这颗子树的方案数。


    我们会发现,某个根节点的答案会由其子树贡献过来,这样我们就可以用 (dsu) 了。

    连续段

    若一颗子树内若干个节点能够形成一段区间,那么它们在 (a) 序列里是连续的,(rank(rank[a[i]]= i)) 就是连续的。

    所以我们在便历一颗子树的时候,将每一个节点的 (rank) 值用一个 (tmp) 数组维护,(tmp[rank[u]]) 表示 (u) 节点在当前统计过的若干个节点,(rank[u]) 作为一个极长区间的左/右端点时,这个区间的长度。

    若一颗子树的所有节点放到一个序列里,(rank) 值有连续的,就会有一段区间,则这个区间形成的方案数为 (frac {len imes (len - 1)} {2})

    合并

    当我们插入一个数 (x)(某个节点的 (rank) 值),我们就可以将 (x - 1)(x + 1) 所在的区间合并起来,并将 (tmp) 数组更新。

    inline void Query (register int u) {
    	register int tmp1 = tmp[rank[u] - 1], tmp2 = tmp[rank[u] + 1]; // 得到左边区间的长度,右边区间的长度
    	register int len = tmp1 + tmp2 + 1; // 合并后区间的长度
    	tmp[rank[u] - tmp1] = tmp[rank[u] + tmp2] = len; // 将合并后的区间的左右端点的tmp更新
    	tmpans += len * (len - 1) / 2 - tmp1 * (tmp1 - 1) / 2 - tmp2 * (tmp2 - 1) / 2; // 注意减掉之前统计过的贡献
    }
    

    答案统计

    但是我们会发现,我们统计的只是 (LCA) 在其子树内的方案数,举个栗子:

    (3) 为根节点的子树,我们统计出来的答案是以 (3,4,5)(LCA) 的答案和。

    (6) 为根节点的子树,我们统计出来的答案是以 (6)(LCA) 的答案和。

    (2) 为根节点的子树,我们统计出来的答案是以 (2,3,4,5,6)(LCA) 的答案和。

    那么我们将 (2) 的答案减去其子树 (3,6) 的答案,就可以得出以 (2)(LCA) 的答案。

    最后乘上权值即可。

    还有,我们会发现,我们只是统计了区间长度 (geqslant 2) 的贡献,我们只需在求一个 (sumval) 加到答案上即可。

    代码

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    
    #define int long long
    
    using namespace std;
    
    const int maxn = 5e5 + 50, INF = 0x3f3f3f3f;
    
    inline int read () {
    	int x = 0, w = 1;
    	char ch = getchar ();
    	for (; ch < '0' || ch > '9'; ch = getchar ()) if (ch == '-') w = -1;
    	for (; ch >= '0' && ch <= '9'; ch = getchar ()) x = x * 10 + ch - '0';
    	return x * w;
    }
    
    int n, ans, sumval;
    int a[maxn], val[maxn], rank[maxn];
    
    struct Edge {
    	int to, next;
    }e[maxn << 1];
    
    int tot, head[maxn];
    
    inline void Add (register int u, register int v) {
    	e[++ tot].to = v;
    	e[tot].next = head[u];
    	head[u] = tot;
    }
    
    int size[maxn], son[maxn];
    
    inline void DFS1 (register int u, register int fa) {
    	size[u] = 1;
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa) continue;
    		DFS1 (v, u);
    		size[u] += size[v];
    		if (size[son[u]] < size[v]) son[u] = v;
    	}
    }
    
    int tmp[maxn], nowans, tmpans, num[maxn], Son;
    
    /*
    nowans 表示以u为根节点的子树,以u为LCA的方案数
    tmpans 表示以u为根节点的子树,以其子树里的点为LCA的方案数
    num[u] 表示以u为根节点的子树,以其子树里的点为LCA的方案数
    tmp[u] 表示以u为某个区间的左右端点,这个区间的长度
    */
    
    inline void Query (register int u) {
    	register int tmp1 = tmp[rank[u] - 1], tmp2 = tmp[rank[u] + 1]; // 得到左边区间的长度,右边区间的长度
    	register int len = tmp1 + tmp2 + 1; // 合并后区间的长度
    	tmp[rank[u] - tmp1] = tmp[rank[u] + tmp2] = len; // 将合并后的区间的左右端点的tmp更新
    	tmpans += len * (len - 1) / 2 - tmp1 * (tmp1 - 1) / 2 - tmp2 * (tmp2 - 1) / 2; // 注意减掉之前统计过的贡献
    }
    
    inline void Clear (register int u, register int fa) { // 清空操作
    	tmp[rank[u]] = 0;
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa) continue;
    		Clear (v, u);
    	}
    }
    
    inline void DFS3 (register int u, register int fa) { // 暴力统计轻儿子答案
    	Query (u);
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa || v == Son) continue;
    		DFS3 (v, u);
    	}
    }
    
    inline void DFS2 (register int u, register int fa, register bool opt) {
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		if (v == fa || v == son[u]) continue;
    		DFS2 (v, u, 0);
    	}
    	if (son[u]) DFS2 (son[u], u, 1), Son = son[u];
    	DFS3 (u, fa), Son = 0;
    	nowans = num[u] = tmpans;
    	for (register int i = head[u]; i; i = e[i].next) {
    		register int v = e[i].to;
    		nowans -= num[v]; // 减去子树的num,只要以u为LCA的答案
    	}
    	ans += nowans * val[u]; // 统计答案
    	if (opt == 0) Clear (u, fa), tmpans = 0; // 轻儿子清空
    }
    
    signed main () {
    	n = read();
    	for (register int v = 2; v <= n; v ++) {
    		register int u = read();
    		Add (u, v), Add (v, u);
    	}
    	for (register int i = 1; i <= n; i ++) a[i] = read(), rank[a[i]] = i;
    	for (register int i = 1; i <= n; i ++) val[i] = read(), sumval += val[i];
    	DFS1 (1, 0);
    	DFS2 (1, 0, 1);
    	printf ("%lld
    ", ans + sumval);
    	return 0;
    }
    
  • 相关阅读:
    @RequestParam和@PathVariable用法小结
    Spring 的监听事件 ApplicationListener 和 ApplicationEvent 用法
    allator 对springBoot进行加密
    commons-lang3相关类实例
    JSP自定义标签
    Netty的简单Demo
    Spring-AOP为类增加新的功能
    深入理解abstract class和interface
    linux的基本操作
    GitHub从小白到熟悉<二>
  • 原文地址:https://www.cnblogs.com/Rubyonly233/p/13775521.html
Copyright © 2011-2022 走看看