2026.5.3情报系统听课笔记


预处理每个点到根节点的 xor 值之后就变成了一个路径上任意两点 xor 的和,这个我们可以通过维护某一位是 \(1\) 的个数,然后分别统计贡献来做到。至于修改操作,就等价于一个子树某一位翻转修改。具体用树链剖分 \(+\) 线段树实现即可。
\(from\) https://www.luogu.com.cn/article/uibh8d3n




区间合并的时候要注意如果左子树的右端和右子树的左端颜色相同那么数量要减一。
但是存在一个问题当前剖到的链与上一次的链在相交的边缘可能颜色相同,如果颜色相同答案需要减一。
所以统计答案的时候要记录下上一次剖到的链的左端点的颜色,与当前剖到的链右端点的颜色(因为在处理出的线段树中越靠近根的点位置越左),比较这两个颜色,若相同则答案减一。
又由于有 \(u\) 和 \(v\) 两个位置在向上走,那么要记录 ans1,ans2 两个变量来存“上一次的左端点颜色”。
有一点需要注意,当 top[u]==top[v] 的时候,即已经在同一个重链上时,两边端点颜色都要考虑与对应 ans 比较颜色,相同答案要相应减一。
\(from\) https://www.luogu.com.cn/article/opq8nngo
长链剖分




若 \(v\) 到连顶的距离 \(\geq k'\),则你可以求出每个长链中都有哪些节点,这种情况就是 \(\mathcal O(1)\) 了。

老师的代码:
点击查看代码
#include <cstdio>
#include <vector>
using namespace std;
typedef unsigned int ui;
const int MAXN = 500005;
const int LOG = 20;
int n, q;
ui s;
inline ui get(ui x) {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return s = x;
}
vector<int> ch[MAXN]; // 子节点
int par[MAXN]; // 父节点
int dep[MAXN]; // 深度(根为 1)
int height[MAXN]; // 子树高度(叶子为 0)
int hson[MAXN]; // 长链剖分的重儿子
int tp[MAXN]; // 所在长链的链顶
int anc[MAXN][LOG]; // 倍增表
int lg2[MAXN]; // 预处理 floor(log2)
// 每条链链顶存储的 up/down 数组,用全局 buffer + 指针
int buf[MAXN * 4], *ptr = buf;
int *up[MAXN], *dn[MAXN];
int chainlen[MAXN]; // 链长
int root;
void dfs1(int u) {
height[u] = 0;
hson[u] = 0;
for (int v : ch[u]) {
dep[v] = dep[u] + 1;
dfs1(v);
if (height[v] + 1 > height[u]) {
height[u] = height[v] + 1;
hson[u] = v;
}
}
}
void dfs2(int u, int chain_top) {
tp[u] = chain_top;
if (hson[u]) dfs2(hson[u], chain_top);
for (int v : ch[u]) {
if (v == hson[u]) continue;
dfs2(v, v);
}
}
void init() {
// 倍增表
for (int i = 1; i <= n; i++) anc[i][0] = par[i];
for (int j = 1; j < LOG; j++)
for (int i = 1; i <= n; i++)
anc[i][j] = anc[anc[i][j - 1]][j - 1];
// log2 预处理
lg2[1] = 0;
for (int i = 2; i <= n; i++)
lg2[i] = lg2[i >> 1] + 1;
// 每条链的 up/down 数组
for (int u = 1; u <= n; u++) {
if (tp[u] != u) continue; // 只处理链顶
int len = height[u]; // 链长(边数)= 链顶的 height
chainlen[u] = len;
// dn[u][0..len]: 从链顶向下的节点
dn[u] = ptr; ptr += len + 1;
int cur = u;
for (int i = 0; i <= len; i++) {
dn[u][i] = cur;
cur = hson[cur];
}
// up[u][0..len]: 从链顶向上的节点(up[u][0] = 链顶自己)
up[u] = ptr; ptr += len + 1;
cur = u;
for (int i = 0; i <= len; i++) {
up[u][i] = cur;
cur = par[cur]; // par[root] = 0,不会越界
}
}
}
int query(int x, int k) {
if (k == 0) return x;
// 第一步:倍增跳 2^j 步
int j = lg2[k];
x = anc[x][j];
k -= (1 << j);
if (k == 0) return x;
// 第二步:跳到链顶,在 up/down 数组中定位
int t = tp[x];
int d = dep[x] - dep[t]; // x 距离链顶的深度差
if (d >= k) {
// 目标在链上(链顶的 down 数组中)
return dn[t][d - k];
} else {
// 目标在链顶之上
return up[t][k - d];
}
}
int main() {
scanf("%u%u%u", &n, &q, &s);
for (int i = 1; i <= n; i++) {
int f;
scanf("%d", &f);
par[i] = f;
if (f == 0) {
root = i;
} else {
ch[f].push_back(i);
}
}
dep[root] = 1;
par[root] = 0; // 根的父亲为 0(哨兵)
dfs1(root);
dfs2(root, root);
init();
long long ans = 0;
ui lastans = 0;
for (int i = 1; i <= q; i++) {
int x = (get(s) ^ lastans) % n + 1;
int k = (get(s) ^ lastans) % dep[x];
lastans = query(x, k);
ans ^= (long long)i * lastans;
}
printf("%lld\n", ans);
return 0;
}

首先这个题可以想到是一个DP。状态设计:\(f_{u,dep}\) 表示 u 的子树中与 u 距离为 dep 的点的个数。
转移方程如下:
然而如果直接暴力转移的话显然会 T 飞或者 M 飞,,所以我们需要一点点的优化。
我们可以采用一个优化策略:对于一个节点 u,我们先对它的长儿子做DP,但这里可以使用一些技巧,让长儿子把 dp 出来的东西直接存到 \(f_u\) 里面去(当然观察 dp 式可以发现这边需要错一位),然后再把其他儿子 dp 出来的东西与 \(f_u\) 暴力合并。
这里详细地说一说到底怎么样实现这个优化(貌似其他题解写得都很简略啊……窝看了半天都看不懂,可能是我太菜了)
首先,我们抛弃传统 DP 的预先为每个节点都申请一片空间的写法(空间开销过大),而是在 DP 的过程中,动态的为节点申请(DP数组的)内存,所以这里我们要采用指针的写法。
然后,我们只对每一个长链的顶端节点申请内存,而对于一条长链上的所有节点,我们让他们可以公用一片空间。具体地说,假设对节点 u 申请了内存之后,设 v 是 u 的长儿子,我们就把 \(f_u\) 数组的起点(的指针)加一当作 \(f_v\) 数组的起点(的指针,下同),以此类推。这也就是上面说的“让长儿子把 dp 出来的东西直接存到 \(f_u\) 里面去”。当然,申请的内存要能装下一条长链。
那么显而易见的,使用了这个优化之后可以把时间和空间都减到 \(O(n)\) 级别的,因为每个节点都只会在它所在的长链顶端被统计(或者说是被暴力合并)一次。
这部分优化的代码长成这个样子:
点击查看代码
void dfs2(int u,int fa)
{
f[u][0]=1; // 先算上自己
if (son[u])
{
f[son[u]]=f[u]+1; // 共享内存,这样一步之后,f[son[u]][dep]会被自动保存到f[u][dep-1]
dfs2(son[u],u);
}
for (int e=head[u];e;e=nex[e])
{
int v=tail[e];
if (v==son[u] || v==fa) continue;
f[v]=now;now+=dep[v]; // 为 v 节点申请内存,大小等于以 v 为顶端的长链的长度
dfs2(v,u);
for (int i=1;i<=dep[v];i++)
{
f[u][i]+=f[v][i-1]; //暴力合并
}
}
}
当然,在 dp 开始前要先为以树根为顶端的长链申请内存。
然而,光有 dp 数组还没用,我们还要统计答案。
我们可以先令 u 节点的答案为它的长儿子的答案加一。然后在暴力合并的过程当中每次检查当前的 dep 是否优于 \(ans_u\) (\(ans_u\) 就是题目要求的东西),如果是的话那就更新答案。
最后如果发现 \(f_{u,ans_u}=1\),即 u 的子树为一条链,无论在哪个深度下都只有一个点的话,那么就把当前节点的答案 \(ans_u\) 设为 0 ,这个应该很好理解。
最后放上完整的代码:
点击查看代码
#include <bits/stdc++.h>
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
#define in inline
#define re register
using namespace std;
typedef long long ll;
typedef double db;
in int read()
{
int ans=0,f=1;char c=getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=-1;
for (;isdigit(c);c=getchar()) ans=(ans<<3)+(ans<<1)+(c^48);
return ans*f;
}
in int cmin(int &a,int b) {return a=min(a,b);}
in int cmax(int &a,int b) {return a=max(a,b);}
int n;
int buf[1000005];
int *f[1000005],*g[1000005],*now=buf;
int nex[2000005],head[1000005],tail[2000005],tot;
int ans[1000005];
void addedge(int u,int v)
{
nex[++tot]=head[u];
head[u]=tot;
tail[tot]=v;
}
int dep[1000005],son[1000005];
void dfs1(int u,int fa) // 长链剖分
{
for (int e=head[u];e;e=nex[e])
{
int v=tail[e];
if (v==fa) continue;
dfs1(v,u);
if (dep[v]>dep[son[u]]) son[u]=v;
}
dep[u]=dep[son[u]]+1;
}
void dfs2(int u,int fa) //做dp
{
f[u][0]=1;
if (son[u])
{
f[son[u]]=f[u]+1; // 共享内存
dfs2(son[u],u);
ans[u]=ans[son[u]]+1; //从长孩子节点继承答案
}
for (int e=head[u];e;e=nex[e])
{
int v=tail[e];
if (v==son[u] || v==fa) continue;
f[v]=now;now+=dep[v]; // 分配内存
dfs2(v,u);
for (int i=1;i<=dep[v];i++)
{
f[u][i]+=f[v][i-1]; //暴力合并
if (f[u][i]>f[u][ans[u]] || (f[u][i]==f[u][ans[u]] && i<ans[u])) ans[u]=i; //更新答案
}
}
if (f[u][ans[u]]==1) ans[u]=0;
}
int main()
{
n=read();
for (int i=1;i<n;i++)
{
int u=read(),v=read();
addedge(u,v);
addedge(v,u);
}
dfs1(1,0); // 长链剖分
f[1]=now;now+=dep[1]; // 为根节点的答案分配内存
dfs2(1,0);
for (int i=1;i<=n;i++) cout<<ans[i]<<endl;
return 0;
}
\(from\) https://www.luogu.com.cn/article/83per2n0





怎么建立虚树?看看这篇文章:https://www.luogu.com.cn/article/zyv7zgvz


点击查看代码
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#define pii std::pair<int, int>
using std::cin;
using std::cout;
const int N = 5e5 + 10;
typedef long long ll;
int tot;
int top;
bool qu[N];
int fa[N];
int lb[N];
int id[N];
int dfn[N];
int min[N];
int stk[N];
int dep[N];
ll f[N];
std::set<int> s;
std::vector<pii> e[N];
std::vector<pii> g[N];
void dfs(int x, int fat)
{
dfn[x] = ++tot;
fa[x] = fat;
dep[x] = dep[fat] + 1;
int a = fa[x];
int b = lb[a], c = lb[b];
if (dep[a] - dep[b] == dep[b] - dep[c])
lb[x] = c;
else
lb[x] = fa[x];
for (auto nxt : e[x])
{
int to = nxt.first;
if (to == fat)
continue;
int val = nxt.second;
min[to] = std::min(min[x], val);
dfs(to, x);
}
}
int lca(int x, int y)
{
if (x == y)
return x;
if (dep[x] < dep[y])
std::swap(x, y);
while (dep[x] > dep[y])
{
if (dep[lb[x]] > dep[y])
x = lb[x];
else
x = fa[x];
}
while (x != y)
{
if (lb[x] != lb[y])
{
x = lb[x];
y = lb[y];
}
else
{
x = fa[x];
y = fa[y];
}
}
return x;
}
void add(int x, int y)
{
if (dep[x] > dep[y])
std::swap(x, y);
g[x].push_back({y, min[y]});
s.insert(x);
}
void dfs1(int x, int v)
{
for (auto nxt : g[x])
{
int to = nxt.first;
int val = nxt.second;
dfs1(to, val);
}
ll sum = 0;
if (x == 1)
{
for (auto nxt : g[x])
{
int to = nxt.first;
sum += f[to];
}
f[x] = sum;
}
else if (qu[x])
f[x] = v, qu[x] = false;
else
{
for (auto nxt : g[x])
{
int to = nxt.first;
sum += f[to];
}
f[x] = std::min(sum, 1ll * v);
}
}
int main()
{
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i < n; ++i)
{
int u, v, w;
cin >> u >> v >> w;
e[u].push_back({v, w});
e[v].push_back({u, w});
}
min[1] = 1e9;
dfs(1, 0);
int m;
cin >> m;
while (m--)
{
for (auto num : s)
g[num].clear();
s.clear();
int k;
cin >> k;
for (int i = 1; i <= k; ++i)
cin >> id[i], qu[id[i]] = true;
std::sort(id + 1, id + k + 1, [&](const int &a, const int &b){return dfn[a] < dfn[b];});
stk[top = 1] = id[1];
for (int i = 2; i <= k; ++i)
{
while (true)
{
int l = lca(id[i], stk[top]);
if (dep[l] >= dep[stk[top - 1]])
{
if (l != stk[top])
{
add(l, stk[top]);
if (l != stk[top - 1])
stk[top] = l;
else
top--;
}
break;
}
else
add(stk[top - 1], stk[top]), top--;
}
stk[++top] = id[i];
}
if (stk[1] != 1)
add(1, stk[1]);
for (int i = 1; i < top; ++i)
add(stk[i], stk[i + 1]);
dfs1(1, 0);
cout << f[1] << '\n';
}
return 0;
}
这里使用了一个奇技淫巧。
就是我们不直接维护每一条边的值,而是维护儿子到根的边权的前缀 \(min\),思考一下为什么。

浙公网安备 33010602011771号