【题解】[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 欢迎私信反馈意见。

原文地址:https://www.cnblogs.com/5ab-juruo/p/solution-bzoj4771.html