跳转至

解绳子

约 638 个字 1 行代码 预计阅读时间 2 分钟

出题人:Riser

赛事来源:SECO Puzzle Hunt - 捕/获/梦/境 (2026)

本站导航: 返回到当前赛事页面

题目分区:二区 - 沙海(2°32′S,43°7′W)

备注:这是一道交互题,请在官网存档站进行解题

剧透警告

题目主题:数据库;地铁

相关工具:地铁线路

1. 第七题难度太大了!或许我忽略了什么? (2提示点)

你可以注意一下这些节点的颜色,把它们联系到ft上。

2. 这道题的主题是什么? (2提示点)

地铁。

3. 这些题目分别对应什么? (6提示点)

请将本题主题对应的词语(中国大陆用法)重复两遍进行提交,里程碑会进行进一步的反馈。

4. 该如何提取? (3提示点)

找到标有数字的节点的英文名,用这些数字提取位数。

里程碑 1: 地**铁

地铁地铁 : 每道题对应的城市分别是:兰州、哈尔滨、南昌、无锡、南宁、昆明、宁波。

最终答案

POPCORN


题目详情

为什么许多人每天沿着这些绳子走?


总结

题解

ft提示说“许多人”、“每天”、“沿着绳子”,可以联想到日常的固定轨道交通工具,结合题目节点的颜色暗示,不难理解本题主题是地铁。~然后就是根据节点的颜色,判断出是哪个城市的线路~然后就是逐个解开图案,根据颜色和交点在地铁图里找一下对应的城市。这里需要注意题目给的颜色并不是和地铁颜色完全对应的,可能会有一定的色彩浓淡差异。解题代码里写了一个根据颜色进行筛选的脚本,但从最终结果来看效果不佳,仅供参考。

得到之后,利用标有数字的地铁站的英文名进行提取。

序号 城市 地铁站 英文名 数字 提取
1 兰州 五里铺 WULIPU 5 P
2 哈尔滨 博物馆 MUSEUM OF HEILONGJIANG PROVINCE STATION 7 O
3 南昌 绳金塔 SHENGJIN PAGODA 9 P
4 无锡 市民中心 CIVIC CENTER 1 C
5 南宁 朝阳广场 CHAOYANG SQUARE 4 O
6 昆明 联大街 LIANDA STREET 9 R
7 宁波 外滩大桥 WAITAN STREET 6 N
解题代码
from lxml import html
from pathlib import Path
from pydantic import BaseModel
from collections import defaultdict

class Metro(BaseModel):
    district: str
    lineName: str
    hexColor: str | None
    rgbColor: tuple[int, int, int] | None


def parse_rgb(text: str):
    text = ''.join(filter(lambda x: x in '0123456789,', text))
    if len(text) < 3:
        return None
    parts = [x.strip() for x in text.split(",")]
    if len(parts) != 3:
        return None
    try:
        tuple(map(int, parts))
    except:
        print(f'请检查html文件中{parts}(对应{text})颜色是否有误。')
        return None
    return  tuple(map(int, parts))# type: ignore

def normalize_hex_color(color: str) -> str:
    color = color.strip().upper()
    if not color.startswith("#"):
        color = "#" + color
    return color


def hex_to_rgb(color: str) -> tuple[int, int, int]:
    color = normalize_hex_color(color)[1:]
    if len(color) != 6:
        raise ValueError(f"非法 16 进制颜色: #{color}")
    return (
        int(color[0:2], 16),
        int(color[2:4], 16),
        int(color[4:6], 16),
    )

def parse_line_name(td: html.HtmlElement) -> str:
    # 优先取链接文字,例如 “北京1号线”
    links = td.xpath(".//a")
    if links:
        text = links[0].text_content().strip()
        if text:
            return text

    # 回退:取整个单元格文本,并清理掉前面的色块字符
    text = td.text_content().strip()
    text = text.replace("█", "").strip()
    return text

def parse_hex_color(td: html.HtmlElement):
    text = td.text_content().strip().upper()
    
    if len(text) < 6:
        return None
    return text

def parse_table(table: html.HtmlElement) -> list[Metro]:
    result: list[Metro] = []
    current_district: str | None = None

    for tr in table.xpath(".//tr"):
        tds = tr.xpath("./td")

        if not tds:
            continue

        # 只要遇到横跨三列的标题行,就更新当前地区名
        # 不判断它是“华北地区”还是“北京市”
        if len(tds) == 1 and tds[0].get("colspan") == "3":
            current_district = tds[0].text_content().strip()
            continue

        # 真正的数据行:三个 td
        if len(tds) == 3:
            if not current_district:
                continue

            line_name = parse_line_name(tds[0])
            hex_color = parse_hex_color(tds[1])
            rgb_color = parse_rgb(tds[2].text_content().strip())
            if hex_color and rgb_color and current_district not in ['通长图', 'G总的图', '图标', '未知']:
                result.append(
                    Metro(
                        district=current_district,
                        lineName=line_name,
                        hexColor=hex_color,
                        rgbColor=rgb_color,
                    )
                )

    return result

def color_distance2(a: tuple[int, int, int], b: tuple[int, int, int]) -> int:
    dr = a[0] - b[0]
    dg = a[1] - b[1]
    db = a[2] - b[2]
    return dr * dr + dg * dg + db * db


def build_district_metros(metros: list[Metro]) -> dict[str, list[Metro]]:
    district_map: dict[str, list[Metro]] = defaultdict(list)
    for metro in metros:
        district_map[metro.district].append(metro)
    return dict(district_map)


def guess_district(colors: list[str], metros: list[Metro]) -> str | None:
    """
    根据给定颜色组合猜测所属城市。
    允许输入颜色与标准颜色有细微差异。

    算法:
    - 对每个城市
    - 对每个输入颜色,找到该城市中颜色差异最小的一条线路
    - 把这些最小差异累加,得到总分
    - 返回总分最小的城市
    """
    if not colors:
        return None

    input_rgbs = [hex_to_rgb(c) for c in colors if c.strip()]
    if not input_rgbs:
        return None

    district_map = build_district_metros(metros)

    best_district: str | None = None
    best_score: int | None = None
    ranked_district = {}
    for district, district_metros in district_map.items():
        if not district_metros:
            continue

        total_score = 0
        lines = []
        for input_rgb in input_rgbs:
            score_list = [(color_distance2(input_rgb, metro.rgbColor), metro.lineName) for metro in district_metros]
            score_list.sort(key=lambda x: x[0])
            total_score += score_list[0][0]
            lines.append(score_list[0][1])
        if best_score is None or total_score < best_score:
            best_score = total_score
            best_district = district
        ranked_district[district] = (total_score, lines)
    ranked_district = dict(sorted(ranked_district.items(), key=lambda x: x[1]))
    return best_district, ranked_district

def guess_district_scores(colors: list[str], metros: list[Metro]) -> list[tuple[str, int]]:
    """
    返回所有城市的匹配得分,分数越小越接近。
    """
    if not colors:
        return []

    input_rgbs = [hex_to_rgb(c) for c in colors if c.strip()]
    if not input_rgbs:
        return []

    district_map = build_district_metros(metros)
    scores: list[tuple[str, int]] = []

    for district, district_metros in district_map.items():
        if not district_metros:
            continue

        total_score = 0
        for input_rgb in input_rgbs:
            min_dist = min(
                color_distance2(input_rgb, metro.rgbColor)
                for metro in district_metros
            )
            total_score += min_dist

        scores.append((district, total_score))

    scores.sort(key=lambda x: x[1])
    return scores

def test():
    testcases = [
        (["#feb500", "#b20001", "#25ac7f"], '哈尔滨市'),
        (["#E9AE00", "#EB7835", "#30895F"], '成都市'),
    ]
    for ind, testcase in enumerate(testcases):
        best, candidates = guess_district(testcase[0], metros)
        print(f'第{ind+1}个测试:{" / ".join(testcase[0])}')
        print(f'测试{"不" if best != testcase[1] else ""}通过:标准答案【{testcase[1]}】,计算最佳答案【{best}】,对应线路{" / ".join(candidates[best][1])}。')
        # print(f'其余候选答案:{"、".join(candidates.keys())}')
        print('========================================')

if __name__ == '__main__':
    cache = Path(__file__).parent / "metro-colors.html"
    if not cache.is_file():
        print(
            f"请使用浏览器打开https://zhrail.huijiwiki.com/wiki/%E6%A0%87%E5%BF%97%E8%89%B2%E4%B8%80%E8%A7%88 ctrl+S保存到 {cache.as_posix()} 后再重新运行本脚本"
        )
        exit(0)

    with open(cache, "r", encoding="utf8") as f:
        source = html.parse(f)

    tables = source.xpath('//*[@id="mw-content-text"]//table[@class="wikitable"]')

    metros: list[Metro] = []
    for table in tables:
        metros.extend(parse_table(table))
    
    # 测试
    # test()
    for testcase in [
        ['#ff5d5d','#3eb1ff'],
        ['#f44336','#ffb000','#3b8b5a'],
        ['#1aa043','#2c6fdb','#ffcc00','#ff1f1f'],
        ['#c14cc5','#2fb8f4','#f44242','#35bc4e'],
        ['#2ca84f','#9966cc','#3366cc','#c4cc23','#ff3333'],
        ['#36ba55','#ff9900','#d663c9','#27c1b9','#2b7ee0','#ff0101'],
        ['#ffa329','#f456a6','#ea2f2f','#2730a0','#008bd8','#875d32','#a2c60a']
    ]:
        best, candidates = guess_district(testcase, metros)
        print(f'最佳匹配:{best},对应线路{" / ".join(candidates[best][1])}')
评价

比赛时队友神力做的。只给部分地铁站去找全国的地铁真是正常人类能做的题吗,苦苦又力力啊。还得靠Milestone去做啊。