「学习总结」久月

久月也是个可爱的月份呢 QAQ~

点分治

点分治三连: - Grt( o ): 找到结点 o 所在子树的 重心。 - solve( o ): 在以结点 o 为根的子树中,处理经过根节点路径或点对。 - divide( o ): 点分治主函数,调用 solve(o) 处理以 o 为根 经过点 o 的信息,然后删除点 o ,递归处理其儿子(divide(ex)

注意

  • 应保证 solve(o) 在执行过程中严格 \(O(\text{size}_o)\) 否则会导致总时间复杂度不正确。
  • 应保证 两次执行 Grt() 第二次调用只是为了确定以重心为根节点的子树大小。
  • solve(o)中往往是在考虑所有之前访问过的子树结点 到 当前刚刚访问的结点 之间统计答案。

例题 1

给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。

int sum;
int si[_N];
bool vis[_N];
bool judge[int(1e7 + 100)];
int qer[_N];
int MX[_N];
int ans[int(1e7 + 100)];
int rt;

void grt(int o, int f)
{
    si[o] = 1;
    MX[o] = 0;
    int MAX = 0;
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (ex == f || vis[ex])
            continue;
        grt(ex, o);
        si[o] += si[ex];
        MAX = max(si[ex], MAX);
    }
    MAX = max(MAX, sum - si[o]);
    MX[o] = MAX;
    if (MAX < MX[rt])
        rt = o;
}

int cnt = 0;
int tmp[_N];
void GetAllDis(int o, int f, int dis)
{
    if (dis > 1e7)
        return;
    tmp[++cnt] = dis;
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (ex == f || vis[ex])
            continue;
        GetAllDis(ex, o, dis + edge[i].w);
    }
}

queue<int> Q;
void solve(int o)
{
    judge[0] = true;
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (vis[ex])
            continue;
        cnt = 0;
        GetAllDis(ex, 0, edge[i].w);
        for (int j = 1; j <= cnt; j++)
        {
            for (int k = 1; k <= m; k++)
            {
                if (qer[k] >= tmp[j])
                    ans[k] |= judge[qer[k] - tmp[j]];
            }
        }
        for (int j = 1; j <= cnt; j++)
        {
            judge[tmp[j]] = 1;
            Q.push(tmp[j]);
        }
    }
    while (!Q.empty())
    {
        judge[Q.front()] = 0;
        Q.pop();
    }
}

void divide(int o)
{
    vis[o] = 1;
    solve(o);
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (vis[ex])
            continue;
        sum = si[ex];
        MX[rt = 0] = INT_MAX;
        grt(ex, 0);
        grt(rt, 0);
        divide(rt);
    }
}
int main()
{
    n = read(), m = read();
    for (int i = 1; i < n; i++)
    {
        int u = read(), v = read(), w = read();
        add(u, v, w);
        add(v, u, w);
    }
    for (int i = 1; i <= m; i++)
        qer[i] = read();
    sum = n;
    MX[rt = 0] = INT_MAX;
    grt(1, 0);
    grt(rt, 0);
    divide(rt);
    for (int i = 1; i <= m; i++)
    {
        if (ans[i])
            puts("AYE");
        else
            puts("NAY");
    }
    return 0;
}

例题 2

「luogu-P2634」「国家集训队」聪聪可可

给出一个带权树,求出所有满足长度为 \(3\) 的倍数的简单路径数量。

统计时,统计每个子树内,分别统计 根到子树中结点的路径长度 \(\text{mod}\ 3\) 后,不同值的数量。 在根节点拼接即可。

int n;
bool vis[_N];
namespace GetCenter
{
    int sum;
    int si[_N];
    int MX[_N];
    int rt;
    void dfs(int o, int f)
    {
        si[o] = 1;
        MX[o] = 0;
        int MAX = 0;
        for (int i = head[o]; i; i = edge[i].nxt)
        {
            int ex = edge[i].node;
            if (ex == f || vis[ex])
                continue;
            dfs(ex, o);
            si[o] += si[ex];
            MAX = max(MAX, si[ex]);
        }
        MAX = max(MAX, sum - si[o]);
        MX[o] = MAX;
        if (MX[rt] > MX[o])
            rt = o;
    }
    int grt(int o, int _sum)
    {
        MX[rt = 0] = INT_MAX;
        sum = _sum;
        dfs(o, 0);
        dfs(rt, 0);
        return rt;
    }
} // namespace GetCenter
using GetCenter::grt;
using GetCenter::si;

int ans = 0;
int cnt[3];
void GetDis(int o, int f, int dis)
{
    cnt[dis % 3]++;
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (ex == f || vis[ex])
            continue;
        GetDis(ex, o, dis + edge[i].w);
    }
}
int Bef[3];
void solve(int o)
{
    Bef[0] = 1;
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (vis[ex])
            continue;
        memset(cnt, 0, sizeof(cnt));
        GetDis(ex, 0, edge[i].w);
        for (int i = 0; i < 3; i++)
            ans += Bef[i] * cnt[(3 - i) % 3] * 2;

        for (int i = 0; i < 3; i++)
            Bef[i] += cnt[i];
    }
    memset(Bef, 0, sizeof(Bef));
}

void divide(int o)
{
    vis[o] = true;
    solve(o);
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (vis[ex])
            continue;
        int rt = grt(ex, si[ex]);
        divide(rt);
    }
}
int main()
{
    n = read();
    for (int i = 1; i < n; i++)
    {
        int u = read(), v = read(), w = read();
        add(u, v, w);
        add(v, u, w);
    }
    int rt = grt(1, n);
    divide(rt);
    ans += n;
    int A = ans, B = n * n;
    int g = gcd(A, B);
    printf("%d/%d\n", A / g, B / g);
    return 0;
}

栗题 3

「luogu-P4178」聪聪可可

给定一棵 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量

对每个儿子统计所有可能的长度,乘法原理以此匹配即可。

int vis[_N];
namespace GetCenter
{
    int si[_N];
    int MX[_N];
    int rt;
    int sum;
    void dfs(int o, int f)
    {
        si[o] = 1;
        int MAX = INT_MIN;
        for (int i = head[o]; i; i = edge[i].nxt)
        {
            int ex = edge[i].node;
            if (ex == f || vis[ex])
                continue;
            dfs(ex, o);
            si[o] += si[ex];
            MAX = max(si[ex], MAX);
        }
        MAX = max(MAX, sum - si[o]);
        MX[o] = MAX;
        if (MAX < MX[rt])
            rt = o;
    }
    int grt(int o, int _sum)
    {
        MX[rt = 0] = INT_MAX;
        sum = _sum;
        dfs(o, 0);
        dfs(rt, 0);
        return rt;
    }
} // namespace GetCenter
using GetCenter::grt;
using GetCenter::si;

int judge[_N], tjt = 0;
int tmp[_N], ttt = 0;
void GetDis(int o, int f, int w)
{
    if (w > k)
        return;
    tmp[++ttt] = w;
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (ex == f || vis[ex])
            continue;
        GetDis(ex, o, w + edge[i].w);
    }
}
int ans = 0;
void solve(int o)
{
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (vis[ex])
            continue;

        ttt = 0;
        GetDis(ex, 0, edge[i].w);

        for (int i = 1; i <= ttt; i++)
        {
            int now = k - tmp[i];
            int Q = (upper_bound(judge + 1, judge + 1 + tjt, now) - judge - 1);
            ans += Q;
        }
        for (int i = 1; i <= ttt; i++)
            judge[++tjt] = tmp[i];
        sort(judge + 1, judge + 1 + tjt);
    }
    ans += (upper_bound(judge + 1, judge + 1 + tjt, k) - judge - 1);
    tjt = 0;
}

void divide(int o)
{
    vis[o] = true;
    solve(o);
    for (int i = head[o]; i; i = edge[i].nxt)
    {
        int ex = edge[i].node;
        if (vis[ex])
            continue;
        int rt = grt(ex, si[ex]);
        divide(rt);
    }
}

int main()
{
    n = read();
    for (int i = 1; i <= n - 1; i++)
    {
        int u = read(), v = read(), w = read();
        add(u, v, w);
        add(v, u, w);
    }
    k = read();
    int rt = grt(1, n);
    divide(rt);
    printf("%d", ans);
    return 0;
}

线段树合并

栗题

「luogu-P4556」「Vani有约会」雨天的尾巴

首先村落里的一共有 \(n\) 座房屋,并形成一个树状结构。然后救济粮分 \(m\) 次发放,每次选择两个房屋 \((x , y)\),然后对于 \(x\)\(y\) 的路径上(含 \(x\)\(y\))每座房子里发放一袋 \(z\) 类型的救济粮。

然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。

重点是维护树上差分,然后在每一个叶子结点,建立一棵线段树,到达每一个非叶子结点,合并其子节点的线段树。

线段树应动态开点,保证空间复杂度正确。

marge

分情况讨论: 待填

int marge(int x, int y, int nowl, int nowr){
    if(!x) return y;
    if(!y) return x;
    if(nowl == nowr) return (Val[x].Max += Val[y].Max, x);
    int mid = (nowl + nowr) >> 1;
    ls(x) = marge(ls(x), ls(y), nowl, mid);
    rs(x) = marge(rs(x), rs(y), mid + 1, nowr);
    maintain(x);
    return x;
}