[CTSC2001] 排序二叉树

GitHub跳转原题关系图返回列表

先把四个面的单位三角形按立体共边关系建成度数不超过 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 型记忆化搜索。

设当前节点是 uu,父节点是 parentparent,再给一个边界 boundbound

parentparentboundbound 一起就能确定当前整棵子树允许出现的值域:

  • 如果 u<parentu < parent,说明当前子树在父亲的左边,值域是某个左区间
  • 如果 u>parentu > parent,说明当前子树在父亲的右边,值域是某个右区间

这样转移时只需要看 uu 的相邻点:

  • 小于 uu 且落在合法区间里的,才可能当左儿子
  • 大于 uu 且落在合法区间里的,才可能当右儿子

因为固定父边后最多只剩两个方向,所以左右子树可以独立取最优,然后加上当前节点。

于是做法就是:

  1. 先把所有真实共边关系建图
  2. dp(u,bound,parent)dp(u, bound, parent) 表示一个带父边和区间边界的最优子树
  3. 枚举每个点作为整棵 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;
}

复杂度

总点数 tot=4n2tot = 4n^2

状态数约为 O(tot2)O(tot^2),每个状态只看常数个邻居。

所以时间复杂度:

O(tot2)O(tot^2)

空间复杂度:

O(tot2)O(tot^2)

总结

这题表面上是立体几何,真正的算法核心其实是:

  • 先把几何问题翻译成小度数图
  • 再利用 BST 的值域约束做区间型记忆化搜索

图一旦建对,后面的 DP 结构是很规整的。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析