1. 程式人生 > >Princeton-Algorithms-PartI

Princeton-Algorithms-PartI

題注

Princeton Algorithm Part I中Week 3的Assignment實際上也是很早前就做完了,不過因為懶,一直沒放到網上來… 正好新週期的Algorithm課也督促我複習一遍演算法知識,另一方面也有機會把所有的Assignment做完吧。上個週期中,Week 4的Assignment沒有拿到滿分,就萌生退意了。這回捲土重來,希望能拿到一個Perfect的結果(當然了,Princeton的公開課是沒有Accomplishment的,做這個只是學習和課外消遣而已)。

Week 3的題目是圖論中的一個很典型的問題,在LeetCode中也有相應的演算法題,也算是為刷LeetCode圖論的題打點基礎吧!

題目

Programming Assignment 3: Pattern Recognition

Write a program to recognize line patterns in a given set of points.

Computer vision involves analyzing patterns in visual images andreconstructing the real-world objects that produced them. The processin often broken up into two phases: feature detection

andpattern recognition. Feature detection involves selectingimportant features of the image; pattern recognition involvesdiscovering patterns in the features. We will investigate aparticularly clean pattern recognition problem involving points andline segments. This kind of pattern recognition arises in many otherapplications such as statistical data analysis.

The problem.Given a set of N distinct points in the plane, draw every (maximal) line segment that connects a subset of 4 or more of the points.

Points and lines

Point data type.Create an immutable data type Point that represents a point in the planeby implementing the following API:

public class Point implements Comparable<Point> {
   public final Comparator<Point> SLOPE_ORDER;        // compare points by slope to this point

   public Point(int x, int y)                         // construct the point (x, y)

   public   void draw()                               // draw this point
   public   void drawTo(Point that)                   // draw the line segment from this point to that point
   public String toString()                           // string representation

   public    int compareTo(Point that)                // is this point lexicographically smaller than that point?
   public double slopeTo(Point that)                  // the slope between this point and that point
}
To get started, use the data typePoint.java,which implements the constructor and thedraw(), drawTo(), and toString() methods.Your job is to add the following components.
  • The compareTo() method should compare points by their y-coordinates,breaking ties by their x-coordinates.Formally, the invoking point(x0, y0)is less than the argument point(x1, y1)if and only if either y0 < y1 or ify0 = y1 and x0 < x1.
  • The slopeTo() method should return the slope between the invoking point(x0, y0) and the argument point(x1, y1), which is given by the formula(y1y0) / (x1x0).Treat the slope of a horizontal line segment as positive zero [added 7/29];treat the slope of a vertical line segment as positive infinity;treat the slope of a degenerate line segment (between a point and itself) as negative infinity.
  • The SLOPE_ORDER comparator should compare points by the slopes theymake with the invoking point (x0, y0).Formally, the point (x1, y1) is less thanthe point (x2, y2) if and only if the slope(y1y0) / (x1x0) is less than the slope(y2y0) / (x2x0).Treat horizontal, vertical, and degenerate line segments as in the slopeTo() method.

Brute force.Write a program Brute.java that examines 4 points at a time and checks whetherthey all lie on the same line segment, printing out any such linesegments to standard output and drawing them using standard drawing.To check whether the 4 points p, q, r, and s are collinear,check whether the slopes between p and q, between p and r, and between p and sare all equal.

The order of growth of the running time of your program should beN4 in the worst case and it should use space proportional to N.

A faster, sorting-based solution.Remarkably, it is possible to solve the problem much faster than thebrute-force solution described above.Given a point p, the following method determines whether pparticipates in a set of 4 or more collinear points.

  • Think of p as the origin.
  • For each other point q, determine the slope it makes with p.
  • Sort the points according to the slopesthey makes with p.
  • Check if any 3 (or more) adjacent points in the sorted order have equalslopes with respect to p.If so, these points, together with p, are collinear.
Applying this method for each of the N points in turn yields anefficient algorithm to the problem.The algorithm solves the problem because points that have equal slopes with respect to p are collinear, and sorting brings such points together.The algorithm is fast because the bottleneck operation is sorting.
Points and slopes

Write a program Fast.java that implements this algorithm.The order of growth of the running time of your program should beN2 log N in the worst case and it should use space proportional to N.

APIs. [Added 7/25]Each program should take the name of an input file as a command-line argument,read the input file (in the format specified below),print to standard output the line segments discovered (in the format specified below),and draw to standard draw the line segments discovered (in the format specified below).Here are the APIs.

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

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

Input format.Read the points from an input file in the following format:An integer N, followed by Npairs of integers (x, y), each between 0 and 32,767.Below are two examples.

% more input6.txt       % more input8.txt
6                       8
19000  10000             10000      0
18000  10000                 0  10000
32000  10000              3000   7000
21000  10000              7000   3000
 1234   5678             20000  21000
14000  10000              3000   4000
                         14000  15000
                          6000   7000

Output format.Print to standard output the line segments that your program discovers, one per line.Print each line segment as an ordered sequence of its constituent points,separated by " -> ".

% java Brute input6.txt
(14000, 10000) -> (18000, 10000) -> (19000, 10000) -> (21000, 10000)
(14000, 10000) -> (18000, 10000) -> (19000, 10000) -> (32000, 10000)
(14000, 10000) -> (18000, 10000) -> (21000, 10000) -> (32000, 10000)
(14000, 10000) -> (19000, 10000) -> (21000, 10000) -> (32000, 10000)
(18000, 10000) -> (19000, 10000) -> (21000, 10000) -> (32000, 10000)

% java Brute input8.txt
(10000, 0) -> (7000, 3000) -> (3000, 7000) -> (0, 10000) 
(3000, 4000) -> (6000, 7000) -> (14000, 15000) -> (20000, 21000) 

% java Fast input6.txt
(14000, 10000) -> (18000, 10000) -> (19000, 10000) -> (21000, 10000) -> (32000, 10000) 

% java Fast input8.txt
(10000, 0) -> (7000, 3000) -> (3000, 7000) -> (0, 10000)
(3000, 4000) -> (6000, 7000) -> (14000, 15000) -> (20000, 21000)
Also, draw the points using draw() and draw the line segmentsusing drawTo().Your programs should call draw() once for each point in the input file andit should call drawTo() once for each line segment discovered.Before drawing, use StdDraw.setXscale(0, 32768) and StdDraw.setYscale(0, 32768) to rescale the coordinate system.For full credit, do not print permutations of points ona line segment (e.g., if you outputpqrs,do not also output eithersrqp orprqs).Also, for full credit in Fast.java,do not print or plot subsegments of a line segment containing5 or more points (e.g., if you output pqrst,do not also output eitherpqst orqrst);you may print out such subsegments in Brute.java.

分析

這道題本身就是演算法中的一個很經典的問題。Princeton對於題目的介紹已經非常詳細了,包括兩種演算法的描述,一些特定的問題,一些特殊情況的注意事項等等。我們一個一個來看這些問題。

Point類的實現

不管用什麼方法,Point類終究是要實現的,因此我們先來看如何實現Point類。題目中已經給出了一個Point.java的模板,示例如下:

/*************************************************************************
 * Name:
 * Email:
 *
 * Compilation:  javac Point.java
 * Execution:
 * Dependencies: StdDraw.java
 *
 * Description: An immutable data type for points in the plane.
 *
 *************************************************************************/

import java.util.Comparator;

public class Point implements Comparable<Point> {

    // compare points by slope
    public final Comparator<Point> SLOPE_ORDER;       // YOUR DEFINITION HERE

    private final int x;                              // x coordinate
    private final int y;                              // y coordinate

    // create the point (x, y)
    public Point(int x, int y) {
        /* DO NOT MODIFY */
        this.x = x;
        this.y = y;
    }

    // plot this point to standard drawing
    public void draw() {
        /* DO NOT MODIFY */
        StdDraw.point(x, y);
    }

    // draw line between this point and that point to standard drawing
    public void drawTo(Point that) {
        /* DO NOT MODIFY */
        StdDraw.line(this.x, this.y, that.x, that.y);
    }

    // slope between this point and that point
    public double slopeTo(Point that) {
        /* YOUR CODE HERE */
    }

    // is this point lexicographically smaller than that one?
    // comparing y-coordinates and breaking ties by x-coordinates
    public int compareTo(Point that) {
        /* YOUR CODE HERE */
    }

    // return string representation of this point
    public String toString() {
        /* DO NOT MODIFY */
        return "(" + x + ", " + y + ")";
    }

    // unit test
    public static void main(String[] args) {
        /* YOUR CODE HERE */
    }
}
我們的目標是首先實現Point類的幾個核心功能;然後來實現SLOPE_ORDER內部類,用於對兩個Point進行對比。

slopeTo()

slopeTo類相當於來計算兩點間連線的斜率了。初中數學我們就知道,有如下幾種情況。

(1)兩個點的y座標相同,即連線與x軸平行。這種情況的斜率為0。但是我們要注意一個問題,在Java中實際上有兩種0,即+0和-0,並且Java內建的比較函式認為+0>-0。這是怎麼來的呢?在題目的CheckList中已經給出了相關的介紹,我們放在這裡供大家參考:

What does it mean for slopeTo() to return positive zero?Java (and the IEEE 754 floating-point standard) define two representations of zero: negative zero and positive zero.

double a = 1.0;
double x = (a - a) /  a;   // positive zero ( 0.0)
double y = (a - a) / -a;   // negative zero (-0.0)
Note that while (x == y) is guaranteed to be true,Arrays.sort() treatsnegative zero as strictly less than positive zero.Thus, to make the specification precise, we require you to return positive zero for horizontal line segments.Unless your program casts to the wrapper type Double (either explicitly or via autoboxing),you probably will not notice any difference in behavior;but, if your program does cast to the wrapper type and fails only on (some) horizontal line segments, this may bethe cause.

(2)兩個點的x座標相同,即連線與y軸平行。根據題目要求,我們要把這種斜率定義為正無窮大。在Java中,Double類給出了雙精度浮點數正無窮大的表示,為Double.POSITIVE_INFINITY,因此用這個值代表正無窮大即可。

(3)兩個點相同。這是一個極為特殊的情況,在CheckList中也有過說明,內容如下:

Can the same point appear more than once as input to Brute or Fast?You may assume the input to Brute and Fast are N distinct points.Nevertheless, the methods implemented as part of the Point data typemust correctly handle the case when the points are not distinct:for the slopeTo() method, this requirement is explicitly stated in the API;for the comparison methods, this requirement is implict in the contracts for Comparable and Comparator.

對於這種情況,我的處理方法是將其結果設定為負無窮大,即Double.NEGATIVE_INFINITY。

(4)其他一般情況。這種情況下,我們直接計算斜率即可,計算公式很簡單,為:(y_1 - y_0) / (x_1 - x_0)。不過注意到點的座標表示是int,斜率為double,因此我們還需要一個強制轉換來保證計算的精度。

compareTo()

這個類的實現也很容易,直接按照題目要求來實現即可。當y_0 < y_1或者當y_0 = y_1且x_0 < x_1時,返回-1。

SLOPE_ORDER

這個類的實現也很容易,分別計算兩個點對於第三個點的斜率(呼叫slopeTo()),然後返回兩個結果的比較值即可。因為在slopeTo()中我們已經處理過了3種特殊的情況,因此直接返回比較值就是正確的結果。

蠻力搜尋的實現和注意事項

蠻力搜尋的實現很簡單,相當於把所有點四個四個的比較,看看斜率是不是一樣,如果一樣就在一條直線上。蠻力搜尋需要注意的情況是不要重複比較,舉個例子:p,q,r,s點比較了,就不要比較p,q,s,r等類似的四組點了。因此,直觀地看我們相當於從所有N個點中,依次取出4個點逐個比較了。為了保證比較不重複,用如下的迴圈來處理比較:

for (int i=0; i<N; i++){
    for (int j=i+1; j<N; j++){
	for (int k=j+1; k<N; k++){
	    for (int l=k+1; l<N; l++){
	        //進行比較
	    }
	}
    }
}

實現例項資料讀入的main函式

蠻力搜尋實現完畢後,怎麼進行測試呢?這就涉及到例項資料讀取的問題了。讀取的順序是:讀取Point總數N;依次讀取每一個Point的x座標和y座標;執行演算法得出結果。不得不說,Princeton自己實現的一個stdlib真是非常好用,以至於我現在實現自己的一些程式都會用這個jar,也推薦給大家好好看看,可以減輕很多開發工作呢。

例項資料讀入main函式的例項如下,供大家參考:

public static void main(String[] args){
	// rescale coordinates and turn on animation mode
        StdDraw.setXscale(0, 32768);
        StdDraw.setYscale(0, 32768);
        
	String filename = args[0];
        In in = new In(filename);
        int N = in.readInt();
        Point[] pointArray = new Point[N];
        for (int i = 0; i < N; i++) {
            int x = in.readInt();
            int y = in.readInt();
            Point p = new Point(x, y);
            pointArray[i] = p;
            p.draw();
        }
        //在這裡呼叫演算法
}

Fast Algorithm的實現

Fast Algorithm的原理很簡單,實際上是一種快速找到與自己slope相同的點的方法。演算法本身確實沒什麼說的,不過有一些細節問題需要注意。

1. 如何處理類似p,q,r,s和p,q,s,r的問題?這個問題剛開始我還覺得是一個問題,結果最後發現這根本不是個問題… 因為p->q的斜率和q->p的斜率根本就是不一樣的,互為相反數。所以這種演算法可以直接處理連續5個點以及以上的問題。舉例來說,如果p,q,r,s,t點在一個直線上,那麼排序結果p->q,p->r,p->s,p->t的斜率都一樣,這五個點在一個直線上。

2. 如果p,q,r,s,t這5個點在一條直線上,那麼從q搜尋的時候q,r,s,t這4個點也在一條直線上,如何處理這種情況呢(意味著兩組結果,還是隻能輸出q,r,s,t這一種結果)?這是一個值得考慮的問題。一種簡單的解法是,開闢一個新的儲存空間,對於5個點以上在一條直線的情況,把其子情況儲存在新的儲存空間中。當找到新的解時,對比新解的起始點和斜率是否與儲存的解和斜率是否相同(這是因為一個點和一個斜率唯一確定一條直線)。不過,這個問題實際上也不需要考慮,在CheckList中也給出了類似的解釋:

I'm having trouble avoiding subsegments Fast.java when there are 5 or more points on a linesegment. Any advice?Not handling the 5-or-more case is a bit tricky,so don't kill yourself over it.

程式碼

Point.java

import java.util.Comparator;

public class Point implements Comparable<Point> {

    // compare points by slope
    public final Comparator<Point> SLOPE_ORDER = new SlopeOrder();       // YOUR DEFINITION HERE

    private final int x;                              // x coordinate
    private final int y;      						  // y coordinate

    private class SlopeOrder implements Comparator<Point>{

		public int compare(Point p1, Point p2) {
			double slopeP1 = slopeTo(p1);
			double slopeP2 = slopeTo(p2);
			if (slopeP1 == slopeP2) return 0;
			if (slopeP1 < slopeP2) return -1;
			else return +1;
		}
    	
    }
    // create the point (x, y)
    public Point(int x, int y) {
        /* DO NOT MODIFY */
        this.x = x;
        this.y = y;
    }

    // plot this point to standard drawing
    public void draw() {
        /* DO NOT MODIFY */
        StdDraw.point(x, y);
    }

    // draw line between this point and that point to standard drawing
    public void drawTo(Point that) {
        /* DO NOT MODIFY */
        StdDraw.line(this.x, this.y, that.x, that.y);
    }

    // slope between this point and that point
    public double slopeTo(Point that) {
        int dx = that.x - this.x;
        int dy = that.y - this.y;
        if (dx == 0 && dy == 0) return Double.NEGATIVE_INFINITY;
        if (dx == 0) return Double.POSITIVE_INFINITY;
        if (dy == 0) return +0;
        else return (double)dy / (double)dx;
    }

    // is this point lexicographically smaller than that one?
    // comparing y-coordinates and breaking ties by x-coordinates
    public int compareTo(Point that) {
        if (this.y < that.y) return -1;
        if (this.y == that.y && this.x < that.x) return -1;
        if (this.y == that.y && this.x == that.x) return 0;
        else return +1;
    }

    // return string representation of this point
    public String toString() {
        /* DO NOT MODIFY */
        return "(" + x + ", " + y + ")";
    }

    // unit test
    public static void main(String[] args) {
        /* YOUR CODE HERE */
    }
}

Brute.java
public class Brute {
	public static void main(String[] args){
		// rescale coordinates and turn on animation mode
        StdDraw.setXscale(0, 32768);
        StdDraw.setYscale(0, 32768);
        
		String filename = args[0];
        In in = new In(filename);
        int N = in.readInt();
        Point[] pointArray = new Point[N];
        for (int i = 0; i < N; i++) {
            int x = in.readInt();
            int y = in.readInt();
            Point p = new Point(x, y);
            pointArray[i] = p;
            p.draw();
        }
        Quick3way.sort(pointArray);
        BruteForce(pointArray);
        StdDraw.show(0);
    }
	
	private static void BruteForce(Point[] pointArray){
		int N = pointArray.length;
		for (int i=0; i<N; i++){
			for (int j=i+1; j<N; j++){
				for (int k=j+1; k<N; k++){
					for (int l=k+1; l<N; l++){
						if (pointArray[i].slopeTo(pointArray[j]) == pointArray[i].slopeTo(pointArray[k]) &&
							pointArray[i].slopeTo(pointArray[j]) == pointArray[i].slopeTo(pointArray[l])){
							StdOut.println(pointArray[i] + " -> " + pointArray[j] + " -> "  + pointArray[k] + " -> "  + pointArray[l]);
							pointArray[i].drawTo(pointArray[l]);
						}
					}
				}
			}
		}
	}
}

Fast.java
public class Fast {
	public static void main(String[] args){
		// rescale coordinates and turn on animation mode
        StdDraw.setXscale(0, 32768);
        StdDraw.setYscale(0, 32768);
        
		String filename = args[0];
        In in = new In(filename);
        int N = in.readInt();
        if (N < 4){
        	return;
        }
        Point[] pointArray = new Point[N];
        for (int i = 0; i < N; i++) {
            int x = in.readInt();
            int y = in.readInt();
            Point p = new Point(x, y);
            pointArray[i] = p;
            p.draw();
        }
        Quick3way.sort(pointArray);
        FastMethod(pointArray);
        StdDraw.show(0);
    }
	
	private static void FastMethod(Point[] pointArray){
		int N = pointArray.length;
		for (int i=0; i<N; i++){
			Point origPoint = pointArray[i];
			Point[] otherPoint = new Point[N-1];
			for (int j=0; j<pointArray.length; j++){
				if (j < i) otherPoint[j] = pointArray[j];
				if (j > i) otherPoint[j-1] = pointArray[j];
			}
			Arrays.sort(otherPoint, origPoint.SLOPE_ORDER);
			
			int count = 0;
			int index = 0;
			double tempSlope = origPoint.slopeTo(otherPoint[0]);
			for (int j=0; j<otherPoint.length;j++){
				if (Double.compare(origPoint.slopeTo(otherPoint[j]),  tempSlope) == 0){
					count++;
					continue;
				}else{
					if (count >=3){
						if (otherPoint[index].compareTo(origPoint) >=0){
							StdOut.print(origPoint + " -> ");
							for (int k=index; k<j-1; k++){
								StdOut.print(otherPoint[k] + " -> ");
							}
							StdOut.println(otherPoint[j-1]);
							origPoint.drawTo(otherPoint[j-1]);
						}
					}
					count = 1;
					index = j;
					tempSlope = origPoint.slopeTo(otherPoint[j]);
				}
			}
			if (count >= 3){
				if (otherPoint[index].compareTo(origPoint) >= 0){
					StdOut.print(origPoint + " -> ");
					for (int k=index; k<otherPoint.length - 1; k++){
						StdOut.print(otherPoint[k] + " -> ");
					}
					StdOut.println(otherPoint[otherPoint.length-1]);
					origPoint.drawTo(otherPoint[otherPoint.length-1]);
				}
			}
		}
		StdDraw.show(0);
	}
}