Невырожденные треугольники алгоритм
作成日: 2025年1月30日
作成日: 2025年1月30日
Дано множество точек (x, y) на плоскости, как найти максимальное количество невырожденных треугольников, использующих различные точки (каждая точка должна быть использована не более одного раза для какого-то из треугольников). Распиши логику поиска максимально простым языком и приведи реализацию на python
Ниже описана одна из возможных стратегий нахождения максимально возможного числа невырожденных (то есть с ненулевой площадью) треугольников, в которых каждая точка используется не более одного раза. Сразу важное замечание: в общем случае задача сводится к поиску максимального паросочетания в 3‑униформном гиперграфе (где каждая «гиперрёбра» – это тройка точек, образующая ненулевой треугольник). Подобные задачи в общем виде являются NP‑трудными, то есть нет известного эффективного алгоритма, который за разумное время обязательно найдёт оптимальное решение для очень большого числа точек.
Однако на практике (и в учебных целях) обычно либо ограничиваются малыми наборами точек (тогда можно перебрать все комбинации), либо используют жадные или эвристические методы (которые не всегда дают строгое максимум, но часто дают достаточно хорошее решение).
Ниже опишем простой подход:
Проверка, что треугольник не вырожден
Чтобы треугольник из трёх точек , , был невырожденным (имел ненулевую площадь), необходимо, чтобы эти три точки не лежали на одной прямой.
Удобно использовать формулу удвоенной площади треугольника:
Если , треугольник невырожденный, иначе – вырожден (точки коллинеарны).
Общий план жадного алгоритма
Почему алгоритм не всегда оптимален?
Ускорения и варианты
Ниже приведён самый прямолинейный жадный пример на Python, который перебирает тройки в лоб: берёт первую, проверяет, нет ли вырождения, если вырожденная – пробует другие тройки (переставляет одну из точек), и так далее. Как только найдена невырожденная тройка – удаляем её из списка и увеличиваем счётчик.
pythondef area2(p1, p2, p3): """ Функция возвращает удвоенную площадь треугольника, заданного точками p1, p2, p3. Если результат 0, то треугольник вырожден. """ (x1, y1), (x2, y2), (x3, y3) = p1, p2, p3 return x1*(y2 - y3) + x2*(y3 - y1) + x3*(y1 - y2) def is_non_degenerate(p1, p2, p3): """ Возвращает True, если треугольник невырожден (площадь != 0). """ return area2(p1, p2, p3) != 0 def max_triangles(points): """ Жадный алгоритм, возвращающий максимальное количество невырожденных треугольников, где каждая точка используется не более одного раза. points: список кортежей (x, y). """ # Копируем список, чтобы не портить исходные данные pts = points[:] # Можно, например, отсортировать точки, чтобы был детерминированный порядок pts.sort() result = 0 triangles = [] # здесь можем хранить сами тройки, если нужно # Пока точек >= 3, пытаемся найти невырожденную тройку while len(pts) >= 3: found_triangle = False # Перебираем все комбинации из трех разных индексов (i < j < k) # чтобы найти первую подходящую невырожденную. # При большом числе точек это O(n^3) – неэффективно, но просто. # Для оптимизации можно делать другие подходы, например, брать # первую точку и искать пару с ней, которая даст невырожденность. import itertools for combo in itertools.combinations(range(len(pts)), 3): i, j, k = combo p1, p2, p3 = pts[i], pts[j], pts[k] if is_non_degenerate(p1, p2, p3): # Нашли невырожденную тройку – удалим эти точки # Чтобы аккуратно удалить по индексам, которые меняются # (после удаления) – сначала собираем сами точки used_points = [pts[i], pts[j], pts[k]] # Добавим в список найденных треугольников (если хотим вывести потом) triangles.append(used_points) # Удалим из pts по фактическим объектам for up in used_points: pts.remove(up) result += 1 found_triangle = True break # выходим из цикла по combo if not found_triangle: # Не смогли найти ни одной невырожденной тройки – выходим break return result, triangles # Пример использования if __name__ == "__main__": # Пример набора точек points = [ (0, 0), (1, 0), (2, 0), # эти три коллинеарны (0, 1), (2, 2), (10, 10), (1, 1), (2, 1), ] max_count, all_found_triangles = max_triangles(points) print("Максимальное количество невырожденных треугольников:", max_count) print("Сами треугольники:") for t in all_found_triangles: print(t)
area2(p1, p2, p3)
: возвращает удвоенную площадь. Если результат ноль, значит коллинеарны.is_non_degenerate(p1, p2, p3)
: просто проверяет, что area2(...) != 0
.max_triangles(points)
:
Данный алгоритм не всегда даёт строго максимальное число треугольников, однако для многих случаев (особенно если точки «случайно» разбросаны и мало коллинеарных) он выдаст . При наличии большого количества коллинеарных точек можно «подвиснуть» на переборе или получить не лучший набор троек, но с точки зрения простой реализации это зачастую приемлемое решение.
Если требуется абсолютное максимум для небольшого , можно реализовать полный перебор с бэктрекингом: перебирать все возможные способы группировки точек по 3 и выбирать среди них тот, в котором максимальное число невырожденных троек. Но такой подход уже экспоненциальен (вплоть до порядка при грубом рассмотрении), и для больших работать не будет.
Приведённый код демонстрирует один из самых простых жадных подходов.
Измени решение так, чтобы при количестве точек от 3 до 300 алгоритм всегда находил максимальное число невырожденных треугольников
Ниже приведён один из способов гарантированно найти максимальное число непересекающихся (по точкам) невырожденных треугольников из данного набора точек. Важно понимать, что задача в общем случае сводится к поиску максимального паросочетания в 3‑униформном гиперграфе и является NP‑трудной. Однако на практике для (при разумной конфигурации точек и наличии достаточных вычислительных ресурсов) зачастую можно попытаться решить задачу при помощи полного перебора (с агрессивным бранч‑и‑баунд / бэктрекинг) либо использовать Integer Linear Programming (ILP, целочисленное линейное программирование) в связке с хорошим солвером (например, CBC, GLPK, Gurobi, CPLEX, OR-Tools).
Ниже мы покажем подход с ILP‑формулировкой. Он даёт точное (оптимальное) решение. Но нужно иметь в виду:
Тем не менее, с точки зрения корректности и теоретической возможности найти максимум, ILP‑подход надёжен. Для небольших (до сотен) наборов точек, особенно если их тройки формируют не слишком плотный гиперграф (например, многие тройки вырожденные из-за коллинеарности), такой метод может сработать.
Пусть у нас есть точек, перенумерованных индексами .
Предварительно сформируем список всех невырожденных троек:
Проверку «невырожден» делаем, используя ненулевую площадь (коллинеарность проверяется через ).
Для каждой такой тройки вводим бинарную переменную .
Целевая функция:
То есть мы стремимся взять как можно больше невырожденных троек.
Ограничения, чтобы каждая точка не участвовала более чем в одном треугольнике:
Иными словами, суммарно по всем тройкам, которые включают точку , переменные не должны давать сумму больше 1.
Так мы получаем классическую постановку Maximum 3-Matching в виде задачи целочисленного линейного программирования.
pulp
)Ниже приведён пример, как можно оформить решение через PuLP. Если у вас установлены другие солверы (GLPK, CBC, Gurobi, CPLEX, OR-Tools и др.), код может немного различаться, но общая идея сохраняется.
python!pip install pulp import pulp def area2(p1, p2, p3): """Удвоенная площадь треугольника из трёх точек.""" (x1, y1), (x2, y2), (x3, y3) = p1, p2, p3 return x1*(y2 - y3) + x2*(y3 - y1) + x3*(y1 - y2) def solve_max_triangles(points): """ Возвращает (max_count, selected_triangles), где: max_count — максимальное число невырожденных треугольников, каждая точка используется не более одного раза. selected_triangles — список самиx треугольников (каждый треугольник — это кортеж индексов (i,j,k), i<j<k). """ n = len(points) if n < 3: return 0, [] # Шаг 1. Соберём все невырожденные тройки (i<j<k) valid_triples = [] for i in range(n-2): for j in range(i+1, n-1): for k in range(j+1, n): if area2(points[i], points[j], points[k]) != 0: valid_triples.append((i, j, k)) # Если тройки не найдены, то ответ 0 if not valid_triples: return 0, [] # Шаг 2. Создаём задачу линейного программирования problem = pulp.LpProblem("MaxNonDegenerateTriangles", pulp.LpMaximize) # Шаг 3. Для каждой тройки вводим переменную (0 или 1) # Используем словарь (i,j,k) -> pulp.LpVariable x = {} for (i, j, k) in valid_triples: var_name = f"x_{i}_{j}_{k}" x[(i,j,k)] = pulp.LpVariable(var_name, cat=pulp.LpBinary) # Шаг 4. Целевая функция: максимизировать сумму x_{i,j,k} problem += pulp.lpSum([x[t] for t in valid_triples]), "Objective" # Шаг 5. Ограничения: каждая точка может участвовать не более чем в одной тройке # Для каждой точки v суммируем все x_{i,j,k}, где v in (i,j,k) for v in range(n): triples_with_v = [] for (i, j, k) in valid_triples: if v in (i, j, k): triples_with_v.append(x[(i,j,k)]) if triples_with_v: problem += pulp.lpSum(triples_with_v) <= 1, f"Point_{v}_constraint" # Шаг 6. Запускаем решение solver = pulp.PULP_CBC_CMD(msg=False) # встроенный CBC-солвер problem.solve(solver) # Шаг 7. Считываем решение selected_triangles = [] for (i, j, k) in valid_triples: if pulp.value(x[(i,j,k)]) > 0.5: # т.к. это бинарные переменные selected_triangles.append((i, j, k)) max_count = len(selected_triangles) return max_count, selected_triangles # Пример использования if __name__ == "__main__": # Набор точек (можно поменять на свой) points_example = [ (0,0), (1,0), (2,0), (0,1), (1,1), (2,2), (10,10) ] max_cnt, triangles = solve_max_triangles(points_example) print("Максимальное число треугольников:", max_cnt) print("Сами треугольники (по индексам):", triangles)
Генерация всех невырожденных троек
Перебираем все комбинации с . Для каждой проверяем, не лежат ли точки на одной прямой (через area2(...) != 0
).
valid_triples
.Построение ILP
Ограничения «каждая точка не более чем в одном треугольнике»
Решение
Запускаем встроенный в PuLP (по умолчанию) солвер CBC. При наличии других (например, Gurobi, CPLEX, GLPK) можно подключать их (если установлены) для ускорения.
Восстановление ответа
Для можно пытаться делать итеративный перебор всех способов «разбить» точки на тройки (или отказаться от некоторых точек). Но даже это растёт экспоненциально и к быстро становится непрактичным.
pulp
демонстрирует, как это сделать:
Такой подход при достаточных ресурсах всегда находит максимум для .