[CQOI2007] 三角形

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

把问题拆成沿三条边分别递归找邻居:父内能直接找到就替换末位,否则沿同向边递归向父三角形外查。

OJ: luogu

题目 ID: P4536

难度:普及+/提高

标签:递归模拟图形思维

日期: 2026-06-20 13:35

题意

给定 Sierpinski 三角形中的一个小三角形编号 T..., 要求输出所有“靠在它边上”的三角形编号。

这里“靠在边上”指的是:

  • 对方不包含当前三角形
  • 当前三角形的一整条边,是对方某条边的一部分

思路

先看一个可以直接验证想法的朴素解:

cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

struct Point {
    long long x, y;
};

struct Triangle {
    string code;
    Point a, b, c;
};

vector<Triangle> all_triangles;
string target_code;
int max_depth;

Point mid(Point p, Point q) {
    return {(p.x + q.x) / 2, (p.y + q.y) / 2};
}

void add_triangle(const string &code, Point a, Point b, Point c) {
    all_triangles.push_back({code, a, b, c});
}

void build_all(const string &prefix, Point a, Point b, Point c, int depth) {
    Point ab = mid(a, b);
    Point ac = mid(a, c);
    Point bc = mid(b, c);

    // 1: 顶部,2: 左下,3: 右下,4: 中间倒三角
    add_triangle(prefix + '1', ac, bc, c);
    add_triangle(prefix + '2', a, ab, ac);
    add_triangle(prefix + '3', ab, b, bc);
    add_triangle(prefix + '4', ab, ac, bc);

    if (depth == max_depth) return;

    build_all(prefix + '1', ac, bc, c, depth + 1);
    build_all(prefix + '2', a, ab, ac, depth + 1);
    build_all(prefix + '3', ab, b, bc, depth + 1);
}

bool is_prefix_noncontain_hole(const string &small, const string &big) {
    // 判断 small 是否是 big 的“包含性前缀”。
    if ((int)small.size() >= (int)big.size()) return false;
    if (big.substr(0, small.size()) != small) return false;
    if (!small.empty() && small.back() == '4') return false;
    return true;
}

bool same_point(Point p, Point q) {
    return p.x == q.x && p.y == q.y;
}

bool on_segment(Point p, Point a, Point b) {
    long long cross = (p.x - a.x) * (b.y - a.y) - (p.y - a.y) * (b.x - a.x);
    if (cross != 0) return false;
    return min(a.x, b.x) <= p.x && p.x <= max(a.x, b.x) &&
           min(a.y, b.y) <= p.y && p.y <= max(a.y, b.y);
}

vector<pair<Point, Point>> edges(const Triangle &t) {
    vector<pair<Point, Point>> e;
    e.push_back({t.a, t.b});
    e.push_back({t.a, t.c});
    e.push_back({t.b, t.c});
    return e;
}

bool adjacent(const Triangle &A, const Triangle &B) {
    if (A.code == B.code) return false;
    if (is_prefix_noncontain_hole(B.code, A.code)) return false;

    vector<pair<Point, Point>> ea = edges(A);
    vector<pair<Point, Point>> eb = edges(B);

    for (int i = 0; i < (int)ea.size(); i++) {
        Point p = ea[i].first;
        Point q = ea[i].second;
        for (int j = 0; j < (int)eb.size(); j++) {
            Point u = eb[j].first;
            Point v = eb[j].second;
            if (on_segment(p, u, v) && on_segment(q, u, v)) {
                return true;
            }
        }
    }
    return false;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string full_name;
    cin >> full_name;
    target_code = full_name.substr(1);
    max_depth = (int)target_code.size();

    long long scale = 1LL << max_depth;
    Point a = {0, 0};
    Point b = {scale, 0};
    Point c = {0, scale};
    build_all("", a, b, c, 1);

    Triangle target;
    for (int i = 0; i < (int)all_triangles.size(); i++) {
        if (all_triangles[i].code == target_code) {
            target = all_triangles[i];
            break;
        }
    }

    vector<string> ans;
    for (int i = 0; i < (int)all_triangles.size(); i++) {
        if (adjacent(target, all_triangles[i])) {
            ans.push_back(all_triangles[i].code);
        }
    }

    sort(ans.begin(), ans.end());
    ans.erase(unique(ans.begin(), ans.end()), ans.end());

    for (int i = 0; i < (int)ans.size(); i++) {
        cout << 'T' << ans[i] << '\n';
    }

    return 0;
}

这题最容易被几何图形吓到,但真正有用的信息都已经写在编号里了。

对于任意一个小三角形,我们只需要分别找出:

  • 斜边外侧邻居
  • 水平边外侧邻居
  • 竖直边外侧邻居

把这三种结果去重后输出即可。

关键观察是:看编号最后一位就能判断,某条边的邻居是:

  1. 仍然在当前父三角形内部;
  2. 还是已经跑到父三角形外部,需要继续沿同向边递归向上找。

如果最后一位是 4,说明当前是父三角形中央的倒三角, 它三条边外侧邻居都在父内部,可以直接确定。

如果最后一位是 1/2/3,则恰好只有一条边能在父内部直接连到中央的 4, 其余两条边都落在父三角形边界上, 因此外侧邻居只能来自父三角形沿同向边外侧的邻居。

所以写一个递归函数:

  • get_neighbor(path, side)

每次:

  • 能在父内部直接找到,就改末位返回;
  • 否则删掉最后一位,递归去父亲同一条边继续找。

这样就能在 O(n)O(n) 时间内得到三条边的全部邻居。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

string path_code;

// side:
// 'D' -> 斜边(在右三角仿射模型里,对应对角线方向)
// 'H' -> 水平边
// 'V' -> 竖直边
string get_neighbor(const string &path, char side) {
    if (path.empty()) return "";

    int len = (int)path.size();
    char last = path[len - 1];
    string prefix = path.substr(0, len - 1);

    if (last == '4') {
        if (side == 'D') return prefix + '2';
        if (side == 'H') return prefix + '1';
        return prefix + '3';
    }

    if (last == '1') {
        if (side == 'H') return prefix + '4';
        return get_neighbor(prefix, side);
    }

    if (last == '2') {
        if (side == 'D') return prefix + '4';
        return get_neighbor(prefix, side);
    }

    // last == '3'
    if (side == 'V') return prefix + '4';
    return get_neighbor(prefix, side);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    string full_name;
    cin >> full_name;
    path_code = full_name.substr(1);

    vector<string> answer;
    answer.push_back(get_neighbor(path_code, 'D'));
    answer.push_back(get_neighbor(path_code, 'H'));
    answer.push_back(get_neighbor(path_code, 'V'));

    sort(answer.begin(), answer.end());
    answer.erase(unique(answer.begin(), answer.end()), answer.end());

    for (int i = 0; i < (int)answer.size(); i++) {
        if (answer[i].empty()) continue;
        cout << 'T' << answer[i] << '\n';
    }

    return 0;
}

复杂度

设编号长度为 n

每次找某条边的邻居,最坏递归 O(n)O(n); 一共只找三次,所以总复杂度是 O(n)O(n)

总结

这题的关键不是几何作图,而是把几何关系翻译成编号递归关系:

  • 父内能找就直接找
  • 父内找不到就向上递归

一旦抓住这个结构,代码会非常短。