遍歷台北捷運109個車站要花多久時間? 基於 Held Karp 算法尋找最佳路線!

閱讀時間約 26 分鐘
台北捷運集章全制霸

台北捷運集章全制霸

本文搬運自本人部落格: 米蟲的程式小窩

前言

台北捷運最近推出了一個很瘋狂的集章全制霸活動,集滿5條捷運路線(不含環狀線),總共109個車站,就可以抽大獎,到底有誰會閒閒沒事想參加這個活動? 啊,我女朋友剛剛問我要不要參加,沒事沒事,真是個好活動! 問題來了,該如何用最短的時間集齊所有車站並回到出發地呢?

本文將使用 Held Karp 算法來計算從台北車站出發,遍歷 109 個車站,並在最後回到台北車站所需花費的最短時間與路徑,不想看過程的人也可以直接到最後面看結果。XD

旅行推銷員問題

肯定有人發現了,這本質就是一個旅行推銷員問題,可以簡稱為 TSP問題 (Travelling salesman problem)。借用維基百科的介紹,其問題內容為「給定一系列城市和每對城市之間的距離,求解訪問每座城市一次並回到起始城市的最短迴路。」

我們可以使用台北捷運開放資料 API 中的 捷運路線基本資料 來建立 Graph,再搭配 TSP 算法理論上就能算出最佳路徑與花費時間了,為了方便計算先不考慮班次跟轉乘時間了。另外,雖然環狀線不在集章活動的範圍內,但身為一條「捷徑」,肯定還是要考慮進去的

  • 資料格式
lines = [
        {
        "LineNo": "BL",
        "LineID": "BL",
        "RouteID": "BL-1",
        "TrainType": 1,
        "TravelTimes": [
          {
            "Sequence": 1,
            "FromStationID": "BL23",
            "FromStationName": {
              "Zh_tw": "南港展覽館",
              "En": "Taipei Nangang Exhibition Center"
            },
            "ToStationID": "BL22",
            "ToStationName": {
              "Zh_tw": "南港",
              "En": "Nangang"
            },
            "RunTime": 112,
            "StopTime": 0
          },

          ... and more
    ]
  • 建 Graph
original_graph = defaultdict(dict)
for line in lines:
    for edge in line["TravelTimes"]:
        start = edge['FromStationName']['Zh_tw']
        end = edge['ToStationName']['Zh_tw']
        time = edge["RunTime"]
        original_graph[start][end] = time
        original_graph[end][start] = time

Held Karp 算法

Held Karp 算法是基於動態規劃來求解TSP問題的一種算法,簡單來說,這個算法會遍歷所有可能的狀態,包括已訪問節點所在節點,並根據當前狀態下去計算下一步該如何走。

為了記錄所有可能,我們可以用 binary 來表示已訪問節點,1表示已經過,0表示還沒經過,舉例來說

  • 000: 表示尚未訪問任何節點
  • 001: 表示已訪問第1個節點
  • 101: 表示已訪問第1跟第3個節點

Held Karp 算法理論上能求出最佳解,可惜時間複雜度高,將已訪問節點(2n) x 當前節點(n) x 下一節點(n),會得到時間複雜度是 O(n22n)

Anyway,時間複雜度那麼高,最多也就容許圖中有20~25個節點,台北捷運109個站是要算到天荒地老? 所以我們的下個目標是要將 Graph 縮小!

BTW, 因為算法需要知道任意兩點之間的成本,所以下文中任意兩點間的距離使用 Dijkstra 來計算最短路徑。

Cut the tails and circle

仔細觀察捷運圖,會發現大部分捷運尾巴的部分是可以分開計算的,比如民權西路到淡水、大安到動物園等,直覺上來說這些路線肯定只能原路折返,更準確地講我們可以不斷地將 indegree 為 1 的節點移除掉,用剩餘的節點計算就好。

接著看到文湖線與板南線圍起的圓圈,如果你今天要用最快的時間遍歷圓圈內的所有車站,你會怎麼走這個圓圈? 要馬順時鐘繞完要馬逆時鐘繞完,絕對不會有繞一半往回走的情況,所以這一段我們也可以先移除。

真正要考慮的部分

真正要考慮的部分

tails = [
    ('淡水', '民權西路'), ('新北投', '北投'), ('蘆洲', '大橋頭'),
    ('迴龍', '頭前庄'), ('頂埔', '板橋'), ('南勢角', '景安'),
    ('新店', '大坪林'), ('小碧潭', '七張'), ('松山', '南京復興'),
    ('動物園', '大安'), ('象山', '大安')
]

# 拆成3段算,否則用最短路徑算會直接走下去
circles = [
    ("南京復興", "內湖"), ("內湖", "南港"), ("南港", "忠孝復興")
]

Compress path

算了一下,剩下的節點還是太多了啊,這時候就拿出第二招「路徑壓縮」,將不重要的節點從路徑中移除。舉例來說,從古亭到大坪林雖然要經過 4 站 (台電大樓、公館、萬隆、景美),但這 4 個站都在同一條線上且沒有分支,可以把它們當作是順便「路過」,將這些中間的節點移除,用頭尾的節點做計算就好。

前面提到的圓圈部分其實也能算是一種路線壓縮 (忠孝復興 <--> 南京復興),只留下頭跟尾,中間部分都省略。

路徑壓縮示意圖

路徑壓縮示意圖


# Key Stations
key_stations = [
    '民權西路', '雙連', '中山', '台北車站', '台大醫院', '中正紀念堂',
    '中山國小', '行天宮', '松江南京', '忠孝新生', '東門',
    '古亭', '南京復興', '忠孝復興', '大安', '北門', '西門',
    '小南門', '善導寺', '頭前庄', '板橋', '景安', '大坪林'
]

# compress path
n = len(key_stations)
graph = [[0]*n for _ in range(n)]
sub_paths = defaultdict(dict)
for start_idx in range(n-1):
    for end_idx in range(start_idx+1, n):
        cost, path = dijkstra(original_graph, key_stations[start_idx], key_stations[end_idx])
        graph[start_idx][end_idx] = cost
        graph[end_idx][start_idx] = cost
        sub_paths[start_idx][end_idx] = path
        sub_paths[end_idx][start_idx] = path[::-1]

如果我們只用頭跟尾去計算最佳路線,那我們要如何保證算法會經過那些被我們刪掉的節點呢? 同樣舉圓圈部分為例,要走入圓圈的前提是其中一段路的起點與終點分別是南京復興或忠孝復興,如果算出來根本不是這樣走呢?

其中一個解決方法是「提供誘因」,引誘算法選擇走這些路線,對於最短路徑算法來說,最大的誘因是什麼呢? 答案就是距離/時間,只要將兩點之間的距離/時間設為 0,那算法便會更偏好這條路徑。(當然,最後是會加回去的。)

# 引誘路線
key_paths = [
    ("忠孝復興", "南京復興"), ("民權西路", "頭前庄"), ("西門", "板橋"),
    ("古亭", "景安"), ("古亭", "大坪林"), ("大安", "東門")
]

adjusted_graph = [row[:] for row in graph]
for start, end in key_paths:
    start_idx = key_stations.index(start)
    end_idx = key_stations.index(end)
    adjusted_graph[start_idx][end_idx] = 0
    adjusted_graph[end_idx][start_idx] = 0

既然這個做法聽起來那麼讚,那就把看起來能壓縮的路線全部壓一遍吧? 很遺憾,也不是所有路線都管用,舉例,從民權西路經中山國中、行天宮到松江南京的路線,就算將距離設為 0,在很多情況下算法還是不會經過,而是選擇走其他條路,具體來說究竟哪些可以壓縮哪些不行我也說不清楚,如果有想法的話歡迎提供建議! 整體來說,將路線密集區的節點保留起來,可以保留各種路線被算法嘗試的可能性。

算法實現

Held Karp

from tqdm import tqdm

def held_karp(graph, adjusted_graph, start, end):
    '''
    graph: n*n 的 matrix of distance
    adjusted_graph: n*n 的 matrix of adjusted distance
    start: (int) 起點的 index
    end: (int) 終點的 index
    '''

    n = len(adjusted_graph)
    dp = [[float('inf')] * n for _ in range(1 << n)]
    parent = [[-1] * n for _ in range(1 << n)]  # 用來追蹤路徑
    dp[1 << start][start] = 0  # 起點,start

    for mask in tqdm(range(1 << n)):
        for i in range(n):
            if not (mask & (1 << i)):
                continue  # 節點 i 不在當前的子集中
            for j in range(n):
                if mask & (1 << j):
                    continue  # 節點 j 已經在子集中
                new_mask = mask | (1 << j) # 將節點 j 加入子集
                if dp[new_mask][j] > dp[mask][i] + adjusted_graph[i][j]:
                    dp[new_mask][j] = dp[mask][i] + adjusted_graph[i][j]
                    parent[new_mask][j] = i

    # 計算到達終點的最小成本
    min_cost = float('inf')
    last_node = -1
    for i in range(1, n):
        cost = dp[(1 << n) - 1][i] + graph[i][end] # 回起點所花費的成本要用原本的 graph 做計算
        if min_cost > cost:
            min_cost = cost
            last_node = i

    # 重建路徑
    path = []
    mask = (1 << n) - 1
    while last_node != -1:
        path.append(last_node)
        new_mask = mask ^ (1 << last_node)
        last_node = parent[mask][last_node]
        mask = new_mask

    path.reverse()
    if path[-1] != end:
        path.append(end)

    return min_cost, path

Dijkstra

import heapq

def dijkstra(graph, start, end):
    # 儲存從起點到各點的最短距離
    distances = {vertex: float('infinity') for vertex in graph}
    distances[start] = 0

    previous_nodes = {vertex: None for vertex in graph} # 紀錄前一個點
    priority_queue = [(0, start)] # min heap

    while priority_queue:
        current_distance, current_vertex = heapq.heappop(priority_queue)

        if current_vertex == end: # 抵達終點即停止計算
            break
        if current_distance > distances[current_vertex]:
            continue

        for neighbor, weight in graph[current_vertex].items():
            distance = current_distance + weight
            if distance < distances[neighbor]: # 找到更短路徑則更新距離、加入佇列
                distances[neighbor] = distance
                previous_nodes[neighbor] = current_vertex
                heapq.heappush(priority_queue, (distance, neighbor))

    # 重建最短路徑
    path = []
    current_node = end
    while current_node is not None:
        path.append(current_node)
        current_node = previous_nodes[current_node]
    path.reverse()

    return distances[end], path

完整程式碼

from data import key_stations, key_paths, lines, tails, circles
from collections import defaultdict
from dijkstra import dijkstra
from held_karp import held_karp

# build graph
original_graph = defaultdict(dict)
for line in lines:
    for edge in line["TravelTimes"]:
        start = edge['FromStationName']['Zh_tw']
        end = edge['ToStationName']['Zh_tw']
        time = edge["RunTime"]
        original_graph[start][end] = time
        original_graph[end][start] = time

# cacluate cost of tails, round-trip
tail_cost = 0
for start, end in tails:
    cost, path = dijkstra(original_graph, start, end)
    tail_cost += cost * 2

# cacluate cost of circles, one-way
circle_cost = 0
for start, end in circles:
    cost, path = dijkstra(original_graph, start, end)
    circle_cost += cost

# compress path
n = len(key_stations)
graph = [[0]*n for _ in range(n)]
sub_paths = defaultdict(dict)
for start_idx in range(n-1):
    for end_idx in range(start_idx+1, n):
        cost, path = dijkstra(original_graph, key_stations[start_idx], key_stations[end_idx])
        graph[start_idx][end_idx] = cost
        graph[end_idx][start_idx] = cost
        sub_paths[start_idx][end_idx] = path
        sub_paths[end_idx][start_idx] = path[::-1]

# adjust compressed graph
adjusted_graph = [row[:] for row in graph]
for start, end in key_paths:
    start_idx = key_stations.index(start)
    end_idx = key_stations.index(end)
    adjusted_graph[start_idx][end_idx] = 0
    adjusted_graph[end_idx][start_idx] = 0

# held karp
start_station = "台北車站"
end_station = "台北車站"
start_idx = key_stations.index(start_station)
end_idx = key_stations.index(end_station)
core_cost, path = held_karp(graph, adjusted_graph, start_idx, end_idx)

# add key_path back, start from 1
for start, end in key_paths[1:]:
    start_idx = key_stations.index(start)
    end_idx = key_stations.index(end)
    core_cost += graph[start_idx][end_idx]

# show result
total_cost = tail_cost + circle_cost + core_cost
h = total_cost // 3600
m = total_cost // 60 % 60
s = total_cost % 60
print(f"總耗時: {h}h {m}m {s}s")
print('')
print("核心部分路線:")
for i in range(len(path)-1):
    start_idx = path[i]
    end_idx = path[i+1]
    print(f"{i+1}. {'-->'.join(sub_paths[start_idx][end_idx])}")

結果

終於來到本篇文章精華? 根據計算結果,從台北車站出發,遍歷 109 個車站,並在最後回到台北車站所需花費的時間是 4小時52分鐘7秒,核心路線如下,記得遇到 tails 與 circle 時要繞過去就行了。

1. 台北車站-->善導寺
2. 善導寺-->忠孝新生
3. 忠孝新生-->東門
4. 東門-->大安森林公園-->大安
5. 大安-->忠孝復興
6. 忠孝復興-->南京復興
7. 南京復興-->松江南京
8. 松江南京-->行天宮
9. 行天宮-->中山國小
10. 中山國小-->民權西路-->雙連-->中山
11. 中山-->雙連
12. 雙連-->民權西路
13. 民權西路-->大橋頭-->台北橋-->菜寮-->三重-->先嗇宮-->頭前庄
14. 頭前庄-->新埔民生-->板橋
15. 板橋-->新埔-->江子翠-->龍山寺-->西門
16. 西門-->北門
17. 北門-->西門-->小南門
18. 小南門-->中正紀念堂-->古亭-->頂溪-->永安市場-->景安
19. 景安-->景平-->秀朗橋-->十四張-->大坪林
20. 大坪林-->景美-->萬隆-->公館-->台電大樓-->古亭
21. 古亭-->中正紀念堂
22. 中正紀念堂-->台大醫院
23. 台大醫院-->台北車站

不過說真的,雖然路線可能是最好沒錯,但這個計算結果肯定是低估多了,原因如文章中所提,並沒有將班次與轉乘的因素考慮進去。

再者,這只是「遍歷」所花費的時間,大家還記得活動的目的是要集章嗎? 這些集章的機台大多都在站外,也就是你各位要出站啊 😂😂😂,保守一點算,假設每站要多停留5分鐘,換算下來差不多就要再多個 9 小時,這還只是保守,想買捷運一日票在一天內集齊所有章不是不可能,但這表示一大早就要開始集章,並且整天都要很認真地跑路線才能辦到,實在太累人了啊,這裡強烈建議:

"買月票或跟有月票的朋友借,分幾天慢慢跑完,比較輕鬆還不用多花半毛錢喔" 😉

1會員
1內容數
米蟲的程式小窩
留言0
查看全部
發表第一個留言支持創作者!
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
達美航空開航台北西雅圖 6月7日起每日直飛便利連結全美 全美最大航空公司達美Delta全新回歸,今日宣佈將於2024年6月7日正式開通台北每日直飛西雅圖航班,是唯一於白天出發的西行航班,讓台灣旅客享有便捷的直飛航班,更能透過達美航空的西雅圖樞紐,便利轉飛連接全美各大城市飛航網絡,全方位暢
Thumbnail
​咕咚的意思是形容重物撞擊或器物落水所發出的連續聲音。中國小說經典名著《紅樓夢》第六七回有說道:「在磚地上咕咚咕咚碰的頭山響。」 擬聲詞。形容重物撞擊或器物落水所發出的連續聲音。咕咚小火鍋這家店就是如此情景店家。 咕咚小火鍋相關資訊:: ​地址: 台北市中山區錦州街399號 ​營業時間:
Thumbnail
​ 便當可說是普羅大眾的食物選擇,更是許多外食族天天必吃的三餐。便當是一種相當方便的用餐形式,形形色色便當讓人眼花繚忘,你最愛哪一家便當店呢??哪一款便當又是你的最愛主菜便當呢??這家嘉瑋便當便是一個頗具特色與知名度便當店、如果你住松山區或是在松山區工作或是就學者,嘉瑋便當你絕對可以放入你便當店私
Thumbnail
台灣有許多烘培業者,不只是烘培而且還與生活各種場合需求為營業項目。這篇介紹這家飛士蘭外燴家,是全國第一家國家級認證的外燴廚房,這家食品在市面上烘培業者中雖然不算是老字號品牌,但卻是頗受歡迎烘培業者之一。 飛士蘭外燴家相關資訊:: 地址: 台北市內湖區舊宗路二段121巷9號1樓 營業時間: AM9:0
Thumbnail
現在時序來到三月、天氣越來越熱了,全家便利超商延續草莓、巧克力等口味霜淇淋熱賣熱潮,近期再持續推出濃郁的抹茶霜淇淋,一推出就掀起打卡熱潮,成為不少網友及臉書社團的話題中心。再有賣抹茶霜淇淋門市引起熱賣一陣旋風。 全家便利商店春日抹茶霜淇淋相關資訊:: 地址: 全家便利超商各地特定門市 電話: 02
Thumbnail
【李婉如/ 報導】國際超模大賽第十屆在台北文華東方華麗舉辦總決賽盛典,台灣總冠軍在12月飛往成都,代表寶島台灣爭奪亞洲總決賽冠軍。大賽開場由「浪LIVE」直播藝人「小阿妹簡語卉」高亢演唱點燃戰火,選手來自台灣、印尼、馬來西亞各大賽區,T台組超模穿上法國花藝世家大師Claude Cossec金色戰神、
Thumbnail
夜市中也能創人充滿美麗能量,夜市不只吃美食,更能讓你邊吃美食、邊看展覽、邊創作,在台北市知名師大夜市這間隱藏複合式咖啡廳,就是如此場合。響ART一間師大夜市開了間複合式場地,小編因故來到這個場地,讓小編同時也是藝術工作者;也不禁讚歎夜市中也有如此藝術場域。 響ART相關資訊:: 地址: (師大店)
Thumbnail
阿城鵝肉相關資訊:: 地址: 台北市中山區吉林路105號 (中山一店) 電話: 02 2265 0484 02 8261 9747 營業時間: PM11:00-PM20:00 備註: 台菜餐廳 吉林路美食商圈內,一顆明星店家、二家並存店面 連續二年米其林必比登評鑑推薦餐廳 台菜玄機 ​
Thumbnail
市面上便當非常多,但是如果你是日式美食控,築田便當會是你最佳良伴選擇。築田便當精緻菜色讓人忍不住想說讚!!吃上便當就像秒飛到日本感覺。對於外食族常吃便當來說彷彿就像是給您回家吃飯的滋味。 築田便當相關資訊:: 地址: 台北市信義區忠孝東路五段191號 電話: 02 2766 0868
Thumbnail
​ 今年新冠疫情一整年讓民眾生活緊張,大家是否覺得生活很燜呢??很多人選擇不出門悶在家裡,生活不應該如此黯淡。遠離疫情最好方法,就是走出到戶外。這四天(十月二十四至十月二十八日止)短短四天不訪來到中正紀念堂民主大道欣賞一場花藝秀。絕對讓你拋開疫情的憂慮與增添生活情趣。 花毯展示之展覽相關資訊:: ​
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
美國總統大選只剩下三天, 我們觀察一整週民調與金融市場的變化(包含賭局), 到本週五下午3:00前為止, 誰是美國總統幾乎大概可以猜到60-70%的機率, 本篇文章就是以大選結局為主軸來討論近期甚至到未來四年美股可能的改變
Thumbnail
達美航空開航台北西雅圖 6月7日起每日直飛便利連結全美 全美最大航空公司達美Delta全新回歸,今日宣佈將於2024年6月7日正式開通台北每日直飛西雅圖航班,是唯一於白天出發的西行航班,讓台灣旅客享有便捷的直飛航班,更能透過達美航空的西雅圖樞紐,便利轉飛連接全美各大城市飛航網絡,全方位暢
Thumbnail
​咕咚的意思是形容重物撞擊或器物落水所發出的連續聲音。中國小說經典名著《紅樓夢》第六七回有說道:「在磚地上咕咚咕咚碰的頭山響。」 擬聲詞。形容重物撞擊或器物落水所發出的連續聲音。咕咚小火鍋這家店就是如此情景店家。 咕咚小火鍋相關資訊:: ​地址: 台北市中山區錦州街399號 ​營業時間:
Thumbnail
​ 便當可說是普羅大眾的食物選擇,更是許多外食族天天必吃的三餐。便當是一種相當方便的用餐形式,形形色色便當讓人眼花繚忘,你最愛哪一家便當店呢??哪一款便當又是你的最愛主菜便當呢??這家嘉瑋便當便是一個頗具特色與知名度便當店、如果你住松山區或是在松山區工作或是就學者,嘉瑋便當你絕對可以放入你便當店私
Thumbnail
台灣有許多烘培業者,不只是烘培而且還與生活各種場合需求為營業項目。這篇介紹這家飛士蘭外燴家,是全國第一家國家級認證的外燴廚房,這家食品在市面上烘培業者中雖然不算是老字號品牌,但卻是頗受歡迎烘培業者之一。 飛士蘭外燴家相關資訊:: 地址: 台北市內湖區舊宗路二段121巷9號1樓 營業時間: AM9:0
Thumbnail
現在時序來到三月、天氣越來越熱了,全家便利超商延續草莓、巧克力等口味霜淇淋熱賣熱潮,近期再持續推出濃郁的抹茶霜淇淋,一推出就掀起打卡熱潮,成為不少網友及臉書社團的話題中心。再有賣抹茶霜淇淋門市引起熱賣一陣旋風。 全家便利商店春日抹茶霜淇淋相關資訊:: 地址: 全家便利超商各地特定門市 電話: 02
Thumbnail
【李婉如/ 報導】國際超模大賽第十屆在台北文華東方華麗舉辦總決賽盛典,台灣總冠軍在12月飛往成都,代表寶島台灣爭奪亞洲總決賽冠軍。大賽開場由「浪LIVE」直播藝人「小阿妹簡語卉」高亢演唱點燃戰火,選手來自台灣、印尼、馬來西亞各大賽區,T台組超模穿上法國花藝世家大師Claude Cossec金色戰神、
Thumbnail
夜市中也能創人充滿美麗能量,夜市不只吃美食,更能讓你邊吃美食、邊看展覽、邊創作,在台北市知名師大夜市這間隱藏複合式咖啡廳,就是如此場合。響ART一間師大夜市開了間複合式場地,小編因故來到這個場地,讓小編同時也是藝術工作者;也不禁讚歎夜市中也有如此藝術場域。 響ART相關資訊:: 地址: (師大店)
Thumbnail
阿城鵝肉相關資訊:: 地址: 台北市中山區吉林路105號 (中山一店) 電話: 02 2265 0484 02 8261 9747 營業時間: PM11:00-PM20:00 備註: 台菜餐廳 吉林路美食商圈內,一顆明星店家、二家並存店面 連續二年米其林必比登評鑑推薦餐廳 台菜玄機 ​
Thumbnail
市面上便當非常多,但是如果你是日式美食控,築田便當會是你最佳良伴選擇。築田便當精緻菜色讓人忍不住想說讚!!吃上便當就像秒飛到日本感覺。對於外食族常吃便當來說彷彿就像是給您回家吃飯的滋味。 築田便當相關資訊:: 地址: 台北市信義區忠孝東路五段191號 電話: 02 2766 0868
Thumbnail
​ 今年新冠疫情一整年讓民眾生活緊張,大家是否覺得生活很燜呢??很多人選擇不出門悶在家裡,生活不應該如此黯淡。遠離疫情最好方法,就是走出到戶外。這四天(十月二十四至十月二十八日止)短短四天不訪來到中正紀念堂民主大道欣賞一場花藝秀。絕對讓你拋開疫情的憂慮與增添生活情趣。 花毯展示之展覽相關資訊:: ​