题解:P16310 [ICPC 2023 Jinan R] 开灯 2

很有意思的题。

根据题述条件,不难看出一个合法的点亮灯泡的方案相当于一个极大独立集。也就是说,我们要构造一个 \(m\) 条边、最大度数为 \(d\) 的无向连通图,使得图中的极大独立集个数最大化。

我们指出,使得极大独立集个数最大化的图,一定可以取一棵树。

证明

使用调整法。

任取一个极大独立集个数最大的无向连通图 \(G\),取环上一条边 \((u,v)\) 将其删去,新增一个点 \(x\),添加边 \((u,x)\),得到新图 \(G'\)

显然 \(G'\) 依旧是连通图。而边数保持不变,\(deg_u\) 不变,\(deg_v\) 减少 \(1\)\(deg_x=1\),因此最大度数不增。

任取 \(G\) 中的一个极大独立集 \(S\)。分类讨论:

  • \(u\notin S\):取 \(S'=S\cup\{x\}\)。因为 \(x\) 只与 \(u\) 相连,所以 \(S'\)\(G'\) 的独立集。进一步验证极大性:\(u\) 有已选邻点 \(x\);对于 \(v\),若 \(v\notin S\),那么 \(v\)\(G\) 中必然有非 \(u\) 的已选邻点。因此 \(S'\) 也是 \(G'\) 的极大独立集。
  • \(u\in S\)\(v\) 有非 \(u\) 的已选邻点:取 \(S'=S\)。显然 \(S'\)\(G'\) 的独立集。进一步验证极大性:\(x\) 有已选邻点 \(u\)\(v\) 有非 \(u\) 的已选邻点。因此 \(S'\) 也是 \(G'\) 的极大独立集。
  • \(u\in S\)\(v\) 只有 \(u\) 这个已选邻点:取 \(S'=S\cup\{v\}\)。因为 \((u,v)\) 被删除,所以在 \(G'\) 中,选择 \(v\) 满足独立性。进一步验证极大性:\(x\) 有已选邻点 \(u\)。因此 \(S'\)\(G'\) 的极大独立集。

于是调整后,\(G'\) 的极大独立集大小不减。不断将 \(G\) 调整成一棵树即可。\(\Box\)

问题转化为,求 \(m+1\) 个点、最大度数为 \(d\) 的树中,极大独立集数量的最大值。

不妨先考虑对于一棵树,我们怎么求出极大独立集数量。

树形 DP,对于 \(sub_u\),设 \(f_{u,0/1/2}\) 表示满足条件的方案数,其中三种状态的定义分别为:

  • \(f_{u,0}\)\(u\) 被选中。
  • \(f_{u,1}\)\(u\) 未被选中,但存在已选的儿子节点。
  • \(f_{u,2}\)\(u\) 未被选中,没有已选的儿子节点,也就是父亲节点必须被选中。

叶子节点的状态为 \(f_{u,0}=1,f_{u,1}=0,f_{u,2}=1\)

考虑加入一个儿子节点 \(v\) 时如何合并:

\[\begin{align*} f'_{u,0}&=f_{u,0}(f_{v,1}+f_{v,2})\\ f'_{u,1}&=f_{u,1}(f_{v,0}+f_{v,1})+f_{u,2}f_{v,0}\\ f'_{u,2}&=f_{u,2}f_{v,1} \end{align*} \]

最终答案即为 \(f_{rt,0}+f_{rt,1}\)

我们并不知道树的具体结构,因此要在对树结构 DP 的同时,加入上面的 DP 状态。

考虑一些很暴力的想法:设 \(h_{e,p}\) 表示考虑一棵 \(e\) 条边的有根树,根节点至多有 \(p\) 个儿子,所有点的度数不超过 \(d\),此时所有可能的三元组 \((f_{u,0},f_{u,1},f_{u,2})\) 构成的集合。

设临时状态 \(dp_{i,j}\) 表示当前根节点连了 \(i\) 个儿子,子树内总共有 \(j\) 条边,此时所有可能的三元组构成的集合。刷表转移,枚举新的儿子中的边数 \(k\),满足 \(j+k+1\leq e\),然后将 \(dp_{i,j},h_{k,d-1}\) 中的三元组两两合并后加入到 \(dp_{i+1,j+k+1}\) 中。注意这里三元组的合并方式,就是前面的 DP 中的合并方式。

得到 \(dp_{i,j}\) 后,我们有

\[h_{e,p}=\bigcup_{i=0}^{p}dp_{i,e} \]

最终答案即为

\[\max_{(f_0,f_1,f_2)\in h_{m,d}}\{f_0+f_1\} \]

不难看出,实际上我们只需要对 \(h_{e,d-1/d}\) 做 DP。

但是这样三元组个数太多了,大概率过不了。

考虑支配思想,观察到对于两个三元组 \((f_0,f_1,f_2),(f_0',f_1',f_2')\),若 \(f_0\geq f_0'\land f_1\geq f_1'\land f_2\geq f_2'\),则 \((f_0,f_1,f_2)\) 支配 \((f_0',f_1',f_2')\),可以删去后者。在转移过程中进行剪枝,剔除无用状态即可。

还需要构造方案。对于每个状态 \((f_0,f_1,f_2)\),我们额外记录儿子节点状态 \((k,id)\) 构成的集合,其中 \(k\) 表示儿子子树中的边数,\(id\) 表示这棵子树对应 \(g_{k,d-1}\) 中的第 \(id\) 个状态。递归构造即可。

实际实现时可以按 \(d\) 分组,每次固定 \(d\) 做 DP,然后回答所有对应的测试数据。

粗略地分析时间复杂度。设剪枝后集合的最大大小为 \(S\)。固定 \(e\),剪枝前集合的大小最坏可以达到 \(\mathcal{O}(eS^2)\),因此这部分剪枝的复杂度为 \(\mathcal{O}(\min(d,e)e^3S^4)\),DP 的转移复杂度则为 \(\mathcal{O}(\min(d,e)e^3S^2)\)。对于单个 \(d\),总的时间复杂度为 \(\mathcal{O}(dm^4S^4)\)。单次查询的时间复杂度为 \(\mathcal{O}(S+m)\)。可以验证 \(S=125\),且实际实现完全达不到上界,可以通过。

代码挺好写的。

主要代码
struct State {
	int a, b, c;
	vector<pii> son;
};
vector<State> f[MAXM], g[MAXM], dp[MAXM][MAXM];

vector<State> comp(vector<State> vec) {
	sort(vec.begin(), vec.end(), [&](const State &lhs, const State &rhs) {
		if (lhs.a != rhs.a) return lhs.a < rhs.a;
		else if (lhs.b != rhs.b) return lhs.b < rhs.b;
		else return lhs.c < rhs.c;
	});
	vec.erase(unique(vec.begin(), vec.end(), [&](const State &lhs, const State &rhs) {
		return lhs.a == rhs.a && lhs.b == rhs.b && lhs.c == rhs.c;
	}), vec.end());
	int n = vec.size();
	vector<bool> del(n);
	for (int i = 0; i < n; ++i) {
		for (int j = i + 1; j < n; ++j) {
			if (vec[i].b <= vec[j].b && vec[i].c <= vec[j].c) {
				del[i] = true;
                break;
            }
        }
    }
	vector<State> res;
	for (int i = 0; i < n; ++i)
		if (!del[i])
			res.emplace_back(move(vec[i]));
	return res;
}

void solve(int d) {
	// f: d, g: d - 1
	for (int i = 0; i < MAXM; ++i) {
		f[i].clear();
		g[i].clear();
	}
	f[0].push_back({1, 0, 1});
	g[0].push_back({1, 0, 1});
	for (int e = 1; e < MAXM; ++e) {
		int mxDeg = min(d, e), mxDeg2 = min(d - 1, e);
		for (int i = 0; i <= mxDeg; ++i)
			for (int j = 0; j <= e; ++j)
				dp[i][j].clear();
		dp[0][0].push_back({1, 0, 1});
		for (int i = 0; i < mxDeg; ++i) {
			for (int j = 0; j <= e; ++j) {
				if (dp[i][j].empty()) continue;
				for (int k = 0; k <= e - j - 1; ++k) {
					const auto &vec = g[k];
					for (const auto &cur : dp[i][j]) {
						for (int id = 0; id < vec.size(); ++id) {
							const auto &[a, b, c, _] = vec[id];
							State nxt = {
								cur.a * (b + c),
								cur.b * (a + b) + cur.c * a,
								cur.c * b
							};
							nxt.son = cur.son;
							nxt.son.emplace_back(k, id);
							dp[i + 1][j + k + 1].emplace_back(move(nxt));
						}
					}
				}
			}
			for (int j = 0; j <= e; ++j)
				if (!dp[i + 1][j].empty())
					dp[i + 1][j] = comp(move(dp[i + 1][j]));
		}
		vector<State> cur;
		for (int i = 0; i <= mxDeg; ++i) {
			for (const auto &st : dp[i][e]) cur.emplace_back(st);
			if (!cur.empty()) cur = comp(move(cur));
			if (i == mxDeg2) g[e] = cur;
			if (i == mxDeg) f[e] = cur;
		}
	}
}

pair<int, vector<pii>> query(int m) {
	pii mx = {0, 0};
	for (int i = 0; i < f[m].size(); ++i)
		chkMax(mx, pii{f[m][i].a + f[m][i].b, i});
	vector<pii> T;
	int cnt = 1;

	auto dfs = [&](auto &&self, int u, int faU, const State &cur) -> void {
		if (faU) T.emplace_back(faU, u);
		for (auto [e, id] : cur.son) self(self, ++cnt, u, g[e][id]);
	};

	dfs(dfs, 1, 0, f[m][mx.second]);
	return {mx.first, move(T)};
}
posted @ 2026-06-20 13:45  P2441M  阅读(8)  评论(0)    收藏  举报