树结构

試著忘記壹切 提交于 2019-12-04 06:34:07


URAL1039 Anniversary Party

\(O(n)\)

一棵树, 相邻点不能同时选, 问最大点权和

int ind[MAXN], val[MAXN];
int f[MAXN][2];
void DFS(int u, int fa)
{
    f[u][1] += val[u];
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS(v, u);
        f[u][0] += max(f[v][0], f[v][1]);
        f[u][1] += f[v][0];
    }
}

int main()
{
    n = in();
    for (int i = 1; i <= n; ++ i) val[i] = in();
    for (int i = 1; i < n; ++ i)
    {
        int l = in(), k = in();
        ++ ind[l];
        addedge(k, l);
    }
    for (int i = 1; i <= n; ++ i) 
        if (!ind[i]) 
        {
            DFS(i, 0);
            printf("%d\n", max(f[i][0], f[i][1]));
        }
    return 0;
}

CF1038A.The Fair Nut and the Best Path

好题

\(O(n)\)

一棵树, 有点权和边权, 你可以选一条路径, 满足在点上加油(加点权), 走过一条边耗油(减边权), 任意时刻油量不得为负

不能只选一个点, 求最大点权和\(-\)边权和

例如下图:

\(2\rightarrow 1\rightarrow 3\)

\(2\rightarrow 4\)

Sol:

在一条路径上走, 如果某时刻油量为负了, 那就不合法了

同时, 如果把"+点-边"看做一组 从下一个非负的组开始肯定可以更加优

所以只需要考虑最优, 而不需要考虑合不合法了

LL ans;
LL f[MAXN], fi[MAXN], se[MAXN];
void DFS(int u, int fa)
{
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        LL w = adj[i].w;
        if (v == fa) continue;
        DFS(v, u);
        se[u] = max(se[u], fi[v] + val[v] - w);
        if (se[u] > fi[u]) swap(se[u], fi[u]);
    }
    ans = max(ans, fi[u] + se[u] + val[u]);
}

HDU2196: Computer

\(O(n)\)

维护树上每个点最远能到达的距离, 有边权

  1. 可以维护每个点向下的最大次大路径, 和往哪个儿子走, 然后第二遍\(DFS\)再算一下往上走的最长路径
  2. 还可以第一遍只维护最大路径, 第二遍时先处理出除了最大儿子的其他儿子往上的最长, 同时算次大路, 最后更新给大儿子, 这样保证到达这个点时祖先向上走的最长路都已经算好了

第二种的代码(第一种大概要记录两条路的方向, 好烦)

//注意初始化啊, WA了好几发
LL up[MAXN], dw[MAXN], g1[MAXN];
void DFS(int u, int fa)
{
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to; LL w = adj[i].w;
        if (v == fa) continue;
        DFS(v, u);
        if (dw[v] + w > dw[u]) g1[u] = v, dw[u] = dw[v] + w;
    }
    
}
void DFS2(int u, int fa)
{
    LL se = 0;
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to; LL w = adj[i].w;
        if (v == fa) continue;
        if (v != g1[u]) up[v] = w + max(dw[u], up[u]), se = max(se, dw[v] + w);
        else up[v] = w;
    }
    up[g1[u]] += max(se, up[u]);
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS2(v, u);
    }
}
int main()
{
    while (~scanf("%d", &n))
    {
        nume = 0;
        memset(head, -1, sizeof head);
        memset(up, 0, sizeof up);
        memset(dw, 0, sizeof dw);
        memset(g1, 0, sizeof g1);
        for (int i = 1; i < n; ++ i) 
        {
            int u; LL w;
            scanf("%d%lld", &u, &w);
            addedge(u, i + 1, w);
            addedge(i + 1, u, w);
        }
        DFS(1, 0);
        DFS2(1, 0);
        for (int i = 1; i <= n; ++ i) printf("%lld\n", max(up[i], dw[i]));
    }
    return 0;
}

BZOJ1040 骑士

\(O(n)\)

\(n(\le 10^6)\)个骑士, 每个人有且仅有一个仇恨对象, 并有一个点权

问选出一些骑士, 满足其中任意两人没有仇恨关系, 最大点权和

Sol:

发现是基环树森林, 那么用栈记录当前的DFS路径, 就可以找环

然后换上的每个点为根做子树的树型DP, 然后再拿环上的点做一个DP, 钦点第一个点选不选

然后WA了, 原来互相仇恨会产生重边, 发现一个联通里最多一组重边, 并且形成了二元环, 那就用异或判反向边的方式回避这个问题, 让他自然形成二元环

也可以单独分类讨论,当树来做

错误原因: 栈中的前一段可能不是环!!!

bool vis[MAXN], flag, onc[MAXN];
int stk[MAXN], top, rec[MAXN], cnt;
void DFS(int u, int fa) // findcircle
{
    if (flag) return;
    if (vis[u]) 
    {
        flag = true;
        while (stk[top] != u) rec[++ cnt] = stk[top --];
        rec[++ cnt] = stk[top --];
        return ;
    }
    vis[u] = true; stk[++ top] = u;
    for (int i = head[u]; i != -1; i = adj[i].nex)
    {
        if ((i ^ 1) == fa) continue;
        int v = adj[i].to;
        DFS(v, i);
    }
    -- top;
}
LL f[MAXN][2], g[MAXN][2];
void DFS2(int u, int fa)
{
    vis[u] = true;
    f[u][0] = 0;
    f[u][1] += val[u];
    for (int i = head[u]; i != -1; i = adj[i].nex)
    {
        int v = adj[i].to;
        if ((i ^ 1) == fa || onc[v]) continue;
        DFS2(v, i);
        f[u][0] += max(f[v][0], f[v][1]);
        f[u][1] += f[v][0];
    }
}
LL circle(int opt)
{
    memset(g, 0, sizeof g);
    g[1][0] = (1LL * (opt == 0) * f[rec[1]][0]);
    g[1][1] = (1LL * (opt == 1) * f[rec[1]][1]);
    for (int i = 2; i <= cnt; ++ i)
    {
        g[i][0] = max(g[i - 1][0], g[i - 1][1]) + f[rec[i]][0];
        g[i][1] = g[i - 1][0] + f[rec[i]][1];
    }
    if (opt == 1) return g[cnt][0];
    else return max(g[cnt][0], g[cnt][1]);
}
void solve() 
{
    for (int i = 1; i <= n; ++ i) 
    {
        if (vis[i]) continue;
        cnt = top = 0; flag = false;
        memset(stk, 0, sizeof stk);
        memset(rec, 0, sizeof rec);
        DFS(i, -1);
        for (int j = 1; j <= cnt; ++ j) onc[rec[j]] = true;
        for (int j = 1; j <= cnt; ++ j) DFS2(rec[j], -1);
        ans += max(circle(0), circle(1));
    }
}

int main()
{
    memset(head, -1, sizeof head);
    n = in();
    for (int i = 1; i <= n; ++ i)
    {
        val[i] = in();
        int x = in();
        addedge(i, x); addedge(x, i);
    }
    solve();
    printf("%lld\n", ans);
    return 0;
}
/*
0853~1151
 */

NOI2002贪吃的九头龙

好题

\(O(n)\)

题意概述:

给一个\(n(\le 300)\)的以\(1\)为根的树, 以及树边的边权

贪吃龙有\(m\)个头

要求:

  • 树上的每个点属于一个头(点不要求连续), 每个头至少一个点
  • 假如相邻两点属于一个头, 则需要花费边权
  • 树根属于大头, 且大头恰有\(k\)个节点

Sol:

  1. 仔细观察, 发现除了大头以外, 其他的头都没有区别

    而且为了尽量少花费边权, 我们会尽量使得相邻点属于的头不同

    也就是说, 只要满足总个数的要求, 就不会无解

  2. 然后, 假如这个节点不是大头, 那么假如至少还有另一种不是大头, 那么可以使得儿子节点的头与他不同

    如果没有大头了, 那不是大头的儿子就都要花费了

  3. 简单设\(f[u][j][0/1]\)\(u\)为根的子树, 选了\(j\)个大头, \(u\)这个点是不是大头的最有答案

    那么可以想到依次扫儿子, 然后枚举这个儿子选了\(k\)个大头来转移

  4. 然后考虑让\(j\)倒着扫, 实现滚动

    于是发现可以\(j==k\),所以倒着扫也不能解决, 那只能开一个暂时的数组, 存储上一个儿子\(DP\)完时的数据

  5. 如果儿子是\(0\), 那么只有\(m=2\)且这个点是\(0\)才需要代价

    f[u][0][0] = f[u][1][1] = 0;
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to, w = adj[i].w;
        if (v == fa) continue;
        DFS(v, u);
        memcpy(pre, f[u], sizeof pre);
        memset(f[u], 0x3f3f3f, sizeof f[u]);
        for (int j = K; j >= 0; -- j)
        {
            for (int k = 0; k <= j; ++ k)
            {
                f[u][j][1] = min(f[u][j][1], pre[k][1] + min(f[v][j - k][1] + w, f[v][j - k][0]));
                f[u][j][0] = min(f[u][j][0], pre[k][0] + min(f[v][j - k][0] + (m == 2) * w, f[v][j - k][1]));
            }
        }
    }

BZOJ1304: CQOI2009叶子的染色

给定\(m\)个节点的树, 只知道\(1\)\(n\)是叶子, 不知道根是哪个

节点可以是透明, 白色, 和黑色. 给出根到每个叶子\(i\)的路径中最后一个有色的节点颜色为\(c[i]\), 即从\(i\)往上跳(包括自己)遇到的第一个颜色

随意钦点一个根, 使得满足以上条件且需要染最少的点, 求最少染色数

Sol:

  • 显然染在越浅的地方越优
  • 所以可以从下往上染色时, 此时处理的子树的根一定要染色
    • 若儿子原先染色跟自己相同, 可以消去它的颜色, 少染一次
    • 若不同, 只能保留他的颜色
  • \(f[u][0/1]\)为以\(u\)为根的儿子方向子树代价, \(g[u][0/1]\)为父亲方向子树代价
  • 计算\(g\)的时候算兄弟的贡献时, 可以用父亲的贡献减去自己参与计算时的贡献
int f[MAXN][2], g[MAXN][2];
void DFS(int u, int fa)
{
    int sum = 0, cnt0 = 0, cnt1 = 0;
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS(v, u);
        f[u][0] += min(f[v][0] - 1, f[v][1]);
        f[u][1] += min(f[v][1] - 1, f[v][0]);
    }
}
void DFS2(int u, int fa)
{
    ans = min(ans, min(g[u][0] + f[u][0], g[u][1] + f[u][1]) - 1);
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        g[v][0] = min(g[u][0] + f[u][0] - min(f[v][0] - 1, f[v][1]) - 1, g[u][1] + f[u][1] - min(f[v][1] - 1, f[v][0])) + 1;
        g[v][1] = min(g[u][1] + f[u][1] - min(f[v][1] - 1, f[v][0]) - 1, g[u][0] + f[u][0] - min(f[v][0] - 1, f[v][1])) + 1;
        DFS2(v, u);
    }
}

XY题: 好朋友

题意:

\(n\)个点\(n\)条无向边的连通图, 保证没有重边自环, 有边权, 每个点必须选择仅一个相邻的点连接, 问最小的连边的最大值为多少, 不满足连边需求输出”no answer”

输入格式:

  第一行一个正整数n,下面n行每行三个数u,v,w,表示u到v有一条边权为w的双向边。

样例输入:

4
1 2 3
2 3 10
3 4 3
1 4 1

样例输出:

数据范围:

50%的数据满足n<=20;

80%的数据满足n<=1000;

100%的数据满足n<=100000,-10^9<=w<=10^9

时间限制: 1S

空间限制: 64M

\(n\)个点\(n\)条边一定要想到环+树, 这题保证联通, 也就是只一个基环树

  • 偶数环有两种取边的方法
  • 不在环上的点取边确定
  • 如果确定不在环上的点向环上的点连边, 则环上的点也取边确定, 相当于其他点

依据这些性质, 可以考虑从一些叶子开始跑, 并将当前度数为\(1\)的点压进队列, 保证队列里的点取边确定

假如这样仍然有没访问到的点, 那么他们都有\(>1\)的度, 即形成一个环, 对于环只要DFS一下即可

注意无解的情况:

  • \(n\)是奇数
  • 队列里某点的出边连向已经取过的点
  • 环上的点确定后, 发现某时刻一个点度数为\(0\) (从两边都给他减了度数)
  • DFS环发现是奇数环
int ind[MAXN], ans = INF;
queue <int> q;
int mat[MAXN];
bool vis[MAXN], vis2[MAXN], now;
void DFS(int u, int cnt, int m1, int m2, int fa)
{
    if (now) return;
    if (vis2[u])
    {
        if ((cnt - 1) % 2 != 0) { printf("no answer\n"); exit(0); }
        ans = min(ans, max(m1, m2));
        now = true;
        return ;
    }
    vis2[u] = true;
    for (int i = head[u]; i != -1; i = adj[i].nex)
    {
        if (now) return;
        int v = adj[i].to;
        if ((i ^ 1) == fa || vis[v]) continue;
        if (cnt % 2 == 1) DFS(v, cnt + 1, min(m1, adj[i].w), m2, i);
        else DFS(v, cnt + 1, m1, min(m2, adj[i].w), i);
    }
}
 
int main()
{
    memset(head, -1, sizeof head);
    n = in();
    if (n % 2 == 1) return printf("no answer\n"), 0;
    for (int i = 1; i <= n; ++ i)
    {
        int u = in(), v = in(), w = in();
        addedge(u, v, w);
        addedge(v, u, w);
        ind[u] ++, ind[v] ++;
    }
    for (int i = 1; i <= n; ++ i)
        if (ind[i] == 1) q.push(i);
    while (!q.empty())
    {
        int u = q.front(); q.pop();
        vis[u] = true;
        int link = 0;
        for (int i = head[u]; i != -1; i = adj[i].nex)
        {
            int v = adj[i].to;
            if (vis[v]) continue;
            if (mat[v]) return printf("no answer\n"), 0;
            ind[v] --; vis[v] = true; mat[u] = mat[v] = 1;
            ans = min(ans, adj[i].w);
            link = v;
        }
        for (int i = head[link]; i != -1; i = adj[i].nex)
        {
            int v = adj[i].to;
            if (vis[v]) continue;
            ind[v] --;
            if (ind[v] == 0) return printf("no answer\n"), 0;
            if (ind[v] == 1) q.push(v);
        }
    }
    int sta = 0;
    for (int i = 1; i <= n; ++ i)
        if (!vis[i]) { sta = i; break; }
    if (!sta) return printf("%d\n", ans), 0;
    DFS(sta, 1, INF, INF, -1);
    printf("%d\n", ans);
    return 0;
}

URAL1018 Binary Apple Tree

\(n\)个点的树, 有树边, 根为\(1\), 要求保留\(m\)条边, 仍然是一颗以\(1\)为根的树, 求他的最大边权和

int siz[MAXN], f[MAXN][MAXN];
void DFS(int u, int fa)
{
    siz[u] = 0;
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS(v, u);
        siz[u] += siz[v] + 1;
        for (int j = min(m, siz[u]); j >= 0; -- j)
            for (int k = min(j - 1, siz[v]); k >= 0; -- k)
                f[u][j] = max(f[u][j], f[u][j - 1 - k] + f[v][k] + adj[i].w);
    }
}

int main()
{
    n = in(), m = in();
    for (int i = 1; i < n; ++ i)
    {
        int u = in(), v = in(), w = in();
        addedge(u, v, w); 
        addedge(v, u, w);
    }
    DFS(1, 0);
    printf("%d\n", f[1][m]);
    return 0;
}

POJ3107 Godfather

\(n\)个结点的树,要求删除一个节点之后,让剩下的块中节点数量最多的最少. 按编号顺序输出所有方案

Sol:

求树的所有重心

最大的儿子\(\le n/2\)就是重心, 假如大儿子\(>n/2\)就一定可以往那边移, 某时刻\(\le n/2\)

int siz[MAXN], hsiz[MAXN];
void DFS(int u, int fa)
{
    siz[u] = 1;
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS(v, u);
        siz[u] += siz[v];
        hsiz[u] = max(hsiz[u], siz[v]);
    }
    hsiz[u] = max(hsiz[u], n - siz[u]);
}

int main()
{
    n = in();
    for (int i = 1; i < n; ++ i) 
    {
        int u = in(), v = in();
        addedge(u, v); addedge(v, u);
    }
    DFS(1, 0);
    for (int i = 1; i <= n; ++ i)
        if (hsiz[i] <= n / 2) printf("%d ", i);
    return 0;
}

CF337D Book of Evil

\(n(\le 10^5)\)个点的树, 有\(m(\le n)\)个点有标记, 问有几个点和离他最远的标记点的距离小于\(d(\le n)\)

Sol:

记录每个点子树内的答案数, 在算一遍子树外的答案数

int mark[2][MAXN];
bool vis[2][MAXN];
 
void DFS1(int u, int fa)
{
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS1(v, u);
        if (vis[0][v]) 
        {
            vis[0][u] = true;
            mark[0][u] = max(mark[0][u], mark[0][v] + 1);
        }
    }
//    printf("0 %d : %d %d\n", u, mark[0][u], vis[0][u]);
}
void DFS2(int u, int fa)
{
//    printf("1 %d : %d %d\n", u, mark[1][u], vis[1][u]);
    int fi = -1, se = -1;
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        if (vis[0][v]) se = max(se, mark[0][v] + 1);
        if (se > fi) swap(fi, se); 
    }
    for (int i = head[u]; i; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        if (vis[0][u])
        {
            if (!vis[0][v] || mark[0][v] + 1 != fi) 
            {
                vis[1][v] = true;
                mark[1][v] = fi + 1;
            }
            else if (mark[0][v] + 1 == fi && se != -1) 
            {
                vis[1][v] = true;
                mark[1][v] = se + 1;
            }
        }
        if (vis[1][u]) 
        {
            vis[1][v] = true;
            mark[1][v] = max(mark[1][v], mark[1][u] + 1);
        }
        DFS2(v, u);
    }
}
 
int main()
{
    n = in(); m = in(); d = in();
    for (int i = 1; i <= m; ++ i) 
    {
        p[i] = in(), mark[0][p[i]] = mark[1][p[i]] = 0;
        vis[0][p[i]] = vis[1][p[i]] = true;
    }
    for (int i = 1; i < n; ++ i) 
    {
        int u = in(), v = in();
        addedge(u, v); addedge(v, u);
    }
    DFS1(1, 0); DFS2(1, 0);
    int ans = 0;
    for (int i = 1; i <= n; ++ i)
        if ((!vis[0][i] || (vis[0][i] && mark[0][i] <= d)) && (!vis[1][i] || (vis[1][i] && mark[1][i] <= d)))
            ++ ans;
    printf("%d\n", ans);
    return 0;
}

CF739B Alyona and a tree

\(n(\le 200000)\)个点的树, 有点权边权, 根为\(1\), 一个节点\(u\)能控制子树(不含自己)中和他的距离小于那个点点权的点

求每个点控制的点数

Sol:

记录当前根到这个点路径上各个深度是哪个点, 然后由于\(dis[u]-dis[x]\le a[u]\)满足单调性, 可以二分找

然后差分做标记

int n, m;
LL a[MAXN];

int lg[MAXN], rec[MAXN], tag[MAXN];
LL dis[MAXN], dep[MAXN];
void solve(int u)
{
    int l = 1, r = dep[u], ret;
    while (l <= r)
    {
        int mid = (l + r) >> 1;
        if (dis[u] - a[u] <= dis[rec[mid]]) ret = mid, r = mid - 1;
        else l = mid + 1;
    }
    ++ tag[u];
    -- tag[rec[ret - 1]];
}
void DFS(int u, int fa) // dis[u] - a[u] <= dis[x]
{
    dep[u] = dep[fa] + 1;
    rec[dep[u]] = u;
    solve(u);
    for (int i = head[u]; i != -1; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;       
        dis[v] = dis[u] + adj[i].w;
        DFS(v, u);
    }
}
void DFS2(int u, int fa)
{
    for (int i = head[u]; i != -1; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS2(v, u);
        tag[u] += tag[v];
    }
}

int main()
{
    memset(head, -1, sizeof head);
    n = in();
    for (int i = 1; i <= n; ++ i) a[i] = in();
    for (int i = 2; i <= n; ++ i)
    {
        int to = in(), w = in();
        addedge(to, i, w);
        addedge(i, to, w);
    }
    DFS(1, -1);
    DFS2(1, -1);
    for (int i = 1; i <= n; ++ i) printf("%d ", tag[i] - 1);
    return 0;
}

ZOJ3649 Social Net

题意: 给一个图, 有点权边权, 让你求最大生成树的值, 之后询问这棵树上的两个点\(x,y\)的路径信息, 即从\(x\)走向\(y\)的路径上, 后面的点减去前面的点的最大差值(有方向)

Sol:

可以想到用一些倍增数组维护信息, 需要:

  1. 向上跳...步后的祖先
  2. 向上跳...步经过的最大/最小值
  3. 向上跳...步这个路径 从上到下/从下到上 的最大差值(两个方向)

注意: 预处理维护倍增最大差值时要考虑前半段后半段, 以及跨越两段的差值, 而查询时也一样, 不能只把二进制拆分后的每段最大差值求最大, 也要像之前一样考虑跨越两端的差值

错因: 之前只做过倍增拆分后段与段之间无关的题(如最大最小, 求和), 没想到查询时也要这样维护, 考虑不周全

//错误片段
int LCA(int x, int y)
{
    int ret = 0, mnl = a[x], mxr = a[y];
    while (dep[x] < dep[y])
    {
        ret = max(ret, f[1][lg[dep[y] - dep[x]]][y]);
        mxr = max(mxr, mx[lg[dep[y] - dep[x]]][y]);
        y = up[lg[dep[y] - dep[x]]][y];
    }
    while (dep[x] > dep[y])
    {
        ret = max(ret, f[0][lg[dep[x] - dep[y]]][x]);
        mnl = min(mnl, mn[lg[dep[x] - dep[y]]][x]);
        x = up[lg[dep[x] - dep[y]]][x];
    }
    ret = max(ret, mxr - mnl);
    if (x == y) return ret;
    for (int i = lg[dep[x]]; i >= 0; -- i)
        if (up[i][x] != up[i][y])
        {
            ret = max(ret, max(f[0][i][x], f[1][i][y]));
            mnl = min(mnl, mn[i][x]); mxr = max(mxr, mx[i][y]);
            x = up[i][x], y = up[i][y];
        }
    mnl = min(mnl, mn[0][x]), mxr = max(mxr, mx[0][y]);
    ret = max(max(ret, mxr - mnl), max(f[0][0][x], f[1][0][y]));
    return ret;
}

做法已经改对了, 可是还是WA, 对拍也没有问题...

次日, 数据生成的点权可以再大一点? 题目给了\(10^5\), 我rand\(5*10^4\)

一拍真错了, 错因: INF设得太小

int n, m, q;
LL a[MAXN];

struct Edge { int u, v; LL w; } edge[MAXN];
bool cmp(Edge a, Edge b) { return a.w > b.w; }
int fa[MAXN];
int getf(int x) { return fa[x] == x ? x : (fa[x] = getf(fa[x])); }
bool unite(int x, int y)
{
    int fx = getf(x), fy = getf(y);
    if (fx == fy) return true;
    else { fa[fx] = fy; return false; }
}
LL Kruskal()
{
    LL ret = 0;
    for (int i = 1; i <= n; ++ i) fa[i] = i;
    sort(edge + 1, edge + m + 1, cmp);
    for (int i = 1, cnt = 0; i <= m; ++ i)
    {
        int u = edge[i].u, v = edge[i].v;
        if (unite(u, v)) continue;
        addedge(u, v);
        addedge(v, u);
        ret += 1LL * edge[i].w;
        if (++ cnt == n - 1) break;
    }
    return ret;
}

int dep[MAXN], lg[MAXN];
LL up[18][MAXN], mx[18][MAXN], mn[18][MAXN], f[2][18][MAXN]; //f[0] 向上走的路径, f[1]向下走的路径
void DFS(int u, int fa)
{
    dep[u] = dep[fa] + 1; 
    up[0][u] = fa;
    mx[0][u] = max(a[fa], a[u]); mn[0][u] = u == 1 ? a[u] : min(a[fa], a[u]);
    f[0][0][u] = ((u == 1) ? 0LL : max(0LL, a[fa] - a[u])); 
    f[1][0][u] = ((u == 1) ? 0LL : max(0LL, a[u] - a[fa]));
    for (int i = 1; (1 << i) <= dep[u]; ++ i)
    {
        up[i][u] = up[i - 1][up[i - 1][u]];
        mx[i][u] = max(mx[i - 1][u], mx[i - 1][up[i - 1][u]]);
        mn[i][u] = min(mn[i - 1][u], mn[i - 1][up[i - 1][u]]);
        f[0][i][u] = max(max(f[0][i - 1][u], f[0][i - 1][up[i - 1][u]]), mx[i - 1][up[i - 1][u]] - mn[i - 1][u]);
        f[1][i][u] = max(max(f[1][i - 1][u], f[1][i - 1][up[i - 1][u]]), mx[i - 1][u] - mn[i - 1][up[i - 1][u]]);
    }
    for (int i = head[u]; i != -1; i = adj[i].nex)
    {
        int v = adj[i].to;
        if (v == fa) continue;
        DFS(v ,u);
    }
}
void LCA(LL x, LL y, LL & lca, LL & mnl, LL & mxr)
{
    mnl = a[x], mxr = a[y];
    while (dep[x] < dep[y])
    {
        mxr = max(mxr, mx[lg[dep[y] - dep[x]]][y]);
        y = up[lg[dep[y] - dep[x]]][y];
    }
    while (dep[x] > dep[y])
    {
        mnl = min(mnl, mn[lg[dep[x] - dep[y]]][x]);
        x = up[lg[dep[x] - dep[y]]][x];
    }
    if (x == y) return (void)(lca = x);
    for (int i = lg[dep[x]]; i >= 0; -- i)
        if (up[i][x] != up[i][y])
        {
            mnl = min(mnl, mn[i][x]); mxr = max(mxr, mx[i][y]);
            x = up[i][x], y = up[i][y];
        }
    mnl = min(mnl, mn[0][x]), mxr = max(mxr, mx[0][y]);
    lca = up[0][x];
}
LL getup(LL x, LL lca)
{
    LL nowmn = 100001, ret = 0, i ;
    while (x != lca)
    {
        i = lg[dep[x] - dep[lca]];
        ret = max(ret, f[0][i][x]);
        ret = max(ret, mx[i][x] - nowmn);
        nowmn = min(nowmn, mn[i][x]);
        x = up[i][x];
    }
    return ret;
}
LL getdw(int lca, int x)
{
    LL nowmx = 0, ret = 0, i;
    while (x != lca)
    {
        i = lg[dep[x] - dep[lca]];
        ret = max(ret, f[1][i][x]);
        ret = max(ret, nowmx - mn[i][x]);
        nowmx = max(nowmx, mx[i][x]);
        x = up[i][x];
    }
    return ret;
}

int main()
{
    for (int i = 1; i <= 50000; ++ i) lg[i] = lg[i - 1] + ((2 << lg[i - 1]) == i);
    while (scanf("%d", &n) != EOF)
    {
        nume = 0;
        memset(head, -1, sizeof head);
        memset(mn, 0x3f3f3f, sizeof mn);
        memset(mx, 0, sizeof mx);
        memset(f, 0, sizeof f);
        memset(up, 0, sizeof up);
        memset(dep, 0, sizeof dep);
        for (int i = 1; i <= n; ++ i) scanf("%lld", &a[i]);
        scanf("%d", &m);
        for (int i = 1; i <= m; ++ i)
        {
            LL u, v, w;
            scanf("%lld%lld%lld", &u, &v, &w);
            edge[i] = (Edge) { u, v, w };
        }
        printf("%lld\n", Kruskal());
        DFS(1, 0);
        scanf("%d", &q);
        while (q --)
        {
            LL x, y; scanf("%lld%lld", &x, &y);
            LL lca, mnl, mxr;
            LCA(x, y, lca, mnl, mxr);
            printf("%lld\n", max(mxr - mnl, max(getup(x, lca), getdw(lca, y))));
        }
    }
    return 0;
}
/*
 */

Codeforces 294E. Shaass the Great

题意:
一棵\(N(\le 5000)\)个点的树, 有边权, 你要选择一条边删去, 再新连一条同样权值的边, 要求保证仍然是一棵树, 且所有点对距离和最小, 求这个值

Sol:
假设删去一条边\((u,v)\), \(v\)方向的大小为\(l\), \(u\)方向大小为\(n-l\)

则对于\(u\)方向的某一个边\((p,q)\), \(q\)靠近\(u\):
\(q\rightarrow p\)方向为\(p\)的子树,大小为\(x\)

  • 若新边连到子树外, 则这条边的贡献仍然是\(w*x*(n-x)\)为变
  • 若连到子树内, 则贡献变为\(w*(x+l)*(n-x-l)\), 增加了\(w*l*(n-2x)\)(可能为负)

对于\(v\)方向也同理

那么枚举删边之后, 显然\(l\)是确定的, 而对于某条边\((p,q)\), \(x\)也是确定的, 所以可以对于两个方向设\(f[u][0/1]\)表示\(u\)节点子树内/外有连接点时, 子树内所有边的最小增加贡献(可能为负), 然后DP一下

  • \(f[u][0]\), 内, 连接处在某一个儿子
  • 否则儿子也不存在连接点

注意自己设定的DP的定义: 连接点可以放在字数的根, 不计算根到根的父亲的贡献

\(O(n^2)\)

int n, m;

LL tot, ans;

int head[MAXN], nume;
struct Adj { int nex, to, w; } adj[MAXN << 1];
void addedge(int from, int to, int w)
{
    adj[++ nume] = (Adj) { head[from], to, w };
    head[from] = nume;
}
void link(int u, int v, int w) 
{ 
    addedge(u, v, w); addedge(v, u, w); 
} 
int siz[MAXN][2];
LL f[MAXN][2];
void DFS2(int u, int fa, int l)
{
    f[u][0] = f[u][1] = 0;
    siz[u][1] = 1;
    for (int i = head[u]; i; i = adj[i].nex)
    {
    int v = adj[i].to;
    if (v == fa) continue;
//  printf("%d --> %d (%d)\n", u, v, l);
    DFS2(v, u, l);
    siz[u][1] += siz[v][1];
    f[u][1] += f[v][1];
    }
    f[u][0] = f[u][1];
    for (int i = head[u]; i; i = adj[i].nex)
    {
    int v = adj[i].to, w = adj[i].w;
    if (v == fa) continue;
    f[u][0] = min(f[u][0], f[u][1] - f[v][1] + f[v][0] + 1LL * w * l * (n - 2 * siz[v][1] - l));
    }
//    printf("f[%d][0] = %lld f[%d][1] = %lld\n", u, f[u][0], u, f[u][1]);
}
void DFS(int u, int fa) // 枚举每一条边
{
    siz[u][0] = 1;
    for (int i = head[u]; i; i = adj[i].nex)
    {
    int v = adj[i].to, w = adj[i].w;
    if (v == fa) continue;
    DFS(v, u);
//  printf("%d <==> %d (%d)\n", u, v, w);
    tot += 1LL * w * siz[v][0] * (n - siz[v][0]);
    siz[u][0] += siz[v][0];
    DFS2(u, v, siz[v][0]); DFS2(v, u, n - siz[v][0]);
    ans = min(ans, f[u][0] + f[v][0]);
    }
}

int main()
{
    n = in();
    for (int i = 1; i < n; ++ i)
    {
    int u = in(), v = in(), w = in();
    link(u, v, w);
    }
    DFS(1, 0);
    printf("%lld\n", ans + tot);
    return 0;
}

什么? 删掉的边直接连两个树的重心就是最优的?

怎么证啊

换一个思路, 不是考虑每条边的贡献, 而是每个点的贡献

删边后得到两棵树, 此时总距离和=子树内距离和+子树间距离和
而两个子树都只有一个出口, 即所有点要到这个点, 那么显然是选择子树中到所有点距离和最小的那个点重心

BZOJ2427: [HAOI2010]软件安装 <简单环缩点 + 树型DP>

简单题

竟然忘记只有树根才能取值??!!

统计子树内外的颜色数量

子树内: 所有点标记+1, 同种颜色DFS序相邻的LCA 标记-1
子树外: 所有同种颜色的LCA就是第一个独占他的

子树外:

考虑找出每个子树独占的颜色数, 然后子树外的颜色数就是 总颜色数 - 独占颜色

处理出每种颜色的最左边和最右边的DFS序, 然后最小的/最深的 独占这个颜色的子树就是 最小的包含这个区间的子树区间, 在这个子树的根打标记, 然后贡献他的祖先

可以将颜色区间和子树区间排序, 用个指针插入set来二分找到

struct Node 
{
    int x, y, id;
    bool operator < (const Node b) const 
    {
        return (y != b.y) ? (y < b.y) : (x > b.x);
    }
} a[MAXN], col[MAXN];
///
    a[u] = (Node) { dfn[u], idx, u } ;
    if (!col[c[u]].id)
    {
    col[c[u]] = (Node) { dfn[u], dfn[u], c[u] };
    }
    else 
    {
    col[c[u]].x = min(col[c[u]].x, dfn[u]);
    col[c[u]].y = max(col[c[u]].y, dfn[u]);
    }
///
    sort(a + 1, a + n + 1, cmp);
    sort(col + 1, col + m + 1, cmp);
    for (int i = 1, j = 0; i <= m && j <= n; ++ i)
    {
    if (!col[i].id) continue;
    while (a[j + 1].x <= col[i].x && j + 1 <= n) ++ j, S.insert(a[j]);
    set <Node> :: iterator it = S.lower_bound(col[i]);
    Node fd = *it;
    tag[fd.id] ++;
    }

其实这个子树的根就是所有这种颜色的LCA
所以直接不断求相邻LCA即可

子树内

树上差分

首先给每个点打上一个标记, 表示这个点的颜色可以贡献给所有祖先

然后考虑怎么减去重复计算的颜色

对于每种颜色, 按照DFS序把相邻(挑出这种颜色后相邻, 即原DFS序中最近的)的两个点的标记-1

为什么只需要减去相邻的LCA而不是所有两点的LCA呢?

考虑树上一个节点有多个儿子, 如果这棵子树有这种颜色, 那么应当只贡献一次, 所以加总过来应当是\(\sum tag_{son} - num\)

然后还有儿子有这种颜色, 父亲也有的情况, 也应当-1

即应当对所有 相邻的 有这种颜色的 两个儿子的 父亲的标记-1

然后就会发现DFS序上相邻的LCA-1, 就会枚举到所有层面:

  • 两个兄弟有同种颜色: 左儿子的最右DFS序 和 右儿子最左DFS序 相邻
  • 第一个祖先和自己相邻

这样就是完整的了

线性做法(查询\(\ge\)k):

要算一个子树内外的颜色数量:
子树内的DFS序是连续的, 子树外是除了这一段的前缀 + 后缀拼成的
所以可以复制一整段DFS序接到后面

然后放一个指针在后面表示子树内的右端点
另一个指针在前面找出最靠右的符合的左端点(右指针向右移动左指针肯定也向右, 保证了复杂度)

查询的时候看最右左端点是否在子树左边界以内即可

NOIP2012d2t3 疫情控制

有一棵有边权的树, 从1到n标号, 根是1号
有m个军队分布在点上(一个点可能有多个)
要求 同时 移动一些军队到一些点(但是不能移到 ), 使得根到每个叶子的路径上都有军队, 并且移动时间最小(即军队最大移动时间最小)

Sol:

看错几次题意;

可以想到二分答案然后检验, 暴力把所有军队尽量跳到最靠近根的点(倍增), 然后有些点跳到1了, 还有些留在子树里跳不上来, 如果后者可以满足某棵子树的要求, 那么之后就不用管这个子树了

接下来: 有些子树一个军队也没有, 其他子树军队全部跳到了1

这些到1的要调到一些1的儿子

如果直接贪心匹配最远的和最近的, 那么自己的军队跳上去又跳回来是会多算的

假如所有点跳上去又跳回原来的树, 时间仍然允许, 也就是就算多算一点也没事, 那么就可以上述贪心匹配了, 大不了多算了跳回自己

接下来是关键:
假如某个子树有些点跳上去回不来(显然是深度最深的那几个), 那么根据贪心的策略, 选那些深度较浅的点(所有)跳回来会产生浪费(可以自己留着, 让这些点去满足其他的), 而深度更深(其他子树)的点肯定又跳不过来.
所以得到结论: 这棵子树深度最深的的点假如跳不回来, 就让他留着

这样之后, 剩下的点都是就算多算距离也能回到自己, 就可以贪心匹配了

BZOJ3626: [LNOI2014]LCA <树 + 转化 + 树剖 + 线段树 + 差分>

给出一个 \(n\) 个节点的有根树(编号为 \(0\)\(n-1\),根节点为 \(0\))。一个点的深度定义为这个节点到根的距离 \(+1\)

有q次询问,每次询问给出l r z
\(\sum_{l<=i<=r}dep[LCA(i,z)]\)

Sol:

假设只有一个询问, l r z
显然可以对与 \([l, r]\)\(z\) 暴力求所有LCA的深度, 复杂度\(O(n\lg n)\)

有没有更快的做法呢?
随便以 \(1\) 为根, 然后查询每个点能作为几个LCA(i, z), \(O(n)\)

具体做法就是统计每个点的子树中有\([l, r]\)里的几个点, LCA都在1~z的链上, 设这条链为\(u_0, u_1, \dots, u_k\), 容易得到答案为\(\sum dep[u_i] \cdot (cnt[u_i] - cnt[u_{i + 1}])\)

然后考虑本体:

区间\([l, r]\)没有什么具体意义, 应该不能对单个询问\(O(n)\)暴力\([l, r]\)变成\(O(\lg n)\), 能优化的话应该是优化所有查询

不难想到有些询问的\([l, r]\)是有交集的, 对于这部分可以尝试同意对一些询问z统计答案
上面那个式子化简得到\(\sum_{i=0} (i + 1) \cdot (cnt[u_i] - cnt[u_{i + 1}]) = \sum_{i=0} cnt[u_i]\)

这样就比较方便了, 如果能快速得到cnt[], 用树剖求和1~z的链就行了

然后想到差分\([l, r]\rightarrow [1, l - 1], [1, r]\), 所以先离线询问, 排序, 依次对链上的cnt[]加1, 这时r=i的询问\([1, r]\)已经搞定, 求一次答案, 然后减1来一遍减去\([1, l - 1]\)的答案即可

\(O(n\lg ^2 n)\)

struct Ask
{
    int l, r, z, id;
} ask[MAXN];
bool cmpl(Ask a, Ask b) { return a.l < b.l; }
bool cmpr(Ask a, Ask b) { return a.r < b.r; }

const LL MOD = 201314;
LL ans[MAXN];

int siz[MAXN], dep[MAXN];
int idx, dfn[MAXN], fa[MAXN], top[MAXN], hson[MAXN];
void DFS(int u, int ma)
{
    siz[u] = 1; fa[u] = ma;
    dep[u] = dep[ma] + 1;
    for (int i = head[u]; i; i = adj[i].nex)
    {
    int v = adj[i].to;
    if (v == ma) continue;
    DFS(v, u);
    siz[u] += siz[v];
    if (!hson[u] || siz[v] > siz[hson[u]]) hson[u] = v;
    }
}
void DFS2(int u, int t)
{
    dfn[u] = ++ idx;
    top[u] = t;
    if (hson[u]) DFS2(hson[u], t);
    for (int i = head[u]; i; i = adj[i].nex)
    {
    int v = adj[i].to;
    if (v == hson[u] || v == fa[u]) continue;
    DFS2(v, v);
    }
}

namespace Segt
{
#define ls (now << 1)
#define rs (now << 1 | 1)
#define mid ((l + r) >> 1) 
    const int MAXT = MAXN << 2;
    LL tag[MAXT], sum[MAXT];
    void pushup(int now) { sum[now] = (sum[ls] + sum[rs]) % MOD; }
    void addtag(int now, int l, int r, int v)
    {
    (sum[now] += (1LL * v * (r - l + 1) % MOD)) %= MOD;
    (tag[now] += v) %= MOD;
    }
    void pushdown(int now, int l, int r)
    {
    if (!tag[now]) return ;
    addtag(ls, l, mid, tag[now]); addtag(rs, mid + 1, r, tag[now]);
    tag[now] = 0;
    }
    LL query(int now, int l, int r, int x, int y)
    {
    if (x > r || y < l) return 0;
    if (x <= l && r <= y) return sum[now];
    pushdown(now, l, r);
    return (query(ls, l, mid, x, y) + query(rs, mid + 1, r, x, y)) % MOD;
    }
    void modify(int now, int l, int r, int x, int y, int v)
    {
    if (x > r || y < l) return;
    if (x <= l && r <= y) { addtag(now, l, r, v); return; }
    pushdown(now, l, r);
    modify(ls, l, mid, x, y, v); modify(rs, mid + 1, r, x, y, v);
    pushup(now);
    }
    void clear(int now, int l, int r)
    {
    sum[now] = tag[now] = 0;
    if (l == r) return;
    clear(ls, l, mid); clear(rs, mid + 1, r);
    }
#undef ls
#undef rs
#undef mid
}

void modify(int x, int y, int v)
{
    while (top[x] != top[y])
    {
    if (dep[top[x]] > dep[top[y]]) swap(x, y);
    Segt::modify(1, 1, n, dfn[top[y]], dfn[y], v);
    y = fa[top[y]];
    }
    if (dep[x] > dep[y]) swap(x, y);
    Segt::modify(1, 1, n, dfn[x], dfn[y], v);
}
LL query(int x, int y)
{
    LL ret = 0;
    while (top[x] != top[y])
    {
    if (dep[top[x]] > dep[top[y]]) swap(x, y);
    (ret += Segt::query(1, 1, n, dfn[top[y]], dfn[y])) %= MOD;
    y = fa[top[y]];
    }
    if (dep[x] > dep[y]) swap(x, y);
    (ret += Segt::query(1, 1, n, dfn[x], dfn[y])) %= MOD;
    return ret;
}

int main()
{
    n = in(); m = in();
    for (int i = 2; i <= n; ++ i)
    {
    int fa = in() + 1;
    addedge(fa, i);
    }
    DFS(1, 0);
    DFS2(1, 1);
    for (int i = 1; i <= m; ++ i) ask[i] = (Ask) { (int)in() + 1, (int)in() + 1, (int)in() + 1, i } ;
    sort(ask + 1, ask + m + 1, cmpr);
    for (int i = 1, j = 1; i <= n; ++ i)
    {
    modify(1, i, 1);
    while (ask[j].r < i && j <= m) ++ j;
    while (ask[j].r == i && j <= m) 
    {
        (ans[ask[j].id] += query(1, ask[j].z)) %= MOD;
        ++ j;
    }
    }
    Segt::clear(1, 1, n);
    sort(ask + 1, ask + m + 1, cmpl);
    for (int i = 1, j = 1; i <= n; ++ i)
    {
    while (ask[j].l < i && j <= m) ++ j;
    while (ask[j].l == i && j <= m) 
    {
        (ans[ask[j].id] += query(1, ask[j].z)) %= MOD;
        ++ j;
    }
    modify(i, 1, -1);
    }
    for (int i = 1; i <= m; ++ i) printf("%lld\n", (ans[i] + MOD) % MOD);
    return 0;
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!