SPOJ QTREE4 - Query on a tree IV
SPOJ QTREE4 - Query on a tree IV
Link
非常好的QTREE4,使我的大脑
\(Part.1\) 题目大意
每一个节点都有一个颜色(在黑和白中切换),有两种操作,第一种是反转一个节点的颜色,第二种是查询全局最大的两个白点之间的距离。
\(Part.2\) 思路
首先考虑如果没有修改,如何求解全局最大的两个白点之间的距离。我们可以对于每一个节点维护两个值 \(mx_i\) 和 \(se_i\) ,分别表示在 \(u\) 的子树中从 \(u\) 出发距离最远的白点的距离和在 \(u\) 的子树中不与 \(mx_i\) 重叠路径的白点中距离最远的距离。答案就是
如何动态维护一个 \(mx_u\) 和 \(se_u\) 呢?
可以发现,更改一个点上的颜色会影响 \(root\) 到 \(u\) 的 \(mx\) 和 \(se\)。同时 \(mx_{fa_{u}}\) 会从 \(mx_{u}\) 转移,可以看作是一种区间的合并。考虑可以使用树链剖分,进行单点的修改,同时进行区间跳(一条重链一条重链地跳),可以在 \(O(Nlog^2N)\) 的时间内完成。
\(Part.3\) 做法
对于 节点 \(u\) 记录一个 \(top_u\) 表示在 \(u\) 这棵子树中不走重儿子节点距离最远的白点的距离。记录 \(sec_u\) 表示不进入 \(top_u\) 进入的子树和重子树的最远白点的距离。默认初始值为 \(- \infty\)。
稍微详细
首先进行重链剖分。
每一条重链经过处理的 \(dfs\) 序都是连续的,所以对于每一条重链都开一颗线段树。对于 \([l, r]\) 区间具体维护 \(3\) 个值。
- \(lmax\) : 从 \(dfs\) 序为 \(l\) 出发,不进入 \(dfs\) 序为 \(r\) 为根的重子树,所能找到的最远白点的距离。
- \(rmax\) : 从 \(dfs\) 序为 \(r\) 出发,只在 \(dfs\) 序为 \(l\) 为根的子树中且不进入 \(dfs\) 序为 \(r\) 的重子树中,所能找到的最远白点的距离。
- \(midmax\) : 原树中满足 $dfn_{LCA(u, v)}\in [l, r] $ 的点对中 \(u, v\) 都是白点的最大距离。
多棵线段树,动态开点,由于我的实现中共用一个数组,所以使用单独数组记录一个重链中线段树的根结点编号,记为 \(rt_u (top_u = u)\)。
维护好信息后,我们可以求得答案
\(Part.4\) 树链剖分
树链剖分
其中,红色的线段表示 \(lson\) 中的 \(lmax\), 橙色的线段表示重链,深蓝色的线段表示 \(rson\) 中的 \(lmax\)。黄色表示 \(lson\) 的全程。粉色表示 \(dist_{mid, mid + 1}\)。
可以发现,整棵树合并过后的 \(lmax\) 的构成为黄色路径或红色路径+粉色路径+蓝色路径, 合并时取 \(\max\)。\(rmax\) 同理。
考虑 \(val\) 的转移。首先可以继承左右儿子的 \(val\),也可以让左儿子往右走,让右儿子往左走,再加上两个区间中间,即 \(dist_{mid, mid + 1}\)
线段树上的叶子节点,可以使用 \(mx_u\) 和 \(se_u\)。
\(mx_u\) 和 \(se_u\) 的值其实可以扩展到这条重链下面经过一条轻边所能够到达的重链上。
如何优秀的处理这样的先后关系呢?其实很简单,因为挂在下面的重链,\(dfs\) 序一定比上面的小,所以倒序枚举 \(dfs\) 序,进行初始化即可。
如果在初始化时已经将 \(se_u\) 和 \(mx_u\) 知道了,如何求解 \(lmax, rmax, val\) 呢?分类讨论颜色。
- \(rnk_u\) 为白色,\(lmax, rmax \leftarrow \max\{mx_{rnk_u}, 0\}\) 因为可以以当前节点为起点,以当前节点为终点。\(val \leftarrow \max \{mx_{rnk_u}, mx_{rnk_u} + se_{rnk_u}, 0\}\)。
- \(rnk_u\) 为黑色, \(lmax, rmax, val \leftarrow mx_{rnk_u} + se_{rnk_u}\)。
\(mx_{rnk_u}\) 表示以 \(rnk_u\) 为路径结尾的最大距离。\(mx_{rnk_u} + se_{rnk_u}\) 为两条路径拼接起来的距离。
考虑如何维护 \(mx_u\) 和 \(se_u\),暂时按下不表。
查询
如果我们已经可以高效求得 \(mx\) 和 \(se\),那么我们可以对于全局开一个一样的数据结构,将每一条重链的答案塞进去,每次取出最大的那个就可以了。
修改
是较难的一个部分。
其实就是边修改边往上跳。
对于当前的点 \(u\) 会影响 \(fa_{top_u}\) 的 \(mx\), \(se\)。容易发现就必须要消除更改的影响,即删除 \(lmax_{rt_{top_u}} + dis_{top_u} - dis_{fa_{top_u}}\), 然后在往上跳了一条重链一条轻边过后插入 \(lmax_{rt_{top_u}} + dis_{top_u} - dis_{fa_{top_u}}\)。
在全局中更改当前重链的答案。
解开谜题
你会发现,我们需要维护一种数据结构,可以快速查找第一个和第二个元素,可以支持随机插入与删除。
你会发现,这不就 multiset
吗?所以我们对于每一条重链和全局开一个 multiset
然后进行维护就可以了。
\(Part.5\) 代码
这道题目思维难度较大,实现难度也有,建议自己实现。
#include <bits/stdc++.h>
#define FASTIO \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0);
#define debug cout << "--------------------------\n";
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int N = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int siz[N], fa[N], son[N], dep[N], top[N], tid[N], rnk[N], len[N], dist[N], root[N], light[N], n, m;
vector<pii> G[N];
namespace HLD
{
int dcnt = 0;
void reset()
{
for (int i = 1; i < N; ++i)
G[i].clear();
dcnt = 0;
}
void dfs1(int x, int f)
{
siz[x] = 1;
fa[x] = f;
son[x] = -1;
for (auto [y, w] : G[x])
if (y != f)
{
dist[y] = dist[x] + w;
dfs1(y, x);
siz[x] += siz[y];
if (son[x] == -1 || siz[y] > siz[son[x]])
son[x] = y;
}
}
void dfs2(int x, int tp)
{
tid[x] = ++dcnt;
rnk[dcnt] = x;
top[x] = tp;
len[tp]++;
if (son[x] != -1)
dfs2(son[x], tp);
for (auto [y, w] : G[x])
if (y != fa[x] && y != son[x])
dfs2(y, y);
}
};
struct heap
{
struct cmp
{
bool operator() (int p, int q) const
{
return p > q;
}
};
typedef multiset<int, cmp> hp;
hp S;
void insert(int x)
{
S.insert(x);
}
void del(int x)
{
auto v = S.lower_bound(x);
if (v != S.end())
S.erase(v);
}
int top()
{
if (S.empty())
return -INF;
return *(S.begin());
}
int sec()
{
int fir = top();
if (fir != -INF)
{
del(fir);
int sen = top();
insert(fir);
return sen;
}
else
return -INF;
}
} h[N], ans;
namespace Seg
{
struct node
{
int l, r, ls, rs;
int lmax, rmax, midmax;
} tree[N << 2];
inline int lson(int x) { return tree[x].ls; }
inline int rson(int x) { return tree[x].rs; }
int ptr;
void merge(int x)
{
int l = tree[x].l, r = tree[x].r, mid = (l + r) >> 1;
tree[x].lmax = max(tree[lson(x)].lmax, dist[rnk[mid + 1]] - dist[rnk[l]] + tree[rson(x)].lmax);
tree[x].rmax = max(tree[rson(x)].rmax, dist[rnk[r]] - dist[rnk[mid]] + tree[lson(x)].rmax);
tree[x].midmax = max({tree[lson(x)].midmax, tree[rson(x)].midmax, tree[lson(x)].rmax + tree[rson(x)].lmax + dist[rnk[mid + 1]] - dist[rnk[mid]]});
}
void build(int &pos, int l, int r)
{
if (!pos)
pos = ++ptr;
tree[pos].l = l, tree[pos].r = r;
if (l == r)
{
int x = rnk[l];
for (auto [y, w] : G[x])
{
if (y != fa[x] && y != son[x])
h[x].insert(tree[root[top[y]]].lmax + w);
}
int d1 = h[x].top();
int d2 = h[x].sec();
tree[pos].lmax = tree[pos].rmax = max(d1, 0);
tree[pos].midmax = max({0, d1, d1 + d2});
return;
}
int mid = (l + r) >> 1;
build(tree[pos].ls, l, mid);
build(tree[pos].rs, mid + 1, r);
merge(pos);
}
void update(int pos, int x, int tp)
{
if (tree[pos].l == tree[pos].r)
{
if (x != tp)
h[x].insert(tree[root[tp]].lmax + dist[tp] - dist[x]);
int d1 = h[x].top();
int d2 = h[x].sec();
if (light[x])
{
tree[pos].lmax = tree[pos].rmax = d1;
tree[pos].midmax = d1 + d2;
}
else
{
tree[pos].lmax = tree[pos].rmax = max(d1, 0);
tree[pos].midmax = max({0, d1, d1 + d2});
}
return;
}
int mid = (tree[pos].l + tree[pos].r) >> 1;
if (tid[x] <= mid)
update(tree[pos].ls, x, tp);
else
update(tree[pos].rs, x, tp);
merge(pos);
}
};
void change(int x)
{
int last = x;
while (x)
{
int tp = top[x];
int p1 = Seg::tree[root[tp]].midmax;
if (fa[tp])
{
h[fa[tp]].del(Seg::tree[root[tp]].lmax + dist[tp] - dist[fa[tp]]);
}
Seg::update(root[tp], x, last);
int p2 = Seg::tree[root[tp]].midmax;
if (p1 != p2)
{
ans.del(p1);
ans.insert(p2);
}
last = tp;
x = fa[top[x]];
}
}
int main()
{
FASTIO;
cin >> n;
for (int i = 1; i < n; ++i)
{
int u, v, w;
cin >> u >> v >> w;
G[u].push_back(make_pair(v, w));
G[v].push_back(make_pair(u, w));
}
HLD::dfs1(1, 0);
HLD::dfs2(1, 1);
for (int i = n; i >= 1; --i)
{
int x = rnk[i];
if (x == top[x])
{
Seg::build(root[x], tid[x], tid[x] + len[x] - 1);
ans.insert(Seg::tree[root[x]].midmax);
}
}
cin >> m;
int cnt = n;
for (int i = 1; i <= m; ++i)
{
string op;
cin >> op;
if (op == "C")
{
int u;
cin >> u;
light[u] ^= 1;
if (!light[u])
cnt++;
else
cnt--;
change(u);
}
else
{
if (cnt == 0)
{
cout << "They have disappeared.\n";
}
else
{
cout << ans.top() << '\n';
}
}
}
return 0;
}
其他
关于这道题其实还有其他做法,如 LCT
, 点分治
, 但是树剖做法也是一种不做的思路,这道题目的这种解法最早出自2009年的集训队论文。奇怪的解法又增加了。