介绍
京都大学的合作食堂设有膳食系统。
简单来说,这个系统就像一个普通的学校食堂。
通过在财政年度开始时支付一年的用餐费,您可以免费使用 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。
稍后,请分别获取饭碗和米饭的菜单。 (原因后面会解释)
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 中为每个菜单获取名称、价格和分数。
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]$。
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日元)。
该程序仍然可以播放,所以我认为它会继续播放。
原创声明:本文系作者授权爱码网发表,未经许可,不得转载;
原文地址:https://www.likecs.com/show-308626157.html