![]() |
![]() |
|||||||||||||||
![]() | ||||||||||||||||
![]() |
![]() АЛГОРИТМЫ Новости Рассылка новостей Форум AlgoPascal Редактор блок-схем Статьи О сайте Контакты |
![]() |
![]() Булева алгебра выпуклых полигоновАвтор: Павлов Дмитрий
ВведениеПостановка задачи: Построение булевой алгебры над различными типами полигонов и ее реализация. Определение 1: Полигоном называется конечная упорядоченная последовательность точек плоскости {(xn, yn )} n=0,1,2,...N. При этом сами точки (xn, yn ) называются вершинами полигона. Ребрами полигона или границей, называются вектора (xn- xn-1, уn- уn-1 ) n=1,...,N. Точка называется внутренней точкой полигона, если почти все лучи выходящие из этой точки пересекают границу нечетное число раз. Это самое общее определение полигона. Однако на практике приходится сталкиваться с частными случаями полигонов. Работа посвящена, как раз рассмотрению этих важных частных случаев. Определение 2: Полигон называется простым, если множество точек пересечения ребер полигона между собой пусто.
Здесь алгоритм нахождения точек пресечения требует количество операций порядка O(LM), где L число вершин в первом полигоне, M во втором полигоне. Вообще сложность операции нахождения точек пересечения полигонов, определяет сложность построения всех булевых операций. Для любых четных L и M можно построить примеры полигонов, у которых каждое ребро первого полигона пересекаются со всеми ребрами второго полигона (рис 1.), что покаэывает неулучшаемость оценки O(LM) для построения алгебры простых полигонов. (Можно ли построить соответствующие примеры для нечетного L и четного M? для нечетных L и М?)
Определение 3: Простой полигон называется выпуклым, если z[а,b]>0, где а=(xn- xn-1, уn- уn-1 ), b=(xn+1- xn, уn+1- уn ), n=1...N. (Сложение индексов происходит как в группе ZN+1). Для выпуклых полигонов существует весьма оригинальный алгоритм нахождения точек пересечения, имеющий сложность O(L+M). Таким образом, все булевы операции над выпуклыми полигонами реализуются за линейное время. Результаты булевых операций над выпуклыми и простыми полигонами выводят нас из класса простых полигонов, и приводят нас к понятию полигона с дырой. Определение 4: Полигон с дырами это множество полигонов Р={B, H0,H1, H2,...,HS}, где полигон B содержит Hi для всех i=0,...,S, называется внешним полигоном, а полигоны Hi, i=0,...,S со свойством Hi З Hj = Ж i,j=0..S, называются дырами. Точка является внутренней для полигона с дырой, если она принадлежит нечетному числу полигонов из Р. Пример полигона с дырой приведен на рисунке 2.
Пусть есть два полигона с дырами и Р={B, H0,H1, H2,...,HS}, Q={D, G0,G1, G2,...,GK}. Оценим количество действий, необходимых для построения булевых операций. Пусть b количество вершин в полигоне В, d количество вершин в полигоне D, hi количество вершин в дыре Hi, gi количество вершин в дыре Gi. Пусть H=max{hi,b} i=0..S, G=max{ gj,d} j=0..K. Тогда количество действий достаточных для реализации какой либо операции оценивается, как O(SKHG). Задачи приводящие к построению булевых операций над полигонами.
Задача 1. Картография. Рисунок 3 иллюстрирует данную ситуацию. Для решения необходимо, чтобы карты губернии и лесов были заданы как полигоны, далее, очевидно, надо взять их пересечение.
Задача 2. Движение какого либо виртуального объекта по дисплею.
Таким образом, есть возможность добиться более качественной анимации(?). При этом если движение объекта предсказуемо, то предсказуемо и какие ребра старого полигона пересекутся с ребрами нового полигона. То есть в случае простых полигонов, например, можно уйти от оценки O(LM) для нахождения разности, что делает перерисовку объекта еще более быстрой. На этом круг задач приводящих к построению булевых операций не исчерпывается. Желающие смогут найти несколько примеров подобного рода в книге Препараты и Шеймоса "Вычислительная геометрия". Замечание:Знак (?) будет означать, что автор не до конца уверен в истинности данного утверждения Структуры данныхПрежде чем приступить к описанию алгоритмов построения алгебры, остановимся на структурах данных, использовавшихся при этом. Ниже приведены структура sList и класс CPolygon. struct sList { CPoint pointList;// Координаты вершины BOOLEAN bTop; //Вершина собсвенная или добавленная BOOLEAN bUsed; // Использовалась или нет double z; sList *pList; //Указатель на следующую вершину sList *prevList; //Указатель на предыдущую вершину }; Cтруктура sList хранит список вершин и представляет собой двусвязанную динамическую структуру данных. class CPolygon { BOOLEAN bInit; // полигон задан или нет sList *flist,*elist; //Указатели на первую и последнюю вершину public: long int nTop; //Число вершин (начинается с 0) CPolygon() //Конструктор { nTop=-1;bInit=FALSE; flist=elist=NULL; } sList *Init(sList *p); //Инициализация полигона void Free();//Освобождение полигона BOOLEAN GetbInit() {return bInit;} void Show(CClientDC* p,COLORREF c); void Show(CClientDC* pDC,COLORREF cl,int nWidthPen); BOOLEAN GetbInit() {return bInit;} void SetbInitFalse() {bInit=FALSE;} sList *GetfList() {return flist;} sList *GeteList() {return elist;} void SeteList(sList *pe) {elist=pe;elist->pList=NULL;} BOOLEAN PointBelong(CPoint point); // Принадлежность точки полигону как замкнутуму подмножеству R2 BOOLEAN PointBelong(CPoint point,BOOLEAN bFr); // Принадлежность точки полигону как открытому подмножеству R2 BOOLEAN PointBelong(double xt,double yt); // Принадлежность точки с рациональными координатами полигону BOOLEAN PointBelongFr(CPoint point); //Принадлежность границе void PlusVector(int dx,int dy); //Параллельный перенос полигона void DelFalseTop();//Удалить из полигона точки пересечения BOOLEAN OrientRight();//Ориентация правая или нет void ChangeOrient();//Поменять ориентацию void Circle() //Замкнуть полигон { if (nTop<0) return; flist->prevList=elist; elist->pList=flist; } void UnCircle() //Разомкнуть полигон { if (nTop<0) return; flist->prevList=NULL; elist->pList=NULL; } }; Замечание: Здесь и в дальнейшем, чтобы не загромождать основную идею алгоритмов, приводятся несколько упрощенные структуры данных, чем те, что используются в реальной программе. Чтобы "вдохнуть жизнь" в полигон, необходимо сначала его инициализировать, то есть вызвать метод Init(sList *p). Параметром этого метода служит начало списка вершин. Далее опишем класс CTwoPolygons, где мы и будем выполнять все наши булевы операции. class CTwoPolygons { sList *pfInter,*peInter;//Список вершин с результатом операции public: CPolygon a,b; //Полигоны над которыми выполняются операции BOOLEAN bTwoInit; //Истина, если полигоны a и b инициализированы, Ложь в противоположном случае CTwoPolygons() { bTwoInit=FALSE; pfInter=poInter=peInter=NULL; } int InsertCutPoint(); //Вставляет точки пересечения полигонов a и b, в оба полигона. //Возвращает число вставленных точек. int FindCutPoint(); //Возвращает число точек пересечения sList *Intersection(); //Формирует список с пересечением sList *Trim(); //Формирует список с разностью sList *Weld();(); //Формирует список с объединением BOOLEAN Ainsideb(); // а внутри b BOOLEAN Binsidea(); //b внутри а void OperationFree(); //Освобождение списка с результатом операции }; Результаты операций даже над выпуклыми полигонами выводят нас из класса полигонов. Поэтому необходимо иметь класс, который бы умел принимать результаты булевых операций. Ниже описывается структура sOperationPolygon и класс CResult. struct sOperationPolygon { CPolygon polygon; int typePolygon; //1-внешний полигон, 0-дырка BOOLEAN bUsed; //Использовался или нет sOperationPolygon *pOPolygon; //Указатель на следующий полигон }; Структура sOperationPolygon представляет собой односвязанный список полигов. class CResult { sOperationPolygon *pfOperation; // Первый полигон в списке sOperationPolygon *peOperation; // Последний полигон в списке BOOLEAN bInitResult; // Список пуст или нет int nComponent; // Длина списка public: CResult() { pfOperation=NULL; peOperation=NULL; bInitResult=FALSE; nComponent=0; } BOOLEAN InitResult(sList *p); //Заполнение списка void ShowResult(CClientDC* p,COLORREF c); void ShowResult(CClientDC* pDC,COLORREF cl,int nWidthPen); void FreeResult(); //Освлбождение списка void PlusVectorResult(int dx,int dy); //Параллельный перенос списка полигонов void SetAllbUsedFalse(); void SetAllbUsedTrue(); BOOLEAN BelongResult(CPoint point); //Принадлежность списку void AddPolygon(sList *pL,int type); //Добавить полигон в список BOOLEAN ThereIsHole(); //Есть дырки в списке или нет BOOLEAN DeleteHole(sOperationPolygon *p); //Удалить дырку BOOLEAN DeletePolygon(sOperationPolygon *pt); //Удалить полигон из списка BOOLEAN GetbInitResult() {return bInitResult;} sOperationPolygon *GetpfOperation() {return pfOperation;} sOperationPolygon *GetpeOperation() {return peOperation;} int GetnComponent() {return nComponent;} }; Дальнейшая схема действий приведена на рисунке 5.
Поиск точек пересеченияМетод поиска точек пересечения двух выпуклых полигонов подробно описан в работе "Вычислительная геометрия" Препарата, Шеймос. Здесь мы лишь вкратце напомним его. Положение двух ребер пересекаемых полигонов, описывается одной из шести (с точностью до перестановки) ситуаций. (рис. 6)
Пусть прямые порожденные ребрами полигона заданы в параметрическом виде: X=x1+l*t; Y= y1+m*t; X=x3+p*s; Y= y3+q*s; Где l= x2 -x1, m= y2 -y1, p= x4 -x3, q= y4 -y3. Ребра пересекаются тогда, и только тогда, когда t,s содержатся в отрезке [0,1). Это ситуация (2, 2). Основная идея метода основана на разумном продвижении по ребрам полигона. Для создания механизма продвижения по границам необходимо рассмотреть каждый случай в отдельности. Следует двигаться по тому полигону, чья граница в данный момент не может принадлежать пересечению. Следуя этому методу за 2*(L+M) шагов мы найдем все точки пересечения. (Верно ли, что, если за L+M шагов мы не нашли ни одной точки пересечения, то их нет вообще?). Найденные точки пересечения мы загоняем в структуру sCutList. struct sCutList { CPoint cutPoint; double z; //Координата z векторного произведения ребра первого полигона на ребро // второго полигона double ta,tb; // параметры t,s (См. выше) sList *paCutList; // Указатель на ребро sList *pbCutList; // Указатель на ребро sCutList *pCutList; // Указатель на следующую точку пересечения sCutList *prevCutList; // Указатель на предыдущую точку пересечения }; Далее, чтобы корректно вставить точки пересечения в полигон, необходимо должным образом их упорядочить. Выбираем первое ребро первого полигона и ищем точки пересечения соотвествующие этому ребру (переменная *paCutList), упорядочиваем их по параметру ta и затем вставляем в полигон. Вставка в динамичекую структуру данных выполняется за костантное время (?). Выполняем аналогичную процедуру для других ребер полигона. Аналогично поступаем и со вторым полигоном. Описанные выше действия - поиск точек пересечения и вставку их в полигон выполняет метод класса CТwoPolygons InsertCutPoint(). Он возвращает число точек пересечения. Дальнейшие усилия будут направленны на непосредственное формирование списков с результатами булевых операций. Пересечение полигоновПусть переменные класса CTwoPolygons: a, b уже содержат копии исходных полигонов. Постановка задачи: Сформировать объект класса CResult I такой, что для всех x из того что x содержиться в I следует, что x содержится в a и x содержится в b. Устанавливаем правую ориентацию полигонов. Вызываем метод N=InsertCutPoint(). Если N=0, то проверяем включения a в b, и b в а. Если ни одно из включений не выполнилось то возвращаем Null и выходим. Если выполнилось первое включение, то заполяем список вершинами полигона а, если второе вершинами полигона b.
Пусть N не равно 0. В результате будет сформирован список содержащий пересечение данных полигонов. В данном алгоритме мы использовали тот факт, что пересечение выпулых полигонов либо пусто либо односвязно. Разность полигонов.Разность выпуклых полигонов уже не может быть выпуклым полигоном, кроме того может расподаться на несколько полигонов и иметь дыры. Постановка задачи: Сформировать объект класса CResult Т такой, что если x содержится в Т, то x содержится в a и x не содержится в b. Устанавливаем правую ориентацию полигонов. Вызываем метод N=InsertCutPoint(). Если N=0, проверяем включения a в b, b в a. Если выполнилось первое включение возвращаем Null и уходим. Если выполнняется второе включение, заполняем список сначала вершинами полигона а, затем вершинами полигона b и уходим. Если ни одно из включений не выполнилось заполняем список вершинами полигона а и уходим.
Пусть N не равно 0. В итоге получим список, с помощью которого мы инициализируем объект класса CResult. Полученный список полигонов будет содержать искомую разность. Объединение полигонов.Постановка задачи: Сформировать объект класса CResult W такой, что если x содержится в W, то x содержится в a или x содержится в b. Устанавливаем правую ориентацию полигонов. Вызываем метод N=InsertCutPoint(). Если N=0, проверяем включения проверяем включения a в b, b в a. Если выполнилось первое включение возвращаем список вершин полигона b, если второе список вершин полигона а. Если данные включения не выполняются, последовательно заполняем список вершинами полигонов a и b.
Пусть N не равно 0. В итоге будет сформирован список с искомым объединением. Оценка сложности.Для реализации любой булевой операции, в общем случае, достаточно 2*(L+M)+3*(L+M) шагов. 2*(L+M) шагов - чтобы найти точки пересечения и 3*(L+M) шагов - чтобы сформировать результат и вставить (удалить) точки пересечения. В случае, если ребра полигонов не пересекаются, через L+M шагов мы сумеем установить этот факт. L+M шагов требуется, чтобы проверить два включения. Таким образом, трудоемкость построения алгебры выпуклых полигонов оценивается следующим образом: 2*(L+M) ≤ T(L, M)≤ 5*(L+M) |
![]() |
||||||||||||
|
![]() |