杭电多校算法拾遗

杭电多校算法拾遗

树上启发式合并(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。观察到在第一次递归子树后不必完全清空数据结构,仍可保留一部分数据,这样第二次递归后就能少递归一些分支。以下是 树上启发式合并 的过程。

  1. 来到了一个根节点,此时数据结构中没有信息。
  2. 遍历所有轻子树,不保留轻子树 对全局数据结构的改变。遍历时要求其抹除数据,以便遍历其他子树。
  3. 最后遍历重子树,保留重子树 对全局数据结构的改变。这时数据结构中只有重子树的信息。
  4. 最后再遍历一遍轻子树,将所有轻子树后代添回数据结构中。
  5. 回到了根节点,此时数据结构中有所有子树节点。
  6. 如果我自己(根)对于父亲来说是轻子树,那么我将数据结构清零,否则直接回溯。

在算法实现中可以使用 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\) 的三等分点,我们能求出:

\[m_l = (2l + r) /3 \\ m_r = (l + 2r) / 3 \]

拿单峰下凸函数 \(f(x)\) 举例,我们要求其 \([l, r]\) 上的极小值,则每次

\[\begin{aligned} \text{if }f(m_l) < f(m_r) \quad &l = m_l\\ \text{otherwise} \quad &r = m_r \end{aligned} \]

下面展示了 \(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\)

可能会有一些缺失,但总体成一下凸函数,水平边长类似。二者之和也下凸,运用三分即可。

posted @ 2024-07-27 20:40  anjack_511  阅读(6)  评论(0编辑  收藏  举报