Вычислительная геометрия Вычислительная геометрия – это раздел информатики, изучающий алгоритмы решения геометрических задач. Такие задачи встречаются в машинной графике, проектировании интегральных схем, технических устройств и др. Исходными данными такого рода задач могут быть множество точек на плоскости, набор отрезков, многоугольник (заданный например, списком своих вершин в порядке движения по часовой стрелки) и т.п. результатом может быть либо ответ на какой то вопрос (типа принадлежит ли точка отрезку с концевыми точками, пересекаются ли два отрезка, …), либо какойто геометрический объект (например, наименьший выпуклый многоугольник, соединяющий заданные точки, площадь многоугольника, и т.п.) В этом разделе мы рассмотрим решение некоторых геометрических задач. В наших задачах исходными данными будет множество точек на плоскости Рi, заданных своими координатами: Pi = (Xi,Yi), где Xi,Yi принадлежат множеству действительных чисел R. (В языке Паскаль это тип real, но целесообразно применять тип extended). Геометрические задачи в информатике встречаются довольно часто, так как компьютер является очень удобным и быстродействующим средством для их решения, поскольку ручной счёт здесь абсолютно неприменим. Так же часто задачи вычислительной геометрии встречаются на олимпиадах разного уровня, так как они требуют от участника собранности и точности даже в мелких деталях реализации. Малейшая ошибка в них приводит к тому, что автор программы вынужден тратить множество драгоценного времени на отладку программы и поиск в ней ошибки. Поэтому именно тот, кто успешно справляется с такими задачами в течение короткого времени, достоин наивысшей похвалы, каковой является победа на олимпиаде. В задачах аналитической геометрии используются все стандартные геометрические объекты: точки, отрезки, вектора (а также их скалярное и векторное произведения), прямые, многоугольники (выпуклые и невыпуклые) и т.д.. Поэтому даже школьнику уровня 7-8 класса несложно будет разобраться в изложенном материале. Будем рассматривать задачи вычислительной геометрии только на плоскости и только в декартовой системе координат. Точки и вектора. Координаты точки будем задавать парой чисел (x,y). Кроме того, нам понадобится понятие вектора, т.е. направленного отрезка. Его координаты также будем a (x,y). обозначать парой чисел, например будем находить следующим образом: x=x2-x1 (x1,y1) вектора; y=y2-y1 (x2,y2) вектора. Длина вектора a в таком случае теореме Пифагора: a x2 y2 Координаты вектора координаты координаты начала конца легко выражается по Поскольку длина вектора равна длине отрезка, образующего этот вектор, из предыдущей формулы легко выразить формулу расстояния между точками A(x1;y1) и B(x2;y2). AB= используют векторов. ( x1 x 2 ) 2 ( y1 y 2 ) 2 Большинство задач понятия скалярного и (скалярное произведение) где: вычислительной геометрии векторного произведений (a b ) a * b * cos a b a * b * sin (векторное произведение) , a , b - длины векторов a, b соответственно; - угол между направлениями векторов. Поскольку все величины в вышеуказанных формулах скалярные, т.е. числовые, результат также будет являться скаляром. Скалярное и векторное произведение векторов могут быть также выражены через их координаты: (a b ) x1 * x 2 y1 * y 2 (скалярное произведение) a b x1 * y 2 y1 * x 2 . (векторное произведение) Таким образом, зная координаты двух векторов, легко найти значения всех основных тригонометрических функций для угла между ними: x1 * y 2 y1 * x2 sin 2 2 2 2 ( x1 y1 )( x2 y 2 ) cos x1 * x 2 y1 * y 2 ( x1 y1 )( x 2 y 2 ) В отличие от скалярного, векторное произведение векторов имеет две геометрические интерпретации. Во-первых, его модуль равен площади параллелограмма, ограниченного самими векторами и параллельными им отрезками, проходящими через концы другого вектора. Проще говоря, если перенести вектора так, чтобы они исходили из начала координат, и соединить их концы, то площадь полученного треугольника будет равна половине модуля векторного произведения этих векторов. Во-вторых, знак векторного произведения определяет положение векторов друг относительно друга: a по часовой стрелке Если векторное произведение a b 0 , то относительно b a и b лежат на одной Если векторное произведение a b 0 , то прямой a Если векторное произведение b 0 , то a против часовой стрелке относительно b 2 2 2 Прямые и вектора. 2 Будем использовать два основных способа задания прямых в декартовой плоскости: (x-x1)(y2-y1)=(y-y1)(x2-x1) (уравнение прямой, проходящей через точки (x1,y1) и (x2;y2)); y=kx+b (уравнение прямой, тангенс угла наклона которой к оси абсцисс равен k, и пересекающей ось ординат в точке с абсциссой b). При реализации такого способа представления необходимо помнить, что прямая, параллельная оси ординат, будет иметь коэффициент k, равный бесконечности, то есть каким-либо способом предусматривать такой вариант. Существует ещё немало способов представления прямых в декартовой плоскости, но все они, с помощью элементарных алгебраических преобразований, приводятся к одному из двух данных представлений. Не будем настаивать на одном из этих представлений - заметим лишь, что несложно выполнять преобразования между данными двумя представлениями. Приведём несколько примеров таких преобразований, которые могут пригодиться нам в дальнейшем: Задана прямая y=kx+b. Найти её представление в виде (x-x1)(y2y1)=(y-y1)(x2-x1), т.е. найти две точки (x1;y1) и (x2;y2), проходящие через эту прямую. Решая эту задачу, мы можем даже взять точки с заданными абсциссами или ординатами. То есть мы выбираем произвольные x1, x2, а затем подставляем их в исходное уравнение прямой, откуда получаем: y1=kx1+b; y2=kx2+b. Прямая проходит через найденные точки (x1;y1) и (x2;y2), следовательно её уравнение может быть записано в виде (x-x1)(y2-y1)=(y-y1)(x2-x1). Задана прямая (x-x1)(y2-y1)=(y-y1)(x2-x1). Найти её представление в виде y=kx+b. Раскроем скобки в исходном уравнении: x(y2 - y1) - x1(y2 - y1) = y(x2 - x1) y1(x2 - x1); y(x2 - x1) = x(y2 - y1) + y1(x2 - x1) x1(y2 - y1); y y1 y1 ( x2 x1 ) x1 ( y 2 y1 ) yx 2 x2 x1 x2 x1 y y1 y y1 yx 2 ( y1 x1 2 ) x2 x1 x2 x1 Таким образом, видим, что в полученном уравнении y y1 y y1 k 2 b y1 x1 2 x 2 x1 x2 x1 В практической реализации, после подсчёта k, b можно вычислить проще. Для этого достаточно вспомнить, что прямая проходит через точку (x1;y1): y=kx+b; y1=kx1+b; b=y1-kx1. Кроме того, нужно обязательно помнить, что для прямой, параллельной оси ординат, x1=x2, а значит, невозможно будет найти искомое представление для данной прямой. Задана прямая ax + dy + c=0. Найти её представление в виде y = kx + b. Выполним следующие преобразования над исходным уравнением: ax + dy + c=0; отсюда dy=-ax-c; ax c ax c a c y y ; y ( x) ( ) . Получаем ; d d d d d a c k ,b d d Аналогично, в этом примере не стоит забывать, что прямая может быть параллельна оси ординат. Задана прямая (x - x1)(y2 - y1) = (y - y1)(x2 - x1). Найти её представление в виде ax + by + c = 0. Раскроем скобки в исходном уравнении: x(y2 - y1) - x1(y2 - y1) = y(x2 - x1) - y1(x2 - x1); x(y2 - y1) + y(x1 - x2) + y1(x2 - x1) - x1(y2 y1)=0. Из уравнения видим, что: a=y2-y1; b=x1-x2; c=y1(x2-x1)x1(y2-y1). Расстояние от точки ( x0 , y0 ) до прямой ax + by + c = 0 выражается формулой ax0 by 0 c l a2 b2 Поскольку мы уже умеем переводить представления прямых из одного вида в другой, вместо коэффициентов a, b и c в эту формулу можем подставить другие выражения, полученные ранее. Далее рассмотрим несколько простейших задач, необходимых при решении более сложных. Оформим их решение в виде функций и процедур Turbo Pascal, которые по желанию можно объединить в модуль (unit), либо вставлять в исходный текст программ, решающих более сложные задачи. Будем предполагаем, что все прямые для этих алгоритмов задаются в виде (x-x1)(y2-y1)=(y-y1)(x2-x1). Если при написании программы появляется необходимость использовать другое представление прямой, нужно просто воспользоваться методами перехода от одного представления к другому, описанными ранее. 1. Заданы точки A(x1;y1), B(x2;y2) и прямая (x-x3)(y4-y3)=(yy3)(x4-x3). Определить, лежат ли точки по разные стороны от заданной прямой. По условию, прямая проходит через точки C(x3;y3) и D(x4;y4). Для того, чтобы определить, лежат ли точки по разные стороны от прямой, возьмём точку C и построим вектора CD, CA, CB . Если вектора СA и CB лежат по разные стороны относительно CD , точки будут лежать по разные стороны от прямой. В обратном случае точки будут лежать по одну сторону от прямой. Для того, чтобы определить, по разные ли стороны от прямой лежат векторы, требуется проверить, совпадают ли знаки векторных произведений этих векторов на вектор, лежащий на прямой, в нашем случае вектор CD . Таким образом, в нашем случае достаточно проверить следующее условие: Sign( CA CD ) Sign( CB CD ). Sign - это известная таким образом: математическая функция, определяющаяся Sign(x)= Внесём в наш модуль две первые функции: function VectorMult(x1,y1,x2,y2:real):real; begin VectorMult:=x1*y2-x2*y1; end; {VectorMult} function Sign(n:real):shortint; begin if n>0 then Sign:=1 else if n<0 then Sign:=-1 else Sign:=0; end; Очевидно, что если одна из точек лежит на прямой, то для неё векторное произведение будет равно нулю. В этом случае будем считать, что она лежит по ту же сторону от прямой относительно первой точки. Но если есть необходимость использовать другой подход, нужно будет просто проверять вариант, когда векторное произведение нулевое. function OtherSide(x1,y1,x2,y2,x3,y3,x4,y4:real):boolean; var v0x,v0y,v1x,v1y,v2x,v2y:real; {координаты векторов} begin v0x:=x4-x3; v0y:=y4-y3; v1x:=x1-x3; v1y:=y1-y3; v2x:=x2-x3; v2y:=y2-y3; if (v0x*v1y-v0y*v1x)*(v0x*v2y-v0y*v2x)<0 then OtherSide:=true else OtherSide:=false; {Можно заменить на if Sign(VectorMult(v0x,v0y,v1x,v1y))<>Sign(VectorMult(v0x,v0y,v2x,v 2y))} end; {OtherSide} 2. Заданы точки A(x1;y1), B(x2;y2), C(x3;y3) и D(x4;y4). Определить, пересекаются ли отрезки AB и CD. Отрезки пересекаются тогда и только тогда, когда: точки A и B лежат по разные стороны относительно прямой CD; точки С и D лежат по разные стороны относительно прямой AB. Для проверки этих условий воспользуемся уже написанной функцией OtherSide. Function LinesCross(x1,y1,x2,y2,x3,y3,x4,y4:real):boolean; begin if OtherSide(x1,y1,x2,y2,x3,y3,x4,y4) and OtherSide(x3,y3,x4,y4,x1,y1,x2,y2) then LinesCross:=true else LinesCross:=false end; 3. Заданы прямая (x-x1)(y2-y1)=(y-y1)(x2-x1) и точка (x0;y0). Определить, лежит ли точка на прямой. Такая задача решается очевидно. Ясно, что если точка лежит на прямой, то её координаты удовлетворяют уравнению этой прямой. Следовательно, достаточно лишь проверки следующего условия: (x0 - x1)(y2 - y1) = (y0 - y1)(x2 - x1). function PointOnLine(x0,y0,x1,y1,x2,y2:real):boolean; begin if (x0-x2)*(y1-y2)=(y0-y2)*(x1-x2) then PointOnLine:=true else PointOnLine:=false end; 4. Заданы точки A(x1;y1), B(x2;y2), C(x3;y3) и Определить, лежат ли отрезки AB и CD на одной прямой. Уравнение прямой AB: D(x4;y4). (x-x1)(y2-y1)=(y-y1)(x2-x1). Чтобы отрезок CD лежал на этой же прямой, необходимо, чтобы обе точки C и D лежали на этой прямой, т.е. удовлетворяли её уравнению. При реализации этого алгоритма можно воспользоваться ранее описанной функцией PointOnLine: function SameLine(x1,y1,x2,y2,x3,y3,x4,y4:real):boolean; begin if PointOnLine(x3,y3,x1,y1,x2,y2) and PointOnLine(x4,y4,x1,y1,x2,y2) then SameLine:=true else SameLine:=false end; 5.Заданы прямые (x - x1)(y2 - y1) = (y - y1)(x2 - x1) и (x - x3)(y4 - y3)=(y - y3)(x4 - x3). Найти точку их пересечения (если таковая имеется). Очевидное решение состоит в том, чтобы решить систему уравнений прямых: Однако если попытаться раскрывать скобки, выражать значение одной из неизвестных переменных через другую и подставлять выражение в другое уравнение, получим достаточно сложные вычисления, которых хотелось бы избежать. Поэтому для простоты решения приведём заданные представления прямых к виду y=kx+b. Мы уже знаем, как это делать. Тогда достаточно будет решить систему: k1x+b1=k2x+b2; x(k1-k2)=b2-b1; x b2 b1 k1 k 2 y теперь можно найти из любого начального уравнения системы. Теперь можно подумать о том, что будет, если одна из прямых (или обе) параллельны оси y, ведь тогда мы не сможем вычислить для неё угловой коэффициент k. Поэтому перед его вычислением будем проверять параллельность прямой оси y. Пусть, например, прямая (x - x1)(y2 - y1) = (y - y1)(x2 -x1) параллельна оси y, т.е. x2 = x1 и нужно найти точку её пересечения с прямой y = k2x + b2, которая не параллельна оси y, т.е. k2 существует. Тогда абсцисса пересечения этих двух прямых, очевидно, и будет x1, ординату же можно получить из уравнения второй прямой. Аналогичным способом поступаем в случае, если вторая прямая параллельна оси y. Теперь остаётся только подумать о том, как учесть возможную параллельность заданных прямых. Ответ очевиден: прямые параллельны, если у них совпадает угловой коэффициент, т.е. k1=k2. Нужно лишь дополнительно проверить, параллельны ли обе прямые оси y и если это так - значит, они параллельны друг другу. В нашей реализации этого алгоритма в процедуру в качестве параметров задаются координаты x1, y1, x2, y2, x3, y3, x4, y4, в качестве переменных - результаты выполнения процедуры: x,y координаты точки пересечения (если она существует); err логическая переменная, которая истинна, если прямые параллельны. procedure CrossPoint(x1,y1,x2,y2,x1,y1,x2,y2:real; var x,y:real; var err:boolean); var k1,b1,k2,b2:real; begin if (x1=x2) and (x3=x4) then err:=true else begin k1:=(y1-y2)/(x1-x2); b1:=y1-k*x1; k1:=(y3-y4)/(x3-x4); b1:=y3-k*x3; if x1=x2 then begin {Первая прямая - параллельна оси y, вторая - нет} x:=x1; y:=k2*x+b2; end else if x3=x4 then begin x:=x3; y:=k1*x+b1; end else if k1=k2 then err:=true {прямые параллельны} else begin x:=(b2-b1)/(k1-k2); y:=k1*x+b1; end; end; end; {CrossPoint} Задача "Точка в многоугольнике". Условия. Многоугольник (необязательно выпуклый) задан координатами своих вершин (xi;yi), 1 i N в порядке обхода. Определить, лежит ли точка с координатами (x0;y0) внутри или вне многоугольника. Входные данные содержатся в файле input.txt: в первой строке - пара чисел (x0;y0), в каждой (i+1)-й строке координаты i-й точки (xi;yi). Выходные данные нужно вывести в файл output.txt: единственная строка этого файла должна содержать одно число - 1, если точка внутри многоугольника (или на его стороне), 0 - в противном случае. Пример. Input.txtt 5 3.1 Output.txt 1 Input.txt 5 3.7 1 1 1 1 6 3.8 6 3.8 3 3 3 3 Output.txt 0 5 3 5 3 2 2 2 2 2 4 2 4 Решение. Можно предложить несколько способов решения этой задачи. На наш взгляд, самый оптимальный из них следующий. Проведём луч из точки (x;y) в любом направлении и найдём количество пересечений этого луча со сторонами многоугольника. Очевидно, что если точка внутри многоугольника, полученное количество будет нечётным. Если же точка не принадлежит многоугольнику, количество пересечений будет чётным либо нулевым. На этой идее и основан наш алгоритм. Для удобства мы проводим луч через точку (x0; y0) параллельно оси абсцисс. Для определения результата используется логическая функция XOR. Ясно, что если количество истинных операндов в выражении XOR будет нечётным, в конце концов её значение будет истинно - это и будет означать, что точка лежит в многоугольнике. В противном случае результат будет ложным - точка вне многоугольника. Кроме того, в программе используется функция CrossPoint, описанная в модуле Geometry, текст которого приведен в начале этого раздела. uses Geometry; const maxn=100; {максимальное количество точек многоугольника} type index=1..maxn; coord=real; {координаты} var dat,out:text; n,i,i1:index; x,y:array[index] of coord; x0,y0:coord; {координаты точки} inpoly:boolean; function min(a,b:real):real; begin if a<b then min:=a else min:=b; end; {min} function max(a,b:real):real; begin if a>b then max:=a else max:=b; end; {max} function ExCrossPoint(i:index):boolean; {Определяет, пересекается ли проведенный луч с i-й стороной многоугольника} var cx,cy:real; {координаты точки пересечения} err:boolean; begin if i=n then i1:=1 else i1:=i+1; CrossPoint(x0,y0,x0+1,y0,x[i],y[i],x[i1],y[i1],cx,cy,err); if err then ExCrossPoint:=false else if (cx>=x0) and (cx>=min(x[i],x[i1])) and (cx<=max(x[i],x[i1])) and (cy>=min(y[i],y[i1])) and (cy<=max(y[i],y[i1])) {точка пересечения должна принадлежать стороне} then ExCrossPoint:=true else ExCrossPoint:=false; end; {ExistSamePoint} begin {Main} Assign(dat,'input.txt'); Reset(dat); ReadLn(dat,x0,y0); {координаты заданной точки} while not eof(dat) do begin Inc(n); ReadLn(dat,x[n],y[n]); end; Close(dat); for i:=1 to n do begin if (x0=x[i]) and (y0=y[i]) then begin {Точка (x0;y0) совпадает с одной из вершин многоугольника} inpoly:=true; Break; end; inpoly:=inpoly xor ExCrossPoint(i); end; Assign(out,'output.txt'); Rewrite(out); if inpoly then write(out,1) else write(out,0); Close(out); end. Задача "Максимальный квадрат" (SqMax) Условия. На плоскости задано N точек с координатами (xi;yi), 1 i N, 4 N 300. Из множества заданных точек необходимо выделить точки, являющиеся вершинами квадрата, содержащего максимальное число заданных точек. Предполагается, что точки, расположенные на сторонах квадрата, также принадлежат квадpату. Входные данные содержатся в файле input.txt следующим обpазом: файл содеpжит N+1 стpоку, в пеpвой стpоке записано число N - количество заданных точек, в каждой из следующих N cтpок pасполагается паpа чисел (xi;yi) - координат iй точки, pазделенных одним или несколькими пpобелами. Выходные следующим образом: данные нужно вывести в файл output.txt если pешения не существует, то файл содержит единственную строку: 'NO SOLUTION'; o если pешение существует, то файл содеpжит пять стpок: в пеpвой стpоке записано число N - количество точек, содеpжащихся в найденном o квадpате, в каждой из последующих четыpех стpок pасполагается паpа чисел, pазделенных одним или несколькими пpобелами,- координаты точек квадрата в порядке обхода. Пример. Input.txt Output.txt Input.txt 11 7 4 100 0 100 0 0 0 0 100 0 10 0 200 -100 0 5 0 11 100 100 100 50 0 1 Output.txt NO SOLUTION 0 5 50 150 150 50 300 0 100 -30 -30 0 100 0 -100 Решение. Эта задача не имеет никакого "красивого" решения. Вся красота здесь состоит в том, чтобы максимально оптимизировать алгоритм исчерпывающего поиска. Перебирать будем не все комбинации четырёх точек, а все комбинации пар точек. Для каждой такой пары будем строить квадрат, содержащий отрезок между этими точками, затем проверять, имеются ли все точки этого квадрата в исходном наборе. Если это так, то остаётся посчитать количество точек, входящих в этот квадрат и, если оно больше, чем уже полученное максимальное, запомнить его. Проблема заключается только в том, как описать построение квадрата по двум точкам (как проверить вхождение в него точек, мы уже знаем из предыдущей задачи). Оказывается, необязательно даже использовать векторное произведение достаточно выполнить несложные операции с координатами двух исходных точек. Отметим ещё, что по двум точкам можно построить два квадрата. Однако нам достаточно рассматривать только один, так как второй будет рассмотрен при обработке других точек. uses Geometry; const maxn=400; {максимальное кол-во точек} maxs=4; {кол-во точек в решении} type index=0..maxn; coord=longint; {координаты} indexs=1..maxs; var dat,out:text; n,i,j,k,col,max:index; x,y:array[index] of coord; {координаты точек} solx,soly:array[indexs] of coord; {решение} s:indexs; x3,y3,x4,y4:coord; s1,s2,s3,s4:shortint; function ExistPoint(px,py:coord):boolean; {Определяет, имеется ли точка (px;py) в наборе входных данных} begin for k:=1 to n do if (px=x[k]) and (py=y[k]) then begin ExistPoint:=true; Exit; end; ExistPoint:=false; end; {ExistPoint} begin {Main} Assign(dat,'input.txt'); Reset(dat); ReadLn(dat,n); for i:=1 to n do ReadLn(dat,x[i],y[i]); Close(dat); for i:=1 to n-1 do for j:=i+1 to n do begin { Первые две точки квадрата - (x[i];y[i]) и (x[j];y[j]) } x3:=x[i]+y[i]-y[j]; y3:=y[i]+x[j]-x[i]; {третья точка} x4:=x[j]+y[i]-y[j]; y4:=y[j]+x[j]-x[i]; {четвертая точка} if ExistPoint(x3,y3) and ExistPoint(x4,y4) then begin {Посчитать кол-во точек, входящих в найденный квадрат} col:=0; for k:=1 to n do begin {s1,s2,s3,s4 - знаки векторных произведений} s1:=Sign(VectorMult(x[j]-x[i],y[j]-y[i],x[k]x[i],y[k]-y[i])); s2:=Sign(VectorMult(x4-x3,y4-y3,x[k]-x3,y[k]y3)); s3:=Sign(VectorMult(x3-x[i],y3-y[i],x[k]x[i],y[k]-y[i])); s4:=Sign(VectorMult(x4-x[j],y4-y[j],x[k]x[j],y[k]-y[j])); if (s1<>s2) and (s3<>s4) then Inc(col); {точка лежит в квадрате} end; if col>max then begin {Запомнить текущее оптимальное решение} max:=col; solx[1]:=x[i]; soly[1]:=y[i]; solx[2]:=x[j]; soly[2]:=y[j]; solx[3]:=x4; soly[3]:=y4; {выдаем в порядке обхода} solx[4]:=x3; soly[4]:=y3; end; end; end; Assign(out,'output.txt'); Rewrite(out); if max=0 then writeln(out,'NO SOLUTION') else begin writeln(out,max); for s:=1 to 4 do writeln(out,solx[s],' ',soly[s]); end; Close(out); end. Принадлежность точки отрезку с концевыми точками. Принадлежит ли точка A(x,y) отрезку с концевыми точками B(x1,y1) и C(x2,y2)? Точки отрезка Z можно описать уравнением pOB+(1-p)OC=Z, 0<=p<=1, OB и OC - векторы. Если существует такое p, 0<=p<=1, что pOB+(1-p)OC=A (1), то A лежит на отрезке, иначе - нет. Равенство (1) расписывается по координатно так: px1+(1-p)x2=x (2) py1+(1-p)y2=y (3) Из первого уравнения находим p, подставляем во второе: если получаем равенство и 0<=p<=1, то A на отрезке, иначе - нет. Напишем программу. uses crt; var d:boolean; x,y,x1,y1,x2,y2:extended; function pixelon(x,y,x1,y1,x2,y2:extended):boolean; var p1,p2:extended; begin if (x1=x2) and(y1=y2) then begin pixelon:=true;exit;end; if x1=x2 then p1:=(y-y2)/(y1-y2) else p1:=(x-x2)/(x1-x2); p2:=p1*y1+(1-p1)*y2; if (p1>=0)and(p1<=1)and(p2=y) then pixelon:=true else pixelon:=false; end; begin clrscr; write('введите координаты точки А(x,y):'); readln(x,y); write('введите координаты точки B(x1,y1):'); readln(x1,y1); write('введите координаты точки C(x2,y2):');readln(x2,y2); d:=pixelon(x,y,x1,y1,x2,y2); if d then writeln('точка на отрезке') else writeln('точка не на отрезке'); readkey; end. Окружность и три точки Пусть даны три точки на плоскости Р1, Р2, Р3 Вычислить координаты центра окружности точки. проходящей через эти Проведем через пары точек две прямые. Первая линия пусть проходит через P1 и P2, а прямая b - через P2 и P3. Уравнения этих прямых будут где m - коэффициент наклона линии, получаемый из Центр круга - находится на пересечении двух перпендикулярных прямых, проходящих через середины отрезков P1P2 и P2 P3. Легко доказать, что прямая, перпендикулярная к линии с коэффициентом наклона m имеет коэффициент наклона -1/m, значит уравнения прямых, перпендикулярных a и b и проходящих через середины P1P2 и P2P3 будут Они пересекаются в центре, и решение относительно x дает Значение у вычислим подстановкой x в уравнение одного из перпендикуляров. Можно и наоборот: сначала решить относительно y, а потом найти x. Знаменатель (mb - ma) равен нулю, когда прямые параллельны. В этом случае они совпадают, то есть круга не существует. Если какая-нибудь из прямых вертикальна, то ее коэффициент наклона m равен бесконечности. Этого можно избежать, просто поменяв порядок точек так, чтобы вертикальных линий не появлялось. Векторное произведение (второй способ). При решении геометрических задач полезно использовать векторное произведение. Пусть даны векторы P1 и P2. В большинстве случаев нас будут интересовать только векторы, лежащие в одной плоскости, поэтому векторным произведением P1 x P2 можно понимать площадь параллелограмма (с учетом знака), образованного точками (0,0), P1, P2, P1 + P2 = (X1 + X2 , Y1 + Y2). Для вычисления векторного произведения более удобно вычисления определителя матрицы P1 x P2 = X1 Y2 – X2 Y1. Рис. 1. (а) Векторное произведение векторов P1 и P2 это площадь параллелограмма (с учётом знака), (б) Векторы в светлой части плоскости получаются из вектора P поворотом по часовой стрелке, в тёмной—против. Итак векторное произведение на плоскости для двух векторов определяется по формуле W = X1Y2 – X2Y1. Если W>0, то кратчайший поворот P2 относительно (0,0) совмещающий его с P1, происходит по часовой стрелке, если W<0, то против. На рисунке 1 (б) в светлой части плоскости получаются из вектора Р поворотом по часовой стрелки, в тёмной – против. Граница этих областей состоит их векторов, векторное произведение которых с Р равно нулю. Эти векторы лежат на одной прямой с Р. Теперь напишем формулу, которая позволяет определить в какую сторону надо поворачивать вектор Р0Р1 вокруг Р0 чтобы его наложить на Р0Р2 (по часовой стрелке или против). Считая началом координат точку Р0, мы получим два вектора Р1 – Р0 = (Х1 – Х0, Y1 - Y0) и P2 – P0 = (Х2 – Х0, Y2 - Y0). Их векторное произведение равно W = (P1 – P0) x (P2 – P0) = (X1 – X0) (Y2-Y0) – (X2 - X0) (Y1 – Y0). Если W>0, то вращать надо против часовой стрелки («в положительном направлении»), если W<0 – то по часовой стрелке. Для примера рассмотрим такую простую задачу. Даны три точки со своими координатами А(X1,Y1), B(X2,Y2), C(X3,Y3). Нужно определить, какой поворот создаёт точка B(X2,Y2) по отношению к точке C(X3,Y3) относительно точки A(X1,Y1). uses crt; var x1,y1,x2,y2,x3,y3:extended; d:boolean; function poworot(x1,y1,x2,y2,x3,y3:extended):boolean; var d:extended; begin d:=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1); writeln(d); if d=0 then begin writeln('поворота нет, точки лежат на одной прямой');halt;end; if d>0 then poworot:=true else poworot:=false; end; begin clrscr; write('введите координаты первой точки:');readln(x1,y1); write('введите координаты второй точки:');readln(x2,y2); write('введите координаты третьей точки:');readln(x3,y3); d:=poworot(x1,y1,x2,y2,x3,y3); if d then writeln('"положительное направление" - против часовой стрелки') else writeln('"отрицательное направление" - по часовой стрелке'); readkey; end. При помощи векторного произведения можно определить направление поворота. Данный метод встречается в очень многих задачах вычислительной геометрии, например, для получения выпуклой оболочки многоугольника на плоскости, координаты N точек. Эту задачу мы рассмотрим ниже. где заданы Пересекаются ли отрезки. (1 способ) Следующим, очень нужным методом вычислительной геометрии является определение пересечения двух отрезков. Существуют много способов, чтобы определить пересечение двух отрезков. Мы рассмотрим для этого два этапа и в конце размышлений определим критерии пересечения двух отрезков. Первый этап состоит в том, что если ограничивающие прямоугольники отрезков не имеют общих точек, то сами отрезки не пересекаются. Ограничивающий прямоугольник фигуры – это наименьший из прямоугольников со сторонами, параллельными осям координат, которые содержат данную фигуру. Пусть даны отрезки A и B со своими координатами начала и конца. A – ((X1,Y1) и (X2,Y2)), B – ((X3,Y3) и (X4,Y4)). Для отрезка A таковым будет прямоугольник с левым нижним углом в точке c абсциссой X1n = min(X1,X2) и ординатой Y1n = min(Y1,Y2) и c правым верхним углом в точке с абсциссой X2w = max(X1,X2) и ординатой Y2w = max(Y1,Y2). Для отрезка B таковым будет прямоугольник с левым нижним углом в точке c абсциссой X3n = min(X3,X4) и ординатой Y3n = min(Y3,Y4) и c правым верхним углом в точке с абсциссой X4w = max(X3,X4) и ординатой Y4w = max(Y3,Y4).. Здесь min и max это функции определения минимального и максимального среди двух чисел. Условие пересечения двух прямоугольников, заданных левым нижним и правым верхним углами таково: Прямоугольник, образованный отрезком А с координатами в левой нижней точке (X1n, Y1n) и с координатами в правой верхней точке (X2w, Y2w) и прямоугольник, образованный отрезом B с координатами в левой нижней точке (X3n, Y3n) и с координатами в правой верхней точке (X4w, Y4w) имеет общие точки тогда и только тогда, когда (X2w>=X3n) и (X4w>=X1n) и (Y2w>=Y3n) и (Y4w>=Y1n). Первые два условия соответствуют пересечению X – проекций, вторые два – Y – проекций. Если на этом этапе не удается установить, что отрезки не пересекаются, то мы переходим ко второму этапу и проверяем, пересекаются ли каждый из данных отрезков с прямой, содержащей другой отрезок. Отрезок пересекает прямую, если его концевые точки лежат по разные стороны от неё или если один из концов лежит на прямой. Проверим это условие. Для чего будем использовать векторное произведение. Точки P3 и P4 лежат по разные стороны от прямой, образованный точками P1 и P2, если векторы P1P3 и Р1Р4 имеют различную ориентацию относительно вектора Р1Р2, т.е. если знаки векторных произведений (Р3 - Р1) х (Р2 - Р1) и (Р4 - Р1) х (Р2 – Р1) различны. Если одна их этих векторных произведений равно нулю, от одна из точек Р3 или Р4 принадлежат прямой, образованный точками Р1 и Р2 В обеих случаях отрезки пересекаются, однако возможен и другой случай: хотя отрезки проходят первый этап и векторное произведение равно нулю, всё-таки пересечения нет. Поэтому условие «отрезок пересекает прямую» надо проверить для обоих отрезков. Критичный случай, если один из отрезков, например Р1Р2 равен нулю, то вопрос о том, пересекает ли отрезок Р3Р4 прямую Р1Р2 лишен смыла, т.е. много прямых. Тем не менее, можно вычислить векторные произведения (Р3 – Р1) х (Р2 – Р1) и (Р4 – Р1) х (Р2 – Р1), которые в данном случае равны нулю. Итак, критерии пересечения отрезков таков: отрезки Р1Р2 и Р3Р4 пересекаются тогда и только тогда, когда одновременно выполнены три условия: 1) пересекаются ограничивающие их прямоугольники (X2w>=X3n) и (X4w>=X1n) и (Y2w>=Y3n) и (Y4w>=Y1n) 2) ((P3 P1) x (P2 –P1)) x ((P4 – P1) x (P2 – P1))<=0 3) ((P1 P3) x (P4 –P3)) x ((P2 – P3) x (P4 – P3))<=0 Последние два условия попробуем представить в виде выражения, используя координаты начала и конца отрезков. ((P3 - P1) x (P2 –P1)) = (X3 – X1)(Y2 – Y1)-(X2 - X1)(Y3 – Y1) ((P4 - P1) x (P2 – P1)) = (X4 – X1)(Y2 – Y1)-(X2 - X1)(Y4 – Y1) ((P1 P3) x (P4 –P3)) = (X1 – X3)(Y4 – Y3)-(X4 - X3)(Y1 – Y3) ((P2 – P3) x (P4 – P3)) = (X2 – X3)(Y4 – Y3)-(X4 - X3)(Y2 – Y3) Критерии пересечения двух отрезков у которых заданы координаты. 1) (X2w>=X3n) и (X4w>=X1n) и (Y2w>=Y3n) и (Y4w>=Y1n) 2) (X3 – X1)(Y2 – Y1)-(X2 - X1)(Y3 – Y1) x (X4 – X1)(Y2 – Y1)(X2 - X1)(Y4 – Y1)<=0 3) (X1 – X3)(Y4 – Y3)-(X4 - X3)(Y1 – Y3) x (X2 – X3)(Y4 – Y3)(X4 - X3)(Y2 – Y3) <=0 В пункте 1) координаты минимальных и максимальных значений абсцисс и ординат точек прямоугольников образованных точками отрезков А и В. Исходя из вышеизложенного попробуем написать программу, которая определяет пересекаются ли два отрезка или нет. uses crt; var x1,y1,x2,y2,x3,y3,x4,y4:extended; d:boolean; function min(a,b:extended):extended; begin if a>b then min:=b else min:=a; end; function max(a,b:extended):extended; begin if a>b then max:=a else max:=b; end; function potrezok(x1,y1,x2,y2,x3,y3,x4,y4:extended):boolean; var mx1,my1,mx2,my2:extended; mx3,my3,mx4,my4:extended; d,p1,p2:boolean; begin mx1:=min(x1,x2);my1:=min(y1,y2); mx2:=max(x1,x2);my2:=max(y1,y2); mx3:=min(x3,x4);my3:=min(y3,y4); mx4:=max(x3,x4);my4:=max(y3,y4); d:=(mx2>=mx3)and(mx4>=mx1)and(my2>=my3)and(my4>=my1); P1:=((X3-X1)*(Y2-Y1)-(X2-X1)*(Y3-Y1))*((X4-X1)*(Y2-Y1)-(X2X1)*(Y4-Y1))<=0; P2:=((X1-X3)*(Y4-Y3)-(X4-X3)*(Y1-Y3))*((X2-X3)*(Y4-Y3)-(X4X3)*(Y2-Y3))<=0; IF D AND P1 AND P2 THEN D:=TRUE ELSE D:=FALSE; if d then potrezok:=true else potrezok:=false; end; begin clrscr; write('введите координаты первой точки:');readln(x1,y1); write('введите координаты второй точки:');readln(x2,y2); write('введите координаты третьей точки:');readln(x3,y3); write('введите координаты третьей точки:');readln(x4,y4); d:=potrezok(x1,y1,x2,y2,x3,y3,x4,y4); if d then writeln('отрезки пересекаются') else writeln('отрезки не пересекаются'); readkey; end. Пересекаются ли 2 отрезка? (2 способ) ----------------------------------------------------------------------Определяет пересечение отрезков A(ax1,ay1,ax2,ay2) и B (bx1,by1,bx2,by2), функция возвращает TRUE - если отрезки пересекаются, а если пересекаются в концах или вовсе не пересекаются, возвращается FALSE (ложь) ----------------------------------------------------------------------function Intersection(ax1,ay1,ax2,ay2,bx1,by1,bx2,by2:real):boolean; var v1,v2,v3,v4:real; begin v1:=(bx2-bx1)*(ay1-by1)-(by2-by1)*(ax1-bx1); v2:=(bx2-bx1)*(ay2-by1)-(by2-by1)*(ax2-bx1); v3:=(ax2-ax1)*(by1-ay1)-(ay2-ay1)*(bx1-ax1); v4:=(ax2-ax1)*(by2-ay1)-(ay2-ay1)*(bx2-ax1); Intersection:=(v1*v2<0) and (v3*v4<0); end; begin { основная программа, вызов функции - тест } writeln(Intersection(1,1,5,5,1,2,3,1)); {test1, Intersection} writeln(Intersection(1,1,5,5,1,2,1,3)); {test2, Intersection} end. yes no Принадлежность точки заданному многоугольнику, у которого заданы координаты точек по обходу по или против часовой стрелки. Полученный нами метод нахождения пересечения отрезков позволяет очень красиво решить задачу на принадлежность точки многоугольнику. Пусть многоугольник на плоскости задается координатами своих N вершин в порядке обхода их по контуру по часовой стрелке (контур самопересечений не имеет). Для заданной точки Z(x,y) определить, принадлежит ли она стороне многоугольника или лежит внутри или вне его. Идея решения этой задачи состоит в том, что нужно подсчитать количество пересечений луча, который имеет начало в данной точке Z(x,y) и параллельна любой из осей координат. Из данного луча выделим отрезок с началом в точке Z(x,y) и с концом в точке с максимальным значением абсциссы всех точек плюс 1 (эта точка заведомо находится вне многоугольника). Далее подсчитываем количество пересечений данного отрезка со всеми отрезками многоугольника в порядке обхода их по или против часовой стрелки. Если оно нечетное, то точка внутри, если четное - то снаружи. В алгоритме безразлично, выпуклый он или нет. Рассмотрим программу для определения принадлежности точки многоугольнику. uses crt; type b=record x,y:extended; end; var x,y,maxi:extended; i,n,k,nom:integer; d:boolean; a:array[1..100] of b; function min(a,b:extended):extended; begin if a>b then min:=b else min:=a; end; function max(a,b:extended):extended; begin if a>b then max:=a else max:=b; end; function potrezok(x1,y1,x2,y2,x3,y3,x4,y4:extended):boolean; var mx1,my1,mx2,my2:extended; mx3,my3,mx4,my4:extended; d,p1,p2:boolean; begin mx1:=min(x1,x2);my1:=min(y1,y2); mx2:=max(x1,x2);my2:=max(y1,y2); mx3:=min(x3,x4);my3:=min(y3,y4); mx4:=max(x3,x4);my4:=max(y3,y4); d:=(mx2>=mx3)and(mx4>=mx1)and(my2>=my3)and(my4>=my1); P1:=((X3-X1)*(Y2-Y1)-(X2-X1)*(Y3-Y1))*((X4-X1)*(Y2-Y1)-(X2X1)*(Y4-Y1))<=0; P2:=((X1-X3)*(Y4-Y3)-(X4-X3)*(Y1-Y3))*((X2-X3)*(Y4-Y3)-(X4X3)*(Y2-Y3))<=0; IF D AND P1 AND P2 THEN D:=TRUE ELSE D:=FALSE; if d then potrezok:=true else potrezok:=false; end; procedure zapmas; begin assign(input,'input.txt'); reset(input); read(n); for i:=1 to n do read(a[i].x,a[i].y); read(x,y); close(input); end; procedure kolper; begin k:=0; maxi:=a[i].x; nom:=1; for i:=2 to n do if maxi<a[i].x then begin maxi:=a[i].x;nom:=i;end; a[n+1].x:=a[1].x; a[n+1].y:=a[1].y; for i:=1 to n do if potrezok(a[i].x,a[i].y,a[i+1].x,a[i+1].y,x,y,a[nom].x+1,a[nom].y ) then k:=k+1; if odd(k) then writeln('точка внутри многоугольника') else writeln('точка вне многоугольника'); end; begin clrscr; zapmas; kolper; readkey; end. Функция potrezok была разобрана выше. Координаты многоугольника и заданной точки считываем из файла input.txt. Предварительно найдем номер точки с максимальным значением абсциссы. Далее просмотрим все отрезки по обходу по или против часовой стрелки на пересечение с отрезком в начале в заданной точке Z(x,y) и с концом в точке с номером точки с максимальной абсциссой плюс 1 и подсчитываем количество пересечений. Пересечение прямой и отрезка. Во многих задачах вычислительной геометрии встречаются задачи, где нужно определить пересечение прямой и отрезка, если заданы уравнение прямой ax +by +c =0 (или y = kx + b) и координаты начала (X1,Y1) и конца (X2,Y2) отрезка. Вариант 1. Можно через концы отрезка провести прямую cx+d=y и определить, принадлежит ли точка пересечения двух прямых x, еслиона существует, отрезку. То есть, мы должны решить уравнение x*(c-a)=(b-d), найти y = ax+b, и проверить выполнение неравенств x1<=x<=x2, y1<=y<=y2. Но при нахождении x при делении могут возникнуть большие вычислительные погрешности или даже переполнение или потеря значимости, в результате чего получится неверный ответ. Вариант 2. Обозначим F(x,y)=ax+b-y. Прямая ax+b=y разбивает плоскость на три части: в одной F(x,y)>0, в другой F(x,y)<0 и на прямой ax+b=y F(x,y)=0 . Если эта прямая пересекает отрезок, то либо концы отрезка лежат в различных полуплоскостях, либо хотя бы одна концевая точка отрезка лежит на прямой. Это равносильно выполнению следующего неравенства F(x1,y1)*F(x2,y2)<=0. Таким образом, не вычисляя точку пересечения, мы по знаку функционала судим о том, имеют ли прямая и отрезок общую точку. Очевидно, что второй вариант решения подзадачи предпочтительнее первого. Представим решение задачи вторым методом на языке Паскаль. uses crt; var k,b,x1,y1,x2,y2:extended; d:boolean; function potrprm(k,b,x1,y1,x2,y2:extended):boolean; var f1,f2:extended; begin f1:=k*x1+b-y1; f2:=k*x2+b-y2; if f1*f2<=0 then potrprm:=true else potrprm:=false; end; begin clrscr; write('введите k и b для уравнения') ; readln(k,b); write('введите координаты начала отрезка(x1, y1):'); readln(x1,y1); write('введите координаты конца отрезка(x2, y2):'); readln(x2,y2); d:=potrprm(k,b,x1,y1,x2,y2); if d then writeln('отрезок и прямая пересекаются') else writeln('отрезок и прямая не пересекаются'); readkey; end. Если уравнение прямой задана парметрически в виде ax+by+c = 0, то в строке function potrprm(k,b,x1,y1,x2,y2:extended):boolean; вместо k и b нужно ввести параметры a,b,c, т.е. function potrprm(a,b,c,x1,y1,x2,y2: extended):boolean; и строки f1:=k*x1+b-y1; f2:=k*x2+b-y2; нужно заменить на строки f1:=a*x1+b*y1+c; f2:=a*x2+b*y2+c; Вычисление площади многоугольника если заданы вершин многоугольника. координаты Решения нескольких из представленных ниже задач основаны на нахождении площади многоугольника и полярного угла точки. Поэтому имеет смысл привести здесь формулы для их вычисления. Пусть (х1, y1), (x2, у2), ..., (хN,уN) — координаты вершин заданного многоугольника в порядке обхода по или против часовой стрелки. Тогда его ориентированная площадь S равна S = 1 (x1y2 – x2y1+ 2 x2y3 – x3y2 +…+ xNy1 – x1yn) При использовании этой формулы не случится ничего страшного, если какие-то две вершины «многоугольника» будут совпадать или какие-то три — лежать на одной прямой. Если координаты вершин были заданы в порядке обхода против часовой стрелки, то число S, вычисленное противном обычной по случае этой оно геометрической формуле, будет получится отрицательным, площади нам положительным. и для необходимо В получения взять его абсолютное значение. Далее покажем применение этой формулы для нахождения площади многоугольника, у которого заданы координаты вершин в порядке их обхода по часовой или против часовой стрелки. uses crt; type b=record x,y:extended; end; var a:array[1..200] of b; s:extended; i,n:integer; begin clrscr; assign(input,'input.txt'); reset(input); read(n); for i:=1 to n do read(a[i].x,a[i].y); close(input); a[n+1].x:=a[1].x;a[n+1].y:=a[1].y; s:=0; for i:=1 to n do s:=s+(a[i].x*a[i+1].y-a[i+1].x*a[i].y); s:=abs(s/2); writeln(s:4:2); readkey; end. Координаты вершин в массиве а в виде списка с двумя полями. Координаты вершин многоугольника считывается из файла input.txt. Для удобства обхода многоугольника в массиве а вводится n+1 элемент, значение которого равно значению первого элемента массива. Полярный угол точки. Построение вогнутой оболочки. Полярный угол точки (х, у) может, конечно, быть вычислен через арктангенс. Представленная ниже функция pu вычисляет полярный угол данной точка с координатами (x ,y). Функция возвращает число из диапазона [0,2pi] Function pu (x,y:extended):extended; Var p:extended; begin If х=0 Then p:=pi/2+pi*Ord(y<0) Else Begin p:=ArcTan(y/x)+pi; If x>0 Then If y>=0 Then p:=p-pi Else p:=p+pi; End; pu:=p; end; Однако сопроцессоры семейства 80х87 имеют для этого спе- циальную команду FPATAN, использовать которую существенно проще и эффективнее. Представленная ниже функция угол точки с координатами (х,у), вычисляет полярный возвращая число из диапазона [-pi, pi]. Function Pu (Const x, у : Extended) : Extended; Assembler; { Для Delphi эта директива не нужна } Asm FLD у; FLD x; FPATAN; FWAIT; End; Далее на примере задачи построения вогнутой оболочки покажем применение этой функции. Пусть на плоскости заданы множество точек N со своими координатами (х1, y1), (x2, у2), ..., (хN,уN). Нужно обойти эти точки по или против часовой стрелки или построить вогнутую оболочку. Идея решения задачи состоит в том, что в начале находятся координаты «центра масс» данных точек. Затем для каждой точки относительно центра масс определяются полярные углы. После чего нужно произвести сортировку по полярным углам или по возрастанию или по убыванию. Вместе с сортировкой полярных углов сортировать и массив, где хранятся номера заданных точек. Пример решения задачи представлен ниже. {$A+,B-,D+,E+,F-,G-,I+,L+,N+,O-,P-,Q-,R-,S+,T-,V+,X+} {$M 64384,0,655360} uses crt,graph; type b=record x,y:extended; no:integer; pu:extended; end; var a:array[1..1500] of b; i,n,j,gr,z:integer; cx,cy,f:extended; Function pu (x,y:extended):extended; Var p:extended; begin If x=0 Then p:=pi/2+pi*Ord(y<0) Else Begin p:=ArcTan(y/x)+pi; If x>0 Then If y>=0 Then p:=p-pi Else p:=p+pi; End; pu:=p; end; procedure formmas; begin write(' введите количество точек на плоскости:'); readln(n); cx:=0;cy:=0; for i:=1 to n do begin a[i].x:=random(600)+30; a[i].y:=random(440)+30; a[i].no:=i; cx:=cx+a[i].x; cy:=cy+a[i].y; end; cx:=cx/n;cy:=cy/n; end; procedure posrpixel; begin initgraph(gr,gr,'c:\tp7\bgi'); for i:=1 to n do putpixel(trunc(a[i].x),trunc(a[i].y),7); circle(trunc(cx),trunc(cy),2); end; procedure poiskpu; begin for i:=1 to n do a[i].pu:=pu(cx-a[i].x,cy-a[i].y); end; procedure postwogob; begin for i:=1 to n-1 do for j:=i+1 to n do if a[i].pu>a[j].pu then begin f:=a[i].pu;a[i].pu:=a[j].pu;a[j].pu:=f; z:=a[i].no;a[i].no:=a[j].no;a[j].no:=z; end; end; procedure posrline; begin a[n+1].x:= a[1].x;a[n+1].y:=a[1].x; a[n+1].no:= a[1].no; for i:=1 to n do line(trunc(a[a[i].no].x),trunc(a[a[i].no].y),trunc(a[a[i+1].no]. x),trunc(a[a[i+1].no].y)); end; begin clrscr; formmas; posrpixel; poiskpu; postwogob; posrline; readkey; end. Построение выпуклой оболочки. Выпуклой оболочкой конечного числа точек N называется наименьший, выпуклый многоугольник, содержащий все точки из N (некоторые из точек могут быть внутри многоугольника, некоторые на его сторонах, а некоторые будут его вершинами). Наглядно это можно представить себе так: в точках N вбиваются гвозди, на которые натянута резинка, охватывающая их все – эта резинка и будет выпуклой оболочкой множества гвоздей. Вычисление выпуклой оболочки важно не только само по себе, но и как промежуточный этап для многих задач вычислительной геометрии. Например задача о наиболее удаленных точках. Дано множество из N точек. Нужно выбрать пару максимально удаленных друг от друга точек. Можно доказать, что эти точки будут вершинами выпуклой оболочки. А для выпуклого N-угольника можно найти две наиболее удаленные вершины за время О(N). Идея заключается в том, что зажимаем его между двумя параллельными вершинами и вращаем. Тем самым можно найти две наиболее удалённые среди N точек за время О(N). Рассмотрим один из способов получения выпуклого многоугольника называемый просмотром Грэхема. Просмотр Грэхема использует стек Res, в котором хранятся точки, являющиеся кандидатами в выпуклую оболочку. Каждая точка исходного множества из N точек в некоторый момент помещается в стек; если она не является вершиной выпуклого выпуклой оболочки, то через некоторое время точка будет удалена из стека, а если является – то останется. В момент окончания работы алгоритма в стеке Res находятся в точности все вершины выпуклой оболочки в порядке обхода против часовой стрелки. При обходе будем использовать определение векторного произведения, на примере определения правого поворота. W = (P1 – P0) x (P2 – P0) = (X1 – X0) (Y2-Y0) Y0). –(X2 - X0) (Y1 – Сохраняем направление последней найденной стороны и выбираем точку с ближайшим (в положительную сторону) направлением луча. Пример реализации программы имеет следующий вид. {$A+,B-,D+,E+,F-,G-,I+,L+,N+,O-,P-,Q-,R-,S+,T-,V+,X+} {$M 64384,0,655360} uses crt,graph; Const MaxN=5100; Var x,y: Array[1..MaxN] Of word; Angle: Array[1..MaxN] Of real; Res: Array[0..MaxN+1] Of Integer; N,i, M,gr1,gr2,kol: Integer; cx,cy:real; Procedure Init;{ Инициализация } Var i: Integer; Begin write('введите количество точек'); Read(N); For i:=1 To N Do Begin X[i]:=random(600)+10; Y[i]:=random(440)+20; cx:=cx+x[i];cy:=cy+y[i]; Res[i]:=i; End; cx:=cx/n;cy:=cy/n; M:=N; End; Procedure Sort(K, L: Integer);{ Сортировка} Var i, j, P: Integer; T, X: real; Begin i:=K; j:=L; X:=Angle[(K+L) Div 2]; repeat while (X<Angle[i]) do Inc(i); while (Angle[j]<X) do Dec(j); if i<=j then begin T:=Angle[i]; Angle[i]:=Angle[j]; Angle[j]:=T; P:=Res[i]; Res[i]:=Res[j]; Res[j]:=P; Inc(i); Dec(j); end; until I>j; If K<j Then Sort(K,j); If L>i Then Sort(i,L); End; Function ProvRight(x1,y1,x2,y2,x3,y3: real): Boolean;{ Проверка угла на то, что правый} Begin ProvRight:=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1)>=0; End; Function pu (x,y:real):real; Var p:real; begin If x=0 Then p:=pi/2+pi*Ord(y<0) Else Begin p:=ArcTan(y/x)+pi; If x>0 Then If y>=0 Then p:=p-pi Else p:=p+pi; End; pu:=p; end; Procedure OprAngle; { Определение полярных углов } Var i: Integer; Begin x[n+1]:=x[1];y[n+1]:=y[1]; For i:=1 To N Do Angle[i]:=(pu(cx-x[i],cy-y[i])); End; Procedure Over(k: Integer); Var i: Integer; Begin Dec(M); For i:=k To M Do Res[i]:=Res[i+1]; End; Procedure Exclud;{ Исключение правых углов } Var i: Integer; CanGo: Boolean; Begin i:=1; CanGo:= True; While (i<>1) Or (CanGo) Do Begin Res[0]:=Res[M]; Res[M+1]:= Res[1]; If i=2 Then CanGo:= False; If ProvRight(x[Res[i-1]],y[Res[i1]],x[Res[i]],y[Res[i]],x[Res[i+1]],y[Res[i+1]]) Then Begin Over(i); i:=(i-2+M) Mod M+1; CanGo:=True; End Else i:=i Mod M+1; End; End; procedure strojit; begin initgraph(gr1,gr2,'c:\tp7\bgi'); res[kol+1]:=res[1]; x[n+1]:=x[1]; y[n+1]:=y[1]; for i:=1 to n do putpixel(x[i],y[i],2); for i:=1 to m do begin line(x[res[i]],y[res[i]],x[res[i+1]],y[res[i+1]]); end; end; BEGIN clrscr; Init; OprAngle; Sort(1, N); Exclud; strojit; readkey; END. В программе представлено графическое описание выпуклой оболочки на экране монитора, где координата Х лежит в пределах от 0 до 640, а Y от 0 до 480. Формирование точек на экране производится функцией random. Во время генерирования точек определяется «центр масс» многоугольника. Эти действия проводятся в процедуре init; В процедуре OprAngle относительно центра масс находятся полярные углы точек многоугольника. Процедура с параметрами Sort (используется рекурсивная быстрая сортировка) сортирует полярные углы с одновременной сортировкой номеров точек данного многоугольника. Процедура ProvRight проверяет угол на то, что он правый. Процедура Over запоминает те, вершины, которые будут вершинами выпуклого многоугольника. И наконец процедура Exclud исключает правые углы (самая основная процедура). Следующая программа тоже есть представление просмотра Грэхема реализованная учеником Республиканского лицея интерната г.Чебоксары Ивановым Максимом. uses crt,graph; var f:text; r:array[1..4000,1..2]of integer; a,b,c,kol:longint; r2:array[1..4000]of integer; n,l,er,er2,la,al,la2:longint; i:integer; procedure enter; begin fillchar(r2,sizeof(r2),0); fillchar(r,sizeof(r),0); write('введите количество точек:'); readln(n); for a:=1 to n do begin r[a,1]:=random(540)+50; r[a,2]:=random(380)+50; end; end; procedure naim(var naim:longint); var cc:integer; begin cc:=1; for a:=2 to n do begin if r[a,2]<r[cc,2] then cc:=a; if r[a,2]=r[cc,2] then if r[a,1]<r[cc,1] then cc:=a; end; naim:=cc; end; function strel(x1,y1,x2,y2,x3,y3:longint):integer; var bol:longint; begin bol:=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1); if bol<0 then strel:=-1 else if bol>0 then strel:=1 else strel:=0; end; procedure polugl(x,y:integer); var kolb:integer; kodd:integer; begin kolb:=r[l,1];r[l,1]:=r[1,1];r[1,1]:=kolb; kolb:=r[l,2];r[l,2]:=r[1,2];r[1,2]:=kolb; for a:=2 to n do for b:=a+1 to n do begin kodd:=strel(x,y,r[a,1],r[a,2],r[b,1],r[b,2]); if kodd=-1 then begin kolb:=r[a,1];r[a,1]:=r[b,1];r[b,1]:=kolb; kolb:=r[a,2];r[a,2]:=r[b,2];r[b,2]:=kolb; end; end; end; procedure poisk; begin naim(l); polugl(r[l,1],r[l,2]); kol:=3; r2[1]:=1;r2[2]:=2;r2[3]:=3; for a:=4 to n do begin er:=strel(r[r2[kol-1],1],r[r2[kol-1],2],r[r2[kol],1], r[r2[kol],2],r[a,1],r[a,2]); if (er=1) then begin r2[kol+1]:=a; kol:=kol+1; end; if er=-1 then begin r2[kol]:=a; repeat er2:=strel(r[r2[kol-2],1],r[r2[kol-2],2],r[r2[kol1],1], r[r2[kol-1],2],r[r2[kol],1],r[r2[kol],2]); if er2=-1 then begin kol:=kol-1; r2[kol]:=r2[kol+1]; r2[kol+1]:=0; end; until er2<>-1; end; end; r2[kol+1]:=1;kol:=kol+1; end; procedure risuem; begin i:=0;initgraph(i,i,'C:\TP7\BGI'); for a:=1 to n do putpixel(r[a,1],r[a,2],15); for a:=2 to kol do line(r[r2[a],1],r[r2[a],2],r[r2[a-1],1],r[r2[a-1],2]); end; begin clrscr;randomize; enter; poisk; risuem; readkey; closegraph; end. Следующая программа представляет собой метод Джарвиса. USES CRT,graph; Var First,Cur,Min,i,N,M : longint; z : Real; k,l:integer; A:ARRAY[1..2,1..10001] OF INTEGER; IND:ARRAY[1..10001] OF INTEGER; Function Det(a,b,c,d : Real) : Real; Begin Det:=a*d-b*c; End; Function P3Square(P1x,p1y,P2x,p2y,P3x,p3y : integer) : Real; Begin P3Square:=Det(P1x-P3x,P2x-P3x,P1y-P3y,P2y-P3y)/2; End; Function BtwS(x,a,b : Real) : Boolean; Begin BtwS:=(a<=x) And (x<=b) Or (a>=x) And (x>=b); End; { Аналогично предыдущей функции, но проверка строгая } Function PBtwS(x1,y1,x2,y2,x3,y3:integer) : Boolean; Begin PBtwS:=BtwS(x1,x2,x3) And BtwS(y1,y2,y3); End; Begin CLRSCR; { Метод Джарвиса } { Находим самую нижнюю из самых левых точек с минимальным номером } WRITE('ВВЕДИТЕ КОЛИЧЕСТВО ТОЧЕК:');READLN(N); initgraph(k,l,'c:\tp7\bgi'); FOR I:=1 TO N DO begin a[1,i]:=random(600); a[2,i]:=random(400); putpixel(a[1,i],a[2,i],7); end; First:=1; For i:=1 To N Do If (A[1,i]>=A[1,First]) And (A[2,i]<=A[2,First]) Then First:=i; { "Заворачиваем подарок" } M:=1; Cur:=First; Repeat Ind[M]:=Cur; Inc(M); Min:=Cur; { Цикл в противоположном направлении и строгие неравенства! } For i:=N DownTo 1 Do Begin If (A[1,i]=A[1,Cur]) And (A[2,i]=A[2,Cur]) Then Continue; z:=P3Square(A[1,Cur],A[2,Cur],A[1,i],A[2,i],A[1,Min],A[2,Min]); If (Min=Cur) Or (z>0) Or (z=0) And PBtwS(A[1,Min],A[2,Min],A[1,Cur],A[2,Cur],A[1,i],A[2,i]) Then Min:=i; End; Cur:=Min; Until Cur=First; for i:=1 to m-2 do line(a[1,ind[i]],a[2,ind[i]],a[1,ind[i+1]],a[2,ind[i+1]]); line(a[1,ind[1]],a[2,ind[1]],a[1,ind[m-1]],a[2,ind[m-1]]); readkey; closegraph; FOR I:=1 TO M-1 DO WRITE(IND[I],' '); READKEY; End. Алгоритм построения выпуклой оболочки uses graph,crt; const filein = 'input.txt'; {получаем данные из указанного файла} var a: array [1..100] of record x,y: integer; end; {запись для хранения множества точек} n: word; {кол-во полученных точек} co: array [1..100] of byte; {стек; по окончании работы алгоритма в нем будут "лежать" номера точек, входящих в выпуклую оболочку} s: byte; {"указывает" на вершину стека} i: integer; {для циклов} label m; {процедура получения данных из файла} procedure inp; begin close(input); assign(input, filein); reset(input); n:=0; while not eof do begin inc(n); readln(a[n].x, a[n].y); end; close(input); end; {процедура сортировки полученных точек по x-овой координате} {метод сортировки - QuickSort} procedure sort(l,p: integer); var i,j,revx,revy,x: integer; label loop; begin x:=a[(l+p) div 2].x; i:=l; j:=p; loop: while a[i].x<x do inc(i); while a[j].x>x do dec(j); if i<=j then begin revx := a[i].x; revy := a[i].y; a[i].x := a[j].x; a[i].y := a[j].y; a[j].x := revx; a[j].y := revy; inc(i); dec(j); goto loop; end else begin if l<j then sort(l,j); if i<p then sort(i,p); end; end; {понкция, возвращающая знак векторного произведения 2-х векторов} function vectz(x1,y1,x2,y2,x3,y3,x4,y4: real): shortint; begin if (x2-x1)*(y4-y3)-(x4-x3)*(y2-y1) > 0 then vectz:=1 else if (x2-x1)*(y4-y3)-(x4-x3)*(y2-y1) < 0 then vectz:=-1 else vectz:=0; end; {процедура, "рисующая" на дисплее все точки и показыкающая выпуклую оболочку} procedure show; var gd,gm: integer; const k = 2; begin detectgraph(gd,gm); initgraph(gd,gm,''); for i:=1 to s-1 do line(a[co[i]].x*k, a[co[i]].y*k, a[co[i+1]].x*k, a[co[i+1]].y*k); line(a[co[s]].x*k, a[co[s]].y*k, a[co[1]].x*k, a[co[1]].y*k); setcolor(red); for i:=1 to n do circle(a[i].x*k, a[i].y*k, 2); readkey; closegraph; end; {main} BEGIN inp; {получаем данные} sort(1,n); {сортируем} fillchar(co, sizeof(co), 0); {"очищаем" стек} s:=1; co[s]:=1; {"кладем" в стек 1-ю точку, кот-я заведомо принадлежит выпуклой оболочке} {"ищим" следующую точку} for i:=2 to n do if vectz(a[1].x, a[1].y, a[n].x, a[n].y, a[1].x, a[1].y, a[i].x, a[i].y)>0 then break; s:=2; co[s]:=i; {"кладем" в стек} {находим точки выпуклой оболочки находящиеся над вектором 1,n} while co[s]<>n do begin m: for i:=co[s]+1 to n do if vectz(a[1].x, a[1].y, a[n].x, a[n].y, a[1].x, a[1].y, a[i].x, a[i].y) > 0 then break; while vectz(a[co[s-1]].x, a[co[s-1]].y, a[co[s]].x, a[co[s]].y, a[co[s]].x, a[co[s]].y, a[i].x, a[i].y) > 0 do begin if s=2 then {проверка; если 2-я выбранная нами точка не принадлежит выпуклой оболочке, то выберем ее заново} begin for i:=co[s]+1 to n do if vectz(a[1].x, a[1].y, a[n].x, a[n].y, a[1].x, a[1].y, a[i].x, a[i].y)>0 then break; co[s]:=i; goto m; end else {а если она не 2-я, то просто "выкинем" её из стека} begin co[s]:=0; dec(s); end; end; {ну а "совсем иначе", стало быть она принадлежит выпуклой оболочке; записываем ее в стек} inc(s); co[s]:=i; end; {находим точки выпуклой оболочки находящиеся под вектором 1,n} while co[s]<>1 do begin for i:=co[s]-1 downto 1 do if vectz(a[1].x, a[1].y, a[n].x, a[n].y, a[1].x, a[1].y, a[i].x, a[i].y) < 0 then break; if i=1 then break; while vectz(a[co[s-1]].x, a[co[s-1]].y, a[co[s]].x, a[co[s]].y, a[co[s]].x, a[co[s]].y, a[i].x, a[i].y) > 0 do begin co[s]:=0; dec(s); end; inc(s); co[s]:=i; end; {рисуем =))} show; END Примеры решения некоторых задач. Задача 103. Полусортировка Источник: Летние сборы - 2001 Максимальный балл: 30 Ограничение по времени: 10 секунд Тесты Условие В ориентированном графе из каждой вершины выходит не более k (1<=k<=20) дуг. Требуется расположить вершины графа на прямой слева направо, так, чтобы в вершину входило не более k дуг из вершин, расположенных левее нее. Две вершины нельзя помещать в одну точку. Входные данные Во входном файле задано число k, число вершин графа N (1<=N<=10000) а затем N строк по k чисел в каждой, задающих номера вершин, в которую идут ребра из данной или -1, если соответствующего ребра нет. Выходные данные В выходной файл вывести N чисел: первое из них задает номер самой левой вершины, вторая - следующей за ней и так далее. Пример входного файла 2 3 2 3 3 1 -1 -1 Пример выходного файла 3 2 1 Разбор Будем строить ответ с конца. Заведем массив data, который будет каждой вершине сопоставлять количество ребер, идущих в нее из еще не вошедших в ответ вершин (т.е. из тех вершин, которые, если мы на данном шаге поместим ее в ответ, окажутся левее нее). Тогда условие требует, чтобы в момент добавления вершины i в ответ выполнялось условие data[i]<=k. Применим жадный алгоритм. Будем на каждом шаге добавлять в ответ вершину с минимальным значением data, а затем обновлять массив data для тех вершин, в которые шли ребра из данной. При реализации data как обычного массива мы будем на каждом шаге искать минимальный элемент за время O(N), а затем обновлять значения data за O(k), так как из вершины выходит не более k ребер. Общее время работы за N шагов будет O(N2). Если же реализовать массив data с помощью кучи, то мы будем искать минимальный элемент за время O(log N) и обновлять массив data за время O(klog N); общее время работы будет O(NklogN). Решение program HalfSort; {(c) Semyon Dyatlov, 2002} const maxn = 10000; maxk = 20; var n, k, m : integer; e : array [1..maxn, 1..maxk] of integer; data, h, h1, ans : array [1..maxn] of integer; { m - heap size h - the heap e - the graph data - number of entering edges ans - the answer } procedure Load; var i, j : integer; begin assign(input,'input.txt'); reset(input); assign(output,'output.txt'); rewrite(output); fillchar(data,sizeof(data),0); read(k,n); for i:=1 to n do for j:=1 to k do begin read(e[i,j]); if e[i,j]<>-1 then inc(data[e[i,j]]); end; end; procedure swap(a, b : integer); var t : integer; begin t := h[a]; h[a] := h[b]; h[b] := t; h1[h[a]] := a; h1[h[b]] := b; end; procedure heapify(a : integer); var l : integer; begin l := a; if (2*a<=m) and (data[h[2*a]]<data[h[l]]) then l := 2*a; if (2*a+1<=m) and (data[h[2*a+1]]<data[h[l]]) then l := 2*a+1; if l<>a then begin swap(l,a); heapify(l); end; end; procedure Solve; var i, j, t, c : integer; begin m := n; for i:=1 to m do begin h[i] := i; h1[i] := i; end; for i:=m downto 1 do heapify(i); for i:=1 to n do begin {Extract minimum} c := h[1]; swap(1,m); dec(m); heapify(1); h1[c] := 0; ans[n+1-i] := c; {Update data} for j:=1 to k do if (e[c,j]<>-1) and (h1[e[c,j]]<>0) then begin dec(data[e[c,j]]); t := h1[e[c,j]]; while (t>1) and (data[h[t div 2]]>data[h[t]]) do begin swap(t,t div 2); t := t div 2; end; end; end; end; procedure Save; var i : integer; begin for i:=1 to n do write(ans[i],' '); writeln; close(input); close(output); end; begin Load; Solve; Save; end. Задача 104. Сотовая связь в деревне Источник: Летние сборы - 2001 Максимальный балл: 40 Ограничение по времени: 15 секунд Тесты Условие Компания Малоярославец-телеком решила распространить свое влияние на близлежащие населенные пункты. Исследования показали, что все сколько-нибудь значимые поселения располагаются вдоль прямолинейного участка железной дороги Малоярославец-Калуга. Для обслуживания абонентов было решено установить K передатчиков. При работе сотовый телефон подключается к ближайшему передатчику. Исследования показали, что недовольство связью прямо пропорционально расстоянию от абонента до ближайшего передатчика. Таким образом, суммарное недовольство населенного пункта это произведение количества жителей в нем на недовольство каждого. Требуется разработать программу, которая минимизирует максимальное недовольство среди населенных пунктов. Входные данные Входной файл содержит следующие три строки. Первая строка содержит число N (1<=N<=10000) - количество населенных пунктов и число K (1<=К<=N) - количество передатчиков. Вторая строка содержит N чисел P1, P2, ... , PN, (0<=Pi<=10000), каждое из которых задает расстояние от Малоярославца до соответствующего населенного пункта. Третья строка содержит N чисел - количества жителей в каждом населенном пункте (натуральные числа, не превосходящие 100). Выходные данные Выходной файл должен содержать K чисел - расстояния от передатчиков до Малоярославца (с точностью четыре знака после запятой), упорядоченные по неубыванию. Пример входного файла 2 1 1 2 1 1 Пример выходного файла 1.5 Разбор Эта задача решается бинарным поиском по максимальному недовольству. Действительно, если мы можем для числа bound определить, можно ли расставить не более K станций так, чтобы максимальное недовольство не превышало bound, то бинарным поиском по bound мы найдем ответ. Теперь, пусть нам дано конкретное число bound. Чтобы максимальное недовольство от деревни i не превышало bound, хотя бы одна станция должна попасть на отрезок [p[i]bound/c[i], p[i]+bound/c[i]], где c - массив, задающий количества жителей в деревнях. Применим жадный алгоритм. Отсортируем отрезки деревень по возрастанию левого конца и пройдем по ним слева направо, по пути расставляя станции. Если очередной отрезок не будет содержать последнюю станцию, то рассмотрим два случая: Левый конец отрезка правее станции. Создадим новую станцию и поместим ее на правый конец данного отрезка. Правый конец отрезка левее станции. Переместим последнюю станцию на правый конец данного отрезка. Мы не будем доказывать, почему в данном случае жадный алгоритм выдаст решение с минимальным количеством станций. Заметим только, что если мы рассмотрим отрезки, на правых концах которых расположены станции, то никакие два из этих отрезков не будут пересекаться. Каждый шаг бинарного поиска требует O(NlogN) времени для выполнения сортировки отрезков. Решение program Village_Phones; {(c) Semyon Dyatlov, 2003} {$N+} const maxn = 10000; eps = 1e-8; var n, k : integer; p : array [1 .. maxn] of integer; c : array [1 .. maxn] of byte; ans : array [1 .. maxn] of extended; procedure Load; var i : integer; begin assign(input,'phone.in'); reset(input); assign(output,'phone.out'); rewrite(output); read(n,k); for i:=1 to n do read(p[i]); for i:=1 to n do read(c[i]); end; function ok(bound : extended) : boolean; var i, t, m : integer; order : array [1..maxn*2] of integer; procedure swap(a, b : integer); var t : integer; begin t := order[a]; order[a] := order[b]; order[b] := t; end; function greater(a, b : integer) : boolean; var pa, pb : extended; begin pa := p[order[a]] - bound / c[order[a]]; pb := p[order[b]] - bound / c[order[b]]; greater := pa > pb+eps; end; procedure heapify(a : integer); var l : integer; begin l := a; if (2*a<=m) and greater(2*a,l) then l := 2*a; if (2*a+1<=m) and greater(2*a+1,l) then l := 2*a+1; if l<>a then begin swap(l,a); heapify(l); end; end; begin for i:=1 to n do order[i] := i; m := n; for i:=m downto 1 do heapify(i); while m>1 do begin swap(1,m); dec(m); heapify(1); end; t := 1; ans[1] := p[order[1]]+bound/c[order[1]]; for i:=2 to n do if (p[order[i]]-bound/c[order[i]] > ans[t]+eps) then begin inc(t); ans[t] := p[order[i]]+bound/c[order[i]]; end else if (p[order[i]]+bound/c[order[i]] < ans[t]-eps) then ans[t] := p[order[i]]+bound/c[order[i]]; ok := t<=k; end; procedure Solve; var l, r, m : extended; begin l := 0; r := 1000000; while r-l>1e-6 do begin m := (l+r)/2; if ok(m) then r := m else l := m; end; ok(r); end; procedure Save; var i : integer; begin for i:=1 to k do writeln(ans[i]:0:4); close(input); close(output); end; begin Load; Solve; Save; end. Задача 105. Дешифрование системы Рабина Источник: Летние сборы - 2002 Максимальный балл: 40 Тесты Условие Криптосистема Рабина использует в качестве закрытого ключа тройку (n, p, q), где p и q простые и n=pq. Зашифрованный текст получается из исходного по формуле c=x 2 mod n. Задача дешифрования сводится к извлечению корня по простому модулю и затем применению китайской теоремы об остатках. Решите первую из этих задач - найдите корни уравнения x2 mod p=a, где p простое. Входные данные Входной файл содержит числа p и a, гарантируется, что существует хотя бы одно решение. Выходные данные В выходной файл выведите все такие x (0<=x<n), что x2 mod p=a, по одному на строке, в возрастающем порядке. Пример входного файла 11 3 Пример выходного файла 5 6 Разбор Заметим, что нахождение квадратного корня из a по модулю p равносильно разложению на множители многочлена x2-a в поле вычетов по модулю p. Поэтому мы можем применить алгоритм Берлекампа для разложения многочленов на множители в полях вычетов по простому модулю. Для данной задачи рассмотрим два случая: Случай 1. p+1 делится на 4. Известно, что если a - квадрат некоторого числа по модулю p, то a(p-1)/2 mod p=1. Отсюда a(p+1)/2 mod p=a, и, если число (p+1)/2 четное, то один из искомых корней - a(p+1)/4. Случай 2. p+1 не делится на 4. Выберем некоторое случайное натуральное число k. Возведем многочлен x+k в степень (p-1)/2 по модулю многочлена x2-a в поле вычетов по модулю p. Получим некоторый многочлен mx-n. Если m не равно нулю (как оказывается, чисел k, при которых выполняется это условие, не меньше половины), то искомый корень равен nm-1 mod p. Решение program Kill_Rabin; const maxl = 200; type TLong = array [1..maxl] of integer; var p,a,t,r,zero,px,pxx : TLong; lastmod : integer; procedure ReadLong(var x : TLong); var s : string; i : integer; begin readln(s); fillchar(x,sizeof(x),0); for i:=length(s) downto 1 do x[i] := ord(s[length(s)+1-i]) - ord('0'); end; procedure IncLong(var x : TLong); var i : integer; begin i := 1; while (x[i]=9) do begin x[i] := 0; inc(i); end; inc(x[i]); end; procedure DecLong(var x : TLong); var i : integer; begin i := 1; while (x[i]=0) do begin x[i] := 9; inc(i); end; dec(x[i]); end; procedure DivLong(var x : TLong; y : byte); var i,c,o : integer; begin o := 0; for i:=maxl downto 1 do begin c := o * 10 + x[i]; x[i] := c div y; o := c mod y; end; lastmod := o; end; function LongCompLong(x, y : TLong) : ShortInt; var i : integer; begin for i:=maxl downto 1 do begin if x[i] > y[i] then LongCompLong := 1; if x[i] < y[i] then LongCompLong := -1; if x[i] <> y[i] then exit; end; LongCompLong := 0; end; procedure MulLong(var x : TLong; y : byte); var i,o,c : integer; begin o := 0; for i:=1 to maxl do begin c := o + x[i] * y; x[i] := c mod 10; o := c div 10; end; end; procedure LongAddLong(x, y : TLong; var z : TLong); var i,o,c : integer; begin o := 0; for i:=1 to maxl do begin c := o + x[i] + y[i]; z[i] := c mod 10; o := c div 10; end; end; procedure ShiftLong(var x : TLong; y : Integer); var i : integer; begin for i:=maxl downto 1 do if i>y then x[i] := x[i-y] else x[i] := 0; end; procedure LongMulLong(x, y : TLong; var z : TLong); var i : integer; t : TLong; begin t := y; fillchar(z,sizeof(z),0); for i:=1 to maxl do begin y := t; MulLong(y,x[i]); LongAddLong(z,y,z); ShiftLong(t,1); end; end; procedure LongSubLong(x, y : TLong; var z : TLong); var i,o,c : integer; begin o := 0; for i:=1 to maxl do begin c := x[i] - y[i] - o; o := ord(c < 0); z[i] := c + o * 10; end; end; procedure LongModLong(x, y : TLong; var z : TLong); var i : integer; t : TLong; begin z := x; for i:=maxl downto 1 do begin t := y; ShiftLong(t,i-1); while LongCompLong(t,z)<>1 do LongSubLong(z,t,z); end; end; procedure LongPowLong(x, y : TLong; var z : TLong); begin z := zero; z[1] := 1; while LongCompLong(y,zero)=1 do begin DivLong(y,2); if lastmod=1 then LongMulLong(z,x,z); LongModLong(z,p,z); LongMulLong(x,x,x); LongModLong(x,p,x); end; end; procedure Load; begin assign(input,'rabin.'+paramstr(1)); reset(input); assign(output,'sqrt.'+paramstr(1)); rewrite(output); ReadLong(p); ReadLong(a); fillchar(zero,sizeof(zero),0); end; procedure MulX(x1, x2, y1, y2 : TLong; var z1, z2 : TLong); var a_,b_,c_,t_ : TLong; begin LongMulLong(x1,y1,a_); LongMulLong(x2,y2,c_); LongMulLong(x1,y2,b_); LongMulLong(x2,y1,t_); LongAddLong(b_,t_,b_); z1 := b_; LongMulLong(a_,a,z2); LongAddLong(z2,c_,z2); LongModLong(z1,p,z1); LongModLong(z2,p,z2); end; procedure Solve; var b : TLong; ca,cb,da,db : TLong; begin b := p; IncLong(b); DivLong(b,4); px := p; DecLong(px); pxx := px; DecLong(pxx); if lastmod=0 then begin LongPowLong(a,b,r); end else begin randomize; repeat ca := zero; cb := zero; da := zero; db := zero; ca[1] := 1; cb[1] := random(10); cb[2] := random(10); db[1] := 1; b := p; DecLong(b); DivLong(b,2); while (LongCompLong(b,zero)=1) do begin DivLong(b,2); if lastmod=1 then MulX(ca,cb,da,db,da,db); MulX(ca,cb,ca,cb,ca,cb); end; if LongCompLong(db,zero)=0 then db := px else DecLong(db); if LongCompLong(da,zero)=1 then break; until false; LongSubLong(p,db,db); LongModLong(db,p,db); LongPowLong(da,pxx,r); LongMulLong(r,db,r); LongModLong(r,p,r); end; end; procedure Save; var i,l : integer; rr,t : TLong; begin LongSubLong(p,r,rr); if LongCompLong(r,rr)=1 then begin t := r; r := rr; rr := t; end; for l:=maxl downto 1 do if r[l]<>0 then break; for i:=l downto 1 do write(chr(r[i] + ord('0'))); writeln; for l:=maxl downto 1 do if rr[l]<>0 then break; for i:=l downto 1 do write(chr(rr[i] + ord('0'))); writeln; close(input); close(output); end; begin Load; Solve; Save; end. Задача 106. Взлом RSA Источник: Летние сборы - 2002 Максимальный балл: 40 Тесты Условие Будучи в отъезде, Витя решил послать оставшимся трем членам жюри сверхсекретное сообщение. Зная изобретательность отдельных участников сборов, которые могли бы его перехватить, он решил зашифровать свое сообщение с использованием криптосистемы RSA. Для этого каждый из трех оставшихся членов жюри выбрал для себя модуль Ni (Ni=piqi, pi, qi - различные простые, здесь и далее i принимает значения 1,2,3). Сообщение от Вити представляло собой число x, меньшее каждого из модулей Ni. Кроме того, члены жюри выбрали для себя открытые ключи ei=3 и вычислили закрытые ключи di. Тем самым, в результате шифрования были получены зашифрованные сообщения yi=xei mod Ni. Как Витя и опасался, один из участиников сборов по имени Петя умудрился перехватить зашифрованные сообщения yi. Кроме того, в одной из умных книжек Петя читал, что знания величин Ni и yi достаточно, чтобы восстановить исходное сообщение за разумное время. Помогите ему в этом: найдите исходное сообщение x. Входные данные Входной файл содержит 6 чисел: N1, N2, N3, y1, y2, y3 в указанном порядке. Выходные данные В выходной файл выведите число x. Пример входного файла 161 187 247 55 29 216 Пример выходного файла 6 Разбор Применим здесь китайскую теорему об остатках. По условию мы знаем x 3 mod Ni для i=1,2,3; отсюда мы можем восстановить значение x3 по модулю M=N1N2N3. Но x<Ni; значит, x3<M, и значение x3 по модулю M будет равно x3. Таким образом мы узнаем x3, и останется лишь извлечь из x3 кубический корень (например, бинарным поиском). Задача 107. Взлом системы Эль-Гамаля Источник: Летние сборы - 2002 Максимальный балл: 30 Тесты Условие Криптографическая система Эль-Гамаля существенно использует сложность задачи вычисления дискретного логарифма. Вам дано простое число p, генератор a группы Zp*, и число b. Найдите indab, т. е. такое x, что ax mod p=b. Входные данные Входной файл содержит числа p, a и b. Выходные данные Выведите в выходной файл число x. Пример входного файла 7 3 2 Пример выходного файла 2 Разбор Пусть m=1+[p], где [x] - целая часть числа x. Тогда, разделив искомое x на m с остатком, получим, что a=my+z, где 0<=y,z<m. Тогда условие преобразуется в amy mod p=b-z mod p. Как видим, левая часть равенства зависит только от y, а правая - только от z. Теперь, сделаем списки левых и правых частей в зависимости от y и z, отсортируем их и найдем общий элемент - пару (y,z) такую, что amy mod p=b-z mod p. Ответом будет число a=my+z.