1 条题解
-
0
一、题意理解
我们有一棵 个节点的树,其中:
- 节点 是正确节点,节点 是错误节点。
- 从 到 的路径上节点编号递增(这条路径是主链)。
- 存档只能在正确节点上设置,且 和 必须存档,总共存档次数 。
- 玩家从 出发,每次在当前节点等概率选择一个儿子走。
- 如果走到错误叶子,下一步会读档(回到当前存档点)。
- 如果走到一个存档位置 (且 大于当前存档点 ),则存档点更新为 。
- 走到正确叶子 时结束。
- 目标是选择存档位置(除了 和 外再选 个正确节点),使得从 到 的期望步数最小。
二、样例分析
样例:
n=3, m=7, p=2 边: 1 4 2 5 3 6 3 7存档次数 ,必须存 和 ,没有额外选择。
实际上输入是: 1→4
2→5
3→6
3→7所以树是:
1 / \ 4 2 / \ 5 3 / \ 6 7是主链。
计算期望
存档点: 和 。
从 出发:
- 有 个儿子:(错误节点)和 (正确节点)。
- 概率 到 (错误叶子),走 步到 ,然后读档回到 ,总步数 = (其中 是从 开始的期望)。
- 概率 到 ,走 步到 ,然后从 继续。
从 出发:
- 有 个儿子:(错误叶子)和 (存档点)。
- 概率 到 :走 步到 ,读档回到 ,总步数 = 。
- 概率 到 :走 步到 (存档),然后结束(因为 )。
设 为从 开始的期望步数, 为从 开始的期望步数。
方程: 因为到 要读档回到 ,到 就结束(步数 )。
因为到 读档回到 ,到 则继续。
解得:
。
。代入 :
。符合样例。
三、问题抽象
我们有一条主链 ,每个节点 可能有一些错误子树。
存档点选择在主链的正确节点上,目标是最小化从 到 的期望步数。
四、动态规划思路
设 表示当前存档点在 ,还剩 次存档机会(包括当前节点 的存档)时,走到 的期望步数。
转移:
- 如果在 不设下一个存档点(即 次机会留给后面),则从 出发,可能走到错误子树(读档回到 )或走到儿子 (继续)。
- 如果在 设下一个存档点 ,则走到 后存档点更新为 。
实际上更常见的状态设计: 表示当前存档点在 ,下一个存档点在 ()时,从 走到 的期望步数。
那么总期望就是 ,其中 。
五、计算
从 出发,目标走到 (存档点),中途不经过其它存档点。
对于主链上的节点 :
- 从 出发,有若干儿子,其中一个是 (主链下一个),其它可能是错误子树。
- 设 是 的儿子数。
- 走到 的概率是 ,走到任意一个错误子树的概率是 。
如果走到错误子树:
- 期望步数 = 走到错误叶子的期望步数 + 1(读档步) + 从 重新开始的期望步数。
这会导致方程: 设 表示从 开始,存档点在 ,目标走到 的期望步数。
对于 :(已经到达目标存档点)。
对于 : $ E_x = 1 + \frac{1}{deg(x)} E_{x+1} + \frac{deg(x)-1}{deg(x)} (T_x + 1 + E_i) $ 其中 是从 出发走到错误叶子的期望步数(不读档)。
六、计算
对于错误子树,从 走到错误叶子的期望步数:
- 如果 是叶子:(但 是正确节点,不会直接是错误叶子,这里指错误子树里的节点) 实际上 对于错误节点 : $ T_u = 1 + \frac{1}{deg(u)} \sum_{v \in son(u)} T_v $ 如果 是错误叶子,则 ,。
我们可以预处理所有错误节点的 。
七、优化与实现
我们可以预处理 ,然后利用动态规划选择存档点: 设 表示当前存档点在 ,还剩 次存档机会,到 的最小期望步数。 $ F[i][k] = \min_{i < j \le n} \big( dp[i][j] + F[j][k-1] \big) $ 初始 ,答案 。
复杂度:预处理 是 ,选择存档点是 ,在 时可过。
八、代码框架(C++)
//#define Plus_Cat "" #include <bits/stdc++.h> #define INF 0x3f3f3f3f #define ll long long #define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d)) #define FOR(i,a,b) for(int i(a);i<=(int)(b);++i) #define DOR(i,a,b) for(int i(a);i>=(int)(b);--i) #define tomax(a,...) ((a)=max({(a),__VA_ARGS__})) #define tomin(a,...) ((a)=min({(a),__VA_ARGS__})) #define EDGE(g,i,x,y) for(int i=(g).h[(x)],y=(g)[(i)].v;~i;y=(g)[(i=(g)[i].nxt)>0?i:0].v) #define main Main();signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);return Main();}signed Main using namespace std; constexpr int N(700 + 10), M(1500 + 10); constexpr double DINF(1e100); struct Ecat { #define DE(...) E(#__VA_ARGS__,__VA_ARGS__) template<class T>void operator()(const char *fmt, const T a) { cerr << fmt << ":" << a << ".\n"; } template<class T, class...Types>void operator()(const char *fmt, const T a, const Types ...args) { while (*fmt^',') cerr << *fmt++; return cerr << ':' << a << ", ", (*this)(++fmt, args...); } } E; int Cas, n, m, Q; int deg[M]; double w[M], c[N][N], f[N][N]; vector<int> g[M]; void dfs(int u) { w[u] = 1; for (int v : g[u]) dfs(v), w[u] += w[v] / deg[u]; } #define mid ((l+r)>>1) void Sep(const int p, int l, int r, int L, int R) { if (l > r) return; int Mid(L); f[p][mid] = DINF; FOR(i, L, min(R, mid - 1)) { double tmp(f[p - 1][i] + c[i][mid]); if (tmp < f[p][mid]) f[p][mid] = tmp, Mid = i; } Sep(p, l, mid - 1, L, Mid), Sep(p, mid + 1, r, Mid, R); } #undef mid int Cmain() { /*DE("Input");*/ cin >> n >> m >> Q; FOR(i, n + 1, m) { int u, v; cin >> u >> v, g[u].push_back(v); } /*DE("DP on Tree");*/ FOR(u, 1, m)deg[u] = (int)g[u].size() + (u < n); FOR(u, 1, n - 1) { dfs(u), w[u] = 0; for (int v : g[u]) w[u] += w[v]; } /*DE("DP on Line");*/ FOR(i, 1, n) { c[i][i] = 0; FOR(j, i + 1, n)c[i][j] = c[i][j - 1] * deg[j - 1] + deg[j - 1] + w[j - 1]; } FOR(i, 2, n)f[1][i] = DINF; f[1][1] = 0; FOR(i, 2, Q)Sep(i, 1, n, 1, n); /*DE("Output");*/ cout << fixed << setprecision(4) << f[Q][n] << endl; /*DE("Clear");*/ FOR(i, 1, m)g[i].clear(); return 0; } signed main() { #ifdef Plus_Cat freopen(Plus_Cat ".in", "r", stdin), freopen(Plus_Cat ".out", "w", stdout); #endif for (cin >> Cas; Cas; --Cas) Cmain(); return 0; }
九、总结
本题的关键在于:
- 理解存档机制和读档规则对期望步数的影响。
- 将问题分解为计算相邻存档点之间的期望步数。
- 利用动态规划选择最优的存档位置。
- 注意走到错误叶子时额外的一步读档时间。
- 1
信息
- ID
- 4575
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者