謝爾賓斯基三角形能用編程寫出來么?該怎麼寫?
來個Haskell版本, 這個版本的特點是, 不需要開一個 n^2 的數組來表示畫布的狀態, 而是通過對 "圖形" 做抽象, 並且實現幾個基本的 圖形上的 "組合運算", 最後通過圖形的組合運算得到結果. 因此寫這個代碼的過程中, 沒有太多的trick (不需要關心數組越界, 沒有什麼magic number).
另外由於圖形的每個具體像素的值都是在輸出的時候才產生 (通過 draw 這個函數), 所以相較於其它答主開一個n方的canvas數組來保存圖形的 push 的方式來說, 這裡用的是一種 pull 的模式. 因為用的是這種pull的模式, 我們能表達出無限大的圖形, 而把所有具體的計算都留在了圖形輸出的時候.
也因為這種pull的方式, 這段代碼的空間複雜度比較低 (這裡對空間的需求取決於函數調用棧的深度, 空間複雜度為 O(log n)), 而時間複雜度則較高 (為 O(n^2 * log n)), 其中 n 為三角形的邊長.
完整代碼如下:
import Control.Monad
vecAdd (i0, j0) (i1, j1) = (i0 + i1, j0 + j1)
vecSub (i0, j0) (i1, j1) = (i0 - i1, j0 - j1)
inRect ((r, c), (rn, cn)) (pr, pc) = pr &>= r pr &< r + rn pc &>= c pc &< c + cn
move vec g = g . (`vecSub` vec)
inset rect g0 g1 = p -&> if inRect rect p then g0 p else g1 p
draw ((r, c), (rn, cn)) g = do
forM_ [r..r+rn-1] $
-&> do
forM_ [c..c+cn-1] $ c -&> do
putStr $ g (r, c)
putStr "
"
simpleTriangle = ((r, c) -&> if (r, c) == (0, 0) then "△" else if (r, c) == (0, 1) then "" else " ")
sierpinskiTriangle 0 = simpleTriangle
sierpinskiTriangle n = let
t = sierpinskiTriangle (n-1)
h = 2 ^ (n-1)
sz = (h, 2*h)
lpos = (h, 0)
rpos = (h, 2*h)
in inset (lpos, sz) (move lpos t) . inset (rpos, sz) (move rpos t) $ move (0, h) t
drawTriangle n = draw ((0, 0), (2^n, 2^(n+1))) $ sierpinskiTriangle n
--- play ground ---
dot (r, c) = if (r, c) == (0, 0) then "X" else " "
d = draw ((0, 0), (10, 10))
t0 = simpleTriangle
t = sierpinskiTriangle
dt = drawTriangle
main = do
forM_ [0..5] $
-&> do
putStrLn $ "
Sierpinski Triangle " ++ show n ++ ":"
drawTriangle n
運行效果:
稍微說一下:
這裡做了幾個抽象:
1. 點 和 向量: type Point = (Int, Int), type Vec = (Int, Int); 為了簡單直接用tuple表示點和向量.
2. 矩形: type Rect = (Point, Vec); 這裡用矩形的左上角坐標表示位置, 用左上角到右下角的向量表示矩形的尺寸.
3. 圖形: type Graph = Point -&> Pixel; 注意這裡把圖形定義為了一個 "點到像素的函數".
4. 像素: type Pixel = String; 因為是在控制台上畫圖, 所以把像素定義為字元串.
然後基於上述抽象, 定義了幾個圖形上的運算:
1.. 圖形上的平移運算: move :: Vec -&> Graph -&> Graph , 即把給定圖形按照給定向量移動, 得到新圖形
2. 圖形上的嵌入運算: inset :: Rect -&> Graph -&> Graph -&> Graph, 即把給定圖形的指定部分嵌入到另一個圖形中, 返回新的圖形
3. 圖形的繪製: draw :: Rect -&> Graph -&> IO () , 即繪製給定圖形的給定區域
然後核心代碼就是下面這幾行:
sierpinskiTriangle 0 = simpleTriangle
sierpinskiTriangle n = let
t = sierpinskiTriangle (n-1)
h = 2 ^ (n-1)
sz = (h, 2*h)
lpos = (h, 0)
rpos = (h, 2*h)
in inset (lpos, sz) (move lpos t) . inset (rpos, sz) (move rpos t) $ move (0, h) t
其中第一行就是說, 0階sierpinskiTriangle就是一個simpleTriangle.
第二行及後面就是計算了一些參數, 最後返回一個圖形的表達式:
inset (lpos, sz) (move lpos t) . inset (rpos, sz) (move rpos t) $ move (0, h) t
其中 t 是 n-1 階的 sierpinskiTriangle,
h 是 n-1 階 sierpinskiTriangle 的高度,
sz 是 n-1 階 sierpinskiTriangle 的尺寸, 這裡用一個圖形的BBox左上角到右下角的向量表示.
lpos 和 rpos 分別是左下角三角形 和 右下角三角形的位置
Mathematica:
Graphics@Nest[Scale[#,0.5,{0,0}]~Translate~CirclePoints[0.5,3],
Triangle@CirclePoints[3],6]
Graphics@Point@FoldList[+##/2,{0,0},N@CirclePoints[3]~RandomChoice~1*^4]
ArrayPlot[CellularAutomaton[22,{{1},0},2^8],AspectRatio-&>0.87]
Nest[Subsuperscript[#,#,#],o,6]
@羅宸的Haskell實現很有意思,但是代碼對初學者來說還是相對難理解了些。
我這裡恰好有一年多前用Haskell寫的畫sierpinski三角形的代碼,也來湊個熱鬧。
infixl 5 ===
(===) = (++)
infixl 5 |||
(|||) = zipWith (++)
center n = map ((spaces ++) . (++ spaces))
where spaces = replicate (2 ^ (n - 1)) " "
-- | Impletement print function in normal recursive form with new operators
sierpinskiS :: Int -&> [[Char]]
sierpinskiS 0 = ["△"]
sierpinskiS n = center n s
===
(s ||| s)
where s = sierpinskiS (n - 1)
main = mapM_ putStrLn $ sierpinskiS 4
輸出結果如下:
△
△ △
△ △
△ △ △ △
△ △
△ △ △ △
△ △ △ △
△ △ △ △ △ △ △ △
△ △
△ △ △ △
△ △ △ △
△ △ △ △ △ △ △ △
△ △ △ △
△ △ △ △ △ △ △ △
△ △ △ △ △ △ △ △
△ △ △ △ △ △ △ △ △ △ △ △ △ △ △ △
可以看到上面的代碼比@羅宸的簡潔不少,相對要直觀些。如果用vector代替String類型,性能也會高不少。
不過當初寫這個代碼可不是為了這個簡單的目標,而是乘機將F-Alg和Free Monad都整了個遍,分別用這些高級技術實現了畫sierpinski三角形的函數。將這些東西都理解透了。
--------------- 我是分割線 -----------------------------------下面是 @羅宸的代碼的更新版,主要是使用Applicative來組合siepinski三角形。
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE FlexibleContexts #-}
import Control.Applicative
blank = const " "
triangle (0, 0) = "△ "
triangle _ = ""
addVec2 (r1, c1) (r2, c2) = (r1 + r2, c1 + c2)
subVec2 (r1, c1) (r2, c2) = (r1 - r2, c1 - c2)
inrect (xr, xc) ((r, c), (rn, cn)) = xr &>= r xr &< r + rn xc &>= c xc &< c + cn
move vec g = g . (`subVec2` vec)
draw ((r, c), (rn, cn)) g = do
forM_ [r..r+rn-1] $
-&> do
forM_ [c..c+cn-1] $ c -&> do
putStr $ g (r, c)
putStr "
"
sierpinskiT 0 = triangle
sierpinskiT n = condImage &<$&> inTop n &<*&> move (0, h) t
&<*&> (condImage &<$&> inBotL n &<*&> move (h, 0) t
&<*&> (condImage &<$&> inBotR n &<*&> move (h, 2*h) t
&<*&> blank))
where
t = sierpinskiT (n-1)
inTop n p = p `inrect` ((0, h), (h, 2*h))
inBotL n p = p `inrect` ((h, 0), (h, 2*h))
inBotR n p = p `inrect` ((h, 2*h), (h, 2*h))
h = 2 ^ (n-1)
condImage cond t1 t2 = if cond then t1 else t2
drawTriangle n = draw ((0, 0), (2^n, 2^(n+1))) $ sierpinskiT n
再次改進後的代碼如下
import Data.Monoid
type Graph a = (Int, Int) -&> a
type Vec2 = (Int, Int)
newtype CharPixel = CharPixel { getStr :: String }
instance Monoid CharPixel where
mempty = CharPixel " "
CharPixel " " `mappend` CharPixel " " = CharPixel " "
CharPixel " " `mappend` CharPixel a = CharPixel a
CharPixel a `mappend` CharPixel b = CharPixel a
triangle (0, 0) = CharPixel "△"
triangle _ = CharPixel " "
-- triangle (0, 0) = Just "△"
-- triangle _ = Nothing
addVec2 (r1, c1) (r2, c2) = (r1 + r2, c1 + c2)
subVec2 (r1, c1) (r2, c2) = (r1 - r2, c1 - c2)
inrect (xr, xc) ((r, c), (rn, cn)) = xr &>= r xr &< r + rn xc &>= c xc &< c + cn
move vec g = g . (`subVec2` vec)
draw ((r, c), (rn, cn)) g = do
forM_ [r..r+rn-1] $
-&> do
forM_ [c..c+cn-1] $ c -&> do
putStr $ g (r, c)
putStr "
"
sierpinskiT 0 p = triangle p
sierpinskiT n p = if p `inrect` ((0, 0), (h, w)) then
(centerT n t `above` (t `beside` t)) p
else
mempty
where
t = sierpinskiT (n-1)
above = aboveT n
beside = besideT n
h = 2 ^ n
w = 2 ^ (n+1)
aboveT :: Monoid a =&> Int -&> Graph a -&> Graph a -&> Graph a
aboveT n t1 t2 = t1 &<&> move (2 ^ (n-1), 0) t2
besideT :: Monoid a =&> Int -&> Graph a -&> Graph a -&> Graph a
besideT n t1 t2 = t1 &<&> move (0, 2 ^ n) t2
centerT n = move (0, 2 ^ (n-1))
drawTriangle n = draw ((0, 0), (2^n, 2^(n+1))) (getStr . sierpinskiT n)
-- drawTriangle n = draw ((0, 0), (2^n, 2^(n+1))) . fmap (maybe " " id) $ sierpinskiT n
運行後輸出結果同上。
可以從 Studio/FractalsVisualizationJ - J Wiki 里抄
f=: ,~,.~
f ^:3 ,1
1 1 1 1 1 1 1 1
1 0 1 0 1 0 1 0
1 1 0 0 1 1 0 0
1 0 0 0 1 0 0 0
1 1 1 1 0 0 0 0
1 0 1 0 0 0 0 0
1 1 0 0 0 0 0 0
1 0 0 0 0 0 0 0
" *" {~ f ^:3 ,1
********
* * * *
** **
* *
****
* *
**
*
" *" {~ f ^:2 ,1
****
* *
**
*
" *" {~ f ^:4 ,1
****************
* * * * * * * *
** ** ** **
* * * *
**** ****
* * * *
** **
* *
********
* * * *
** **
* *
****
* *
**
*
從Sierpinski三角的定義出發畫自然可以,但無論時空複雜度還是編程複雜度都太高了,其實Sierpinski三角形有個非常優美的位運算結論以及畫法
在屏幕上i行j列的像素,如果(ij)==j那就輸出這個像素,否則不輸出,這樣的圖形就是Sierpinski三角
int h = 30;
int w = 30;
for(int i = 0; i &< h; i++)
{
for(int j = 0; j &< w; j++)
{
printf("%c", ((ij)==j)?"#":" ");
}
puts("");
}
當然控制台輸出#號太丑了,我們可以用html5的canvas畫下來看效果!
&
&
&
&
&
var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");
var CanvasWidth = canvas.width;
var CanvasHeight = canvas.height;
var ImgData = ctx.getImageData(0, 0, CanvasWidth, CanvasHeight);
for(var i = 0; i &< CanvasHeight; i++)
{
for(var j = 0; j &< CanvasWidth; j++)
{
ImgData.data[(i * CanvasWidth + j) * 4 + 0] = ((i j) == j)? 255 : 0; // Red channel
ImgData.data[(i * CanvasWidth + j) * 4 + 1] = ((i j) == j)? 255 : 0; // Green channel
ImgData.data[(i * CanvasWidth + j) * 4 + 2] = ((i j) == j)? 255 : 0; // Blue channel
ImgData.data[(i * CanvasWidth + j) * 4 + 3] = 255; // Alpha channel
}
}
ctx.putImageData(ImgData, 0, 0);
&
&