• 树结构



    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
    ", max(f[i][0], f[i][1]));
            }
        return 0;
    }
    

    CF1038A.The Fair Nut and the Best Path

    好题

    (O(n))

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

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

    例如下图:

    (2 ightarrow 1 ightarrow 3)

    (2 ightarrow 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
    ", 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
    ", 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,-109<=w<=109

    时间限制: 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
    "); 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
    "), 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
    "), 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
    "), 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
    ", ans), 0;
        DFS(sta, 1, INF, INF, -1);
        printf("%d
    ", 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
    ", 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
    ", u, mark[0][u], vis[0][u]);
    }
    void DFS2(int u, int fa)
    {
    //    printf("1 %d : %d %d
    ", 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
    ", 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
    ", 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
    ", 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 ightarrow 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)
    ", 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
    ", 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)
    ", 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
    ", ans + tot);
        return 0;
    }
    

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

    怎么证啊

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

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

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

    简单题

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

    统计子树内外的颜色数量

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

    子树外:

    (Mine)
    考虑找出每个子树独占的颜色数, 然后子树外的颜色数就是 总颜色数 - 独占颜色
    处理出每种颜色的最左边和最右边的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] ++;
        }
    

    (Right)
    其实这个子树的根就是所有这种颜色的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(nlg 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] ightarrow [1, l - 1], [1, r]), 所以先离线询问, 排序, 依次对链上的cnt[]加1, 这时r=i的询问([1, r])已经搞定, 求一次答案, 然后减1来一遍减去([1, l - 1])的答案即可

    (O(nlg ^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
    ", (ans[i] + MOD) % MOD);
        return 0;
    }
    

    子树合并复杂度为 (O(n^2))

    void DFS(int u, int fa)
    {
        siz[u] = 1;
        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);
    	for (int j = siz[u]; j >= 1; -- j)
    	{
    	    for (int k = 1; k <= siz[v]; ++ k)
    		    ...
    	}
    	siz[u] += siz[v];
        }
    }
    

    这段代码每次枚举 "加入前的(u)" 和 "待加入的(v)", 最后将子树 (v) 加入 (u)

    但是由于这样任意 两点只会在LCA时被枚举一次 , 所以复杂度是 (O(n^2))

    并且还有一个结论: 第二维只枚举 (k) 个时, 复杂度时 (O(nk))

    https://blog.csdn.net/lyd_7_29/article/details/79854245

    BZOJ4033: [HAOI2015]树上染色

    题意:

    树上有 (n) 个点, 你需要将 (m) 个点染黑, 其余白色
    并最大化 (黑点之间距离和 + 白点之间距离和)

    Sol:

    一开始有两种思路:

    • 转化成 (所有点对距离和 - 黑白之间距离和) 最小
    • 每个边对答案的贡献是两边对应点数的乘积

    本来以为照套路要转化成第一点才能做, 实际上这题直接原意就可以做, 无需转化(转化也行)

    重要的是第二点

    考虑只要一个子树内选定了黑点个数为 (k), 那么在子树外无论怎么选, 与子树内都是无关的, 即可以无后效性的选择子树内黑点个数为 (k) 时的最优子状态

    (dp[u][j]) 表示子树 (u) 内选择了 (k) 个黑点时, 子树内所有边对答案的贡献, 然后不断加入子树即可

    运用上述结论, 复杂度时(O(nm))

    注意这题DP状态可能从自己转移过来(子树不选黑点), 所以要注意滚动/特判

    LL f[MAXN][MAXN];
    int siz[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;
    			LL w = adj[i].w;
    			if (v == fa) continue;
    //			printf("%d --> %d (%lld)
    ", u, v, adj[i].w);
    			DFS(v, u);
    			siz[u] += siz[v];
    			for (int j = min(m, siz[u]); j >= 0; -- j)
    				{
    					if (j <= siz[u] - siz[v])
    						f[u][j] = max(f[u][j], f[v][0] + f[u][j] + w * (siz[v] * (n - siz[v] - m)));
    					for (int k = min(siz[v], j); k >= 1 && j - k <= siz[u] - siz[v]; -- k)
    						f[u][j] = max(f[u][j], f[v][k] + f[u][j - k] + w * (k * (m - k) + (siz[v] - k) * (n - siz[v] - m + k)));
    				}
    //			for (int j = 0; j <= min(siz[u], m); ++ j) printf("f[%d][%d] = %lld
    ", u, j ,f[u][j]);
    		}
    }
    
    int main()
    {
    	n = in();
    	m = in();
    	for (int i = 1; i < n; ++ i)
    		{
    			int u = in(), v = in();
    			LL w = in();
    			link(u, v, w);
    		}
    	DFS(1, 0);
    	printf("%lld
    ", f[1][m]);
    	return 0;
    }
    
  • 相关阅读:
    jquery $.fn $.fx 的意思以及使用
    jQuery树形控件zTree使用
    myeclipse9.0安装svn插件
    读取properties和xml中配置文件的值
    Jquery之ShowLoading遮罩组件
    程序员需谨记的8条团队开发原则(转)
    决策树算法
    第N个丑数
    数组反转
    倒数第K个结点
  • 原文地址:https://www.cnblogs.com/Kuonji/p/11837901.html
Copyright © 2020-2023  润新知