介绍

京都大学的合作食堂设有膳食系统。

简单来说,这个系统就像一个普通的学校食堂。
通过在财政年度开始时支付一年的用餐费,您可以免费使用 550 日元的餐点。从四月以来一直独居的大学生家长看来,即使没有钱,也可以在食堂吃到营养丰富的食物。

但是,即使在平日、周六、暑假等不需要来大学的日子里,如果不继续使用食堂,就无法取回钱。 (剩余部分可结转至会计年度末或扣除数百日元手续费后退还。)
另一方面,如果你每天继续使用食堂,你将可以使用比你一开始支付的更多的钱。他们被称为奴隶。

这顿饭每餐限550日元。有。京都大学的食堂是食堂式的,所以可以组合各种菜肴。

这就提出了一个问题,“你真的能用550日元做一顿营养丰富的饭菜吗?”这道题相当难,虽然京都大学的学生从一年级开始就有一道题,但大多数人即使每天都在食堂,也没有解决这道题就毕业了。

所以,这次想验证一下,能不能在550日元以内做出让磨坊主,尤其是饭奴们营养满足的组合。

评价标准

确定“营养令人满意”菜单的标准,即最佳解决方案的标准。

在食堂里,每道菜都会根据其中所含的成分和营养成分被打上红色、绿色或黄色的分数。检查您的收据以查看您的组合总共有多少积分。此外,在收据的底部,每种颜色所需的物品数量都被写为“一顿饭的标准”。

京大生協の食堂の最適解

所以,让我们以食堂显示的三色分数作为标准。

这次的最佳解决方案是“满足每种颜色标准且总价最低的组合”。

数据

自助餐厅菜单大约每周更改一次,因此我们需要获取当天自助餐厅菜单的价格和分数数据。

这一次,你可以在这个网站上看到自助餐厅的菜单,所以我会从这里刮下来。另外,这一次,我将在京都大学食堂中给出北方食堂的最佳解决方案。

您可以在下一页查看北分食堂的菜单列表。

https://west2-univ.jp/sp/menu.php?t=650113

另请参阅菜单页面。比如咸猪排碗

https://west2-univ.jp/sp/detail.php?t=650113&c=819043

在菜单的URL中,“t=XXXXXX”有代表食堂的参数(北分食堂、中央食堂等),“c=XXXXXX”有代表菜单的参数。

所以,让我们从列表页面获取菜单的 ID,并从每个菜单页面获取价格和分数。

获取今天的菜单列表

首先,从列表中的站点获取菜单的 ID。列表页面应该总是有一个指向菜单页面的链接,所以刮掉它。

但是,这个网站有个陷阱,如果按原样获取 HTML,则只能获取主菜链接。事实上,直到小菜和饭碗展开,链接才会出现。

因此,您必须在获取 ID 之前单击此按钮。如果您查看页面源代码,您将看到每种类型的“id=on_a”和“id=on_c”,因此请按附加到它的按钮,然后获取 HTML。

稍后,请分别获取饭碗和米饭的菜单。 (原因后面会解释)

获取 t_menu。 py
import asyncio
from pyppeteer.launcher import launch
from pyppeteer.page import Page, Response
from pyppeteer.browser import Browser
from pyppeteer.element_handle import ElementHandle
import time
import re

#メニューサイトのHTMLを取得する
async def extract_html(push_buttons) -> str:
    # push_buttons:ボタンを押したいジャンルの"on_XX"の「XX」が入っているリスト
    # ブラウザを起動。headless=Falseにすると実際に表示される
    browser: Browser = await launch()

    try:
        page: Page = await browser.newPage()

        # 北部食堂のメニュー一覧のサイトへ移動
        response: Response = await page.goto('https://west2-univ.jp/sp/menu.php?t=650113')
        if response.status != 200:
            raise RuntimeError(f'site is not available. status: {response.status}')

        for x in push_buttons:
            # 押したいボタンを指定してクリック
            buttons: ElementHandle = await page.querySelector('#on_' + x)
            await buttons.click()
            # サイトに負荷を掛けないように時間を空けましょう
            time.sleep(1)

        # ボタンを押した後でHTMLを取得
        html: str = await page.content()

        return html
    finally:
        await browser.close()

#b:副菜、e:デザート、bunrui1:オーダーコーナーのボタン
#主菜は元からボタンが押してある扱いなので、主菜、副菜、デザート、オーダーの情報が入っているHTMLが取得できる。
html: str = asyncio.get_event_loop().run_until_complete(extract_html(["b", "e", "bunrui1"]))

#正規表現を用いて、メニューのIDを取得する
menu = re.findall(r';c=.*">', html)
#menuには';c=XXXXXX">'を満たす文字列が入っています。


#同じようにして丼についてもHTMLを取得する。d:丼のボタン
#こちらは、主菜と丼の情報が入っているHTML
html: str = asyncio.get_event_loop().run_until_complete(extract_html(["d"]))

#正規表現を用いて、メニューのIDを取得する
menu_domburi = re.findall(r';c=.*">', html)

现在您可以通过在菜单中提取“c = XXXXXXX”来获取ID。

获取价格和分数

现在我们有了一个菜单 ID 列表,下一步是获取每个菜单的价格和分数。

这个工作可以通过从获取到的ID跳转到菜单页面,获取原样的HTML来完成,但是有两点需要注意。

首先是 menu 和 menu_domburi 有重复的主菜 ID。您不需要多次检查同一菜单的价格,因此检查重复的 ID 然后获取价格等。

第二个是米饭和盖饭菜单有大小。咸猪排、咖喱等饭碗菜单可以选择S、M、L尺寸,但只能通过获取的ID获取M尺寸信息。

那么,如果有可能获得其他尺寸的菜单信息,这不是真的.

令人惊讶的是,如果您检查碗的 M 尺寸 ID 的 ID ± 1,您可以打开该菜单的 S 和 L 尺寸页面。1.这是从明天起不能使用的琐事。顺便说一句,白米有SS和LL两种尺寸,可以用ID±2打开。

考虑到这一点,让我们创建一个菜单 ID 列表,并从 HTML 中为每个菜单获取名称、价格和分数。

获取 t_menu 信息。 py
menu_id = set()

for s in menu:
    menu_id.add(s[3:-2]) # IDの文字列だけを取り出す

for s in menu_domburi:
    # 丼のメニューだけを追加する
    if s[3:-2] not in menu_id:
        # IDが6桁ではない場合は、Mサイズしかない(多分)
        if len(s) != 11:
            menu_id.add(s[3:-2])
        else:
            # S,Lサイズも追加する
            n = int(s[3:-2])
            # 814702は白ご飯のID
            # 通常の丼はS,M,Lサイズ、白ご飯はSS~LLサイズ
            if n != 814702:
                for i in range(3):
                    menu_id.add(str(n-1+i))
            else:
                for i in range(5):
                    menu_id.add(str(n-2+i))

print(menu_id, len(menu_id))

from urllib import request

def get_info(id):
    # サイトに負荷を掛けないように時間を空けましょう
    time.sleep(1)
    # IDからhtmlを取得
    response = request.urlopen("https://west2-univ.jp/sp/detail.php?t=650113&c=" + id)
    content = response.read()
    response.close()
    html = content.decode()

    # メニュー名を取得
    name = re.search(r"<h1>.*<span>", html)
    name = name.group()[4:-6]

    # 価格
    price = re.search(r"\d+</strong>円", html)
    price = int(price.group()[:-10])

    # 点数
    # 点数は小数点第1位まで示されているので、後の計算のために10倍して整数型で記録しておく
    rgy = re.findall(r".:\d+.\d", html)
    if rgy:
        r = [0] * 3
        for i in range(3):
            if len(rgy[i]) == 5: # 点数が10点未満(X.X点のとき)
                r[i] = int(rgy[i][2] + rgy[i][4])
            else: # 点数が10点以上(XX.X点のとき)
                r[i] = int(rgy[i][2:4] + rgy[i][5])
        return name, [price] + r # 名前,[価格,赤,緑,黄色]をリターンする
    else: #万が一点数を取得できなかった場合
        return

menu_list = dict()

for id in menu_id:
    menu_info = get_info(id)
    if not menu_info:
        name = menu_info[0]
        info = menu_info[1]
        menu_list[name] = info

如果您查看menu_list,您可以看到今天菜单的名称、价格以及每种颜色的 10 倍值。

最优解计算

现在我有了价格和分数数据,我将尝试计算最优解。

在确定最佳解决方案时,我将添加这次仅采用相同菜单之一的条件。自助餐厅有大约 20 到 30 种菜单,所以我认为大多数人不会多次点相同的菜单。另外,我不认为吃同一菜单的多个盘子是一顿均衡的饭菜,所以我认为这是一个很好的条件。

但是,虽然与计算方法有关,例如,我认为可以同时订购“盐酱肉排M”和“盐酱肉排L”。 (如果这是最佳解决方案,让我们假设它就是这样。)

现在,让我们考虑一种具体的计算方法。作为最简单的计算方法,我可以想出一种方法,将所有组合都制作出来,并从条件中检查最佳组合。

如果菜单上有$N$项,那么所有菜单项的组合总和为$2$,因此我们可以看到菜单组合的总数为$2^N$。

这取决于一天,但在北分食堂大约有 $N=40$ 的物品。因此,似乎可以通过计算$2^{40}\约10^{12}$路来计算最优解,即以计算机的能力计算大约1万亿路。

然而,事实上,即使使用计算机,计算所有 1 万亿种方式也需要一点时间。任何从事竞技编程的人都知道,在计算机上计算 $10^9$ 大致相当于 1 秒,因此在 $1000$ 秒 $\approx15 中粗略计算 $10^{12}$ 次我知道大约需要 $ 分钟。

如果是15分钟左右,可能问题不大,但是如果$N=50$加上10个项目的话,至少需要一周时间。。

动态规划

因此,我想使用动态规划作为一种可以快速计算出最优解的算法。这在竞技编程中也很熟悉,但难度较大,所以我将在另一篇文章中解释。

我认为这会有所帮助。
组织典型的 DP(动态编程)模式第 1 部分 ~ 背包 DP 版 ~

这一次,将是背包问题的一个应用。
让我们先检查计算量。令 $N$ 为项目数,$R,G,Y$ 为三种颜色中每种颜色的标准点数的 10 倍。至于$N$,它已经从指数顺序变成了线性顺序,所以即使项目数量增加,计算时间也不会爆炸式增加。2
如果用男款标准算的话,估计再大也只有$10^8$左右,看来可以秒算。 (我使用的是 Python,它不是一个很好的程序,所以实际上可能需要更长的时间。另外,Python 有一个可以进行相同优化的库,所以你应该使用它。我认为。)

在这个动态规划中,
$dp[i][r'][g'][y'] = 最小成本和组合红色:r' 点,绿色:g' 点,黄色:y' 点在菜单中最多 i 项 $
记得计算
至于$r',g',y'$,像$0.5$点一样很难处理成十进制数,所以设置$r = 10r'$,这样就可以整数形式处理了。为此,我提前记录了分数乘以10的值。

但是在计算$dp[i+1]$的时候,只需要$dp[i]$,所以不用记住$dp[i-1]$,所以实际上$dp[是边持有边进行计算的两个 r][g][y]$。

最佳。 py
N = len(menu_list) # 品数
R = 27 # 赤色の点数(×10)
G = 10 # 緑色の点数(×10)
Y = 57 # 黄色の点数(×10)

dp = [[[[10000, set([])] for i in range(Y+1)] for i in range(G+1)] for i in range(R+1)]

dp[0][0][0][0] = 0

# 実際に遷移を計算します
for name, info in menu_list.items():
    # ndp = dp[i+1], dp = dp[i] として計算しています
    ndp = [[[[10000, set([])] for i in range(Y+1)] for i in range(G+1)] for i in range(R+1)]
    price, red, green, yellow = info
    for j in range(R+1):
        nr = min(R, j+red)
        for k in range(G+1):
            ng = min(G, k+green)
            for m in range(Y+1):
                ny = min(Y, m+yellow)
                if ndp[j][k][m][0] > dp[j][k][m][0]:
                    ndp[j][k][m][0] = dp[j][k][m][0]
                    ndp[j][k][m][1] = dp[j][k][m][1]
                if ndp[nr][ng][ny][0] > (price + dp[j][k][m][0]):
                    ndp[nr][ng][ny][0] = price + dp[j][k][m][0]
                    ndp[nr][ng][ny][1] = dp[j][k][m][1] | set([name])
    dp = list(ndp)

def output(t):
    for i in range(0):
        print()
    print("-------------------")
    print("{}円".format(t[0]))
    for x in t[1]:
        p, r, g, y = menu_list[x]
        print("{}  {}円 赤:{}点 緑:{}点 黄:{}点".format(x, p, r/10, g/10, y/10))
    print("--------------------")

#dp[R][G][Y]には最適解の値段と、その組み合わせが入っています。
output(dp[R][G][Y])

结果公布

通过依次连接这些程序,可以自动获取菜单、价格、分数,并自动计算最优解。

让我们进行实际计算。

结果
-------------------
398円
ほうれん草  66円 赤:0.0点 緑:0.2点 黄:0.0点
大学芋  88円 赤:0.0点 緑:0.8点 黄:1.2点
牛乳  85円 赤:1.7点 緑:0.0点 黄:0.0点
ライス  115円 赤:0.0点 緑:0.0点 黄:5.1点
温泉玉子  44円 赤:1.0点 緑:0.0点 黄:0.0点
--------------------

今天的最优解是 398 日元。
当我实际安排在餐厅时是这样的。

京大生協の食堂の最適解

收据

京大生協の食堂の最適解

考虑

我只是考虑一下。
只有小菜,是男大学生吃的精致菜单。
这可能是营养最佳的,但它不是我每天都可以作为一顿饭吃的东西。我真的很想要主菜。

看着菜单组合,感觉就像是专门为每种颜色准备的小菜。毕竟,我发现绿色0.8分的大学土豆是作弊。主菜三种颜色都有评分,是不是因为评分对价格效率不高?

结论

您可以享用一顿营养丰富的饭菜(550日元)。

该程序仍然可以播放,所以我认为它会继续播放。

  1. 根据菜单和日期,有时只提供M尺寸,但除非你去自助餐厅,否则无法确认,所以这次我将计算其他尺寸。另外,即使在点菜菜单中,也有饭碗菜单可以选择S尺寸和L尺寸的情况,但这是假设只有M尺寸的情况下计算的。

  2. 例如,使用第一种方法,当添加两个项目时,计算时间会增加四倍,而使用动态规划,即使项目数增加一倍,计算时间也会增加一倍。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308626157.html

相关文章:

  • 2022-12-23
  • 2022-12-23
  • 2021-12-07
  • 2021-10-21
  • 2022-01-02
猜你喜欢
  • 2022-12-23
  • 2021-08-21
  • 2022-01-20
  • 2021-09-30
  • 2021-11-18
相关资源
相似解决方案