Logo ryp的博客

博客

PSS Round - 09.10(Easy Version)比赛须知

2025-09-10 13:34:16 By ryp

题目顺序是:

T1 T2 T3 T4 T5

368 369 370 365 364

WyOJ Musume 代码

2025-09-09 10:37:11 By ryp
import asyncio
import time
import websockets
import aiohttp
import traceback
import json
import random
import base64
import aiosqlite
import re
import os

wsserver = 'ws://172.17.0.2:3001/'
ws = None
commands = dict ()
trusted_senders = [ 2681243014 ]
confirm_queue = dict ()
reply_queue = dict ()
file_queue = dict ()
sqlconn = None
GROUP_ID = 1061173220

def wyoj_enable (qq, name):
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        raise Exception ('wyoj_enable: Invalid username; possible injection')
    os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'U', qq = '{qq}' where username = '{name}'\"")

def wyoj_disable (name):
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        raise Exception ('wyoj_disable: Invalid username; possible injection')
    os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'B' where username = '{name}'\"")

def wyoj_adminize (name):
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        raise Exception ('wyoj_adminize: Invalid username; possible injection')
    os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'S' where username = '{name}'\"")

# 暂时不考虑嵌套。
def wyoj_deadminize (name):
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        raise Exception ('wyoj_deadminize: Invalid username; possible injection')
    os.system (f"sudo docker exec uoj-db mysql -proot app_uoj233 -e \"update user_info set usergroup = 'U' where username = '{name}'\"")

def wyoj_admins ():
    return os.popen ("sudo docker exec uoj-db mysql -proot app_uoj233 -e \"select username from user_info where usergroup = 'S';\"").read ()

def wyoj_banned ():
    return os.popen ("sudo docker exec uoj-db mysql -proot app_uoj233 -e \"select username from user_info where usergroup = 'B';\"").read ()

async def db_insert_bind (uid, name):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'insert into binds (qq, name) values (\'{uid}\', \'{name}\')');
    await sqlconn.commit ()
    await cursor.close ()

async def db_delete_bind (name):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'delete from binds where name = \'{name}\'');
    await sqlconn.commit ()
    await cursor.close ()

async def db_query_by_id (uid):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select qq, name from binds where qq = \'{uid}\'')
    res = await cursor.fetchall ()
    await cursor.close ()
    return res

async def db_query_by_name (name):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select qq, name from binds where name = \'{name}\'')
    res = await cursor.fetchall ()
    await cursor.close ()
    if len (res) > 1:
        raise Exception ('重复绑定')
    if len (res) == 1:
        return res[0]
    return None

async def db_query_binds ():
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select qq, name from binds order by qq')
    res = await cursor.fetchall ()
    await cursor.close ()
    return res

async def db_count_bound (qq):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select count (*) from binds where qq = \'{qq}\'')
    res = await cursor.fetchone ()
    await cursor.close ()
    return res[0]

async def db_check_bound (name):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select count (*) from binds where name = \'{name}\'')
    res = await cursor.fetchone ()
    await cursor.close ()
    return res[0] > 0

async def db_banned (qq):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select count (*) from bans where qq = \'{qq}\'')
    res = await cursor.fetchone ()
    await cursor.close ()
    return res[0] > 0

async def db_banned_all ():
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'select qq from bans')
    res = await cursor.fetchall ()
    await cursor.close ()
    return res

async def db_ban (qq):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'insert into bans (qq) values (\'{qq}\')');
    await sqlconn.commit ()
    await cursor.close ()

async def db_unban (qq):
    cursor = await sqlconn.cursor ()
    await cursor.execute (f'delete from bans where qq = \'{qq}\'');
    await sqlconn.commit ()
    await cursor.close ()

refpatt = re.compile (r'\[CQ:reply,id=([0-9]*)\]')
atpatt = re.compile (r'\[CQ:at,qq=([0-9]*)\]')
class MessageMeta:
    def __init__ (self, data):
        s = data['raw_message']
        self.data = data
        self.uid = data['user_id']
        self.gid = data['group_id']
        self.msgid = data['message_id']
        self.refs = re.findall (refpatt, s)
        self.ats = re.findall (atpatt, s)

        i = 0
        flag = False
        while i < len (s) and (flag or s[i] == '['):
            flag |= s[i] == '['
            flag &= s[i] != ']'
            i += 1
        while i < len (s) and s[i] == ' ':
            i += 1
        self.msg = s[i:]
        self.cmd = self.msg.split ()

async def reply_msg (meta, msg):
    return await ws.send (json.dumps ({
        'action': 'send_group_msg',
        'params': {
            'group_id': meta.gid,
            'message': [ { 'type': 'reply', 'data': { 'id': meta.msgid } },
                        { 'type': 'text', 'data': { 'text': msg } } ]}}))

def download_dir (s):
    return f'./download/{s}'

FILE_KEEP_MIN = 60
REPOTER_GAP = 5
CHUNK_SIZE = 1048576 * 16

async def receive_file (meta, data):
    size = int (data['message'][0]['data']['file_size'])
    url = data['message'][0]['data']['url']
    name = data['message'][0]['data']['file']
    if not name.endswith ('.zip'):
        return await reply_msg (meta, f'识别到非 ZIP 格式文件,未下载')
    await reply_msg (meta, f'识别到 ZIP 格式文件,大小 %.3lf MB 正在下载' % (size / 1048576))

    start_time = time.time ()
    rx = 0
    finished = False

    async def reporter ():
        while not finished:
            duration = time.time () - start_time
            if rx != 0:
                await reply_msg (meta, f'已下载 %.3lf MB = %.3lf MB/s = %.3lf Mbps, ETA %.2lf 秒'
                             % (rx / 1048576, rx / 1048576 / duration, rx * 8 / 1048576 / duration, duration / rx * size))
            await asyncio.sleep (REPOTER_GAP)

    asyncio.create_task (reporter ())

    async with aiohttp.ClientSession () as sess:
        async with sess.get (url) as resp:
            if resp.status != 200:
                await reply_msg (meta, f'请求 {url} 失败。HTTP 状态码 {resp.status}')
            filename = download_dir (str (meta.msgid))
            with open (filename, 'wb') as f:
                async for chunk in resp.content.iter_chunked (CHUNK_SIZE):
                    f.write (chunk)
                    rx += len (chunk)
            duration = time.time () - start_time
            finished = True
            await reply_msg (meta, f'已成功下存到 {filename},耗时 %.3lf 秒,合 %.3lf MB/s = %.3lf Mbps\n将在 {FILE_KEEP_MIN} 分钟后删除。'
                             % (duration, size / duration / 1048576, size * 8 / 1048576 / duration))
            await asyncio.sleep (FILE_KEEP_MIN * 60)
            await reply_msg (meta, f'到时,删除文件 {filename}')
            os.system (f'rm {download_dir (filename)}')

def random_token ():
    s = ''
    for i in range (16):
        s += random.choice ('abcdefghijklmnopqrstuvwxyz')
    return s
def gen_confirm_token (meta):
    while True:
        s = str (meta.uid) + random_token ()
        s = base64.b64encode (s.encode ('utf-8')).decode ('utf-8')
        if s not in confirm_queue:
            return s

CONFIRM_TIMEOUT = 60
async def confirm (meta, msg):
    token = gen_confirm_token (meta)
    future = asyncio.Future ()
    confirm_queue[token] = future
    await reply_msg (meta, f'{msg}\n{CONFIRM_TIMEOUT} 秒内 /confirm {token} 来确认')
    try:
        result = await asyncio.wait_for (future, timeout=CONFIRM_TIMEOUT)
    except asyncio.TimeoutError:
        await reply_msg (meta, f'超时,取消操作')
        result = False
    else:
        await reply_msg (meta, f'成功确认')
    finally:
        del confirm_queue[token]
    return result

def register_cmd (cmd):    
    def decorator (fn):
        if cmd in commands:
            raise Exception(f'试图第二次注册命令 {cmd}')
        commands[cmd] = fn
        def wrapper (*args, **kwargs):
            return fn (*args, **kwargs)
        return wrapper
    return decorator

def trusted_sender (fn):
    async def wrapper (*args, **kwargs):
        meta = args[0]
        if meta.uid in trusted_senders:
            return await fn (*args, **kwargs)
        return await reply_msg (args[0], f'执行该命令的权限不足')
    return wrapper

@register_cmd ('/confirm')
async def cmd_confirm (meta, arg):
    if len (arg) != 2:
        return await reply_msg (meta, f'需要令牌')
    token = arg[1]
    if token not in confirm_queue:
        return await reply_msg (meta, f'无效令牌')
    uid = base64.b64decode (token).decode ('utf-8')[:-16]
    if uid != str (meta.uid):
        return await reply_msg (meta, f'错误的确认者。期望 {uid},得到 {meta.uid}')
    confirm_queue[token].set_result (True)

@register_cmd ('/testre')
async def cmd_testre (meta, arg):
    res = await wait_reply (meta, f'请回复这条消息')
    if not res:
        await reply_msg (meta, '为啥不回复?')
    await reply_msg (meta, f'你说:{' '.join (res)}')

@register_cmd ('/ping')
async def cmd_ping (meta, arg):
    s = f'PONG {meta.uid}'
    if meta.uid in trusted_senders:
        s += ' (privileged)'
    if await db_banned (meta.uid):
        s += ' (banned)'
    await reply_msg (meta, s)

MAX_BINDS = 3
@register_cmd ('/bind')
async def cmd_bind (meta, arg):
    if len (arg) != 2:
        return await reply_msg (meta, f'需要 WyOJ 用户名\nUsage: /bind username')
    name = arg[1]
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        return await reply_msg (meta, '不合法用户名')
    if not await confirm (meta, f'确认将 {meta.uid} 绑定到 {name} 吗?'):
        return
    if await db_check_bound (name):
        qq, _ = await db_query_by_name (name)
        return await reply_msg (meta, f'无法绑定 {name};已绑定到 {qq}')
    if await db_count_bound (meta.uid) >= MAX_BINDS:
        return await reply_msg (meta, f'无法绑定更多账号。单个 QQ 绑定最大值为 {MAX_BINDS}')
    await db_insert_bind (meta.uid, name)
    wyoj_enable (meta.uid, name)
    await reply_msg (meta, f'成功将 {name} 绑定到 {meta.uid}')

@register_cmd ('/rebind')
@trusted_sender
async def cmd_rebind (meta, arg):
    if len (arg) != 2:
        return await reply_msg (meta, f'需要 WyOJ 用户名\nUsage: /bind username')
    name = arg[1]
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        return await reply_msg (meta, '不合法用户名')
    if not await db_check_bound (name):
        return await reply_msg (meta, f'无绑定')
    qq, _ = await db_query_by_name (name)
    await db_delete_bind (name)
    await db_insert_bind (qq, name)
    wyoj_enable (qq, name)
    await reply_msg (meta, f'已重新绑定')

@register_cmd ('/refresh')
@trusted_sender
async def cmd_refresh (meta, arg):
    q = await db_query_binds ()
    s = ''
    cnt = 0
    for qq, name in q:
        await db_delete_bind (name)
        await db_insert_bind (qq, name)
        wyoj_enable (qq, name)
        s += f'{qq}\t\t{name}\n'
        cnt += 1
    await reply_msg (meta, f'已成功绑定:\n{s}\n计 {cnt} 人')

@register_cmd ('/unbind')
async def cmd_unbind (meta, arg):
    if len (arg) != 2:
        return await reply_msg (meta, f'需要 WyOJ 用户名\nUsage: /unbind username')
    name = arg[1]
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        return await reply_msg (meta, '不合法用户名')
    if not await db_check_bound (name):
        return await reply_msg (meta, '该账号未绑定')
    qq, _ = await db_query_by_name (name)
    if qq != str (meta.uid):
        if meta.uid in trusted_senders:
            await reply_msg (meta, f'你是管理员 {meta.uid},强制解绑,等待确认')
        else:
            return await reply_msg (meta, f'{name} 与 {qq} 的绑定无法由你 ({meta.uid}) 解绑')
    if not await confirm (meta, f'确认将 {meta.uid} 从 {name} 解绑吗?'):
        return
    if not await db_check_bound (name):
        return await reply_msg (meta, f'尚无绑定,无法解绑')
    await db_delete_bind (name)
    wyoj_disable (name)
    await reply_msg (meta, f'成功将 {meta.uid} 与 {name} 解绑')

@register_cmd ('/adminize')
@trusted_sender
async def cmd_adminize (meta, arg):
    if len (arg) not in [ 2, 3 ]:
        return await reply_msg (meta, f'需要用户名及时间\nUsage: /adminize username [time-in-minutes]')
    name = arg[1]
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        return await reply_msg (meta, '不合法用户名')
    time = -1
    if len (arg) == 3:
        try:
            time = int (arg[2])
        except ValueError:
            return await reply_msg (meta, f'不合法的时间 {arg[2]}')
    if not await confirm (meta, f'确定要赋予 {name} 以 {time} 分钟的管理员权限吗?'):
        return
    wyoj_adminize (name)
    await reply_msg (meta, f'{name} 已被赋予管理员权限')
    if time > 0:
        await asyncio.sleep (time * 60)
        wyoj_deadminize (name)
        await reply_msg (meta, f'{name} 的管理员权限已到期')

@register_cmd ('/deadminize')
@trusted_sender
async def cmd_deadminize (meta, arg):
    if len (arg) not in [ 2, 3 ]:
        return await reply_msg (meta, f'需要用户名\nUsage: /deadminize username')
    name = arg[1]
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        return await reply_msg (meta, '不合法用户名')
    if not await confirm (meta, f'确定要取消 {name} 的管理员权限吗?'):
        return
    wyoj_deadminize (name)
    await reply_msg (meta, f'{name} 已被赋予管理员权限')

async def cmd_query_qq (meta, arg):
    qq = arg[2]
    try:
        int (qq)
    except ValueError:
        await reply_msg (meta, f'无效 QQ 号 {qq}')
    res = await db_query_by_id (qq)
    await reply_msg (meta, f'QQ {qq} 绑定到 {len (res)} 个账号:{", ".join (map (lambda x: x[1], res))}')

async def cmd_query_me (meta, arg):
    arg.append ('qq')
    arg.append (str (meta.uid))
    await cmd_query_qq (meta, arg)

async def cmd_query_name (meta, arg):
    name = arg[2]
    if re.match (r'^([A-Za-z0-9_]*)$', name) is None:
        return await reply_msg (meta, '不合法用户名')
    res = await db_query_by_name (name)
    if not res:
        await reply_msg (meta, f'{name} 未绑定')
    else:
        await reply_msg (meta, f'{name} 绑定到 {res[0]}')

@trusted_sender
async def cmd_query_all (meta, arg):
    await reply_msg (meta, f'绑定表:\n{"\n".join (map (lambda x: x[0] + "\t\t" + x[1], await db_query_binds ()))}')

@trusted_sender
async def cmd_query_banned (meta, arg):
    await reply_msg (meta, wyoj_banned ())

@trusted_sender
async def cmd_query_admins (meta, arg):
    await reply_msg (meta, wyoj_admins ())

@register_cmd ('/query')
async def cmd_query (meta, arg):
    if len (arg) == 1:
        return await cmd_query_me (meta, arg)
    elif len (arg) == 2:
        if arg[1] == 'all':
            return await cmd_query_all (meta, arg)
        elif arg[1] == 'banned':
            return await cmd_query_banned (meta, arg)
        elif arg[1] == 'admins':
            return await cmd_query_admins (meta, arg)
    elif len (arg) == 3:
        if arg[1] == 'qq':
            return await cmd_query_qq (meta, arg)
        elif arg[1] == 'name':
            return await cmd_query_name (meta, arg)
    await reply_msg (meta, '期望零个或两个参数\nUsage: /query || /query qq qqid || /query name username')

@register_cmd ('/ban')
@trusted_sender
async def cmd_ban (meta, arg):
    if len (arg) != 2:
        return await reply_msg (meta, f'需要一个参数\nUsage: /ban username')
    try:
        qq = str (int (arg[1]))
    except ValueError:
        return await reply_msg (meta, f'无效 QQ 号 {qq}')
    if not await confirm (meta, f'确定要封禁 {qq} 及其名下的所有账号吗?'):
        return
    await db_ban (qq)
    res = await db_query_by_id (qq)
    for _, name in res:
        wyoj_disable (name)
    await reply_msg (meta, f'被封禁的用户:{", ".join (map (lambda x: x[1], res))}')

@register_cmd ('/unban')
@trusted_sender
async def cmd_unban (meta, arg):
    if len (arg) != 2:
        return await reply_msg (meta, f'需要一个参数\nUsage: /unban username')
    try:
        qq = str (int (arg[1]))
    except ValueError:
        return await reply_msg (meta, f'无效 QQ 号 {qq}')
    if not await confirm (meta, f'确定要解封 {qq} 吗(不解封账号)?'):
        return
    await db_unban (qq)
    await reply_msg (meta, '成功解封')

@register_cmd ('/banlist')
async def cmd_banlist (meta, arg):
    await reply_msg (meta, f'被封禁的 QQ:{", ".join (map (lambda x: x[0], await db_banned_all ()))}')

@register_cmd ('/entrust')
@trusted_sender
async def cmd_entrust (meta, arg):
    global trusted_senders
    if len (meta.ats) == 0:
        await reply_msg (meta, f'需要 At 某人\nUsage: [@someone..] /entrust [@someone..]')
    if not await confirm (meta, f'确定信任 {' '.join (meta.ats)} 吗?'):
        return
    for i in meta.ats:
        try:
            int (i)
        except ValueError:
            return await reply_msg (meta, f'非法 At')
    trusted_senders += map (int, meta.ats)
    await reply_msg (meta, f'成功信任')

@register_cmd ('/detrust')
@trusted_sender
async def cmd_detrust (meta, arg):
    global trusted_senders
    if len (meta.ats) == 0:
        await reply_msg (meta, f'需要 At 某人\nUsage: [@someone..] /entrust [@someone..]')
    if not await confirm (meta, f'确定取消信任 {' '.join (meta.ats)} 吗?'):
        return
    for i in meta.ats:
        try:
            int (i)
        except ValueError:
            return await reply_msg (meta, f'非法 At')
        if int (i) not in trusted_senders:
            await reply_msg (meta, '其实 {i} 并未被信任。')
    for i in meta.ats:
        trusted_senders.remove (int (i))
    await reply_msg (meta, f'成功取消信任')

@register_cmd ('/trusts')
async def cmd_trusts (meta, arg):
    await reply_msg (meta, f'信任用户:{' '.join (map (str, trusted_senders))}')


async def run_cmd (cmd):
    proc = await asyncio.create_subprocess_shell (
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE)
    stdout, stderr = await proc.communicate ()
    return stdout.decode ('utf-8'), stderr.decode ('utf-8')

@register_cmd ('/upload')
@trusted_sender
async def cmd_upload (meta, arg):
    pid = arg[1]
    try:
        pid = int (pid)
    except ValueError:
        return await reply_msg (meta, f'非法题号')
    if len (meta.refs) != 1:
        return await reply_msg (meta, f'需要有且仅有一个指向题目数据压缩包的引用')
    if not os.path.exists (download_dir (meta.refs[0])):
        return await reply_msg (meta, f'数据文件未被下载:错误的引用或者已经超时')
    await reply_msg (meta, f'正在上传数据')
    stdout, stderr = await run_cmd (f'/home/ryp/upload {pid} {download_dir (meta.refs[0])}')
    await reply_msg (meta, f'命令执行完毕。标准输出:\n{stdout}\n\n标准错误:{stderr}\n' \
            f'若无错误,请打开 https://oj.ryp.org.cn/problem/{pid}/manage/data 点击校验配置并同步数据')

async def dispatch (meta):
    if not meta.cmd[0] in commands:
        return await reply_msg (meta, f'未知命令 {meta.cmd[0]}')
    await commands[meta.cmd[0]] (meta, meta.cmd)

async def handle_msg (data):
    try:
        data = json.loads (data)
        meta = MessageMeta (data)
        if meta.gid != GROUP_ID:
            return
        if data['message'][0]['type'] == 'file':
            return await receive_file (meta, data)
        if meta.msg[0] != '/':
            return
        print (f'收到群 {meta.gid} 的用户 {meta.uid} 的命令:{' '.join (meta.cmd)}')
        await dispatch (meta)
    except KeyError as e:
        pass
    except Exception as e:
        print (f'未知错误:{e}')
        traceback.print_exc ()

async def client_loop ():
    global ws, sqlconn

    ws = await websockets.connect (wsserver)
    sqlconn = await aiosqlite.connect ('data.db')
    print ('已成功连接到 NapCat 服务器')
    async for msg in ws:
        asyncio.create_task (handle_msg (msg))

if __name__ == '__main__':
    asyncio.run (client_loop ())

新博客

2025-09-04 15:59:41 By ryp
import tkinter as tk
import tkinter.simpledialog
from tkinterdnd2 import DND_FILES, TkinterDnD

rt = TkinterDnD.Tk ()
rt.title ('WyOJ 数据配置工具')

def ask_subtask_nr ():
    n = tk.simpledialog.askinteger (title='Subtask 数量',
                                    prompt='要配置的 Subtask 数量',
                                    initialvalue=0)

    print (f'你想配置 {n} 个 Subtask')
    return n

def configure_subtask (i):
    def config ():
        win = tk.Toplevel (rt)
        win.title (f'配置 Subtask {i}')

        droparea = tk.Label (win,
                             text='>>> 请将本 Subtask 中的测试点拖到这里 <<<',
                             height=10)
        txt = tk.Text (win)
        txt.pack ()

        def drop (ev):
            fp = ev.data
            txt.insert (tk.END, f'{fp}\n')

        droparea.drop_target_register (DND_FILES)
        droparea.dnd_bind ('<<Drop>>', drop)
        droparea.pack ()
        txt.pack ()

    return config

buttons = list ()
def create_subtask_buttons (nr):
    for i in range (1, nr + 1):
        butt = tk.Button (rt, text=f'配置 Subtask {i}',
                          command=configure_subtask (i))
        butt.place (x=80, y=20 + i * 40, width=60, height=30)
        butt.pack ()
        buttons.append (butt)

nr = ask_subtask_nr ()
create_subtask_buttons (nr)
rt.mainloop ()

WyOJ 账号激活方法

2025-08-28 21:27:56 By ryp

为了减少完全以刷号为意义的小号出现,并且将线上可能出现的不恰当行为言论与线下相关联,我们决定 任何账号都必须绑定到 QQ 以进行活动

具体方法:

  • 加入 QQ 群 1061173220,口令是 wyojyydsrypnb

  • 在群中发消息 /bind XXX,其中 XXX 是你的用户名

  • WyOJ Musume,也就是 WyOJ 的机器人,会回应你的消息。

  • 根据 WyOJ Musume 的指示来完成操作,绑定成功后,你的账号便可用。

注意:

每个账号只能绑定一个 QQ,但一个 QQ 可以绑定最多三个账号。

若某个账号决定被封禁,其对应的 QQ 会被封禁,对应 QQ 下的所有账号也会被封禁。

2025.8.22 题解

2025-08-22 16:51:12 By ryp

书记的纸牌游戏 1

$\text{Subtask 1(40pts)}$

递推,比正解还麻烦,略。

$\text{Subtask 2(60pts)}$

超级笨蛋博弈论,找规律。

考虑倒推,剩 $1$ 到 $m$ 张是先手有必胜策略,$m+1$ 是后手有必胜策略。

继续,考虑 $m+2$ 到 $2m+1$,先手方可以将纸牌数控制为 $m+1$,使得对方从 $m+1$ 开始取,进而自己有必胜策略;若 $2m+2$,则不论如何取,剩下纸牌在 $[m+2,2m+1]$ 范围内,后手有必胜策略。

归纳可知,若初始纸牌数 $n$ 为 $m+1$ 的倍数,后手有必胜策略,否则先手有必胜策略。

购物计划

$\text{Subtask 1(10pts)}$

$n\le 10$,乱搞点暴搜之类的东西大概就可以过的吧(其实我也不知道有什么具体的办法)。


$\text{Subtask 2(50pts)}$

$n\le 10^3$,是不加优化的 DP 做法。

问题的一个难解决的地方就在于分组的实质是选数并建立集合,而不是选取某一段。但要使每个集合中的最大值减最小值(集合即每一组,值即体积,下同)尽可能小,每个集合的元素就一定是排序后的数中某连续的一段,于是直接按物品体积进行排序,这样就可以把集合问题转化为分段问题了。

设 $f_i$ 表示将前 $i$ 个物品进行分组的最小“不整齐程度”,起初 $f_0=0$,有如下转移:

$$f_i=\begin{cases}v_i-v_1 & 1\le i< 2k \\\min \limits_{j=0}^{i-k}(f_j+v_i-v_{j+1}) & 2k\le i\le n \end{cases}$$

对于 $2k\le i\le n$ 的情况,枚举每个 $j$,取最小的 $f_j-v_{j+1}$ 进行转移,$f_n$ 即为最终答案。

转移次数 $O(n)$,转移复杂度 $O(n)$,总的时间复杂度为 $O(n^2)$。


$\text{Subtask 3(40pts)}$

考虑对上述 $O(n^2)$ 的做法进行优化。

复杂度高的原因出在每次取 $\min \limits_{j=0}^{i-k}(f_j-v_{j+1})$ 时需要 $O(n)$ 枚举 $j$,而实际上 $\left [0,i-k \right ]$ 是一个左端静止且右边递增的区间,因此可以直接用一个变量维护这个最值(甚至不需要写单调队列)。

现在,转移次数 $O(n)$,转移复杂度 $O(1)$,DP 的时间复杂度为 $O(n)$,由于排序是 $O(n \log n)$ 的,故总的时间复杂度为 $O(n \log n)$,可以通过此题。


代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
const int N=1e6+5;
const ll inf=1e18;
int n,k;
ll v[N],f[N],mini;
int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&v[i]);
    }
    sort(v+1,v+1+n);
    for(int i=1;i<=n;i++)
    {
        if(i<k*2)f[i]=v[i]-v[1];
        else
        {
            mini=min(mini,f[i-k]-v[i-k+1]);
            f[i]=v[i]+mini;
        }
    }
    printf("%lld",f[n]);
    return 0;
}

烟花大会

$\text{Subtask 1(30pts)}$


$O(n^2)$ 求出任意两点之间的边权(即时间,下同),直接从 $1$ 跑最短路。


$\text{Subtask 2(70pts)}$


考虑边权与坐标的关系,若从一点 $i$ 走到 $j$ 的时间是 $|x_i-x_j|$,且存在另一点 $k$ 满足 $x_k$ 在 $x_i,x_j$ 之间,那么边 $(i,j)$ 可被边 $(i,k),(k,j)$ 代替,也即经过 $k$ 不会使时间更长,这是因为:

$$\begin{aligned} dis_{i,j} &= |x_i-x_j| \\ &= |x_i-x_k| + |x_k-x_j| \\ &\ge \min \left\{ |x_i-x_k|,|y_i-y_k| \right\} + \min \left\{ |x_k-x_j|,|y_k-y_j| \right\} \\ &= dis_{i,k} +dis_{k,j} \end{aligned}$$

纵坐标同理。

对此推广,我们得到结论:对所有点按横坐标排序,相邻点连边,然后纵坐标做相同处理,然后跑最短路即可。边数是 $O(n)$ 的,可以通过此题。


代码

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<algorithm>
#define ll long long
using namespace std;
const int N=1e5+5;
const ll inf=1e18;

int n;
struct node
{
    int num;
    ll x,y;
}dot[N];
bool cmp1(node a,node b)
{
    return a.x<b.x;
}
bool cmp2(node a,node b)
{
    return a.y<b.y;
}

struct edge
{
    int v;
    ll w;
};
vector<edge>e[N];
priority_queue<pair<ll,int> >q;
ll d[N];
bool vis[N];

void dijkstra()
{
    for(int i=1;i<=n;i++)d[i]=inf;
    d[1]=0;
    q.push(make_pair(0,1));
    while(!q.empty())
    {
        int u0=q.top().second;
        q.pop();
        if(vis[u0])continue;
        vis[u0]=1;
        for(int i=0;i<e[u0].size();i++)
        {
            int v0=e[u0][i].v,w0=e[u0][i].w;
            if(d[u0]+w0<d[v0])
            {
                d[v0]=d[u0]+w0;
                q.push(make_pair(-d[v0],v0));
            }
        }
    }
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        dot[i].num=i;
        scanf("%lld%lld",&dot[i].x,&dot[i].y);
    }

    sort(dot+1,dot+1+n,cmp1);
    for(int i=1;i<n;i++)
    {
        int u0=dot[i].num,v0=dot[i+1].num,
        w0=dot[i+1].x-dot[i].x;
        e[u0].push_back({v0,w0});
        e[v0].push_back({u0,w0});
    }

    sort(dot+1,dot+1+n,cmp2);
    for(int i=1;i<n;i++)
    {
        int u0=dot[i].num,v0=dot[i+1].num,
        w0=dot[i+1].y-dot[i].y;
        e[u0].push_back({v0,w0});
        e[v0].push_back({u0,w0});
    }

    dijkstra();
    printf("%lld",d[n]);
    return 0;
}

告白气球

$\text{Subtask 1(10pts)}$

$O(n^3)$ 超级大暴力,略。

$\text{Subtask 2(5pts)}$

容易知道均衡区间不存在,答案全是 $0$。

$\text{Subtask 3(15pts)}$

建 $ST$ 表维护区间 $min$ 和 $\max$,$O(1)$ 判断一个区间是否合法,时间复杂度 $O(n^2)$。

$\text{Subtask 4(35pts)}$

这一档区分出常数太大的 $\log$ 以及一些神秘复杂度,像根号、$\log^2$ 之类的,放到下面讲。

$\text{Subtask 5(35pts)}$

我们考虑位置 $i$ 成为一个均衡区间的左端点需要满足的限制(右端点同理):

  1. 右边有比 $v_i$ 大的数;
  2. 右边有比 $v_i$ 大的数。

也就是说,$i$ 符合条件需要右端点 $j$ 足够大。

我们做四次单调栈分别预处理出每个 $v_i$ 的左侧和右侧第一个大于或小于 $v_i$ 的数的位置(不存在则用 $0$ 或 $n+1$ 补充)。左侧的两个位置取 $\min$,记为 $L_i$;右侧取 $\max$,记为 $R_i$。

那么 $[i,j]$ 是均衡区间的充要条件是,$j>R_i$ 且 $L_j>i$。

因此,将所有 $(j,L_j)$ 视为平面上的点,$i$ 的答案即平面上符合上述条件的点的个数,离线下来用树状数组做二位数点即可。注意要离散化。

这个做法有 $O(n\log n)$ 的时间复杂度和 $O(n)$ 的空间复杂度。

ryp Avatar