杭电多校算法拾遗
杭电多校算法拾遗
树上启发式合并(DSU on tree)
From D1T2 树
题意简述: 给定一棵根为 1 的树,点 \(i\) 有权值 \(A_i\)。对于每个节点 \(i\),要求计算:$$ans_i = \sum\limits_{u,v \in subtree(i)} \max(A_u, A_v) \times |A_u - A_v|$$输出 \(\mathrm{XOR}_i \ (ans_i\ \mod {2^{64}})\) 即可。
数据范围: \(n \leq 5\times 10^5, \ \ 1 \leq A_i \leq 10^6\)。
什么是树上启发式合并(DSU on tree): 我们希冀通过类似于树形 dp 的方式从下往上合并子树节点的贡献。具体地,每个根节点的答案继承了其各个分支子树内的贡献,同时还要算上跨越根节点的答案。对于前半部分,直接 dfs 即可;而对于后半部分跨越根的,假定这个答案是可用数据结构维护的,我们设想的暴力思路如下:
- 开一个全局的数据结构,对于每个根节点,递归完子树后,再递归一遍所有后代,塞回数据结构里求解(否则这个数据结构会被其他子树覆盖);
- 对于每个根都开一个数据结构,最后回溯时合并子树的数据,求出方案。
第一种方案是 \(O(n^2)\) 的会爆时间;第二种在本题中可应用线段树合并求解(暂不拾遗)。
我们考虑优化方案 1。观察到在第一次递归子树后不必完全清空数据结构,仍可保留一部分数据,这样第二次递归后就能少递归一些分支。以下是 树上启发式合并 的过程。
- 来到了一个根节点,此时数据结构中没有信息。
- 遍历所有轻子树,不保留轻子树 对全局数据结构的改变。遍历时要求其抹除数据,以便遍历其他子树。
- 最后遍历重子树,保留重子树 对全局数据结构的改变。这时数据结构中只有重子树的信息。
- 最后再遍历一遍轻子树,将所有轻子树后代添回数据结构中。
- 回到了根节点,此时数据结构中有所有子树节点。
- 如果我自己(根)对于父亲来说是轻子树,那么我将数据结构清零,否则直接回溯。
在算法实现中可以使用 dfn 序简化第二遍遍历轻子树过程。
算法的时间复杂度分析: 我们熟知以下定义与结论,
- 重子树是子树大小最大的子树,轻子树为其余子树;
- 重边是连接重子树与当前根的边,轻边是连接轻子树和当前根的边。
- 任意节点到根的简单路径(链)上不会有超过 \(\log n\) 条轻边。
我们观察算法过程,会发现某个结点被遍历的次数就为其到根 轻边数 + 1,这是因为每次其处于轻子树时,就要被重新遍历一遍。
故我们方可得到本算法时间复杂度为 \(O(n\log n)\)。
对于本题: 数据结构是个树状数组 / 线段树即可,塞入数据结构时统计其点权前后的贡献即可。只展示核心代码。
ll n, cnt, siz[MAXN], son[MAXN], dfn[MAXN], ndf[MAXN];
ull MaxA, ai[MAXN], ans[MAXN], res = 0;
vector<ull> e[MAXN << 1];
struct RES {
ull sm, sm2, ct;
} nas;
void dfs1(ull rt, ull fat)
{ // find out the Heavy/Light subtrees, solve DFN
siz[rt] = 1, son[rt] = 0;
dfn[rt] = ++cnt;
ndf[cnt] = rt;
for(auto to : e[rt]) {
if(to == fat) continue;
dfs1(to, rt);
siz[rt] += siz[to];
if(!son[rt] || siz[to] > siz[son[rt]])
son[rt] = to;
}
}
void calc(ull &res, RES &nas, ull nw)
{
nas = sgt.query(1, 1, MaxA, 1, nw - 1);
res += nw * (nw * nas.ct - nas.sm);
nas = sgt.query(1, 1, MaxA, nw + 1, MaxA);
res += nas.sm2 - nw * nas.sm;
}
void dfs2(ull rt, ull fat, bool kp)
{
for(auto to : e[rt]) { // update Light subtree, delete data
if(to == son[rt] || to == fat) continue;
dfs2(to, rt, false);
}
if(son[rt]) { // update Heavy subtree, maintain data
dfs2(son[rt], rt, true);
}
res = 0;
for(auto to : e[rt]) {
// traversal Light tree's vertices, rejoin data
ans[rt] += (to != fat ? ans[to] : 0);
if(to == fat || to == son[rt]) continue;
for(ull i = dfn[to]; i < dfn[to] + siz[to]; i++)
// use DFN to simplify process
calc(res, nas, ai[ndf[i]]);
for(ull i = dfn[to]; i < dfn[to] + siz[to]; i++)
sgt.update(1, 1, MaxA, ai[ndf[i]], 1);
}
calc(res, nas, ai[rt]);
sgt.update(1, 1, MaxA, ai[rt], 1);
ans[rt] += res * 2;
if(!kp) { // if rt a Light root itself, then delete data
for(ull i = dfn[rt]; i < dfn[rt] + siz[rt]; i++)
sgt.update(1, 1, MaxA, ai[ndf[i]], -1);
}
}
三分
From D3T11 抓拍
题意简述: 一共 \(n\) 人,初始时第 \(i\) 人在 \((x_i, y_i)\)。每个人行走的方向有四种情况,具体地:向东走,横坐标每秒加一;向西走,横坐标每秒减一;向北走,纵坐标每秒加一;向南走,纵坐标每秒减一。每个人的行走方向不会改变,而且永不会停止。
你可以选择一个非负整数秒对所有人抓拍一张照片。该照片可以采取一个水平长方形。要求长方形内(包括边界)包括所有人。求该长方形周长的最小值。
数据范围: \(1\leq n \leq 2 \times 10^5, \ \ -10^9 \leq x_i, y_i \leq 10^9\)。
什么是三分: 类似二分,三分算法作用于一个单峰数列(函数上),能够找到该函数的峰值。名如其实,三分就是找到左右边界的三等分点,然后类二分缩小边界。
对于一段区间 \([l, r]\),定义 \(m_l\) 为靠近 \(l\) 的三等分点、\(m_r\) 为靠近 \(r\) 的三等分点,我们能求出:
拿单峰下凸函数 \(f(x)\) 举例,我们要求其 \([l, r]\) 上的极小值,则每次
下面展示了 \(f(m_l) < f(m_r)\) 的例子,
如果 \(f(m_l) < f(m_r)\),那么最低点一定不在 \([m_r, r]\) 区间内,那么便可将 \(r\) 缩小到 \(m_r\) 处。
对于另一种情况,抑或是单峰上凸找极大值的同理,画画图即可。
Trick: 对于浮点值的三分,直接三分即可;对于离散整数的三分,由于边界不好处理,在 \(l, r\) 足够接近的时候跳出暴力能够避免繁琐讨论。
// 三分模板
const double D_double = 1e-5;
const int D_int = 20;
const int INF = 0x3f3f3f3f;
double Trisection_Double(double &L, double &R, std::function<double> f)
{ // Find minimum
double l = L, r = R, ml, mr;
while(R - L > D_double) {
ml = (2.0 * l + r) / 3.0;
mr = (l + 2.0 * r) / 3.0;
if(f(ml) < f(mr)) r = mr;
else l = ml;
}
return (L + R) / 2.0; // value if f((L + R) / 2.0)
}
int Trisection_Int(int &L, int &R, std::function<int> f)
{ // Find maximum
int l = L, r = R, ml, mr, ans, mx = INF;
while(R - L > D_int) {
ml = (2 * l + r) / 3;
mr = (l + 2 * r) / 3;
if(f(ml) < f(mr)) r = mr;
else l = ml;
}
for(int i = l; i <= r; i++) {
ans = (f(l) < mx) ? i : ans;
mx = max(mx, f(i));
}
return ans;
}
回到本题: 本题需要发现周长的变化随时间是一个单峰函数。
不失一般性地,我们先仅考察在竖直方向上的边长变化。观察到,在竖直方向上,边长只受最前方和最后方的人影响。同时竖直边长也受水平方向的人影响,即水平移动方向上最远的纵坐标差值决定了竖直变长的下限。
- 最一般的情况是最上方的人往下走,最下方的人往上走,此时竖直边长每秒减少 2;
- 如果最上方/最下方的人没入了水平界限时,竖直边长每秒可能减少 1;
- 如果最上方往上走,最下方往下走,竖直边长每秒可能减少 1 或 2;
所以边长的改变遵循下列顺序 \(-2 / s \rightarrow -1 /s \rightarrow 0 /s \rightarrow +1 /s \rightarrow +2 / s\)。
可能会有一些缺失,但总体成一下凸函数,水平边长类似。二者之和也下凸,运用三分即可。