「HDU 6566」The Hanged Man
题目大意
给定一棵 \(N\) 个点的无根树,对于所有 \(1 \leq k \leq M\) 求出满足以下条件的点集 \(S\) 个数。
- \(\forall u, v \in S\) ,\(u\) , \(v\) 在树上不相邻
- \(\sum_{u \in S} a_u = k\)
- \(\sum_{u \in S} b_u\) 在满足上述两个条件的情况下最小
\(1 \leq N \leq 50\) ,\(1 \leq M \leq 5000\) 。
初步思考
先考虑把暴力打出来,去优化。
对于此题,容易想到树上背包,定义 \(dp_{u, v, 0/1}\) 表示处理完了 \(u\) 这颗子树,\(\sum a_i = v\) ,\(u\) 不选/选的最小值&方案数。转移时合并背包,时间复杂度 \(O(NM^2)\) ,显然不行。
分析暴力,发现合并背包目前没有更好的方法,那么暴力优化的方向行不通,就要考虑更改状态 or 改变 dp 方法。
进一步探究
对于此题而言,笔者没想出其他可行的 dp 定义,所以从改变 dp 方法入手。
对于树形 dp ,除了常见的从下至上逐层 dp 外,还有按 dfs 序 dp 的方法,我们从这个角度去探究。
仿照暴力,令 \(dp_{u, v, 0/1}\) 表示处理到结点 \(u\) ,\(\sum a_i = v\) ,\(u\) 不选/选的最小值&方案数,发现若我们在从 \(i\) 转移到 \(i + 1\) 时他们代表的结点“跨”树时(即非祖先关系)我们不知道 \(i + 1\) 代表的结点 \(v\) 相连的结点的选择情况,所以需要将 \(v\) 附近的结点选择情况表示出来。
发现直接状压的话是 \(2^N\) 级别的,接受不了,继续挖掘其他性质。手玩发现(真的易证)当 \(i\) 代表结点 \(u\) 到 \(i + 1\) 代表结点 \(v\) “跨”树时 \(v\) 一定是 \(\text{LCA}(u, v)\) 的一个儿子。所以我们状压只需状压从 \(u\) 到根节点的选择情况,然后回溯时将子结点的答案回收,加入本结点的 \(dp\) 数组即可。
但链仍然可以把这种做法卡成 \(O(NM2^N)\) ,所以对于状压仍然要继续优化,由于要状压从 \(u\) 到根节点的链,我们可以从寻找优化链查找的数据结构的角度入手,一个是树链剖分,一个是点分治,此题中两者皆可,笔者就介绍下点分治吧。
做法
首先对于点分治后构成的点分树有一个性质:在原树中相邻的点 \(u\) 和点 \(v\) 在点分树上一定是祖孙关系。因为当 \(u\) - \(v\) 这条边拆开时一定是当以 \(u\) 或 \(v\) 作为其子树根节点,且另一个点非其子树重心时,此时无论另一个点怎么旋,一定在该子树中,也就跟根节点呈祖孙关系。若 \(u\) - \(v\) 没拆开显然呈祖孙关系(父子被包含于祖孙关系中)。
所以我们可以直接在点分树上进行状压&dp(因为对于第一个条件被转化为某一个祖孙关系,在状压的管辖范畴中),在向下递归时将当前结点加入背包,重点在于如何回溯,由于我们是按 dfs 序 dp ,所以所有已做过背包的点的贡献要保留下来。保留在哪里呢?对于 \(dp_{u, v, msk}\) 我们在把 \(u\) 回溯掉后发现只有 \(msk\) 发生了变化,所以 \(dp_{u, v, msk}\) 要加进 \(dp_{fa_u, v, msk \oplus (1 << u)}\) ,然后回到 \(fa_u\) 继续 dp 。
因为我们最后做完 dp 后所有点的 dp 值全部传回了根节点,根节点到根节点的选点集合为 \(0/1\) ,所以最终答案为 \(dp_{1, k, 0} + dp_{1, k, 1}\) (“\(+\)” 是重载过的,表示更新) 。
Solution
/*
address:https://vjudge.net/problem/HDU-6566
AC 2025/8/12 14:58
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 105, V = 5005;
vector<int>G[N], adj[N];
int n, m;
int a[N], b[N];
struct node {
int v;
LL c;
node() { v = c = 0; }
node(int _v, LL _c) { v = _v, c = _c; }
node operator += (const node& o) {
if (v == o.v) return node(v, c += o.c);
else if (v < o.v) return node(v = o.v, c = o.c);
else return node(v, c);
}
node operator + (const int& b) { return node(v + b, c); }
}dp[N][V];
bool uni[N][N]; //uni[i][j]: u, v 在原树上是否相邻
int siz[N];
inline int getsiz(int u, int fa = 0) {
int ret = 1;
for (auto v : G[u])
if (v != fa) ret += getsiz(v, u);
return ret;
}
int subn;
inline int find(int u, int fa = 0) {
siz[u] = 1;
bool ok = true;
for (auto v : G[u])
if (v != fa) {
int g = find(v, u);
if (g != -1) return g;
siz[u] += siz[v];
ok &= siz[v] <= (subn >> 1);
}
if (ok && subn - siz[u] <= (subn >> 1)) return u;
return -1;
}//找重心
inline void build(int u, int fa) {
subn = getsiz(u);
u = find(u);
adj[fa].push_back(u);
for (auto v : G[u]) {
G[v].erase(lower_bound(G[v].begin(), G[v].end(), u));
build(v, u);
}
}//建点分树
int stk[N]; //stk[i]表示从根节点到u的第i个结点的编号
inline void dfs(int u, int dep) {
if (!dep) {
dp[0][0] = node(0, 1);
dp[1][a[u]] = node(b[u], 1); //根节点一定要初始化
}
else
for (int msk = 0;msk < 1 << dep;++msk) {
bool ok = true;
for (int i = 0;i < dep;++i)
if ((msk >> i & 1) && uni[stk[i]][u]) {
ok = false;
break;
}
if (!ok) continue; //选了u后不合法就不选
for (int i = m;i >= a[u];--i)
if (dp[msk][i - a[u]].c) dp[msk | 1 << dep][i] += dp[msk][i - a[u]] + b[u];
}
stk[dep] = u;
for (auto v : adj[u]) {
dfs(v, dep + 1);
for (int msk = 0;msk < 1 << dep + 1;++msk) {
bool ok = true;
for (int i = 0;i <= dep;++i)
if ((msk >> i & 1) && uni[stk[i]][v]) {
ok = false;
break;
}
if (!ok) continue;
for (int i = 0;i <= m;++i) {
dp[msk][i] += dp[msk | 1 << dep + 1][i];
dp[msk | 1 << dep + 1][i] = node(0, 0);
}
}//回溯
}
}
int main() {
int T, Case = 0;scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
for (int i = 1;i <= n;++i) scanf("%d%d", &a[i], &b[i]);
for (int i = 1;i < n;++i) {
int u, v;scanf("%d%d", &u, &v);
uni[u][v] = uni[v][u] = true;
G[u].push_back(v);G[v].push_back(u);
}
for (int i = 1;i <= n;++i) sort(G[i].begin(), G[i].end()); //笔者点分树写法比较怪,由于要删边,所以需排序,读者有更好的方法自己写即可,别喷
build(1, 0);
dfs(adj[0][0], 0);
printf("Case %d:\n", ++Case);
for (int i = 1;i <= m;++i) printf("%lld ", (dp[0][i] += dp[1][i]).c);
puts("");
for (int i = 0;i <= m;++i) dp[0][i] = dp[1][i] = node(0, 0);
for (int i = 0;i <= n;++i) {
G[i].clear();
adj[i].clear();
for (int j = 1;j <= n;++j) uni[i][j] = false;
}//清空挺多的,细心点
}
return 0;
}

浙公网安备 33010602011771号