【算法】凸包問題--分治法


凸包問題--分治法

求能夠完全包含平面上n個給定點的凸多邊形。


示例:


一、分治法:

(一)算法思路:

(這里所說的直線都是有向直線的。)

將數組升序排序,若x軸坐標相同,按照y軸坐標升序排序。

最左邊的點p1和最右邊的點p_n一定是該集合凸包的頂點。該直線將點分為兩個集合,上包為S1,下包為S2。在p1 p_n線上的點不可能是凸包的頂點,所以不用考慮。

在上包S1中,找到p_max(距離直線p1p_n最遠距離的點),若有兩個距離同樣遠的點,取∠p_max p1 p_n最大的那個點(即△p_max p1 p_n面積最大)。

(一次遞歸到這里結束)

找出S1中所有在直線p1 p_max左邊的點,這些點中一定有構成上包中左半部分邊界的頂點,用上面的算法遞歸查找點,直到上包就是以p1和p_n為端點的線段。

下包S2中找下邊界 同理。


*如何判斷點是否在直線p1 p_max左邊(同 p1 p_n上方)?

如果q1(x1,y1),q2(x2,y2),q3(x3,y3)是平面上的任意三個點,那么三角形△q1 q2 q3的面積等於下面這個行列式絕對值的二分之一。

當且僅當點q3=(x3,y3)位於直線q1 q2的左側時,該表達式的符號為正,該點位於兩個點確定的直線的左側。


(二)實現中碰到的問題

  • 如何用快速排序來排序Point類(內有坐標x,y)的一維數組?

    按照x坐標排序很簡單,若碰到x相同,y不同的怎么辦?


在快排的原基礎上修改,以j向前逼近說明:

(第一個while循環)當前比較數的橫坐標>基准點的時,j向前逼近。此處不加等於號,排序是不穩定的,即相等元素的相對位置可能發生改變。(快排詳見博客:https://www.cnblogs.com/musecho/p/11647349.html)

(第二個while為添加內容)比較相等元素的縱坐標,基准點的更小,j繼續向前逼近,即相等元素的相對位置不發生改變;否則,則改變。也就是將原來快排中while循環拆分為兩個,增加相等元素另外比較縱坐標的情況。

while (i < j && points[j].getX() > center.getX()) {
					j--;
				}
				while (i < j && center.getX() == points[j].getX() && points[j].getY() > center.getY()) {
					j--;
				}
				/*
				 * (i<j)若points[j].getX()< center.getX()或 center.getX() ==
				 * points[j].getX()且points[j].getY()<center.getY() 以上兩種情況,需要賦值
				 */
				if (i < j)// 跳出循環也有可能時因為i=j,所以這里要判斷一下
					points[i++] = points[j];


  • 如果使用全局數組visit標識點是否訪問,能確定凸包的所有頂點,但怎么順序輸出?

在已經求的凸包頂點里逐一確定邊界,判斷是不是所有點都在這條邊界的一側,如果是則確定一條邊界。

        convexHullList.add(convexHullVertex[0]);// 開始點
		int haveCount = 1;// 已經加入點的個數
		// 逐條確定邊界,判斷是否除了該條假設邊界上的點,其他凸包的頂點都在直線的右邊。
		// 如果是,則此條直線為邊界;如果不是,取下一個邊界終點,繼續判斷。
		int start = 0;// 起點
		for (int end = start + 1; haveCount < count;) {
			boolean boundRight = true;
			for (int i = 0; i < count; i++) {
				while (i < count && (i == start || i == end)) {// 不能寫if,start和end可能是連在一起的
					i++;
				}
				if (i >= count)
					break;

				// 點在直線左側或線上,錯誤
				if (PointJudge(convexHullVertex[start], convexHullVertex[end], convexHullVertex[i]) >= 0) {
                    
					boundRight = false;
					end = (end + 1) % count;// end取下一個
					break;
				}
			}
			if (boundRight == true) {
				convexHullList.add(convexHullVertex[end]);
				start = end;
				end = (start + 1) % count;
				haveCount++;
			}
		}

(三)注意點

  • 注意方法PointJudge(Point beginP, Point endP,Point p)和PointCal(Point beginP,Point endP,Point p)中,傳參放在第幾個:

    前兩個點是直線的兩端,第三個是需要判斷的點


  • 注意下包循環中的 起始點、終點、判斷條件
for (int i = begin - 1; i >= end + 1; i--)

(四)源代碼


1.ConvexHullProblem_DC

package ConvexHullProblem;

import java.util.ArrayList;
import java.util.Arrays;

/**
 * 凸包問題(分治法):
 */

public class ConvexHullProblem_DC {
	boolean[] visit;// 標志點是否是凸包的頂點:1是,0不是
	Point[] points;// 所有點
	Point[] convexHullVertex;// 凸包的頂點
	ArrayList<Point> convexHullList = new ArrayList<>();;// 凸包的頂點(順序存放)

	public void ConvexHullProblem(Point[] points) {
		this.points = points;
		quickSort(0, points.length - 1);// 升序排序
//		System.out.println("升序:" + Arrays.toString(points));

		visit = new boolean[points.length];

		recursion(0, points.length - 1);// 上包
		recursion(points.length - 1, 0);// 下包

		orderConvexHull();
	}

	/**
	 * @title: recursion
	 * @description: 在凸包的上包或下包中,找使△p_max p1 p_n面積最大的點p_max,並遞歸
	 * @date: 2019年10月16日 下午8:33:14
	 * @param begin 直線的起點
	 * @param end   直線的終點 void
	 * @throws:
	 */
	void recursion(int begin, int end) {
		// 直線的兩端點為凸包的頂點
		visit[begin] = true;
		visit[end] = true;

		if (begin < end) {
			boolean flag = false;// 標志直線左側是否有點
			int maxArea = 0;// 最大面積
			int maxAreaindex = begin + 1;// 最大面積的點下標

			for (int i = begin + 1; i <= end - 1; i++) {// begin和end已經是頂點,不需要判斷
				if (PointJudge(points[begin], points[end], points[i]) > 0) {// 點在直線左側
					// 找距離最遠的點,因為底相同都是p1 pn,也就是求三角形面積最大的
					flag = true;
					int area = PointCal(points[begin], points[end], points[i]);
					if (area > maxArea) {
						maxArea = area;
						maxAreaindex = i;
					} else if (area == maxArea) {// 若面積相同,取∠p_max p_begin p_end最大的那一個
						System.out.println(22);
						double degreeA = Degree(points[begin], points[i], points[end]);
						double degreeB = Degree(points[begin], points[maxAreaindex], points[end]);
						if (degreeA > degreeB) {
							maxArea = area;
							maxAreaindex = i;
						}
					}
//					System.out.println("area=" + area + ",Point=" + points[i]);
				}
			}
//			System.out.println("maxArea=" + maxArea + ",Point=" + points[maxAreaindex]);
//			System.out.println("over");

			// 若直線左側還有點,則遞歸;沒有點,則結束
			if (flag == true) {
				recursion(begin, maxAreaindex);
				recursion(maxAreaindex, end);
			}
		} else if (begin > end) {
			boolean flag = false;
			int maxArea = 0;// 最大面積
			int maxAreaindex = end + 1;// 最大面積的點下標

			for (int i = begin - 1; i >= end + 1; i--) {// 注意下包循環中的 起始點、終點、判斷條件
				if (PointJudge(points[begin], points[end], points[i]) > 0) {// 點在直線左側
					flag = true;
					int area = PointCal(points[begin], points[end], points[i]);
					if (area > maxArea) {
						maxArea = area;
						maxAreaindex = i;
					} else if (area == maxArea) {// 若面積相同,取∠p_max p_begin p_end最大的那一個
						System.out.println(22);
						double degreeA = Degree(points[begin], points[i], points[end]);
						double degreeB = Degree(points[begin], points[maxAreaindex], points[end]);
						if (degreeA > degreeB) {
							maxArea = area;
							maxAreaindex = i;
						}
					}
//					System.out.println("area=" + area + ",Point=" + points[i]);
				}
			}
//			System.out.println("maxArea=" + maxArea + ",Point=" + points[maxAreaindex]);
//			System.out.println("over");

			if (flag == true) {
				recursion(begin, maxAreaindex);
				recursion(maxAreaindex, end);
			}
		}
	}

	/**
	 * @title: quickSort:運用Hoare
	 * @description: 快速排序: 選取第一個元素作為基准點(可以隨機選取),將剩下元素與基准點進行比較,
	 *               比基准點大的放在右邊,比基准點小的放在左邊, 得到左子表和右子表,遞歸調用本函數;
	 * @param points 數組
	 * @param begin  開始下標
	 * @param end    結束下標
	 * @throws:
	 */
	void quickSort(int begin, int end) {
		if (begin >= 0 && begin < end && end < points.length) {
			int i = begin, j = end;
			Point center = points[i];// 中心元素

			while (i != j) {
				while (i < j && points[j].getX() > center.getX()) {
					j--;
				}
				while (i < j && center.getX() == points[j].getX() && points[j].getY() > center.getY()) {
					j--;
				}
				/*
				 * (i<j)若points[j].getX()< center.getX()或 center.getX() ==
				 * points[j].getX()且points[j].getY()<center.getY() 以上兩種情況,需要賦值
				 */
				if (i < j)// 跳出循環也有可能時因為i=j,所以這里要判斷一下
					points[i++] = points[j];

				while (i < j && points[i].getX() < center.getX()) {
					i++;
				}
				while (i < j && points[i].getX() == center.getX() && points[i].getY() < center.getY()) {
					i++;
				}
				/*
				 * (i<j)若points[i].getX()> center.getX()或 center.getX() ==
				 * points[i].getX()且points[i].getY()>center.getY() 以上兩種情況,需要賦值
				 */
				if (i < j)
					points[j--] = points[i];
			}
			points[i] = center;// 中心元素到達最終位置

			quickSort(begin, i - 1);
			quickSort(i + 1, end);
		}
	}

	/**
	 * @title: PointCal
	 * @description: 計算行列式的值
	 * @date: 2019年10月15日 下午7:53:07
	 * @param beginP 直線的開始點
	 * @param p      判斷的點
	 * @param endP   直線的終點
	 * @return int 行列書的值
	 * @throws:
	 */
	private int PointCal(Point beginP, Point endP, Point p) {
		int cal = 0;// 行列式值

//x1y2+x3y1+x2y3-x3y2-x2y1-x1y3
		cal = beginP.getX() * endP.getY() + p.getX() * beginP.getY() + endP.getX() * p.getY() - p.getX() * endP.getY()
				- endP.getX() * beginP.getY() - beginP.getX() * p.getY();
		return cal;
	}

	/**
	 * @title: PointJudge
	 * @description:返回點p在直線beginP endP的位置
	 * @date: 2019年10月15日 下午7:56:56
	 * @param beginP
	 * @param p      判斷的點
	 * @param endP
	 * @return int :1在直線左側,0在線上,-1在右側
	 * @throws: 注意傳參放在第幾個,前兩個點是直線的兩端,第三個是需要判斷的點
	 */
	private int PointJudge(Point beginP, Point endP, Point p) {
		if (PointCal(beginP, endP, p) > 0) {
			return 1;
		} else if (PointCal(beginP, endP, p) == 0)
			return 0;
		else
			return -1;
	}

	/**
	 * @title: Degree
	 * @description: 余弦公式求∠pa pb pc的度數
	 * @date: 2019年10月16日 下午6:59:29
	 * @param pa 點
	 * @param pb
	 * @param pc
	 * @return double:返回∠c的度數(°為單位)
	 * @throws:
	 */
	double Degree(Point pa, Point pb, Point pc) {
		double degree = 0;// ∠pa pb pc度數

		// 三角形的三邊長
		double a = Math.sqrt(Math.pow(pa.getX() - pb.getX(), 2) + Math.pow(pa.getY() - pb.getY(), 2));
		double b = Math.sqrt(Math.pow(pb.getX() - pc.getX(), 2) + Math.pow(pb.getY() - pc.getY(), 2));
		double c = Math.sqrt(Math.pow(pc.getX() - pa.getX(), 2) + Math.pow(pc.getY() - pa.getY(), 2));

		// 余弦公式求∠pa pb pc度數
		System.out.println("acos=" + Math.acos((a * a + b * b - c * c) / (2.0 * a * b)));
		degree = Math.toDegrees(Math.acos((a * a + b * b - c * c) / (2.0 * a * b)));
		System.out.println("degree=" + degree);

		return degree;
	}

	/**
	 *@title: orderConvexHull 
	 *@description: 凸包頂點按順時針輸出
	 *@date: 2019年10月19日 上午9:28:44 
	 *void
	 *@throws:
	 */
	void orderConvexHull() {
		/** 將凸包頂點存放進另一個數組 */
		int count = 0;// 凸包的頂點個數
		for (int i = 0; i < visit.length; i++) {
			if (visit[i] == true) {
				count++;
			}
		}
		convexHullVertex = new Point[count];
		for (int j = 0, i = 0; j < visit.length; j++) {
			if (visit[j] == true) {
				convexHullVertex[i] = points[j];
				i++;
			}
		}

		convexHullList.add(convexHullVertex[0]);// 開始點
		int haveCount = 1;// 已經加入點的個數
		// 逐條確定邊界,判斷是否除了該條假設邊界上的點,其他凸包的頂點都在直線的右邊。
		// 如果是,則此條直線為邊界;如果不是,取下一個邊界終點,繼續判斷。
		int start = 0;// 起點
		for (int end = start + 1; haveCount < count;) {
			boolean boundRight = true;
			for (int i = 0; i < count; i++) {
				while (i < count && (i == start || i == end)) {// 不能寫if,start和end可能是連在一起的
					i++;
				}
				if (i >= count)
					break;

				// 點在直線左側或線上,錯誤
				if (PointJudge(convexHullVertex[start], convexHullVertex[end], convexHullVertex[i]) >= 0) {
//					System.out.println("****");
//					System.out.println(convexHullVertex[start]);
//					System.out.println(convexHullVertex[end]);
//					System.out.println(convexHullVertex[i]);
//					System.out.println("****");
					
					boundRight = false;
					end = (end + 1) % count;// end取下一個
					break;
				}
			}
			if (boundRight == true) {
				convexHullList.add(convexHullVertex[end]);
				start = end;
				end = (start + 1) % count;
				haveCount++;
			}
		}
		convexHullList.add(convexHullVertex[0]);// 結束點
		System.out.println("凸包頂點順時針輸出:" + convexHullList);
	}
}


2.Point

package ConvexHullProblem;

/**
 * 點的信息
 */
public class Point {
	private int x, y;// 橫縱坐標

	public Point(int x,int y) {
		this.x=x;
		this.y=y;
	}
	
	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public String toString() {
		return " (" + x + ", "+ y + ")";
	}

}


3.Demo_DC

package ConvexHullProblem;

import java.util.Arrays;
import java.util.Scanner;

public class Demo_DC {
	public static void main(String[] args) {

		//示例
		Point[] points=new Point[13];
		points[0] = new Point( 4, 5);
		points[1] = new Point(10, 11);
		points[2] = new Point( 4, 11);
		points[3] = new Point( 1, 1);
		points[4] = new Point( 10, 6);
		points[5] = new Point( 8, 14);
		
		//橫坐標最兩側的點
		points[6] = new Point( 13, 7);
		points[7] = new Point( 13, 0);
		points[8] = new Point( 0, 9);
		points[9] = new Point( 0, 7);
		
		points[10] = new Point( 5, 5);
		points[11] = new Point( 7, 9);
		points[12] = new Point( 11, 3);

//		Scanner scanner = new Scanner(System.in);
//		System.out.println("**********凸包問題**********");
//		System.out.println("請輸入點的數量:");
//		int n = scanner.nextInt();
//		Point[] points = new Point[n];
//		points = randomPoint(points);
		
		System.out.println("隨機生成點:" + Arrays.toString(points));
		ConvexHullProblem_DC convexHullProblem = new ConvexHullProblem_DC();
		convexHullProblem.ConvexHullProblem(points);

	}

	static Point[] randomPoint(Point[] points) {
		for (int i = 0; i < points.length; i++) {
			int x = (int) (Math.random() * 21);// [0-20]
			int y = (int) (Math.random() * 21);// [0-20]

			points[i] = new Point(x, y);
		}

		return points;
	}
}


(運行結果)



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM