先把四个面的单位三角形按立体共边关系建成度数不超过 3 的图,再做带父边与值域边界的记忆化搜索,分别取当前节点左右子树的最优解。
OJ: luogu
题目 ID: P1267
难度:提高+/省选-
标签:树形dp图论递归构造
日期: 2026-06-21 00:07
题意
把正三棱锥四个面上的所有单位三角形看成点。
如果两个单位三角形在真实立体结构中共边,就认为它们相邻。
现在每个点上放了一个互不相同的数,要求从中选出尽量多的点,组成一棵二叉搜索树,并满足:
- 父子节点必须相邻
- 左子树所有值小于根
- 右子树所有值大于根
输出最大节点数。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAX_FACE = 5;
const int MAXN = 8;
const int MAXPOS = 20;
const int MAXV = 150;
int n;
int tot;
int mp[MAX_FACE][MAXN][MAXPOS];
int deg_cnt[MAXV];
int g[MAXV][4];
bool has_edge[MAXV][MAXV];
unordered_map<unsigned long long, int> memo;
void add_edge(int u, int v) {
if (has_edge[u][v]) {
return;
}
has_edge[u][v] = true;
has_edge[v][u] = true;
g[u][deg_cnt[u]++] = v;
g[v][deg_cnt[v]++] = u;
}
void build_graph() {
for (int face = 1; face <= 4; face++) {
for (int row = 1; row <= n; row++) {
for (int col = 1; col <= row * 2 - 1; col++) {
cin >> mp[face][row][col];
}
}
}
for (int face = 1; face <= 4; face++) {
for (int row = 2; row < n; row++) {
for (int col = 2; col < row * 2 - 1; col++) {
int u = mp[face][row][col];
add_edge(u, mp[face][row][col - 1]);
add_edge(u, mp[face][row][col + 1]);
if (col & 1) {
add_edge(u, mp[face][row + 1][col + 1]);
} else {
add_edge(u, mp[face][row - 1][col - 1]);
}
}
}
}
if (n >= 2) {
for (int face = 1; face <= 4; face++) {
for (int col = 2; col <= n * 2 - 1; col += 2) {
int u = mp[face][n][col];
add_edge(u, mp[face][n][col - 1]);
add_edge(u, mp[face][n][col + 1]);
add_edge(u, mp[face][n - 1][col - 1]);
}
}
}
for (int col = 1; col <= n * 2 - 1; col++) {
add_edge(mp[1][n][col], mp[4][n - col / 2][1]);
add_edge(mp[2][n][col], mp[4][col / 2 + 1][(col / 2 + 1) * 2 - 1]);
add_edge(mp[3][n][col], mp[4][n][n * 2 - col]);
}
for (int row = 1; row <= n; row++) {
add_edge(mp[1][row][1], mp[3][row][row * 2 - 1]);
add_edge(mp[2][row][1], mp[1][row][row * 2 - 1]);
add_edge(mp[3][row][1], mp[2][row][row * 2 - 1]);
}
}
unsigned long long encode_state(int u, int parent, int low, int high) {
unsigned long long key = 0;
key = (unsigned long long) u;
key = key * 256ULL + (unsigned long long) parent;
key = key * 256ULL + (unsigned long long) low;
key = key * 256ULL + (unsigned long long) high;
return key;
}
// brute.cpp:直接按 BST 定义搜索。
// 状态显式带上下界,写法更直观,但只适合小数据。
int dfs(int u, int parent, int low, int high) {
unsigned long long key = encode_state(u, parent, low, high);
unordered_map<unsigned long long, int>::iterator it = memo.find(key);
if (it != memo.end()) {
return it->second;
}
int best_left = 0;
int best_right = 0;
for (int i = 0; i < deg_cnt[u]; i++) {
int v = g[u][i];
if (v == parent) {
continue;
}
if (v < low || v > high) {
continue;
}
if (v < u) {
best_left = max(best_left, dfs(v, u, low, u - 1));
} else if (v > u) {
best_right = max(best_right, dfs(v, u, u + 1, high));
}
}
int res = best_left + best_right + 1;
memo[key] = res;
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
tot = 4 * n * n;
build_graph();
int ans = 0;
for (int root = 1; root <= tot; root++) {
ans = max(ans, dfs(root, 0, 1, tot));
}
cout << ans << '\n';
return 0;
}这题最麻烦的其实是第一步:把四个面折叠后的真实邻接关系建成图。
图建好以后,问题就清楚很多了:
- 每个单位三角形只有 3 个相邻点
- 如果当前点已经确定了父亲,那么最多只剩 2 个方向可以继续选儿子
这非常适合做 BST 型记忆化搜索。
设当前节点是
- 如果
,说明当前子树在父亲的左边,值域是某个左区间 - 如果
,说明当前子树在父亲的右边,值域是某个右区间
这样转移时只需要看
- 小于
且落在合法区间里的,才可能当左儿子 - 大于
且落在合法区间里的,才可能当右儿子
因为固定父边后最多只剩两个方向,所以左右子树可以独立取最优,然后加上当前节点。
于是做法就是:
- 先把所有真实共边关系建图
- 写
表示一个带父边和区间边界的最优子树 - 枚举每个点作为整棵 BST 的根,取最大值
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAX_FACE = 5;
const int MAXN = 20;
const int MAXPOS = 40;
const int MAXV = 1305;
int n;
int tot;
int mp[MAX_FACE][MAXN][MAXPOS];
int deg_cnt[MAXV];
int g[MAXV][4];
bool has_edge[MAXV][MAXV];
// memo[u][pi][bound]:
// 当前根为 u,父亲是 g[u][pi],另一个边界值是 bound 时,
// 能形成的最大 BST 节点数。
int memo[MAXV][4][MAXV];
void add_edge(int u, int v) {
if (has_edge[u][v]) {
return;
}
has_edge[u][v] = true;
has_edge[v][u] = true;
g[u][deg_cnt[u]++] = v;
g[v][deg_cnt[v]++] = u;
}
int find_parent_index(int u, int parent) {
for (int i = 0; i < deg_cnt[u]; i++) {
if (g[u][i] == parent) {
return i;
}
}
return -1;
}
int dp(int u, int bound, int parent) {
int pi = find_parent_index(u, parent);
if (memo[u][pi][bound] != 0) {
return memo[u][pi][bound];
}
int left_limit, right_limit;
if (bound < parent) {
left_limit = bound;
right_limit = parent - 1;
} else {
left_limit = parent + 1;
right_limit = bound;
}
int best_left = 0;
int best_right = 0;
// 父亲已确定后,剩下的最多两个相邻点里,
// 只需分别挑一个最优左儿子和一个最优右儿子。
for (int i = 0; i < deg_cnt[u]; i++) {
int v = g[u][i];
if (v == parent) {
continue;
}
if (v < left_limit || v > right_limit) {
continue;
}
if (v < u) {
best_left = max(best_left, dp(v, left_limit, u));
} else {
best_right = max(best_right, dp(v, right_limit, u));
}
}
memo[u][pi][bound] = best_left + best_right + 1;
return memo[u][pi][bound];
}
void build_graph() {
// 每个面按行存:第 i 行有 2*i-1 个单位三角形。
for (int face = 1; face <= 4; face++) {
for (int row = 1; row <= n; row++) {
for (int col = 1; col <= row * 2 - 1; col++) {
cin >> mp[face][row][col];
}
}
}
// 面内相邻。
for (int face = 1; face <= 4; face++) {
for (int row = 2; row < n; row++) {
for (int col = 2; col < row * 2 - 1; col++) {
int u = mp[face][row][col];
add_edge(u, mp[face][row][col - 1]);
add_edge(u, mp[face][row][col + 1]);
if (col & 1) {
add_edge(u, mp[face][row + 1][col + 1]);
} else {
add_edge(u, mp[face][row - 1][col - 1]);
}
}
}
}
// 最后一行的内部相邻。
if (n >= 2) {
for (int face = 1; face <= 4; face++) {
for (int col = 2; col <= n * 2 - 1; col += 2) {
int u = mp[face][n][col];
add_edge(u, mp[face][n][col - 1]);
add_edge(u, mp[face][n][col + 1]);
add_edge(u, mp[face][n - 1][col - 1]);
}
}
}
// 三个侧面和底面的公共边。
for (int col = 1; col <= n * 2 - 1; col++) {
add_edge(mp[1][n][col], mp[4][n - col / 2][1]);
add_edge(mp[2][n][col], mp[4][col / 2 + 1][(col / 2 + 1) * 2 - 1]);
add_edge(mp[3][n][col], mp[4][n][n * 2 - col]);
}
// 三个侧面的公共棱。
for (int row = 1; row <= n; row++) {
add_edge(mp[1][row][1], mp[3][row][row * 2 - 1]);
add_edge(mp[2][row][1], mp[1][row][row * 2 - 1]);
add_edge(mp[3][row][1], mp[2][row][row * 2 - 1]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
tot = 4 * n * n;
build_graph();
int ans = 0;
// 枚举根节点。根最多选一个左儿子、一个右儿子。
for (int root = 1; root <= tot; root++) {
int best_left = 0;
int best_right = 0;
for (int i = 0; i < deg_cnt[root]; i++) {
int v = g[root][i];
if (v < root) {
best_left = max(best_left, dp(v, 1, root));
} else {
best_right = max(best_right, dp(v, tot, root));
}
}
ans = max(ans, best_left + best_right + 1);
}
cout << ans << '\n';
return 0;
}复杂度
总点数
状态数约为
所以时间复杂度:
空间复杂度:
总结
这题表面上是立体几何,真正的算法核心其实是:
- 先把几何问题翻译成小度数图
- 再利用 BST 的值域约束做区间型记忆化搜索
图一旦建对,后面的 DP 结构是很规整的。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。



