题目笔记——P16957 [SCCPC 2026] 那一年的秘密基地
P话
本人先学的线段树,之后写啥都不想写树状数组了,码风可能很屎,请原谅……
题目大意:
给一棵 \(n\) 个点的树,以及一个长度为 \(n\) 的访问排列 \(a\)。
若选点 \(r\) 为根,危险度 \(f_r(a)\) 表示:按访问顺序看,后访问的点是先前访问点的祖先的二元组数量。
每次可以任选根 \(r\),使危险度最小。
有 \(q\) 次操作,每次交换排列中相邻两个位置 \(a_i,a_{i+1}\),要求输出初始时以及每次操作后,能选到的最小危险度。
解题思路
考虑先求出修改前的答案。
修改前的答案
每一次的根不同,可以换根DP,这样就需要先求出以一为根的答案。
以一为根的答案
既然我们已经有了一个排列——\(a_i\) 表示第 \(i\) 个被访问的点的下标,就再定义一个序列 \(pos_i\) 表示下标为 \(i\) 的点是第几个被访问的。
那么求以一为根的答案就是在这棵以一为根的树上 \(pos_v \lt pos_u\) 且 \(u\) 为 \(v\) 的祖先的二元组 \((u,v)\) 的个数。
因为 \(pos \le n\),所以可以考虑权值线段树维护当前时间节点是否被访问过,进行一次 dfs。每到一个节点就将线段树上这个点的 \(pos\) 标记,并储存枚举这个节点的子树前后比这个节点的 \(pos\) 小的已经标记过的点个数,将这两个数值求差并计入 \(cnt\) 中,\(cnt_i\) 表示 \(i\) 这个节点的子树中可以与 \(i\) 节点共同构成对答案有贡献的二元组 \((u,v)\) 的节点个数。
那么以一为根的答案就是所有节点的 \(cnt\) 之和。
换根DP
定义 \(c_i\) 为以 \(i\) 为根的子树中比 \(i\) 的父亲节点的 \(pos\) 小的节点个数,\(f_i\) 为 \(i\) 的父亲节点的其他子树中 \(pos\) 小于 \(i\) 的节点个数。
考虑从父亲节点 \(u\) 到其儿子节点 \(v\) 的转移。
对于 \(u\) 除去 \(v\) 的其他子树 \(pos\) 小于 \(i\) 的节点,当根节点从 \(u\) 转移到 \(v\) 时,这些节点会与 \(v\) 一起对答案多贡献。
对于以 \(v\) 为根的子树中比 \(u\) 的 \(pos\) 小的节点,当根节点从 \(u\) 转移到 \(v\) 时,这些节点与与 \(u\) 一起对答案的贡献会消失。
综上,可得动态转移方程:
\(c_i\) 可以在 dfs 时计算,\(f_i\) 又可以看作所有 \(pos\) 小于 \(i\) 的节点减去以 \(i\) 为根的子树中 \(pos\) 小于 \(i\) 的节点,也就是 \(pos_i-1-cnt_i\)。
每次修改后的答案
下面题解将用 \(u,v\) 来分别代替 \(a_i,a_{i+1}\)。
我们发现将 \(u,v\) 交换访问顺序对于整棵树只会修改 \(u,v\) 的子树。对于以 \(u\) 到 \(v\) 的路径作为“主链”,主链上除 \(u,v\) 两点的点以及这些点向主链外延申的所有点,以这些点为根的答案保持不变。
由于我们是以初始的 \(1\) 作为根求得答案,所以对于以 \(1\) 为根的这个树上 \(u,v\) 的不同位置关系要分类讨论。
1. \(u\) 为 \(v\) 的祖先
当以以 \(u\) 为根的子树外的节点或这个子树内除了可以到达 \(v\) 的儿子外的其他儿子的子树上的节点为根时,多了一个 \((u,v)\) 的二元组,答案加 \(1\)。
以以 \(v\) 为根的子树上的节点为根时,少了一个 \((u,v)\) 的二元组,答案减 \(1\)。
以其他节点为根时答案不变。
2. \(v\) 为 \(u\) 的祖先
与上面相反。
当以以 \(v\) 为根的子树外的节点或这个子树内除了可以到达 \(u\) 的儿子外的其他儿子的子树上的节点为根时,少了一个 \((u,v)\) 的二元组,答案减 \(1\)。
以以 \(u\) 为根的子树上的节点为根时,多了一个 \((u,v)\) 的二元组,答案加 \(1\)。
以其他节点为根时答案不变。
3. \(u,v\) 互不为对方的祖先
以以 \(u\) 为根的子树上的节点为根时,多了一个 \((u,v)\) 的二元组,答案加 \(1\)。
以以 \(v\) 为根的子树上的节点为根时,少了一个 \((u,v)\) 的二元组,答案减 \(1\)。
线段树维护
因为我们每次修改答案时修改整个子树,所以我们可以建立一个新的 dfs 序,并在这上面区间修改。
最后每次修改完后输出当前线段树上从 \(1\) 到 \(n\) 的最小答案即可。
代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define left (now << 1)
#define right ((now << 1) | 1)
#define mid ((l + r) >> 1)
const int N = 6e5 + 20, M = 1e6 + 20;
struct node{
int l, r, c;
}tr[M], Dp[M];
int n, q, a[N], pos[N], c[N], cnt[N], dp[N], ans = 1e18;
// c[i]: 以i为根的子树上,节点权值>pos[p[i]]的数量。
int Cnt, dfn[N], nw[N], siz[N], tag[N];
int f[N][25], dep[N];
vector<int> e[N];
void push_up(int now){
tr[now].c = tr[left].c + tr[right].c;
return ;
}
void build(int now, int l, int r){
tr[now].l = l, tr[now].r = r;
if(l == r){
tr[now].c = 0;
return ;
}
build(left, l, mid);
build(right, mid + 1, r);
push_up(now);
return ;
}
void change(int now, int p){
if(tr[now].l == tr[now].r){
tr[now].c = 1;
return ;
}
if(tr[left].r >= p) change(left, p);
else change(right, p);
push_up(now);
return ;
}
int query(int now, int l, int r){
if(l > r) return 0;
if(l <= tr[now].l && tr[now].r <= r) return tr[now].c;
int ans = 0;
if(tr[left].r >= l) ans += query(left, l, r);
if(tr[right].l <= r) ans += query(right, l, r);
return ans;
}
void dfs(int u, int fa){
dfn[u] = ++ Cnt;
int pre = query(1, 1, pos[fa] - 1), Pre = query(1, 1, pos[u] - 1);
change(1, pos[u]);
siz[u] = 1;
f[u][0] = fa;
dep[u] = dep[fa] + 1;
for(int i = 1; i <= 20; i++) f[u][i] = f[f[u][i - 1]][i - 1];
for(int v : e[u]){
if(v == fa) continue;
dfs(v, u);
siz[u] += siz[v];
}
c[u] = query(1, 1, pos[fa] - 1) - pre;
cnt[u] = query(1, 1, pos[u] - 1) - Pre;
dp[1] += cnt[u];
}
void dfs1(int u, int fa){
nw[dfn[u]] = dp[u];
// nw[u] = dp[u];
for(int v : e[u]){
if(v == fa) continue;
// dp[v] = dp[u] - c[v] + (n - pos[v] - siz[v] + 1 + cnt[v]);
dp[v] = dp[u] - c[v] + (pos[v] - 1 - cnt[v]);
dfs1(v, u);
}
}
int Lca(int u, int v){
if(dep[u] < dep[v]) swap(u, v);
for(int i = 20; ~i; i--){
if(dep[f[u][i]] >= dep[v]) u = f[u][i];
}
if(u == v) return u;
for(int i = 20; ~i; i--){
if(f[u][i] != f[v][i]){
u = f[u][i];
v = f[v][i];
}
}
return f[u][0];
}
void push_up_dp(int now){
Dp[now].c = min(Dp[left].c, Dp[right].c);
return ;
}
void push_down_dp(int now){
Dp[left].c += tag[now];
Dp[right].c += tag[now];
tag[left] += tag[now];
tag[right] += tag[now];
tag[now] = 0;
return ;
}
void build_dp(int now, int l, int r){
Dp[now].l = l, Dp[now].r = r;
tag[now] = 0;
if(l == r){
Dp[now].c = nw[l];
return ;
}
build_dp(left, l, mid);
build_dp(right, mid + 1, r);
push_up_dp(now);
return ;
}
void change_dp(int now, int l, int r, int k){
if(l <= Dp[now].l && Dp[now].r <= r){
Dp[now].c += k;
tag[now] += k;
return ;
}
push_down_dp(now);
if(Dp[left].r >= l) change_dp(left, l, r, k);
if(Dp[right].l <= r) change_dp(right, l, r, k);
push_up_dp(now);
return ;
}
int query_dp(int now, int l, int r){
if(l <= Dp[now].l && Dp[now].r <= r) return Dp[now].c;
int ans = 1e18;
push_down_dp(now);
if(Dp[left].r >= l) ans = min(ans, query_dp(left, l, r));
if(Dp[right].l <= r) ans = min(ans, query_dp(right, l, r));
return ans;
}
int find(int u, int v){
for(int i = 20; ~i; i--){
if(dep[f[u][i]] > dep[v]) u = f[u][i];
}
return u;
}
int x, sn, lca;
signed main(){
scanf("%lld%lld", &n, &q);
for(int i = 1; i <= n; i++){
scanf("%lld", &a[i]);
pos[a[i]] = i;
}
for(int i = 1, u, v; i < n; i++){
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
build(1, 1, n);
dfs(1, 0);
dfs1(1, 0);
for(int i = 1; i <= n; i++) ans = min(ans, dp[i]);
cout << ans << endl;
build_dp(1, 1, n);
while(q --){
cin >> x;
lca = Lca(a[x], a[x + 1]);
if(lca == a[x]){
sn = find(a[x + 1], a[x]);
change_dp(1, 1, n, 1);
change_dp(1, dfn[sn], dfn[sn] + siz[sn] - 1, -1);
change_dp(1, dfn[a[x + 1]], dfn[a[x + 1]] + siz[a[x + 1]] - 1, -1);
}
else if(lca == a[x + 1]){
sn = find(a[x], a[x + 1]);
change_dp(1, 1, n, -1);
change_dp(1, dfn[sn], dfn[sn] + siz[sn] - 1, 1);
change_dp(1, dfn[a[x]], dfn[a[x]] + siz[a[x]] - 1, 1);
}
else{
change_dp(1, dfn[a[x]], dfn[a[x]] + siz[a[x]] - 1, 1);
change_dp(1, dfn[a[x + 1]], dfn[a[x + 1]] + siz[a[x + 1]] - 1, -1);
}
swap(a[x], a[x + 1]);
cout << query_dp(1, 1, n) << endl;
}
return 0;
}

浙公网安备 33010602011771号