Monday, December 26, 2011

以 Python 做測試驅動開發 (Test-driven development) 練習 -- 測試一個多邊形是否近似於四角形 (二)

回顧一下前篇文章中提到測試驅動開發(TDD)的三步驟:

1. 寫好程式的介面,只要能夠通過編譯即可
2. 增加測試
3. 執行測試,如果有失敗的測試的話就修改程式,然後回到步驟2

回到我們目前的IsQuadrilateral函數,目前的實作非常簡單,只是檢查傳入的多邊形是否有四個頂點,若是的話就傳回真。現在要來處理其他狀況:長的像四角形的五角形(或更多角)。解法如前篇所說,我們先取多邊形的第一個點做為p0,然後找出最遠的點p1,接著找離p1最遠的點p0。我們把這件工作交給FindFarthestPoint,此函數接受一個點p和一個集合S,傳回在集合S中離p最遠的點。這時我們依照TDD的精神,加上個測試先。testFindFarthestPoint利用我們之前定義好的square,測試離左下角最遠的點是否為右上角。

# 要放到 class TestSequenceFunctions 裡面
def testFindFarthestPoint(self):
    self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,0]), self.square)[0], numpy.array([1,1])) )
    self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,2]), self.square)[0], numpy.array([1,0])) )

def FindFarthestPoint(p, polygon):
    x = polygon[0,]-p[0]
    y = polygon[1,]-p[1]
    idx = numpy.argmax(x**2 + y**2)
    return polygon[:,idx], idx

好了,雖然繞了些路,終於萬事俱備,可以來寫判斷三角形的函數了。由於我們已經把多邊形切成兩半,各自是兩群連續的點的集合。我們可以想像從p1p2的連線,如果途中有一個凸出的點,p1到p2就會是一個三角形(或者更多角)。我使用CountMiddleVertex來計算一群連續的點之中有幾個凸出的點。這會是一個遞迴函數,先找到最突出的點p3,然後用p3切割線段,然後分別呼叫CountMiddleVertex繼續計算。

# 要放到 class TestSequenceFunctions 裡面
def testCountVertex(self):
    self.assertEqual(CountMiddleVertex(self.square2),   1)
    self.assertEqual(CountMiddleVertex(self.triangle),  1)
    self.assertEqual(CountMiddleVertex(self.square),    2)
    self.assertEqual(CountMiddleVertex(self.pentagon1), 3)
    self.assertEqual(CountMiddleVertex(self.pentagon2), 3)
    self.assertEqual(CountMiddleVertex(self.hexagon),   4)

def CountMiddleVertex(pointset, threshold=0.03):
    pointnum = pointset.shape[1]
    if pointnum <=2:
        return 0
    p1 = pointset[:, 0]
    p2 = pointset[:, -1]
    dist2 = (p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]) 
    triarea = []
    for p in range(pointset.shape[1]):
        triarea.append(PolygonArea(numpy.column_stack((p1,p2,pointset[:,p]))))
    # 如果最突出的點超過門檻值
    if max(triarea)*1.0/dist2 > threshold:
        midpoint = triarea.index(max(triarea))
        vertexnum = 1 + CountMiddleVertex(pointset[:, 0:midpoint+1])
            + CountMiddleVertex(pointset[:, midpoint:], maxnum-vertexnum);
    return 0

CountMiddleVertex測試完成後,我們就利用它來判定三角形吧。更新後的IsQuadrilateral以及完整程式如下。為了讓CountMiddleVertex執行快一些,我加了一個參數maxnum,讓它可以早點結束。

import numpy
import unittest

def PolygonArea(polygon):
    x,y = numpy.column_stack((polygon, numpy.array([polygon[0,0],polygon[1,0]])))
    return abs((numpy.dot(x[0:-1],y[1:]) - numpy.dot(x[1:],y[0:-1])))/2.0 
    
def FindFarthestPoint(p, polygon):
    x = polygon[0,]-p[0]
    y = polygon[1,]-p[1]
    idx = numpy.argmax(x**2 + y**2)
    return polygon[:,idx], idx

def CountMiddleVertex(pointset, maxnum=10, threshold=0.03):
    pointnum = pointset.shape[1]
    if pointnum <=2:
        return 0
    p1 = pointset[:, 0]
    p2 = pointset[:, -1]
    dist2 = (p1[0]-p2[0])*(p1[0]-p2[0]) + (p1[1]-p2[1])*(p1[1]-p2[1]) 
    triarea = []
    for p in range(pointset.shape[1]):
        triarea.append(PolygonArea(numpy.column_stack((p1,p2,pointset[:,p]))))
    if max(triarea)*1.0/dist2 > threshold:
        midpoint = triarea.index(max(triarea))
        vertexnum = 1 + CountMiddleVertex(pointset[:, 0:midpoint+1]);
        if vertexnum >= maxnum:
            return maxnum
        return vertexnum + CountMiddleVertex(pointset[:, midpoint:], maxnum-vertexnum);
    return 0

def IsQuadrilateral(polygon):
    x,y = polygon
    
    if len(x) < 4 or len(x)!=len(y):
        return False
   
    p1, p1idx = FindFarthestPoint(polygon[:,0], polygon)
    p2, p2idx = FindFarthestPoint(p1, polygon)
    
    if p1idx > p2idx:
        p1idx,p2idx = p2idx,p1idx
        p1,p2 = p2,p1
    
    if CountMiddleVertex(polygon[:, p1idx:p2idx+1], 2)==1 and \
        CountMiddleVertex(numpy.column_stack((polygon[:, p2idx:], polygon[:, :p1idx+1])), 2)==1:
        return True
    else:
        return False
    
class TestSequenceFunctions(unittest.TestCase):
    def setUp(self):
        self.triangle   = numpy.array( [ (0,10,0),(0,0,10)  ] )
        self.square     = numpy.array( [ (0,1,1,0),(0,0,1,1)  ] )
        self.square2    = numpy.array( [ (0,0.5,1,0),(0,-0.01,0,1)  ] )
        self.pentagon1  = numpy.array( [ (0,1,1,0,-1),(0,0,1,1,0.5)  ] )
        self.pentagon2  = numpy.array( [ (0,1,1,0,-0.01),(0,0,1,1,0.5)  ] )
        self.pentagon3  = numpy.array( [ (0,1,1,0,-0.21),(0,0,1,1,0.5)  ] )
        self.hexagon    = numpy.array( [ (1,1,0,-1,-1,0),(1,-1,-2,-1,1,2)  ] )
        
    def testFindFarthestPoint(self):
        self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,0]), self.square)[0], numpy.array([1,1])) )
        self.assertTrue( numpy.array_equal(FindFarthestPoint(numpy.array([0,2]), self.square)[0], numpy.array([1,0])) )
        
    def testCountVertex(self):
        self.assertEqual(CountMiddleVertex(self.square2),   1)
        self.assertEqual(CountMiddleVertex(self.triangle),  1)
        self.assertEqual(CountMiddleVertex(self.square),    2)
        self.assertEqual(CountMiddleVertex(self.pentagon1), 3)
        self.assertEqual(CountMiddleVertex(self.pentagon2), 3)
        self.assertEqual(CountMiddleVertex(self.hexagon),   4)
    
    def testIsQuadrilateral(self):
        self.assertEqual(IsQuadrilateral(self.triangle),   False)
        self.assertEqual(IsQuadrilateral(self.square),     True)
        self.assertEqual(IsQuadrilateral(self.pentagon1),  False)
        self.assertEqual(IsQuadrilateral(self.pentagon2),  True)
        self.assertEqual(IsQuadrilateral(self.pentagon3),  False)
        self.assertEqual(IsQuadrilateral(self.hexagon),    False)

if __name__ == '__main__':
    unittest.main()

Sunday, December 18, 2011

以 Python 做測試驅動開發 (Test-driven development) 練習 -- 測試一個多邊形是否近似於四角形 (一)

這次用Python來練習驅動程式開發。
TDD簡單的說只有三步
1. 寫好程式的介面,只要能夠通過編譯即可
2. 增加測試
3. 執行測試,如果有失敗的測試的話就修改程式,然後回到步驟2

我要寫的程式是輸入一個多邊形的頂點,檢查是否為近似四邊形。
這個問題可以很簡單,我們只需要檢查多邊形的頂點數目,
如果是四個點的話就是四邊形。
當然我希望這個程式的能力不只如此,不然就沒有練習的效果啦。
其實這個問題是從實際的問題演變過來的,實際上是應用在擴增實境中,
我們從攝影機抓取影像,然後找出影像中的正方形,做為一個參考點。
一個平片上的正方形從不同角度去看,會變成一個任意的四邊形,
我們從影像分割出許多多邊形,然後判斷像不像四邊形。然而由於影像處理必定會有誤差,
所以一個四邊形可能有一些凸出的部分,而會變成五角形或六角形。我們的程式須要能處理這種情況。
這也是採用TDD的理由,我們可以一步一步改進我們的程式。

開始囉
首先定義函數IsQuadrilateral,輸入為一個多邊形,輸出真假值。由於現在還沒有測試,
不假思索就可以得到一個最簡單的實作方法,一律傳回假就好。
def IsQuadrilateral(polygon):
  return False

接著是增加測試,利用Python的unittest模組,我們將測試程式包在TestSequenceFunctions物件中。

import unittest
import numpy
class TestSequenceFunctions(unittest.TestCase):
  def setUp(self):
    self.triangle   = numpy.array( [ (0,1,0),(0,0,1)  ] )
    self.square     = numpy.array( [ (0,1,1,0),(0,0,1,1)  ] )
  def testIsQuadrilateral(self):
    self.assertEqual(IsQuadrilateral(self.triangle),    False)
    self.assertEqual(IsQuadrilateral(self.square),      True)
if __name__ == '__main__':
    unittest.main()

setUp函數是用來建立測試用的資料,這邊我們建立了兩個二維陣列來表示三角形和正方形的頂點。
numpy是常用的數學函式庫,程式中的 [ (0,1,0),(0,0,1)  ] 表示一個二維陣列,第一列是三個頂點的x座標 0,1,0,
第二列三個頂點的y座標 (0,0,1)。square物件存放的是正方形,有四個頂點。

完成以後就是執行測試。結果在第二個測試失敗了,所以我們要回頭修改程式。第二版的程式如下:檢查傳入的多邊形是否含有四個頂點,
有的話就為真,否則為假。

def IsQuadrilateral(polygon):
    x,y = polygon
    if len(x) != 4 or len(x)!=len(y):
        return False
    return True

現在回頭再跑測試,就可以過關啦。當然我們不能停在這裡,要繼續加測試才行。

五角形1:
self.pentagon1 = numpy.array( [ (0,1,1,0,-1),(0,0,1,1,0.5)  ] )
五角形2:
self.pentagon2 = numpy.array( [ (0,1,1,0,-0.01),(0,0,1,1,0.5)  ] )
五角形3:
self.pentagon3 = numpy.array( [ (0,1,1,0,-0.21),(0,0,1,1,0.5)  ] )
此處我們用了三個五角形,長相如下:

其中第二個五角形其實是四角形,只是左邊有一點小突出而已,我們的新測試如下:

def testIsQuadrilateral(self):
    self.assertEqual(IsQuadrilateral(self.triangle),   False)
    self.assertEqual(IsQuadrilateral(self.square),     True)
    self.assertEqual(IsQuadrilateral(self.pentagon1),  False)
    self.assertEqual(IsQuadrilateral(self.pentagon2),  True)
    self.assertEqual(IsQuadrilateral(self.pentagon3),  False)

修改演算法,四邊形是兩個三角形所組成,所以我們先嘗試找到四邊形的兩個頂點。這裡我用了一個經驗法則,先任意選取polygon上的一個點p0,然後選取離p0最遠的點p1,接著選取離p1最遠的點p2,如此一來p1和p2就是近似四邊形上的兩個點。p1p2的連線把polygon分成兩半,我們接下來只需要判斷這兩半是否為三角形即可。請見下面的圖。

為了要找到離p0最遠的點,我需要再寫一個函數FindFarthestPoint,同時需要新的測試來驗證這新的函數。
今天就先寫到這裡吧。來回顧一下到目前為止我們的程式看起來如何:

import unittest
import numpy

# 主函數
def IsQuadrilateral(polygon):
    x,y = polygon
    if len(x) != 4 or len(x)!=len(y):
        return False
    return True

# 測試程式
class TestSequenceFunctions(unittest.TestCase):
    # 此函數在 testIsQuadrilateral 被執行前會被呼叫,以建立測試所需的物件
    # 想一想為何需要一個獨立的 setUp 函數呢?
    def setUp(self):
        self.triangle   = numpy.array( [ (0,10,0),(0,0,10)  ] )
        self.square     = numpy.array( [ (0,1,1,0),(0,0,1,1)  ] )
        self.pentagon1  = numpy.array( [ (0,1,1,0,-1),(0,0,1,1,0.5)  ] )
        self.pentagon2  = numpy.array( [ (0,1,1,0,-0.01),(0,0,1,1,0.5)  ] ) # 這個測試會失敗
        self.pentagon3  = numpy.array( [ (0,1,1,0,-0.21),(0,0,1,1,0.5)  ] )

    def testIsQuadrilateral(self):
      self.assertEqual(IsQuadrilateral(self.triangle),   False)
      self.assertEqual(IsQuadrilateral(self.square),     True)
      self.assertEqual(IsQuadrilateral(self.pentagon1),  False)
      self.assertEqual(IsQuadrilateral(self.pentagon2),  True)
      self.assertEqual(IsQuadrilateral(self.pentagon3),  False)

if __name__ == '__main__':
    unittest.main()  # 會呼叫TestSequenceFunctions裡所有的測試函數

Saturday, December 3, 2011

我的軟體生涯發展規劃

到業界工作進一年來,有幸遇到功力深厚又照護晚輩的前輩。由於師徒制在軟體業不盛行,一般的人不會花多餘時間指導新人,一來是沒有回報,二來是怕被超越。對我來說學到的東西中,是前輩指出我的兩個最大的缺點。

1. 應付了事。我向來是那種很會考試的人,對於大多數問題都能找到一個答案並且應付過去。但是久而久之會養成一種交差了事的態度,只求解決眼前的問題,沒有去思索整個問題的來龍去脈,並且透徹了解問題。

2. 獨善其身。所謂教學相長,我平時學到的東西很少與人分享,一方面是個性不主動親近人,另一方面是表達能力不強。


前輩給我的建議是先改進第二點,練習成為某個領域的大師,並且能幫別人速成。所謂速成就是指:比自己更快速學成。他指出唯有能幫別人速成,才表示你已經學的透徹。由於現在沒有學生,我先嘗試把自己學習的心得以教學的方式放在部落格上。

最後分享一篇文章做為結尾: 給學生與軟體業新進的十招

Sunday, November 27, 2011

家庭幫手機器人的需求

我是個很懶的人,在家裡時用過的東西常隨手就丟,完全違反"物歸原位"的整潔原則哈。所以想說如果能有個機器人幫我拿東西收東西多好。

情境:我所需要的東西放在一個儲物櫃中,我呼叫機器人幫我拿來,用完後幫我收好。
為了達成這個目標,這樣的機器人需要以下能力:

接收命令的能力。最好是用語音輸入,可以簡單的用單字即可,如"剪刀"

辨識物體的能力。視覺能力,能夠在儲物櫃中找到特定物體。
移動能力。可以來回於使用者和儲物櫃之間。
取得物體的能力。打開儲物櫃,取出物體,所以要有"手"。

以上幾項技術早已存在,語音輸入有Apple最近發表的Siri語音助理,特定物體辨識在電腦視覺領域已經有一定的成熟,移動能力早已成熟,最後是拿取物體的能力,這部分也不成問題。照這樣看來,我們在市面上應該能找到這樣的產品才對。所以離題一下,來搜尋看看有哪些公司已經發表家事機器人。

Willow Garage
主要產品: PR2, OpenCV, ROS (Robot Operating System)

iRobot
主要產品: Roomba (地上亂跑的吸塵器那隻), Scooba (這隻可以拖地!), Looj (清除房屋外沿的排水溝), Verro (清潔游泳池)

比照這兩家公司,可以看出Willow Garage走通用型機器人,而iRobot是專用型的。

結尾來對照一下現實中的機器人和想像中的機器人做家事吧~




摺衣服



摺襪子

Friday, November 11, 2011

為什麼我們還沒有萬能家務機器人?

2012將至,
為什麼我們還沒有機器人可以幫我整理衣服?
為什麼我們還沒有機器人可以幫我收拾房間?

或者更積極一些的問題
我們要克服哪些技術上的困難才能做出家務機器人?
哪個技術是我最感興趣的?

牢騷才發完,便發覺最近Honda公開了Asimo的最新情況 [1] (連結內含影片)
我們得到甚麼新東西?
1. 單腳跳,很有意思,不過還不知道有甚麼用
2. 時速 9km小跑,還不錯,當個跑腿小弟挺合適
3. 推餐車,單手轉開水壺蓋,並將水倒至紙杯中。Cool,有服務生的潛力。

唯一的遺憾是Sony向來保密,所以沒有內部的技術資料。我認為走開源路線的Willow Garage會有更好的發展。他們的PR2現在可以...幫你買三明治做早餐摺襪子

Saturday, August 13, 2011

Firebug幫你抓取網頁上Flash內顯示的圖片

好幾個月沒有新文章。這段時間本來想寫些 Perl 和 Web 技術的學習心得,但是總覺得沒啥好寫,要寫教學類的文章太花時間,而且也不會寫的比現有的文章更好;要寫最新技術也不成,自己還在入門的程度。考慮太多反而啥都沒寫。
後來想想反正我也不是靠blog吃飯,只要寫的東西能讓自己高興就好了,所以發了這篇短文 :)

由來是這樣的,最近在 PTT c_chat 版上看到關於 Creative Comic Collection 創作集的討論正夯(沒看過的人趕快到 http://digitalarchives.tw/Theme/CCC/CCC6/index.jsp 去看)。我覺得這是學界的國家燒錢計劃中成果相當好的一個。

CCC第六期的主題是青澀韶光-台灣女學生圖錄,我發現網頁上方的Flash有提供預覽的功能:
http://digitalarchives.tw/Theme/CCC/CCC6/CCC6_02.jsp
不過很可惜的是他的桌步下載只有一張而已啊 [1]

為了多抓幾張圖,我嘗試用Flash編輯工具來看看Flash裡面是否有圖,不過很可惜的是他的Flash是專門做幻燈片的,所以是在執行時才會載入圖片。

後來轉念一想,既然Flash是在執行時才向Server讀取圖片,那我只要知道Flash程式透過瀏覽器去哪邊抓圖不就好了。嘗試一下Firebug的網路連線監看功能,馬上就看出來啦。


http://digitalarchives.tw/Theme/CCC/CCC6/C6/large/01.jpg

http://digitalarchives.tw/Theme/CCC/CCC6/C6/large/03.jpg


不過圖片果然是被裁剪過的,這告訴我們不要只想偷別人的圖哪 XD,應該要直接向作者請求才對

[1] 桌步由此下載: http://digitalarchives.tw/Theme/CCC/download.jsp 蚩尤那張不錯,不過我最想要的是A士和Salah.D的那兩張 :)

Sunday, April 10, 2011

初學 XSLT 心得: 將 XML 文件轉換成 MediaWiki 文件

這篇文章紀錄我接觸 XSLT 的心得,可供有興趣的朋友參考。

首先解釋一下主題,假設我們手上有一份 XML 文件,是一份紀錄書店銷售紀錄的 XML 文件,內容為各個分店所賣出的書籍和BD。

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="store2wiki.xsl"?>
<sale>
    <store name="Taipei">
        <book ISBN="9784757733299">
            <title>バカとテストと召喚獣</title>
            <author>井上堅二</author>
        </book>
        <book ISBN="9784757735057">
            <title>バカとテストと召喚獣2</title>
            <author>井上堅二</author>
        </book>
        <BD EAN="4935228096671">
            <title>Madoka Magica Vol.1"</title>
        </BD>
    </store>
</sale>

這樣的文件,正常人類是不會想看的,連我自己打起來都很累。那個角括號看起來真的很不習慣。所以啦,為了要把這份文件做成易讀的格式,我決定用 wiki 的條列方式來呈現此文件,利用縮排來呈現 XML 中的樹狀結構。
  • Store: Taipei
    • Book (IAN=978-4757733299)
      • title: バカとテストと召喚獣
      • author: 井上堅二
    • Book (IAN=978-4757735057)
      • title: バカとテストと召喚獣2
      • author: 井上堅二
    • BD: (IAN=4935228096671)
      • title: Madoka Magica Vol.1

這樣看起來是不是清楚多了?
wiki 的種類很多,我採用 Wikipedia 的系統 MediaWiki,用它的語法的話,文件必須寫成如下的形式:

* store: Taipei
:* book (IAN=9784757733299)
::* title: バカとテストと召喚獣
::* author: 井上堅二
:* book: (IAN=9784757735057)
::* title: バカとテストと召喚獣2
::* author: 井上堅二
:* BD: (IAN=4935228096671)
::* title: Madoka Magica Vol.1

每一行前面的星號是標明項目,冒號的數量表示縮排的深度。

問題定義完成。在開始之前,先解釋一些名詞
XML: 全名是 Extensible Markup Language,中文可翻成延伸標記語言。這是一種表示資料的方式。邏輯上來看,是將資料表示成樹狀結構,每個element都會有自己的屬性,並且可以擁有子element。不熟的朋友可參考 http://www.w3schools.com/xml/

XSLT: 全名是 XSL Transformations,可用來將XML文件轉換成另外一個 XML 或 HTML。不過當你了解他的轉換原理後,就會了解到他可以將XML轉成任何格式的文件。這也就是我這次嘗試使用XSLT將XML轉成MediaWiki的理由。

XSLT讓我們定義轉換XML文件的方法,簡單來說我們要提供的是一套規則,說明對於哪些element要施行何種轉換,瀏覽器會依據規則幫我們轉換文件。簡單的說,瀏覽器會從樹根出發,採用一個可以施行的規則,輸出對應的文字。一般說來在規則中會指示瀏覽器移動到子element,並繼續套用規則。

開始動工。我設計規則只有一條: 印出目前的 element 的所有屬性,每個一行,然後繼續處理子element。大約十多行 XSLT 就可以做到。

指定規則是用 template 指令: <xsl:template match="*">
我們用match來指定element的名稱,有符合的就會套用此規則,這裡採用了萬用字元*,所以可以對應到任何element。
接下來用<xsl:value-of select="name()"/>印出此element的名稱。接著是用<xsl:for-each select="@*">找出此element的所有屬性,注意到屬性的開頭是@。
以上三個指令就能讓我們作絕大多數的事,template是用來建立規則,value-of 是用來輸出文字,for-each是用來處理每個子element。


說起來簡單,麻煩的是要在每行的最前面加入冒號,這必須根據目前的深度來決定。
為了解決這個問題,我用了一個小技巧,由於在每個element我們可以使用 ancestor::* 取得他的祖先,這會是一個sequence,因此我使用 for-each 指令,每看到一個祖先element就印出一個冒號。請見第7行。
另外有兩個小地方要注意。
  1. XML 中的空白字元: 回頭看看我們的 XML 檔案,裡面含有縮排的空白字元,但是我們輸出時不想要這些空白字元(還有換行符號!)所以加入第三行指令 <xsl:strip-space elements="*"/> 以去除這些字元,各位可以把這行拿掉看看會如何 :)
  2. 將最內層的 element 和 parent 一起顯示: 你會注意到 title 和 author 這兩個 element 內部只含有文字,對於 XSLT 來說,文字也算是 element 的一個子 element,所以依照我們的規則,他會被顯示在下一行。但是我不想要這種結果,所以就採用了11~13行的手法,11行表示只套用內含文字的子element,然後12行印出換行,最後13行再套用剩餘的子element。

總計15行code搞定!

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:output indent="no" method="text" />
    <xsl:template match="text()">:<xsl:value-of select="."/></xsl:template>
    <xsl:template match="*">
        <xsl:for-each select="ancestor::*">:</xsl:for-each>* <xsl:value-of select="name()"/>
        <xsl:for-each select="@*">
            <xsl:text> (</xsl:text><xsl:value-of select="name(.)"/>=<xsl:value-of select="."/><xsl:text>)</xsl:text>
        </xsl:for-each>
        <xsl:apply-templates select="text()"/>
        <xsl:text>&#10;</xsl:text>
        <xsl:apply-templates select="*"/>
    </xsl:template>
</xsl:stylesheet>

參考資料
[1] W3C 的官方文件 http://www.w3.org/TR/xslt
[2] FAQ 專區 http://www.dpawson.co.uk/xsl/sect2/sect21.htm

Friday, March 11, 2011

讓 Perl 走進你的生活

本文是用來回顧剛接觸 Perl 的心得,以及推薦新手跳入 Perl 所寫的。

從 1987 年 Larry Wall 發明 Perl 以來,今年 Perl 已經24 歲,可能比你我還年輕,不過以電腦語言的角度來看算是老了。筆者在多年前就已經接觸過 Perl,可惜當時覺得程式艱澀難懂,不如 C 來的直接。直到最近因工作需要重新學習,才發覺過往的偏見。

借用 Alan Perlis 的話: "A language that doesn't affect the way you think about programming, is not worth knowing." (若是一種語言不會影響你對寫程式的想法,那便不值得學習。)
而讓我改變對寫程式的想法的,便是 Learning Perl 這本書。

Learning Perl的作者 Randal L. Schwartz (以下簡稱 Randal)在書的第一章就指出,Perl是非常高階的語言,所以code的密度很高,長度只有相對應的C的四分之一到四分之三。我認為用一個中文字來表現Perl的編程精神就是""。短程式的好處不少,不論是閱讀時間或是寫程式的時間都比較短。這就像是自然語言中的成語,可以把一段故事濃縮成寥寥數字。

借用 Randal 第一章的程式碼來說明


while (<>) {
  chomp;
  print join("\t", (split /:/)[0, 2, 1, 5] ), "\n";
}

希望第一次看到 Perl 的朋友不要轉頭離開。
任何程式語言的目的都是為了幫助使用者解決問題,而不是折磨使用者。
這段Perl程式目的是: 依序讀取命令列中每個檔案的每一行,去除尾部的換行符號厚,依冒號分隔欄位,並列印出其中四個欄位的值。假設我們有下的檔案 data.txt:

Id:Name:Age:Gender:Height:Weight

001:John:20:Male:180:70
002:Alice:17:Female:160:50



當我們在命令列下這樣的指令時:

> perl program.pl data.txt

結果會是:

Id      Age     Name    Weight
001     20      John    70
002     17      Alice   50


如果你看到這裡,一定會想知道這段程式要怎麼解析,<>是甚麼? chomp是甚麼? join 和 split 是甚麼?
在此筆者不會做完整解釋,請隨著 Randal 一同進入 Perl 的世界即可,那過程會比我現在說明得更加有趣數十倍。不過我想帶給各位Perl這種所謂"高階"程式的精神,就是要符合使用者的自然習慣。我們可以用一種很不嚴謹的角度來看剛才這段程式。首先我們想要一行一行處理資料,所以就需要一個 while 迴圈,由於我們要從命令列指定的檔案讀出資料,這整件事就用一個名詞 <> 來表示,讀到一行資料以後要用冒號分隔,所以就使用 split。

至於讀者可能會問說,那讀入的資料是暫存在哪個變數呢? Perl認為這是可以省略的變數,所以動了一些手腳。就好比我們在日常生活中,你拿起一個裝水的杯子,下一個動作是喝水,你不需要再多說明我是從手上的杯子喝水的。

最後結尾放上 Learning Perl 這本書的封面,各位請不要拿錯了呀 :)

Saturday, February 12, 2011

shuffling / random permutation

又到了野人獻曝的時間了 :)

最近和前輩在聊面試有那些問題可以出
前輩提到一個經典的問題:如何隨機排序一組撲克牌?
換個說法,就是要產生一種52張牌的排列。

前輩提到這個問題可以用排序演算法來解決。
我上維基查了一下,果然在Knuth的大作 The art of computer programming 中有給出兩個演算法 [1]。

假設我們要排序的陣列是 src,大小為N,以 0-based 作為索引方式。
第一個演算法是這樣的:產生另外一個陣列 a,給 a 中的每一個元素一個亂數,接下來對 a 排序,排序的同時將 src 內元素依相同順序移動。排序時可以選用任意的演算法,因此可以在 O(N lg N) 時間內完成。

Knuth 書中的第二個方法更有效率,只需 O(N) 時間即可完成。演算法是: 從 i=N-1 開始依次往前走訪陣列的每個位置,對於每個 i ,產生一個亂數 0<=j<i ,並交換 src[i], src[j]。以 python 來寫的話會像這樣:

for i in range(N-1, 0, -1):
    j = random.randint(0,i)
    src[i], src[j] = src[j], src[i]

有看過這些演算法的朋友會覺得到目前為止都是老調重彈,沒錯,請在往下看。我認為這兩個演算法其實是有關連的,第二個方法可視為第一個方法的特例: 也就是使用 selection sort 來做排序。selection sort 的想法為,每次從未排序的數列中挑出最大(或最小)的一個,將其移到排序完的數列。用簡單的例子來表現:

假設我們要按遞增順序排序這四個數字: 3 2 4 1

第一步是找出最大的數字 4,並放到陣列尾端
3 2 (4) [1] => 3 2 1 | 4

第二步會找到 3,並放到 4 的前面
(3) 2 [1] | 4 => 1 2 | 3 4

接下來會找到 2,順序不變
1 (2) | 3 4 => 1 | 2 3 4

排序完成!

觀察每一步的過程,可分成兩階段,第一階段是找出未排序數列中最大的數字,第二階段是交換到排序數列的開頭。其中第一階段原本要花費 O(n) 的時間以找出最大的值所在的位置,但是既然在第一個演算法中,每個位置的值都是亂數,所以我們可以直接從中隨機挑一個位置當作是最大的數字,這樣有相同的效果,而且時間複雜度只有 O(1)。

好,獻曝完畢 :p

以 selection sort 能夠說明第二個演算法是第一個方法的特例,也許嘗試其他排序法會帶來不同的發現呢。

[1] http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm

Friday, January 14, 2011

Kurukshetra Athena 來自印度的數學程式競賽

上週從 Project Euler 傳來一個數學程式競賽的消息。
競賽的網址如下:
http://www.athena.kurukshetra.org.in/

這個競賽的題目都是數學相關問題,包含動態規劃、數論和許多領域,厲害的人也許能夠不依賴電腦就算出答案哩。雖然今年比賽已經結束了,有興趣的人還是可以去看看題目,並期待明年的比賽!

我今年沒有花很多時間,一周的時間內只完成了七題 :p
Score: 700 (完成七題)
Rank: 241

Question 6

There are 52735727364727372 students and 2889221271829121 teachers in a school . On children's day each teacher brings x toffees with him/her . All the toffees are collected and distributed equally to all the students . At the end it was found that there was exactly one toffee remaining .
What is the least possible value of x ?


我的想法
恩,這是很經典的一個問題。
首先我發現到分配之後只剩下一顆太妃糖,這表示老師的數目a和學生的數目b是互質的。接著把未知數 x 和 a,b 關聯起來,並且引入一個輔助變數 y 來表示每個學生所拿到的太妃糖數目,寫成數學形式是:
ax-by=1

接著我去查詢兩個互質的數有甚麼關連,發現了一個形式相似的公式: Bézout's identity,此公式說對於任意非零整數a和b,令其最大公因數為d,則存在x和y使得 ax+by=d。

要解出 x 和 y 可使用 Extended Euclidean algorithm,用 python 來寫的話只需要六行,而且還支援大數運算,真是相當輕鬆。

def extended_gcd(a,b):
  """Return x,y satisfy ax+by=gcd(a,b)"""
  if a % b == 0:
    return 0,1
  else:
    r = extended_gcd(b, a % b)
  return r[1], r[0] - (r[1] * (a / b))


找到 x, y 之後回到原來的問題。理論上符合 ax+by=d 的 (x,y) 有無限多組,此題目要求我們找的 x 為最小的正整數,剛好 extended_gcd 演算法得到的就是題目所要的。