2017年5月6日 星期六

[JAVA] Convex Hull

建議可以先到 這裡 了解 Convex Hull 演算法的基本概念



我的做法:

1. JAVA AWT 視窗的座標原點是在最左上角,我沒有重新修改,這並不影響我的運算


2. 一開始我先找出左界和右界,再依此左界來找出最左上角的點,我將會把此點當成我的起始點位(current),如下圖:


3. 現在我們可以先確定最左上角的點也是凸包的其中一點了,我要以這點為起始點位(current),用迴圈由右往左尋找這些點中可以與起始點形成斜率較大的點,找到後除了用個 buffer 把它紀錄下來之外,也要把它放進current,因為我們要繼續以這點為起始點繼續尋找下個點,以此類推,直到抵達右界(right)


4. 抵達右界後(right)之後我們要反過來由左往右尋找斜率較大的點,就如同步驟3一樣,只
方向變了而已。如下圖:


5. 如此一來就可以找出最外圍的點有哪些了,因為在過程中找出來的點我們都存到一個buffer
,所以就可以直接把這些座標取出來畫一個凸包


<註>在第 3 4 步驟的時候要注意,假設找到兩個斜率一樣大的點,我們要取長度比較長的那
個,所以記得多寫一個判斷式


把程式碼貼過來的時候都會出現一些小問題
如果真的需要程式的話可以留言給我~我會發給你


附上程式碼:

/*Convex.java*/
import java.awt.*;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
import java.util.Vector;

class Convex extends Frame implements ActionListener, TextListener, MouseMotionListener{
 /*-------------定義會用到的東西----------------*/
 static Convex frm = new Convex();
 static Button btn = new Button("Send");
 static TextArea txa = new TextArea("請輸入要幾個點");
 //static TextArea txa1 = new TextArea("資訊三甲 陳文遠 D0349119 作業二 : Convex Hull");
 static Label lab = new Label();
 static Label lab1 = new Label("資訊三甲陳文遠D0349119作業二:Convex Hull");
 int amount;  //接收由文字方塊輸入的數字

 //定義一個 Vertex 類別,放置 x y 形成一個座標
 private static class Vertex {
     public int x;
     public int y;

     //建構子,可以直接拿來放 x y
     public Vertex(int x, int y) {
       this.x = x;
       this.y = y;
     }
   }
   /*----------------------------------------------*/



 /*-------------程式的 Entry Point---------------*/
 public static void main(String args[]){
  btn.addActionListener(frm);  //將frm設為btn的事件傾聽者
  txa.addTextListener(frm);  //將frm設為txa的事件傾聽者
  frm.addMouseMotionListener(frm);

  frm.setLayout(null);  //不使用版面配置
  frm.setTitle("Convex Hull");  //設定視窗名稱
  btn.setBounds(425,500,75,50);  //定義按鈕格式
  txa.setBounds(200,500,225,50);  //定義文字方塊格式
  lab1.setBounds(100,30,300,40); //定義文字方塊格式
  lab1.setBackground(Color.yellow);  //lab1設成黃色背景
  lab.setBounds(30,500,190,50);
  //txa1.setEditable(false);  //把 txa1 設為不可編輯
  frm.setSize(500,550);  //設定視窗大小
  frm.add(btn);  //加入按鈕
  frm.add(txa);  //加入文字方塊
  frm.add(lab1); //加入label
  frm.add(lab);  //加入label
  frm.setVisible(true);  //顯示出視窗

  //案 X 時讓視窗可以自動關閉
  frm.addWindowListener(new WindowAdapter(){
   public void windowClosing(WindowEvent e){
    frm.dispose();
   }
  });
 }
 /*-----------------------------------------------*/



 /*--------------------處理事件-------------------*/
 //處理 button 的事件
 public void actionPerformed(ActionEvent e){
  Graphics g = getGraphics();
  update(g);  //先清空畫面後再呼叫paint函式
 }

 //處理 TextArea 的事件
 public void textValueChanged(TextEvent e){
  //幫接收到的字串轉成整數放到 amount
  amount = Integer.valueOf(txa.getText());
 }

 //處理滑鼠移動事件
 public void mouseMoved(MouseEvent e){
  lab.setText("座標:("+e.getX()+","+e.getY()+")");
 }

 //處理滑鼠拖曳事件
 public void mouseDragged(MouseEvent e){
  lab.setText("座標:("+e.getX()+","+e.getY()+")");
 }
 /*-----------------------------------------------*/



 /*---------------------畫圖----------------------*/
 //在平面上畫出亂數產生的點
 public void paint(Graphics g){
  //陣列的大小由我們自己的輸入來決定
  int x[] = new int[amount];
  int y[] = new int[amount];
  List<Vertex> vertices = new Vector<Vertex>();

  //標出座標點
  for(int i=0; i<x.length; i++){
   //x y由亂數產生
   x[i] = (int)(Math.random()*441)+30;
   y[i] = (int)(Math.random()*405)+75;

   Vertex v = new Vertex(x[i], y[i]);
   vertices.add(v);  //做成一個座標陣列
   g.setColor(Color.red);  //設定點的顏色為紅色
   g.fillOval(x[i], y[i], 5, 5);  //畫出亂數座標點
  }

  //計算執行 Convex Hull 演算法所要花的時間
  long begin = System.currentTimeMillis();
  List<Vertex> polygon = convexhull(vertices);  //執行 Convex Hull 演算法
  long elapsed = System.currentTimeMillis()-begin;
  double seconds = elapsed/1000.0;

  //依序將最外圍的點做連線
  for(int i=1; i<polygon.size(); i++) {
   g.setColor(Color.black);
        g.drawLine(polygon.get(i-1).x, polygon.get(i-1).y, polygon.get(i).x, polygon.get(i).y);
     }
     //最後記得再把最後一個點連回第一點
     g.setColor(Color.black);
     g.drawLine(polygon.get(0).x, polygon.get(0).y, polygon.get(polygon.size()-1).x, polygon.get(polygon.size()-1).y);

     System.out.printf("處理了%d個點,耗費了 %f 秒\n", vertices.size(), seconds);
 }
 /*-----------------------------------------------*/



 /*------執行 Convex Hull,找出凸包最外圍的點-----*/
 public static List<Vertex> convexhull(List<Vertex> vertices){
  List<Vertex> polygon = new Vector<Vertex>();
  int left, right;  //左右範圍
  int dxs,dys;  //起始斜率
     int dxc,dyc;  //暫存斜率
     int dxt,dyt;  //測試斜率
     int lqc,lqt;  //長度平方(暫存/測試)
     int compareResult;  //斜率比較結果
     int x;  //存放數據用的
  Vertex current = null;  //目前拜訪的點
  Vertex next = null;  //下一個選擇點

  //先取出座標點的左右範圍
  left = vertices.get(0).x;
  right = vertices.get(0).x;
  for(int i=1; i<vertices.size(); i++){
   //依序取出 x 來做比較,藉此求出左右範圍
   x = vertices.get(i).x;
   if(x<left) left = x;
   if(x>right) right = x;
  }

  //找出最左上角的點,準備當成起點
  for(Vertex v : vertices){
  //依序把 vertices 加入 v
   if(v.x==left && (current==null || v.y>current.y))
    current = v;
    //再找出一個y最大的點,放到current
  }

  //將我們剛剛找到的起點加入polygon,就是最左上角的點
  polygon.add(current);



  //1. 我們首先定義起始斜率為無限大
  //2. 以currnt.x為基準來測試右邊的點
  //3. 在這些斜率中找出斜率最大的
  //4. 若有斜率相同的情況則取長度最長者
  //5. 新找到的斜率就作為下次的起始斜率
  //6. 繞完迴圈之後就可以得到全部座標中的斜率最大者
  dys = 1;  dxs = 0;  //先把起始斜率設成無限大
  while(current.x<=right){
   dyc = -1;  dxc = 0;  lqc = 0;
   for(Vertex v : vertices){
   //依序把 vertices 加入 v
    if(v.x>=current.x){
     dyt = v.y - current.y;
     dxt = v.x - current.x;

     //把起始斜率和測試斜率送到 compareSlope 函式作比較
     if(compareSlope(dyt, dxt, dys, dxs) == -1){
      compareResult = compareSlope(dyt,dxt,dyc,dxc);
      lqt = dyt*dyt+dxt*dxt;  //計算長度平方

      if(compareResult>=0)
       if(compareResult>0 || lqt>lqc){
        next = v;
        dyc = dyt;
        dxc = dxt;
        lqc = lqt;
       }
     }
    }
   }

   if(next == null) break;
   //若找到我們要的點以後,就把起始斜率換成最後一次的暫存斜率
   dys = dyc;
   dxs = dxc;
   polygon.add(next);  //把找出來的點加到polygon
   vertices.remove(next);  //把我們找過的點移除掉
   current = next;  //把next放到current,把它當成目前點位來繼續找下個點
   next = null;  //把next清空
  }


  //1. 我們首先定義起始斜率為無限大
  //2. 以 current.x 為基準來測試左邊的點
  //3. 在這些斜率中找出斜率最大的
  //4. 若有斜率相同的情況則取長度最長者
  //5. 新找到的斜率就作為下次的起始斜率
  //6. 繞完迴圈之後就可以得到全部座標中的斜率最大者
  dys = 1;  dxs = 0;
  while(current.x>left){
   dyc = -1;  dxc = 0;  lqc = 0;
   for(Vertex v : vertices){
    if(v.x<current.x){
     dyt = v.y - current.y;
     dxt = v.x - current.x;

     if(compareSlope(dyt, dxt, dys, dxs) == -1){
      compareResult = compareSlope(dyt, dxt, dyc, dxc);
      lqt = dyt*dyt+dxt*dxt;  //計算長度平方

      if(compareResult>=0)
       if(compareResult>0 || lqt>lqc){
        next = v;
        dyc = dyt;
        dxc = dxt;
        lqc = lqt;
       }
     }
    }
   }

   if(next == null) break;
   dys = dyc;
   dxs = dxc;
   polygon.add(next);
   current = next;
   next = null;
  }

  return polygon;
  //回傳我們找到的最外圍的點
 }
 /*-----------------------------------------------*/



 /*-------------------比較斜率--------------------*/
 public static int compareSlope(int dy2, int dx2, int dy1, int dx1){
  if(dx2!=0 && dx1!=0){
  //如果兩個數的斜率都不是無限的話...
    double test = dy2*dx1-dy1*dx2;
    return (int)Math.signum(test);
    //若 test>0 則返回 1,若 test<0 則回傳 -1,若 test 為 0 則回傳 0
  }

  else{
   //只有其中一數的斜率是無限的時候
   if(dx2!=0 || dx1!=0){
    if(dx2 == 0){
     // dy2/dx2 斜率,無限大或無限小
     return dy2>=0 ? 1 : -1;
    }
    else{
     // dy1/dx1 斜率,無限大或無限小
     return dy1>=0 ? -1 : 1;
    }
   }

   else{
   //此情況代表兩個座標點的斜率都是無窮
    if(dy2>=0){
     // dy2/dx2 斜率,無限大或無限小
     return dy1>=0 ? 0 : 1;
    }
    else{
     // dy2/dx2 斜率,無限大或無限小
     return dy1>=0 ? -1 : 0;
    }
   }
  }
 }
 /*-----------------------------------------------*/
}




執行結果圖:




















































沒有留言:

張貼留言