CF1120D Power Tree

Solution Part1

子树操作,可以用DFS序解决。
这一题,我们实际上只用将叶子节点按DFS序从小到大排列,将其编号为1...n(n为叶子数量)。
对于点u,它控制的区间就是[cnt + 1,cnt + siz[u] + 1],cnt表示比u的DFS序小的叶子的数量,siz[u]表示u的子节点中的叶子数量。
我们将这个序列差分,对于每个区间[a,b],在点a与点b + 1之间连一条边,权值为控制[a,b]的代价,数字可以通过这条边流向另一个点,一端数字加w,另一端数字减w。
因为有的区间右端点可能为n,所以我们要多增加一个点n + 1,点n + 1的权值恒为0。这也意味着差分数组和为0。
这样我们的问题就变成了,在一个有n + 1个点的图上,选的边可以通过“搬运”两端的数字将图清零,求选边的最小代价。
很明显,只要图连通,数字就可以在图上自由流动,不管如何规定权值都可以将图清零。
求一遍最小生成树即可。

Solution Part2

第二行要我们输出可能存在于最优集合中的点,也就是求出所有最优集合的并集。
我们将其转化为求图上所有最小生成树的边集的并集。
我们考虑Kruskal算法。
Kruskal算法通过“合并”点集来求最小生成树。
我们将“合并”在一起的点通过类似强连通缩点的形式将其缩为一个点。此时图中所有点都属于不同的集合(因为缩点了)。若此时图中最小权值为w,此时所有权值为w的边都属于该图的某一个最小生成树。

Code

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 2e5 + 10;

struct EDGE{
    int u,v,w,id;
}e[MAXN];

bool cmp(EDGE a,EDGE b) {
    return a.w < b.w;
}

vector<int> edge[MAXN];
int wei[MAXN],n,siz[MAXN],fa[MAXN],num[MAXN],cnt,lcnt,ncnt;
bool mark[MAXN];
long long ans;

int find(int x) {return x == fa[x]?x:fa[x] = find(fa[x]);}

void merge(int a,int b) {fa[find(a)] = find(b);return;}

void DFS(int u,int pre) {
    int len = edge[u].size();
    num[u] = ++cnt;
    if (len == 1 && u != 1) {
        lcnt++;
        e[num[u]] = {lcnt,lcnt + 1,wei[u],u};
        siz[u] = 1;
        return;
    }
    e[num[u]].u = lcnt + 1;
    for (int i = 0; i < len; i++) {
        int v = edge[u][i];
        if (v == pre) continue;
        DFS(v,u);
        siz[u] += siz[v];
    }
    e[num[u]].v = e[num[u]].u + siz[u];
    e[num[u]].w = wei[u];
    e[num[u]].id = u;
}

void Kruskal() {
    sort(e + 1,e + n + 1,cmp);
    for (int i = 1; i <= lcnt + 1; i++) fa[i] = i;
    int j = 1;
    for (int i = 1; i <= n; i = j + 1) {
        while (j + 1 <= n && e[j + 1].w == e[i].w) j++;
        for (int k = i; k <= j; k++) {
            if (find(e[k].u) != find(e[k].v)) mark[e[k].id] = true,ncnt++;
        }
        while (i <= j) {
            int a = e[i].u,b = e[i].v;
            if (find(a) != find(b)) {
                merge(a,b);
                ans += e[i].w;
            }
            i++;
        }
    }
    return;
}

int main() {
    scanf("%d",&n);
    for (int i = 1; i <= n; i++) {
        scanf("%d",&wei[i]);
    }
    for (int i = 1; i < n; i++) {
        int u,v;
        scanf("%d%d",&u,&v);
        edge[u].push_back(v);
        edge[v].push_back(u);
    }
    DFS(1,0);
    Kruskal();
    cout << ans << ' ' << ncnt << endl;
    for (int i = 1; i <= n; i++) {
        if (mark[i]) cout << i << ' ';
    }
    cout << endl;
    return 0;
}

注:判断叶子节点时也要判它是不是根,不然遇到只有两个点的树就挂了。

posted @ 2021-08-10 20:10  kjd123456  阅读(21)  评论(0编辑  收藏  举报