1 条题解
-
0
详细题解
1. 基本观察与预处理
将边长按非递减排序:
。
树有 个节点, 条边,带权直径定义为树上最远两点之间的路径长度之和。引理:任意一棵树中,直径至少为 。
证明:取最长边 (长度 )和次长边 (长度 )。在树中,连接 的某个端点和 的某个端点的唯一路径一定包含这两条边(可能还经过其他边),因此路径长度 。故直径 。因此,若 ,直接输出
No。2. 分类构造
我们尝试用给定的 条边构造一棵树,使其直径恰好为 。
直径是一条简单路径,设其顶点序列为 ,边依次为 ,总长度 。
其余 条边需要挂在直径的某个顶点上(不能挂在边上,因为树中边只能连接节点)。情况一:最长边 属于直径
假设直径包含 。
令直径上的边长集合为 ,满足 且 。
构造方法:- 将 中的边按任意顺序连成一条路径(例如 ),使得路径总长为 。
- 取路径上的某个内部顶点 (例如 ),将剩余的所有边(长度均 )全部接到 上。
需要验证直径不会变大:
设 到路径左端 的距离为 ,到右端 的距离为 ,则 。
对于任意一条挂在 上的边,长度为 ,新叶子到 的距离为 ,到 的距离为 。
由于 且 ,可得 (因为 ),所以 。
而 和 中至少有一个 ,另一个 ,但为了安全我们只需将 选为 (即 边的一个端点)。此时若 是路径的第一条边,则 ,。
因为 ,有 (由 保证),且 。
因此所有新路径长度 ,直径仍为 。结论:若存在一个包含 的子集,其和为 ,则答案为
Yes。
判断方法:对 条边做 0/1 背包(),检查能否凑出 且同时包含 (等价于先用 初始化背包,再对其它边做背包)。情况二:最长边 不属于直径
此时直径完全由其它 条边中的若干条构成,总和为 。
设直径上的顶点序列为 ,边集 ,。
我们需要将 以及所有不在 中的边(包括 )挂到直径上的某个顶点 上,且不能产生长度超过 的路径。设 将直径分为左右两段,长度分别为 和 ()。
挂在 上的任一条边长度为 ,新叶子到 的距离为 ,到 的距离为 。
要求 且 ,即 。
由于最长边 也要挂上去,最严格的条件是 ,即又因为 ,所以必须 。
因此,问题转化为:是否存在一个子集 满足:
- ;
- 可以将 中的边排列成一条路径,并选定一个顶点 ,使得该顶点到路径两端的距离 和 满足 且 。
由于 中的边可以任意排序,顶点 可以对应路径上的任意一个顶点。设 为从某一端到该顶点的距离,则 必须是 的某个子集和(即前缀和), 是补集和。
因此我们需要存在一个子集 ,使得 且 ,。换句话说,我们需将 中的某些边分配到左右两侧(分别代表直径上 左侧和右侧的边),剩余边(包括 )不参与直径。设左侧和为 ,右侧和为 ,则 ,且 。
此外,没有参与直径的边(包括 )全部挂在 上,已经由 保证了安全性。因此,我们只需检查:能否从 中选出一个子集,并将其划分为两个部分(左侧和右侧),使得两部分和分别为 和 ,且 ,,。
3. 二维背包判定
定义 表示可以用若干条边(来自 )构造出左侧和为 、右侧和为 的两个不相交的边集(即每条边至多使用一次)。
初始 。
对于每条长度 ,我们考虑三种选择:- 不使用该边;
- 放在左侧,则 增加 ;
- 放在右侧,则 增加 。
因为 和 都不超过 ,状态数为 ,,状态数约 。
使用二维bitset优化:
bitset<MAXD+1> dp[MAXD+1],其中dp[a]是一个长度为 的bitset,表示在当前左侧和为 时,右侧可能达到的和的集合。
转移时,对于每个 ,倒序更新 (避免同一条边使用多次),并利用移位操作:
新dp[a][b] = dp[a][b] | dp[a-l][b] | dp[a][b-l]。
用bitset实现时,dp[a] |= dp[a-l]表示将右侧和集合并起来,但注意还要处理放在右侧的情况:
实际上可以用临时数组或从大到小更新,更简洁的方式是:for (int i = d; i >= 0; --i) { dp[i] |= dp[i] << l; // 将当前右侧集合整体左移 l,表示在右侧加 l if (i >= l) dp[i] |= dp[i-l]; // 左侧加 l }但需小心
dp[i]的移位操作不应影响本层循环还未处理到的状态。通常采用二维bitset同时更新:for (int i = d; i >= 0; --i) { for (int j = d; j >= 0; --j) { if (i >= l) dp[i][j] = dp[i][j] | dp[i-l][j]; if (j >= l) dp[i][j] = dp[i][j] | dp[i][j-l]; } }用
bitset可以这样写:for (int i = d; i >= l; --i) dp[i] |= dp[i-l]; for (int i = d; i >= 0; --i) dp[i] |= dp[i] << l;注意顺序:先处理左侧加 ,再处理右侧加 。因为右侧加 会改变
dp[i]自身的bitset,所以需要用dp[i]的旧值来更新,应该倒序处理i且使用临时变量?实际上dp[i] |= dp[i] << l相当于在同一状态上扩展右侧,由于我们是倒序i且dp[i]本身是正在更新的,为了避免使用新产生的状态,通常的做法是先备份或按另一种顺序。更安全的方法是对每条边新建一个ndp,但那样开销大。因为 ,我们可以直接对每条边做三重循环( 均倒序),复杂度 为 ,不可接受。因此必须用bitset且仔细安排顺序。标准做法是:
bitset<MAXD+1> dp[MAXD+1]; dp[0][0] = 1; for (int len : lengths_without_max) { for (int i = d; i >= 0; --i) { // 选择放在右侧:dp[i] 整体左移 len dp[i] |= (dp[i] << len); if (i >= len) { // 选择放在左侧:从 dp[i-len] 继承 dp[i] |= dp[i - len]; } } }这里先处理右侧移位,再处理左侧继承,且
i从大到小。注意dp[i] << len可能会使某些位超出 ,需要掩码。
最终检查是否存在 满足 且 ,且dp[a][d-a] == 1。由于 和 对称,我们也可以只枚举 并检查
dp[a][d-a],但需要同时检查dp[d-a][a]。实际上dp[a][b]与dp[b][a]是等价的(因为我们没有区分左右顺序),所以只需检查 即可。4. 算法流程
对于每个测试用例:
- 读入 和数组 。
- 对 排序,令 ,。
- 如果 ,输出
No,结束。 - 检查情况一:用所有 条边做背包,判断是否能凑出 且必须包含 。
可以这样实现:初始化bitset<MAXD+1> f; f[l_n] = 1;然后对其它边做普通背包。最后若f[d] == 1,输出Yes。 - 否则,检查情况二:
- 如果 ,输出
No(因为不可能同时满足 且 )。 - 否则,对 做上述二维背包,判断是否存在 使得
dp[a][d-a] == 1。若存在,输出Yes,否则No。
- 如果 ,输出
5. 复杂度分析
- 排序 ,总 不超过 ,可忽略。
- 背包部分:
情况一:一维bitset,,最大约 次位运算。
情况二:二维bitset,状态 个bitset,每个长度 ,总位数 。对于每条边,需要遍历 从 到 并对bitset进行移位和或操作,每次移位复杂度 。总操作次数约为 。代入 得 次位操作,现代计算机可以在 2 秒内完成(尤其是使用std::bitset优化)。 - 总测试用例 之和满足 总和不超 ,因此最坏情况就是单个测试用例 。
6. 示例验证
以第一个样例:
, 。排序后 ,,。
检查包含 4 的子集和为 10:,存在,输出Yes。第二个样例:
, ,排序 ,,,直接No。第三个样例:
, ,排序 ,,。
检查包含 7 的子集和为 18:尝试 包含一个 7,存在,输出Yes。
代码框架(C++)
#include <bits/stdc++.h> using namespace std; const int MAXD = 2000; void solve() { int n, d; cin >> n >> d; vector<int> l(n); for (int i = 0; i < n; ++i) cin >> l[i]; sort(l.begin(), l.end()); int ln = l.back(); int lprev = l[n-2]; if (ln + lprev > d) { cout << "No\n"; return; } // Case 1: diameter contains ln bitset<MAXD+1> f; f[ln] = 1; for (int i = 0; i < n-1; ++i) { f |= (f << l[i]); } if (f[d]) { cout << "Yes\n"; return; } // Case 2: diameter does NOT contain ln if (d < 2 * ln) { cout << "No\n"; return; } bitset<MAXD+1> dp[MAXD+1]; bitset<MAXD+1> mask; for (int i = 0; i <= d; ++i) mask.set(i); dp[0][0] = 1; for (int i = 0; i < n-1; ++i) { int len = l[i]; for (int a = d; a >= 0; --a) { // put on right side dp[a] |= (dp[a] << len) & mask; if (a >= len) { // put on left side dp[a] |= dp[a - len]; } } } bool ok = false; for (int a = ln; a <= d - ln; ++a) { if (dp[a][d - a]) { ok = true; break; } } cout << (ok ? "Yes\n" : "No\n"); } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) solve(); return 0; }
- 1
信息
- ID
- 7170
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者