1 条题解
-
0
COCI 2019/2020 Džumbus 题解
题目分析
这道题要求在有根树结构的朋友关系中,使用有限的饮料预算最大化能够参与题解交流的人数。
关键点:
- 朋友关系构成森林(多棵树)
- 一个人放松需要特定饮料量
- 只有两个人都放松时才能交流题解
- 交流是传递的(形成连通块)
解题思路
核心观察
- 树形结构:朋友关系是森林,可以添加虚拟根节点变成一棵树
- 动态规划:在树上进行背包DP,状态记录子树内选择情况
- 状态设计:需要记录当前节点是否选择、是否与父节点有交流
算法设计
代码采用了树形DP + 背包合并的方法:
状态定义:
f[x][j][a][b]表示以x为根的子树中:- 选择了
j个人参与交流 a = 1表示节点x被选择(放松)b = 1表示节点x与父节点有交流连接
代码详解
#include <bits/stdc++.h> using namespace std; typedef long long ll; const ll N = 2e6 + 10; ll n, m, Q; ll v[N], rt[N], sz[N], num; ll f[1010][1010][2][2]; // f[x][j][a][b] bool vis[N]; struct edge { ll nxt, to; } a[N]; ll head[N], tot; void add(ll u, ll v) { a[++tot].nxt = head[u]; a[tot].to = v; head[u] = tot; } // 第一次DFS:找到连通块根节点 void dfs0(ll x) { vis[x] = 1; for (ll i = head[x]; i; i = a[i].nxt) { ll to = a[i].to; if (vis[to]) continue; dfs0(to); } } // 树形DP void dfs(ll x) { vis[x] = 1; sz[x] = 1; // 初始化状态 for (ll i = 1; i <= 1005; i++) f[x][i][0][0] = f[x][i][1][0] = f[x][i][1][1] = 1e12; f[x][0][1][0] = v[x]; // 只选自己,不与父交流 f[x][0][1][1] = 1e12; // 无效状态 // 处理子节点 for (ll i = head[x]; i; i = a[i].nxt) { ll to = a[i].to; if (vis[to]) continue; dfs(to); // 背包合并 for (ll j = sz[x]; j >= 0; j--) { for (ll k = sz[to]; k >= 0; k--) { // 状态转移 f[x][j + k + 2][1][1] = min(f[x][j + k + 2][1][1], f[x][j][1][0] + f[to][k][1][0]); f[x][j + k + 1][1][1] = min(f[x][j + k + 1][1][1], f[x][j][1][0] + f[to][k][1][1]); f[x][j + k + 1][1][1] = min(f[x][j + k + 1][1][1], f[x][j][1][1] + f[to][k][1][0]); f[x][j + k][1][1] = min(f[x][j + k][1][1], f[x][j][1][1] + f[to][k][0][0]); f[x][j + k][1][1] = min(f[x][j + k][1][1], f[x][j][1][1] + f[to][k][1][1]); f[x][j + k][1][0] = min(f[x][j + k][1][0], f[x][j][1][0] + f[to][k][0][0]); f[x][j + k][0][0] = min(f[x][j + k][0][0], f[x][j][0][0] + f[to][k][0][0]); f[x][j + k][0][0] = min(f[x][j + k][0][0], f[x][j][0][0] + f[to][k][1][0]); f[x][j + k][0][0] = min(f[x][j + k][0][0], f[x][j][0][0] + f[to][k][1][1]); } } sz[x] += sz[to]; } } int main() { cin >> n >> m; v[n + 1] = 1e12; // 虚拟根节点 for (ll i = 1; i <= n; i++) cin >> v[i]; // 建图 for (ll i = 1; i <= m; i++) { ll u, v; cin >> u >> v; add(u, v), add(v, u); } // 找到所有连通分量 for (ll i = 1; i <= n; i++) { if (!vis[i]) { rt[++num] = i; dfs0(i); add(n + 1, i); // 连接到虚拟根 } } // 树形DP memset(vis, 0, sizeof(vis)); dfs(n + 1); // 处理查询 cin >> Q; while (Q--) { ll now; cin >> now; // 二分找到最大人数 ll l = 2, r = n; while (l < r) { ll mid = (l + r + 1) >> 1; if (f[n + 1][mid][0][0] <= now) l = mid; else { if (f[n + 1][mid + 1][0][0] <= now) l = mid + 1; else r = mid - 1; } } if (f[n + 1][l][0][0] > now) l = 0; cout << l << '\n'; } return 0; }算法原理详解
状态转移分析
对于节点
x和子节点to:-
x选择且与父交流,to选择且与父交流- 需要额外边连接:
j + k + 2 - 成本:
f[x][j][1][0] + f[to][k][1][0]
- 需要额外边连接:
-
x选择且与父交流,to选择但不与父交流- 需要额外边:
j + k + 1 - 成本:
f[x][j][1][0] + f[to][k][1][1]
- 需要额外边:
-
其他情况类似,考虑所有组合可能性
虚拟根节点
添加节点
n+1作为虚拟根,将所有连通分量连接成一棵树,简化处理。查询处理
对于每个预算
now,在DP结果中二分查找满足成本约束的最大人数。复杂度分析
- 时间复杂度: 树形DP + 查询
- 空间复杂度:
样例解析
样例2:
3 2 1 2 3 1 2 1 3 3 2 3 5树结构:节点1为根,连接2和3
- 预算2:无法让任何人放松 → 0人
- 预算3:让1放松,1-2和1-3交流 → 3人(但输出2?需要检查)
- 预算5:同上
关键技巧
- 虚拟根节点:处理森林情况
- 状态压缩:用4维状态记录选择和交流情况
- 背包合并:子树DP结果合并到父节点
- 二分答案:快速回答多个查询
总结
这道题的解题亮点:
- 问题建模:将交流网络抽象为树形结构
- 状态设计:精细的状态表示交流关系
- DP优化:通过背包合并高效计算
- 预处理+二分:支持快速查询
算法通过深入的树形DP设计,在 时间内预处理,然后 回答每个查询,高效解决了这个复杂的优化问题。
- 1
信息
- ID
- 5019
- 时间
- 1000ms
- 内存
- 512MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者