cf1521 D. Nastia Plays with a Tree

题意:

给定一棵树。每次操作可以删一条边同时加一条边,问把树变成一条链至少要几次操作

思路:

官方题解看不懂,狠狠参考了几篇洛谷题解 https://www.luogu.com.cn/problem/solution/CF1521D

法一:贪心

如果节点 \(u\) 的后代是一条链,即 \(u\)\(u\) 的所有后代都最多只有一个儿子,就把 \(u\) 称为直链点;如果 \(u\) 恰有两个儿子且两个儿子都是直链点,就把 \(u\) 称为凸链点

做 dfs 到 \(u\) 时,\(u\) 的儿子们都已经处理好,即每个儿子要么是直链点要么是凸链点。现在考虑要把 \(u\) 处理成啥点:如果让 \(u\) 变成凸链点所需的操作数等于变成直链点,就让 \(u\) 变成直链点;否则一定变成凸链点需要的次数一定比直链点少一次,就让 \(u\) 变成凸链点。

如果某儿子 \(v\) 是凸链点,那一定不能留着,要删边 \(u-v\)

具体讨论一下:设 \(cnt\)\(u\) 的儿子数,

若只有一个儿子或者没儿子,不用操作;

若儿子全是凸链点,就花 \(cnt\) 次操作把 \(u\) 变成直链点。虽然花 \(cnt\) 次操作也能变成凸链点,但直链点比较灵活,变直链点的话后面操作空间比较大;

若有一个儿子是直链点,就花 \(cnt-1\) 次操作把其他儿子(凸链)都摘下来接到直链点后,使 \(u\) 变成直链点;

若有至少两个儿子是直链点,令这两个儿子不动,其他儿子接在他俩后面,即花费 \(cnt-2\) 次操作让 \(u\) 变成凸链点。

代码怎么写呢?注意可以先删所有要删的边,再加边。上面的删边操作实际上把树切成了若干连通块,每块都是一条链,最后加(块数-1)条边把这些链连成一条就行了。我觉得删边不好表示,就用不删的边建了个新图,最后在新图上dfs找出所有的链

//代码就dfs1函数值得看看,其他都是初始化、dfs2找连通块、输出答案等废话
const signed N = 5 + 2e5;
int n, d[N];
vector<int> G[N], _G[N];
vector<PII> del;
bool vis[N]; int idx, hh[N], tt[N]; //每条链的头尾

void init(int n) {
    for(int i = 0; i <= n; i++)
        G[i].clear(), _G[i].clear(), d[i] = 0,
        hh[i] = tt[i] = vis[i] = 0;
    del.clear();
    idx = 0;
}

void dfs1(int u, int fa) {
    for(int v : G[u]) if(v != fa)
        dfs1(v, u);

    int zhi = 0; //直链数量
    for(int v : G[u]) if(v != fa) {
        if(zhi < 2 && d[v] <= 2)
            zhi++, _G[u].pb(v), _G[v].pb(u);
        else del.pb({u,v}), d[u]--, d[v]--;
    }
}

void dfs2(int u, int fa) {
    vis[u] = 1;
    if(d[u] == 1) {
        if(!fa) hh[idx] = u;
        else tt[idx] = u;
    }

    for(int v : _G[u]) if(v != fa)
        dfs2(v, u);
}

void sol() {
    cin >> n;
    init(n);
    for(int i = 1; i < n; i++) {
        int x, y; cin >> x >> y;
        G[x].pb(y), G[y].pb(x);
        d[x]++, d[y]++;
    }

    dfs1(1, 0);

    //在删边后的图中找连通块,即链
    for(int u = 1; u <= n; u++)
        if(d[u] == 0) hh[idx] = tt[idx] = u, idx++;
        else if(d[u] == 1 && !vis[u]) dfs2(u, 0), idx++;

    cout << del.size() << endl;
    for(int i = 0; i < idx-1; i++)
        cout << del[i].fi << ' ' << del[i].se << ' ',
        cout << tt[i] << ' ' << hh[i+1] << endl;
}

法二:dp

仅考虑 \(u-\)子树,\(f_{u,1}\) 表示 \(u\) 是某条链的端点的答案,\(f_{u,0}\) 表示所有情况的答案

\(v_i\)\(u\) 的儿子,有三种情况:

\(u\) 跟所有儿子断开,\(x_1=\sum f_{v_i,0}\)

\(u\) 跟一个儿子连接,\(x_2=max\{f_{v_i,1}+\sum\limits_{j\neq i} f_{j,0}\}\)

\(u\) 跟两个儿子连接(即 \(u\) 属于一条凸链),\(x_3=\max \{ f_{i,1}+f_{j,1}+\sum f_{k,0} \},i,j,k\) 两两不相等

为了输出方案要记录转移。代码懒得写。

posted @ 2022-07-19 20:40  Bellala  阅读(83)  评论(0)    收藏  举报