题目
题目信息
-
来源: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]可持久化线段树中。这种做法 貌似 是对的,但会出现一些问题。形如:
- 一个满足 BFS 序的遍历不一定满足 DFS 序。如果插入节点的 DFS 序介于两个节点之间怎么办?
- 如何寻找一个节点的 DFS 序前驱、后继?
- 不会 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)),有亿点常数,但不是很紧。
注意事项
- 请随时判
set
是否为空; - 可持久化线段树下标是 DFS 序而非节点序号;
- 多组数据请不要滥用 memset。
- 如果感觉代码逻辑很混乱,就写 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。我觉得我应该讲清楚了。