• 【题解】[BZOJ#4771] 七彩树


    题目

    题目信息

    • 来源:BZOJ,Claris;

    • 在线评测地址:DBZOJ#4771

    • 运行限制:(5.00 extrm{ms}/512 extrm{MiB})(DBZOJ 上看不了时空,这里是大致上限)。

    题目描述

    给定一棵 (n) 个点的有根树,编号依次为 (1)(n),其中 (1) 号点是根节点。每个节点都被染上了某一种颜色,其中第 (i) 个节点的颜色为 (c_i)。如果 (c_i=c_j),那么我们认为点 (i) 和点 (j) 拥有相同的颜色。定义 (depth_i)(i) 节点与根节点的距离,为了方便起见,你可以认为树上相邻的两个点之间的距离为 (1)

    站在这棵色彩斑斓的树前面,你将面临 (m) 个问题。每个问题包含两个整数 (x)(d),表示询问 (x) 子树里且深度不超过 (depth_x+d) 的所有点中出现了多少种不同的颜色。请写一个程序,快速回答这些询问。强制在线

    输入格式

    每个测试点有多组数据。(虽然就一个测试点)

    第一行包含一个正整数 (T),表示测试数据的组数。

    每组数据中,第一行包含两个正整数 (n)(m),表示节点数和询问数。

    第二行包含 (n) 个正整数,其中第 (i) 个数为 (c_i),表示节点 (i) 的颜色。

    第三行包含 (n-1) 个正整数,其中第 (i) 个数为节点 (i+1) 的父亲节点的编号。

    接下来 (m) 行,每行两个整数 (x)(d),依次表示每个询问,真实的 (x)(d) 分别是 (x operatorname{xor} last)(d operatorname{xor} last),其中 (last) 表示这组数据中上一次询问的答案,如果这是当前数据的第一组询问,那么 (last=0)

    数据规模与约定

    (1le Tle 500)(1le m, nle 10^5)(sum n+sum mle 5 imes 10^5)(1le c_ile n)

    保证 (1le xle n)(0le d<n)

    分析

    给定一棵树,每个节点上有颜色,求一个子树内深度小于等于一个定值的所有节点中不同颜色的数量。

    先考虑没有深度限制怎么做,即统计一个子树内不同的颜色数。

    考虑节点对于答案的贡献:如果某节点的颜色从未出现过,则贡献显然为 (1);但如果某节点的颜色有前驱呢?

    首先观察到一个显而易见的的性质:如果有两个节点同时在某子树中,则它们的 LCA 也在该子树中。所以,我们可以将该节点的贡献设为 (1),而将其与同色前驱(DFS 序[1])的 LCA 的贡献减一。

    考虑这样为什么是对的:注意到一个子树在 DFS 序上是一个区间,而:

    • 对于区间内的同色节点,其相邻的 LCA 也在该区间内(上述性质);
    • 对于区间外的同色节点,其 LCA 显然不在该区间内。

    假设有 (x) 个此类节点,则 LCA 共有 (x-1) 个,最后整体的贡献就是 (1)

    这样,方法也呼之欲出了——我们按照 DFS 序遍历,对于每个节点统计贡献,然后用线段树区间和即可。


    接下来,我们考虑原题。有一种基于弱化版的暴力做法:对于每个深度,建一棵树,然后进行上述操作;询问时根据深度最低点选择树查询即可。这种方法肯定无法通过,但这启发我们基于最低层进行查询。

    我们考虑使用可持久化线段树优化该算法:根据 BFS 序逐层遍历节点,然后插入[2]可持久化线段树中。这种做法 貌似 是对的,但会出现一些问题。形如:

    1. 一个满足 BFS 序的遍历不一定满足 DFS 序。如果插入节点的 DFS 序介于两个节点之间怎么办?
    2. 如何寻找一个节点的 DFS 序前驱、后继?
    3. 不会 T 吗?空间开得下吗?

    首先,问题 2 可以通过 set 解决。问题 3 会在最后讨论。我们考虑如何解决问题 1。

    方法很简单:先还原前驱、后继对 LCA 的贡献,然后分别统计该节点与前驱、该节点与后继的 LCA 的贡献,最后统计该节点本身的贡献。[3]

    伪代码:

    tree[lca(next, prev)]++
    tree[lca(x, next)]--
    tree[lca(x, prev)]--
    tree[x]++
    

    如果仍难以理解可以自行模拟。

    这样,我们将原问题转化成了弱化版,就可以解决了。


    最后分析一下时空复杂度:

    首先,该程序要预处理 LCA,然后建可持久化线段树;接下来对于每个节点,查询前驱和后继,插入可持久化线段树;最后处理每次询问,都要区间求和。

    单组复杂度:(mathcal{O}((n+m)log n)),整体复杂度:(mathcal{O}(T(n+m)log n)sim mathcal{O}(5 imes 10^5 imes 17)) 可以通过。

    而空间上,需要可持久化线段树进行 (mathcal{O}(n)) 次插入操作,复杂度 (mathcal{O}(nlog n)),有亿点常数,但不是很紧。

    注意事项

    1. 请随时判 set 是否为空;
    2. 可持久化线段树下标是 DFS 序而非节点序号;
    3. 多组数据请不要滥用 memset。
    4. 如果感觉代码逻辑很混乱,就写 namespace 分而治之吧!

    Code

    由于代码实在是太长了(~300 行,6k),所以这里分段展示,完整代码可以看这里

    部分变量定义&简单函数:

    constexpr int max_n = 100000, max_s = 131072, max_lgn = 18; // max_s:线段树大小
    
    int hd[max_n], vl[max_n], des[max_n], nxt[max_n], e_cnt = 0; // 图
    int dfn[max_n], endp[max_n], fnl[max_n], ind = 0, fdp = 0;
    // dfn:DFS 序,endp:子树区间末尾指针,fnl:每一层的可持久化树根指针,ind:时间戳,fdp:最大深度
    int n;
    
    inline int my_min(int a, int b) { return (a < b)? a:b; }
    inline void my_swap(int& a, int& b) { int k = a; a = b, b = k; }
    
    #define gc getchar
    inline int read() // 快读
    {
    	int c = gc(), t = 1, n = 0;
    	while (isspace(c)) { c = gc(); }
    	if (c == '-') { t = -1, c = gc(); }
    	while (isdigit(c)) { n = n * 10 + c - '0', c = gc(); }
    	return n * t;
    }
    #undef gc
    
    void add_edge(int s, int t) // 给图加边
    {
    	des[e_cnt] = t;
    	nxt[e_cnt] = hd[s];
    	hd[s] = e_cnt++;
    }
    

    LCA:

    namespace LCA
    {
    	int fa[max_lgn][max_n], dep[max_n];
    	// 倍增求 LCA
    	
    	void dfs(int id, int ff) // DFS,求 fa[0]、dep、dfn 和 endp
    	{
    		fa[0][id] = ff, dfn[id] = ind++;
    		for (int p = hd[id]; p != -1; p = nxt[p])
    		{
    			dep[des[p]] = dep[id] + 1;
    			dfs(des[p], id);
    		}
    		endp[id] = ind - 1;
    	}
    	
    	void lca_init() // 初始化 fa
    	{
    		for (int i = 1; i < max_lgn; i++)
    			for (int j = 0; j < n; j++)
    			{
    				if (fa[i-1][j] != -1)
    					fa[i][j] = fa[i-1][fa[i-1][j]];
    				else
    					fa[i][j] = -1;
    			}
    	}
    	
    	int lca(int a, int b) // LCA
    	{
    		if (dep[a] < dep[b])
    			my_swap(a, b);
    		for (int i = max_lgn - 1; i >= 0; i--)
    			if (fa[i][a] != -1 && dep[fa[i][a]] >= dep[b])
    				a = fa[i][a];
    		
    		if (a == b)
    			return a;
    		for (int i = max_lgn - 1; i >= 0; i--)
    			if (fa[i][a] != -1 && fa[i][a] != fa[i][b])
    				a = fa[i][a], b = fa[i][b];
    		
    		return fa[0][a];
    	}
    }
    

    可持久化线段树

    namespace SEGTREE
    {
    	constexpr int mem_size = (max_s * 2 + 1) + max_n * (max_lgn * 2); // 内存池大小
    	
    	struct node // 节点
    	{
    		int l, r, vl;
    	};
    	node tr[mem_size]; // 内存池
    	int rt[2*max_n+1], n_cnt = 0, lrt = 0; // rt:根指针,n_cnt:内存分配指针,lrt:最大根编号+1
    	
    	int nnode(int val = 0) // 分配节点
    	{
    		tr[n_cnt].l = -1, tr[n_cnt].r = -1;
    		tr[n_cnt].vl = val;
    		return n_cnt++;
    	}
    	
    	int build(int l, int r) // 建树
    	{
    		if (l == r)
    			return nnode();
    		
    		int mid = (l + r) >> 1, id = nnode();
    		
    		tr[id].l = build(l, mid);
    		tr[id].r = build(mid + 1, r);
    		
    		return id;
    	}
    	
    	int _modify(int at, int l, int r, int pos, int val) // 单点修改
    	{
    //		fprintf(stderr, "In %d, [%d, %d] for %d to %d.
    ", at, l, r, pos, val);
    		
    		if (l == pos && pos == r)
    			return nnode(val + tr[at].vl);
    		
    		int mid = (l + r) >> 1, id = nnode();
    		
    		if (pos <= mid)
    		{
    			tr[id].l = _modify(tr[at].l, l, mid, pos, val);
    			tr[id].r = tr[at].r;
    		}
    		else
    		{
    			tr[id].r = _modify(tr[at].r, mid + 1, r, pos, val);
    			tr[id].l = tr[at].l;
    		}
    		
    		tr[id].vl = tr[tr[id].l].vl + tr[tr[id].r].vl;
    		return id;
    	}
    	
    	int modify(int pos, int val) // (因为太麻烦了就封装了一下
    	{
    		rt[lrt] = _modify(rt[lrt-1], 1, n, pos + 1, val);
    		return rt[lrt++];
    	}
    	
    	int query(int at, int L, int R, int l, int r) // 区间和询问
    	{
    		if (L <= l && r <= R)
    			return tr[at].vl;
    		
    		int mid = (l + r) >> 1, ret = 0;
    		
    		if (L <= mid && l <= R)
    			ret += query(tr[at].l, L, R, l, mid);
    		if (L <= r && mid < R)
    			ret += query(tr[at].r, L, R, mid + 1, r);
    		
    		return ret;
    	}
    }
    

    BFS:

    namespace FNINIT
    {
    	struct node // set 节点
    	{
    		int dn, id;
    		node(int _d = 0, int _i = 0) : dn(_d), id(_i) { }
    		bool operator<(const node& n) const
    		{
    			return dn < n.dn;
    		}
    	};
    	
    	set<node> st[max_n];
    	queue<int> q;
    	
    	void bfs()
    	{
    		using namespace SEGTREE;
    		using namespace LCA;
    		
    		int cur, la, lb;
    		
    		q.push(0);
    		while (!q.empty()) // BFS
    		{
    			cur = q.front();
    			q.pop();
    			
    			auto it = st[vl[cur]].emplace(dfn[cur], cur).first; // 插入
    			
    			modify(dfn[cur], 1);
    			if (it == st[vl[cur]].begin()) // 没有前驱
    			{
    				if (st[vl[cur]].size() != 1) // 有后继,直接相连
    					modify(dfn[lca(cur, (++it)->id)], -1);
    			}
    			else // 有前驱
    			{
    				auto prv = it, suc = it;
    				--prv, ++suc;
    				
    				if (suc == st[vl[cur]].end()) // 没有后继,直接连
    					modify(dfn[lca(cur, prv->id)], -1);
    				else // 有后继,具体实现参考注释 3
    				{
    					la = lca(prv->id, suc->id), lb = lca(prv->id, it->id);
    					
    					if (la == lb)
    						modify(dfn[lca(suc->id, it->id)], -1);
    					else
    						modify(dfn[lb], -1);
    				}
    			}
    			fnl[dep[cur]] = rt[lrt-1]; // 更新可持久化树根信息
    			
    			for (int p = hd[cur]; p != -1; p = nxt[p]) // 继续 BFS
    				q.push(des[p]), fdp = dep[des[p]];
    		}
    	}
    }
    

    处理单点&主程序:

    void solve()
    {
    	using namespace LCA;
    	using namespace SEGTREE;
    	using namespace FNINIT;
    	
    	memset(hd, -1, sizeof hd);
    	memset(dfn, -1, sizeof dfn);
    	
    	ind = e_cnt = fdp = 0;
    	n_cnt = lrt = 0;
    	
    	n = read();
    	int q = read(), lstans = 0, ta, tb;
    	
    	for (int i = 0; i < n; i++) // 输入
    	{
    		vl[i] = read() - 1;
    		if (!st[vl[i]].empty()) // 清空 set
    			st[vl[i]].clear();
    	}
    	
    	for (int i = 1; i < n; i++) // 建图
    	{
    		ta = read() - 1;
    		add_edge(ta, i);
    	}
    	
    	dep[0] = 0;
    	dfs(0, -1); // 预处理 LCA 和 DFS 序
    	lca_init();
    	
    	rt[lrt++] = build(1, n); // 建树
    	
    	bfs(); // BFS,最后预处理
    	
    	while (q--) // 处理询问
    	{
    		ta = (read() ^ lstans) - 1; // 5ab 用的是 0 开始的下标,所以需要进行转换
    		tb = read() ^ lstans;
    		
    		lstans = query(fnl[my_min(dep[ta]+tb, fdp)], dfn[ta] + 1, endp[ta] + 1, 1, n); // 记录并输出答案
    		printf("%d
    ", lstans);
    	}
    }
    
    int main()
    {
    	int cas = read();
    	while (cas--)
    		solve(); // 多组数据
    	return 0; // 然后就 AC 了、
    }
    

    后继

    做到这道题也是机缘巧合,这是某模拟赛题目的强化版。

    5ab 肝这道题肝了一天,300 行也是 5ab 迄今写过的最长代码,题解也写了 1h。我觉得我应该讲清楚了。



    1. 本文 DFS 序均指先序遍历。 ↩︎

    2. 就是统计贡献,下文将使用此类表达。 ↩︎

    3. 还有一种只用两次操作的方法。易观察到一个性质:该节点与其前驱和后继的 LCA 中,必有至少一个为其前驱和后继的 LCA。这样,分类讨论一下就能在两步内完成插入。给出的代码就是这样实现的。 ↩︎


    非常感谢您读完此文章。

    如果您喜欢这篇文章,请点一个赞支持一下博主。

    如果您对此文有意见,欢迎在评论区留下你的看法,5ab 会尽力达到。

    如果您真的不喜欢这篇文章,也请不要喷,5ab 欢迎私信反馈意见。

  • 相关阅读:
    逆元
    C++快读
    最长单调上升子序列(LIS) O(nlogn)求法
    【简●解】巴厘岛的雕塑
    【简●解】学校食堂
    【简●解】[HNOI2005]星际贸易
    差分约束系统小结
    【简•解】花园
    最小生成树小结
    概率及期望DP小结
  • 原文地址:https://www.cnblogs.com/5ab-juruo/p/solution-bzoj4771.html
Copyright © 2020-2023  润新知