zoukankan      html  css  js  c++  java
  • [BZOJ4987] Tree

    首先可以考虑一下 (k = n) 的特殊情况,问题就转化为选择一个遍历顺序走完所有点使得总代价最小。

    这样看来还是不好直接做,但可以反过来考虑每条边对答案的贡献。

    那么你会发现对于一条边 ((u, v)) 如果只被遍历一次,那么当前仅当必须先遍历完一遍的子树再遍历另一边的子树,否则这条边至少要被遍历两次。

    那么我们肯定要尽可能让更多的边只被遍历一次。

    于此同时你会发现因为只被遍历一次的要求,这些只被遍历一次的边一定会出现在同一条链上。

    为了尽可能让更多的边只被遍历一次,我们就必须选择这整一条链上的边让其只被遍历一次。

    于此同时这样的链有很多条,为了让答案尽可能小,我们一定要选择一条最长的链,也就是树的直径。

    那么对于其他的边,是否都能只被遍历两次呢?

    答案是可以的,因为我们从直径的一端开始可以很轻松地构造出一种方案。

    那么 (k = n) 时答案就为所有边权的两倍减去直径的长度。

    下面回到原问题。

    你会发现原问题与 (k = n) 的情况本质不同在于可能选出的 (k) 个点不是联通的,但仔细一想你会发现这是不可能的。

    因为如果选出的点不连通,我们每次都可以将一个连通块内选出一个点和另一个联通块接在一起,这样不会更劣。

    因此原问题就转化为选择一个大小为 (k) 的连通子树,使得其边权的两倍减去直径最小。

    可以发现这还是不好做,可以再进一步简化这个问题,如果不考虑直径的影响怎么做。

    你会发现可以直接考虑一个简单的树形 (dp),令 (dp_{i, j}) 表示以 (i) 为根的子树内包括 (i) 在内的联通子树选择了 (j) 个点的最小边权和。

    那么转移就直接考虑子树内 (v) 是否添加进来,添加进来几个点,树形背包转移即可。

    接下来再考虑直径的影响,先考虑上面那个 (dp) 能否改动后沿用上来。

    你会发现在合并 (v) 所在联通子树时直径只有 (3) 种情况。

    • 在之前 (u) 所在的联通子树内。

    • 在当前 (v) 所在的联通子树内。

    • (u, v) 中各伸出一条链来共同连成直径。

    那么 (dp) 时显然我们需要维护一个答案数组 (dp_{i, j}) 表示在以 (i) 为根的子树内选择了 (j) 个点的最小答案。

    那么你会发现对于前两种情况,转移相当于直接并上来一个新的联通子树边权的两倍,于是我们还需要维护一个 (g_{i, j}) 表示以 (i) 为根的子树内选择了 (j) 个点联通子树边权和两倍的最小值。

    对于第三种情况,为了能够合并,我们也必须维护一个 (f_{i, j}) 表示以 (i) 为根的子树内选择了 (j) 个点组成的联通子树边权和两倍减去从底部伸到 (i) 的最长链长度的最小值。

    (dp, f, g) 就直接按照上面的方式转移即可,具体直接看代码。

    #include <bits/stdc++.h>
    using namespace std;
    #define rep(i, l, r) for (int i = l; i <= r; ++i)
    #define dep(i, l, r) for (int i = r; i >= l; --i)
    #define Next(i, u) for (int i = h[u]; i; i = e[i].next)
    const int N = 3000 + 5;
    struct edge { int v, next, w;} e[N << 1];
    int n, m, u, v, w, tot, ans = 1e9, h[N], sz[N], f[N][N], g[N][N], dp[N][N];
    int read() {
        char c; int x = 0, f = 1;
        c = getchar();
        while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
        while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
        return x * f;
    }
    void add(int u, int v, int w) {
        e[++tot].v = v, e[tot].w = w, e[tot].next = h[u], h[u] = tot;
        e[++tot].v = u, e[tot].w = w, e[tot].next = h[v], h[v] = tot;
    }
    void dfs(int u, int fa) {
        dp[u][1] = f[u][1] = g[u][1] = 0, sz[u] = 1;
        Next(i, u) {
            int v = e[i].v; if(v == fa) continue;
            dfs(v, u);
            dep(j, 0, sz[u]) dep(k, 0, min(sz[v], m - j)) {
                g[u][j + k] = min(g[u][j + k], g[u][j] + g[v][k] + 2 * e[i].w);
                f[u][j + k] = min(f[u][j + k], min(f[u][j] + g[v][k] + 2 * e[i].w, g[u][j] + f[v][k] + e[i].w));
                dp[u][j + k] = min(dp[u][j + k], dp[u][j] + g[v][k] + 2 * e[i].w);
                dp[u][j + k] = min(dp[u][j + k], g[u][j] + dp[v][k] + 2 * e[i].w);
                dp[u][j + k] = min(dp[u][j + k], f[u][j] + f[v][k] + e[i].w);
            }
            sz[u] += sz[v];
        }
    }
    int main() {
        n = read(), m = read();
        memset(f, 0x3f, sizeof(f));
        memset(g, 0x3f, sizeof(g));
        memset(dp, 0x3f, sizeof(dp));
        rep(i, 1, n - 1) u = read(), v = read(), w = read(), add(u, v, w);
        dfs(1, 0);
        rep(i, 1, n) ans = min(ans, dp[i][m]);
        printf("%d", ans);
        return 0;
    }
    

    值得一提的是,从特殊情况出发和转化在本题中起着很大的作用。

    GO!
  • 相关阅读:
    【Qt】Qt之自定义界面(QMessageBox)【转】
    【WinForm】线程中向listview添加数据
    【Qt】Qt之自定义界面(窗体缩放-跨平台终极版)【转】
    【Qt】Qt之自定义界面(窗体缩放)【转】
    【Qt】Qt之自定义界面(添加自定义标题栏)【转】
    【Qt】Qt之自定义界面(实现无边框、可移动)【转】
    【Qt】QWidget、QDialog、QMainWindow的异同点【转】
    【Qt】QSettings读写注册表、配置文件【转】
    【Qt】QSettings介绍【转】
    【Qt】Qt之启动外部程序【转】
  • 原文地址:https://www.cnblogs.com/Go7338395/p/13787615.html
Copyright © 2011-2022 走看看