Gym103373 G. Garden Park

Gym103373 G. Garden Park

题意:

给定一个有\(n\)个节点的树,及\(n-1\)条边的边权,计算有多少条不同的路径经过的边是严格递增的。

分析:

不妨选定点\(1\)为根,树上所有的路径,要么是先向根走的,要么是先向叶子走的。

所以容易想到这样定义状态,dp1[u]为以u为起点,先向叶子走的答案,dp2[u]为以u为起点,先向根走的答案。

但这样定义有一个问题,dp1[u]的答案中第一步的边权是什么都有可能,难以从叶子向上递推计算,但是dp2[u]没有这样的问题,先向根走的第一条边是唯一的,所以需要改进一下dp1[u]的定义方式。

因为只要确定第一步的边权,就可以递推,而确定第一步的边权,只需要知道选了哪个儿子即可,加之儿子的父亲是唯一的,所以在记u的父亲是f的情况下,我们可以定义dp1[u]是以f为起点,第一步向u走的答案。

这样对于dp1[u]来说,只需要一次dfs即可在\(O(n)\)的时间内计算。

struct Edge {
    int u, v, w; // 由u指向v边权为w的边
};

// u是当前节点,f是u的父亲(若不存在,即为0),w是f到u这条边的边权
// dp1[u]为以f为起点,第一步向u走的答案
void dfs(int u, int f, int w) {
    // 如果f不是0(即u不是根节点),f到u这条边会对dp1[u]提供1的贡献
    // 因为根节点没有父亲,所以dp1[u]没有意义
    if (f) dp1[u] = 1;
    for (auto& e : G[u]) {
        if (e.v == f) continue;
        dfs(e.v, u, e.w);
        // u不是根节点时,如果满足e.w > w,dp1[e.v]的答案应当加进dp1[u]的答案中
        if (f && e.w > w) dp1[u] += dp1[e.v];
    }
}

接下来,要计算dp2[u],很容易想到一个\(O(n^2)\)的转移

struct Edge {
    int u, v, w; // 由u指向v边权为w的边
};
// dp2[u]为以u为起点,先向根走的答案
// f是u的父亲,ff是f的父亲,w1是(f,u)边的边权,w2是(ff,f)边的边权
void dfs2(int u, int f, int ff, int w1, int w2) {
    if (f) {
        // 只走(u,f)有1的贡献
        dp2[u] = 1;
        // 满足条件时,dp2[f]应该计算进dp2[u]中
        if (ff && w2 > w1) dp2[u] += dp2[f];
        // 枚举u的兄弟,将所有边权大于w1的对应dp1全部统计进dp2[u]中
        for (auto& e : G[f]) {
            if (e.v == u) continue;
            if (e.v == ff) continue;
            if (e.w > w1) dp2[u] += dp1[e.v];
        }
    }
    for (auto& e : G[u]) {
        if (e.v == f) continue;
        dfs2(e.v, u, f);
    }
}

由于,这道题的评测数据太弱了,这样写也能过,但是显然可以构造一个\(n=2\times 10^5\)的,\(1\)节点边分别连向\(2,3,\dots,n\)的一棵树,卡掉上面的做法。

分析一下,可以发现,每次在考虑同辈之间的每个\(u\)的时候,都会把兄弟扫一遍,这样是很蠢的。

容易发现对于f和他的儿子们以及他们之间的边,第一步从某个儿子到f走的边,可以按边权从大到小考虑,这样在考虑某个儿子的时候,能够同时维护或者预处理出上面代码中“所有边权大于w1的对应dp1”之和。

所以在读入树的信息后,可以对于每个G[u]u的邻接边)按边权从大到小排序,然后预处理出某个儿子u对应的“所有边权大于w1的对应dp1”之和,即为sum[u]

利用这些可以优化dfs2,具体细节见代码,尤其是边权相等的问题。

排序是\(O(n\log n)\)的,预处理和两个dfs都是\(O(n)\)的。

代码:

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef long long Lint;
const int maxn = 2e5 + 10;
struct Edge {
    int u, v, w;
    bool operator<(const Edge& rhs) const { return w > rhs.w; }
};
int n;
vector<Edge> G[maxn];
Lint dp1[maxn], sum[maxn];  // dp1[u]为以f为起点,第一步向u走的答案
Lint dp2[maxn];             // dp2[u]为以u为起点,先向根走的答案
// 为了方便预处理和简化代码,fa[u]为u的父亲,w[u]为(u,fa[u])边的边权
int fa[maxn];
int w[maxn];

void dfs(int u, int f, int w_) {
    if (f)
        dp1[u] = 1;
    fa[u] = f, w[u] = w_;
    for (auto& e : G[u]) {
        if (e.v == f)
            continue;
        dfs(e.v, u, e.w);
        if (f && e.w > w_)
            dp1[u] += dp1[e.v];
    }
}

void dfs2(int u) {
    if (fa[u]) {
        dp2[u] = 1;
        if (fa[fa[u]] && w[fa[u]] > w[u])
            dp2[u] += dp2[fa[u]];
        dp2[u] += sum[u];
    }
    for (auto& e : G[u]) {
        if (e.v == fa[u])
            continue;
        dfs2(e.v);
    }
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        G[u].push_back({u, v, w});
        G[v].push_back({v, u, w});
    }
    // 对于每个i的儿子们按边权从大到小排序
    for (int i = 1; i <= n; i++)
        sort(G[i].begin(), G[i].end());
    dfs(1, 0, 0);
    for (int u = 1; u <= n; u++) {
        // 预处理u的儿子们
        // s代表枚举到当前儿子的dp1之和,p_s代表严格大于当前儿子对应边权的那些儿子的dp1之和。
        Lint s = 0, p_s = 0;
        // 写成这样子是为了处理边权相等的问题
        for (int i = 0; i < G[u].size();) {
            auto e = G[u][i];
            if (e.v == fa[u]) {
                i++;
                continue;
            }
            sum[e.v] = p_s;
            s += dp1[e.v];
            i++;
            // 处理边权相等的问题
            int tw = e.w;
            while (i < G[u].size()) {
                e = G[u][i];
                if (e.v == fa[u]) {
                    i++;
                    continue;
                }
                if (e.w != tw)
                    break;
                sum[e.v] = p_s;
                s += dp1[e.v];
                i++;
            }
            p_s = s;
        }
    }
    dfs2(1);
    Lint ans = 0;
    for (int i = 2; i <= n; i++)
        ans += dp1[i] + dp2[i];
    cout << ans << '\n';
}
posted @ 2022-03-17 21:12  聆竹听风  阅读(190)  评论(0编辑  收藏  举报