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';
}