把问题拆成沿三条边分别递归找邻居:父内能直接找到就替换末位,否则沿同向边递归向父三角形外查。
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;
}这题最容易被几何图形吓到,但真正有用的信息都已经写在编号里了。
对于任意一个小三角形,我们只需要分别找出:
- 斜边外侧邻居
- 水平边外侧邻居
- 竖直边外侧邻居
把这三种结果去重后输出即可。
关键观察是:看编号最后一位就能判断,某条边的邻居是:
- 仍然在当前父三角形内部;
- 还是已经跑到父三角形外部,需要继续沿同向边递归向上找。
如果最后一位是 4,说明当前是父三角形中央的倒三角,
它三条边外侧邻居都在父内部,可以直接确定。
如果最后一位是 1/2/3,则恰好只有一条边能在父内部直接连到中央的 4,
其余两条边都落在父三角形边界上,
因此外侧邻居只能来自父三角形沿同向边外侧的邻居。
所以写一个递归函数:
get_neighbor(path, side)
每次:
- 能在父内部直接找到,就改末位返回;
- 否则删掉最后一位,递归去父亲同一条边继续找。
这样就能在
代码
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。
每次找某条边的邻居,最坏递归
总结
这题的关键不是几何作图,而是把几何关系翻译成编号递归关系:
- 父内能找就直接找
- 父内找不到就向上递归
一旦抓住这个结构,代码会非常短。
