专栏文章
题解:AT_agc008_f [AGC008F] Black Radius
AT_agc008_f题解参与者 2已保存评论 1
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 1 条
- 当前快照
- 1 份
- 快照标识符
- @min6c6zq
- 此快照首次捕获于
- 2025/12/01 21:17 3 个月前
- 此快照最后确认于
- 2025/12/01 21:17 3 个月前
很好玩的题目!没有想到题解区做法,这里给一个鏖战许久的容斥做法(不知道为什么写了这么长)。
这个关键点、距离等限制有一种 [十二省联考 2019] 希望 的既视感(虽然这个题更早)。直接算单个点的贡献就是以其为根时的最大深度,直接加起来会算重。
和希望那题一样,猜测对于一个树上连通块 ,能染出来 的 在树上也是一个连通块。这应该是容易证明的,具体在下面的求解过程中也可以感知到。
先考虑 怎么做。
考虑容斥,可以直接套点边容斥,就是只需要算点的方案数减去边的方案数。更多关于这个结论的内容可以去希望的题解区。
点的方案数上面提过了,就是以其为根时的最大深度(根的深度为 )。
至于边的方案,考察树上一条边 ,其中 是 的父亲:
令 表示在 子树外一点到 的最大距离, 表示在 子树内一点到 的最大距离。考虑选择 进行染色,是否存在 使得选择 染色的方案一样。建议画图感知一下下面的分讨。
-
若 ,此时必须有 ,否则在 子树内的染色情况不同。那么这时必须要求 ,否则在 子树外的染色情况不同。这种情况的方案数为 。
-
若 ,此时必须有 ,否则在 子树外的染色情况不同。同时有 ,否则在 子树内的染色情况不同。这种情况的方案数为 ,可以看到和上面那种情况是恰好互补的。
-
若 ,这是唯一剩下的没有被上面讨论到的情况。此时取 是唯一解,方案数为 。
综合一下几种情况,一条边 的方案数为 。
自此解决了 的点,发现非常良心地有这档分(于是我以为我的思路是正解思路)。Code。
接下来考虑原题目。
原题目由于指定了关键点,容斥会有点麻烦。具体来说,不妨回归本源,钦定一些关键点 ,算 合法的方案数并带上系数 。
由于 合法等价于 的斯坦纳树合法,因此可以以树上连通块为计数对象。可以得到当且仅当一个树上连通块的叶子(度数 )全都是关键点,且非叶点全都不是关键点的时候会有贡献。至于为什么是这样,在这里有详细的阐释。
将连通块上的所有点依次标号为 。令 “子树”外最大距离为 ,“子树”内最大距离为 。在此定义 的“子树”:对于一个点 ,如果 的路径上经过了除 以外的连通块上的点, 在 的“子树”外,否则在 的“子树”内。
再设 取 进行染色时 个点的染色结果相同。
从上面 的做法可以看出,只可能存在至多一个 满足 ,在 确定后所有的 都确定了。同时有 ,即 可以染到外面的所有点,否则外面的点染色情况不同。总的贡献为 。
于是可以发现对于一个连通块真正产生贡献的其实只有一个 ,即 最大的那个。因为要求了 ,又有 ,所以只有 最大的那个会产生贡献。注意到一个更强的说法是一个连通块内至多只有一个点满足 。
Caution:这里需要注意的是连通块的根如果是关键点,度数必须为 ,否则度数必须不为 ,不然与斯坦纳树的定义相悖。在下面的讨论中,默认这一条件已经考虑。
于是考虑分开计算每个满足 的点 的贡献。
-
对于一个在连通块上不为根的关键点 来说,如果其子树外存在其它关键点,其容斥系数之和为 ,否则为 。这是因为假设其子树外有 个关键点满足到 的路径上没有其它关键点,容斥系数为 。
-
对于一个在连通块上为根的关键点来说,如果其子树内存在其它关键点,其容斥系数之和为 ,否则为 。
-
对于一个在连通块上的非关键点 来说——这是最麻烦的情况,因为其“子树”内外的最大距离是不确定的——先再明确一遍其“子树”的定义:如果一个点到 的路径上经过了除 以外的连通块上的点,这个点在 的“子树”外,否则在 的“子树”内。由于子树内最大距离大于子树外最大距离才会产生贡献, 儿子(这里暂且以 为根,就是将其原树上的父亲也视为儿子)的子树当中最大距离最大的那个一定会被划分到 的“子树”内,而其它的儿子可以随意划分(当然只有在其子树内有关键点的时候才能被划分到 的“子树”外)。然后再枚举“子树”外的最大距离计算贡献。假设“子树”外最大距离为 ,则考察是否存在至少两个子树内最大距离 的儿子有关键点,有的话容斥系数就是 ,否则为 。
注意还要考虑整个连通块不存在 的情况,这时等价于要求整棵树染色,在实现上可以先全部不管,最后再加上 。
总结一下,这个做法的关键在于容斥之后通过计数对象的转变(从集合到树上联通块再到点的贡献),容斥系数得到了极大的(?)简化,变成了便于计算(?)的形式。
更多细节可以看代码,其实很短。
CPP#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
using LL = long long;
constexpr int MAXN = 2e5 + 5;
int n, mx[MAXN], smx[MAXN], out[MAXN], cnt_key[MAXN];
//子树内最大/次大,子树外最大,子树内关键点数
vector<int> G[MAXN];
LL ans;
string S;
void dfs(int u, int fa) {
cnt_key[u] += (S[u] == '1');
for (int v : G[u]) if (v != fa) {
dfs(v, u);
cnt_key[u] += cnt_key[v];
if (mx[v] + 1 > mx[u]) smx[u] = mx[u], mx[u] = mx[v] + 1;
else smx[u] = max(smx[u], mx[v] + 1);
}
}
void Get_out(int u, int fa) {
if (S[u] == '1') ans += max(mx[u], out[u]);
for (int v : G[u]) if (v != fa) {
out[v] = max(out[u], mx[v] + 1 == mx[u] ? smx[u] : mx[u]) + 1;
Get_out(v, u);
}
}
void Get_ans(int u, int fa) {
for (int v : G[u]) if (v != fa) Get_ans(v, u);
if (S[u] == '1') {
if (cnt_key[1] - cnt_key[u]) ans -= max(0, mx[u] - out[u]);//分讨情况:对于一个在连通块上不为根的关键点 u
for (int v : G[u]) if (v != fa && cnt_key[v]) ans -= max(0, (out[v] - 1) - (mx[v] + 1));//分讨情况:对于一个在连通块上为根的关键点 u
} else {
//分讨情况:对于一个在连通块上的非关键点
G[u].emplace_back(0), mx[0] = out[u] - 1, cnt_key[0] = cnt_key[1] - cnt_key[u];//将原树上的父亲视为儿子
sort(G[u].begin(), G[u].end(), [&](int x, int y) { return mx[x] > mx[y]; });
while (!G[u].empty() && !cnt_key[G[u].back()]) G[u].pop_back();
int vismx = 0;//是否已经有比当前儿子子树最大距离更大的儿子被划分到 u 的“子树”内,如果有的话“子树”内最大距离是多少
for (int v : G[u]) if (v != fa) {
if (vismx && cnt_key[v] && v != G[u].back()) ans -= max(0, vismx - (mx[v] + 1));
vismx = max(vismx, mx[v] + 1);
}
}
}
int main() {
IOS;
cin >> n;
for (int u, v, i = 1; i < n; ++i) cin >> u >> v, G[u].emplace_back(v), G[v].emplace_back(u);
cin >> S; S = '#' + S;
dfs(1, 0), Get_out(1, 0), Get_ans(1, 0);
cout << ans + 1 << '\n';
return 0;
}
相关推荐
评论
共 1 条评论,欢迎与作者交流。
正在加载评论...