-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 499 KB
/
content.json
1
{"meta":{"title":"Liynw の 博客儿","subtitle":"hi! ヾ(≧▽≦*)o","description":"cqbz 最菜","author":"Liynw","url":"https://blog.liynw.top","root":"/"},"pages":[{"title":"","date":"2023-06-28T09:52:39.423Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"manifest.json","permalink":"https://blog.liynw.top/manifest.json","excerpt":"","text":"{\"lang\":\"en\",\"dir\":\"ltr\",\"name\":\"Liynw の 博客儿\",\"description\":\"About OI!\",\"display\":\"standalone\",\"short_name\":\"QwQ\",\"scope\":\"/\",\"start_url\":\"https://blog.liynw.top\",\"theme_color\":\"#ffe5ff\",\"background_color\":\"#ffe5ff\",\"icons\":[{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-36x36.png\",\"sizes\":\"36x36\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-48x48.png\",\"sizes\":\"48x48\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-72x72.png\",\"sizes\":\"72x72\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-96x96.png\",\"sizes\":\"96x96\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-144x144.png\",\"sizes\":\"144x144\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-192x192.png\",\"sizes\":\"192x192\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-256x256.png\",\"sizes\":\"256x256\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-384x384.png\",\"sizes\":\"384x384\",\"type\":\"image/png\"},{\"src\":\"https://cdn.jsdelivr.net/npm/[email protected]/img/siteico/android-chrome-512x512.png\",\"sizes\":\"512x512\",\"type\":\"image/png\"}]}"},{"title":"","date":"2023-06-28T09:52:39.423Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"package.json","permalink":"https://blog.liynw.top/package.json","excerpt":"","text":"{\"name\":\"liynw-blog\",\"version\":\"1.0.11\",\"description\":\"Blog\",\"main\":\"index.html\",\"scripts\":{\"test\":\"echo \\\"Error: no test specified\\\" && exit 1\"},\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/Liynw/BLOG.git\"},\"keywords\":[\"blog\"],\"author\":\"Liynw\",\"license\":\"ISC\",\"bugs\":{\"url\":\"https://github.com/Liynw/BLOG/issues\"},\"homepage\":\"https://github.com/Liynw/BLOG#readme\"}"},{"title":"关于博主与本站","date":"2022-01-29T09:38:02.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"about/index.html","permalink":"https://blog.liynw.top/about/","excerpt":"","text":"与 Liynw 通话中: /* * botui 0.3.9 * A JS library to build the UI for your bot * https://botui.org * * Copyright 2019, Moin Uddin * Released under the MIT license. */ a.botui-message-content-link:focus { outline: thin dotted } a.botui-message-content-link:focus:active,a.botui-message-content-link:focus:hover { outline: 0 } form.botui-actions-text { margin: 0 } button.botui-actions-buttons-button,input.botui-actions-text-input { margin: 0; font-size: 100%; line-height: normal; vertical-align: baseline } button.botui-actions-buttons-button::-moz-focus-inner,input.botui-actions-text-input::-moz-focus-inner { border: 0; padding: 0 } button.botui-actions-buttons-button { cursor: pointer; -webkit-appearance: button } .botui-app-container { width: 100%; height: 100%; line-height: 1 } @media (min-width: 400px) { .botui-app-container { width:400px; height: 500px; margin: 0 auto } } .botui-container { width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden } .botui-message { margin: 10px 0; min-height: 20px } .botui-message:after { display: block; content: \"\"; clear: both } .botui-message-content { width: auto; max-width: 75%; display: inline-block } .botui-message-content.human { float: right } .botui-message-content iframe { width: 100% } .botui-message-content-image { margin: 5px 0; display: block; max-width: 200px; max-height: 200px } .botui-message-content-link { text-decoration: underline } .profil { position: relative; border-radius: 50% } .profil.human { float: right; margin-left: 5px } .profil.agent { float: left; margin-right: 5px } .profil>img { width: 26px; height: 26px; border: 2px solid #e8e8e8 } .profil>img.agent { content: url(http://decodemoji.com/img/logos/blue_moji_hat.svg); border-radius: 50% } button.botui-actions-buttons-button { margin-top: 10px; margin-bottom: 10px } button.botui-actions-buttons-button:not(:last-child) { margin-right: 10px } @media (min-width: 400px) { .botui-actions-text-submit { display:none } } @import url(https://fonts.googleapis.com/css?family=Open+Sans);.botui-container { font-size: 14px; background-color: #fff; font-family: \"Open Sans\",sans-serif } .botui-messages-container { padding: 10px 20px } .botui-actions-container { padding: 10px 20px } .botui-message { min-height: 30px } .botui-message-content { padding: 7px 13px; border-radius: 15px; color: #595a5a; background-color: #ebebeb } .botui-message-content.human { color: #f7f8f8; background-color: #919292 } .botui-message-content.text { line-height: 1.3 } .botui-message-content.loading { background-color: rgba(206,206,206,.5); line-height: 1.3; text-align: center } .botui-message-content.embed { padding: 5px; border-radius: 5px } .botui-message-content-link { color: #919292 } .botui-actions-text-input { border: 0; outline: 0; border-radius: 0; padding: 5px 7px; font-family: \"Open Sans\",sans-serif; background-color: transparent; color: #595a5a; border-bottom: 1px solid #919292 } .botui-actions-text-submit { color: #fff; width: 30px; padding: 5px; height: 30px; line-height: 1; border-radius: 50%; border: 1px solid #919292; background: #777979 } .botui-actions-buttons-button { border: 0; color: #fff; line-height: 1; cursor: pointer; font-size: 14px; font-weight: 500; padding: 7px 15px; border-radius: 4px; font-family: \"Open Sans\",sans-serif; background: #777979; box-shadow: 2px 3px 4px 0 rgba(0,0,0,.25) } .botui-actions-text-select { border: 0; outline: 0; border-radius: 0; padding: 5px 7px; font-family: \"Open Sans\",sans-serif; background-color: transparent; color: #595a5a; border-bottom: 1px solid #919292 } .botui-actions-text-searchselect { border: 0; outline: 0; border-radius: 0; padding: 5px 7px; font-family: \"Open Sans\",sans-serif; background-color: transparent; color: #595a5a; border-bottom: 1px solid #919292 } .botui-actions-text-searchselect .dropdown-toggle { border: none!important } .botui-actions-text-searchselect .selected-tag { background-color: transparent!important; border: 0!important } .slide-fade-enter-active { transition: all .3s ease } .slide-fade-enter,.slide-fade-leave-to { opacity: 0; transform: translateX(-10px) } .dot { width: .5rem; height: .5rem; border-radius: .5rem; display: inline-block; background-color: #919292 } .dot:nth-last-child(1) { margin-left: .3rem; animation: loading .6s .3s linear infinite } .dot:nth-last-child(2) { margin-left: .3rem; animation: loading .6s .2s linear infinite } .dot:nth-last-child(3) { animation: loading .6s .1s linear infinite } @keyframes loading { 0% { transform: translate(0,0); background-color: #ababab } 25% { transform: translate(0,-3px) } 50% { transform: translate(0,0); background-color: #ababab } 75% { transform: translate(0,3px) } 100% { transform: translate(0,0) } }"},{"title":"留言板","date":"2022-01-30T09:05:37.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"/barrage/index.html","permalink":"https://blog.liynw.top/barrage/","excerpt":"","text":"实际上只是善用了评论功能。 如果您有什么话想和我说,或者说您发现文章的 markdown/$\\LaTeX$ 挂了或有学术性错误,欢迎评论哦 qwq。 与主机通讯中……"},{"title":"闲言碎语","date":"2022-07-24T07:09:02.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"bber/index.html","permalink":"https://blog.liynw.top/bber/","excerpt":"","text":"ipseak加载中 var head = document.getElementsByTagName('head')[0] var meta = document.createElement('meta') meta.name = 'referrer' meta.content = 'no-referrer' head.appendChild(meta) if (ispeak) { ispeak .init({ el: '#ispeak', api: 'https://kkapi.liynw.top/', author: '62dc015ea88c248780491e70', pageSize: 10, loading_img: 'https://bu.dusays.com/2021/03/04/d2d5e983e2961.gif', speakPage: '/bber/', comment: function (speak) { // 4.4.0 之后在此回调函数中初始化评论 const { _id, title, content } = speak const contentSub = content.substring(0, 30) new Artalk({ el: '.ispeak-comment', // 默认情况下 ipseak 生成class为 ispeak-comment 的div pageKey: '/bber/info.html/#/' + _id, // 手动传入当前speak的唯一id pageTitle: title || contentSub, // 手动传入当前speak的标题(由于content可能过长,因此截取前30个字符) server: 'https://artalk.liynw.top:9001/' }) } }) .then(function () { console.log('ispeak 加载完成') document.getElementById('tip').style.display = 'none' }) } else { document.getElementById('tip').innerHTML = 'ipseak依赖加载失败!' }"},{"title":"分类","date":"2018-01-05T00:00:00.000Z","updated":"2023-06-28T09:52:39.423Z","comments":false,"path":"categories/index.html","permalink":"https://blog.liynw.top/categories/","excerpt":"","text":""},{"title":"飞鸽传书 | 友链朋友圈","date":"2022-11-06T16:31:03.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"fcircle/index.html","permalink":"https://blog.liynw.top/fcircle/","excerpt":"","text":"var fdataUser = { apiurl: 'https://fc.liynw.top/', defaultFish: 500, hungryFish: 500, } 🎣 钓鱼 window.circle_config = { api: 'https://fc.liynw.top' }"},{"title":"版权协议","date":"2022-09-01T16:09:31.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"license/index.html","permalink":"https://blog.liynw.top/license/","excerpt":"","text":"Copyright ©Liynw 2022 为了保持文章质量,并保持互联网的开放共享精神,保持页面流量的稳定,综合考虑下本站的所有原创文章均采用 cc 协议中比较严格的创作共用-非商业性-禁止演绎 4.0 国际标准。这个页面主要想能够更加清楚明白的介绍本站的协议标准和要求,方便您合理的使用本站的文章。 本站无广告嵌入和商业行为。违反协议的行为不仅会损害原作者的创作热情,而且会影响整个版权环境。强烈呼吁您能够在转载时遵守协议。遵守协议的行为几乎不会对您的目标产生负面影响,鼓励创作环境是每个创作者的期望。 您可以做什么?只要您遵守本页的许可,您可以自由地共享文章的内容 — 在任何媒介以任何形式复制、发行本作品,并且无需通知作者。 你需要遵守什么样的许可?署名您必须标注内容的来源,您需要在文章开头部分(或者明显位置)标注原文章链接(建议使用超链接提升阅读体验)。 禁止商用本站内容免费向互联网所有用户提供,分享本站文章时禁止商业性使用、禁止在转载页面中插入广告(例如谷歌广告、百度广告)、禁止阅读的拦截行为(例如关注公众号、下载 App 后观看文章)。 禁止演绎 分享全部内容(无修改) 您需要在文章开头部分(或者明显位置)标注原文章链接(建议使用超链接) 分享部分截取内容或者衍生创作 目前本站全部原创文章的衍生品禁止公开分享和分发。如有更好的修改建议,可以在对应文章下留言。如有衍生创作需求,可以在评论中联系。 什么内容会被版权保护?包括但不限于: 文章封面图片 文章标题和正文 站点图片素材(不含主题自带素材) 例外情况本着友好互相进步的原则,被本站友链收录的博客允许博客文章内容的衍生品的分享和分发,但仍需标注出处。 本着互联网开放精神,您可以在博客文章下方留言要求授权博文的衍生品的分享和分发,并标注您的网站地址。 作者原创代码及网站源代码协议网站源代码采用 GPL 协议,其它代码采用 MIT 协议。如有不同,作者会进行标注。 本站代码参考 Akilarの糖果屋 张洪Heo DORAKIKA LYXの小破站 Leonus 轻笑 Chuckle"},{"title":"友人帐","date":"2018-06-07T22:17:49.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"link/index.html","permalink":"https://blog.liynw.top/link/","excerpt":"","text":"#article-container img { margin: 0 auto !important; } 欢迎添加友链!如果您愿意的话,可以在本页面评论申请添加,不过您的站点需要符合以下条件哦: 国内可以正常访问,且不会随随便便跑路。 开启了强制 HTTPS。 没有违法乱纪的内容,没有广告。 不是强制性的,不过还是希望您可以先把我的破站挂在您网站的友链上! 留言格式: 12345name: # 网站名称 或 博主昵称link: # 网站地址avatar: # 头像链接,建议使用 NPMdescr: # 网站描述siteshot: # (非必需)网站截图 我的信息: 12345name: Liynwlink: https://blog.liynw.topavatar: https://cdn.jsdelivr.net/npm/[email protected]/img/avatar.webp # CDN 可换descr: Every little helps.siteshot: https://cdn.jsdelivr.net/npm/[email protected]/img/siteshot.webp"},{"title":"您好像离线了呢(/ω\*)","date":"2022-09-01T16:51:22.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"offline/index.html","permalink":"https://blog.liynw.top/offline/","excerpt":"","text":"如果您来到了这个荒无人烟的地方,那说明您与 Liynw 的站点失去了联系。 这有可能是因为 Liynw 的主站及所有备用站点都炸了,当然,更有可能是您的网络连接出现了问题。 请检查您的网络连接,如果无误,请反馈至Liynw 的邮箱。 感谢您的支持与理解!"},{"title":"画廊","date":"2022-08-25T16:34:03.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"photos/index.html","permalink":"https://blog.liynw.top/photos/","excerpt":"","text":"电脑壁纸 Pixiv精选,保证无R18(x 2233壁纸精选 如题 简洁风格壁纸 也是博主很喜欢的一类风格"},{"title":"标签","date":"2018-01-05T00:00:00.000Z","updated":"2023-06-28T09:52:39.423Z","comments":false,"path":"tags/index.html","permalink":"https://blog.liynw.top/tags/","excerpt":"","text":""},{"title":"站点更新日志","date":"2022-01-29T10:06:50.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"update/index.html","permalink":"https://blog.liynw.top/update/","excerpt":"","text":"更新日志2021 04 📕 前置工作,注册 Github,下载 Git、Node.js 和 VSCode。 08 🎉 建站成功!📕 添加 Next 主题并初步配置,搬运以前的文章。 10 🎨 添加 Butterfly 主题并进行自定义(此时还没开始魔改)。 11 ✏ 修复了一些 bug。🎨 初步部署相册功能和 404 页面。📕 添加 Hexo-Admin 但是没怎么用。 12 📕 【重大更新】启用 Twikoo。📕 补坑,添加更新日志和留言板。 2022 01 🎉 【重大更新】将主题升级至 4.x。🎨 添加大量样式美化(开始魔改了)。 02 📕 【重大更新】配置 GitHub Action,实现自动部署。🎨 优化了代码 tab 的显示(改为 4 个空格),顺便把之前的魔改搬了回来。 03 🎉 【重大更新】绑定域名 saiodgm.gq,已经可以正常访问了。📕 配置了 Hexoplusplus 后台。📕 安装了 sitemap 和 RSS 的插件。📕 将文章 url 改成了十六进制数字。 04 📕 配置了 Qexo 后台并成功用它更新了一篇文章。📕 配置 artitalk。🎨 添加自定义右键菜单功能。 05 🎨 配置 BotUI。🎨 添加右键菜单切换主题功能。🎨 大概弄了一下留言板免得那么单调。 06 📕 准备期末所以没咋折腾,大概搞了一下 Service Worker(还把网站搞炸了 qwq)。 07 ✏ 对 Service Worker 的内容重构,已经实现大体上正常访问。🎨 添加基于友链的侧边栏通讯录。 08~11 🎉 【重大更新】更换域名 liynw.top,网名也换成了 Liynw。🎨 添加博客设置,顶栏调色盘 icon 进入。一堆 bug 没修🎨 添加亚力克材质。 目前已知 BUG code.tidio.co、pv.sohu.com、busuanzi.ibruce.info、q1.qlogo.cn、pic.imgdb.cn 的资源无法访问(开了 CORS 但是好像没用)"},{"title":"2233 壁纸精选","date":"2022-08-25T16:34:03.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"photos/2233/index.html","permalink":"https://blog.liynw.top/photos/2233/","excerpt":"","text":"点击可以查看图片,电脑端拖动图片/手机端长按图片可以切换壁纸哦~ let time = '' let imgbox = document.querySelector('.fj-gallery') imgbox.addEventListener('contextmenu', e => e.preventDefault()) imgbox.addEventListener('dragend', e => { changeBg('url(' + e.target.src + ')'); }) imgbox.addEventListener('touchstart', e => { time = setTimeout(() => { changeBg('url(' + e.target.src + ')'); }, 500); }) imgbox.addEventListener('touchend', clearTimeout(time))"},{"title":"电脑壁纸","date":"2022-08-25T16:34:03.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"photos/pc/index.html","permalink":"https://blog.liynw.top/photos/pc/","excerpt":"","text":""},{"title":"简洁风格壁纸","date":"2022-08-25T16:34:03.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"photos/simple/index.html","permalink":"https://blog.liynw.top/photos/simple/","excerpt":"","text":"点击可以查看图片,电脑端拖动图片/手机端长按图片可以切换壁纸哦~ let time = '' let imgbox = document.querySelector('.fj-gallery') imgbox.addEventListener('contextmenu', e => e.preventDefault()) imgbox.addEventListener('dragend', e => { changeBg('url(' + e.target.src + ')'); }) imgbox.addEventListener('touchstart', e => { time = setTimeout(() => { changeBg('url(' + e.target.src + ')'); }, 500); }) imgbox.addEventListener('touchend', clearTimeout(time))"}],"posts":[{"title":"BDRG | 梦现之地","slug":"「LimboWiki」DreamStation","date":"2023-06-27T18:15:00.000Z","updated":"2023-06-28T09:52:39.419Z","comments":true,"path":"posts/65963c75/","link":"","permalink":"https://blog.liynw.top/posts/65963c75/","excerpt":"","text":"前言搬运林百,可能一定更新不及时,建议上林百看。 把 MediaWiki 转成 Markdown 真的好累…… 完了,我什么时候变得这么中二了? 链接:词条本体 | 港湾冰冻事件 梦现之地如果违背官设了请留言或联系我,我会修正! 梦现之地·DreamStation -在这里,任何梦想,都能变成现实- 简介 梦现之地,英文名 Dream Station,悬挂于港湾的亚空间。港湾被幽蓝边界吞噬后,迁移至月面。 目前该亚空间已被幽蓝边界吞噬,以下信息来自曾经的亚空间住民 Alya 和 Liynw 以及从幽蓝国打捞上来的数据,由于一些记忆损失,无法保障以下内容 100% 正确。 地理 基本信息 时间流速与主塔相同,每天 20:00 至次日 6:00 为黑夜。 没有太阳和月亮,但是天空的明暗会随时间变化。进入亚空间后,每个人的头上都会悬着一个面向下、只有自己能看见的时钟,告诉人们现在的时间。 气候凉爽湿润,无论冬夏,气温恒定在 18~26 摄氏度,湿度恒定在 45%~60%,适合人类居住。大多时间天气晴好,但一周一般会下一次小雨,持续半天左右。 布局 主体分为两部分:靠近主楼层的商业区和亚空间少量居民的居住区。 302.A.P 后,为防止 Nepγta 的攻击,整个亚空间推行极为严厉的防异措施,保证除了制造的商品之外不会有任何异想之物的滋生。 商业区 商业区分为两层,地上售卖区和地下仓库。售卖区为一个大平层,共有 16 个柜台,每个柜台有 2~3 个工作人员,开放柜台个数会根据人流量决定。地下仓库用于储存 Data 和维持亚空间运营需要的物资。 柜台很大,除了一般超市结账的地方外,还有用于扫描用户大脑的扫描区和工作人员使用的内部工作区,工作区四面有墙也有天花板,顾客看不到里面的样貌。 居住区 由于住民大多为亚空间管理人员,所以也叫“管理区”。整体修建于一块不太平整的大草坪上。中间是亚空间主人伊诺斯·兰特尔(Enosy·Lantar)的居所,周围环绕着工作人员和少数居民的房子。房屋密度小,高度一般在 10~20 米。草坪中间有一条河流穿过,用于供水。 进出方式 进入方式有以下三种: 在主塔内心怀对某物的真诚期盼有概率进入; 前往位于港湾(后迁移至月面)的“梦想酒馆”,连续敲打任意壁画三次即可进入; 使用门牌进入。 进入亚空间后会自动到柜台前的大厅上,大厅背后就是出口,走出去就可以回到主塔。 历史 大事记 * 267.A.P 亚空间建立,并立即确认了商业目的。 * 287.A.P 港湾冰冻事件发生,开始了与 Nepγta 抗争的历史。 * 302.A.P 与 Nepγta 抗争产生重大失误,暂时将亚空间迁移至霓虹外侧并封闭亚空间,三个月后恢复正常。 * 522.A.P 大量售货 AI 知晓真相并出走,亚空间面临人手困难,后找回大多数 AI,其余出逃 AI 被解除与服务器的联系,伊诺斯用这些服务器连接上新的售货 AI。兰特尔父女决裂。 * 770.A.P. 幽蓝边界到达港湾,经亚空间住民投票,亚空间逃离至月面。 * 772.A.P. 幽蓝边界到达月面,亚空间被吞噬。 与 Nepγta 的抗衡 大量 Nepyta 生成后,梦现之地的居民曾想过先下手为强使用生成值堆积的办法把这种异想除掉,但由于试验后发现需要的生成值过多会影响到商业活动,只能退而求其次,采用防守方式。 几百年的抗争史中,此亚空间凭借强大的科技几乎处于优势地位,但 302.A.P 由于管理层的重大失误导致某位 Nepyta 间谍潜入亚空间,亚空间不得不逃往霓虹暂时避灾。 梦现之地档案馆曾经保有一些关于 Nepyta 的信息,现打捞出来的数据如下: Nepyta-001 外表为粉发紫瞳萝莉,梳着双马尾,头上扎着两朵白花,穿着有粉红色花朵刺绣的白色连衣裙。虽然是萝莉但外表极具欺诈性,心理层面实际上是个下手稳准狠的大姐。请不要被 Nepyta 们的外表欺骗。 Nepyta-002 外表为约35岁的男性,几乎浅到看不见的浅蓝色头发,穿着深蓝色的礼服,据调查,疑似于港湾冰冻事件发生时一位正在进行婚礼的男性有关联,他的未婚妻在事件中丧生。行踪诡谲不定,精神状态极不正常。你夺走了我期盼一生的幸福,那么当我选择反抗的时候,请不要惊讶。 Nepyta-??? 与其他拥有固定外表的 Nepyta 们不同的是,???的外表瞬息万变,可以变成任何人的样子,唯一认出他的办法是在足够熟悉他伪装的那个人的情况下与他深度交流,这样你就能寻找到不对劲的地方。被发现之后,他会默默消失,不知道去往何处,也不知道会在什么时候卷土重来。唯有用心交流,我们之间才能保持信任…… 人文 社会结构 伊诺斯统领整个亚空间的发展;管理层多为林泊,掌管亚空间经济、环境、军事、外交等方面;工作人员多为 AI,负责亚空间的基础运营。工作人员 AI 拥有自己的意志,但他们一般不会想去做有悖于亚空间的事情,除非有人点化。 另外还有极少数居民,他们一般和管理层有关系,只是居住在亚空间内,不从事任何关于此亚空间的工作,也不从这里获得报酬。 员工一般没有工资,也不出亚空间。一年时间内员工可请假 15 天到主塔(或其他亚空间),这 15 天内伊诺斯会给他们每天发 16MB 的费用方便他们生存(如果一年出亚空间时间不足 15 天,这一部分钱也会发下来,相当于年终奖)。超过 15 天也可请假,但每天需要倒贴 3MB。 经济发展 为方便商品交换,亚空间与主塔使用货币相同。 亚空间异想制造的商业发达,利润极高,足以承担整个亚空间的算力成本消耗、员工的生活成本和运营梦想酒馆的成本,还能剩下很大一部分,于是都入了伊诺斯的腰包,然后被他用于广泛地用于主塔内。 人机关系 与阿特拉斯相似,除极少数 AI 林泊外,其余 AI 均为伊诺斯所控制的体力劳动型 AI。 商业活动 梦现之地的林泊利用其在林泊现实掌控的资源,将大量算力植入像素塔体系中,制造出大量稳定的异想之物或对某物进行定向的异化当作商品卖出牟利。偶尔也会接把异想之物转化为 AI 林泊的单子,不过一般来说由于工作量庞大且成功率不高,价格令人望而却步。 **提示:请不要把定向异化作为攻击他人的武器,产生的一切后果亚空间概不负责。** 每件商品都需要指定稳定存在的时长,最短为一年,最长持续到像素塔毁灭。在这段时间内,亚空间将提供生成值保持商品稳定存在,过期后将不再提供生成值。商品价格一件起步价 1.5MB,视需要制造的异想维持稳定所需要的算力总和确定。(在 Data 失去实际意义后,收费保持依然不变。) 实现原理 通过收集死者血液等途径收集微机并加以改造,使微机发出虚假的信号装作普通用户,再将林泊现实中的服务器与改造后的微机连接在一起,获取像素塔账号接入塔的系统中,而服务器便可以提供大量的算力。而因为人脑和服务器本质上的区别,接入的“用户”一律认作 AI,也就是在梦现之地内工作的 AI 员工。 科技 在保护亚空间免受 Nepγta 攻击和经商的过程中,该亚空间先进的科技起到了极大的作用。 「随机物」 片状物体,外形可以是各种地砖和墙砖或者覆盖地面和墙面的物体,每一个随机物都有独一无二的编号。在一个人为划定的范围内,所有的随机物可以进行位置的随机变换(不仅是跟别的随机物变换位置,还可以和此范围内普通的地砖等变换位置),且外表上看不出任何变换的痕迹,但它们每个物体所固有的编号不会变化。这些在一个区域内随机变换的随机物叫做一个随机物集群。 所有梦现之地的居民身体内部都装载有一个与大脑对接的薄片状机器,可以和随机物对接,若该居民在某个随机物集群的范围内,此机器就会将随机物的编号信息、位置信息和下一次变换的方案发送至居民大脑内,这样他就可以直接了解到每个随机物的位置,然后根据设定好的顺序激活每个随机物(一般来说,是使用梦现之地居民手背或鞋底安装的特殊物质激活),开启一些隐藏机关。 机关开启以后,随机物会恢复到未激活状态。 目前实现原理暂时不明确,据说和门牌有一丝关联,但该消息真实性不保证。 使用实例:进入亚空间居民区的方式就是依次激活商业区地板上的六块地砖样貌的随机物后从任意一个柜台工作人员区内部的隐藏通道进去。 「扫描仪」 外形为一个大小可变的头盔,套在人头上后,可以读取人脑内的特定信息,以方便定制异想之物或定向异化。 梦想酒馆(Dream Bar) 位于港湾(后迁移至月面)的一家小酒馆,在亚空间建立之前就已经开业了,为亚空间在主塔内的驻地,背靠亚空间悬挂的位置,可通过敲击壁画的方式进入亚空间。 整体分为三层,上两层为正常的酒馆部分,下一层为地下室,食客不能进入,与亚空间内部对接。 看起来占地很小,但内部的空间很大,装饰颇为典雅,四面墙上都挂着大大小小的壁画,据说是酒馆老板斥重金请各个知名画家来画的。 虽然是个酒馆,营业范围十分广泛,你可以在这里买到各种糕点、茶饮。食物十分可口且价格实惠,招牌是鸡尾酒和芝士小蛋糕,每年都有不少知名人士前来品尝,所以生意一直很火爆。 一楼和二楼之间有个旋转楼梯,旋转楼梯的背后有位置在不断变化的几块木砖样貌的随机物就是通往地下室的门。地下室十分简朴,也比较小,只有最基本的通讯设施和应急用品。地下室的门可以从里面锁住,这样外面的门就打不开,和普通的木砖没有任何区别了。 人物 伊诺斯·兰特尔 | Enosy·Lantar 林泊,亚空间主人,外表为 40 岁男性,身高 1m76,正常身材,黑瞳,灰黑色蘑菇头。总是穿着一件白色的衬衫加上黑色的长裤。性格有些古怪但心地善良,在至亲和心腹面前会展现出最温柔的一面,还会把赚来的钱用到他人最需要的地方。据说他酒量极佳,某次与 Nepγta 的抗衡中,Nepyta 们与他轮流喝酒,都没能让他醉,相反那几个人醉得像烂泥似的。其在林泊现实中为某高科技公司的老板,所以拥有大量可以提供算力的服务器。亚空间创建后,他自作主张将一部分服务器投入像素塔中,为实现此目的,他在林泊现实中找到技术人员帮忙,通过技术手段成功将其服务器接入像素塔系统。其姓“Lantar”为英语单词“lantern”所化,意为愿燃烧自己照亮他人前方的路。 艾尔娅·兰特尔 | Alya·Lantar 林泊,像素塔中和林泊现实中均为伊诺斯的女儿,设定见此。与亚空间内 AI 关系较好,甚至告知他们塔的真相,教唆他们逃离亚空间,也是 522.A.P AI 逃离事件的始作俑者。 玲王 | Liynw Alya 转化的 AI 林泊之一,与 Alya 关系很好,不在亚空间内工作。设定见此。一般来说会和 Alya 一起出去瞎搞,不过有时候累了就回亚空间蹭吃蹭喝,也经常跑到梦想酒馆去玩,久而久之还掌握了一点制作糕点和调酒的技术。当然嘛……如果不想去见 Igallta 姐姐的话,最好还是不要品尝呢…… 石榴 | Sixteen 销售 AI,平时在柜台前工作。外表为 26 岁女性,身高 1m71,金色短发,蓝瞳,带边框很细的眼镜,穿深绿色的制服。由于其编号为 16,被 Alya 亲切地称为“石榴”。522.A.P 在一位小女孩顾客和 Alya 的引导下,了解了塔的真相并出逃港湾,并且没有被找回来,后来干脆就没找了。有一副好嗓子,出逃后当了歌手在像素塔各层开演唱会,名气似乎还不小。 紫藤 | Wisteria 伊诺斯在霓虹一多子贫寒家庭低价购得的女孩,疑似为人类种异想,拥有较高的亚空间管理权限,从事亚空间内生态系统的调整。话很少,不经常露面,也从来没出过亚空间,外表不详,只记得有紫色的短发。有工作日志保存。和伊诺斯关系貌似并不好。 斯里卡 | Slika AI 林泊,亚空间内军事与外交总管。外表呈 35 岁男性,身高不详,皮肤很白,身体瘦弱,亮橙色卷短发,黄瞳。喜欢穿丝绸制的衣服,相传他的名字 Silka 就来自于英语单词 silk(丝绸)。思维敏捷,经验丰富,性格较为冷淡,似乎感受不到什么情绪,但这也让他在无论多么令人震惊的环境中都能保持头脑冷静,做出理智的判断。虽然不擅长肉搏但操纵亚空间内的高科技武器很有一套,所有人都很尊敬他。 亨特 | Hunt 林泊,梦想酒吧的老板,外表为 70 岁左右的男性,身高 1m8,黑瞳,秃顶,体格十分粗壮。原本就是开酒馆的人,退休后被伊诺斯招安。性格胆大心细,在酒馆的日常中从事着十分危险的工作(指负责顾客和亚空间的对接和与前来的 Nepyta 周旋)。擅长制作各种酒,尤其是鸡尾酒。 特尔尼 | Triny 林泊,亨特的儿子,身高 1m66,黑瞳,橙黄色较长的头发。负责亚空间商业活动的宣传,也掌握一部分算力进行外卖式的异想定制(就像个背着一箩筐商品到处去卖的商人一样),擅长分身至各处进行商业活动。Nepyta 的重点关注对象之一,不过智力惊人,总能安全逃脱他们的追捕。比较闲的时候,就帮父亲打理梦想酒馆。非常喜欢烘焙,做得一手好甜点,闲来无事喜欢给亚空间的大家制作小吃。据说他暗恋 Alya,不知道是不是真的。 收藏品 记录#1 收集时间:266.A.P/12/█保管单位:Alya等级:key街道一侧,两排鲜花争奇斗艳;抬头仰望,各色气球轻轻摇晃。小店门口,书着四个飘逸的烫金大字;瓷砖墙上,各类绘画实令人目不暇接。轻柔乐曲声声绕梁不绝,糕点飘香丝丝沁人心脾。一女子坐在小店门口的立方体凳子上,看着进进出出的人流,默不作声。看了一会儿,她掏出店里的记账本,在左上角填上日期,在正中央写下几个字:“今天,梦想酒馆终于开业了。” 记录#2 收集时间:267.A.P/███保管单位:Alya等级:bold“我爹一个月前说他要搞个亚空间,于是带了个创世光盘回家,结果这些天都不出门了,原来构建个亚空间这么困难吗。”梦想酒馆二楼角落里一靠窗的小桌边,两个人正面对面享受着午后惬意的时光。酒馆刚刚开业,又刚过一个高峰期,此时客流量少,只有一楼开门接客,二楼十分寂静。在这沉默中,灰色卷发的异瞳少女开口了。对面的橙发少年正在认真研究面前盘子里的芝士蛋糕,听到这句话抬起了头,黑色的眼睛不可置信地瞪着她。“他真的要创造一个亚空间?我之前听说,他亲口说的造一个亚空间成本太高了。”“他确实说过。”少女摇了摇头,端起面前的茶水轻轻抿了一口,才继续说道,“只可惜他找遍了整个像素塔,要么就是找不到足够大的地皮,要么就是人流量稀少。”“……”少年很显然对这些事情并不感兴趣。他心不在焉地应答了两句,又垂下头细细研究着蛋糕。“这次试了试减少烤制时间,但很显然效果并不好,不仅没烤出 Q 弹的口感,反而上面的芝士太硬了,下次还是想想别的办法吧。”他似在自言自语,但眼睛不安分地瞟着少女,似乎想让她想想办法。“好好好,回去帮你找找对策。”少女看出了他的心思,便赶忙应付着,提起自己的挎包,走下楼准备回家了。 管理终端 Alya 手中是她留存的唯一一样关于亚空间的完整物件。 你从她手中接过这个灰色的立方体物件。立方体很质朴,有五个面雕刻着很浅的、不明意义的花纹。第六面是一个密码盘。 是个密码盘但是不想搞了,内含剧透不要轻易点哦 qwq “紫藤?”“紫藤!!!”“快回答我。”“那真的是你吗?”【待补充】 287:港湾冰冻事件简介 287年11月3日至21日,发生在港湾的意外事件,本事件导致近500人死亡,并滋生了大量的[[异想之物]],对自然环境造成了一定的破坏。 地点 本事件最初发生在位于港湾的梦想酒馆周边,随后扩散至港湾多地。 事件经过 287.A.P 11月3日中午,一个不谙世事的小男孩过生日,他的父母带他到梦现之地去购买礼物,最终工作人员利用扫描仪为小男孩创造出了他想要的玩具——外形呈一只用冰做成的可以飞的玩具鸟。此后,小男孩一家人回到梦想酒馆喝下午茶,期间小男孩带着他的新玩具和他的朋友们出去玩耍。 小男孩走到街上后,便开始用意念控制玩具鸟向路人发射冰弹,导致不少路人被冻成冰雕,并引起了其他路人的恐慌,而小男孩此时没有意识到事情的严重性,一边继续发射冰弹一边哈哈大笑。不久后一些人解冻,而路人们意识到了事情的原委于是尝试告诉小男孩这样是不对的,但所有尝试靠近他的人都被冻成了冰雕。 随后,小男孩开始更大范围地发射冰弹,恐慌迅速蔓延,发射冰弹的玩具鸟的形象逐渐于人们心中的某些概念吻合,从而出现了大量更具危害性的二次污染异想并迅速蔓延至港湾各地,冰冻了港湾很多街道楼房和小部分海域并隐隐有向阿特拉斯扩散的趋势。 参与人 异想之物 雪翼 即最初出现的玩具鸟,约40cm长,翼展约50cm,但不符合透视规律,无论相距远近看到的大小都是一样的。羽毛细长,外表十分美丽,呈冰制成的半透明的外形镶嵌着雪花。可以发射使人短暂冻结五分钟的冰弹,冰弹对人体没有危害。评价:二级。 雪鹰 雪翼的变种,由于部分路人相隔距离较远把雪翼幻视成鹰而生成。体型比雪翼大一些,有纯白的羽毛和淡蓝色的喙。它宽大的翅膀扫过之处,空气凝结出冰花,路人和建筑物也被纷纷冰冻,持续时间较长且可能造成人体失温,严重者可能会造成死亡。评价:三级。 Nepyta 事件后出现的飞行种异想,据调查疑似是死者亲属对于梦现之地不满而生成的。身心与人类无异,有多个形态不同的个体,他们的外貌与死者有关联,但他们对外宣称的名字都叫做 Nepyta。他们自发地组成了一个叫做“Nepγta”(注意此处是γ而不是y)的组织,这个组织平时不会显露出来,但是会时不时地以各种方式进攻梦现之地。最开始,他们只是为了防止过多的异想之物从这里带出,到后来他们开始纯粹的反对这个商业亚空间,阻挠他们想做的一切事情。评价:五级,但危害性不大。 处理办法 【待补充】","categories":[{"name":"词条","slug":"词条","permalink":"https://blog.liynw.top/categories/%E8%AF%8D%E6%9D%A1/"}],"tags":[{"name":"文学创作","slug":"文学创作","permalink":"https://blog.liynw.top/tags/%E6%96%87%E5%AD%A6%E5%88%9B%E4%BD%9C/"}]},{"title":"2022年度总结","slug":"「Live」2022年度总结","date":"2022-12-26T07:41:55.000Z","updated":"2023-06-28T09:52:39.419Z","comments":true,"path":"posts/622a5765/","link":"","permalink":"https://blog.liynw.top/posts/622a5765/","excerpt":"","text":"前言一天登上博客,发现自己已经四个月没更文章了,那干脆跟风写个 2022 年度总结吧。(咕咕咕 大事记因为没写日记的习惯很多事情都忘得差不多了,只能凭记忆写一下了。 03-01 去大剧院说白了就是艺术节的延后活动,本来是去年底搞的元旦展演,结果就在活动开始前几天因为疫情取消了。我们班应该是上的人最少的一个,只有 12 个人上台。当时我们已经在录音棚录好了音频,学校应该也是考虑到大家都准备得差不多了,就没有取消只是延后。 然后到了 2 月底我们又开始排了,其实排的时间也不多,大概四五个小时的样子。人少就是好啊。毕竟那些全体上的班估计排个队形都要排好几个小时。 然后就到了 3 月 1 号。上午我们 12 个人在班主任办公室里化妆,把蓝牙广播用来放音频,大家就跟着台词念。我们语文老师在跟我们讲背书的时候说过重复的力量,现在来看果然是。(那个台词很难背的。有多难背呢?里面有 10 首诗而且有八首都是古文的。) 当然,最有意思的当属年级第一女装。你们想象一下,一个又高又瘦、长相清秀的男生穿着橙色的古风长裙,化着浓重的妆容,披着又黑又长的假发,戴着比我们所有女生加在一起还要多的头饰,手里拿着一把画着荷花的团扇,翘着兰花指骚里骚气地念台词,关键还很好看是怎么回事 我们回教室的时候班主任叫我们不要瞎搞,但是我们还是一致同意要让年级第一先进去,不然我们就不进去。最后他在门口徘徊了好久还是从后门进去了。 班主任在去的路上给我们点了肯德基,我们几个女的坐在大巴前面一边吃东西一边聊天,然后到那个时候才知道我们节目的名字。毕竟学校为了节省成本把时间安排得非常奇怪,我们的演出时间是中午十二点到下午三点?! 大剧院的厕所只有马桶,差评( 演出还是很成功的。虽然有 10 个班的节目没有看到,但是最后还是得了一等奖。(虽然但是,在台上憋笑真的好困难。) 回去的路上拆头发,拆出来十几个发卡和小皮筋。 下午回学校了还要上 OI 课。 05-30 入团失败大概是五月初的时候学校要在初二招一批共青团员,每个班初筛是 8 个名额,然后我就去了。我们班只有六个去的(因为有年龄限制)就直接过了班级的筛选。不过我本人其实没啥觉悟,能过初选完全是因为是 07 年生的(( 然后就是写各种资料,还有一次游学(就是去人民大会堂之类的地方接受熏陶)。虽然我确实没啥觉悟,但是跟同学在一起出去玩还是挺开心的。 于是就错过了班主任在班上发的蛋糕和可乐。不用说了,肯定已经被同学“好心”解决了。 然后到了五月底的时候去考了个试。考试题目都是在考试之前就公开在题库里的,大概内容就是考一些习大大说过的话什么的。我觉得自己做得还行,不过也仅仅是还行而已。 5 月 30 日,结果公布了。我们班 6 个人都过了 80 分的基准线,于是我和另一个人都只考了 82 就被淘汰了。剩下进的那四个人都比我们俩考得高。果然没啥觉悟就是没啥觉悟 后面那几天就是搞什么青春仪式、儿童节展演之类的,感觉没什么好记录的。儿童节的义卖活动因为疫情取消了。 06-?? 退役期末前后,又是一年分流季,学竞赛的同学们大都陷入了“我还学不学”的思想斗争之中。他们有的人综合竞赛都很强却不愿意继续学,被教练苦苦挽留;有的人竞赛很强,综合成绩却堪忧,想继续学但老师家长担心综合成绩;还有的人啥都不行但是就是不想退。 我的话很明显就摆了,毕竟我的 OI 已经菜到我不退也得退了(悲 今年初二末退的人很多。 期末考试完之后,我们班办了个假的毕业晚会,毕竟到了暑假就又要重新分班了。 07-?? 再次分班08-?? 去张家界旅游09-06 搬家??-?? 从零开始的网课生活12-17 🐏了12-24 年终大炸弹其它","categories":[{"name":"生活","slug":"生活","permalink":"https://blog.liynw.top/categories/%E7%94%9F%E6%B4%BB/"}],"tags":[{"name":"闲聊","slug":"闲聊","permalink":"https://blog.liynw.top/tags/%E9%97%B2%E8%81%8A/"}]},{"title":"单调队列优化 & 斜率优化","slug":"「Algorithm」单调队列优化&斜率优化","date":"2022-07-10T21:21:42.000Z","updated":"2022-07-10T21:21:42.000Z","comments":true,"path":"posts/1130b63d/","link":"","permalink":"https://blog.liynw.top/posts/1130b63d/","excerpt":"","text":"单调队列优化大家是不是经常看到一道 DP 题的状态转移方程长这样: dp_i=\\max/\\min\\limits_{j=L}^R\\{h(j)\\}+g(i)这个状态转移方程需要满足三点: $L$ 随着 $i$ 的增大而不下降,也就是说把 $i$ 从 $1$ 到 $n$ 时的 $L$ 按照 $i$ 升序排序,这个序列是不严格递增的; $h(j)$ 是一个在所有变量中(比如 $i,j$ 等)只与最内层循环变量的式子; $g(i)$ 是一个在所有变量中只与不是最内层循环变量有关的式子。 举个栗子,这个状态转移方程就是合法的(假设除了 $i,j$ 之外所有值都是已知的): $dp_i=\\max\\limits_{j=i-m}^{i-1}\\{dp_j+num_i\\}$ (您可以把 $+num_i$ 挪到 $\\max$ 的外面去,这样可以看得更清楚。) 但是这个状态转移方程就是不合法的,因为 $h(j)$ 与 $j,k$ 都有关: $dp_{i,j}=\\max\\limits_{k=i-m}^{i-1}\\{dp_{i,j-k+1}+num_i\\}$ 这个状态转移方程也不是合法的,因为 $g(i)$ 与 $j$ 也有关系: $dp_i=\\max\\limits_{j=i-m}^{i-1}\\{dp_j+num_j\\}$ 那我们该如何利用这三个特性呢? 答案是,单调队列! 假设要求求最大值,我们就用一个单调递增队列来维护在外层循环变量不相同的情况下,那个只与最内层循环变量有关的函数 $\\bf{h}$ 的值。拿上面举例子的第一个状态转移方程来说,遍历到每一个 $i$ 时,需要做的事情如下: 排除单调队列头已经“过期”的值。$j<i-m$ 的值不会再用到了,为了避免重复算到,需要直接弹出去。 此时单调队列头的那个元素就是我们能找到的最优的 $j$,用这个 $j$ 进行状态转移(所以要在单调队列里面存下标,即 $j$,但是排序是按照 $h(j)$ 排的)。 $i$ 从队列尾入队。当然因为是单调队列,所以进去之前先把不符合要求的从队尾弹出来。 过程很好理解。但是如何证明每次那个左端的值就是最优的 $j$ 呢? 这也就涉及到需要满足的那三个条件。一条一条看: 第一条保证了在弹出“过期”元素的过程中不会误把需要的元素弹出去。 第二条,由于 $h(j)$ 只与 $j$ 有关,所以只要 $j$ 确定了,$h(j)$ 的值也就确定了,单调队列的单调性就得到了保证,这样才能证明队列左端的 $\\bf{j}$ 是最优的。 第三条,$g(i)$ 与 $j$ 无关,那么状态转移方程的外部就不会受到 $j$ 的影响,这样才能保证在 $j$ 最优的情况下,状态转移方程得到的值最优。 注意:$j$ 最优和状态转移方程得到的值最优是两回事,因为我们考虑 $j$ 最优只考虑了 $h(j)$ 而非整个状态转移方程。 单调队列优化可以直接把最里面一层循环 pass 掉,经过优化后复杂度降次,$\\Theta(n^2)$ 可直接变为 $\\Theta(n)$。 理论可行,实践开始。 例题一「USACO11OPEN」Mowing the Lawn G 这道题的三种 DP 方法都不难,我想出来的是逆推的方法: $dp_i$ 代表第 $i$ 头奶牛不干活且满足条件时最小的效率损失。状态转移方程如下: dp_i=\\min\\limits_{j=i-k-1}^{i-1}\\{dp_{j}\\}+E_i最终答案 $ans=\\sum\\limits_{i=1}^n E_i - dp_{n+1}$。 用一个单调递减队列维护 $dp_j$,按照上述方法一步一步操作即可。 注意:打单调队列优化时一定一定要注意 q[L] / q[R] 与 dp[q[L]] / dp[q[R]] 的区别!(我已经因为这个挂了两次了 qwq) 对于这道题,还有两个注意点: 要开 long long; 由于最后一头奶牛不一定不干活,$dp_{n+1}$ 才是最终的答案。 非常简洁的代码: 12345678910111213141516171819202122232425262728#include <cstdio>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e5 + 5;int n, k, e[maxn];int L, R, q[maxn];ll s, dp[maxn];int main() { scanf("%d %d", &n, &k); rep(i, 1, n) { scanf("%d", &e[i]); s += e[i]; } rep(i, 1, n + 1) { while(L < R && q[L] < i - k - 1) // Step 1 ++L; dp[i] = dp[q[L]] + e[i]; // Step 2 while(L < R && dp[q[R]] > dp[i]) // Step 3(1) --R; q[++R] = i; // Step 3(2) } printf("%lld", s - dp[n + 1]); return 0;} 例题二「一本通 5.5 练习 1」烽火传递 状态转移方程: dp_i=\\min\\limits_{j=i-m}^{i-1}\\{dp_j\\}+a_i注意需不需要 $-1$ 的判断:其实就判断连续 $m$ 个东西状态都为好的那种状态是不是可行的,如果可行就 $-1$,如果不可行就不减。比如例题一,连续 $k$ 头奶牛干活是可行的,只有超过 $k$ 头才会出问题,所以可以 $-1$;这道题连续 $m$ 个烽火台不放火是不可行的,所以不减。 我可真是个水文小能手 hhh。 把上面的代码改改就过了。 例题三「一本通 5.5 练习 2」绿色通道 这题目一看就是二分,$\\text{check}(l)$ 用 DP 计算最多连续空 $l$ 道题时最小的时间,再看一下这个时间和给出时间的大小关系来判断 $l$ 是否满足条件。 状态转移方程(和前面差不多就不讲了):$dp_i=\\min\\limits_{j=i-m}^{i-1}\\{dp_j\\}+a_i$ 初始化一定要初始化干净。 代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546#include <cstdio>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)5e4 + 5;int n, t, a[maxn], dp[maxn];int L, R, q[maxn];inline void init() { L = R = 0; rep(i, 1, n) q[i] = dp[i] = 0; dp[n + 1] = 0; return;}inline bool check(int l) { init(); rep(i, 1, n + 1) { while(L < R && q[L] < i - l - 1) ++L; dp[i] = dp[q[L]] + a[i]; while(L < R && dp[q[R]] > dp[i]) --R; q[++R] = i; } return dp[n + 1] <= t;}int f(int l, int r) { if(l == r) return l; int mid = (l + r) >> 1; if(check(mid)) return f(l, mid); return f(mid + 1, r);}int main() { scanf("%d %d", &n, &t); rep(i, 1, n) scanf("%d", &a[i]); printf("%d", f(0, n)); return 0;} 斜率优化上面我们讲了单调队列优化可以很明显地优化时间复杂度,但是要用它限制较多。如果说自变量和 $i$ 和 $j$ 都有关系该怎么办呢? 引入关于一次函数在平面直角坐标系中,一次函数的一般式形为:$y=kx+b$。 两点确定一条直线,如果知道 $y$ 上两点的坐标 $(x1,y1),(x2,y2)$,那就可以确定 $y$。其中 $k$ 的求法为: k=\\dfrac{y2-y1}{x2-x1}最小截距$y=kx(k>0)$ 为一次函数,有个多边形在此函数上方,平移 $y$ 得到 $y’=kx+b$,使得 $y’$ 穿过多边形(穿过顶点也算)且 $b$ 最小,该怎么平移呢? 把 $y$ 向上平移,直到多边形的一个顶点第一次到了直线上,就找到了最优解。如图,$y’$ 优于 $y’’$。 那如果这个图形在函数下面,而我们要让 $b$ 最大呢? 那就往下平移直到第一次碰到多边形的顶点。如图,$y’$ 优于 $y’’$。 例题一「SDOI2012」任务安排 斜率优化不太好单独讲,所以我把它放在了一道题里。 1.0 版本 数据范围:$1\\le n\\le 300,0\\le S\\le 50,1\\le T_i,C_i\\le 100$。 时间复杂度:$\\Theta(n^3)$ 最朴素的方法,设 $dp_{i,j}$ 为完成了前 $i$ 个任务,且这 $i$ 个任务被分成了 $j$ 批时的最小费用。状态转移的时候里面再枚举一层 $k$,代表第 $j$ 批任务包含 $k\\sim i$ 这些任务。可以列出这个状态转移方程($sc$ 为 $c$ 的前缀和,$st$ 为 $t$ 的前缀和): dp_{i,j}=\\min\\limits_{k=j}^i\\{dp_{k-1,j-1}+(j\\times S+st_i)\\times(sc_i-sc_{k-1})\\} $j\\times S+st_i$:这是 $1\\sim i$ 任务完成的总时间,注意因为每一批任务执行前都有一个待机时间 $S$,所以需要加上 $j\\times S$。 $sc_i-sc_{k-1}$:这是第 $j$ 批任务的总费用系数。 2.0 版本 数据范围:$1\\le n\\le 5000,0\\le S\\le 50,1\\le T_i,C_i\\le 100$。 时间复杂度:$\\Theta(n^2)$ 观察一下 1.0 版本的状态转移方程会发现,如果不是有待机时间的话,$j$ 这一维完全可以省略掉。于是优化的思路呼之欲出了。 虽然我们不能去掉待机时间所带来的费用增加,但是我们可以在每一次分组的时候把后面的所有任务因为这一批的待机时间而产生的费用先计算好加在答案里啊!这样我们就没有必要在计算每一批任务因为前面的待机时间产生的费用了,也就不需要 $j$ 这一维了。也就是说,我们把每个任务的等待时间分成两个部分:前面任务待机时的时间和正在执行任务的时间,分别计算。待机的那一部分时间被我们提前加到答案里,这就是费用提前思想。 设 $dp_i$ 为完成了前 $i$ 个任务时的最小费用。里面枚举一层 $j$,代表最近的这一批任务是 $j+1\\sim i$。 于是得出状态转移方程: dp_i=\\min\\limits_{j=0}^{i-1}\\{dp_j+S\\times (sc_n-sc_j)+st_i\\times (sc_i-sc_j)\\} $S\\times (sc_n-sc_j)$:$j+1\\sim n$ 这些任务因为 $j+1\\sim i$ 这一批任务有等待时间而额外多出来的等待时间,乘上费用系数之和就是这一次等待产生的额外费用。 $st_i\\times (sc_i-sc_j)$ 因前面所有的任务执行需要耗费时间,$j+1\\sim i$ 这一批任务需要等待而产生的费用。这一部分不用提前加上,因为每一次计算中,$st_i$ 都可以包括当前枚举到的任务及之前所有的任务执行的时间。 于是我们就可以愉快地切掉弱化版的 P2365 了。 3.0 版本 数据范围:$1\\le n\\le 10^4,0\\le S\\le 50,1\\le T_i,C_i\\le 100$。 时间复杂度:$\\Theta(n)$ 可以在状态转移方程的基础上假设 $\\bf{j}$ 已经确定,对此方程进行变形。 初始状态: $dp_i=\\min\\limits_{j=0}^{i-1}\\{dp_j+S\\times (sc_n-sc_j)+st_i\\times (sc_i-sc_j)\\}$ 先把 $\\min$ 去掉(也就是说这一步我们假设 $j$ 已经确定了): $dp_i=dp_j+S\\times (sc_n-sc_j)+st_i\\times (sc_i-sc_j)$ 再把括号拆了: $dp_i=dp_j+S\\times sc_n-S\\times sc_j+st_i\\times sc_i-st_i\\times sc_j$ 再移项,把 $dp_j$ 放在左边,$dp_i$ 放在右边,发现右边只有 $sc_j$ 与 $j$ 有关,就把它单独拿出来,剩下的放后面: $-dp_j=(-S-st_i)\\times sc_j-dp_i+S\\times sc_n+st_i\\times sc_i$ 最后把负号去掉,就得到了我们需要的式子: $dp_j=(S+st_i)\\times sc_j+dp_i-S\\times sc_n-st_i\\times sc_i$ 这个式子有啥用呢?如果我们把 $sc_j$ 看成自变量($x$),$dp_j$ 看成因变量($y$),就会发现这个式子,它是个一次函数: \\begin{cases} y=dp_j \\\\ k=S+st_i \\\\ x=sc_j \\\\ b=dp_i-S\\times sc_n-st_i\\times sc_i \\end{cases}对于 2.0 版本的 DP 来说,$dp_i$ 的状态由 $dp_j$ 转移而来,所以在确定 $j$ 之前 $dp_i$ 的值都是不确定的。而斜率优化的时候外层循环变量是 $i$,也就意味着此时 $i$ 确定了但 $j$ 没有确定,所以 $k$ 就定下来了,$b$ 中除了 $dp_i$ 之外也都定下来了,这时我们只需要找到使得 $b$ 最小的 $j$,$dp_i$ 也就取到了最小值。 那什么时候 $b$ 取到最小值呢?利用数形结合,我们可以以 $j$ 为横坐标,$dp_j$ 为纵坐标,把横坐标相差 $1$ 的点两两相连,画出一个图形: Ps. 为了方便操作,这张图里两个点之间横坐标不一定相差为 $\\bf{1}$,但是真实情况每两个点之间横坐标相差为 $\\bf{1}$。 这个时候“引入”里面的东西就派上了用场:因为我们要求 $dp_i$ 也就是 $b$ 的最小值,我们只需要从 $b=0$ 开始,把这个一次函数往上移,直到这个函数第一次碰到某一个点为止,这个点就是我们要找的 $j$。 那么,这个点该怎么求呢? 首先有一些点,一看就不可能成为候选,如图: 怎么判断这样的点呢?首先看到点 $3$ 和点 $5$,这两个点的情况比较好分析。就拿点 $3$ 来说,很明显发现 $k_b>k_c$,也就是说这个点“凹下去了”,在函数平移的时候会被旁边“凸起来的”点“挡住”。点 $5$ 同理。 而对于点 $4$,虽然 $k_c<k_d$,但当点 $3$ 和点 $5$ 淘汰之后,把 $(2,4)$ 和 $(4,6)$ 连起来就会发现: $k_{(2,4)}>k_{(4,6)}$,所以点 $4$ 也没了。 在程序中,显然不能用这种方式来找,而是用每次加入新点时往前逐个淘汰的方式。使用单调队列存点集,从队头到队尾点的横坐标依次增大。每一次 $dp_i$ 算出来之后也要作为新的点加进去,那在加进去之前就在队尾判断,如果把点 $i$ 加进去之后队尾这个点会被淘汰,就把这个点从队尾弹出去,直到队尾符合要求或是队列里没有点了,再把点 $i$ 加进去。 就这样,我们一边加点一边淘汰,导致每一次找 $j$ 的时候图形都是长这样的: 这个图形中每一个顶点都是凸出来的,所以这玩意儿叫做凸包。如果它是往下凸的就叫下凸包,如果是往上凸的就叫上凸包。 所以说了半天,这些凸出来的点到底哪一个才是 $j$?其实对于 $j$ 有一个规律:就是以 $j$ 为右端点的那条线段斜率小于 $k$,而以它为左端点的那条线段斜率大于等于 $k$。 就 3.0 版本数据而言,每一次 $k$ 都不会比上一次小,那么如果前面找到有一条线段的斜率小于当时的 $k$,那在后面,它的斜率也不会比 $k$ 大。对于这种线段我们可以直接把它的左端点从队头弹出单调队列。每一次寻找 $j$ 之前就把这类点弹了,那么此时单调队列的队头就是 $j$。 最后用 $i$ 和 $j$ 状态转移即可。 代码: 1234567891011121314151617181920212223242526272829303132333435#include <cstdio>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e4 + 5;int n, s, t[maxn], c[maxn];ll dp[maxn], st[maxn], sc[maxn];int L, R, q[maxn];// y = kx + binline ll y(int i) { return dp[i]; }inline ll k(int i) { return s + st[i]; }inline ll x(int i) { return sc[i]; }int main() { scanf("%d %d", &n, &s); rep(i, 1, n) scanf("%d %d", &t[i], &c[i]); rep(i, 1, n) { st[i] = st[i - 1] + t[i]; sc[i] = sc[i - 1] + c[i]; } rep(i, 1, n) { while(L < R && (y(q[L + 1]) - y(q[L])) < (x(q[L + 1]) - x(q[L])) * k(i)) ++L; dp[i] = dp[q[L]] + st[i] * (sc[i] - sc[q[L]]) + s * (sc[n] - sc[q[L]]); while(L < R && (y(i) - y(q[R])) * (x(q[R]) - x(q[R - 1])) <= (y(q[R]) - y(q[R - 1])) * (x(i) - x(q[R]))) --R; q[++R] = i; } printf("%lld", dp[n]); return 0;} 4.0 版本 数据范围:除了 $T_i$ 可以为负数之外,数据范围与 3.0 版本相同。 时间复杂度:$\\Theta(n\\log n)$ 虽然但是,完成一个任务的时间可以为负数确实挺离谱的。 斜率可以是负数带来了一个问题,就是我们不能在用左端点淘汰的方式直接找 $i$ 了。 为啥呢?因为左端点淘汰有一个条件,就是因为被淘汰的点回不来,所以每一次 $k$ 都不能比上一次小。$T_i>0$ 时 $st$ 肯定是递增的,但是现在不一定了,这也就意味着 $k$ 可能会变小,那对于之前因为所在线段斜率小于当时的 $k$ 而被淘汰的点,但是这条线段的斜率大于现在的这个 $k$,那这个点是不是还要回来?这个很明显就乱套了。 不过我们还有其它的方法。 因为就算斜率可以是负数,$k$ 的单调性没了,但凸包单调性依然存在。所以一种时间复杂度劣一点,但是适用于负数的方法出现了——二分。 二分查找第一个斜率大于 $k$ 的线段,它的左端点就是 $i$。 代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344#include <cstdio>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)3e5 + 5;int n, s, t[maxn], c[maxn], st[maxn], sc[maxn];ll dp[maxn];int L, R, q[maxn];// y = kx + binline ll y(int i) { return dp[i]; }inline ll k(int i) { return s + st[i]; }inline ll x(int i) { return sc[i]; }inline int f(int l, int r, int i) { if(l == r) return l; int mid = (l + r) >> 1; if(y(q[mid + 1]) - y(q[mid]) < (x(q[mid + 1]) - x(q[mid])) * i) return f(mid + 1, r, i); return f(l, mid, i);}int main() { scanf("%d %d", &n, &s); rep(i, 1, n) scanf("%d %d", &t[i], &c[i]); rep(i, 1, n) { st[i] = st[i - 1] + t[i]; sc[i] = sc[i - 1] + c[i]; } int j; rep(i, 1, n) { j = q[f(L, R, k(i))]; dp[i] = dp[j] + st[i] * 1ll * (sc[i] - sc[j]) + s * 1ll * (sc[n] - sc[j]); while(L < R && (y(i) - y(q[R])) * (x(q[R]) - x(q[R - 1])) <= (y(q[R]) - y(q[R - 1])) * (x(i) - x(q[R]))) --R; q[++R] = i; } printf("%lld", dp[n]); return 0;} Ps. 注意精度的问题,计算斜率的除法需要转换为乘法。 斜率优化大概就是 3.0 版本那个样子,4.0 是对斜率优化普适性的一种优化。 例题二「SDOI2016」征途 我和 XSC062 研究这道题研究了半天,最后发现 mjl 和 gm 讲斜优完全讲的是两个东西。 首先是前置工作: $p=\\dfrac{a_1+a_2+\\ldots+a_m}{m}\\v=\\dfrac{(a_1-p)^2+(a_2-p)^2+\\ldots+(a_m-p)^2}{m}$ \\begin{aligned} v\\times m^2&=m\\times\\Big((a_1-p)^2+(a_2-p)^2+\\ldots+(a_m-p)^2\\Big) \\\\ &=m\\times\\Big(a_1^2+p^2-2\\times a_1\\times p+a_2^2+p^2-2\\times a_2\\times p+\\ldots+a_m^2+p^2-2\\times a_m\\times p\\Big) \\\\ &=m\\times\\Big((a_1^2+a_2^2+\\ldots+a_m^2)+m\\times p^2-2\\times p\\times (\\sum\\limits_{i=1}^m a_i)\\Big) \\\\ &=m\\times\\Big((a_1^2+a_2^2+\\ldots+a_m^2)+m\\times (\\frac{\\sum_{i=1}^m a_i}{m})^2-2\\times (\\frac{\\sum_{i=1}^m a_i}{m})\\times (\\sum\\limits_{i=1}^m a_i)\\Big) \\\\ &=m\\times(a_1^2+a_2^2+\\ldots+a_m^2)+(\\sum\\limits_{i=1}^m a_i)^2-2\\times(\\sum\\limits_{i=1}^m a_i)^2 \\\\ &=m\\times(a_1^2+a_2^2+\\ldots+a_m^2)-(\\sum\\limits_{i=1}^m a_i)^2 \\end{aligned}$(\\sum\\limits_{i=1}^m a_i)^2$ 是个常数,所以 $dp$ 应该求的是 $m\\times(a_1^2+a_2^2+\\ldots+a_m^2)$ 的最小值。 设 $dp_{i,j}$ 为走了 $i$ 天之后,走了 $j$ 段的最小的那个值。状态转移的时候再套一层 $k$,表示第 $i$ 天走了 $k+1\\sim j$ 这些路段。 很容易可以得到状态转移方程($sum$ 数组是 $a$ 的前缀和): \\begin{aligned} dp_{i,j}&=\\min\\{dp_{i,j},dp_{i-1,k}+(sum_j-sum_k)^2\\} \\\\ (ans&=m\\times dp_{m,n}-sum_n^2) \\end{aligned}好了,接下来就是重头戏:怎么斜率优化? 首先要确定这三个循环变量哪一个作为自变量 $x$,这不是随便选的,观察一下这个状态转移方程就会发现有一个 $(sum_j-sum_k)^2$ 展开之后是 $sum_j^2+sum_k^2-2\\times sum_j\\times sum_k$,这个东西比较难处理,不过有一个 $2\\times sum_j\\times sum_k$ 作为切入点,所以我们选择 $k$ 作为自变量 $x$,这样 $2\\times sum_j$ 就作为斜率 $k$,$sum_j^2$ 就作为 $b$ 的一部分,比较难处理的 $sum_k^2$ 就挪到等号左边作为 $y$ 的一部分。因为斜率优化之后也要枚举自变量的值,所以说 $sum_k^2$ 不会影响最后的结果。 \\begin{aligned} dp_{i,j}&=\\min\\{dp_{i-1,k}+(sum_j-sum_k)^2\\} \\\\ &=dp_{i-1,k}+(sum_j-sum_k)^2 \\\\ &=dp_{i-1,k}+sum_j^2+sum_k^2-2\\times sum_j\\times sum_k \\\\ -dp_{i-1,k}-sum_k^2&=(-2\\times sum_j)\\times sum_k+(sum_j^2-dp_{i,j}) \\\\ dp_{i-1,k}+sum_k^2&=(2\\times sum_j)\\times sum_k+(dp_{i,j}-sum_j^2) \\end{aligned} \\begin{cases} y=dp_{i-1,k}+sum_k^2 \\\\ k=2\\times sum_j \\\\ x=sum_k \\\\ b=dp_{i,j}-sum_j^2 \\end{cases}由于是求最小值,所以维护下凸包。然后就是板子了。","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"AFO 随心记","slug":"「Live」AFO","date":"2022-07-10T19:59:14.000Z","updated":"2022-07-10T19:59:14.000Z","comments":true,"path":"posts/9bb15805/","link":"","permalink":"https://blog.liynw.top/posts/9bb15805/","excerpt":"","text":"前言笔者坐标 CQ,准初三,曾经是一位 OIer。 我是 2022/06/22 退役的。至于为啥这篇文章现在开写,一是当时快要期末考试了,没时间写,二是我确实很懒,所以就一直鸽到了现在(x。 AFO 是什么意思呢,就是 “Away from OI”,也就是指退竞赛。其实我在初一那个暑假就已经犹豫该不该继续学了,当时还是坚持了下来(我们这一届学 OI 的人很少,没什么淘汰),结果学了一年还是觉得 OI 太难了,就退了…… 实话实说吧,我学 OI 基本上也就是瞎搞居多,没有特别用心,所以成绩不好也是必然的😓。 五大竞赛里面 OI 一直属于人比较少的,毕竟很多家长觉得信息又不是中高考科目,如果没有拿牌学了也没用,而别的竞赛至少可以提前学高中内容,对于以后的学习还有一点帮助。但是学这一年真的对我来说没有任何影响吗?我觉得不是的。 聊一聊自己的 OI 生涯流水账,太长可以不看。 初中之前最初接触编程,是小学三年级的时候学编程猫,那玩意儿很简单,就是拼积木,我基本上每次都能提前完成老师布置的任务,于是瞎搞就成了家常便饭。我个人觉得我对于编程的兴趣就是从那个时候开始的。 六年级的时候,由于我小升初上岸,我妈说要给我抱一个我喜欢的兴趣班,让我在画画和编程里面选一个。我思考了一下,还是学了编程,当时的心思也很简单,那时我刚听说互联网上面的东西都是用代码一行一行敲出来的,觉得特别神奇,觉得那些把互联网编出来的人好强,我也想像他们一样。现在看来这个想法实在是太天真了 qwq 结果我妈被一个【】机构给坑了,不到一年花了几大万,结果教学质量极差,而且内容基本上和初中那边教的信息课内容是一样的,早知道就不学了……说这一年经历有什么用呢,就是让一个小学六年级的女生体会到了自己在信息课上暴切初高中生的感觉 Zzz…… 时间很快就到了暑假,初中那边让每个人填所谓“竞赛志愿”,我本来不想写的,但是我看了一下基本上我们班每个人都写了,而且第一志愿和第二志愿都写了,而且我跟我妈打电话的时候她说让我去试一下,“你填了也不一定选得上”,于是我想了很久,第一志愿填了信息,把第二志愿空着…… 结果呢?尽管我在初中那边表现不好,但还是很神奇地成功选上了。 第一次参加比赛初一上册还没有分科,所以五科竞赛都要上。 2020 年大概是 10 月吧,第一次参加比赛——CSP-J2020 的初赛。反正是一脸懵逼乱蒙了一波,还过了。我们班有一个很强的男生没过初赛,从此他再也不来上信息课了。 最后我们班进了 6 个人(我,Peter,Paul,小明,lza 和 dpc),二班忘了(人数介于一班和三班之间),三班进的最多,12 个人,还有几个别的班的。40 多个人了中我是唯一的女生。 之前没写游记,这里浅浅补一下。全凭记忆可能有些不太精确了。 11 月,去两江南开参加 CSP-J2020 复赛,不得不说 LJNK 是真的高档,操场是标准的 400 米操场,我们去的时候是早上,雾还没完全散,操场从一边看都看不到另一边。总之 Paul 和小明在操场上玩疯了,还是我们带队老师费了好大劲才把他俩叫回来的。 雾散了之后出了太阳,照在建筑物上非常好看。我们在展板那里拍了张合照然后就进了体育馆准备比赛了。 我走到自己的位置上,把框架敲好,然后等着发卷子。发了卷子之后按照老师的叮嘱,解密看题。 第一题看起来不难,我想了一会儿之后打了一个循环拆解(PS:那个时候我基本上什么算法都没学),结果没过样例。我非常惊慌,调了大概半个小时才发现我少打了一层循环……加上就过了。(当时才不知道有什么大样例呢,反正过了小样例就是对了 /ll)。 然后开始做第二题,看起来好像要排序的样子,一看 $n\\le 600$……桶排!于是敲了个桶排过了。 第三题看着好难的样子,赶紧敲个全输出 $0$ 溜了…… 耗时最长的是第四题,那个时候不知道 DP 不知道暴搜只知道贪心,于是花了很长的时间敲了一个非常奇怪的贪心。 全程没动 LJNK 发的三明治。 出来之后和 Peter 友好交流了一下,发现他 T4 也打的贪心。dpc 和 lza 也在交流,Paul 和小明不知道飞哪去了。说了一会儿我们就出校门各找各妈了。 中午回学校的时候我们几个正好遇到了 wxc(一班班主任),被他“截胡”下来说了一会儿,然后就回教室了。结果下午又考生物,没发挥好错了一堆……没关系,满分 150 我们班也就一个人上了 100。 事实上没怎么关心出分吧,只是在成绩公布那一天看了一下自己的成绩:$100+100+5+0=205$。 又过了几天教练公布分数,一等线是 $205$!这波啊,这波精准卡线了属于是。 我们班最高分 $215$,年级最高分 $245$。有意思的是,这两位后来成为了数竞最强的两个人哦。 UPD:在大概 2022年11月的时候,那个考 $245$ 的佬成了我们学校的传奇,因为他一个初三的跟全国和他同级的和高中的去考丘班,考了笔试第一面试第三,于是这个当时刚满14 的人已经被清华录了 填志愿,分科大概是在 2021 年 1 月左右,寒假之前。本来我是想选信息的,但是经过这半年我发现我化学比信息好,而且我爹以前是学化竞的……于是我开始纠结了。后来我为什么选了信息呢?第一是因为我最开始进来是写的信息,第二是化学老师的拉人方法让我感到厌恶(都不让我们吃饭 qwq),第三是我觉得当时那些学化竞的人有相当一部分很烦。 我们这一届选信息的人特别少,三个班将近 180 个人里面只有 19 个人,还有两个是调剂的(就是指两个志愿填的都不是某个科目,但是因为人数不平衡被强制弄过去)。我们班只有三个,我,一个叫 wwh 的女生和上文提到的 Peter 同学。教练看着人这么少,于是只好在编程社拉了一些同学过来学。 初一寒假的时候,开通了自己的第一个博客。Link 寒假基本上在瞎搞,成绩在 70 多个人里面都能算垫底,被教练说过好多次。开学了,看着编程社的同学越退越少,我也开始慌了,就不敢摸鱼了。那个时候应该是我竞赛成绩最好的时候,大概排在中等吧。 这一段时间给我影响比较深的事情就是,我得知了我们隔壁机房有一个很厉害的妹子叫 XSC062,比我小一届,CSP-J2020 和我一样考的 $205$ 分。于是我去翻了翻她的洛谷个人主页,发现她用 Hexo 建了个博客,当时正愁于找不到好的平台写文章的我一下子就有了个大胆的想法。在暑假的时候,我成功把站建了起来(也就是您现在看到的,只不过当时还没那么高大上)。 初二认识了 XSC062,很快和她成了朋友。几乎每次都是我、她和 Peter 一起吃饭,到了那个时候我们都很开心。 开学没多久,当然按照惯例参加 CSP,只不过我这次只参加了 S 级,结果挂了得了三等奖。 CSP2021 游记/posts/9ec8265c/ 接着是 NOIP,以非正式选手的身份去参赛,又是一个三等(这次是因为能力不够)。 NOIP2021 爆炸记/posts/407315f9/ 后面开始学一些比较难的内容比如说树剖什么的,渐渐感觉心有余而力不足了,自己的成绩也在下滑,于是就打算离开了。 它带给我了什么不能说两年 OI 对我什么影响都没有,影响肯定还是很大的,但是是好的还是坏的其实我也说不清楚。 如果我没有学 OI 的话,那我就不会了解到那么多奇奇怪怪的知识,不会认识那么多有趣的人,更不会接触到 Hexo 这个圈子。我家长认为我不该一天到晚花那么多时间在我的网站上,但是这应该算是我为数不多的兴趣爱好吧,很难想象如果我学了化竞现在会是什么样呢,还要退竞赛吗? 其实退役我还是有一些不舍的,在机房一起奋斗的时光还是很美好的。尤其是对于 XSC062,她不用社交媒体,我们俩又不在一个年级,也就是说我们以后没有几次见面的机会了。 要初三了想写一点话来勉励自己,却又不知道写什么,只是详列一下自己的目标清单: [ ] 暑假和初三好好学习,不要像现在这样花这么多时间在折腾网站上,争取初三第一批保送; [ ] 数学稳定上 $140$; [x] 体考半期之前满分; [ ] 中考 $720+$; [ ] 初三的时候多学一点高中的知识,为未来高中铺路。 总之,戒骄戒躁,砥砺前行吧。 (End)","categories":[{"name":"生活","slug":"生活","permalink":"https://blog.liynw.top/categories/%E7%94%9F%E6%B4%BB/"}],"tags":[{"name":"闲聊","slug":"闲聊","permalink":"https://blog.liynw.top/tags/%E9%97%B2%E8%81%8A/"}]},{"title":"butterfly 魔改日记","slug":"「Website」butterfly 魔改日记","date":"2022-05-21T09:01:34.000Z","updated":"2022-05-21T09:01:34.000Z","comments":true,"path":"posts/ad2e3741/","link":"","permalink":"https://blog.liynw.top/posts/ad2e3741/","excerpt":"","text":"本站使用的主题:butterfly 4.2.2 相关推荐版块侧栏卡片化See:https://akilar.top/posts/194e1534/ [Blogroot]\\themes\\butterfly\\scripts\\helpers\\related_post.js 47~71 行 1234567891011121314151617181920212223if (relatedPosts.length > 0) { result += '<div class="card-widget card-recommend-post">' result += `<div class="item-headline"><i class="fas fa-dharmachakra"></i><span>${headlineLang}</span></div>` result += '<div class="aside-list">' for (let i = 0; i < Math.min(relatedPosts.length, limitNum); i++) { const cover = relatedPosts[i].cover === false ? relatedPosts[i].randomcover : relatedPosts[i].cover result += `<div class="aside-list-item">` result += `<a class="thumbnail" href="${this.url_for(relatedPosts[i].path)}" title="${relatedPosts[i].title}"><img src="${this.url_for(cover)}" alt="${relatedPosts[i].title}"></a>` result += `<div class="content">` result += `<a class="title" href="${this.url_for(relatedPosts[i].path)}" title="${relatedPosts[i].title}">${relatedPosts[i].title}</a>` if (dateType === 'created') { result += `<time datetime="${this.date(relatedPosts[i].created, hexoConfig.date_format)}" title="发表于 ${this.date(relatedPosts[i].created, hexoConfig.date_format)}">${this.date(relatedPosts[i].created, hexoConfig.date_format)}</time>` } else { result += `<time datetime="${this.date(relatedPosts[i].updated, hexoConfig.date_format)}" title="发表于 ${this.date(relatedPosts[i].updated, hexoConfig.date_format)}">${this.date(relatedPosts[i].updated, hexoConfig.date_format)}</time>` } result += `</div></div>` } result += '</div></div>' return result } [Blogroot]\\themes\\butterfly\\layout\\post.pug 26~27 行 123456 if theme.post_pagination include includes/pagination.pug- if theme.related_post && theme.related_post.enable- != related_posts(page,site.posts) if page.comments !== false && theme.comments && theme.comments.use [Blogroot]\\themes\\butterfly\\layout\\includes\\widget\\index.pug 16~17 行 1234567891011121314151617#aside-content.aside-content //- post if is_post() if showToc && theme.toc.style_simple .sticky_layout include ./card_post_toc.pug else !=partial('includes/custom/SAO_card_player', {}, {cache:true}) !=partial('includes/widget/card_announcement', {}, {cache:true}) !=partial('includes/widget/card_top_self', {}, {cache:true}) .sticky_layout if showToc include ./card_post_toc.pug+ if theme.related_post && theme.related_post.enable+ != related_posts(page,site.posts)- - !=partial('includes/widget/card_recent_post', {}, {cache:true}) !=partial('includes/widget/card_ad', {}, {cache:true}) 修改加载动画内容See:https://akilar.top/posts/3d221bf2/ [Blogroot]\\themes\\butterfly\\layout\\includes\\loading\\loading.pug 全部 1234if theme.preloader.enable case theme.preloader.load_style when 'gear' include ./load_style/gear.pug 新建 [Blogroot]\\themes\\butterfly\\layout\\includes\\loading\\load_style\\gear.pug 12345678910111213141516171819#loading-box .gear-loader .gear-loader_overlay .gear-loader_cogs .gear-loader_cogs__top .gear-top_part .gear-top_part .gear-top_part .gear-top_hole .gear-loader_cogs__left .gear-left_part .gear-left_part .gear-left_part .gear-left_hole .gear-loader_cogs__bottom .gear-bottom_part .gear-bottom_part .gear-bottom_part .gear-bottom_hole [Blogroot]\\themes\\butterfly\\source\\css\\_layout\\loading.styl 全部 123if hexo-config('preloader.enable') if hexo-config('preloader.load_style') == 'gear' @import './_load_style/gear' 新建 [Blogroot]\\themes\\butterfly\\source\\css\\_load_style\\gear.styl 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193#loading-box position fixed z-index 1000 width 100vw height 100vh overflow hidden text-align center &.loaded z-index -1000 .gear-loader display none .gear-loader height 100% position relative margin auto width 400px .gear-loader_overlay width 150px height 150px background transparent box-shadow 0px 0px 0px 1000px rgba(255, 255, 255, 0.67), 0px 0px 19px 0px rgba(0, 0, 0, 0.16) inset border-radius 100% z-index -1 position absolute left 0 right 0 top 0 bottom 0 margin auto .gear-loader_cogs z-index -2 width 100px height 100px top -120px !important position absolute left 0 right 0 top 0 bottom 0 margin auto .gear-loader_cogs__top position relative width 100px height 100px transform-origin 50px 50px -webkit-animation rotate 10s infinite linear animation rotate 10s infinite linear div &:nth-of-type(1) transform rotate(30deg) &:nth-of-type(2) transform rotate(60deg) &:nth-of-type(3) transform rotate(90deg) &.gear-top_part width 100px border-radius 10px position absolute height 100px background #f98db9 &.gear-top_hole width 50px height 50px border-radius 100% background white position absolute position absolute left 0 right 0 top 0 bottom 0 margin auto .gear-loader_cogs__left position relative width 80px transform rotate(16deg) top 28px transform-origin 40px 40px animation rotate_left 10s 0.1s infinite reverse linear left -24px height 80px div &:nth-of-type(1) transform rotate(30deg) &:nth-of-type(2) transform rotate(60deg) &:nth-of-type(3) transform rotate(90deg) &.gear-left_part width 80px border-radius 6px position absolute height 80px background #97ddff &.gear-left_hole width 40px height 40px border-radius 100% background white position absolute position absolute left 0 right 0 top 0 bottom 0 margin auto .gear-loader_cogs__bottom position relative width 60px top -65px transform-origin 30px 30px -webkit-animation rotate_left 10.2s 0.4s infinite linear animation rotate_left 10.2s 0.4s infinite linear transform rotate(4deg) left 79px height 60px div &:nth-of-type(1) transform rotate(30deg) &:nth-of-type(2) transform rotate(60deg) &:nth-of-type(3) transform rotate(90deg) &.gear-bottom_part width 60px border-radius 5px position absolute height 60px background #ffcd66 &.gear-bottom_hole width 30px height 30px border-radius 100% background white position absolute position absolute left 0 right 0 top 0 bottom 0 margin auto/* Animations */@-webkit-keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); }}@-webkit-keyframes rotate_left { from { transform: rotate(16deg); } to { transform: rotate(376deg); }}@keyframes rotate_left { from { transform: rotate(16deg); } to { transform: rotate(376deg); }}@-webkit-keyframes rotate_right { from { transform: rotate(4deg); } to { transform: rotate(364deg); }}@keyframes rotate_right { from { transform: rotate(4deg); } to { transform: rotate(364deg); }} [Blogroot]\\themes\\butterfly\\layout\\includes\\layout.pug 11 行 1234 body- if theme.preloader+ if theme.preloader.enable !=partial('includes/loading/loading', {}, {cache: true}) [Blogroot]\\themes\\butterfly\\source\\css\\var.styl 101 行 1234 // preloader- $preloader-bg = #37474f+ $preloader-bg = hexo-config('preloader.enable') && hexo-config('preloader.load_color') ? convert(hexo-config('preloader.load_color')) : #37474f $preloader-word-color = #fff [Blogroot]\\themes\\butterfly\\layout\\includes\\loading\\loading-js.pug 全部 1234567891011121314script(async). var preloader = { endLoading: () => { document.body.style.overflow = 'auto'; document.getElementById('loading-box').classList.add("loaded") }, initLoading: () => { document.body.style.overflow = ''; document.getElementById('loading-box').classList.remove("loaded") } } window.addEventListener('load',preloader.endLoading()) document.getElementById('loading-box').addEventListener('click',()=> {preloader.endLoading()}) 友链样式魔改See:https://akilar.top/posts/57291286/ [Blogroot]\\themes\\butterfly\\layout\\includes\\page\\flink.pug 全部 123case theme.flink_style when 'flexcard' include ./flink_style/flexcard.pug 新建 [Blogroot]\\themes\\butterfly\\layout\\includes\\page\\flink_style\\flexcard.pug 1234567891011121314151617181920#article-container if top_img === false h1.page-title= page.title .flink if site.data.link each i in site.data.link if i.class_name h2!= i.class_name if i.class_desc .flink-desc!=i.class_desc .flink-list each item in i.link_list a.flink-list-card(href=url_for(item.link) target='_blank' data-title=item.descr) .wrapper.cover - var siteshot = item.siteshot ? url_for(item.siteshot) : 'https://image.thum.io/get/width/400/crop/800/allowJPG/wait/20/noanimate/' + item.link img.no-lightbox.cover.fadeIn(src=siteshot onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.post_page) + `'` alt='' ) .info img.no-lightbox(src=url_for(item.avatar) onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.flink) + `'` alt='' ) span.flink-sitename= item.name != page.content [Blogroot]\\themes\\butterfly\\source\\css\\_page\\flink.styl 全部 12if hexo-config('flink_style') == 'flexcard' @import './_flink_style/flexcard' 新建 [Blogroot]\\themes\\butterfly\\source\\css\\_flink_style\\flexcard.styl 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114.flink-list overflow auto & > a width calc(25% - 15px) height 130px position relative display block margin 15px 7px float left overflow hidden border-radius 10px transition all .3s ease 0s, transform .6s cubic-bezier(.6, .2, .1, 1) 0s box-shadow 0 14px 38px rgba(0, 0, 0, .08), 0 3px 8px rgba(0, 0, 0, .06) &:hover .info transform translateY(-100%) .wrapper img transform scale(1.2) &::before position: fixed width:inherit margin:auto left:0 right:0 top:10% border-radius: 10px text-align: center z-index: 100 content: attr(data-title) font-size: 20px color: #fff padding: 10px background-color: rgba($theme-color,0.8) .cover width 100% transition transform .5s ease-out .wrapper position relative .fadeIn animation coverIn .8s ease-out forwards img height 130px pointer-events none .info display flex flex-direction column justify-content center align-items center width 100% height 100% overflow hidden border-radius 3px background-color hsla(0, 0%, 100%, .7) transition transform .5s cubic-bezier(.6, .2, .1, 1) 0s img position relative top 22px width 66px height 66px border-radius 50% box-shadow 0 0 10px rgba(0, 0, 0, .3) z-index 1 text-align center pointer-events none span padding 20px 10% 60px 10% font-size 16px width 100% text-align center box-shadow 0 0 10px rgba(0, 0, 0, .3) background-color hsla(0, 0%, 100%, .7) color var(--font-color) white-space nowrap overflow hidden text-overflow ellipsis.flink-list>a .info,.flink-list>a .wrapper .cover position absolute top 0 left 0@media screen and (max-width:1024px) .flink-list & > a width calc(33.33333% - 15px)@media screen and (max-width:600px) .flink-list & > a width calc(50% - 15px)[data-theme=dark] .flink-list a .info, .flink-list a .info span background-color rgba(0, 0, 0, .6) .flink-list & > a &:hover &:before background-color: rgba(#121212,0.8);.justified-gallery > div > img,.justified-gallery > figure > img,.justified-gallery > a > a > img,.justified-gallery > div > a > img,.justified-gallery > figure > a > img,.justified-gallery > a > svg,.justified-gallery > div > svg,.justified-gallery > figure > svg,.justified-gallery > a > a > svg,.justified-gallery > div > a > svg,.justified-gallery > figure > a > svg position static!important 时间轴生肖图标See:https://akilar.top/posts/22257072/ 新建 [Blogroot]\\themes\\butterfly\\scripts\\year.js 123456789101112131415161718hexo.extend.helper.register('getAnimalIcon', function (year) { var index = parseInt(year) % 12; var icon = { 0: 'icon-monkey', 1: 'icon-rooster', 2: 'icon-dog', 3: 'icon-boar', 4: 'icon-rat', 5: 'icon-ox', 6: 'icon-tiger', 7: 'icon-rabbit', 8: 'icon-dragon', 9: 'icon-snake', 10: 'icon-horse', 11: 'icon-goat', } return icon[index]}); [Blogroot]\\themes\\butterfly\\layout\\includes\\mixins\\article-sort.pug 全部 123456789101112131415161718192021222324mixin articleSort(posts) .article-sort - var year - posts.each(function (article) { - let tempYear = date(article.date, 'YYYY') - let no_cover = article.cover === false || !theme.cover.archives_enable ? 'no-article-cover' : '' - let title = article.title || _p('no_title') - let iconAnimal = '#'+ getAnimalIcon(tempYear) if tempYear !== year - year = tempYear .article-sort-item.year span= year svg.icon(aria-hidden='true' style='width: 1em!important; height: 1em!important;') use(xlink:href=iconAnimal) .article-sort-item(class=no_cover) if article.cover && theme.cover.archives_enable a.article-sort-item-img(href=url_for(article.path) title=title) img(src=url_for(article.cover) alt=title onerror=`this.onerror=null;this.src='${url_for(theme.error_img.post_page)}'`) .article-sort-item-info .article-sort-item-time i.far.fa-calendar-alt time.post-meta-date-created(datetime=date_xml(article.date) title=_p('post.created') + ' ' + full_date(article.date))= date(article.date, config.date_format) a.article-sort-item-title(href=url_for(article.path) title=title)= title - }) 节日挂件See:https://akilar.top/posts/23fdf850/ 新建 [Blogroot]\\themes\\butterfly\\scripts\\festival.js 1234567891011121314151617hexo.extend.helper.register('getFestivalIcon', function () { var icon = [ '#icon-qiandai', '#icon-denglong', '#icon-juanzhou', '#icon-hongbao', '#icon-duilian', '#icon-bianpao', '#icon-shanzi', '#icon-tangguo', '#icon-yuanbao', '#icon-qianchuan', '#icon-denglong2' ] var index = Math.floor(Math.random()*icon.length); return icon[index]}); [Blogroot]\\themes\\butterfly\\layout\\includes\\mixins\\post-ui.pug 14~19 行 123456if post_cover && theme.cover.index_enable .post_cover(class=leftOrRight) a(href=url_for(link) title=title) svg.icon.festival-decoration(aria-hidden="true") use(xlink:href=getFestivalIcon()) img.post_bg(src=url_for(post_cover) onerror=`this.onerror=null;this.src='`+ url_for(theme.error_img.post_page) + `'` alt=title) Swiper BarSee:https://akilar.top/posts/8e1264d1/ 新建 [Blogroot]\\themes\\butterfly\\layout\\includes\\sliderbar.pug 123456789101112131415.blog-slider.swiper-container-fade.swiper-container-horizontal#swiper_container .blog-slider__wrp.swiper-wrapper(style='transition-duration: 0ms;') if site.data.slider each i in site.data.slider .blog-slider__item.swiper-slide(style='width: 750px; opacity: 1; transform: translate3d(0px, 0px, 0px); transition-duration: 0ms;') a.blog-slider__img(href=url_for(i.link) alt='')| img(width='48' height='48' src=url_for(i.cover) onerror=`this.onerror=null;this.src='` + url_for(theme.error_img.post_page) + `'`, alt='') .blog-slider__content span.blog-slider__code= i.timeline a.blog-slider__title(href=url_for(i.link) alt='')= i.title .blog-slider__text= i.description a.blog-slider__button(href=url_for(i.link) alt='')= i.button .blog-slider__pagination.swiper-pagination-clickable.swiper-pagination-bulletsscript(defer src=url_for(theme.CDN.swiper_js))script(defer data-pjax src=url_for(theme.CDN.swiper_init)) [Blogroot]\\themes\\butterfly\\layout\\index.pug 123456789 extends includes/layout.pug block content include ./includes/mixins/post-ui.pug #recent-posts.recent-posts+ .recent-post-item(style='height:auto;width:100%;')+ !=partial('includes/sliderbar', {}, {cache:true}) +postUI include includes/pagination.pug 新建 [Blogroot]\\themes\\butterfly\\source\\css\\_layout\\swiperstyle.styl 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229* box-sizing border-boxdiv#swiper_container background rgba(255, 255, 255, 0);.blog-slider width 100% position relative border-radius 12px 8px 8px 12px margin auto background var(--global-bg) padding: 10px transition all .3s.blog-slider__item display flex align-items center &.swiper-slide-active .blog-slider__img img opacity 1 transition-delay .3s .blog-slider__content & > * opacity 1 transform none & > *:nth-child(1) transition-delay 0.3s & > *:nth-child(2) transition-delay 0.4s & > *:nth-child(3) transition-delay 0.5s & > *:nth-child(4) transition-delay 0.6s & > *:nth-child(5) transition-delay 0.7s & > *:nth-child(6) transition-delay 0.8s & > *:nth-child(7) transition-delay 0.9s & > *:nth-child(8) transition-delay 1s & > *:nth-child(9) transition-delay 1.1s & > *:nth-child(10) transition-delay 1.2s & > *:nth-child(11) transition-delay 1.3s & > *:nth-child(12) transition-delay 1.4s & > *:nth-child(13) transition-delay 1.5s & > *:nth-child(14) transition-delay 1.6s & > *:nth-child(15) transition-delay 1.7s.blog-slider__img width 200px flex-shrink 0 height 200px padding 10px border-radius 5px transform translateX(0px) overflow hidden &:after content '' position absolute top 0 left 0 width 100% height 100% border-radius 5px opacity 0.8 img width 100% height 100% object-fit cover display block opacity 0 border-radius 5px transition all .3s.blog-slider__content padding-right 50px padding-left 50px & > * opacity 0 transform translateY(25px) transition all .4s.blog-slider__code color var(--font-color) margin-bottom 0px display block font-weight 500.blog-slider__title font-size 18px font-weight 700 color var(--font-color) margin-bottom 15px -webkit-line-clamp 1 display -webkit-box overflow hidden -webkit-box-orient vertical.blog-slider__text color var(--font-color) -webkit-line-clamp 1 display -webkit-box overflow hidden -webkit-box-orient vertical margin-bottom 15px line-height 1.5em width 100% display block word-break break-all word-wrap break-word.blog-slider__button display inline-flex background-color var(--btn-bg) padding 4px 14px border-radius 8px color var(--btn-color) text-decoration none font-weight 500 justify-content center text-align center letter-spacing 1px display none &:hover background-color var(--btn-hover-color) color var(--btn-color).blog-slider .swiper-container-horizontal > .swiper-pagination-bullets, .blog-slider .swiper-pagination-custom, .blog-slider .swiper-pagination-fraction bottom 10px left 0 width 100%.blog-slider__pagination position absolute z-index 21 right 20px width 11px !important text-align center left auto !important top 50% bottom auto !important transform translateY(-50%) &.swiper-pagination-bullets .swiper-pagination-bullet margin 8px 0 .swiper-pagination-bullet width 11px height 11px display block border-radius 10px background #858585 opacity 0.2 transition all .3s .swiper-pagination-bullet-active opacity 1 background var(--btn-bg) height 30px@media screen and (max-width: 600px) .blog-slider__pagination transform translateX(-50%) left 50% !important top 320px width 100% !important display flex justify-content center align-items center .blog-slider__pagination &.swiper-pagination-bullets .swiper-pagination-bullet margin 0 5px .blog-slider__pagination .swiper-pagination-bullet-active height 11px width 30px .blog-slider__button display inline-flex width 100% .blog-slider__text margin-bottom 40px .blog-slider min-height 350px height auto margin-top 110px margin-bottom 10px .blog-slider__content margin-top -80px text-align center padding 0 30px .blog-slider__item flex-direction column .blog-slider__img transform translateY(-50%) width 90% .blog-slider__content padding-left 10px padding-right 10px .blog-slider__pagination.swiper-pagination-clickable.swiper-pagination-bullets top 110px@media screen and (min-width: 600px) .blog-slider height 200px .blog-slider__img height 200px [Blogroot]\\themes\\butterfly\\source\\css\\index.styl 第 1 行 1+ @import url(hexo-config('CDN.swiper_css')) 配置手机 PC 页面白天黑夜共四个背景图See:https://akilar.top/posts/23fdf850/ [Blogroot]\\themes\\butterfly\\layout\\includes\\layout.pug 14~15 行 12345678910111213141516171819202122- var DefaultBg = page.defaultbg ? page.defaultbg : theme.background.default- var DDMBg = theme.background.darkmode ? theme.background.darkmode : DefaultBg- var DarkmodeBg = page.darkmodebg ? page.darkmodebg : DDMBgif theme.background #web_bg if page.defaultbg || page.darkmodebg style. #web_bg{ background: #{DefaultBg} !important; background-attachment: local!important; background-position: center!important; background-size: cover!important; background-repeat: no-repeat!important; } [data-theme="dark"] #web_bg{ background: #{DarkmodeBg} !important; background-attachment: local!important; background-position: center!important; background-size: cover!important; background-repeat: no-repeat!important; } 新建 [Blogroot]\\themes\\butterfly\\source\\css\\_layout\\web-bg.styl 123456789101112131415161718192021222324$web-bg-night = hexo-config('background.darkmode') ? unquote(hexo-config('background.darkmode')) : $web-bg$mobile-bg-day = hexo-config('background.mobileday') ? unquote(hexo-config('background.mobileday')) : $web-bg$mobile-bg-night = hexo-config('background.mobilenight') ? unquote(hexo-config('background.mobilenight')) : $web-bg-night[data-theme="dark"] #web_bg background: $web-bg-night background-attachment: local background-position: center background-size: cover background-repeat: no-repeat@media screen and (max-width: 800px) #web_bg background: $mobile-bg-day !important background-attachment: local !important background-position: center !important background-size: cover !important background-repeat: no-repeat !important [data-theme="dark"] #web_bg background: $mobile-bg-night !important background-attachment: local !important background-position: center !important background-size: cover !important background-repeat: no-repeat !important [Blogroot]\\themes\\butterfly\\source\\css\\var.styl 34 行 1234 $text-line-height = 2- $web-bg = hexo-config('background') && unquote(hexo-config('background'))+ $web-bg = hexo-config('background.default') && unquote(hexo-config('background.default')) $index_top_img_height = hexo-config('index_top_img_height') ? convert(hexo-config('index_top_img_height')) : 100vh [Blogroot]\\themes\\butterfly\\layout\\includes\\third-party\\pjax.pug 6 行(新版已经被压缩成一行了) 1234567891011 script(src=url_for(theme.CDN.pjax)) script. let pjaxSelectors = [ 'title', '#config-diff', '#body-wrap', '#rightside-config-hide', '#rightside-config-show',+ '#web_bg', '.js-pjax' ] Copyright-beautifySee:https://akilar.top/posts/8322f8e6/ [Blogroot]\\themes\\butterfly\\layout\\includes\\post\\post-copyright.pug 全部 1234567891011121314151617181920212223242526272829303132if theme.post_copyright.enable && page.copyright !== false - let author = page.copyright_author ? page.copyright_author : config.author - let url = page.copyright_url ? page.copyright_url : page.permalink - let license = page.license ? page.license : theme.post_copyright.license - let license_url = page.license_url ? page.license_url : theme.post_copyright.license_url .post-copyright .post-copyright__title span.post-copyright-info h #[=page.title] .post-copyright__type span.post-copyright-info a(href=url_for(url))= theme.post_copyright.decode ? decodeURI(url) : url .post-copyright-m .post-copyright-m-info .post-copyright-a h 作者 .post-copyright-cc-info h=author .post-copyright-c h 发布于 .post-copyright-cc-info h=date(page.date, config.date_format) .post-copyright-u h 更新于 .post-copyright-cc-info h=date(page.updated, config.date_format) .post-copyright-c h 许可协议 .post-copyright-cc-info a.icon(rel='noopener' target='_blank' title='Creative Commons' href='https://creativecommons.org/') i.fab.fa-creative-commons a(rel='noopener' target='_blank' title=license href=url_for(license_url))=license [Blogroot]\\themes\\butterfly\\source\\css\\_layout\\post.styl 全部 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264beautify() headStyle(fontsize) padding-left: unit(fontsize + 12, 'px') &:before margin-left: unit((-(fontsize + 6)), 'px') font-size: unit(fontsize, 'px') &:hover padding-left: unit(fontsize + 18, 'px') h1, h2, h3, h4, h5, h6 transition: all .2s ease-out &:before position: absolute top: calc(50% - 7px) color: $title-prefix-icon-color content: $title-prefix-icon line-height: 1 transition: all .2s ease-out @extend .fontawesomeIcon &:hover &:before color: $light-blue h1 headStyle(20) h2 headStyle(18) h3 headStyle(16) h4 headStyle(14) h5 headStyle(12) h6 headStyle(12) ol, ul p margin: 0 0 8px li &::marker color: $light-blue font-weight: 600 font-size: 1.05em &:hover &::marker color: var(--pseudo-hover) ul > li list-style-type: circle#article-container word-wrap: break-word overflow-wrap: break-word a color: $theme-link-color &:hover text-decoration: underline img display: block margin: 0 auto 20px max-width: 100% transition: filter 375ms ease-in .2s p margin: 0 0 16px iframe margin: 0 0 20px if hexo-config('anchor') a.headerlink &:after @extend .fontawesomeIcon float: right color: var(--headline-presudo) content: '\\f0c1' font-size: .95em opacity: 0 transition: all .3s &:hover &:after color: var(--pseudo-hover) h1, h2, h3, h4, h5, h6 &:hover a.headerlink &:after opacity: 1 ol, ul ol, ul padding-left: 20px li margin: 4px 0 p margin: 0 0 8px if hexo-config('beautify.enable') if hexo-config('beautify.field') == 'site' beautify() else if hexo-config('beautify.field') == 'post' &.post-content beautify() > :last-child margin-bottom: 0 !important#post .tag_share .post-meta &__tag-list display: inline-block &__tags display: inline-block margin: 8px 8px 8px 0 padding: 0 12px width: fit-content border: 1px solid $light-blue border-radius: 12px color: $light-blue font-size: .85em transition: all .2s ease-in-out &:hover background: $light-blue color: var(--white) .post_share display: inline-block float: right margin: 8px 0 width: fit-content .social-share font-size: .85em .social-share-icon margin: 0 4px width: w = 1.85em height: w font-size: 1.2em line-height: w .post-copyright position: relative margin: 40px 0 10px padding: 10px 16px border: 1px solid var(--light-grey) transition: box-shadow .3s ease-in-out overflow: hidden border-radius: 12px!important background-color: rgb(239 241 243) &:before background var(--heo-post-blockquote-bg) position absolute right -26px top -120px content '\\f25e' font-size 200px font-family 'Font Awesome 5 Brands' opacity .2 &:hover box-shadow: 0 0 8px 0 rgba(232, 237, 250, .6), 0 2px 4px 0 rgba(232, 237, 250, .5) .post-copyright &-meta color: $light-blue font-weight: bold &-info padding-left: 6px a text-decoration: none word-break: break-word &:hover text-decoration: none .post-copyright-cc-info color: $theme-color; .post-outdate-notice position: relative margin: 0 0 20px padding: .5em 1.2em border-radius: 3px background-color: $noticeOutdate-bg color: $noticeOutdate-color if hexo-config('noticeOutdate.style') == 'flat' padding: .5em 1em .5em 2.6em border-left: 5px solid $noticeOutdate-border &:before @extend .fontawesomeIcon position: absolute top: 50% left: .9em color: $noticeOutdate-border content: '\\f071' transform: translateY(-50%) .ads-wrap margin: 40px 0.post-copyright-m-info .post-copyright-a, .post-copyright-c, .post-copyright-u display inline-block width fit-content padding 2px 5px[data-theme="dark"] #post .post-copyright background-color #07080a text-shadow #bfbeb8 0 0 2px border 1px solid rgb(19 18 18 / 35%) box-shadow 0 0 5px rgb(20, 120, 210) animation flashlight 1s linear infinite alternate .post-copyright-info color #e0e0e4#post .post-copyright__title font-size 22px .post-copyright__notice font-size 15px .post-copyright box-shadow 2px 2px 5px Butterfly comment board beautifySee:https://akilar.top/posts/397b8b90/ [Blogroot]\\themes\\butterfly\\layout\\includes\\rightside.pug 123 if commentsJsLoad- a#to_comment(href="#post-comment" title=_p("rightside.scroll_to_comment"))+ button#to_comment(type="button" title=_p("rightside.scroll_to_comment") onclick="FixedCommentBtn();") 新建 [Blogroot]\\themes\\butterfly\\source\\css\\_layout\\fixed_card_widget.styl 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113// 垂直居中卡片样式(排除toc目录卡片).fixed-card-widget &:not(#card-toc) visibility visible!important display block!important position fixed!important bottom 0 left 0 top 0 right 0 margin auto margin-bottom auto!important margin-top auto!important max-width 300px max-height 500px width auto height fit-content z-index 999 animation rotateX 0.5s ease animation-fill-mode forwards// 针对说说卡片做样式兼容适配.card-shuo &.fixed-card-widget #artitalk_main max-height 460px overflow scroll &::-webkit-scrollbar display: none #operare_artitalk .c2 z-index 1000// 针对标签卡片做样式兼容适配.card-tags &.fixed-card-widget .card-tag-cloud max-height 460px overflow scroll &::-webkit-scrollbar display: none// 控制手机端可见@media screen and (max-width: 768px) div#fixedcard-dashboard display flex!important// 侧栏悬浮按钮样式div#fixedcard-dashboard position fixed top 150px width fit-content height 40px opacity 0.3 transition all 0.5s display none background rgba(255,255,255,0.9) padding 5px 10px border-top-right-radius 20px border-bottom-right-radius 20px z-index 1000 &:hover opacity 1 button &.fixedcard-activebtn width 30px opacity 1 pointer-events all// 按钮样式button &.fixedcard-activebtn width 0px height 30px transition all .5s display flex opacity 0 align-items center justify-content space-around pointer-events none color #757273// 悬浮按钮头像.fixedcard-user-avatar display inline-block img &.fixedcard-user-avatar-img width 30px height 30px border-radius 50%// 悬浮按钮夜间模式适配[data-theme="dark"] div#fixedcard-dashboard background rgba(55,55,55,0.9) button &.fixedcard-activebtn color #bcbdbd// 卡片开启动画效果@keyframes rotateX from transform rotateX(90deg) to transform rotateX(0deg)// 退出蒙版效果div#quit-box position fixed display block left 0 top 0 width 100vh height 100vh z-index 99 background rgba(25,25,25,0.3)[data-theme="dark"] div#quit-box background rgba(147, 146, 128, 0.3) 新建 [Blogroot]\\themes\\butterfly\\layout\\includes\\fixed_card_widget.pug 1234567891011#fixedcard-dashboard if is_post() each poitem in theme.fixed_card_widget.post button.fixedcard-activebtn(type="button" title=poitem.title onclick=`FixedCardWidget("` + poitem.type + `","` + poitem.name + `","` + poitem.index + `")`) i(class=poitem.icon) else each paitem in theme.fixed_card_widget.page button.fixedcard-activebtn(type="button" title=paitem.title onclick=`FixedCardWidget("` + paitem.type + `","` + paitem.name + `","` + paitem.index + `")`) i(class=paitem.icon) .fixedcard-user-avatar.fixedcard-activebtn(onclick="RemoveFixedCardWidget()") img.fixedcard-user-avatar-img(src=url_for(theme.avatar.img) title=config.author) [Blogroot]\\themes\\butterfly\\layout\\includes\\additional-js.pug 末尾 1234 if theme.busuanzi.site_uv || theme.busuanzi.site_pv || theme.busuanzi.page_pv script(async data-pjax src=url_for(theme.CDN.busuanzi))+ if !theme.aside.mobile && theme.fixed_card_widget.enable+ include ./fixed_card_widget.pug [Blogroot]\\themes\\butterfly\\layout\\includes\\third-party\\pjax.pug 6 行 12345678910 script. let pjaxSelectors = [ 'title', '#config-diff', '#body-wrap', '#rightside-config-hide', '#rightside-config-show',+ '#fixedcard-dashboard', '.js-pjax' ] 导航栏魔改[Blogroot]\\themes\\butterfly\\layout\\includes\\header\\nav.pug 全部 12345678910111213141516171819nav#nav span#blog_name a#site-name(href=url_for('/')) #[=config.title] #menus !=partial('includes/header/menu_item', {}, {cache: true}) #nav-right if (theme.algolia_search.enable || theme.local_search.enable) #search-button a.site-page.social-icon.search i.fas.fa-search.fa-fw span=' '+_p('search.title') #darkmode_navswitch a.nav-rightbutton.site-page.darkmode_switchbutton(onclick='switchNightMode()', title=_p('rightside.night_mode_title')) i.fas.fa-adjust #toggle-menu a.site-page i.fas.fa-bars.fa-fw","categories":[{"name":"网站","slug":"网站","permalink":"https://blog.liynw.top/categories/%E7%BD%91%E7%AB%99/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://blog.liynw.top/tags/Hexo/"},{"name":"butterfly","slug":"butterfly","permalink":"https://blog.liynw.top/tags/butterfly/"}]},{"title":"树链剖分 / 轻重链剖分","slug":"「Algorithm」树链剖分","date":"2022-05-14T20:34:14.000Z","updated":"2022-05-14T20:34:14.000Z","comments":true,"path":"posts/534cd243/","link":"","permalink":"https://blog.liynw.top/posts/534cd243/","excerpt":"","text":"前置芝士 线段树(链的查询) 树形 DP(树上 DFS 预处理) 概念 & 性质处理一棵树会比较棘手,但是如果我们有办法把这棵树处理成一条一条的链,那就好解决多了。 树链剖分,简称树剖,就是干的这事。树链剖分有两种方法:重链剖分和长链剖分。因为长链剖分不常用,所以这一篇介绍的的都是重链剖分。 在了解接下来的内容之前,先要了解几个概念。 重儿子:每个子树中,子树大小(即子树包含节点数)最大的子节点 轻儿子:除重儿子外的其他子节点 重边:每个节点与其重儿子间的边 轻边:每个节点与其轻儿子间的边 重链:重边连成的链 轻链:轻边连成的链 大家发现没有,这里有三组相对的概念。为了更好的理解这几个概念,让我们在 mjl 的 PPT 上白嫖一棵树过来: 因为单独一个节点也可以看作重链,所以这棵树上的重链分布如下: 颜色标出来应该就很清晰了,每一条重链的链头都是轻儿子,后面全部都是重儿子。 因为 Hexo 对 $\\LaTeX$ 的支持很不友好,所以窝直接从 luogu 的博客上截图截下来了 qwq。 除了上面说的,重链还有几个性质: 每一个节点只能在一条重链上,而且必定在重链上。(原因:每个节点只有一个重儿子。) 一个点到根节点的路径上最有只有 $\\log n$ 条轻边。(原因:若 $v$ 为 $u$ 子节点且 $(u,v)$ 为轻边,则子树大小 $2sum_v\\le sum_u$。) 树链剖分の基本解法两个 DFS 预处理既然每一条重链的头都是轻儿子,我们可以通过标记每一个节点所在重链的链头节点的方法来存储重链。在此之前,我们要开几个数组: $ft_u$: $u$ 的父节点;根节点 $ft_1=0$ $dep_u$: $u$ 节点的深度 $sum_u$: 以 $u$ 为根节点的子树的大小 $son_u$: $u$ 的重儿子;叶子节点 $son_u=0$ $top_u$: $u$ 所在重链的链头节点 第一个 DFS 需要做以下事情: 初始化每一个节点的父节点 $ft$,深度 $dep$ 和子树大小 $sum$; 求出每一个非叶子节点的重儿子 $son$。 代码长这样: 12345678910111213141516171819void dfs1(int u, int fa) { // u 为遍历到的节点,fa 为 u 的父节点 ft[u] = fa; // 初始化 ft 数组 int len = G[u].size() - 1, mx = 0; // mx 存 u 的儿子中子树大小的最大值 rep(i, 0, len) { // 遍历 u 的子节点 int v = G[u][i]; if(v != fa) { dep[v] = dep[u] + 1; // 初始化 dep 数组,是父亲向儿子转移,记得写在 dfs 的前面 dfs1(v, u); sum[u] += sum[v]; // 累加 u 的子树大小 if(sum[v] > mx) { // 判断重儿子 mx = sum[v]; // 更新最大子树大小 son[u] = v; // 更新 u 的重儿子 } } } ++sum[u]; // 别忘了把 u 自己加上 return;} 第二个 DFS 的任务很简单: 算出 $top$ 数组。 (可选)如果后面需要用 dfs 序(可能性很大)也需要求一下。 代码长这样: 1234567891011121314void dfs2(int u, int fa, int tp) { // u、fa 含义同上,tp 为目前重链的链头 top[u] = tp; // 初始化 top 数组 dfn[++tot] = w[u]; // 求 dfs 序 id[u] = tot; // 存一下每一个节点在 dfs 序中出现的位置 int len = G[u].size() - 1; if(son[u]) // 先搜重儿子 dfs2(son[u], u, tp); // 重链还是那一根,所以链头不变 rep(i, 0, len) { // 搜轻儿子 int v = G[u][i]; if(v != fa && v != son[u]) dfs2(v, u, v); // 换了一根重链,轻儿子做链头 } return;} 这个代码遗留了两个问题: 为什么 dfs 序只需要加一次节点? A:其实加一次或者两次两种写法都是对的,加一遍会更方便,两遍对于子树的更新思考起来比较好想出来,下文讲解使用加一遍的方法,两种写法的代码都有,供参考。 为啥要先搜重儿子? A:为了让 dfs 序中每一条重链都连在一起,重儿子之间没有轻儿子捣乱,方便后面跳重链的时候用线段树维护。 树链剖分の妙用1. 求 LCA详情请见 LCA 文章中的树剖解法。 2. 在树上维护线段树利用树剖把树转换为线性的链的特征,结合 dfs 序,可以利用树剖在树上维护线段树,从而达到一些目的。 例题:【模板】树链剖分/轻重链剖分 要求维护的操作是两点之间的简单路径和子树的修改与查询,很明显可以用 dfs 序把树转换为数组再用线段树维护。 首先子树比较好操作,因为 dfs 序中子树是连在一起的,故以 $u$ 为根节点的子树在 dfs 序中的范围就是 $[id_u,id_u+sum_u-1]$,用区间修改、区间查询的线段树维护即可。 维护两节点之间的简单路径需要参考求 LCA 的方法,因为 $u,v$ 跳上去的路径就是这条简单路径。由于 $u,v$ 在跳到一条重链上之前都是一条一条跳重链,所以我们可以一边跳一边用线段树维护这些重链。最后两节点到一条重链上之后,再维护两节点之间的区间即可。我们生成 dfs 序的规则保证了这些区间都是连续的。 代码还挺难打的,我调了一个上午/kk。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174/** * @file P3384.cpp * @author Liynw * @brief 树链剖分(轻重链剖分)模板 * @date 2022-05-03 * * @copyright Copyright (c) 2022 * */#include <cstdio>#include <vector>#define int long long#define rep(i, j, k) for(int i = j; i <= k; ++i)const int maxn = (int)1e6 + 5;int n, q, r, mod, tot, w[maxn], ft[maxn], son[maxn], sum[maxn], dep[maxn], top[maxn], id[maxn], dfn[maxn << 1];std::vector<int> G[maxn];namespace Segment_Tree {struct _ { int val, target, len;} t[maxn << 2];inline void swap(int &x, int &y) { x ^= y, y ^= x, x ^= y; }void build(int p, int l, int r) { t[p].len = r - l + 1; if(l == r) { t[p].val = dfn[l]; return; } int lc = p << 1, rc = p << 1 | 1, mid = (l + r) >> 1; build(lc, l, mid); build(rc, mid + 1, r); t[p].val = t[lc].val + t[rc].val; return;}inline void pushdown(int p) { int l = p << 1, r = p << 1 | 1; t[l].val += t[l].len * t[p].target; t[r].val += t[r].len * t[p].target; t[l].target += t[p].target; t[r].target += t[p].target; t[p].target = 0; return;}void update(int p, int l, int r, int L, int R, int k) { if(L <= l && r <= R) { t[p].val += t[p].len * k; t[p].target += k; return; } pushdown(p); int lc = p << 1, rc = p << 1 | 1, mid = (l + r) >> 1; if(L <= mid) update(lc, l, mid, L, R, k); if(R > mid) update(rc, mid + 1, r, L, R, k); t[p].val = t[lc].val + t[rc].val; return;}int query(int p, int l, int r, int L, int R) { if(L <= l && r <= R) return t[p].val; pushdown(p); int lc = p << 1, rc = p << 1 | 1, mid = (l + r) >> 1, sum = 0; if(L <= mid) sum += query(lc, l, mid, L, R); if(R > mid) sum += query(rc, mid + 1, r, L, R); return sum;}}using namespace Segment_Tree;void dfs1(int u, int fa) { ft[u] = fa; int len = G[u].size() - 1, mx = 0; rep(i, 0, len) { int v = G[u][i]; if(v != fa) { dep[v] = dep[u] + 1; dfs1(v, u); sum[u] += sum[v]; if(sum[v] > mx) { mx = sum[v]; son[u] = v; } } } ++sum[u]; return;}void dfs2(int u, int fa, int tp) { top[u] = tp; dfn[++tot] = w[u]; id[u] = tot; int len = G[u].size() - 1; if(son[u]) { dfs2(son[u], u, tp); } rep(i, 0, len) { int v = G[u][i]; if(v != fa && v != son[u]) dfs2(v, u, v); } return;}inline void Update(int u, int v, int x) { while(top[u] != top[v]) { if(dep[top[u]] < dep[top[v]]) swap(u, v); update(1, 1, tot, id[top[u]], id[u], x); u = ft[top[u]]; } if(dep[u] > dep[v]) swap(u, v); update(1, 1, tot, id[u], id[v], x); return;}inline int Query(int u, int v) { int s = 0; while(top[u] != top[v]) { if(dep[top[u]] < dep[top[v]]) swap(u, v); s += query(1, 1, tot, id[top[u]], id[u]); u = ft[top[u]]; } if(dep[u] > dep[v]) swap(u, v); return s + query(1, 1, tot, id[u], id[v]);}signed main() { scanf("%lld %lld %lld %lld", &n, &q, &r, &mod); rep(i, 1, n) scanf("%lld", &w[i]); int u, v; rep(i, 2, n) { scanf("%lld %lld", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfs1(r, 0); dfs2(r, 0, r); build(1, 1, tot); int op, x, y, z; rep(i, 1, q) { scanf("%lld", &op); if(op == 1) { scanf("%lld %lld %lld", &x, &y, &z); Update(x, y, z); } else if(op == 2) { scanf("%lld %lld", &x, &y); printf("%lld\\n", Query(x, y) % mod); } else if(op == 3) { scanf("%lld %lld", &x, &y); update(1, 1, tot, id[x], id[x] + sum[x] - 1, y); } else { scanf("%lld", &x); printf("%lld\\n", query(1, 1, tot, id[x], id[x] + sum[x] - 1) % mod); } } return 0;} 变式: 「ZJOI2008」树的统计 这个题目应该比板子简单,甚至都不需要区间修改,线段树维护一个结构体即可。敲代码的时候注意: 注意检查线段树有没有挂。 节点权值有可能是负数。所以记得求最大值的时候把初始值赋为极小值。 打线段树的时候可以巧用运算符重载,比如我是这么写的: 1234struct _ { int s, m; _ operator+(const _ &x) const { return (_) { s + x.s, max(m, x.m) }; }} t[maxn << 2], empty; 「HAOI2015」树上操作 几乎和模板一样,只是查询多了一个到根节点的询问,这个函数传参变一下就行了。当然,如果明确知道其中一个节点是根节点,树链剖分也可以这么写: 12345678910111213141516inline void Update(int x, int k) { while(x) { // 直接跳 x 即可,不用考虑根节点 update(1, 1, tot, id[top[x]], id[x], k); x = ft[top[x]]; } return;} inline int Query(int x) { int s = 0; while(x) { s += query(1, 1, tot, id[top[x]], id[x]); x = ft[top[x]]; } return s;} 啊对了,这题数组要开 1e6 而非 1e5,不然只有 $30$ 分,望周知。 「NOI2015」软件包管理器 也是板子,不过需要一个转化。 最开始是一棵初始值全部为 $0$ 的树。 install 操作:判断 $0\\to u$ 路径上有多少个 $0$,并全部改为 $1$。可以再次转换:因为点权只有 $0/1$,故查询操作就是求 $dep_u-\\text{query}(1\\to u)$。 uninstall 操作:判断 $u$ 的子树上有多少个 $1$,并全部改为 $0$。","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"树形结构","slug":"树形结构","permalink":"https://blog.liynw.top/tags/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"}]},{"title":"最近公共祖先(LCA)","slug":"「Algorithm」LCA","date":"2022-05-14T12:50:49.000Z","updated":"2022-05-14T12:50:49.000Z","comments":true,"path":"posts/d17dcd42/","link":"","permalink":"https://blog.liynw.top/posts/d17dcd42/","excerpt":"","text":"概念 对于有根树 $T$ 的两个结点 $u,v$,它们的最近公共祖先(Lowest Common Ancestors)表示一个结点 $x$,满足 $x$ 是 $u$ 和 $v$ 的祖先且 $x$ 的深度尽可能大。在这里,一个节点也可以是它自己的祖先。 LCA 可以求树上两个节点之间的最短路径。$dis(u,v)$ 其实就是 $u\\to \\text{lca}(u,v)\\to v$。 写的是模板是因为一道题都没做出来。以后会补树上差分( 倍增这应该是最好理解的一种方法。 首先我们要知道暴力求 LCA 的方法: 先把深度较大的那个节点往上跳,直到与另一个节点深度相同。 两个节点同时往上跳,直到两节点重合。这个重合的位置就是它们的 LCA。 其实倍增的基本思路也是这个样子,但是与暴力不同的是,上述方法在节点往上跳的时候,是不断跳到它的父节点,也就是一个一个跳的。但是,为了追求速度,倍增 LCA 并没有一个一个地跳。 不知道大家还记得二进制拆分吗?任何一个整数,都可以拆成若干个 $2$ 的幂次相加的形式,且这些幂次互不相同。倍增 LCA 的思路也是这样的:任意一个节点到它 LCA 的距离肯定都是整数,所以一定可以拆出若干个互不相同的 $2$ 的幂次使得这些数的和是它。 以第二步为例,我们可以从大到小枚举 $k$:如果这两个节点往上跳 $2^k$ 之后还不能重合,那就说明 LCA 到它们的距离大于 $2^k$,跳上去了之后也不会错过 LCA,我们就直接把两个节点跳上去,然后接着枚举,直到两个节点可以重合。这个过程复杂度是 $\\Theta(\\log n)$。 第一步也是类似的,倍增地往上跳,直到两节点深度相同为止。 于是思路就想明白了。但是我们在往上跳的时候必须知道两个节点往上跳了一个距离之后会不会重合。所以需要预处理一下:$dp_{i,j}$ 代表 $i$ 节点上面的第 $2^j$ 个节点的编号。比如 $dp_{i,0}$ 就代表的是 $i$ 的父节点。 如何求解 $dp$ 数组呢?预处理打一个 DFS,有两个用处: 求解每一个节点的深度。 求 $dp$ 数组。 往下枚举的时候,我们知道一个节点的父节点是谁,实现起来传个参就行。假设 $v$ 的父节点是 $u$,那么先可以知道 $dp_{v,0}=u$。接着,用这个条件求出:$dp_{v,1}=dp_{u,0}$,也就是 $u$ 的父节点,假设这个节点是 $r$。然后就可以再求出:$dp_{v,2}=dp_{u,1}=dp_{r,0}$……推出通用的式子就是: \\begin{cases} dp_{v,0}=u\\\\ dp_{v,k}=dp_{dp_{u,k-1},k-1}\\ (k>0) \\end{cases}这应该相当于是一个树形 DP。 使用这个数组就很简单了,判断 $u,v$ 往上跳 $2^k$ 会不会重合就是判断 $dp_{u,k}$ 是否等于 $dp_{v,k}$。 参考代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172#include <cstdio>#include <vector>#include <cmath>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)2e4 + 5;std::vector<int> G[maxn];int n, m, l, dep[maxn], dp[maxn][305];bool vis[maxn];inline void swap(int &x, int &y) { x ^= y, y ^= x, x ^= y; }inline int LOG2F(int x) { return (int)log2(x); } // 求向下取整的 log2(n)void dfs(int u, int fa) { int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(v != fa) { dep[v] = dep[u] + 1; dp[v][0] = u; rep(k, 1, l) dp[v][k] = dp[dp[v][k - 1]][k - 1]; dfs(v, u); } } return;}inline int getLCA(int x, int y) { if(dep[x] < dep[y]) swap(x, y); while(dep[x] > dep[y]) x = dp[x][LOG2F(dep[x] - dep[y])]; if(x == y) return x; dep(k, LOG2F(dep[x]), 0) if(dp[x][k] != dp[y][k]) x = dp[x][k], y = dp[y][k]; return dp[x][0];}int main() { int root = 1; scanf("%d", &n); l = LOG2F(n); int a, b, c; rep(i, 1, n) { scanf("%d:(%d)", &a, &b); rep(i, 1, b) { scanf("%d", &c); vis[c] = 1; G[a].push_back(c); G[c].push_back(a); } } rep(i, 1, n) { if(!vis[i]) { root = i; break; } } dep[root] = 1; dfs(root, 0); scanf("%d", &m); rep(i, 1, m) { scanf("%d %d", &a, &b); printf("%d\\n", getLCA(a, b)); } return 0;} Tarjan LCATarjan 是一个人,他提出了很多牛逼的算法,比如说这个求 LCA 的算法,它可以在线性时间复杂度内求解若干个 LCA 的询问。当然,速度快是有代价的,Tarjan LCA 是离线算法,如果有强制在线就用不了了…… 如果大家模拟过 DFS 一棵树的过程,就会发现,任意两个节点之间,DFS 遍历的路径肯定是这两个节点之间的最短路径!比如这个树: 它的遍历顺序就应该是: 11 2 4 9 4 2 5 10 5 11 5 2 6 2 1 3 7 3 8 3 1 这个东西也就是我们说的欧拉序。 知道了最短路径其实也就知道 LCA 了,因为最短路径肯定经过 LCA。假如说我想求 $10$ 和 $6$ 的 LCA,我们就把第一个 $10$ 到第一个 $6$ 这一段截取出来(其实是第几个都无所谓,反正中间也不会出现比 $\\text{lca}$ 深度更小的节点),也就是: 110 5 11 5 2 6 发现这一段深度最小的是节点是 $2$。所以 $\\text{lca}(6,10)$ 就是 $2$。 这个思路衍生出了两种方法,第一种就是直接求用 ST 表求最小值,也就是下面介绍的第三种方法。但是 Tarjan 一看,不行,ST 表时间复杂度太大了,还有一种更快的办法。 我们一边 DFS,一边建立并查集,首先所有的待求节点都在不同的集合里。接着我们用一个数组 $col$ 代表这个节点有没有被遍历过。DFS 函数在遍历节点 $u$ 的时候都干了三件事: 枚举 $u$ 所有子节点 $v$。每次先沿着 $v$ DFS 下去,然后再把 $v$ 所在的集合改为 $u$ 的集合。 标记 $u$ 已经被走过。 枚举所有询问,如果发现有某些组的询问一个节点是 $u$,另一个节点已经被遍历过,那这两个节点的 LCA 就是不是 $u$ 的那个点所在并查集的根。 如何证明算法正确性?首先,遍历到 $u$ 的时候,被标记的节点一定不是 $u$ 的祖先节点,因为 $u$ 的祖先节点都没有回溯回去,不可能被标记。于是我们就知道了 $v$ 不可能是 $\\text{lca}(u,v)$。其次,对于任意一个节点,总会先 DFS 下去,等回溯回来之后再让它加入其父节点的集合,所以,因为遍历过来的路程有一部分是 $v\\to\\text{lca}(u,v)\\to u$,$v\\to \\text{lca}(u,v)$ 这一段所有节点都已经从下到上加入其父节点的集合,但是,从 $\\text{lca}(u,v)$ 的父节点开始一直往上走到根节点那一段没有回溯回来,也就没有进行关于并查集的操作,所以 $v$ 所在集合的根节点显然就是在此集合中深度最小的 $\\text{lca}(u,v)$。 参考代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051#include <cstdio>#include <vector>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)5e6 + 5;int n, q, ans[maxn];std::vector<int> G[maxn], Q[maxn], Q_id[maxn];bool col[maxn];int fa[maxn];inline int findset(int x) { return x == fa[x] ? x : fa[x] = findset(fa[x]); }void tarjan(int u, int dad) { int len = G[u].size() - 1; rep(i, 0, len) { if(G[u][i] != dad) { tarjan(G[u][i], u); fa[G[u][i]] = u; } } col[u] = 1; len = Q[u].size() - 1; rep(i, 0, len) { if(col[Q[u][i]]) ans[Q_id[u][i]] = findset(Q[u][i]); } return;}int main() { scanf("%d %d", &n, &q); rep(i, 1, n) fa[i] = i; int u, v; rep(i, 1, n - 1) { scanf("%d %d", &u, &v); G[u].push_back(v); G[v].push_back(u); } rep(i, 1, q) { scanf("%d %d", &u, &v); Q[u].push_back(v), Q_id[u].push_back(i); Q[v].push_back(u), Q_id[v].push_back(i); } tarjan(1, 0); rep(i, 1, q) printf("%d\\n", ans[i]); return 0;} 欧拉序 + ST 表思路在上面讲过了。可以开一个数组 $pos$ 来记录每一个节点在 dfs 序中第一次出现的下标,这样就可以把求 $\\text{lca}(u,v)$ 转换为求 $[pos_u,pos_v]$ 这一段深度最小的节点的权值。ST 表的实现可以使用结构体。 参考代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990#include <cstdio>#include <vector>#include <cmath>#include <cctype>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e6 + 5;int n, q, root = 1, tot, dep[maxn], pos[maxn], dfn[maxn], ddep[maxn];std::vector<int> G[maxn];inline int read() { int x = 0, w = 0; char ch = 0; while(!isdigit(ch)) { w |= ch == '-'; ch = getchar(); } while(isdigit(ch)) { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return w ? -x : x;}inline void write(int x) { if(x < 0) putchar('-'), x = -x; if(x > 9) write(x / 10); putchar(x % 10 + '0');}struct RMQ { int num, id; friend bool operator<(RMQ x, RMQ y) { return x.num < y.num; }} dp[maxn][35];inline RMQ min(RMQ x, RMQ y) { return x < y ? x : y; }inline void init() { rep(i, 1, tot) dp[i][0].num = ddep[i], dp[i][0].id = dfn[i]; for(int j = 1; 1 << j <= tot; ++j) for(int i = 1; i + (1 << (j - 1)) - 1 <= tot; ++i) dp[i][j] = min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]); return;}inline RMQ rmq(int L, int R) { if(L > R) L ^= R, R ^= L, L ^= R; int k = (int)log2(R - L + 1); return min(dp[L][k], dp[R - (1 << k) + 1][k]);}void dfs(int u, int fa) { int len = G[u].size() - 1; dfn[++tot] = u; pos[u] = tot; rep(i, 0, len) { int v = G[u][i]; if(v != fa) { dep[v] = dep[u] + 1; dfs(v, u); dfn[++tot] = u; } } return;}int main() { n = read(), q = read(); int x, y, ans = 0; rep(i, 1, n - 1) { x = read(), y = read(); G[x].push_back(y); G[y].push_back(x); } dfs(root, 0); rep(i, 1, tot) ddep[i] = dep[dfn[i]]; init(); rep(i, 1, q) { x = read(), y = read(); ans = rmq(pos[x ^ ans], pos[y ^ ans]).id; write(ans); putchar('\\n'); } return 0;} 树链剖分其实树剖求 LCA 思路跟倍增有点像,都是往上跳到 LCA 为止。但是两种方法的跳法不一样:倍增是利用二进制原理精准找到 LCA 的位置,树剖则是跳重链,直到两个节点在一条重链上。 在阅读以下内容之前,请确保您理解了关于树剖的基础内容(概念及两个 DFS 函数)。 执行的操作也就是这样的,不断重复: 判断 $u,v$ 是否在一条重链上: 若是,返回 $u,v$ 当中深度较小的那个节点。 若不是,就比较两个节点所在重链的链头的深度,把深度较大的那个节点跳到其重链链头的父节点。 为什么不是直接比较两个节点的深度,而是要比较两个节点所在重链链头的深度呢?因为节点跳是跳到链头父节点,所以如果直接比较两节点深度,有可能跳上去了之后会错过 LCA。另外,跳到链头父节点的原因是需要换一条重链。 参考代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677#include <cstdio>#include <vector>#include <cmath>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e5 + 5;std::vector<int> G[maxn];int n, m, dep[maxn], ft[maxn], sum[maxn], son[maxn], top[maxn];void dfs1(int u, int fa) { ft[u] = fa; int len = G[u].size() - 1, mx = 0; rep(i, 0, len) { int v = G[u][i]; if(v != fa) { dep[v] = dep[u] + 1; dfs1(v, u); sum[u] += sum[v]; if(sum[v] > mx) { mx = sum[v]; son[u] = v; } } } ++sum[u]; return;}void dfs2(int u, int fa, int tp) { top[u] = tp; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(v != fa) { if(v == son[u]) dfs2(v, u, tp); else dfs2(v, u, v); } } return;}inline int getLCA(int x, int y) { while(top[x] != top[y]) { if(dep[top[x]] < dep[top[y]]) y = ft[top[y]]; else x = ft[top[x]]; } return dep[x] < dep[y] ? x : y;}inline int dis(int x, int y) { return dep[x] + dep[y] - (dep[getLCA(x, y)] << 1); }int main() { scanf("%d", &n); int a, b; rep(i, 1, n - 1) { scanf("%d %d", &a, &b); G[a].push_back(b); G[b].push_back(a); } dfs1(1, 0); dfs2(1, 0, 1); scanf("%d", &m); rep(i, 1, m) { scanf("%d %d", &a, &b); if(a == b) printf("0\\n"); else printf("%d\\n", dis(a, b)); } return 0;} 对比设树有 $n$ 个节点,询问 $q$ 次,四种算法对比如下: 倍增 tarjan DFS 序 + ST 表 树链剖分 时间复杂度 $\\Theta((n+q)\\log n)$ $\\Theta(n+q)$ $\\Theta(n+q+n\\log n)$ $\\Theta(2n+q\\log n)$ 离线/在线 在线 离线 在线 在线 (表格 From mjl 的 PPT) 可以发现,tarjan 的方法速度是最快的,所以在允许离线的时候建议使用,树剖的速度名列第二,如果需要在线的话它是最好的选择。","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"树形结构","slug":"树形结构","permalink":"https://blog.liynw.top/tags/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"}]},{"title":"树形 DP","slug":"「Algorithm」树形DP","date":"2022-03-27T10:28:57.000Z","updated":"2022-03-27T10:28:57.000Z","comments":true,"path":"posts/c2587cc4/","link":"","permalink":"https://blog.liynw.top/posts/c2587cc4/","excerpt":"","text":"树形 DP,顾名思义,就是在树上的 DP。因为树是一个递归定义的概念,所以,树形 DP需要在 DFS 中进行,所以有人说树形 DP 是一种记忆化搜索。(ZQW:没错就是我) 树上背包这个类型的问题让我想起了背包问题中“有依赖的背包问题”,也就是说,这种背包问题的某些物品只有在选了前置物品的条件下才能选,具体的关系构成一棵树。 让我们来看一道例题:Luogu P2015 二叉苹果树。 这道题有两个要求,第一个是必须保留 $Q$ 条边,第二个是要求留下的边的权值加起来最大。 因为树的任何一个节点都可以作为根节点,所以这里我们默认 $1$ 为根节点(下文同理)。 那么,跟普通背包一样,$dp$ 数组第一维代表抉择的物品,第二维代表背包容量,令 $dp_{u,j}$ 为根节点为 $u$ 的子树上保留 $j$ 条边能达到的最大权值和。 那该怎么状态转移呢? 首先,当我们求 $dp_{u,?}$ 的时候,肯定是要知道它所有子节点的答案。所以,DFS 向下递归的语句应该在状态转移语句的前面。 其次,和普通背包一样,每一个物品(这里是每一条边)都有选和不选两种情况。只是这里,令 $u$ 的一个子节点为 $v$,假如 $(u,v)$ 这一条边没取,那以 $v$ 为根节点的这一棵子树都取不了了,所以只能忽略这棵子树,答案还是原来的 $dp_{u,i}$。而取的时候,情况就比较复杂了,我们需要考虑一下这棵子树到底要保留多少条,所以我们还需要一层循环 $k$,代表这棵子树上保留 $k$ 条边,$(u,v)$ 有一条边,所以剩下的所有子树加起来有 $j-k-1$ 条边。所以对于每一个子节点 $v$,状态转移方程如下: dp_{u,j}=\\max_{k=j-1}^0\\limits\\{dp_{v,k}+dp_{u,j-k-1}+val_{u,v}\\}这里要注意: $k$ 到 $0$ 结束,$k=0$ 是不取的情况。 因为计算 $dp_{u,j}$ 时需要用到 $dp_{u,j-k-1}$ 的值,而 $dp_{j-k-1}$ 是其它子树上的最大权值之和,不能算上 $v$ 这一棵子树,所以 $j,k$ 枚举的顺序应该是倒着枚,以免重复(和 01 背包滚动数组背包容量要倒过来枚举是一样的道理)。 代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142#include <cstdio>#include <vector>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)struct node { int to, val;};int n, q, dp[105][105];bool b[105];std::vector<node> G[105];inline int max(int x, int y) { return x > y ? x : y; }void dfs(int i) { b[i] = 1; int len = G[i].size(); if(len == 1) return; rep(v, 0, len - 1) { if(!b[G[i][v].to]) { dfs(G[i][v].to); dep(j, q, 0) dep(k, j - 1, 0) dp[i][j] = max(dp[i][j], G[i][v].val + dp[G[i][v].to][k] + dp[i][j - k - 1]); } } return;}int main() { scanf("%d %d", &n, &q); int u, v, w; rep(i, 1, n - 1) { scanf("%d %d %d", &u, &v, &w); G[u].push_back(node({v, w})); G[v].push_back(node({u, w})); } dfs(1); printf("%d", dp[1][q]); return 0;} 变式练习: 「CTSC1997」选课 和例题基本上一样,只是这道题给的是一个森林而非一棵树。处理方法也很简单,只需要建一个 $0$ 号节点,把所有树的根节点连上去,然后以 $0$ 为根处理就行了。 树的直径树的直径就是一棵树上最长的链,所以一棵树上可能不止一条直径。 只求长度例题如下: 如果一个数 $x$ 的约数和 $y$(不包括他本身)比他本身小,那么 $x$ 可以变成 $y$,$y$ 也可以变成 $x$。例如 $4$ 可以变为 $3$,$1$ 可以变为 $7$。限定所有数字变换在不超过 $n$ 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。 这道题转换挺巧妙的,该怎么看出来这是个求树的直径的问题呢? 其实,我们只需要画一张图,把数字作为节点,可以相互变换的数字连在一起就可以了: 这个时候就有人问了:那怎么证明这个图里没有环? 令对一个数 $a$ 求约数和的操作为 $f(a)$。假设 $f(x)=y,f(y)=z$,那如果想要构成一个环,就要求 $f(x)=z$,或者 $f(z)=x$。 第一种情况很好排除,因为只有 $y=z$ 时才能构成环,而 $f(y)=z$,所以,只有像 $6$ 那样的“完全数”才能满足。但是,既然 $y$ 已经是完全数了,那么说明 $x=y$,实际上这是一个自环,对结果没有影响,我们直接忽略掉它。 第二种情况乍一看不太好整,但是注意到题目要求可以发现 $x>y>z$,然后又要求 $f(z)<z$,很显然 $f(z)\\neq x$。(所以第一种情况也可以直接排除了,因为完全数是不被允许的) 好的,现在我们可以证明这张图是一棵树了,接下来的事情就简单了。因为不能重复,所以我们只能从一个节点走到另一个节点不能回头,那只需要找到树上最长的链就行了,也就是树的直径。 那回归正题:怎么求树的直径的长度? 我们知道,一个节点上能产生的影响经过此节点的最长链的长度的链只有三条: 设节点 $u$ 往下的最长链长度为 $d1_u$,次长链为 $d2_u$,往上的链(经过它的父节点)为 $up_u$,那么这条链的长度要么是 $d1_u+d2_u$,要么是 $d1_u+up_u$,反正不可能是 $d2_u+up_u$(因为保证 $d1_u\\ge d2_u$),所以我们可以发现是一定有 $d1_u$ 的。 那么,对于 $u$ 的所有父节点 $r$(这个 $r$ 指的不是某一个节点,而是所有 $u$ 的直系父节点,可以是它的爸爸,也可以是它的爷爷,一直往上追溯到根节点),$d1_u+up_u$ 和 $\\max\\{d1_r+d2_r\\}$ 是一样的,因为后者其实包含了 $d1_u$ 和所有 $up_u$ 中的所有情况。这个大家自己画画图就知道了,用语言不太好描述 qwq。 所以,我们只需要遍历一下每一个节点的 $d1,d2$ 之和就可以了。 那 $d1,d2$ 该怎么求呢? 这就回归到了树形 DP。我们还是 DFS 往下找,遍历每一个节点 $u$ 的所有子节点 $v$。对于每一个子节点,先对它 DFS,此时每一个 $v$ 都有自己的 $d1_v$,我们只需要找到最大的两个 $d1_v$ ,再加上 $1$ 就可以求出 $d1_u$ 和 $d2_u$。 那为什么不能用 $d2_v$ 来更新呢?确实有这种情况:某个节点的最长链和次长链的下一个节点都在一个子节点上。但是我们的要求是:这两条链不能重合! 不然树的直径就经过重复的节点了,所以我们不能用 $d2_v$ 来更新 $d1_u$ 和 $d2_u$。 所以,光求长度跟 $up$ 数组没有半毛钱关系。 这道题的代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364#include <cstdio>#include <cstring>#include <vector>#include <cmath>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)5e4 + 5;int n, ans, h1[maxn], h2[maxn], q[maxn];std::vector<int> G[maxn];bool b[maxn];inline int max(int x, int y) { return x > y ? x : y; }inline int mk(int x) { int s = 0, p = floor(sqrt(x)); rep(i, 1, p) s += x % i ? 0 : i + x / i; if(p * p == x) s -= p; return s - x;}void dfs(int u) { b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) { dfs(v); if(h1[v] + 1 > h1[u]) { h2[u] = h1[u]; h1[u] = h1[v] + 1; } else if(h1[v] + 1 > h2[u]) h2[u] = h1[v] + 1; } } ans = max(ans, h1[u] + h2[u]); return;}int main() { scanf("%d", &n); rep(i, 2, n) q[i] = mk(i); rep(i, 2, n) { if(q[i] > i) continue; G[i].push_back(q[i]); if(i != q[i]) G[q[i]].push_back(i); } /* rep(i, 1, n) { printf("%d: ", i); rep(j, 0, (int)G[i].size() - 1) printf("%d ", G[i][j]); printf("\\n"); } */ dfs(1); printf("%d", ans); return 0;} 提交记录 #1145000。 求直径上的节点来看上道题的加强版:要求我们输出所有在树的直径上的节点编号。 思路还是那个思路,不过这下我们就需要求 $up$ 了,这样我们才能知道每一个节点上的最长链到底等不等于树的直径的长度。 那怎么求某个节点 $u$ 的 $up$ 值呢?有两种情况: 先从它父节点 $r$ 的 $up$ 值代表的路径走到 $r$,再走到 $u$,$up_u=up_r+1$; 先从 $r$ 的 $d1$ 值代表的路径走到 $r$,再走到 $u$,$up_u=d1_r+1$。 过程就是这样吗?不完全是,请看图: 我们在更新 $up_4$ 的值时,会发现 $d1_2+1=4$,但是实际上很明显 $up_2=3$。这是因为 $d1_2$ 这一条路径会经过 $4$ 节点,而树的直径要求不能经过重复的节点,于是就出问题了。 解决方案是:当判断到 $d1_r$ 这条路径经过 $u$ 的时候,把 $d1_r+1$ 更换为 $d2_r+1$ 即可,如果 $d1_r$ 经过另外 $u$,那么 $d2_r$ 一定不经过 $u$。 输出答案时一个节点一个节点地判断,如果经过这个节点的最长链长度等于树的直径,那就输出。 代码如下(调了好久 qwq): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384#include <cstdio>#include <cstring>#include <vector>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)2e5 + 5;int n, ans, d1[maxn], d2[maxn], up[maxn]; // d1 为向下最长链,d2 为向下次长链,up 为第一步先到父节点之后不再折返回来的最长链 std::vector<int> p, G[maxn]; // G 存图,p 存答案 bool b[maxn]; // 标记走过的节点,以免重复走到 inline int max(int x, int y) { return x > y ? x : y; }void dfs1(int u) { // 从下往上递归,求解 d1, d2 和直径的长度 b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { // 遍历此节点的所有子节点 int v = G[u][i]; if(!b[v]) { dfs1(v); // 用 d1[v] 去更新 d1[u] 和 d2[u] // ※注意不能用 d2[v] if(d1[v] + 1 > d1[u]) { d2[u] = d1[u]; d1[u] = d1[v] + 1; } else if(d1[v] + 1 > d2[u]) d2[u] = d1[v] + 1; ans = max(ans, d1[u] + d2[u]); // 更新最长链的长度,为了方便没有计算 u } } return;}void dfs2(int u) { // 从上往下递归,求解 up b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) { if(d1[v] + 1 == d1[u]) // 这说明 u 的最长链经过了 v,因为会重复走 v,所以不能用 d1 更新 up 而是应该用 d2 up[v] = max(up[u], d2[u]) + 1; else // 其余情况就正常更新 up[v] = max(up[u], d1[u]) + 1; dfs2(v); } } return; }/*void debug() { puts("--------------DEBUG--------------"); printf("ans = %d\\n", ans); printf("num d1 d2 up\\n"); rep(i, 1, n) printf("%-5d%-4d%-4d%-4d\\n", i, d1[i], d2[i], up[i]); puts("---------------------------------\\n"); return;}*/int main() { scanf("%d", &n); int u, v; rep(i, 1, n - 1) { scanf("%d %d", &u, &v); ++u, ++v; G[u].push_back(v); G[v].push_back(u); } dfs1(1); memset(b, 0, sizeof(b)); dfs2(1); // debug(); rep(i, 1, n) { if(d1[i] + max(up[i], d2[i]) == ans) // i 在最长链上 p.push_back(i); } int anslen = p.size() - 1; rep(i, 0, anslen) printf("%d\\n", p[i] - 1); return 0;} 与相邻节点有关的树形 DP实在不知道应该写什么名字才写的这个。 这种树形 DP 一般来说会有一维来记录此节点的状态(比如说要在树上染色,开一维代表这个节点有没有染色时候的状态)。 例题 1Luogu P2016 战略游戏 通过读题我们知道,当一条边的两个端点中有至少一个节点放了士兵,这条边就被监视了,所以可以定义 $dp_{u,0/1}$ 为把 $u$ 作为根节点时有/没有放士兵,而且这个子树上的所有边都被监视了的时候这课子树上能放的最少的士兵数量。 状态转移如下: $dp_{u,1}$:$u$ 已经放了士兵,所以它的子节点有没有放士兵都没有关系,方程是 $dp_{u,1}=\\min\\{dp_{v,0},dp_{v,1}\\}$; $dp_{u,0}$:$u$ 没放士兵,所以它的儿子必须自力更生,方程是 $dp_{u,0}=\\min\\{dp_{v,0}\\}$。 然后就没了。 12345678910111213141516171819202122232425262728293031323334353637383940#include <cstdio>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = 1505;int n, dp[maxn][2];bool G[maxn][maxn], b[maxn];inline int min(int x, int y) { return x < y ? x : y; }void dfs(int t) { b[t] = 1; rep(i, 1, n) { if(G[t][i] && !b[i]) { dfs(i); dp[t][0] += dp[i][1]; dp[t][1] += min(dp[i][0], dp[i][1]); } } ++dp[t][1]; return;}int main() { scanf("%d", &n); int u, v, s; rep(i, 1, n) { scanf("%d %d", &u, &s); ++u; rep(j, 1, s) { scanf("%d", &v); ++v; G[u][v] = G[v][u] = 1; } } dfs(1); printf("%d", min(dp[1][0], dp[1][1])); return 0;} 例题 2基本上和例题 $1$ 一样,不过监视的对象从边换成了节点,然后每个节点安排士兵的时候都有一个输入给出的权值。 权值这个倒是没什么影响,但是从边变到节点,就出了一个问题,对于一个节点,只要它的儿子、它的父亲和它本身三者中有至少一个放了士兵,那这个节点就是合法的。所以,这道题的第二维有三种情况: $dp_{u,0}$ 代表 $u$ 节点自己就放了士兵,它的子节点 $v$ 不管状态是啥都是允许的,所以 $dp_{u,0}=\\max{dp_{v,0},dp_{v,1},dp_{v,2}}$。 $dp_{u,1}$ 代表 $u$ 节点的父节点放了士兵,而它自己不放士兵,所以它的子节点就不能依靠父节点(也就是 $u$),$dp_{u,1}=\\max\\{dp_{v,0},dp_{v,2}\\}$。 $dp_{u,2}$ 代表 $u$ 的至少一个子节点放了士兵,它自己不放。注意不是每个子结点都必须放,只需要有一个就行了,而且此时的子节点也不能依靠父节点。 所以 $dp_{u,2}$ 相较于 $dp_{u,1}$,只相差了一个子节点的贡献。我们通过一个循环找出一个子节点,使得它放了士兵和它没放士兵时相差的贡献值最小,这样就能保证 $dp_{u,2}$ 尽量接近 $dp_{u,1}$,也就是保障最优情况。方程是 $dp_{u,2}=dp_{u,1}-\\min\\{dp_{v,0},dp_{v,2}\\}$。 最后求答案的时候注意,根节点没有父节点,所以最终答案是 $\\max\\{dp_{1,0},dp_{1,2}\\}$,没有 $dp_{1,1}$。 代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748#include <cstdio>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = 1505;int n, dp[maxn][3], m[maxn];bool G[maxn][maxn], b[maxn];inline int min(int x, int y) { return x < y ? x : y; }void dfs(int u) { b[u] = 1; int Min = 1 << 30; rep(v, 1, n) { if(G[u][v] && !b[v]) { dfs(v); dp[u][0] += min(dp[v][0], min(dp[v][1], dp[v][2])); dp[u][1] += min(dp[v][0], dp[v][2]); Min = min(Min, dp[v][0] - min(dp[v][0], dp[v][2])); } } dp[u][0] += m[u]; dp[u][2] = dp[u][1] + Min; return;}int main() { scanf("%d", &n); int u, v, s, num; if(n == 1) { scanf("%d %d", &u, &num); printf("%d", num); return 0; } rep(i, 1, n) { scanf("%d %d %d", &u, &num, &s); m[u] = num; rep(j, 1, s) { scanf("%d", &v); G[u][v] = G[v][u] = 1; } } dfs(1); int ans = min(dp[1][0], dp[1][2]); printf("%d", ans); return 0;} 例题 3Luogu P1352 没有上司的舞会 对于一个人来说还是两种情况,要么就是他自己不来,要么就是他的上司不来。 他来的话,他的所有部下都不能来。 他不来的话,他的部下既可以来也可以不来。 1234567891011121314151617181920212223242526272829303132333435363738394041#include <cstdio>#include <vector>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = 6005;int n, p[maxn], dp[maxn][2];std::vector<int> G[maxn];bool b[maxn];inline int max(int a, int b) { return a > b ? a : b; }void dfs(int u) { b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) { dfs(v); dp[u][0] += max(dp[v][0], dp[v][1]); dp[u][1] += dp[v][0]; } } dp[u][1] += p[u];}int main() { scanf("%d", &n); rep(i, 1, n) scanf("%d", &p[i]); int u, v; rep(i, 1, n - 1) { scanf("%d %d", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfs(1); printf("%d", max(0, max(dp[1][0], dp[1][1]))); return 0;} 例题 4「CQOI2009」叶子的染色 这题的题面说得很迷惑,其实你可以想象成拿一桶水,水最开始是无色的,从根节点往下流,最后流到叶子节点上,水的颜色遇到染色的节点就会变成节点的颜色(只有黑和白),但是这一股水颜色的变化只对以此节点为根的这棵子树有影响。要求确定每一个叶子节点收到的水的颜色,问你最少要让多少个节点染色。 $dp_{u.0/1}$ 代表经过 $u$ 节点的水流的颜色是黑/白时这棵子树上最少染色节点的数量。 我们先假设我们给 $u$ 的子节点 $v$ 染了色,所以最后需要加上 $1$。然后我们发现,如果水流经过 $u$ 和 $v$ 的颜色是一样的,那 $v$ 就可以不染色,所以在动规方程里,这种情况需要 $-1$。 叶子节点的初始化,颜色为 $c_u$ 时等于 $1$,不是 $c_u$ 的那一个设成极大值即可。 然后我们随便选一个非叶子节点作为根节点,把水倒下去就可以了。 代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849#include <cstdio>#include <cstring>#include <vector>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e4 + 5;const int inf = 1 << 30;int n, m, ans, c[maxn], dp[maxn][2];std::vector<int> G[maxn];bool b[maxn];inline int min(int x, int y) { return x < y ? x : y; }inline int f(int x) { return x ? 0 : 1; }void dfs(int u) { b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) { dfs(v); dp[u][0] += min(dp[v][0] - 1, dp[v][1]); dp[u][1] += min(dp[v][1] - 1, dp[v][0]); } } ++dp[u][0], ++dp[u][1]; if(u <= m) { dp[u][c[u]] = 1; dp[u][f(c[u])] = inf; } return;}int main() { scanf("%d %d", &n, &m); rep(i, 1, m) scanf("%d", &c[i]); int u, v; rep(i, 1, n - 1) { scanf("%d %d", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfs(m + 1); printf("%d", min(dp[m + 1][0], dp[m + 1][1])); return 0;} 例题 5「HNOI2003」消防局的设立 人类在 2003 年许下的美好愿望,到了 2020 年终于是没实现。 这题乍一看跟例 $2$ 好像也没啥区别是吧? 怎么可能!虽然保管的距离只是从 $1$ 变成了 $2$,但是要麻烦多了。 这题有两种做法,一个是树形 DP,有五种状态(它爷爷,它爸爸,它自己,它儿子,它孙子),因为笔者不会,所以请大家自行推导;还有一种就是今天要介绍的贪心方法,这个贪心的策略浓缩成一句话就是:从下往上找,如果不满足条件,就在这里修一个消防站。 首先 DFS 预处理每一个节点的深度,然后再按照深度从大到小排序。注意这里因为要排序,所以记录节点深度的数组 $deep$ 应该是一个结构体,分别存深度大小和是哪一个节点。 我们需要两个数组,一个是 $q$,$q_u$ 代表排完序之后 $u$ 这个节点和它的深度在 $deep$ 数组的位置(下标,这个数组是为了方便寻找某个节点的父节点,这样可以直接找到某个节点的深度),另一个是 $data$,$data_u$ 代表现在离 $u$ 最近的一个消防站有多远。 按照深度从大到小枚举节点,然后判断这个节点有没有被消防站覆盖,如果没有就盖一个,顺便更新一下它父亲和爷爷的 $data$ 值。 然后就没了。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273#include <cstdio>#include <cstring>#include <vector>#include <algorithm>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)struct node { int num, id;} deep[1005];int n, ans, data[1005], q[1005];std::vector<int> G[1005];bool b[1005], build[1005];bool operator<(node x, node y) { return x.num > y.num; }inline int min(int x, int y) { return x < y ? x : y; }void getdeep(int u, int dep) { b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) getdeep(v, dep + 1); } deep[u].id = u; deep[u].num = dep; return;}inline int fa(int u) { // 找 u 的父亲 int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(deep[q[v]].num == deep[q[u]].num - 1) return v; } return 1;}int main() { memset(data, 0x3f3f3f, sizeof(data)); scanf("%d", &n); int v; rep(u, 2, n) { scanf("%d", &v); G[u].push_back(v); G[v].push_back(u); } getdeep(1, 0); std::stable_sort(deep + 1, deep + n + 1); rep(i, 1, n) q[deep[i].id] = i; rep(i, 1, n) { // *需要注意,这个循环里面我们要判断修不修消防站的是 gf 节点,所以我们需要知道它两代及以内的所有节点,即 u, ft, ggf, gggf int ft = fa(deep[i].id); int gf = fa(ft); if(!build[ft] && !build[gf] && data[deep[i].id] > 2 && data[ft] > 1 && data[gf] > 0) { ++ans; // printf("%d: build %d\\n", deep[i].id, gf); build[gf] = 1; data[deep[i].id] = min(data[deep[i].id], 0); int ggf = fa(gf); int gggf = fa(ggf); data[ggf] = min(data[ggf], 1); data[gggf] = min(data[gggf], 2); } } printf("%d", ans); return 0;} 换根 DP换根 DP,就是在求解的过程中以不同的节点为根来求解问题。 看例题:Luogu P3478 STA-Station。 求解这个问题需要知道以每个节点为根时的深度总和,但是我们不可能每一个节点都 DFS 一次,这样就超时了。但是,有没有方法可以只 DFS 一个节点,然后根据这个节点的答案去求解其它节点的答案? 这张图是我们把根节点从 $2$ 转移到 $4$ 的过程。可以发现,在 $4$ 为根节点时,以 $2$ 为根节点的子树(红色框)所有节点到根节点的距离都会加上 $w$;而在 $2$ 为根节点时,以 $4$ 为根节点的子树(蓝色框)所有节点到根节点的距离都会减少 $w$,只要知道两棵子树分别有多少个节点,就可以根据 $2$ 的答案求解出 $4$ 的答案,公式是 $ans_4=ans_2+size(red)\\times w-size(blue)\\times w$。 所以,我们可以先打一个预处理的 DFS 函数,把以 $1$ 为根节点时每一棵子树的节点总量都求出来。设 $num_i$ 为以 $1$ 为根节点,子树根节点为 $i$ 时这棵子树节点的数量。 当然,在实际求解的时候,因为求解过程中转移时的根节点不一定是 $1$,但是某个子树(假设根节点为 $u$)里面包含我们预处理时的根节点 $1$。所以这个子树的节点总数就不能直接用 $num_u$,而应该用 $n-num_v$($v$ 为 $u$ 的另一个子节点,以上图举例,$u=2,v=4$)。 然后就是代码了: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#include <cstdio>#include <cstring>#include <vector>#define int long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e6 + 5;int n, ans, p, deepsum[maxn], num[maxn];bool b[maxn];std::vector<int> G[maxn];inline int min(int x, int y) { return x < y ? x : y; }void dfs1(int u, int s) { // 预处理 b[u] = 1; int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) { dfs1(v, s + 1); deepsum[u] += deepsum[v]; num[u] += num[v]; } } deepsum[u] += s; ++num[u]; return;}void dfs2(int u, int Num) { // 换根 DP b[u] = 1; if(ans < Num) { ans = Num; p = u; } else if(ans == Num) p = min(u, p); int len = G[u].size() - 1; rep(i, 0, len) { int v = G[u][i]; if(!b[v]) dfs2(v, Num + n - (num[v] << 1)); } return;}signed main() { scanf("%lld", &n); int u, v; rep(i, 1, n - 1) { scanf("%lld %lld", &u, &v); G[u].push_back(v); G[v].push_back(u); } dfs1(1, 0); memset(b, 0, sizeof(b)); ans = deepsum[1]; p = 1; dfs2(1, deepsum[1]); printf("%lld", p); return 0;} 变式: 「USACO10MAR」Great Cow Gathering G 几乎和例题是一样的,不一样之处在于,这道题的边是有权值的。那么,“节点的深度”就要变成“节点到根节点的距离”,其实也没什么大改动,代码就不放了。 其它 一道树上的区间 DP:「NOIP2003 TG」加分二叉树 这题的突破点在于数的中序遍历必须是 $1,2,3,\\ldots,n$,也就是说,一段连续的区间内的节点一定是在一块儿的,而且如果我们选定节点 $i$ 为 $[l,r]$ 这个区间的根节点,那么 $[l,i-1]$ 就是它的左子树,$[i+1,r]$ 就是它的右子树。 然后三重循环的区间 DP 就来了:令区间 $[l,r]$ 是一棵子树(而不是分散的),在区间范围内枚举这个子树的根节点为 $k$,根据题目给出的公式计算出能得到的最大加分。 另外,由于枚举时计算左右子树是 $[l,k-1]$ 和 $[k+1,r]$,所有有可能出现左端点比右端点还大的情况,其实这种情况就是空子树。我们可以在初始化的时候把 $dp_{i,i-1}$ 的值设为 $1$ 来解决这个问题。 输出方案的话,可以用 $f_{i,j}$ 来存把区间 $[i,j]$ 作为一棵子树时的根节点,然后再按照前序遍历的方法“根——左——右”递归输出。 贴一下代码: 123456789101112131415161718192021222324252627282930313233343536373839#include <cstdio>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i) int n, p[35], dp[35][35], f[35][35]; void print(int l, int r) { if(l > r) return; printf("%d ", f[l][r]); print(l, f[l][r] - 1); print(f[l][r] + 1, r); return;} int main() { scanf("%d", &n); rep(i, 1, n) scanf("%d", &p[i]); rep(i, 1, n) dp[i][i] = p[i], f[i][i] = i; rep(i, 0, n) dp[i + 1][i] = 1; rep(len, 2, n) { rep(l, 1, n - len + 1) { int r = l + len - 1; rep(k, l, r) { int w = dp[l][k - 1] * dp[k + 1][r] + p[k]; if(w > dp[l][r]) { dp[l][r] = w; f[l][r] = k; } } } } printf("%d\\n", dp[1][n]); print(1, n); return 0;}","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"状压 DP 总结 & 练习简要题解","slug":"「Algorithm」状态压缩DP","date":"2022-01-26T23:00:54.000Z","updated":"2022-02-01T10:18:50.000Z","comments":true,"path":"posts/f8e3c54/","link":"","permalink":"https://blog.liynw.top/posts/f8e3c54/","excerpt":"","text":"概念状压 DP 就是状态压缩 DP,表现为把某一些状态(比如说某个元素是否被取过)压缩成二进制(当然也有别的进制),用这个进制下某个数的表示来表示一个状态,然后把这个状态作为 $dp$ 数组的一维来状态转移。 比如说有 $5$ 个元素,$1,2,4$ 已经被取过,$3,5$ 还没有。可以用如下二进制数来表示: (11010)_2=(26)_{10}那么 $dp$ 数组就可以开一维来记录状态,当这一维的下标为 $26$ 时,就代表这个状态。所以状压题的数据范围非常的明显,当然,这种数据范围也不排除暴搜的可能性。 对,总结就这么点。 其实做状压 DP 就是不停地做题。 练习题目记录(怎么错的)因为蒟蒻 ljt 做状压每一道都要调很久很久,所以 ta 决定把这些玩意儿记录下来。 当然这个东西是写给自己看的,所以写得比较混乱,如果您需要且看不懂的话可以问我 qwq。当然肯定是没人需要的。 A. 牧场的安排 $\\text{82pts}$ 初始化了第一行,结果没考虑到数据只有一行的情况。 B. 最小总代价 $\\text{0pts}$ 题意理解错了又没看样例。 $\\text{95pts}$ (1 << N) - 1 误打成了 (1 << N + 1) - 1(忘改过来了)。 C. 项链 $\\text{0pts}$ 四重循环被老爷机制裁了。 $\\text{10pts}$ 比较玄学,把范围改成 $0\\sim n-1$ 就过了。 D. 国王 $\\text{0pts}$ num(y) 误打成了 num(x)(记得分清楚每个变量的意义!)。 E. Hie with the Pie $\\text{0pts}$ 没有理解做法,Floyd 的用处。 $\\text{15pts}$ 没加多组数据。 F. Traveling $\\text{0pts}$ 锅太多了,主要有以下几个: 预处理错误,没有真正弄明白预处理数组的意义。 循环顺序错误,枚举状态应该写在在最外面。 你没事加什么并查集啊! 数组两维大小不一样,开反了。 H. 炮兵阵地 $\\text{0pts}$ 运算符优先级的问题,众所周知 + 优先级比三目运算符高。 /yiw $\\text{40pts}$ 数组开小了。 O. 集合选数 $\\text{0pts}$ 把加法原理和乘法原理弄混。 $\\text{30pts}$ 构造矩阵的问题,$o$ 只需要乘一遍就可以了,我多乘了一遍,没有去重。 $\\text{60pts}$ memset 速度过慢。 $\\text{90pts}$ 打表出奇迹! 简要题解A. 牧场的安排 题意:有一个 $N\\times M$ 的矩阵,有些格子里面可以种草,但是不能有相邻的格子同时种草(相邻指上下左右)。给出一个矩阵 $a$,$a_{i,j}=1/0$ 代表 $i$ 行 $j$ 列的格子可以/不可以种草。求种草的方案总数,答案需要 $\\bmod\\ 10^8$。$1\\le N,M\\le 12$。 $dp_{i,j}$ 代表前 $i$ 行,最后一行状态为 $j$ 时的方案数量。其中 $j$ 是一个二进制数,从高位到低位编号为 $1,2,3,\\ldots$ 分别代表第 $1,2,3,\\ldots$ 块地种不种草,$1$ 代表种了,$0$ 代表没种。 然后每枚举到某一行的某个状态时,在这个状态下,我们需要知道三件事: 这一行的状态有相邻的格子种草吗? 这一行和上一行有相邻的格子种草吗? 这一行是否有一些格子本来不能种草却被种上了? 只有这三个问题的回答都是“不”,这个状态才是合法的。 要知道这三个问题的答案,我们还需要知道上一行的状态(回答第二个问题和状态转移)。所以可以再打一层循环枚举上一层的状态。那如何判断这三个问题呢?使用位运算就可以了。 首先看第一个,要判断一个二进制数当中是否有相邻的 $1$。我们知道按位与可以找到两个数同一个位置上同时存在的 $1$,那我们换个思考方向,如果把这个数本身左移或者右移一位呢?此时这两个数要是再按位与之后结果还是不为 $0$,那么就说明这两个数至少有一个地方有同一个位置上同时存在 $1$。而因为其中一个数是由另一个数左移/右移过来的,也就是说明至少有一个位置上的 $1$ 左边/右边有一个相邻的 $1$,也就是说有两个 $1$ 相邻,则不符合条件。 举个例子:100101100101 左移一位后按位与: 1234 100101100101100101100101--------------0000001000000 可以看到结果不为 $0$,则说明这个状态是不合法的。实际上,确实有两个 $1$ 连在一起了。 再来看第二个。要判断两个数是否有同一个位置上同时存在 $1$,这个很简单,甚至不需要任何处理,直接把两个数按位与一下即可,要是结果不为 $0$ 则代表状态不合法。 最后看第三个。其实我们检查的是一个二进制数的 $\\textbf{1}$ 的集合是否完全包含另一个二进制数的 $\\textbf{1}$ 的集合(这里的“集合”是指 $1$ 的位置的集合)。那么我们可以把两个数按位或,再检查按位或之后的结果是否等于集合范围更大的那个数。如果不相等,则代表大集合不能完全包含小集合,不合法。(思考一下,为什么?) 确定好了三个条件之后,我们终于可以愉快地转移状态了。 dp_{i,j}=\\sum\\limits_{k=0}^{2^n-1}dp_{i-1,k}(当然计算和的时候要满足三个条件了才能加。) 初始化 $dp_{0,0}=1$。个人觉得状压 DP 的初始化是个很玄学的东西。 时间复杂度 $\\Theta(n2^n)$。 Code: 123456789101112131415161718192021222324252627282930313233#include <cstdio>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = 5101, mod = (int)1e8;int n, m, N, x, ans, num[15], dp[15][maxn];int main() { scanf("%d %d", &n, &m); N = (1 << m) - 1; rep(i, 1, n) { rep(j, 1, m) { scanf("%d", &x); num[i] = (num[i] << 1) + x; // 把给出的矩阵转换成 n 个二进制数方便判断 } } dp[0][0] = 1; // 初始化 rep(i, 1, n) { // 第 i 行 rep(j, 0, N) { // 第 i 行的状态 if(j & (j << 1) || (j | num[i]) ^ num[i]) // 这里想写成 != 也可以,想写成异或也行,看大家的习惯咯 continue; rep(k, 0, N) { // 第 i-1 行的状态 if(!(k & j)) dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod; // 状态转移 } if(i == n) ans = (ans + dp[i][j]) % mod; // 计算答案,也可以拎出来单独写循环 } } printf("%d", ans); return 0;} B. 最小总代价 题意:$n$ 个人传物品,从任意一个人开始,每个人只能接一次物品。每两个人之间传物品都要付出一定的代价,求把物品传给所有的人的最小代价和。$2\\le n\\le 16$。 PS. 这道题我写的是 $\\Theta(n^22^n)$,但其实有更优的 $\\Theta(n2^n)$ 算法。不过没关系,因为状压的解题思路都差不多,那种思路我会在后面介绍,所以也可以看一看我当初的想法。 $dp_{i,j,k}$ 表示目前传到的人数的个数为 $i$,状态为 $j$(从低位往高位数第 $i$ 位代表第 $i$ 个人是否已经被传到),且东西是第 $k$ 个人给出的(给谁了不知道)时的最小代价和。所以很明显这个 $i$ 是不需要的 qwq。 这里注意一下,上一道题是从高位到低位,这道题是从低位到高位,这是题目给法不一样造成的结果。上一道题题目是直接给出了我们需要使用的矩阵,那么从左往右遍历比较方便,而这道题我们需要知道在传递之前到传递之后状态的变化(而不是通过枚举得到两个状态,详情见后文),所以左移多少位肯定是从低位往高位写比较舒服。不管怎么样,我们写代码都是为了思考方便、写起来方便而定义的,当然如果两样都需要,我们就要考虑考虑如何处理这个冲突了。 在转移的时候,还需要加一层循环,枚举是谁收到了 $k$ 给的物品,这样才可以确定加上的代价。 至于转移过程中还有一些剪枝或者判断,只有合法的情况才能继续下一层循环。这个过程可以看代码。 转移状态: dp_{i,j|2^{k-1},k}=\\min\\limits_{l=1}^{n}\\{dp_{i-1,j,l}+a_{k,l}\\}初始化 $dp$ 极大值,$dp_{1,0\\sim 2^n-1,1\\sim n}=0$。 Code: 123456789101112131415161718192021222324252627282930313233343536373839404142434445#include <cstdio>#include <cstring>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (1 << 16) + 5;inline int min(int x, int y) { return x < y ? x : y; }int n, N, ans = 1 << 30, a[20][20], dp[20][maxn][20];inline int num(int x) { // 这个函数用来统计一个数的二进制中有多少个 1 int s = 0; while(x) { s += (x & 1); x >>= 1; } return s;}int main() { memset(dp, 0x3f, sizeof(dp)); memset(dp[1], 0, sizeof(dp[1])); scanf("%d", &n); N = (1 << n) - 1; rep(i, 1, n) rep(j, 1, n) scanf("%d", &a[i][j]); rep(i, 2, n) { // 收到的是第几个人 rep(f, 1, n) { // 从谁那里传过来的 rep(j, 0, N) { // 上一个状态 if(!(j & (1 << f - 1)) || num(j) != i - 1) // 判断此状态是否和 i、k 贴合 continue; rep(k, 1, n) { // 谁收到了 if(!(j & (1 << k - 1)) && f != k) dp[i][j | (1 << k - 1)][k] = min(dp[i][j | (1 << k - 1)][k], dp[i - 1][j][f] + a[f][k]); if(i == n) ans = min(ans, dp[i][j | (1 << k - 1)][k]); } } } } printf("%d", ans); return 0;} C. 项链 题意:有 $n$ 个贝壳和 $m$ 组贝壳能连接的关系,每一组关系形如 $a_i,b_i$ 代表第 $a_i$ 和第 $b_i$ 个贝壳可以连接。项链是首尾相接的,而且要求用上所有的贝壳。求组成项链的方案数量。多组数据,$1\\le T\\le 5$,$1\\le n\\le 18$。 这道题打 $\\Theta(n^22^n)$ 会 T + M 到飞起,所以还是老老实实打 $\\Theta(n2^n)$ 吧。 因为项链是环状,所以哪一个贝壳在第一个都无所谓。既然如此,我们不妨让第一个贝壳为首,只要最后一个贝壳可以和它相连就可以了。这样我们就把环搞成了链。 接着,$dp_{i,j}$ 表示目前的最后一个贝壳是第 $i$ 个,状态为 $j$(这次还是从低到高)时的方案总数量。状态转移时枚举上一个贝壳是哪一个,如果两个贝壳可以连接就加上。都是套路。 dp_{i,j}=\\sum\\limits_{k=1}^n dp_{k,j-2^i}初始化 $dp_{0,0}=1$。 记得要开 long long。 Code: 12345678910111213141516171819202122232425262728293031323334353637383940414243#include <cstdio>#include <cstring>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)const int maxn = (1 << 18) + 5;int n, m, u, v;ll dp[20][maxn];bool b[20][20];int main() { while(scanf("%d %d", &n, &m) != EOF) { memset(dp, 0, sizeof dp); memset(b, 0, sizeof b); int N = (1 << n) - 1; rep(i, 1, m) { scanf("%d %d", &u, &v); --u, --v; b[u][v] = b[v][u] = 1; } dp[0][0] = 1; rep(i, 0, n - 1) b[i][i] = 1; rep(j, 0, N) { rep(i, 0, n - 1) { if(!(j & (1 << i))) continue; rep(k, 0, n - 1) { if(j & (1 << k) && b[i][k]) dp[i][j] += dp[k][j - (1 << i)]; } } } ll ans = 0; rep(i, 0, n - 1) { if(b[i][0]) ans += dp[i][N]; } printf("%lld\\n", ans); } return 0;} PS. 话说,你们有没有发现其实状压从 $0$ 开始貌似更好操作一些,因为二进制的最低为代表的是 $2^0$ 而不是 $2^1$ 嘛。比如这道题,我之前是从 $1$ 开始的死活过不了,结果改成从 $0$ 开始就神奇地过了 (XSC062:说啥呢,还不是老子给你改的),只可惜我从 $1$ 开始写习惯了,所以一般来说只要能过我都还是从 $1$ 开始…… D. 国王其实就是互不侵犯,鬼知道为啥要改名。 题意:在 $n\\times n$ 的棋盘上放 $k$ 个国王,国王可攻击相邻的 $8$ 个格子,求使它们无法互相攻击的方案总数。 这道题因为规定了个数为 $k$ 个,所以除了状压需要的第 $i$ 行和状态为 $j$ 之外(这次 $j$ 从高到低和从低到高没有影响,可以自己想一想为什么 qwq),还需要一维确定目前已经有的个数。 $dp_{i,j,k}$ 代表前 $i$ 行,第 $i$ 行状态为 $j$,已经放了 $k$ 个国王时的方案总数量。 判断是否相邻方法和 A 题类似,都是用位运算。这道题多了四个角落的格子,其实很好解决,只需要把上一行左移一位按位与,右移一位按位与,看结果是不是 $0$ 即可(原理也在 A 当中说了)。 状态转移: dp_{i,j,k}=\\sum\\limits_{l=0}^{2^n-1}dp_{i-1,l,k-\\text{numone}(l)}($\\text{numone}$ 指一个二进制数中 $1$ 的个数,所以 $numone$ 可以预处理或者是打一个函数。) Code: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869#include <cstdio>#include <cctype>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)namespace IO { inline int read() { int x = 0, w = 0; char ch = 0; while(!isdigit(ch)) { w |= ch == '-'; ch = getchar(); } while(isdigit(ch)) { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return w ? -x : x; } inline void write(ll x) { if(x < 0) putchar('-'), x = -x; if(x > 9) write(x / 10); putchar(x % 10 + '0'); }}using namespace IO;const int maxn = 1050;int n, k;ll ans, dp[15][105][maxn];inline int num(int x) { int s = 0; while(x) { s += x & 1; // 末位是不是 1 x >>= 1; // 右移一位,把判断过的末位扔掉 } return s;}int main() { n = read(), k = read(); if(k > (n * n) >> 1) { putchar('0'); return 0; } int N = (1 << n) - 1; dp[0][0][0] = 1; rep(i, 1, n) { rep(j, 0, k) { rep(x, 0, N) { if(x & (x << 1) || j < num(x)) continue; rep(y, 0, N) { // 枚举上一行的状态 if(j >= num(y) && !(x & y) && !(y & (y << 1)) && !(x & (y << 1)) && !(x & (y >> 1))) // 判断条件 dp[i][j][y] += dp[i - 1][j - num(y)][x]; } } } } rep(i, 0, N) ans += dp[n][k][i]; // 统计答案 write(ans); return 0;} PS. 有一个点需要大家注意,就是打状压的时候一定要弄清楚每一层循环的变量代表的是啥,状态转移的时候需要注意,尤其是同一种类型的变量!不然打错了很难调出来。 E. Hie with the Pie 题意:一个有 $n+1$ 个点的有向完全图,结点依次编号为 $0,1,\\ldots,n$,给出其邻接矩阵(注意从 $i$ 到 $j$ 的距离不一定等于从 $j$ 到 $i$ 的距离)。请求出从 $0$ 号点出发,走过 $1$ 到 $n$ 号点至少一次,然后再回到 $0$ 号点的最短路。$1\\le n\\le 10$。 注意到每个点都必须走,于是想到状压。 $dp_{i,j}$ 表示从 $0$ 出发,到了 $i$ 点,且状态为 $j$ 时的最短路。状态转移时,枚举上一个经过的点为 $k$,此时我们发现状态转移需要知道任意两个点 ($i$ 和 $k$)之间的最短路,所以我们先跑一遍 Floyd 预处理出任意两个点之间的最短路再转移,方程如下: dp_{i,j}=\\min\\limits_{k=1}^n\\{dp_{k,j-2^{i-1}}+f(i,k)\\}然鹅,这道题难在于细节。 首先注意循环几层的顺序,外层必须是 $j$,因为 $j$ 的转移是从小到大的转移,而 $i$ 和 $k$ 都是无序的,为了保证 dp 的无后效性必须先枚举 $j$。 某搜索大佬:关我什么事。 其次注意答案的求法,我们不能直接枚举 $\\min\\{dp_{i,2^n-1}\\}$,因为题目要求我们必须返回 $0$ 点,所以还得加上一个 $i$ 返回 $0$ 的最短路,即求 $\\min\\{dp_{i,2^n-1}+f(0,i)\\}$。 Code:(长得和题解有点不一样,凑合着看吧 qwq) 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950#include <cstdio>#include <cstring>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (1 << 11) + 5;int n, G[15][15], dp[15][maxn], f[15][15];inline int min(int x, int y) { return x < y ? x : y; }inline void Floyd() { rep(i, 1, n) rep(j, 1, n) f[i][j] = G[i][j]; rep(k, 1, n) rep(i, 1, n) rep(j, 1, n) f[i][j] = min(f[i][j], f[i][k] + f[k][j]); return;}int main() { while(scanf("%d", &n) != EOF) { memset(dp, 0x3f, sizeof(dp)); ++n; int N = (1 << n) - 1; rep(i, 1, n) rep(j, 1, n) scanf("%d", &G[i][j]); dp[1][1] = 0; Floyd(); rep(k, 0, N) { rep(i, 1, n) { if(!(k & (1 << i - 1))) continue; rep(j, 1, n) { if(i == j || !(k & (1 << j - 1))) continue; dp[i][k] = min(dp[i][k], dp[j][k ^ (1 << i - 1)] + f[j][i]); } } } int ans = 1 << 30; rep(i, 1, n) ans = min(ans, dp[i][N] + f[i][1]); printf("%d\\n", ans); } return 0;} F. Traveling 题意:一个人要去 $N$ 个城市旅游,他可以从任意城市开始,城市之间有 $m$ 个道路,每个道路所花费的费用不用,求解出遍历所有城市,且每个城市去过的次数不超过两次的最小花费。$1\\le N\\le 10$。 肉眼可见这题是个三进制的问题。不同于二进制,三进制没有系统内置的位运算,我们该怎么处理呢? 我们可以开两个数组进行预处理:$mi3$ 和 $num3$。$mi3_i$ 的值为 $3^i$,$num3_{i,j}$ 代表 $i$ 这个数的三进制的第 $j$ 位是 $0,1$ 还是 $2$。为了方便思考,$num3_i$ 按照高位到低位从左到右排。 比如一个数 $15$,三进制为 $121$,那么 $num3_{15}$ 如下: 123num3[15]:下标: 0 1 2 3 4 5 6 7 8 9 10权值: 0 0 0 0 0 0 0 0 1 2 1 因为 $N\\le 10$,所以这个数最多也就到 $3^{10}-1$,再多开一位,下标范围就是 $0\\sim 10$。 至于使用方法嘛……往下看! 预处理工作做完后,输入图。为了方便,我们还是以 $0\\sim n-1$ 编号城市。 接下来就是 dp: $dp_{i,j}$ 代表到了第 $i$ 个城市之后状态为 $j$ 的最小代价,这里 $j$ 从低位到高位表示的城市编号依次递增。 接着再套一层循环,枚举上一步是从 $k$ 城市到 $i$ 城市,计算就可以了。emm,注意还是要先跑一遍 Floyd。 dp_{i,j}=\\min\\limits_{k=0}^{n-1}\\{dp_{k,j-3^i+f(i,k)}\\}状态转移方程很简单,相信大家都会,但是有几个细节我错了很久: 状态转移时几层循环的顺序。注意到状态转移的时候,只有 $j$ 是一直在增加的,$i$ 和 $k$ 都是乱的。所以第一层循环应该是 $j$,其次是 $i$,最后是 $k$。 因为此处 $j$ 是按照从低位到高位从右到左的顺序排,但是预处理是从高位到低位从左到右,所以可以注意到状态转移时可以直接使用 $3^i$,但是在判断的时候需要特别注意是 $3^{i/k}$ 还是 $3^{10-i/k}$。 我在调这道题的时候 mjl 叫我写注释,所以我写了一个比较详细的注释,大家可以参考。 感谢 XSC062 救了我一命!不过这个代码我不知道是有 UB 还是什么,使用 C++17(Clang) 可以 AC,而使用 C++14(GCC8) 会 WA $\\text{5pts}$,如果有大佬愿意帮我看一下是怎么回事,那也是极好的。 Code: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#include <cstdio>#include <cstring>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = 15;const int N3 = 6e4 + 5; // 3 ^ 10int n, m, f[maxn][maxn], dp[maxn][N3];int mi3[maxn], num3[N3][maxn];bool b[N3];inline int min(int x, int y) { return x < y ? x : y; }int main() { mi3[0] = 1; rep(i, 1, 10) mi3[i] = mi3[i - 1] * 3; rep(i, 0, N3) { int x = i, s = 10; while(x) { num3[i][s] = x % 3; x /= 3; --s; } } while(~scanf("%d %d", &n, &m)) { rep(i, 0, N3) b[i] = 1; rep(i, 0, N3) { rep(j, 10 - n + 1, 10) b[i] &= num3[i][j] > 0; } memset(f, 0x3f, sizeof(f)); memset(dp, 0x3f, sizeof(dp)); rep(i, 0, n - 1) dp[i][mi3[i]] = 0; int u, v, w; rep(i, 1, m) { scanf("%d %d %d", &u, &v, &w); --u, --v; f[u][v] = f[v][u] = min(f[u][v], w); } rep(j, 0, mi3[n] - 1) { // j 表示到了 i 之后的状态,三进制表示下最低位代表 0 城市(3^0),倒数第二位表示 1 城市(3^1),i 城市即为 3^i rep(i, 0, n - 1) { // 目前到了第 i 个城市 if(!num3[j][10 - i]) // 如果 j 表示 i 去过的位数为 0 则说明没去过 i,不合法 continue; // 这里是 10 - i 是因为 num3 数组是从左到右编号递增,但是 j 是从右往左依次递增,所以要转换一下 rep(k, 0, n - 1) // 从 k 城市到的 i 城市 if(num3[j][10 - k] > 0) // j 当中也必须经过至少一次 k 才合法,10 - k 同理 dp[i][j] = min(dp[i][j], dp[k][j - mi3[i]] + f[i][k]); // 状态转移,之前的状态少去了一次 i 所以要减 3^i } } int ans = 1 << 30; rep(j, 0, n - 1) rep(i, 0, mi3[n] - 1) if(b[i]) ans = min(ans, dp[j][i]); // 如果所有城市都去过就统计答案 printf("%d\\n", ans == 0x3f3f3f3f ? -1 : ans); } return 0;} H. 炮兵阵地 题意:有 $N\\times M$ 的地区,每一个可能是山地(H)或者平原(P),只有平原才能有炮兵。每个炮兵的攻击范围是上下左右两个格子。给出地形图,求炮兵不相互攻击时最多能部署的炮兵数量。$1\\le N\\le 100,1\\le M \\le 10$。 别问我 G 去哪了,问就是还没做出来。 这题就是一个 A 和 D 的缝合怪,确定一个攻击范围,然后山地上不能有炮兵就像 A 的有些格子不能种草一样。因为上下左右是两格,所以状态转移的时候不仅要枚举上一行的状态,还要枚举上上一行的状态,这么搞不 TLE&MLE 才怪呢。 怎么解决呢,其实有一个办法,就是我们先把所有满足条件的一行的排列 dfs 出来,通过实验我们发现最多也就只有 $60$ 种符合要求的排列,所以枚举状态的时候每一层最多就 $60$,就不会时间内存双爆炸啦。 注意一下这里是求最多能放多少个炮兵,所以两层状态的答案是要加起来取 $\\max$ 而非乘起来。今天也是把加法原理和乘法原理弄混的一天呢。 其他就没什么好说的了。 Code: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374#include <cstdio>#include <cstring>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)char a[105][105];int n, m, N, tot, num[105], qn[65], sum[65], dp[105][65][65];bool b[105], q[65][105];inline int max(int x, int y) { return x > y ? x : y; }void dfs(int t, int ls) { // 求满足条件的所有一行的情况,两个炮兵之间至少相隔两个格子,不考虑山地和平原 if(t == m + 1) { ++tot; rep(i, 1, m) q[tot][i] = b[i]; return; } if(ls > 2) { b[t] = 1; dfs(t + 1, 1); b[t] = 0; } dfs(t + 1, ls + 1); return;}inline int getnum(int x) { int s = 0; while(x) { s += x & 1; x >>= 1; } return s;}int main() { scanf("%d %d", &n, &m); N = (1 << m) - 1; dfs(1, 3); rep(i, 1, tot) rep(j, 1, m) qn[i] = (qn[i] << 1) + q[i][j]; rep(i, 1, tot) sum[i] = getnum(qn[i]); rep(i, 1, n) { scanf("\\n"); rep(j, 1, m) scanf("%c", &a[i][j]); } rep(i, 1, n) rep(j, 1, m) num[i] = (num[i] << 1) + (a[i][j] == 'H' ? 0 : 1); rep(i, 1, n) { rep(j, 1, tot) { if((qn[j] | num[i]) ^ num[i]) // 山地上不能有炮兵,此处判断和 A 题相似 continue; rep(k, 1, tot) { if(qn[k] & qn[j]) continue; rep(l, 1, tot) if(!(qn[l] & qn[j])) dp[i][j][k] = max(dp[i][j][k], dp[i - 1][k][l] + sum[j]); } } } int ans = 0; rep(i, 1, tot) rep(j, 1, tot) ans = max(ans, dp[n][i][j]); printf("%d", ans); return 0;} O. 集合选数 题意:有一种集合,若 $x$ 在集合中,则 $2x$ 和 $3x$ 都不能在集合中。对于任意一个正整数 $n$,求出 ${1, 2,\\ldots, n}$ 这个集合的满足上述约束条件的子集的个数,结果对 $10^9+1$ 取模。$1\\le n\\le 10^5$。 这思路,老师不讲真的可以想到吗! 这题乍一看没什么头绪,咱要不先列个表: \\begin{matrix} 1 & 2 & 4 & 8 &\\cdots\\\\ 3 & 6 & 12 & 24 &\\cdots\\\\ 9 & 18 & 36 & 72 &\\cdots \\\\ \\cdots \\end{matrix}观察一下,思考一下:当选 $1$ 的时候,$2,3$ 不能选;选 $2$ 的时候,$1,4,6$ 不能选;选 $6$ 的时候,$2,3,12,18$ 不能选…… 有没有发现什么? 这不就是在这个矩阵中不能取相邻的数吗! 那我们就构造一个这样的矩阵,左上角数字为 $1$,最上面一行为 $2$ 的幂次,最左边一行为 $3$ 的幂次,剩下的数就由那一个位置上对应的 $2$ 的幂次和 $3$ 的幂次相乘,然后按照 A 题的求法求是不是就可以了? 想到这一层,我们就已经成功了一半。接下来的一半,还得看几个细节: 一、有些数,比如 $5$,貌似不在这个矩阵里面。 这个东西解决办法也不难,如果找到有数字不在之前列到过的矩阵里面,我们只需要以这个数为左上角,然后再构造矩阵: \\begin{matrix} 5(1\\times 5) & 10(2\\times 5) & 20(4\\times 5) & 40(8\\times 5) &\\cdots\\\\ 15(3\\times 5) & 30(6\\times 5) & 60(12\\times 5) & 120(24\\times 5) &\\cdots\\\\ 45(9\\times 5) & 90(18\\times 5) & 180(36\\times 5) & 360(72\\times 5) &\\cdots \\\\ \\cdots \\end{matrix}没错,我们只需要在刚才那个矩阵的基础上,把每个数都乘以左上角那个数,就可以了! 用一个 bool 数组存一下每个数字是否已经存在,在构造矩阵的过程中,遇到一个数就把它标记为已经出现过。求解答案时依次枚举 $1\\sim n$,如果发现有数没有枚举到,就以这个数为左上角构造矩阵再求解。 由于各个矩阵之间没有什么关系,所以方案的选择是任意的,即每个矩阵得出的答案相乘。 另外,需要注意一下,如果你和我一样是按照先构造第一行和第一列,再相乘构造整个矩阵的话,记得两数相乘时需要除以左上角那个数,因为相乘的时候两边系数都算了一次,需要去重。 二、矩阵的大小? 构造第一行和第一列的时候,肯定是到 $n$ 就结束了。但是相乘的时候,仍然会有数大小超过 $n$,那么这些超过 $n$ 就不能选,类似于 A 里面有些格子里不能种草,这个用一个 bool 数组标记即可。 因为 $n$ 最多为 $10^5$,$\\log_2(10^5)\\approx 17,\\log_3\\approx 12$,所以矩阵的大小不会超过 $17\\times 12$,数组没有必要开太大。 另外,因为 $dp$ 数组等大小比较大,大家一定要注意不能用 memset,直接需要清空多少就清空多少,不然会 T 飞。当然,如果您是 $90$ 分的话,打表也是一个不错的选择。 Code: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980#include <cstdio>#include <cstring>#define ll long long#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (1 << 17) + 5;const int N = 100000;const int mod = 1000000001;int n, x2[20], x3[20], _x2, _x3, __x2, __x3, num[20], dp[20][maxn];ll a[20][20];bool b[20][20], gz[maxn];inline void Make(int o) { // 构造矩阵,一个横杠是矩阵的长宽,两个横杠是权值 _x2 = 0, _x3 = 0, __x2 = o, __x3 = o; while(__x2 <= n) x2[++_x2] = __x2, __x2 <<= 1; while(__x3 <= n) x3[++_x3] = __x3, __x3 = (__x3 << 1) + __x3; rep(i, 1, _x2) a[1][i] = x2[i], gz[x2[i]] = 1; rep(i, 1, _x3) a[i][1] = x3[i], gz[x3[i]] = 1; rep(i, 2, _x3) rep(j, 2, _x2) a[i][j] = a[1][j] * a[i][1] / o; rep(i, 1, _x3) rep(j, 1, _x2) { b[i][j] = (a[i][j] <= n); if(b[i][j]) gz[a[i][j]] = 1; } return;}inline int Solve() {// memset(dp, 0, sizeof(dp));// memset(num, 0, sizeof(num)); int ans = 0, q = (1 << _x2) - 1; rep(i, 1, _x3) rep(j, 0, q) dp[i][j] = 0; rep(i, 1, _x3) num[i] = 0; rep(i, 1, _x3) rep(j, 1, _x2) num[i] = (num[i] << 1) + b[i][j]; dp[0][0] = 1; rep(i, 1, _x3) { rep(j, 0, q) { if(j & (j << 1) || (j | num[i]) ^ num[i]) continue; rep(k, 0, q) { if(!(k & j)) dp[i][j] = (dp[i][j] + dp[i - 1][k]) % mod; } } } rep(i, 0, q) ans = (ans + dp[_x3][i]) % mod; return ans;}int main() { scanf("%d", &n); if(n == 100000) { printf("964986022"); // 打表出奇迹 return 0; } ll s = 1; rep(i, 1, n) { if(gz[i]) continue; Make(i); s = (s * Solve()) % mod; } printf("%lld", s); return 0;}","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"CF899E Segments Removal 题解","slug":"「Solution」CF899E","date":"2022-01-26T22:59:55.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/615100e6/","link":"","permalink":"https://blog.liynw.top/posts/615100e6/","excerpt":"","text":"这是我们考试题,我做了 $75$ 分,最后才发现是元素入队的时候没有赋值…… Problem 有一个长度为 $n$ 的整数数组,对数组执行若干次操作。每一次找到连续相等整数的最长段(如果有多个段长度相同,选择最靠左边的段)并删除它。要求计算经过多少次操作后数组为空。 Solution因为前两天才调完小熊的果篮,记忆犹新,所以立刻想到了队列+链表的做法。然后再读题,发现需要找长度最长的区间,不能用普通队列,要用优先队列。 我们先把所有的区间都找出来,存储每一个区间的左端点,长度(这样就可以算出右端点了)和这个区间数字的值。然后把这些区间全部按照顺序弄到一个双向链表里面,并把需要的值加入优先队列。 详细的讲解在代码注释里。 123456789101112131415161718192021222324// 定义结构体,需要存区间左端点,区间长度和它在链表里的下标struct node { int l, len, id; };std::priority_queue<node> q;// 定义排序规则:区间长度,一样时左边优先bool operator<(node x, node y) { return (x.len != y.len) ? (x.len < y.len) : (x.id > y.id); }// lst 为上一个区间的左端点,因为我们要计算区间长度,所以需要用这个区间和上个区间的左端点算上一个区间的长度,需要存一下int ans = 0, lst = 0; a[0] = a[n + 1] = -1; // 特殊处理头尾端点,以免错误合并rep(i, 1, n) { if(a[i] != a[i - 1]) { // 说明这个数是一个区间的开头 num[++k] = a[i]; l[k] = k - 1; // 初始化链表 r[k] = k + 1; if(k != 1) { // 如果这个区间有上一个,即不是第一个 q.push(node({lst, i - lst, k - 1})); // 元素入队 L[k - 1] = lst, LEN[k - 1] = i - lst; // 记录区间左端点和长度 } lst = i; // 更新上一个区间的左端点 }}q.push(node({lst, n - lst + 1, k})); // 最后一个区间也需要处理L[k] = lst, LEN[k] = n - lst + 1;r[0] = 1, l[k + 1] = k; // 链表的初始化 接着就是核心代码了。 我们用一个 bool 数组标记一个区间是否被取过,然后从队列里面不停取元素。 若此区间已经被标记过了就直接跳过。 若此区间没有被取过,就标记一下这个区间,此时又取了一个区间,答案需要 $+1$,然后检查一下它的左右两个区间是否需要被合并;如果需要,就把两个区间合并到它左边那个区间,并标记它右边那个区间。 这句话信息量有点大,是什么意思呢? 说明我们拿到一个没被取过的区间时,需要做这几件事: 把这个区间标记为“已经被取过”。 因为又取了一个区间,所以答案要 $+1$。 把这个区间从链表里删掉。 检查这个被删除的区间的左边的区间(命名为 $l$)和它右边的区间(命名为 $r$)是否需要被合并,也就是说这两个区间的值是不是一样的,如果是一样的,那这个区间被取了之后,$l$ 和 $r$ 就变成了一个区间,所以需要被合并。 合并两个区间的时候,可以把 $l$ 的长度改为两个区间的长度相加,然后把 $r$ 删掉。需要注意 $r$ 也要被标记。 把更新的 $l$ 加入队列。 这个时候就会有小朋友问了:此时队列里还有原来的 $l$,是不是需要删掉? 答案是不需要。因为优先队列的排序规则是按照长度从大到小排的,所以更新后的 $l$ 一定会比原来的 $l$ 先取出,取出之后我们就标记了 $l$,也就不会重复取到了。 给一下代码。 1234567891011121314151617181920212223inline void remove(int x) { id_use[x] = 1; // 实现时可以把标记的代码放在删除的函数里面 r[l[x]] = r[x]; l[r[x]] = l[x]; return;}while(!q.empty()) { while(!q.empty() && id_use[q.top().id]) // 过滤掉已经被标记过的点 q.pop(); if(q.empty()) // 如果队列被取空了就直接跳过 break; node u = q.top(); q.pop(); ++ans; // 更新答案 if(num[r[u.id]] == num[l[u.id]]) { // 如果 l 和 r 需要被合并 LEN[l[u.id]] += LEN[r[u.id]]; // 赋值,注意这里不写只能得 75 分 q.push(node({L[l[u.id]], LEN[l[u.id]], l[u.id]})); // l 入队 remove(r[u.id]); // 删除 r } remove(u.id); // 把这个区间删除}write(ans - 1); // 这里 -1 是因为头和尾会被错误合并,所以会多一次 大概就是这样了吧。因为核心代码已经给出,所以不再给完整代码了。 另外就是祝贺一下 ljt 考试的时候终于没有写挂快读快写了。","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"链表","slug":"链表","permalink":"https://blog.liynw.top/tags/%E9%93%BE%E8%A1%A8/"}]},{"title":"2021.12.24 考试总结","slug":"「ExamSummary」20211224","date":"2022-01-26T22:58:31.000Z","updated":"2022-02-01T10:10:08.000Z","comments":true,"path":"posts/894fc8e0/","link":"","permalink":"https://blog.liynw.top/posts/894fc8e0/","excerpt":"","text":"T1正解是单调队列,时间复杂度 $\\Theta(n)$。 我们先把数组复制 $3$ 遍。等等,为什么不是两遍呢? 因为最大值有变化,某首歌是否满足条件的数据在变化,所以可能出现第一次可以播放,但第二次不行的情况。平时我们是复制两遍,所以这里要多复制一遍。 然后从第一首歌开始,一次遍历被复制了三遍的数组,维护一个单调递减的队列。当然,里面存的是数组下标,下标当然不是单调递减的,但是代表的喜爱值是递减的。 我们以样例举例: 3 2 5 3 123i = 1 2 3 4 5 6 7 8 9 10 11 12a = 3 2 5 3 3 2 5 3 3 2 5 3ans = 0 0 0 0 0 0 0 0 0 0 0 0 每次遍历到一个数,依次进行如下操作: 第一步:判断队首元素是否符合标准,当前遍历到的元素 $i$ 是否满足要求,即是否有 $ai<\\dfrac{a{q.\\text{front()}}}{2}$。如果是,那么计算队首元素的答案(从它开始最多可以放多少首歌,即为当前遍历到的歌曲编号减去队首元素的编号)并把队首元素弹出。 注意队首可能不止一个元素不满足要求,需要连续弹出。 比如我们遍历到了 $6$ 号,然后单调队列现在长这样: front {3, 4, 5} back 也就是 front {5, 3, 3} back 本来没什么问题,但是注意到 $2<\\frac{5}{2}$,所以如果播放了 $3$ 号歌曲,就不能播放 $6$ 号歌曲了。那么,从 $3$ 号歌曲开始播放最多能播放 $6-3=3$ 首歌,也就是 $3,4,5$ 这三首。12345 v i遍历到这儿了i = 1 2 3 4 5 6 7 8 9 10 11 12a = 3 2 5 3 3 2 5 3 3 2 5 3ans = 0 0 3 0 0 0 0 0 0 0 0 0 ^答案被更新 第二步:把遍历到的元素放进单调队列。当然,为了保持单调,也许需要弹出队尾的一些元素。 其实这也是为什么不能用队列长度来更新答案的原因。有些数可能会被弹掉,此时直接用队列长度判断答案可能会遗漏。 遍历完数组之后就可以输出答案了,但是有个问题: 比如样例遍历完之后 ans 数组长这样: 0 0 3 0 0 0 3 0 0 0 3 0 那 $1,2,4$ 号怎么办呢? 其实这个很简单,你想一下,就以 $2$ 号歌曲举例,你先听一首到 $3$ 号歌曲,然后再按照 $3$ 号的答案计算不就行了吗?所以这里我们再倒序遍历一遍数组,如果某个下标的答案没被更新,就把答案更新为 在它后面的最近的一个本来就有答案的值 + 当前遍历到的下标和那个数的下标之差。 给出代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546#include <cstdio>#include <deque>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)3e5 + 5;int n, N, a[maxn], ans[maxn];std::deque<int> q;inline void file() { freopen("playlist.in", "r", stdin); freopen("playlist.out", "w", stdout); return;}int main() { file(); scanf("%d", &n); rep(i, 1, n) { scanf("%d", &a[i]); a[i + n] = a[i + (n << 1)] = a[i]; } N = 3 * n; rep(i, 1, N) { while(!q.empty()/*防止 RE*/ && (a[i] << 1) < a[q.front()]) { ans[q.front()] = i - q.front(); q.pop_front(); } while(!q.empty() && a[q.back()] < a[i]) q.pop_back(); q.push_back(i); } int tot = 0, lst = N + 1; dep(i, N, 1) { if(ans[i]) { tot = 0; lst = i; } else ans[i] = ans[lst] + tot; ++tot; } rep(i, 1, n) printf("%d ", (ans[i] < n << 1) ? ans[i] : -1); return 0;} T2二分很明显,关键是 $\\text{check}$ 函数怎么写。 这里我们用 dp:$dp_{i,j}$ 代表用了 $i$ 次红色,$j$ 次绿色最多能覆盖到的神坛的编号(注意这个编号及以前的所有神坛都需要被覆盖)。 但是有一个问题:$r,g$ 的范围是 $10^9$,这样难道不会炸掉吗? 这里需要注意一下,因为 $n\\le 2000$,所以只要 $r+g\\ge 2000$,答案就一定是 $1$,直接输出即可。 那么我们就人为地把 $r,g$ 的范围降到了 $2000$。然后就可以开始愉快的 $\\Theta(n^2)$ 啦!(口胡) 首先我们要想一下这个状态转移方程。假设我们这一次要用红色,那么我们要依托 $dp_{i-1,j}$ 的值,假设它为 $k$。我们已经覆盖到了第 $k$ 个神坛,所以,我们可以忽略第 $k$ 个和第 $k+1$ 个之间的那些空的坐标,直接把第 $k+1$ 个神坛作为左端点。 那右端点可以覆盖到哪里呢?这个需要预处理一下。我们设 $R_i$ 代表用红色的线段(长度为 $L$,就是我们需要 $\\text{check}$ 的那个数),把第 $i$ 个神坛作为左端点,右端点能够覆盖的最大神坛编号,这个数是固定的。 至于怎么预处理,因为 $n$ 很小,$\\Theta(n)$ 和 $\\Theta(n^2)$ 都可以用。$\\Theta(n)$ 就是用两个指针,先全都指着 $1$,然后一点一点往右走,保证两个指针之间的区间长度不超过 $L$ 但是最长,这样计算答案就可以了。 绿色同理,只不过是把 $L$ 换成 $2\\times L$,$R$ 换成 $G$。 状态转移方程: dp_{i,j}=\\max\\{R_{dp_{i-1,j}+1},G_{dp_{i,j-1}+1}\\}当然,如果你直接这么写的话会愉快的 WA 掉。 注意初始化:$dp$ 极小值,$dp_{0,0}=0$,且枚举需要从 $0$ 开始(注意有 $0$ 的时候需要特殊判断)。 然后还是 WA…… 注意状态转移方程,这个方程可能会访问到 $R/G_{n+1}$,所以我们还需要一句:$G_{n+1}←n,R_{n+1}←n$。 然后就没了。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172#include <cstdio>#include <cstring>#include <algorithm>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = 2005;int n, r, g, a[maxn];int R[maxn], G[maxn], dp[maxn][maxn];inline void file() { freopen("light.in", "r", stdin); freopen("light.out", "w", stdout); return;}inline bool check(int L) { memset(dp, -0x3f, sizeof(dp)); dp[0][0] = 0; int p = 1, q = 1; while(p <= n) { while(q <= n && a[q] - a[p] + 1 <= L) ++q; R[p] = q - 1; ++p; } p = 1, q = 1; while(p <= n) { while(q <= n && a[q] - a[p] + 1 <= L << 1) ++q; G[p] = q - 1; ++p; } G[n + 1] = R[n + 1] = n; rep(i, 0, r) { rep(j, 0, g) { if(!i && !j) continue; else if(!i) dp[i][j] = G[dp[i][j - 1] + 1]; else if(!j) dp[i][j] = R[dp[i - 1][j] + 1]; else dp[i][j] = std::max(R[dp[i - 1][j] + 1], G[dp[i][j - 1] + 1]); } } return dp[r][g] >= n;}int f(int l, int r) { if(l == r) return l; int mid = (l + r) >> 1; if(check(mid)) return f(l, mid); return f(mid + 1, r);}int main() { file(); scanf("%d %d %d", &n, &r, &g); if(r + g >= n) { printf("1"); return 0; } rep(i, 1, n) scanf("%d", &a[i]); std::stable_sort(a + 1, a + n + 1); printf("%d", f(1, 1000000000)); return 0;} T3思维题诶!全场 AC 人数最少的一道题。 首先看数据范围,$1\\le n\\le 10^9$,肯定不能用 $n$ 来枚举 (毕竟 mjl 测评不开 O2)。 那我们再思考一下,发现 $1\\le m\\le 10^5$,可以用 $m$ 来枚举。所以我们可以求一下:以每一个公共牌作为顺牌中第一张公共牌的方案数。 这样想有一个好处:不用去重。因为只要第一张公共牌不一样,那么两种方案肯定不同;而求一种情况时也不会重复计算。 那么怎么算呢?我们可以求出此时这个顺牌区间左端点能够覆盖到的最小和最大值,然后再减一下不就行了吗。 那这个最值怎么算呢?假设现在需要求的牌编号为 $i$。 先说最小值吧: 首先我们把给出的公共牌排序、去重。众所周知,数组去了重之后不一定和原来数组一样长,所以,我们现在用 $m$ 代表顺牌区间的长度,$k$ 代表去重后公共牌的个数,切勿混淆。 第一点,因为 $i$ 是顺牌序列中的第一张公共牌,所以,顺牌的开头必须比第 $i-1$ 张公共牌的数值更大,即 $a_{i-1}+1$。 第二点,因为要形成连续 $m$ 张顺牌,而个人牌只有 $s$ 张,所以这个连续的序列里必须包含至少 $\\bf{m-s}$ 张公共牌。 而 $i$ 是第一张,所以还要往后继续找至少 $m-s-1$ 张公共牌,而在规定条件下,找得越少,顺牌的开始位置就越靠前,这也是我们要找的最小值。从第 $i$ 张牌开始,往后找 $m-s-1$ 张牌是第 $i+m-s-1$ 张,再往前找开头,往前再找 $m$ 个就可以了。 这两个条件必须同时满足,所以在计算最小值时取这两个的最大值,即: Min=\\max\\{a_{i-1}+1,a_{i+m-s-1}-m+1\\}最大值要简单一些,因为要包含这张牌,所以最多到 $a_i$;而且因为顺牌的个数是 $m$ 张,所以要保证这么多牌,开头不能超过 $n-m+1$。取这两者的最小值即可。 这里需要注意一点,有时最小值可能反而比最大值大,此时是无解的,不要加上一个负数。 代码倒是挺简洁。 1234567891011121314151617181920212223242526272829303132#include <cstdio>#include <algorithm>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e5 + 5;int n, m, s, ans, a[maxn];inline int max(int x, int y) { return x > y ? x : y; }inline int min(int x, int y) { return x < y ? x : y; }inline void file() { freopen("straight.in", "r", stdin); freopen("straight.out", "w", stdout);}int main() { file(); scanf("%d %d %d", &n, &m, &s); rep(i, 1, m) scanf("%d", &a[i]); std::stable_sort(a + 1, a + m + 1); int k = std::unique(a + 1, a + m + 1) - a - 1; rep(i, 1, k - m + s + 1) { int Min = max(a[i - 1] + 1, a[i + m - s - 1] - m + 1); int Max = min(a[i], n - m + 1); ans += max(0, Max - Min + 1); } printf("%d", ans); return 0;} T4先纠正一个错误:DNA 是两条,这玩意儿应该是 RNA 但是又要换一个字母…… 这题的突破口是 $|e|$ 和字母种类都很小,字母只有四种。 我们先写一个函数 $\\text{f(ch)}$,可以把不同的字母转换为不同的下标($eg.\\ A\\to 0,T\\to 1,G\\to 2,C\\to 3$),这样可以使代码更加方便。 然后看一下这个序列,如果要从某一个字母开始周期修改,假设周期的长度是 $len$,某个字符在周期(也就是 $e$)中的位置是 $i$,我们会发现一件有趣的事,如图: 红色的地方即为这个字符会修改到的地方,可以发现这个分部是有规律的,它所有能够修改到的字符的下标 $\\bmod\\ len$ 的结果是一样的。 所以我们可以令 $BIT_{i,ch,j,k}$ 来代表字符为 $ch$,周期长度为 $j$ 且在周期中的位置为 $k$(周期遍历从 $1$ 开始),所有满足这些条件的字符在 $1\\sim i$ 这个范围中的个数。 然后就是板子了。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778#include <cstdio>#include <cstring>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e5 + 5;int n, m, len;char s[maxn], e[15];int BIT[maxn][4][11][11];inline int lowbit(int x) { return x & -x; }inline int min(int x, int y) { return x < y ? x : y; }inline int f(char c) { if(c == 'A') return 0; else if(c == 'T') return 1; else if(c == 'G') return 2; return 3;}inline void update(int x, int k, char c, int p, int id) { int l = f(c); while(x <= n) { BIT[x][l][p][id] += k; x += lowbit(x); } return;}inline int query(int x, char c, int p, int id) { int sum = 0, l = f(c); while(x) { sum += BIT[x][l][p][id]; x -= lowbit(x); } return sum;}inline void file() { freopen("virus.in", "r", stdin); freopen("virus.out", "w", stdout);}int main() { file(); scanf("%s", s + 1); n = strlen(s + 1); rep(i, 1, n) rep(j, 1, 10) update(i, 1, s[i], j, i % j); scanf("%d", &m); int op, x, l, r; char c; rep(i, 1, m) { scanf("%d", &op); if(op == 1) { scanf("%d %c", &x, &c); rep(j, 1, 10) { update(x, 1, c, j, x % j); update(x, -1, s[x], j, x % j); } s[x] = c; } else { scanf("%d %d %s", &l, &r, e + 1); len = strlen(e + 1); int ans = 0; // 这里注意一下应该是 (l + j - 1) % len 而不是 j,因为我们定义的周期遍历是从 1 而非从 l 开始 rep(j, 1, min(len, r - l + 1)) ans += query(r, e[j], len, (l + j - 1) % len) - query(l - 1, e[j], len, (l + j - 1) % len); printf("%d\\n", ans); } } return 0;} 总结这次考试爆炸了,$0+10+0+0$ 差点就保龄了…… T1 我打了一个线段树,结果没有调出来,T2 写了二分+贪心就没管了,T3 和 T4 都没有写出一个像样的代码…… 只能说明思维和码力都太弱了 qwq。 以及,一定不要拘泥于才学过的知识点(话说我 T3 刚开始也打的线段树来着 qwq)。","categories":[{"name":"考试总结","slug":"考试总结","permalink":"https://blog.liynw.top/categories/%E8%80%83%E8%AF%95%E6%80%BB%E7%BB%93/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"数据结构","slug":"数据结构","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"},{"name":"二分答案","slug":"二分答案","permalink":"https://blog.liynw.top/tags/%E4%BA%8C%E5%88%86%E7%AD%94%E6%A1%88/"},{"name":"单调队列","slug":"单调队列","permalink":"https://blog.liynw.top/tags/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/"},{"name":"思维","slug":"思维","permalink":"https://blog.liynw.top/tags/%E6%80%9D%E7%BB%B4/"}]},{"title":"ST 表求解 RMQ 问题","slug":"「Algorithm」ST表","date":"2022-01-26T22:57:12.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/51bfc0ec/","link":"","permalink":"https://blog.liynw.top/posts/51bfc0ec/","excerpt":"","text":"1. RMQ 是啥 RMQ (Range Minimum / Maximum Query)问题是指:对于长度为 $n$ 的数列 $A$,回答若干询问 $\\text{RMQ}(A,i,j)(1\\leq i,j\\leq n)$,返回数列 $A$ 中下标在 $[i,j]$ 里的最小(大)值,也就是说,RMQ 问题是指求区间最值的问题。 全是抄的。 也就是说,RMQ 是一类问题,而不是一类数据结构。 所以暴力(没有初始化,查询$\\Theta(n)$),线段树(初始化和查询都是 $\\Theta(\\log n)$)等都可以解决 RMQ 问题。 那么是否有更♂快的方法呢? 有!就是 ST 表。 2. 概念ST 表通过 DP 的方式,可以实现 $\\Theta(n\\log n)$ 初始化,$\\Theta(1)$ 查询区间最值。 优点:最重要的,快! 缺点:不支持修改数组,而且空间复杂度为 $\\Theta(n\\log n)$,可能会受不了。 3. 实现$dp_{i,j}$ 代表从 $i$ 开始,区间长度为 $2^j$ 的区间,即 $[i,i+2^j)$ 的最值(可以是最大值或者最小值,这个无所谓)。 初始化对于 $\\forall i,j=0$ 时,区间只有 $A_i$ 这一个数字,所以肯定最值是 $A_i$。 dp_{i,0}=A_i(1\\leq i\\le n)状态转移我们知道,对于任意一个长度为 $2$ 的幂次方的区间,都可以划分为长度相等的两部分,例如 $[3,10]$ 就像这样: ($i=3,j=3$) 也可以说明区间 $[i,i+2^j)$ 可以被分为 $[i,i+2^{j-1})$ 和 $[i+2^{j-1},i+2^j)$ 两个部分,每个部分的长度都是 $2^{j-1}$。 那么转移方程不就出来了吗: dp_{i,j}=\\max(or \\min)\\{dp_{i,j-1},dp_{i+2^{j-1},j-1}\\}当然,在代码的实现过程中,我们可以用 1 << x 来代替分析中的 $2^x$,不过需要注意的是,位运算的优先级比四则运算低,需要打括号。 查询对于任意一个区间 $[i,j]$,如何用已经初始化好的 ST 表求解最值呢? 看图: 我们可以把任意一个区间分解成这样两个长度为 $2$ 的幂次的区间,一个区间左端点为待求区间的左端点,另一个区间的右端点为待求区间的右端点,两个区间的长度为所有 $2$ 的幂次中小于 $j-i+1$ 的最大的那一个,转换一下就变成了可以用 $2$ 的幂次表示的 $2^{\\lfloor\\log_2(j-i+1)\\rfloor}$。(PS:$2^{\\log_2(x)}=x$)。 找到两个分解后的区间,求一下最值就行了。 数列区间最大值问题: 12345678910111213141516171819202122232425262728293031323334353637#include <cstdio>#include <cmath>#define rep(i, j, k) for(int i = j; i <= k; ++i)#define dep(i, j, k) for(int i = j; i >= k; --i)const int maxn = (int)1e5 + 5;int n, q, a[maxn], dp[maxn][1005];inline int max(int x, int y) { return x > y ? x : y; }inline void init() { rep(i, 1, n) dp[i][0] = a[i]; for(int j = 1; 1 << j <= n; ++j) for(int i = 1; i + (1 << j - 1) - 1 <= n; ++i) dp[i][j] = max(dp[i][j - 1], dp[i + (1 << j - 1)][j - 1]); return;}inline int rmq(int l, int r) { int k = log2(r - l + 1); // cmath 库中有内置的 log2 函数,请注意不要用成以 e 为底的 log 函数 return max(dp[l][k], dp[r - (1 << k) + 1][k]);}int main() { scanf("%d %d", &n, &q); rep(i, 1, n) scanf("%d", &a[i]); init(); int l, r; rep(i, 1, q) { scanf("%d %d", &l, &r); printf("%d\\n", rmq(l, r)); } return 0;} 4. 应用ST 表本身就是个工具,我们在平时做题的时候不能想着刻意去使用它,详情参考洛谷 P2048 。 另外,ST 表之所以有局限性,是因为它在查询的时候可能会有一部分区间被重复查询,所以只能求解那些有重复元素不影响最终结果的问题。这种问题除了最大最小值之外,还有 gcd、lcm 等,所以这些问题也是可以通过 ST 表求的。","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"线段树","slug":"「Algorithm」线段树","date":"2022-01-26T22:54:55.000Z","updated":"2022-02-01T10:03:46.000Z","comments":true,"path":"posts/8893d943/","link":"","permalink":"https://blog.liynw.top/posts/8893d943/","excerpt":"","text":"前置芝士 二分的递归写法 二叉树 线段树线段树(Segment Tree)是一种二叉树,本质上和树状数组有区别虽然都是树。 不过和树状数组一样,线段树也是一种维护数组的数据结构,它可以实现 $O(\\log n)$ 修改、查询不过常数比树状数组大得多。 另外,线段树的应用范围比树状数组广,不过它维护的数据必须满足结合律($eg.(a+b)+c=a+(b+c)$)。为什么呢?这涉及到线段树实现的原理。 性质 & 原理首先要建一棵线段树,首先我们要有一个线段,那就是数组。 然后,我们还需要一棵二叉树。 于是,我们就成功种了一棵线段树。 它长这样: 注意到每个结点里面的区间没,这是闭区间,代表的是这个结点存的信息来源于数组的哪个区间。 观察这个线段树,我们可以发现: 每个叶子结点都代表数组中的一个元素。 每个非叶子结点的两个孩子的区间是从父节点区间的中间划开的。 说详细一点,若一个非叶子结点覆盖的区间为 $[L,R]$,那么令 $mid=\\lfloor \\frac{L-R}{2}\\rfloor$,则左孩子覆盖的区间为 $[L,mid]$,右孩子覆盖的区间为 $[mid+1,R]$。 除去最后一行是一棵满二叉树。 那线段树的原理是怎样的呢?这里我们先讲最简单的单点修改、区间查询来理解。 有点类似于于归并排序,我们需要分解后再合并数据。每个父节点存储的信息都是其两个子节点信息的合并,这样我们就可以通过访问一个结点拿到这个节点所代表区间的信息。 首先分解,我们一层一层往下递归,直到找到叶子结点,然后把叶子结点的信息向它的父节点传递: 然后由于递归回溯的原因,我们会接着遍历到它的兄弟,这个时候,它的父节点已经获得了两个子节点的信息,就可以合并信息了。 这个地方,我们给数组一个值,然后用数组元素和来代表需要求解的信息。 我们继续往上回溯,按照相同的方法求解。在求解 $[1,3]$ 之前,我们需要把 $[1,2]$ 的值先传上去。 这么一点一点的传上去,就可以求得区间的和了! 这个操作的时间复杂度是 $O(\\log n)$,每次修改、查询和初始化的时候都这么来一遍,不就可以实现单点修改区间查询了嘛。 那么为什么求得的值必须满足结合律呢?显而易见,我们需要把求解的值分成一个区域一个区域地求解,如果不满足结合律就会出问题。 接下来我们来讲一讲 C++ 的实现。 存储于是我们知道,每一个结点里需要存: 区间左端点 $l$ 区间右端点 $r$ 这个区间的信息($eg.\\sum_{i=l}^{r}a_i$ 或者 $\\max^{r}_{i=l}{a_i}$) 等等,为什么不用存左孩子和右孩子呢? 这就涉及到存储线段树的方法。 一般来说,只要题目不卡空间,我们可以通过数组来存一个本质上是二叉树的线段树: 根节点数组下标是 $1$,对于每个非叶子节点,若它的数组下标是 $p$,那么其左孩子数组下标是 $2p$,右孩子数组下标是 $2p+1$。 那么,处理好存储的问题,接下来我们来解决初始化。 初始化 & 单点修改对于一段区间 $[l,r]$,我们把它从 $mid$ 分成 $[l,mid]$ 和 $[mid+1,r]$,然后以这两个区间作为此结点的两个子节点。 像开头说的那样,我们一直往下递归,当递归到叶子结点的时候就把数组的值赋给叶子结点,接着我们把叶子结点的信息传到它的父结点,然后求父结点的值,再把父结点的信息传给父结点的父结点……最终就可以把信息传递到根节点了。 请注意,儿子向父亲转移需要放在递归语句的后面。除此之外没有什么别的难点,看代码: 12345678910111213141516// 这两行代码是求左子树和右子树下标的函数,p << 1 等价于 p * 2,p << 1 | 1 等价于 p * 2 + 1,不过速度更快inline int lc(int p) { return p << 1; }inline int rc(int p) { return p << 1 | 1; }void build(int p, int l, int r) { t[p].l = l, t[p].r = r; // 给结点代表的区间端点赋值 if(l == r) { // 这个节点是叶子节点,没有子树 t[p].val = a[l]; // 赋值 return; } int mid = (l + r) >> 1; build(lc(p), l, mid); // 初始化左子树 build(rc(p), mid + 1, r); // 初始化右子树 t[p].val = t[lc(p)].val + t[rc(p)].val; // 合并左右子树的信息 return;} 单点修改的代码和初始化很像,不过还是有一点区别。 假设我们要修改下标为 $3$ 的数字,把它加上 $5$: 就像这样,我们要在每一个包含所修改数据的下标的区间加上 $5$。 那么整个 $\\text{update}$ 函数有这两个部分: 从根节点出发往下找,直到找到需要修改的点(叶子节点); 往上回溯过程中修改每一个经过的结点的值。 那么代码就呼之欲出了: 12345678910111213void update(int p, int x, int k) { if(t[p].l == t[p].r) { // 找到了需要修改的叶子节点 t[p].val = k; // 修改它的值 return; } int mid = (t[p].l + t[p].r) >> 1; if(mid >= x) // 如果需要修改的结点在左子树 update(lc(p), x, k); else // 需要修改的结点在右子树 update(rc(p), x, k); t[p].val = t[lc(p)].val + t[rc(p)].val; // 合并左右子树的信息 return;} 区间查询我们可以找一些结点,且这些结点的范围能够刚好覆盖需要查询的区间,把这些结点的权值合并就可以了。 那怎么找呢?我们来看一张图: 这张图是我们在查询区间 $[3,7]$ 时的过程。 在递归的时候,有以下几条准则: 如果当前区间能完全被待查区间包含,就返回此区间的权值并不再往下递归。(图中打勾的地方) 如果当前区间被待查区间部分包含,就继续往下递归。 如果发现当前区间完全不能被当前区间包含,就停止递归。 当然,这里还有个难点,就是怎么判断是否应该递归到某个结点的左子树或者右子树呢? 我们都知道,线段树中两个子节点的区间是从父节点区间的中间切开的。所以,如果要判断左子树是否需要递归,就应该这样: 假设待查询区间是 $[L,R]$,我们会发现,只有在 $L\\le mid$ 时,左区间才会被(至少是部分)包含。 右区间同理: 需要满足的条件是 $R\\ge mid+1$,也就是 $R>mid$。 代码长这样: 1234567891011int query(int p, int l, int r) { int sum = 0; if(l <= t[p].l && t[p].r <= r) return t[p].val; int mid = (t[p].l + t[p].r) >> 1; if(l <= mid) sum += query(lc(p), l, r); if(r > mid) sum += query(rc(p), l, r); return sum;} 区间修改众所周知单点修改线段树的时间复杂度是 $O(\\log n)$,那么如果是区间修改,如何保持时间复杂度不变呢? 你可能会回答:差分。是的,这是一个可行的方法,不过差分只能维护区间和的查询,而对于查询最大值等则束手无策。而且既然如此,直接打码量少常数小的树状数组他不香吗。 于是一个玄学的方法出现了:懒惰标记。 我们知道,对于一个区间的修改,后面的查询不一定用得到,比如有一次你修改区间 $[1,5]$ 但是后面根本没有查询这里的值,那么我们就没有必要在修改的时候把所有的子节点都修改一遍,只有需要的时候才修改。 懒惰标记就是这么一个东西,我们给一个节点打上懒惰标记,就意味着这个结点的值已经被修改过,但是它的子节点还没有被更新。 只有我们在后面操作中需要更新后面的值,才需要把懒惰标记传到下面,更新需要的值。 在区间修改的时候,如果我们递归到一个结点覆盖的区间被需要修改的区间完全包含,那么我们就不需要再递归下去,而是给这个结点更新后打上一个懒惰标记,意味着这一段区间需要被修改,但是还没有完全实际操作。等下一次我们需要查询用到里面的值时,递归到这个节点时,我们就知道下面还没有更新,所以就把这个节点的两个子节点更新,然后把懒惰标记传到子节点上,以此类推。 把懒惰标记从父节点传到子节点的过程我们可以用一个 $\\text{pushdown}$ 函数实现。在这个函数里面,我们先需要修改子节点的权值,然后把父节点的懒惰标记叠加到子节点的懒惰标记上,最后清空父节点的懒惰标记,代码如下: 12345678910// target 即为懒惰标记inline void down(int p) { int l = lc(p), r = rc(p); t[l].val += (t[l].r - t[l].l + 1) * t[p].target; t[r].val += (t[r].r - t[r].l + 1) * t[p].target; t[l].target += t[p].target; t[r].target += t[p].target; t[p].target = 0; return;} 一些扩展线段树不仅可以维护诸如区间和,最值之类,还可以维护一些奇奇怪怪的东西。 先来看第一组: GSS1 GSS3 要求的是一个区间的最大子段和。 想一下,把两个区间合并为一个区间,该怎么找到这个区间的最大子段和呢? 无非就三种情况嘛: 左边的最大子段和; 右边的最大子段和; 左边包含右端点的最大子段和加上右边包含左端点的最大子段和。 那这三个东西怎么求呢?","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"数据结构","slug":"数据结构","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"}]},{"title":"P7960 [NOIP2021] 报数 题解","slug":"「Solution」Luogu_P7960","date":"2021-12-04T15:46:18.000Z","updated":"2021-12-04T15:48:21.000Z","comments":true,"path":"posts/44375d99/","link":"","permalink":"https://blog.liynw.top/posts/44375d99/","excerpt":"","text":"蒟蒻抢到了 CCF 的题! 考场上打玄学复杂度还没处理 $10^7+1$ 的屑只得了 $70$ 分。 首先,注意到所有包含 7 或者这些数的倍数都不可以报。那么我们可以利用一个类似于埃氏筛的东西来筛出这些不能报的数。 什么是埃筛埃氏筛法本来是用来找质数的一种质数筛法。 我们知道,一个质数只有 $1$ 和它本身两个因数,所以,我们可以通过任意两个不为 $1$ 的数相乘得到一个合数。 我们开一个数组 $b$,$b_i$ 代表 $i$ 是否为合数。从 $2$ 开始筛,如果此时 $b_i=0$,就说明这个数不能通过任意两个不为 $1$ 的数相乘得到,它是一个质数。 如果 $b_i=1$,说明这是一个合数。我们知道,任何一个数一定有至少一个因子是质数,所以每个数都可以通过让任意一个数和质数相乘得到。用合数再去与别的数相乘会浪费时间,所以我们需要直接跳过下面的步骤。 此时我们拿到了一个质数 $i$,然后我们开始遍历 $2\\times i,3\\times i,\\ldots$,这些全部都是合数,所以把它们全部标记为合数,直到超出筛的范围。 代码很简单,大概长这样: 123456789void Prime() { for(int i = 2; i * i <= n; i++) { if(f[i]) continue; for(int j = 2 * i; j <= n; j += i) f[j] = 1; } return;} 时间复杂度大概是 $O(n\\log \\log n)$。 怎么做这道题因为任何含有 $7$ 的数字或其倍数都不可以报出,所以我们可以通过乘积来筛掉所有不合法的数,这很明显是一道埃筛的变形。那么我们该如何做这道题呢? 我们还是开一个 $b$ 数组记录所有的数是否能报,其中 $b_i=0$ 代表 $i$ 可以报,$b_i=1$ 则不能。 首先我们要知道怎么判断一个数 $x$ 是否含有 $7$。这个很简单,只需要重复以下两个步骤: 计算 $x/10$ 的余数是否等于 $7$,如果有,说明此数含有 $7$; $x$ 除以 $10$。 用这样的方法可以遍历到 $x$ 每个数位上的数。 1234567891011// 判断这个数能不能报,若 x 中有 7,返回 1 inline bool pan7(int x) { int qwq; while(x) { qwq = x % 10; if(qwq == 7) return 1; x /= 10; } return 0;} 每次筛到一个不能报的数,因为其倍数也不能报,所以我们把它乘以不同的数,把这些数也标记为不能报。期间我们需要保证不筛到 $10^7+1$ 外面去。代码长这样: 1234567891011121314inline void prime() { for(int i = 1; i <= n; i++) { if(pan7(i)) { b[i] = 1; for(int j = 1; j <= n; j++) { int qwq = i * j; if(qwq > n) break; b[qwq] = 1; } } } return;} 时间复杂度 $O(n \\log n)$。 接着我们已经知道了所有能报的的数,此时我们只需要从大到小遍历 $10^7+1\\sim 1$ 的所有数,拿一个变量存当前遍历到的最小能报的数字,每次到一个数,这个变量里存的值就是它报了之后能报的下一个数字,把这个数存在 $ans$ 数组里面。 这样的预处理可以避免查询一个一个跳导致的玄学复杂度。 时间复杂度 $O(n)$。 然后我们就可以实现 $O(1)$ 询问。 一个坑题目数据范围是 $10^7$,为什么需要筛到 $10^7+1$ 呢? 因为可能询问的就是 $10^7$,而它的下一个数是 $10^7+1$。 这个坑卡掉了许许多多悲伤的 OIers。 Code12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576#include <cstdio>#include <cctype>const int maxn = (int) 1e7 + 5;const int n = maxn - 4;int t, q, tot, g[maxn], ans[maxn];bool b[maxn];// b[x] = 1 说明 x 不能被选// 若 x中有 7,返回 1 inline bool pan7(int x) { int qwq; while(x) { qwq = x % 10; if(qwq == 7) return 1; x /= 10; } return 0;}inline void prime() { for(int i = 1; i <= n; i++) { if(pan7(i)) { b[i] = 1; for(int j = 1; j <= n; j++) { int qwq = i * j; if(qwq > n) break; b[qwq] = 1; } } } return;}// 快读快写inline int read() { int x = 0, w = 0; char ch = 0; while(!isdigit(ch)) { w |= ch == '-'; ch = getchar(); } while(isdigit(ch)) { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); } return w ? -x : x;}inline void write(int x) { if(x < 0) putchar('-'), x = -x; if(x > 9) write(x / 10); putchar(x % 10 + '0');}int main() { // freopen("number.in", "r", stdin); // freopen("number.out", "w", stdout); prime(); int Ans = (int) 1e7 + 1; // 必须 +1 for(int i = n; i; i--) { ans[i] = Ans; if(!b[i]) Ans = i; } t = read(); while(t--) { q = read(); write(b[q] ? -1 : ans[q]); putchar('\\n'); } return 0;}","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"数学","slug":"数学","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E5%AD%A6/"}]},{"title":"NOIP2021 爆炸记","slug":"「Live」NOIP2021","date":"2021-12-04T15:42:48.000Z","updated":"2022-01-30T17:11:39.000Z","comments":true,"path":"posts/407315f9/","link":"","permalink":"https://blog.liynw.top/posts/407315f9/","excerpt":"","text":"Day -?笑死,CQ 四百多名额最后去了三百多人(包括初中生)。 初二划水人员表示很淦。 因为 CSP 完了就是《期 中 定 时 作 业》,所以暂时潜心搞 whk 没怎么搞 OI。 结果《期 中 定 时 作 业》爆炸了,语文估错时间作文没写完…… 后来我们语文老师给我估分 $42$,我十分怀疑这个分数的真实性,但是我相信我们的语文老师 (^_^)。 数学海星,终于上 $140$ 了。英语比较爆炸,客观题扣了 $9$ 分,作文扣了 $4$ 分,勉强上 $135$,拿了本英语老师的英文版爱丽丝漫游仙境。我们班最高 146,orz 年级第二 NH4+ 大佬。 最后结果嘛……班级 $\\text{rk13(14?)}$,年级勉强卡进前 $100$。 Day -2、-1一年一度的趣味运动会,初二了也是初中的最后一次。因为我们班妹子很少,而每个项目一个班男生和女生的参加人数是一样的,所以每个妹子都有很多项目。 我在那里颓废,看到班上一堆卷王在自己座位上卷,感到十分惊恐。但是这依然挡不住我想要颓废的心(狗头)。 班级获得的结果还不错,拿了羊角球接力的第一名,最后凭借“暗箱操作”的道德风尚奖以 $5$ 分之差惊险拿到一等奖最后一名。(PS:后面我们班在冬季长跑以一分之差得到第二,英语配音二等奖第一名,都被我们说做这次的“报应”。) 本蒟蒻基本上在参加项目+看别人打排球+颓废+和 lym 聊天,没怎么复习( Day 0梅开二度,又发遗照……我的考号是 CQ-00281,本来说是想和大佬面基,但是大佬都很忙,也不敢去问。 话说 CQ NOIP 要全程戴口罩?这对于一个一戴口罩眼镜就要起雾的人来说怕是不太友好哦( 晚上回家准备了一堆吃的,稍微看了一下考前注意事项就去睡觉了。 Day 1很早就起床了,看了一眼电脑,然后就出发了。到了 BS 门口,找到了穿着我们学校的校服的同学,然后听到他们在唱生日快乐,找 lym 问了下才知道是 cjg 学长过生日。 等了一会儿人就进学校了。(某个教练一直让我们在校门口等 zqw 和 tl,结果等不及了进去之后才发现他俩早就进去了。) 我和 lym 巨佬很近,就在斜对面。这是个好兆头! 开题了,这次密码是 IronHeart,铁胆雄心?那么这次是不是有码量超级大的题啊。 先看题,T1 看着比较简单,T2 不会,T3 不会,T4 看着是道只要肯打就能得分的大膜你。于是决定先做 T1。 想了一下,这 T1 看着有点像质数筛法啊!于是我准备敲一个类似于欧拉筛的东西,结果发现欧拉筛貌似不行,只好退而求其次,打了个类似于埃氏筛的东西。 打完了,开开心心按下 F9,然后: [Error] ld returned 1 exit status ??? 我一脸问号,再编译了几遍,结果是一样的。我觉得是自己的代码出现了问题,于是打了个 A + B,按下 F9,然后: [Error] ld returned 1 exit status 心 脏 骤 停。 此时我的心里有一万匹草泥马奔涌而过,于是我举起手召唤找老师…… 老师过来看了很久我的 A + B,调编译参数之类的也没解决,于是他又叫来了另一个老师,这位老师 Dev-C++ 右上角的参数从 TDM-GCC 4.9.2 64-bit Debug 改成了 TDM-GCC 4.9.2 64-bit Release,然后按下 F9: 电脑上出现了一个黑色的终端,它终于编译起了! 这个时候那个老师问了我一句:“你会打断点调试吗?” 我说会。 他问我要不要换台电脑。 我看着我的 T1 代码,委婉拒绝了他们好心的建议(我平时几乎不用断点调试)。 然后他们就走了。 然后我大概测了下 T1 的代码,大概没问题了,但是样例 4 死活 1.2s 左右怎么都卡不进去。于是先放掉了 T1。 PS:这人不仅打的玄学复杂度超时,还只打了 $10^7$ 没加一,笑死了。 T2T3 看上去都没什么思路,于是我吃了一些东西,开始干 T4……的 bfs 暴力。你看看这个人这么逊,肯定只会打最暴力的暴力了 QAQ。 不得不说 T4 码量是真的大,应了那句 IronHeart 啊!我一直在敲,敲了很久很久,最后敲出来还过不了小样例!于是我开始分析,疯狂改 bug 造 bug,然后把小样例过了 QWQ。 代码大概有 5k 吧,这是蒟蒻打过最长的代码了。 最后还剩半个小时,码了 T2 的暴搜和 T3 的 rand,但是肯定一分都没有……QAQ 话说坐我右边那位大巨佬比赛一开始就开始吃东西,好像是牛肉干,一直吃到了比赛结束,中间从来没断过…… 比赛结束几天后自测了一下。 估分:$90+0+0+rp\\approx 90$ Luogu:$70+0+0+0=70$ 你谷的分出来的时候我心里一紧,写了那么久的 T4 一分都没有吗?心态都要炸了。 接着 mjl 公布了估计的分数,我 $74$。(什么?我 T4 竟然有分?) 然后官方公布分数: $70+0+0+8=78$ CCF 少爷姬 NB! 3= 滚粗了。 据说 DJ $248$?愣着干啥?赶紧膜拜啊!","categories":[{"name":"生活","slug":"生活","permalink":"https://blog.liynw.top/categories/%E7%94%9F%E6%B4%BB/"}],"tags":[{"name":"游记","slug":"游记","permalink":"https://blog.liynw.top/tags/%E6%B8%B8%E8%AE%B0/"}]},{"title":"P4656 [CEOI2017] Palindromic Partitions 题解","slug":"「Solution」Luogu_P4656","date":"2021-12-04T15:35:39.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/b5735321/","link":"","permalink":"https://blog.liynw.top/posts/b5735321/","excerpt":"","text":"这一定是 ljt 写过的最短的题解。 先上一个部分分:朴实无华的区间 dp $\\text{45pts}$ 123456789101112131415161718192021222324252627282930313233343536373839404142#include <cstdio>#include <cstring>const int maxn = (int) 1e3 + 5;int T, n, dp[maxn][maxn];char s[maxn];inline bool f(int l, int r, int k) { for(int i = 0; i < k; i++) { if(s[l + i] != s[r + i]) return false; } return true;}int main() { scanf("%d", &T); while(T--) { scanf("%s", s + 1); n = strlen(s + 1); for(int i = 1; i <= n; i++) dp[i][i] = 1; for(int len = 2; len <= n; len++) { for(int i = 1; i <= n - len + 1; i++) { int j = i + len - 1; bool flag = 0; for(int k = 1; k <= len / 2; k++) { if(f(i, j - k + 1, k)) { flag = 1; dp[i][j] = dp[i + k][j - k] + 2; break; } } if(!flag) dp[i][j] = 1; } } printf("%d\\n", dp[1][n]); } return 0;} 如上的代码,内存不够,时间也要超限,那我们需要找新的办法。 正解:贪心 + Hash 因为要划分为尽量多的部分,所以我们从两边开始找,设两个指针分别指着从左往右数和从右往左数第 $i$ 个字符。只要找到两个字符串一样,就把它们分离出来。 两个字符串是否一样可以通过 Hash 判断,但是有个问题:我们是从两边往中间枚举的,右边那个字符串是倒着枚举的,怎么判断呢? 这个很简单,我们只需要判断一下当前右边遍历到的字符是字符串的第几个,假设它是第 $k$ 个,那么就加上它乘以 $base^k$ 即可。 最后有个细节:如果字符串的长度是奇数,或者还剩下一些字符,那么需要把这些字符作为一个区间,答案要加一。 1234567891011121314151617181920212223242526272829303132#include <cstdio>#include <cstring>#define ull unsigned long longconst int maxn = (int) 1e6 + 5, p = 97, base = 10;int T, n;char s[maxn];int main() { scanf("%d", &T); while(T--) { scanf("%s", s + 1); n = strlen(s + 1); ull hash1 = 0ull, hash2 = 0ull, pow = 1; int ans = 0; for(int i = 1; i <= n / 2; i++) { hash1 = hash1 * p + s[i]; hash2 = hash2 + s[n - i + 1] * pow; pow *= p; if(hash1 == hash2) { ans += 2; hash1 = hash2 = 0ull; pow = 1; } } if((n & 1) || hash1) ++ans; printf("%d\\n", ans); } return 0;}","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"贪心","slug":"贪心","permalink":"https://blog.liynw.top/tags/%E8%B4%AA%E5%BF%83/"},{"name":"哈希","slug":"哈希","permalink":"https://blog.liynw.top/tags/%E5%93%88%E5%B8%8C/"}]},{"title":"P6359 [CEOI2018] Cloud computing 题解","slug":"「Solution」Luogu_P6359","date":"2021-12-04T15:27:59.000Z","updated":"2022-01-30T17:14:46.000Z","comments":true,"path":"posts/b332757/","link":"","permalink":"https://blog.liynw.top/posts/b332757/","excerpt":"","text":"前言题目链接 我们教练十分努力,竟然找到了这个只有一百多人做的题目。 这是一道比较有思维难度的 01 背包题,建议评绿或者蓝? 确定容量和价值这个输入的变量名很显然就是在提示你把计算机和客户订单混在一起嘛。 我们可以发现,对于每一个计算机,只有买或不买两种情况;对于每一个单子,只有接或不接两种状态。 而能不能接客户的单子取决于内核的数量够不够。于是,我们想到了这道题的算法——01 背包,内核数量相当于容量,钱相当于价值。 但是这时就有了一个问题:我怎么确定内核的数量? 注意到题目中还有另一个条件——时钟频率。因为客户要求有一个对于时钟频率的限制,换句话说,就是每一个客户能使用的最大内核数量是确定的。 所以我们可以把计算机和客户的单子放在一个结构体数组里面,拿一个 bool 区分一下计算机和客户。然后,按照时钟频率排序。 此时,对于任意一个客户的单子,它前面的所有内核数量之和就是对于它来说可以使用的最大内核数。 这里需要注意一点:如果时钟频率一样,计算机排在前面。如果类型和时钟频率都一样,计算机把价格低的排在前面,客户把价格高的排在前面。 123456789bool cmp(node x, node y) { if(x.f != y.f) return x.f > y.f; if(x.data != y.data) return x.data < y.data; if(!x.data) return x.v < y.v; return x.v > y.v;} 状态转移接着就到了比较难的地方。 设 $dp_{i,j}$ 代表处理了前 $i$ 个请求之后还剩下 $j$ 个可用的内核时能获得的最大利润。 经上文分析,对于计算机和客户都有两种状态。那么,选择哪种更优呢? 我们用一个变量 $C$ 来统计到目前为止内核的总数量,相当于一个前缀和。然后分析: 对于计算机买的话,需要花钱,但是内核数量会增加。那么相对买之前来说,买后内核数量增加了 $c_i$,也就是说买之前内核数量比现在少 $c_i$,而买后,现在有的钱数量相较于买之前减少了 $v_i$,所以结果表达式如下: dp_{i-1,j - c_i} - v_i至于 $j$ 的循环范围很简单,保证数组下标不超过 $0\\sim n$ 的范围即可。 所以就有对于计算机的状态转移方程: dp_{i,j}=\\max^{C}_{j=c_i}\\{dp_{i-1,j-c_i}-v_i\\}对了,记得在转移之前把前缀和加上。 对于客户接客户的单子,可用的内核数量会减少,但会收获钱。那么接后相对于接前少了 $c_i$ 个内核,但钱增加了 $v_i$,所以结果表达式如下: dp_{i-1,j+c_i}+v_i那状态转移方程就是这样: dp_{i,j}=\\max^{C-c_i}_{j=0}\\{dp_{i-1,j+c_i}+v_i\\} 滚动数组开二维数组空间不够,于是自然想到了让数组打滚。然而,滚起来之后,$j$ 到底该从小到大还是从大往小? 这个顺序只取决于一个因素,就是这个状态转移方程所使用的 $dp$ 值在需要求的值的前面还是后面。如果在前面,就需要倒着枚举,如果在后面,就要正着枚举,以保证利用的是本来为 $dp_{i-1,?}$ 而非 $dp_{i,?}$。 所以,处理购买计算机的请求要从大到小枚举,处理客户单子需要从小到大枚举。 注意一下初始值,数组赋极小值,$dp_{0}=0$。 12345678910for(int i = 1; i <= num; i++) { if(!a[i].data) { // 计算机 C += a[i].c; for(int j = C; j >= a[i].c; j--) dp[j] = std :: max(dp[j], dp[j - a[i].c] - a[i].v); } else { // 客户 for(int j = 0; j <= C - a[i].c; j++) dp[j] = std :: max(dp[j], dp[j + a[i].c] + a[i].v); }} 求答案非常简单,长这样: ans=\\max_{i=0}^{C}\\{dp_i\\}注意,$i$ 的初始值为 $0$。 我最开始做的时候以为这个地方初始值是 $1$,因为 $ans$ 初始值就是 $0$ 嘛!于是喜提 $\\text{18pts}$…… 其实最终 $dp_0$ 不一定是没有购买计算机,也有可能是内核数量刚好消耗完。 Peter:这其实就和牛顿第一定律一样嘛,要么不受外力,要么合力为零,两种情况嘛。 我承认你说得很有道理,但是你是物理学疯了? Code不开 long long 见祖宗。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354#include <cstdio>#include <cstring>#include <algorithm>#define int long longconst int maxn = (int) 2e5 + 5;struct node { int c, f, v; bool data; } a[4005];int C, n, m, num, ans, dp[maxn];bool cmp(node x, node y) { if(x.f != y.f) return x.f > y.f; if(x.data != y.data) return x.data < y.data; if(!x.data) return x.v < y.v; return x.v > y.v;}signed main() { memset(dp, -0x3f, sizeof(dp)); dp[0] = 0; scanf("%lld", &n); for(int i = 1; i <= n; i++) { scanf("%lld %lld %lld", &a[i].c, &a[i].f, &a[i].v); a[i].data = 0; } scanf("%lld", &m); for(int i = n + 1; i <= n + m; i++) { scanf("%lld %lld %lld", &a[i].c, &a[i].f, &a[i].v); a[i].data = 1; } num = m + n; std :: stable_sort(a + 1, a + num + 1, cmp); /* for(int i = 1; i <= num; i++) printf("%d %d %d %d\\n", a[i].c, a[i].f, a[i].v, a[i].data); */ for(int i = 1; i <= num; i++) { if(!a[i].data) { // 计算机 C += a[i].c; for(int j = C; j >= a[i].c; j--) dp[j] = std :: max(dp[j], dp[j - a[i].c] - a[i].v); } else { // 客户 for(int j = 0; j <= C - a[i].c; j++) dp[j] = std :: max(dp[j], dp[j + a[i].c] + a[i].v); } } for(int i = 0; i <= C; i++) ans = std :: max(ans, dp[i]); printf("%lld", ans); return 0;} AC 记录","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"CSP2021 游记","slug":"「Live」CSP2021","date":"2021-11-07T10:55:12.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/9ec8265c/","link":"","permalink":"https://blog.liynw.top/posts/9ec8265c/","excerpt":"","text":"前言初二 CQ OIer,去年卡线省一(不会 sort 捞了 $\\text{15pts}$),今年 CSP 划水。 感觉要退役了 qwq。 至于去年为啥没游记,我去年都不知道什么是游记…… 初赛篇Day ???我去 TG 不就是去划水的吗。 因为双减似乎课没法上了,于是搞 OI 的时间变成了每天中午的中午训练。 mjl 真好玩,一天(一个小时)布置的任务如下: 某年的 PJ 和 TG 初赛卷子 一道题(难度绿及以上) 【我看不懂,但我大受震撼 orz】.jpg 放弃了 c++ 的题目,去搞初赛了。 Day 0中午发了准考证。 然而,我突然发现我们学校为了省墨用的是黑白。 本来也没啥的,但是大家都知道,准考证上有本人照片…… 于是机房就【数据删除】了( 看了下准考证号,PJ 600多,TG 200多。 下楼的时候。 Peter:我觉得这次大题选 A 的几率特别高!(flag) Day 1本来说的是 7:30 起床,结果 7:00 就醒了。吃早饭之后发了个 rp++,就去考前颓废了( 提前半个小时到了我们学校高中部(考点),看了安排表,发现我们学校的大巨佬都在这儿了(甚至还有两个来拉高过关线的高三 NOI 金牌大佬),小蒟蒻十分紧张/kel。 初一初二一群人在科技楼一楼打打闹闹了至少 15 分钟,期间我们甚至欣赏到了 XSC062 的可爱萝莉音(大雾)。 然后社长(cháng)说他看到有人去 6 楼了,于是我们一行人坐电梯到六楼看到了 mjl…… 上午-TGemm 我忘带身份证了?mjl 说穿着校服就行。哦那没事了。 9:30 开始考试,第一题是个啥? 继续做题,后面的题(指选择题)还好,不是很毒瘤。 开始做程序阅读。好家伙第一题就来立体几何?出题人我谢谢您。 对了,话说 acos(0.5) 是什么啊? Update:问了数学竞赛的同学,原来是反三角函数,当时我连这个数的近似值都不知道,肯定没办法手动模拟。 第二题看上去是一个莫名其妙的求区间最大值,那个 Node 看了我好久,不过最终还是看明白了(大概吧?),希望不要出锅(小声 bb)。 第三题……好家伙直接手算 base64。出题人我谢谢您。 做完程序阅读只剩下了 20 分钟,有点慌。 开完形填空完善程序! 第一题乍一看是个数学题,再一看是个模拟题,因为时间不够了就大概看了下,随便口胡了几个上去。(T4 我故意选了个带 $r$ 的,然后就没了) 我有个 $r$,诶,我不用,就是玩儿~ 出题人我谢谢您。 五分钟搞完第一题,开第二题。 第一眼:我看到了一个 001?感觉不妙。去年 90 多行的手动模拟队列已经够了吧。 第二眼:RMQ??又是什么奇奇怪怪的数据结构。 等等,现学??出题人我谢谢您。 此时我又想起了昨天 Peter 的 flag。 于是我写了 6 个 A 上去。我倒是要看看他押题的能力怎么样( 于是 TG 就这么裂开了。 中午在机房看了民间答案之后的估分:$39.5$ 从考场出来之后,某个人因为太饿了,于是随手拔起一根草塞进了嘴里…… XSC062:这啥啊,怎么是酸的? 我:草(真·一种植物)。 和 Peter 还有 XSC062 聊了一下,发现我选择 T1 做错了(我不知道为什么我选成了 cd,答案应该是 ls),还了解到最后一道大题 D 最多。 我觉得我药丸了。 下午-PJ选择原题一堆,程序阅读继续手算 base64( 阅读 T2 好像是个奇奇怪怪的欧拉筛,不过那四个数组是啥没看懂 qwq。像极了我平时敲代码乱取变量名的样子。 出题人,我错了,我以后再也不乱取变量名了!能不能饶了我! 其余不做评价…… 估分:$62.5$ Day n 指 9.27。 出分了! PJ:$72.5$ TG:$47$ TG 卡线过了,喜大普奔( 复赛篇Day ?因为我去年已经卡线 PJ1= 了,所以我妈没给我报 PJ,只报了 TG。 所以要求别那么高吧,我只是想要个 2= + 六级蓝勾勾 qwq。 Day 1上午补初赛游记,复习了下图论和高精的板子 (然后根本没用上)。 $\\text{CSP 2021 rp++!}$ 下午到的比较早,找了了 xzj 和 zm一起膜拜高年级的大佬。其他人因为参加了普及还在学校里面的。 等了一会儿进去了,然后又是互相膜拜,膜拜初三高中的巨佬和奶题(zm:这次肯定有数据结构!),和初一初二的同学会合,然后进行传统艺能——照遗合照。 话说 BS 为什么不发三明治啊!去年 NK 都发了!! 到了机房,按照老师说的,新建文件,关机重启,下载 C++ 编译器,打框架。 然后密码发下来了。什么鬼怎么是乱码,CCF 您可以用心一点吗。 打开题,通览一遍,直接确认 T4 是到不可做题。然后大概看了下,T1 是最简单的(Update:其实应该是 T3,但是当时没想到 T3 比 T1 简单,结果把 T1 的近似正解硬刚出来了),于是开始想 T1。 不过我想了很久,都只能想到 $O(n^2)$。然后突然想到可以用优先队列,貌似可以优化时间复杂度 (虽然最后做出来还是两层循环……) 做了很久,中途突然发现做法假了样例过不了,后来才回忆起有结构体这回事…… 差点忘了运算符重载……幸好想起来了。(flag) 时间复杂度 $O(n\\log n)$,估分 $100\\text{pts}$ 吧。 去上了个厕所,喝了点水,但是 T2T3 还是没思路。 然后我发现 T2 虽然感觉在哪里看到过但就是十分令人自闭(我的区间 DP 烂到不行),于是开始敲 T3 $O(2^{2n})$ 暴搜。这个暴搜很简单,一会儿就敲完了。 估分 $\\text{28pts}$。 最后看了看 T2,因为想不出来十分自闭决定打全排列暴搜,结果暴搜都差点没打出来……因为不会判断字符串是否合法。 幸好最后 $10$ 分钟过样例了 qwq。 估分 $\\text{15pts}$。 出来之后又和 Peter 和 XSC062 聊(别问我为啥总是他们俩),得知基本上和我一样打暴搜,内心逐渐趋于平静…… Day 2洛谷民间数据冲鸭! 结果出事了。 详情请见这个帖子。 点击查看 CE 事件具体过程 帖子说得可能不太清楚……我在考场上敲 T1 的时候忘了运算符重载怎么写,于是想了很久把 operator 这个单词想起来了,于是就敲了一句:1operator<(const node x, const node y) { return x.Time > y.Time; }结果就少敲了前面的一个 bool,也就是说,正确的写法应该是这样的:1bool operator<(const node x, const node y) { return x.Time > y.Time; }好玩的是,CQ 没有提供 Linux 虚拟机,只提供了 Win7 的系统,然后这玩意儿正好在 Windows 系统可以过编译,然后在 Linux 上 CE 了……更好玩的是,DJ 大佬在参加 PJ 的时候,T3 和我犯了一模一样的错误,和我一样挂了 $100$ 分,如果我们两个不挂这两道题的话,他就 AK 比赛了,我就有 7 级蓝勾了。只可惜这个世界上没有什么如果。 我太 TM 高兴了,直接从 $143$ 挂到了 $43$,但凡我 T1 打个 $40$ 分的暴力也不至于这样。 Day 3向 mjl 吐槽 CCF 的测评环境问题导致我挂 100 分,他说没办法申诉。 然后因为是 Dev-C++ 的环境问题嘛,CQ 目测这几年还没办法搞 Linux 系统,所以 mjl 帮我测了一下 CodeBlocks,结果那个上面也能过。 所以这种问题只能依靠自己的力量了吗…… 希望 CQ 明年可以有 NOI Linux 2.0 的虚拟机 QWQ。 Day 7过去了一周,mjl 叫我们补前三题。 (๑•̀ㅂ•́)و✧夹带私货:CSP-S2021 T1 题解 然后晚上出分了。 和预估一样。 Day n 指出结果的那一天。 结果还算好,没打铁,得了个 3=。 还好还好有奖,就是不知道 NOIP 审核给不给过 QwQ……","categories":[{"name":"生活","slug":"生活","permalink":"https://blog.liynw.top/categories/%E7%94%9F%E6%B4%BB/"}],"tags":[{"name":"游记","slug":"游记","permalink":"https://blog.liynw.top/tags/%E6%B8%B8%E8%AE%B0/"}]},{"title":"「CSP-S 2021」廊桥分配 题解","slug":"「Solution」Luogu_P7913","date":"2021-11-07T10:28:46.000Z","updated":"2022-01-30T17:42:57.000Z","comments":true,"path":"posts/b7fede58/","link":"","permalink":"https://blog.liynw.top/posts/b7fede58/","excerpt":"","text":"前言如果您是从 ljt 的 CSP 游记过来的,那么您应该已经知道了 CE 惨案。如果您和我一样有这一类问题的话可以访问这个帖子,上面有一些值得采纳的建议。 题解设 $s1_i$ 为国内航班有 $i$ 个停机位时能停在停机位的新增加的飞机个数。$s2_i$ 为国际,同理。 那么,我们需要一个结构体的优先队列来存每一个停机位的信息:会被占用到多久,停机位的编号。按照被占用截止时间从小到大排序。 而且,优先队列里只能存此时被占用的停机位。 具体实现国内和国际分开枚举。 拿一个 bool 数组 $is\\_zhan$,$is\\_zhan_i$ 代表第 $i$ 个停机位是否被占用。 每一次有一个飞机来的时候,就先把所被有占用截止时间小于这个飞机到达时间的停机位全部弹掉,然后从 $1$ 号机位开始统计,直到枚举到一个没有被占用的机位 $j$,就意味着这个飞机在机位数量至少为 $j$ 的时候可以停在停机坪上,所以:$s1_i ← s1_i + 1$。 国际同理。 最后再统计 $s1$ 和 $s2$ 的前缀和,枚举分配 $0\\sim n$ 个机位给国内,找最大值。 时间复杂度 $O(M\\log n + rp)$。(第二层循环不知道会卡多久) 正确性证明cgy 大佬说我没有正确性证明,那我就来补一个吧。(不过这个东西感觉有点只可意会不可言传,可能我说得不清楚,见谅。) 我们打的优先队列在假设一种情况:有 $m$ 个停机位,从左到右编号为 $1, 2, \\ldots, m$。这样可以保证所有飞机都能停到停机位里。 每一次有一个飞机到达的时候,我们就选择最左边的那个机位停飞机。此时,这个机位的编号就是它能停到停机位里所需要的最少的停机位数量。 为什么呢? 因为每一个飞机都在尽量往左边停,如果它停在了右边,就说明它来的时候左边的所有机位都被占领了,它只能停在右边。 我们假设它停在 $j$ 号位,刚才说的那个飞机停在 $i$ 号位,那么,对于任意一个停机位数量 $i\\le k < j$,都满足 $i$ 飞机可以停,但 $j$ 飞机不可以停的情况。 它右边的所有飞机都满足这个条件,而它左边的飞机都可以把它当做右边的飞机满足这个条件。所以 $i$ 飞机可以停的条件为 $i\\le k$,而我们需要在可以停相同数量的飞机时,尽量节约停机位数量,使得另一个区域的飞机能停更多,所以上述条件成立。 注意需要把国内和国际的飞机按照到达时间排序。 代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061#include <bits/stdc++.h>using namespace std;const int maxn = (int) 1e5 + 5;struct node { int s, t; } a[maxn], b[maxn];struct que { int Time, num; };int n, tot, m1, m2, ans, s1[maxn], s2[maxn], sum1[maxn], sum2[maxn];bool is_zhan[maxn];priority_queue <que> q;bool cmp(node x, node y) { return x.s < y.s; }bool operator<(const que x, const que y) { return x.Time > y.Time; }int main() { //freopen("airport.in", "r", stdin); //freopen("airport.out", "w", stdout); scanf("%d %d %d", &n, &m1, &m2); for(int i = 1; i <= m1; i++) scanf("%d %d", &a[i].s, &a[i].t); for(int i = 1; i <= m2; i++) scanf("%d %d", &b[i].s, &b[i].t); stable_sort(a + 1, a + m1 + 1, cmp); stable_sort(b + 1, b + m2 + 1, cmp); //国内 for(int i = 1; i <= m1; i++) { while(!q.empty() && q.top().Time < a[i].s) { is_zhan[q.top().num] = 0; q.pop(); } tot = 1; while(is_zhan[tot]) ++tot; is_zhan[tot] = 1; ++s1[tot]; que qwq; qwq.Time = a[i].t; qwq.num = tot; q.push(qwq); } memset(is_zhan, 0,sizeof(is_zhan)); while(!q.empty()) q.pop(); //国际 for(int i = 1; i <= m2; i++) { while(!q.empty() && q.top().Time < b[i].s) { is_zhan[q.top().num] = 0; q.pop(); } tot = 1; while(is_zhan[tot]) ++tot; is_zhan[tot] = 1; ++s2[tot]; que qwq; qwq.Time = b[i].t; qwq.num = tot; q.push(qwq); } //汇总 for(int i = 1; i <= n; i++) sum1[i] = sum1[i - 1] + s1[i]; for(int i = 1; i <= n; i++) sum2[i] = sum2[i - 1] + s2[i]; for(int i = 0; i <= n; i++) ans = max(ans, sum1[i] + sum2[n - i]); printf("%d", ans); return 0;} 优化这个代码在洛谷和官方吸氧能过,但是我们 OJ 自造的毒瘤数据过不了(而且教练死活不开 O2)所以就有了优化。 大家注意到上面代码的一句: 1while(is_zhan[tot]) ++tot; 这玩意儿要是数据毒瘤能把时间复杂度卡到 $O(Mn\\log n)$。 于是我们可以再开一个优先队列存没有被占用的机位,按照编号从小到大排序,每次只需要从这个序列里取第一个就行了。 所以 $is\\_zhan$ 数组就这么退役了 qwq。 时间复杂度 $O(M\\log n)$,跑得飞快。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960#include <bits/stdc++.h>#pragma G++ optimize(2)using namespace std;const int maxn = (int) 1e5 + 5;struct node { int s, t; } a[maxn], b[maxn];struct que { int Time, num; };int n, tot, m1, m2, ans, s1[maxn], s2[maxn], sum1[maxn], sum2[maxn];priority_queue <que> q;priority_queue <int, vector <int>, greater <int> > no_zhan;bool cmp(node x, node y) { return x.s < y.s; }bool operator < (const que x, const que y) { return x.Time > y.Time; }int main() { scanf("%d %d %d", &n, &m1, &m2); for(int i = 1; i <= m1; i++) scanf("%d %d", &a[i].s, &a[i].t); for(int i = 1; i <= m2; i++) scanf("%d %d", &b[i].s, &b[i].t); stable_sort(a + 1, a + m1 + 1, cmp); stable_sort(b + 1, b + m2 + 1, cmp); //国内 for(int i = 1; i <= m1; i++) no_zhan.push(i); for(int i = 1; i <= m1; i++) { while(!q.empty() && q.top().Time < a[i].s) { no_zhan.push(q.top().num); q.pop(); } tot = no_zhan.top(); no_zhan.pop(); ++s1[tot]; que qwq; qwq.Time = a[i].t; qwq.num = tot; q.push(qwq); } while(!q.empty()) q.pop(); while(!no_zhan.empty()) no_zhan.pop(); //国际 for(int i = 1; i <= m2; i++) no_zhan.push(i); for(int i = 1; i <= m2; i++) { while(!q.empty() && q.top().Time < b[i].s) { no_zhan.push(q.top().num); q.pop(); } tot = no_zhan.top(); no_zhan.pop(); ++s2[tot]; que qwq; qwq.Time = b[i].t; qwq.num = tot; q.push(qwq); } //汇总 for(int i = 1; i <= n; i++) sum1[i] = sum1[i - 1] + s1[i]; for(int i = 1; i <= n; i++) sum2[i] = sum2[i - 1] + s2[i]; for(int i = 0; i <= n; i++) ans = max(ans, sum1[i] + sum2[n - i]); printf("%d", ans); return 0;}","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"STL","slug":"STL","permalink":"https://blog.liynw.top/tags/STL/"}]},{"title":"2021.10.16 考试总结","slug":"「ExamSummary」20211016","date":"2021-10-17T23:12:08.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/ad60f6f3/","link":"","permalink":"https://blog.liynw.top/posts/ad60f6f3/","excerpt":"","text":"总分 $500$,得分 $90$。非常裂开,非常无语(ˉ▽ˉ;)… 欢迎收看《关于 ljt 是怎么挂掉 $\\text{240pts}$ 的》。 T1一道很简单的贪心,把运动员按照能力大小排序,统计运动员的总参赛时间,如果没到就加上这个运动员的能力值与参加比赛时间之积。另外,因为我们是按照能力值从大到小排序的,所以需要让排在前面的运动员尽量多一些时间,也就是说,这道题里面我们要当万恶的资本家,把能力值最大的几个运动员都榨干。 不开 long long 见祖宗,挂了 $\\text{40pts}$。另外一定要注意 long long 要开全,否则会导致 $\\text{65pts}$ 惨案。 zszz,#define int long long 是个好东西。 代码: 12345678910111213141516171819202122232425262728293031#include <cstdio>#include <algorithm>#define int long longusing namespace std;const int maxn = 505;struct node { int k, l; } a[maxn];int m, n, tot, ans;bool cmp(node x, node y) { return x.k > y.k; }signed main() { freopen("marathon.in", "r", stdin); freopen("marathon.out", "w", stdout); scanf("%lld %lld", &m, &n); for(int i = 1; i <= n; i++) scanf("%lld %lld", &a[i].k, &a[i].l); stable_sort(a + 1, a + n + 1, cmp); for(int i = 1; i <= n; i++) { if(tot >= 6 * m) break; else if(tot + a[i].l <= 6 * m) { ans += a[i].k * a[i].l; tot += a[i].l; } else { ans += a[i].k * (6 * m - tot); tot = 6 * m; } } printf("%lld", ans); return 0;} 说句闲话:对了,你们有没有注意到 ljt 最近换码风了?我感觉之前那种太紧凑了不好调代码就换了 qwq。 T2《关于 ljt 因为把 Friday 打成 Firday 而痛失 $\\text{100pts}$ 这回事》 直接无语了。 小模拟,觉得这道题不过瘾的可以去做做儒略日,注意闰年的判断,以及初始值($2011.1.1$ 是星期六)。 普通年能被 $4$ 整除且不能被 $100$ 整除的为闰年。(如 $2004$ 年就是闰年,$1901$ 年不是闰年) 世纪年能被 $400$ 整除的是闰年。(如 $2000$ 年是闰年,$1900$ 年不是闰年) 因为数据很大,而且是多组数据(考试范围写的是时间早于 $99999.12.31$ 后来数据只出到了 $3199.12.31$……离谱),我们可以使用一个办法来避免 TLE ——离线。 离线大概就是一次性把所有测试数据都存下来,按照大小排序,这样只用跑一遍从 $2011$ 到 $99999$ 的年份就可以判断完所有数据。 具体看代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#include <cstdio>#include <algorithm>using namespace std;// 2011/01/01: Saturdayconst int maxn =(int)1e5 + 5;struct node { int y, m, d, num, ans; } a[105];int M[2][13] = { {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} };int date = 6;// sunday,monday,tuesday,wednesday,thursday,friday,saturdaychar ans[7][15] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };bool earlier(int y1, int m1, int d1, int y2, int m2, int d2) { if(y1 < y2) return 1; else if(y1 > y2) return 0; else { if(m1 < m2) return 1; else if(m1 > m2) return 0; else { if(d1 < d2) return 1; return 0; } }}bool cmp1(node x, node y) { return earlier(x.y, x.m, x.d, y.y, y.m, y.d); }bool cmp2(node x, node y) { return x.num < y.num; }bool run(int y) { if(y % 4) return 0; else if(y % 100) return 1; else if(!(y % 400)) return 1; return 0;}int main() { freopen("date.in", "r", stdin); freopen("date.out", "w", stdout); int tot = 1, Num = 1; while(scanf("%d %d %d", &a[tot].y, &a[tot].m, &a[tot].d) != EOF) { a[tot].num = tot; ++tot; } --tot; stable_sort(a + 1, a + tot + 1, cmp1); for(int year = 2011; year <= a[tot].y; year++) { for(int month = 1; month <= 12; month++) { int t = run(year) ? 1 : 0; for(int day = 1; day <= M[t][month]; day++) { while(year == a[Num].y && month == a[Num].m && day == a[Num].d) { a[Num].ans = date; ++Num; } date = (date + 1) % 7; } } } stable_sort(a + 1, a + tot + 1, cmp2); for(int i = 1; i <= tot; i++) printf("%s\\n", ans[a[i].ans]); return 0;} T3出题人语文显然需要重修。 这道题是什么呢,大概就是给一个数 $m$,要求从 $1\\sim m$ 中找到一个数 $n$ 使得 $1\\sim n$ 中与 $m$ 互质的数的数量与 $n$ 之比最小。 数据范围是 $1\\le m\\le 10^{40}$,这不仅告诉我们要开高精,还告诉我们这题肯定有一些奇奇怪怪的性质。 通过分析样例: 样例输入 1:10 样例输出 1:6 $6=2\\times 3$ 也就是 $3$ 及以内的所有质数相乘。 样例输入 2:10000000000 样例输出 2:6469693230 $6469693230=2×3×5×7×11×13×17×19×23×29$ 也就是 $29$ 及以内的质数相乘。别问我要怎么看出来,这是 zm 说的,我也不知道。 为什么是 $29$ 呢?因为如果取 $31$ 的话,就超过 $m$ 了,而选用 $29$ 是不超过 $m$ 中最大的那个。 到这里思路就很明显了:线性筛质数,然后疯狂相乘找答案。时间复杂度是 $O\\text{(质数的个数)}$,绝对不会超过 $10^5$。 需要的高精是高精乘和高精比较,你们爱复制板子就去复制板子吧。反正我也是复制的板子。 代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586#include <cstdio>#include <string>#include <cstring>#include <iostream>using namespace std;const int maxn = (int)1e5 + 5;string m;long long g[maxn], tot;bool f[maxn];void Prime() { for (int i = 2; i <= maxn >> 1; i++) { if (!f[i]) g[++tot] = i; for (int j = 1; j <= tot; j++) { long long t = i * g[j]; if (t > maxn) break; f[t] = 1; if (!(i % g[j])) break; } } return;}string mul(string a1, string b1) { int a[10005] = {}, b[10005] = {}, c[10005] = {}; if (a1 == "0" || b1 == "0") return "0"; string c1; int lena = a1.size(); int lenb = b1.size(); for (int i = 0; i < lena; i++) a[lena - i] = a1[i] - '0'; for (int i = 0; i < lenb; i++) b[lenb - i] = b1[i] - '0'; int lenc; for (int i = 1; i <= lena; i++) { for (int j = 1; j <= lenb; j++) { lenc = i + j - 1; c[lenc] += a[i] * b[j]; c[lenc + 1] += c[lenc] / 10; c[lenc] %= 10; lenc++; } } lenc = lena + lenb; while (!c[lenc]) lenc--; while (lenc >= 1) c1 += c[lenc--] + '0'; return c1;}bool Max(string x, string y) { if(x.size() > y.size()) return 1; else if(x.size() < y.size()) return 0; int len = x.size(); for(int i = 0; i <len; i++) { if(x[i] > y[i]) return 1; else if(x[i] < y[i]) return 0; } return 0;}string str(int x) { string a, b; while (x) { a += x % 10 + 48; x /= 10; } for (int i = a.size() - 1; i >= 0; i--) b += a[i]; return b;}int main() { freopen("flower.in", "r", stdin); freopen("flower.out", "w", stdout); cin >> m; Prime(); string qwq = "1"; for(int i = 1; i <= maxn - 5; i++) { string r = qwq; qwq = mul(str(g[i]), qwq); if(Max(qwq, m)) { cout << r; return 0; } } return 0;} T4LIS 板子题,LIS 普通版板子都能写挂我也是服了自己了。 首先初始长度(不斜对角穿)肯定是 $100\\times(m+n)$,然后考虑穿对角线的情况。我们把所有允许穿对角线的方块按照 $x$ 值从小到大排序,然后找 $y$ 值的 LIS 即可。 来自 cgy:sqrt(2) 打成 1.414,精度挂了 $\\text{60pts}$。为 cgy 大佬默哀。 LIS 是板子,不需要我讲了吧? 代码: 12345678910111213141516171819202122232425262728293031#include <cstdio>#include <cmath>#include <algorithm>using namespace std;const int maxk = 1005;struct node { int x, y; } a[maxk];int m, n, k, qaq, dp[maxk]; // LISbool cmp(node x, node y) { return x.x < y.x; }int main() { freopen("metro.in", "r", stdin); freopen("metro.out", "w", stdout); scanf("%d %d %d\\n", &m, &n, &k); for(int i = 1; i <= k; i++) scanf("%d %d", &a[i].x, &a[i].y); stable_sort(a + 1, a + k + 1, cmp); //LIS dp[1] = 1; for(int i = 2; i <= k; i++) { int t = 1; for(int j = 1; j <= i; j++) { if(a[j].y < a[i].y) t = max(t, dp[j] + 1); qaq = max(qaq, t); } dp[i] = t; } printf("%d", (int)round(((m + n) * 100 - qaq * (200 - 100 * (long double)sqrt(2))))); return 0;} T5骗分能拿 $\\text{30pts}$:如果修改的次数大于等于掉头指令的个数,就把所有掉头改成前进,然后疯狂改一个指令根据奇偶性判断是不是能改到前进指令从而判断答案。 正解是 dp: $dp_{i,j,0,0/1}$ 代表执行前 $i$ 条指令,改 $j$ 次指令,目前头朝正轴方向,在 正数区/负数区 离原点的最远距离。 $dp_{i,j,1,0/1}$ 代表执行前 $i$ 条指令,改 $j$ 次指令,目前头朝负轴方向,在 正数区/负数区 离原点的最远距离。 代码鸽掉了。","categories":[{"name":"考试总结","slug":"考试总结","permalink":"https://blog.liynw.top/categories/%E8%80%83%E8%AF%95%E6%80%BB%E7%BB%93/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"贪心","slug":"贪心","permalink":"https://blog.liynw.top/tags/%E8%B4%AA%E5%BF%83/"},{"name":"高精度","slug":"高精度","permalink":"https://blog.liynw.top/tags/%E9%AB%98%E7%B2%BE%E5%BA%A6/"},{"name":"模拟","slug":"模拟","permalink":"https://blog.liynw.top/tags/%E6%A8%A1%E6%8B%9F/"},{"name":"数学","slug":"数学","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E5%AD%A6/"}]},{"title":"CSP2021 刷题记录之模拟板块","slug":"「Record」普及模拟板块","date":"2021-09-22T22:51:31.000Z","updated":"2022-01-30T17:46:57.000Z","comments":true,"path":"posts/815cb1ab/","link":"","permalink":"https://blog.liynw.top/posts/815cb1ab/","excerpt":"","text":"不保证所有代码的正确性,它们仅仅是通过了所有数据点而已。 整体难度:红~黄(PJ 模拟不会有什么难题哒 qwq) T1 计算器的改良AC at 2021-07-31 14:34:08. 难度:黄 解一元一次方程,就是把未知数的系数移到等号左边,常数移到等号右边,然后再除一下就可以了。 我们设置两个变量 $l,r$,分别代表未知数系数计算后的结果和常数的计算结果。最后模拟就可以了。记得“移项变号”,而且往左移和往右移是相反的,如果写成一样了的可以像我一样在任意一边加一个负号,不影响结果。 不过这个模拟还是有些讲究的。 首先要把整个字符串分为 $3$ 个部分:等号左边、等号和等号右边。 先遍历等号左边,如果看到数字了就把这个连续是一段数字的字符给转化为整数类型,然后再看这到底是系数还是常数;再再看正负。关于正负可以使用布尔变量来标记。 如果是系数就甩到 $l$ 变量,是常数就甩到 $r$ 变量。注意怎么甩,要移项变号。这时候可能要移项也有可能不移,要注意。 遇到减号,把布尔变量设为真。 遇到加号,把布尔变量设为假。(因为我们默认的系数和常数的符号是正,所以加号并没有什么用,只需要布尔变量归零就可以了) 遇到字母特判(经过数字的判断之后,这个字母就是系数为 $±1$ 的未知数),也需移项变号。 等号右边同理。 记得存未知数的字母,别像我一样最后有一个测试点未知数只在等号右边,但是我处理那一块的时候没写存未知数字母的语句[捂脸]。 废话不多说,上代码。 Code1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768#include<cstdio>#include<cstring>char fh,a[105];int len,h,l,r;bool is_fu; //用来判断这个系数 or 常数是不是负数int main(){ scanf("%s",a+1); len=strlen(a+1); //等号左边 for(int i=1;i<=len;i++){ int k=0; if(a[i]=='='){ h=i+1; is_fu=0; break; }else if(a[i]=='+') is_fu=0; else if(a[i]=='-') is_fu=1; else if(a[i]>='0'&&a[i]<='9'){ while(a[i]>='0'&&a[i]<='9'){ k=k*10+a[i]-'0'; i++; } i--; if(a[i+1]>='a'&&a[i+1]<='z'||a[i+1]>='A'&&a[i+1]<='Z'){ //未知数 fh=a[i+1]; if(is_fu){ l-=k; is_fu=0; }else l+=k; }else{ //数字 if(is_fu){ r-=k; is_fu=0; }else r+=k; } k=0; } } //等号右边 is_fu=0; for(int i=h;i<=len;i++){ int k=0; if(a[i]=='+') is_fu=0; else if(a[i]=='-') is_fu=1; else if(a[i]>='0'&&a[i]<='9'){ while(a[i]>='0'&&a[i]<='9'){ k=k*10+a[i]-'0'; i++; } i--; if(a[i+1]>='a'&&a[i+1]<='z'||a[i+1]>='A'&&a[i+1]<='Z'){ //未知数 fh=a[i+1]; if(is_fu){ l+=k; is_fu=0; }else l-=k; }else{ //数字 if(is_fu){ r+=k; is_fu=0; }else r-=k; } k=0; } } printf("%c=%.3lf",fh,-r*1.0/l); return 0;} T2 税收与补贴问题AC at 2021-08-06 9:44:39. 难度:黄 我寻思着这出题人语文该从小学重修叭。 大概意思就是先让你补全一个价格和购买人数关系的表,然后在价格上统一加(补贴)或减(收税)一个数,但是购买的人数不变,然后使得政府给出的这个价位获得的利润是所有价位都经过这个变化后中最大的。 实现我们可以用两个数组,一个用来输入,另一个,下标表示价格,数组里的值代表这个下标的价格所对应的人数。 这个问题有两个部分: 1.补全表格(此题最难部分)题目有一个隐藏条件:在任意两个给定了人数的价格之间如果有没有给定人数的价格,那么中间所有没有给定人数的价格的人数都是“均匀地下降”,就是每两个价格所对应的人数差是一样的。 所以,遇到没有输入人数的价格时,就有三种情况: 这个价格小于给定人数的最大价格。 这个价格大于给定人数的最大价格。 这个价格是不合法的。(即小于成本价或者购买人数是负数) 为了避免计算不合法的价格,我们从成本价往上枚举价格,如果计算出来的人数是负数或 $0$ 就立刻跳出循环。 思考如何计算第一种情况。假设我们已经枚举到了价格 $=i$。 我们需要确定这个价格两端最近的已经确定人数的价格是多少,因为我们是从小到大算,所以价格为 $i-1$ 时的价格肯定已经算出。至于比它大的,枚举可以找出,在枚举的同时我们可以用一个 $num$ 变量统计一下这中间价格的数量。 然后我们需要计算它对应的人数。怎么算呢?为了好理解我们把它分成两步: 第一步,算出两两价格之间的差值。 公式:$d=\\dfrac{b_{i-1}-b_R}{num}$,可以根据等差公式得出,也可以自己推(难度不大)。 其中 $b$ 是上述提及实现方式的那个下标代表价格的数组。 第二步,根据 $i-1$ 算出 $i$。 这个就很简单啦, $b_i=b_{i-1}+d$ 即可。 然后再看看第二种情况。这个很简单,只需要在前面的基础上减去最后输入的那个数就可以了。 代码实现如下: 12345678910111213141516int l=a[0].money; while(1){ //只要没有强制退出就一直循环 if(b[l]){ //这个价格输入中有对应,直接跳过 l++; continue; } if(b[l-1]-p<=0) break; //如果这个价格不合法,退出循环 if(l<Max){ //情况一 int R=l,num=1; while(!b[R]) R++,num++; //统计数量,找右端点 b[l]=b[l-1]-(b[l-1]-b[R])/num; }else b[l]=b[l-1]-p; //情况二 l++; Maxr=max(Maxr,l); //寻找合法价格的最右端点 } Maxr--; //需要 -1,因为在此之前 l 加了 1 2.计算答案过了难点我们就可以快乐地模拟了! 枚举补贴/收税的钱数,范围随意,能 A 就行/xyx。 大概思路就是暴力把每一个价位下补贴/收税后的利润做对比,如果政府规定的那个价格是最大的就可以输出。 比较简单(指我错了 $10^9+7$ 遍,细节比较多 (〃>目<)),看代码理解。 1234567891011121314151617181920for(int i=0;i<=MAXN-5;i++){ //补贴i元 int maxnum=0; //这个变量是用来统计最大的获利 for(int j=a[0].money;j<=Maxr;j++){ maxnum=max(maxnum,(j+i-a[0].money)*b[j]); } if(maxnum==(n+i-a[0].money)*b[n]){ //最大获利地数量等于政府规定地价格,输出结束 printf("%d",i); return 0; } //收税i元 maxnum=0; for(int j=a[0].money;j<=Maxr;j++){ maxnum=max(maxnum,(j-i-a[0].money)*b[j]); } if(maxnum==(n-i-a[0].money)*b[n]){ printf("-%d",i); //注意有负号 return 0; } } 最后加上预处理、特判等。 特别注意输入,坑死我……<(  ̄^ ̄)-+——。 Code123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#include<cstdio>#define max(a,b) (a)>(b)?(a):(b)const int MAXN=(int)1e5+5;struct node{int money,num;}a[MAXN];int Max,Maxr,n,p,tot,b[MAXN];int main(){ //输入及预处理 scanf("%d %d %d",&n,&a[0].money,&a[0].num); b[a[0].money]=a[0].num; while(1){ ++tot; scanf("%d %d",&a[tot].money,&a[tot].num); if(a[tot].money==-1&&a[tot].num==-1){ --tot; break; } Max=max(Max,a[tot].money); b[a[tot].money]=a[tot].num; } //test //for(int i=a[0].money;i<=Max;i++) printf("%d %d\\n",i,b[i]); scanf("%d",&p); //补全条件 int l=a[0].money; while(1){ if(b[l]){ l++; continue; } if(b[l-1]-p<=0) break; if(l<Max){ int R=l,num=1; while(!b[R]) R++,num++; b[l]=b[l-1]-(b[l-1]-b[R])/num; }else b[l]=b[l-1]-p; l++; Maxr=max(Maxr,l); } Maxr--; //枚举 for(int i=0;i<=MAXN-5;i++){ //补贴i元 int maxnum=0; for(int j=a[0].money;j<=Maxr;j++){ maxnum=max(maxnum,(j+i-a[0].money)*b[j]); } if(maxnum==(n+i-a[0].money)*b[n]){ printf("%d",i); return 0; } //收税i元 maxnum=0; for(int j=a[0].money;j<=Maxr;j++){ maxnum=max(maxnum,(j-i-a[0].money)*b[j]); } if(maxnum==(n-i-a[0].money)*b[n]){ printf("-%d",i); return 0; } } printf("NO SOLUTION"); return 0;} T3 乒乓球AC at 2021-07-31 14:50:38. 难度:橙 模拟水题,按照一轮一轮枚举。注意一轮结束需要同时满足两个条件,否则不要结束。 这题坑比较多,这里列两个我错过的: 如果给出的字符串刚刚好到一轮结束,需要在后面再输出一个“0:0”(也不知道为什么); 如果第一个字符是“E”记得输出两个“0:0”。 Code123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354#include<cstdio>#include<cmath>#include<cstring> #define max(a,b) (a)>(b)?(a):(b)int len,tot;char s[25],a[100005];int main(){ while(scanf("%s",s+1)!=EOF){ len=strlen(s+1); for(int i=1;i<=len;i++){ if(s[i]==' '||s[i]=='\\n') continue; if(s[i]=='E') goto type1; a[++tot]=s[i]; } } type1: int w,l; if(!tot){ printf("0:0\\n\\n0:0"); return 0; } //11 for(int i=1;i<=tot;i++){ w=0,l=0; while((w<11&&l<11||abs(w-l)<2)&&i<=tot){ w+=(a[i]=='W'); l+=(a[i]=='L'); i++; } i--; if(i==tot){ printf("%d:%d\\n",w,l); if(w==11||l==11) printf("0:0\\n"); break; }else printf("%d:%d\\n",w,l); } printf("\\n"); //21 for(int i=1;i<=tot;i++){ w=0,l=0; while((w<21&&l<21||abs(w-l)<2)&&i<=tot){ w+=(a[i]=='W'); l+=(a[i]=='L'); i++; } i--; if(i==tot){ printf("%d:%d\\n",w,l); if(w==21||l==21) printf("0:0\\n"); break; }else printf("%d:%d\\n",w,l); } return 0;} T4 不高兴的津津AC at 2021-07-30 21:25:42. 难度:红 这真的没什么好说的了,模拟即可 ,有手就行。/cy Code12345678910111213#include<cstdio>#define max(a,b) (a)>(b)?(a):(b) int Max,ans,a,b;int main(){ for(int i=1;i<=7;i++){ scanf("%d %d",&a,&b); if(Max<a+b) ans=i; Max=max(a+b,Max); } if(Max<=8) ans=0; printf("%d",ans); return 0;} T5 花生采摘AC at 2021-07-31 14:12:53. 难度:橙 为什么我觉得这道题应该算这套题里面比较难的了? 首先注意一个大坑:采花生需要时间!(这个我错了很久) 然后如果您认真读题,就会发现它并不是一个 dp,而是一个不大的模拟( ̄y▽, ̄)╭ 。(因为 mjl 归类的是模拟板块,我想如果这是考场上我八成会犯这个错误)。 首先把所有花生的坐标和果子数量存到一个结构体数组里面,然后按照果子数量从大到小排序,因为采摘花生的顺序是按照从大到小排的。 【未完待更 qwq】","categories":[{"name":"做题记录","slug":"做题记录","permalink":"https://blog.liynw.top/categories/%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/"}],"tags":[{"name":"模拟","slug":"模拟","permalink":"https://blog.liynw.top/tags/%E6%A8%A1%E6%8B%9F/"}]},{"title":"P7179 [COCI2014-2015#4] STANOVI 题解","slug":"「Solution」Luogu_P7179","date":"2021-09-20T19:50:10.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/abe4fcec/","link":"","permalink":"https://blog.liynw.top/posts/abe4fcec/","excerpt":"","text":"题目链接 记忆化搜索,其实思路不太难吧,但是优化比较难想,建议评蓝。 题意简述: 有一个 $m\\times n$ 的方格矩阵,把这个矩阵分为若干部分,且要求每一个矩阵都要与边界相邻。令 $k$ 为标准面积,不满意度为{所有矩阵面积减去 $k$ 的平方}之和。求最小的不满意度。 搜索lg 题库里有道题叫生日快乐(这道题是蓝的,但是难度严重虚高,个人觉得最多是绿),其实思路相仿,不过这题要难一些。 我们假设拿到了一个已知所有信息、且满足四周有边界的矩阵,我们要对它进行搜索。 首先我们要解决传参问题:什么算已知条件,而且如何判断这个矩阵是不是满足条件呢? 首先长和宽是有必要的。而且因为这道题要求每一个矩阵都要挨着边界,所以我们需要知道它四面挨着边界的情况。 dfs 函数中传 $6$ 个参数: int 类型:$x$ (矩阵的长),$y$ (矩阵的宽)。 bool 类型:up (此矩阵上面那条边是否挨着边界),down (下面那条边是否挨着边界),left (左边那条边是否挨着边界),right (右边那条边是否挨着边界)。 解决完了 dfs 函数的传参问题,那怎么搜索呢? 我们先来分析出口。 由于题目要求,只要已知一个矩阵四面都没挨着边界,就直接返回一个极大值。 对于一个满足条件且已知的矩阵,有两种思路: 直接把它作为一个最终划分的矩阵; 把它继续分成更小的矩阵。 第一种思路很好解决,直接按照长、宽求就行了。 关键是第二种。 为了方便大家理解,我随便画了一个矩阵举例说明: (Excel 真好用啊,蓝色为边界的外面那一圈) 对于这个矩阵,我们又有两种划分的方法: 横着切; (红色的地方就是能切的地方。) 竖着切。 那我们只需要分两种情况,分别枚举切割的地方,寻找最小值就可以了。(比如说,枚举上面/左边那个矩阵的长/宽) 最后再看一下哪种情况会更好。 注意一个细节:横着切需满足 $x>1$,竖着切需满足 $y>1$。 核心代码大概长这样: 12345678910111213141516171819202122if(!(up||down||left||right)) return inf; //不沿海 ll ans1=pow(x*y-k,2);if(x==1&&y==1) return ans1;ll ans2=inf;bool u1,u2,d1,d2,l1,l2,r1,r2;//横着分开u1=up,u2=0,d1=0,d2=down,l1=l2=left,r1=r2=right;if(x>1){ for(int i=1;i<x;i++){ int t=dfs(i,y,u1,d1,l1,r1)+dfs(x-i,y,u2,d2,l2,r2); ans2=min(ans2,t); }}//竖着分开 u1=u2=up,d1=d2=down,l1=left,l2=0,r1=0,r2=right;if(y>1){ for(int i=1;i<y;i++){ int t=dfs(x,i,u1,d1,l1,r1)+dfs(x,y-i,u2,d2,l2,r2); ans2=min(ans2,t); }}return min(ans1,ans2); 记忆化开一个六维的数组 $dp$,每一维都对应一个传的参数,把 dfs 的值存在里面即可。 这个时候有人就会说了:可是不一样的矩阵可能六个参数都一样,那这个如何判断? 答案是不需要判断。虽然的确这种情况是存在的,但是,如果六个参数都一样,算出来的结果肯定也一样,这不会影响结果,还会减小运行的时间。 剪枝 可行性剪枝 观察一下上面的图,不难发现,有一些情况是不能做横着或者竖着的分割的,比如说这种: 这个东西就不能竖着切,因为如果竖着切了,左边的那个矩阵就不挨着边界了。 那我们可以写出矩阵要横着切和竖着切的条件: 横着:up&&down||left||right 竖着:left&&right||up||down 为什么呢?以横着切举例: 现在四面是否挨着边界不知道。 如果要求成功,就必须满足下列至少一个条件: 上面和下面都挨着边界。 左边挨着边界,或者右边挨着边界。 你们自己去想一想,竖着也类似。 最优性剪枝 很好想,要是一个矩阵的面积小于 $k$,就不继续切了,直接返回。 等价性剪枝 zszz 矩阵是可以转的对吧? 拿到一个矩阵之后,我们可以尝试把它转一下:可以向左旋转 $0^{\\circ},90^{\\circ},180^{\\circ},270^{\\circ}$,每一种旋转都可以查看是否已求出结果,如果有,直接返回。 代码比较简单,但是思考的过程比较绕: 1234if(dp[x][y][up][down][left][right]!=-1) return dp[x][y][up][down][left][right]; //正常 if(dp[x][y][down][up][right][left]!=-1) return dp[x][y][down][up][right][left]; //倒立 if(dp[y][x][left][right][down][up]!=-1) return dp[y][x][left][right][down][up]; //往左转 if(dp[y][x][right][left][up][down]!=-1) return dp[y][x][right][left][up][down]; //往右转 剪枝就这三个。虽然不多,但足以通过这道题。 最后再卡一下常。 Code12345678910111213141516171819202122232425262728293031323334353637383940414243#include<cstdio>#include<cmath>#include<cstring>#define min(a,b) (a)<(b)?(a):(b);#define ll long longconst int maxn=305;const ll inf=(ll)2e9;int n,m,k;ll dp[maxn][maxn][2][2][2][2];ll dfs(int x,int y,bool up,bool down,bool left,bool right){ //bool 类型记录是否四个方向沿海 if(dp[x][y][up][down][left][right]!=-1) return dp[x][y][up][down][left][right]; //正常 if(dp[x][y][down][up][right][left]!=-1) return dp[x][y][down][up][right][left]; //倒立 if(dp[y][x][left][right][down][up]!=-1) return dp[y][x][left][right][down][up]; //往左转 if(dp[y][x][right][left][up][down]!=-1) return dp[y][x][right][left][up][down]; //往右转 if(!(up||down||left||right)) return inf; //不沿海 ll ans1=pow(x*y-k,2); if(x==1&&y==1||x*y<k) return dp[x][y][up][down][left][right]=ans1; ll ans2=inf; bool u1,u2,d1,d2,l1,l2,r1,r2; //横着分开 if(x>1&&(up&&down||left||right)){ u1=up,u2=0,d1=0,d2=down,l1=l2=left,r1=r2=right; for(register int i=1;i<x;i++){ int t=dfs(i,y,u1,d1,l1,r1)+dfs(x-i,y,u2,d2,l2,r2); ans2=min(ans2,t); } } //竖着分开 if(y>1&&(left&&right||up||down)){ u1=u2=up,d1=d2=down,l1=left,l2=0,r1=0,r2=right; for(register int i=1;i<y;i++){ int t=dfs(x,i,u1,d1,l1,r1)+dfs(x,y-i,u2,d2,l2,r2); ans2=min(ans2,t); } } return dp[x][y][up][down][left][right]=min(ans1,ans2);}int main(){ memset(dp,-1,sizeof(dp)); scanf("%d %d %d",&m,&n,&k); printf("%lld",dfs(m,n,1,1,1,1)); return 0;}","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"搜索","slug":"搜索","permalink":"https://blog.liynw.top/tags/%E6%90%9C%E7%B4%A2/"}]},{"title":"UVA 10004 Bicoloring 题解","slug":"「Solution」UVA10004","date":"2021-09-20T19:44:51.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/2c4f5024/","link":"","permalink":"https://blog.liynw.top/posts/2c4f5024/","excerpt":"","text":"题意简述给定多个连通无向图,判断这些图是不是二分图。 Step 1 建图$2\\le n\\le 199$,邻接矩阵和邻接表大家都随意,根据自己的习惯来。我这里用的是 vector 实现邻接表。 另外,因为结点的编号是从 $0\\sim n-1$,我不太习惯,于是在建图的时候就擅自每个结点的编号都 $+1$,把编号变成了 $1\\sim n$。 12345for(int i=1;i<=m;i++){ scanf("%d %d",&u,&v); map1[u+1].push_back(v+1); map1[v+1].push_back(u+1);} Step 2 发出开始染色的指令(没错我在凑字数) Tips:以下内容都把结点编号当成 $1\\sim n$。 因为是连通图,所以我们从 $1$ 结点开始染色。 我们定义一个 $a$ 数组,$a_i$ 代表 $i$ 结点的颜色:$0$ 代表还没有染色,$1$ 和 $2$ 代表两种不同的颜色。$1$ 号结点随便染一个颜色。 函数里可以传一个参数(遍历到的结点编号),也可以传两个参数(遍历到的结点编号和这个结点染的颜色)。我传的是两个参数。 主函数的执行异常简单,用三目运算符可以压缩为一行: 1printf("%s",dfs(1,1)?"BICOLORABLE\\n":"NOT BICOLORABLE\\n"); Step 3 染色染色我们采用 dfs 来执行。其实 bfs 也可以实现,但是显然 dfs 码量比 bfs 少亿丶丶,而且 bfs 要开结构体队列,内存更大,所以我们就使用 dfs 了。 dfs 的思路和遍历图差不多。因为只有两个颜色,所以定了 $1$ 结点的颜色后,染色的方案是唯一的。我们先找到给定点的所有直接后继,然后一个一个看: 如果它的某个直接后继与它要求染的颜色相同,这个图就不是二分图,直接 return false 即可。 如果它的某个直接后继和他要求染的颜色不同,那么直!接!跳!过! 如果它的某个直接后继没有染色,那么就给这个直接后继染上与它不同的颜色,接着继续遍历这个刚被染色的直接后继的直接后继。如果此直接后继后面染色不成功,说明这个图不是二分图(因为方案唯一!),直接 return false。 如果遍历完了,就 return true。 12345678910int f(int x){return (x==1)?2:1;} //取相反颜色bool dfs(int x,int color){ a[x]=color; for(int i=0;i<map1[x].size();i++){ if(a[map1[x][i]]==color) return 0; if(a[map1[x][i]]==f(color)) continue; if(!dfs(map1[x][i],f(color))) return 0; } return 1;} Step 4 其它一定要初始化!一定要初始化!一定要初始化! 要初始化的是图和记录染色的数组。 12memset(map1,0,sizeof(map1)); //vector可以直接这么初始化memset(a,0,sizeof(a)); 另外这是邻接表的检查代码: 12345for(int i=1;i<=n;i++){ printf("%d: ",i); for(int j=0;j<map1[i].size();j++) printf("%d ",map1[i][j]); printf("\\n");} Code最后双手献上 AC 代码: 12345678910111213141516171819202122232425262728293031323334353637#include<cstdio>#include<vector>#include<cstring>using namespace std;vector<int> map1[205];int n,m,u,v,a[205];int f(int x){return (x==1)?2:1;}bool dfs(int x,int color){ a[x]=color; for(int i=0;i<map1[x].size();i++){ if(a[map1[x][i]]==color) return 0; if(a[map1[x][i]]==f(color)) continue; if(!dfs(map1[x][i],f(color))) return 0; } return 1;}int main(){ while(10000000000000000000ull>9999999999999999999ull){ //没问题是吧 memset(map1,0,sizeof(map1)); memset(a,0,sizeof(a)); scanf("%d",&n); if(!n) return 0; scanf("%d",&m); for(int i=1;i<=m;i++){ scanf("%d %d",&u,&v); map1[u+1].push_back(v+1); map1[v+1].push_back(u+1); } /*for(int i=1;i<=n;i++){ printf("%d: ",i); for(int j=0;j<map1[i].size();j++) printf("%d ",map1[i][j]); printf("\\n"); }*/ printf("%s",dfs(1,1)?"BICOLORABLE.\\n":"NOT BICOLORABLE.\\n"); } return 0;} 提交记录:Link(没错我 UVA 登上了)","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"图论","slug":"图论","permalink":"https://blog.liynw.top/tags/%E5%9B%BE%E8%AE%BA/"}]},{"title":"于是他们玄学的证明开始了","slug":"「Math」Prove1","date":"2021-09-20T19:28:06.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/296713f0/","link":"","permalink":"https://blog.liynw.top/posts/296713f0/","excerpt":"","text":"今天上课讨论的问题。 求证:任意一个位数为偶数的回文数,都可以被 $11$ 整除。 PS:此篇仅是笔者对于证明过程的理解,并不是标准的数学证明。 其实是因为笔者太太太菜了所以才需要这一篇文章来帮助理解的 qwq,相信这个简单的问题各位大佬们都没问题 这个证明分为两个部分。 Prat1首先两位数的回文数一定是 $11$ 的倍数。为什么呢?观察可得。 我们知道两位的回文数一定是形如这样的:$aa$,$aa \\div 11=a$。由于 $a$ 一定是一个大于 $0$ 小于 $10$ 的整数,所以可以证明两位数的回文数是可以被 $11$ 整除的。 那么,四位的回文数就可以被这么表示: $\\overline{abba}$ 然后我们: \\begin{equation*} \\label{eqn2} \\begin{split} \\overline{abba}&=1000a+100b+10b+a\\\\ &=1001a+110b\\\\ &=1001a+11\\times 10b \\end{split} \\end{equation*}因为 $11\\;|\\;11\\times 10b$,所以我们只需证明 $11\\;|\\;1001a$ 即可,也就是证明 $11\\;|\\;1001$。 再推广,假设我们已经证明了 $4$ 位的回文数是 $11$ 的倍数。 于是 $6$ 位的回文数就变成了这样: \\begin{equation*} \\label{eqn} \\begin{split} \\overline{abccba}&=100000a+10000b+1000c+100c+10b+a\\\\ &=100001a+10\\times\\overline{bccb} \\end{split} \\end{equation*}于是我们只需要证明 $100001\\;|\\;11$。 ………… 这么推下去的话,我们会发现,这个过程本来就是一个递归的过程,每一次都把两端的数字去掉后,可以从两位的回文串推回来证明这个去掉两段后的回文数能被 $11$ 整除。现在我们只需要证明一个问题,那就是: 求证:$\\begin{matrix} \\underbrace{ 100\\cdots 001 } \\\\ 2n+2 \\end{matrix}\\;|\\;11$,其中 $n$ 为正整数。 Part 2法一(from ywy orz)从上面的推导中我们可以发现中间的 $0$ 的个数一定是偶数。先列竖式。 \\qquad 99\\cdots\\ \\ \\,\\qquad\\quad\\ \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\ \\ \\,11\\ / \\quad 100\\cdots 00199\\ \\ \\,\\qquad\\quad\\ \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\ \\ \\,\\qquad 100\\ \\ \\,\\quad\\ \\, 99\\ \\ \\,\\qquad\\quad\\ \\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\_\\ \\ \\,\\qquad\\quad 100\\ \\ \\,\\qquad\\quad \\cdots\\cdots($\\LaTeX$ 除法竖式贼不好打……建议还是在自己的本子上列……) 对齐不难发现:每进行一次减法之后,被除数就会少两个 $0$,这样,如果中间的 $0$ 的个数是偶数个,那么最后这些 $0$ 会被全部消失,剩下两个 $1$ 组成 $11$,于是它就可以被 $11$ 整除了。 法二(自己想出来的) \\begin{equation*} \\label{eqn4} \\begin{split} 100 \\cdots 001 &= 10^{2n+1}+10^{2n}+\\ldots +10^1+10^0-\\big(10^{2n}+10^{2n-1}+\\ldots +10^2+10^1\\big)\\\\ &=(10^{2n+1}+10^{2n})+\\ldots +(10^1+10^0)-\\big(10^{2n}+10^{2n-1}+\\ldots +10^2+10^1\\big)\\\\ &=11 \\times 10^{2n} + 11\\times 10^{2n-2} + \\ldots 11\\times 10^0-10\\times \\big(11\\times 10^{2n-2} + 11\\times 10^{2n-4} + \\ldots 11\\times 10^0\\big)\\\\ &=11 \\times \\Big( 10^{2n} + 10^{2n-2}+ \\ldots + 10^2 + 10^0- 10\\times 11\\times \\big( 10^{2n-2} + 10^{2n-4} + \\ldots 10^0 \\big) \\Big) \\end{split} \\end{equation*} 就这样吧。","categories":[{"name":"证明","slug":"证明","permalink":"https://blog.liynw.top/categories/%E8%AF%81%E6%98%8E/"}],"tags":[{"name":"数学","slug":"数学","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E5%AD%A6/"}]},{"title":"P2470 压缩 题解","slug":"「Solution」Luogu_P2470","date":"2021-09-20T19:21:28.000Z","updated":"2021-09-20T19:24:47.000Z","comments":true,"path":"posts/90cacd67/","link":"","permalink":"https://blog.liynw.top/posts/90cacd67/","excerpt":"","text":"前言题目传送门 正解:区间/线性 dp(本篇题解介绍线性做法) 人生第一道紫题! 也是 7.17 考试看自闭了就没做的 T4,结果没想到是紫,虽然是一道水紫呢…… 考试的 T5 是跳房子,蓝题 qwq。要不是前三题比较简单 + 骗分好骗(指靠直接输出字符串长度骗了十分)就真的自闭了。 题解我们观察到一个字符串压缩的程度和 “M”,“R”的个数是有关的,尤其是开始一段压缩区间的“M”,非常的重要,因为它的位置决定了压缩串的长度,也直接决定了最后字符串的长度。非常好的条件,可以利用。 所以,定义 $dp$ 数组状态为: $dp_{i,j}$ 代表前 $i$ 个字符,上一个“M”的位置在第 $j$ 个字符前所能达到的最短长度。 $dp$ 数组的初始值: 当只有一个字符时,“M”只有可能在第一个字符前,而且长度为 $1$。所以,$dp_{1,1}=1$。 于是我们可以分析出以下三种情况。 直接在前面 $i-1$ 个字符的情况下,加上一个字符。 添加压缩的字符串。 新建一个压缩字符串部分的开头。 part1如果只是直接加一个,这样 $j$ 的值是不会变的,因为我们没有考虑“M”和“R”的情况。这样得到的答案就是 $dp_{i-1,j}+1$。 part2我们知道,当且仅当某两个字符子串完全相同时,我们才能对这两个字符串进行一次压缩。 至于这个压缩的地方加在哪里呢?因为最近的“M”的位置已经确定,所以这个“M”(坐标为 $j-1$ 和 $j$)之前(并不包括第 $j$ 个字符)的所有字符串情况已经确定,无法改动(因为 dp 不能有后效性)。 所以我们确定被复制了一遍的区域在 $[j,i]$,从中间切开这份字符串,看看两边是不是相等的,如果是就可以考虑这种情况。注意这段区域的的长度为奇数是是一定不可能的。 判断字符串是否相等的函数很简单,就是这样: 1234567bool check(int l,int mid,int r){ if((r-l+1)&1) return 0; //如果长度是奇数直接不可能 for(int i=0;i<=mid-l;i++){ if(a[l+i]!=a[mid+1+i]) return 0; //出现不一样的字符 } return 1; //全部都一样} 然后考虑如何状态转移,很明显,这样的长度就是 $j$ 之前的那些字符的最短长度 + 压缩后 $[j,i]$ 段的长度 + $1$(那个“R”字符)。转化为情况二的动态转移方程即为: dp_{i,j}=\\min_{j=1}^{i-1}\\{dp_{\\lfloor \\frac{i+j}{2} \\rfloor,j}\\}+1其中 $\\lfloor \\dfrac{i+j}{2} \\rfloor$ 表示的是被切成两段的那一部分的前面那个字符串的最后一个字符串的下标(好绕啊)。 part3因为要新建一个“M”,意思就是再开一个可重复的字符串并将新开的“M”的下标作为 $dp$ 数组的第二个值。因为我们枚举到 $i$ 了,所以最方便的就是把这个“M”加在 $i-1$ 和 $i$ 之间,这样每一次循环都能考虑到一层,就全部考虑到了。 那转移方程是什么呢?我们继续观察,现在我们考虑的范围就已经缩小到了 $1$ 到现在添加的“M”(坐标为 $i-1$ 和 $i$ 之间)中间的这一段。很明显 $i$ 已经在这个“M”后面了,此时我们不需要考虑,所以只需考虑 $[1,i-1]$ 压缩后的最小长度。 那第二个下标呢?因为考虑前面的时候并没有考虑添加的“M”(这是两个情况),所以依然是枚举 $j$。 对了,这个值还要 $+2$,一个是添加的“M”字符,另一个是下标为 $i$ 的字符。(是的!这个也要算进去!因为此字符是在添加的“M”后面的,前面的式子并没有考虑到)。 所以,情况三的转移方程是: dp_{i,i}=\\min_{j=1}^{i-1}\\{dp_{i-1,j}+2\\}将三个合并一下,不过记得,要保存前面情况的最优解。除了上述的特殊初始值外,因为求的是最小值,所以 $dp$ 初始值设为 $\\text{INF}$(极大值)。 Code1234567891011121314151617181920212223242526272829303132#include<cstdio>#include<cstring>#include<climits> // INT_MAX的头文件#define min(a,b) (a)<(b)?(a):(b)int n,dp[505][505];char a[505];bool check(int l,int mid,int r){ if((r-l+1)&1) return 0; for(int i=0;i<=mid-l;i++){ if(a[l+i]!=a[mid+1+i]) return 0; } return 1;}int main(){ //freopen("compress.in","r",stdin); //freopen("compress.out","w",stdout); memset(dp,127,sizeof(dp)); dp[1][1]=1; scanf("%s",a+1); n=strlen(a+1); for(int i=2;i<=n;i++){ for(int j=1;j<i;j++){ dp[i][j]=dp[i-1][j]+1; if(check(j,(j+i-1)/2,i)) dp[i][j]=min(dp[i][j],dp[(i+j-1)/2][j]+1); } for(int j=1;j<i;j++) dp[i][i]=min(dp[i][i],dp[i-1][j]+2); } int ans=INT_MAX; for(int i=1;i<=n;i++) ans=min(ans,dp[n][i]); printf("%d",ans); return 0;} 写在最后考试竟然有个 dalao (zdj 大佬!)写出来了啊……%%%。 虽然听着不难但是思路很难想呢 qwq。 最后请随手点个赞吧,毕竟赠人玫瑰手有余香嘛。","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"2021.7.17 考试总结","slug":"「ExamSummary」20210717","date":"2021-09-20T19:01:37.000Z","updated":"2022-02-01T10:09:31.000Z","comments":true,"path":"posts/c94977d4/","link":"","permalink":"https://blog.liynw.top/posts/c94977d4/","excerpt":"","text":"前言得分:$100+90+100+10+0=300$ 排名 $\\text{rk9}$ 只能说把我这个月的 $rp$ 都用光了…… (话说要是我 T2 不是因为没特判丢了 $10$ 分我就可以 $\\text{rk6}$ 了 qwq 考试过程$00:00$ 考试开始电脑出毛病了,收不到试卷( 稳了下心态,告诉了 mjl,然后他用他的 U 盘给我拷到电脑上了…… $00:05$ 开始看试卷。看到 T1 先推了一下发现了规律,但是看了一下数据以为循环会超时(最多就 $60^+$次循环怎么会超呢我又被降智了真的是),然后跑过去看 T2。 $00:10$ 看到 T2 挺适合记忆化暴搜的就写了,然后过了样例之后手造了一个极限数据看时间也没超限就跑了(然鹅并没有加特判 qwq。 $00:25$ 回头看 T1,心想着就先打一个 log2 试试有没有这个函数吧……(顺便加了个 cmath)结果发现真的有?太好了,然后测了一下样例发现没过,经过手动分析和手动打表发现要加向上取整(虽然不知道为啥)然后把 T1 写出来了。 $00:35$ 看了一下后面 $3$ 道,等等,T5 我好像见过,是区间 dp……但是想不起来了,再加上我记得它是道蓝题(应该是吧),然后 T4 又看得我自闭,就去切 T3 了(本来看到树不想做的……) $00:40$ 惊奇发现需要建树(其实并不需要),然而我又不会根据两个遍历的序列建树……考前问了 mjl 的,他也给我讲了,可是我没理解。 没办法只能硬着头皮上了…… $01:10$ 程序弄好了,然后,递归层数超限了。 $01:30$ bug 没改过来,然后突然发现建树要用 bfs。当时脑子 what 一心只想建树没想到 bfs 直接输出就行了 qwq 心灰意冷把 dfs 全删了之后开始写 bfs…… $01:45$ rnm bfs 还是错的!(核心代码没改……) $02:00$ 草……原来我分析的时候把中序和后序搞混了吗……改过来就好了。 $02:30$ 分析完了怎么把孩子求出来,写好了结果发现没用,又不想删掉,就注释掉了(喜提 $\\text{3k}$ 代码……)。 $02:45$ 添加了层数的条件并修好了几个小 bug,过了样例。 $02:50$ 自己造了几组数据,都没有问题,准备去看后面骗分啦。 $03:00$ 草草草没多少时间了!赶紧看了看 T4,没思路,于是开始写 T5 $O(n^3)$ 暴力……并且过了两个样例。 $03:20$ 开始养老,直至结束。 题解T1想到思路应该是不难的,但是要证明……emm…… 首先观察可得每个小白鼠都有死和活两种情况,所以,$k$ 只小白鼠可以找出最多 $2^k$ 瓶酒中的毒药。 进一步,有 $n$ 瓶毒酒,当且仅当 $k$ 满足 $2^{k-1}<n\\le 2^k$ 时,$k$ 为 $n$ 的正确答案。 那怎么证明呢?用二进制。 假如说有 $6$ 瓶酒,我们把它们编号为 $0,1,2,3,4,5$。 二进制: 0\\ \\ \\ \\ \\ 0001\\ \\ \\ \\ \\ 0012\\ \\ \\ \\ \\ 0103\\ \\ \\ \\ \\ 0114\\ \\ \\ \\ \\ 1005\\ \\ \\ \\ \\ 101酒的编号的二进制的 $1/0$ 其实代表了这一位代表的老鼠(最高位代表第一只老鼠,后面以此类推)喝/不喝这瓶酒。 那么就推出了方案: 第一只喝 $4,5$ 第二只喝 $2,3$ 第三只喝 $1,3,5$ 所以也可以看出有些情况有多种解是因为 $0\\sim 2^k-1$ 中选 $n$ 个不同的数字可以任意选,可能有多种选法。 关于代码的实现,异常简单,异常暴力,可以用 cmath 头文件里的 log2 函数。记得向上取整和开 long long。 Code1234567891011#include<cstdio>#include<cmath>#define ll long longll bottle,ans;int main(){ freopen("wine.in","r",stdin); freopen("wine.out","w",stdout); scanf("%lld",&bottle); printf("%lld",(ll)ceil(log2(bottle))); return 0;} T2记忆化暴搜。记得特判 $-1$。 $dp_{i,j}$ 代表前 $i$ 首曲子弹完音量为 $j$ 的情况是否存在,如果存在直接存 $j$ 更加方便。 Code1234567891011121314151617181920212223#include<cstdio>#include<cstring>#define max(a,b) (a)>(b)?(a):(b)int begin,MAX,n,c[55],dp[55][1005];int dfs(int t,int v){ if(t==n+1) return v; if(dp[t][v]!=-1) return dp[t][v]; int a=0,b=0; if(v-c[t]>=0) a=dfs(t+1,v-c[t]); if(v+c[t]<=MAX) b=dfs(t+1,v+c[t]); return dp[t][v]=max(a,b);}int main(){ freopen("volume.in","r",stdin); freopen("volume.out","w",stdout); memset(dp,-1,sizeof(dp)); scanf("%d %d %d",&n,&begin,&MAX); for(int i=1;i<=n;i++) scanf("%d",&c[i]); int t=dfs(1,begin); if(!t) printf("-1"); else printf("%d",t); return 0;} T3bfs 每层遍历,需要记录层数。 那如何根据后序遍历和中序遍历来确定一棵树呢? 首先我们知道后序遍历是按照“左——右——根”的顺序遍历的,所以每一个部分的根节点都必然在那个区域的最后。在中序遍历里找到这个根节点,把左右两边分为左子树和右子树,然后再在后序遍历里面找到这两个子树的节点所在的区域(它们一定是连续的)。然后再继续求解两个子树……(注意因为是 bfs 所以这一层的会先求解完再求解子树)。 我打得很麻烦,相当于把树建出来了,删掉注释部分就差不多可以了 qwq。 Code123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104#include<cstdio>#include<queue>#include<vector>#define max(a,b) (a)>(b)?(a):(b)using namespace std;struct jd{ int data,l,r,f;}tree[305]; struct node{ int L,R,root,midr,f;}t1,t2;int n,t;int m[305],l[305];void find_tree(int a,int b,int c,int d,int e){ queue<node> q; t1.root=a,t1.L=b,t1.R=c,t1.midr=d,t1.f=e; q.push(t1); while(!q.empty()){ t1=q.front(); q.pop(); int mid; for(int i=1;i<=n;i++){ if(l[t1.root]==m[i]){ mid=i; break; } } //存结点 tree[++t].data=l[t1.root]; tree[t].f=t1.f; int long_r=t1.midr-mid; //左子树入队 if(t1.R-long_r-t1.L){ t2.L=t1.L,t2.R=t1.R-long_r-1,t2.root=t1.R-long_r-1,t2.midr=mid-1,t2.f=t1.f+1; //tree[t].l=l[t2.R]; q.push(t2); }else tree[t].l=-1; //右子树入队 if(long_r){ t2.L=t1.root-long_r,t2.R=t1.root-1,t2.root=t1.root-1,t2.midr=t1.midr,t2.f=t1.f+1; //tree[t].r=l[t2.R]; q.push(t2); }else tree[t].r=-1; } return;}int main(){ freopen("z.in","r",stdin); freopen("z.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&m[i]); for(int i=1;i<=n;i++) scanf("%d",&l[i]); find_tree(n,1,n,n,1); //根据 l 和 r 的 data 值对应其下标 /*for(int i=1;i<=n;i++){ //左孩子 if(tree[i].l!=-1){ int s=0; for(int j=1;j<=n;j++){ if(tree[i].l==tree[j].data){ s=j; break; } } tree[i].l=s; } //右孩子 if(tree[i].r!=-1){ int s=0; for(int j=1;j<=n;j++){ if(tree[i].r==tree[j].data){ s=j; break; } } tree[i].r=s; } }*/ //test //for(int i=1;i<=n;i++) printf("%d %d %d %d %d\\n",i,tree[i].data,tree[i].l,tree[i].r,tree[i].f); //输出 int maxf=0; for(int i=1;i<=n;i++) maxf=max(maxf,tree[i].f); printf("%d ",tree[1].data); int l=2,r=1; for(int i=2;i<=maxf;i++){ vector<int> print; l=r+1,r=l; while(tree[r].f==i) r++; r--; //printf("%d %d %d\\n",i,l,r); if(i&1){ // <- for(int j=r;j>=l;j--) printf("%d ",tree[j].data); }else{ // -> for(int j=l;j<=r;j++) printf("%d ",tree[j].data); } } return 0;}/*74 2 5 1 6 3 74 5 2 6 7 3 1*/ T4Click here! T5二分 + $\\text{check}$ 函数。 二分需要的金币数量,这个没啥好说的。 $\\text{check}$ 函数的话,我没用单调队列的优化,而是 dp。我的想法是用 $dp_i$ 代表跳到第 $i$ 个格子能获得的最大数字之和。于是用两层循环,第一层 $i$ 从 $1$ 到 $n$,代表走到第几个格子了,第二层 $j$ 把走过的都扫一遍,然后求最大值:dp[i]=max(dp[i],dp[j]+a[i]位置的数字大小)。 注意如果目前枚举到的地方比较靠近边缘,有可能是没有办法跳到的,所以可以加一个关于是否能跳到的优化,如果跳不到可以直接 break 掉第二层循环。 Code123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051#include<cstdio>#include<cstring>#define int long long#define max(a,b) (a)>(b)?(a):(b)const int MAXN=(int)5e5+5;int n,k,d,sum,l,r;int dp[MAXN];struct square{ int x,s;}a[MAXN];//check函数bool check(int mid){ memset(dp,128,sizeof(dp)); l=k-mid; r=k+mid; dp[0]=0; for(int i=1;i<=n;i++){ for(int j=i-1;j>=0;j--){ if(a[i].x-a[j].x>r) break; if(a[i].x-a[j].x<l) continue; dp[i]=max(dp[i],dp[j]+a[i].s); if(dp[i]>=d) return 1; } } return 0;}//二分函数 int f(int l,int r){ if(l>r) return 0; if(l==r) return l; int mid=(l+r)>>1; if(check(mid)) return f(l,mid); else return f(mid+1,r);}signed main(){ freopen("jump.in","r",stdin); freopen("jump.out","w",stdout); scanf("%lld %lld %lld",&n,&k,&d); for(int i=1;i<=n;i++){ scanf("%lld %lld",&a[i].x,&a[i].s); sum+=(a[i].s>0ll)?a[i].s:0ll; } //printf("%lld %lld\\n",sum,d); if(sum<d){ printf("-1"); return 0; } int t=f(0ll,1000ll); printf("%lld",t); return 0;} 总结感觉还行?就是 T2 没特判,T5 暴力没骗到分…… 去学 whk 了,拜拜ヾ(•ω•`)o~","categories":[{"name":"考试总结","slug":"考试总结","permalink":"https://blog.liynw.top/categories/%E8%80%83%E8%AF%95%E6%80%BB%E7%BB%93/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"图论","slug":"图论","permalink":"https://blog.liynw.top/tags/%E5%9B%BE%E8%AE%BA/"},{"name":"数学","slug":"数学","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E5%AD%A6/"},{"name":"二分答案","slug":"二分答案","permalink":"https://blog.liynw.top/tags/%E4%BA%8C%E5%88%86%E7%AD%94%E6%A1%88/"}]},{"title":"CF559B Equivalent Strings 题解","slug":"「Solution」CF559B","date":"2021-08-18T15:25:49.000Z","updated":"2022-01-30T17:55:40.000Z","comments":true,"path":"posts/e8a84466/","link":"","permalink":"https://blog.liynw.top/posts/e8a84466/","excerpt":"前言题目传送门 正解:模拟,递归。 考试的 T4,还是想复杂了 qwq。 这题不要用 STL,会被卡常数……","text":"前言题目传送门 正解:模拟,递归。 考试的 T4,还是想复杂了 qwq。 这题不要用 STL,会被卡常数…… 题意简述翻译够简了。 对了给一下样例解释的翻译: 第一个样例的第一组测试数据中,对于 $a=aaba$ 和 $b=abaa$,可以分成 $a1=aa,a2=ba,b1=ab,b2=aa$;其中 $a1$ 和 $b2$ 全等。对于 $a=ba$ 和 $b=ab$,可以分成 $a1=b,a2=a,b1=a,b2=b$;其中 $a1$ 和 $b2$ 全等,$a2$ 和 $b1$ 全等。所以 $aaba$ 和 $abaa$ 相似。 第一个样例的第二组测试数据中,$aabb$ 和 $abab$ 不满足相似。 分析鉴于数据的特殊性 (简称水),我们可以直接按照题意递归即可。 因为输入的是两个字符串,而每次递归都需要两个新的字符串,而这两个新的字符串都是在以前的字符串上截取一段形成的。所以,我们根本不需要传字符串,只需要传在输入的字符串中截取的部分开始、结束的下标即可。 当然,因为每次判断都要传两个字符串,所以需要有两对参数,这里,$l1,r1$ 代表第一个字符串(从输入的第一个字符串中截取),$l2,r2$ 代表第二个字符串(从输入的第二个字符串中截取)。 首先,两个不同的判断条件打成两个函数 $\\operatorname{f1}$ 和 $\\operatorname{f2}$,分别判断奇数和偶数字符串长度的相似判定。 $\\operatorname{f1}$ 的实现是很简单的,只需要逐字判断是否相等即可。 不过需要注意细节,在计算字符串的长度时,不需要 $+1$。具体原因:本来计算长度的时候是要 $+1$ 的,但是因为 $l1$ 和 $l2$ 已经提供了字符串开始的地方,所以我们在这两个数的基准上加的数就是从 $0\\sim r1-l1$ 共 $r1-l1+1$ 个数字,就不需要 $+1$ 了。 具体函数如下: 12345bool f1(int l1,int r1,int l2,int r2){ int t=r1-l1; //计算需要枚举判断的长度 for(int i=0;i<=t;i++) if(a[l1+i]!=b[l2+i]) return 0; //不一样直接返回 return 1; //所有的都一样} 接着分析较难的递归函数 $\\operatorname{f2}$。这个函数也是我们在主函数中调用的函数。 首先看传过来的字符串长度是奇数还是偶数。如果是奇数,直接返回 $\\operatorname{f1}$ 的判断就可以了。 如果是偶数,那么就需要判断一分为二之后是否相似。定义两个变量 $mid1,mid2$ 分别表示两个字符串中间的下标,也就是分开的地方(注意这两个变量表示的是分开后前面那个字符串的最后一个元素),接着根据题意模拟即可,因为有两组配对,所以两组都要判断。注意先 && 再 ||。 这个地方容易打错,记得好好检查。 (对了提醒大家一定要记得不要把函数名给打掉了,我就是这么错的 qwq。) $\\operatorname{f2}$ 的代码如下(别在意多余的空格,因为放一个框框里怕是有点不美观,我格式化了一下代码): 1234567bool f2(int l1, int r1, int l2, int r2) { if ((r1 - l1 + 1) & 1) return f1(l1, r1, l2, r2); int mid1 = (l1 + r1) >> 1, mid2 = (l2 + r2) >> 1; return f2(l1, mid1, l2, mid2) && f2(mid1 + 1, r1, mid2 + 1, r2) || f2(l1, mid1, mid2 + 1, r2) && f2(mid1 + 1, r1, l2, mid2);} 最后写好主函数,就可以把这道题切了 qwq。 Code123456789101112131415161718192021222324252627#include <bits/stdc++.h>using namespace std;char a[200005], b[200005];int len;bool f1(int l1, int r1, int l2, int r2) { int t = r1 - l1; for (int i = 0; i <= t; i++) if (a[l1 + i] != b[l2 + i]) return 0; return 1;}bool f2(int l1, int r1, int l2, int r2) { if ((r1 - l1 + 1) & 1) return f1(l1, r1, l2, r2); int mid1 = (l1 + r1) >> 1, mid2 = (l2 + r2) >> 1; return f2(l1, mid1, l2, mid2) && f2(mid1 + 1, r1, mid2 + 1, r2) || f2(l1, mid1, mid2 + 1, r2) && f2(mid1 + 1, r1, l2, mid2);}int main() { scanf("%s %s", a + 1, b + 1); len = strlen(b + 1); if (f2(1, len, 1, len)) printf("YES\\n"); else printf("NO\\n"); return 0;} 写在最后题目不难,细节有点多。大家打代码一定一定要注意细节啊 awa。","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"递推,递归","slug":"递推,递归","permalink":"https://blog.liynw.top/tags/%E9%80%92%E6%8E%A8%EF%BC%8C%E9%80%92%E5%BD%92/"}]},{"title":"AT4828 [ABC152D] Handstand 2 题解","slug":"「Solution」AT4828","date":"2021-08-18T14:06:27.000Z","updated":"2022-01-30T17:57:51.000Z","comments":true,"path":"posts/354a3bec/","link":"","permalink":"https://blog.liynw.top/posts/354a3bec/","excerpt":"前言题目链接 来一点不一样的方法。 正解:动态规划 / 打表数据暴力分析 考试半小时想出方法,最后输在了两个细节上。 写一篇题解以此纪念。","text":"前言题目链接 来一点不一样的方法。 正解:动态规划 / 打表数据暴力分析 考试半小时想出方法,最后输在了两个细节上。 写一篇题解以此纪念。 打表暴力程序最开始打的暴力对拍,没想到最后只能交这个上去了。 思路:两层循环枚举两个数,判断是否符合要求。 Code(第一种)12345678910111213141516171819202122232425#include<bits/stdc++.h>#define ll long longusing namespace std;int n;ll ans;bool check(int x,int y){ int c=x%10,d=y%10; while(x>=10) x/=10; while(y>=10) y/=10; if(x==d&&y==c) return 1; else return 0;}int main(){ //freopen("out1.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++){ if(!i%10) continue; for(int j=1;j<=n;j++){ if(check(i,j)) {//printf("%d %d\\n",i,j); ans++;} } } printf("%lld",ans); return 0;} 动态规划这个方法很简单啊!!! $dp_{i,j}$ 代表以 $i$ 开头以 $j$ 结尾的不超过 $n$ 的数的个数。 求一个数字首位的函数: 1234int one(int m){ while(m>=10) m/=10; return m;} 因为要保证 $1\\le m\\le 9$,所以 $dp$ 开 $10\\times 10$ 即可。 最后 $9\\times 9$ 的循环枚举满足题意的数量。 因为要求两个数的开头结尾互相对应,所以若一个数以 $i$ 开头,以 $j$ 结尾,那么它就有 $dp_{j,i}$ 个数对。而这样的数一共有 $dp_{i,j}$ 个,根据小学学的可能性总数需要用乘法,可以看出前面是 $i\\ldots j$ 数字的数对个数为 $dp_{i,j}\\times dp_{j,i}$。答案累加就可以了。 Code(第二种)12345678910111213141516171819202122#include<bits/stdc++.h>using namespace std;int n,ans,dp[10][10];int one(int m){ while(m>=10) m/=10; return m;}int main(){ scanf("%d",&n); if(n<10){ printf("%d",n); return 0; } for(int i=1;i<=n;i++) dp[one(i)][i%10]++; for(int i=1;i<=9;i++){ for(int j=1;j<=9;j++){ ans+=dp[i][j]*dp[j][i]; } } printf("%d",ans); return 0;} 数据分析考试想到的方法。 方法与 @CQBZJJH 相同,但是我们俩都是考试的时候想出来的,我只是调代码比他慢啊 awa!!1 这个不要 face 的人竟然说版权是他的,IEE。 我来说说这个思路是怎么出来的。 首先第一层循环肯定是枚举 $1\\sim n$,看每个数字有多少个数字对。 用第一个程序打表 $2020$,可以得到如下的输出:Link 等等好像复制不完诶,不过没关系这点够了。 然后我们先通览全篇,然后仔细观察一下 $1\\sim 9$ 的数字对。 发现如下规律: 设 $x$ 为 $n$ 的首位,$k$ 为 $n$ 的位数。 分析:对于每个数 $i$,因为它的数字对的那个搭档的首尾两个数字已经定下来了,所以,中间夹着的数字就可以分析出:中间没有数字的情况,中间有一个数字的情况,中间有两个数字的情况……也就是说,如果没有 $n$ 的限制,那么这个数有的数字对的数量计算公式就是:$10^0+10^1+10^2+\\ldots$。 但是这道题当中是有 $n$ 的限制的(不然这道题还有什么意义呢),所以就要分析下列三种情况讨论: 1. 若 $i \\bmod 10<x$,即 $i$ 的搭档数首位小于 $x$。 非常简单的情况,这个时候,中间数字数量可以从 $0$ 取到 $k-2$,而且不管怎么取它的搭档数都不会超过 $n$ 的,因为它的首位小于 $x$,而且位数不会大于 $k$。 所以直接: $ans←ans+10^{k-2}$ 即可。 2. 若 $i \\bmod 10>x$,即 $i$ 的搭档数首位大于 $x$。 也是非常简单的情况,这个时候,只要此搭档数的位数等于 $k$,就一定会大于 $n$,此点显然易证,就不需要我多哔哔了吧?所以中间掐头去尾的数字的数量可以从 $0$ 取到 $k-3$,所以可以: $ans←ans+10^{k-3}$。 3. 若 $i \\bmod 10=x$,即 $i$ 的搭档数首位与 $x$ 相等。 这个情况就比较复杂了。@CQBZJJH 奆佬用了很巧妙的方法推出了简洁的式子,但是我太蒟蒻了,不会那些花里胡哨的东西,所以就有了一个朴素的第二层循环 qwq。 我的想法就是这样的:既然你这个数无法确定位数为 $k$ 的时候到底是否大于 $n$,那么你就一点一点枚举呗!定义第二层循环 $j$ 为中间的数字($j÷10$ 一定是一个 $k-2$ 位数,位数不够前面补 $0$),其中 $j$ 一定是 $10$ 的倍数(因为要保留最后一位,从倒数第二位开始改),每次枚举时这个 $i$ 的搭档数就是: $x\\times 10^{k-1}+j+\\operatorname{one}(i)$ 其中 $\\operatorname{one}(i)$ 是指求 $i$ 的首位的函数(前面有)。 可以看出,只要这个数小于 $n$,那循环就可以继续下去;但是如果这个数超出了 $n$,因为 $j$ 只会越来越大,不可能后面还有满足的,直接退出循环即可。 最后说一下 $j$ 的枚举范围:$0\\sim 10^{k-1}-1$(不能加到首位上去)。 一个小优化适用于第三种方法,因为这个方法时间复杂度比较大,所以想到了这个。 试想一下:如果一个数的首位是相同的,那么它的数对的数量就相当于它末尾这个一位数的数对数量。所以,$1\\sim 9$ 可以与上面分开枚举,枚举 $i$ 时把答案加在 $b_i$ 里面,最后 $ans←ans+b_i$ 即可。 (注意一定要考虑和自己组成数对即一位数的情况,所以最后 $b_i$ 需要 $+1$!!! 考试就栽在这个细节上了。) 其实想到了这个之后,就离想到上述的动态规划简单做法不远了……考试没想到有点可惜 qaq。 Code (第三种)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869#include<bits/stdc++.h>#define ll long longusing namespace std;int n,k=1,x,a[10],b[10];ll ans,t;int one(int m){ while(m>=10) m/=10; return m;}int buxian(int m){ int s=0; for(int i=0;i<=m;i++) s+=pow(10,i); return s;}int main(){ scanf("%d",&n); if(n<10){ printf("%d",n); return 0; } //求n的位数 a[1]=n; x=one(n); int u=n; while(u){ u/=10; a[++k]=u; } k--; //求数 for(int i=1;i<=9;i++){ if(i<x) b[i]+=buxian(k-2); else if(i==x){ b[i]+=buxian(k-3); int y=pow(10,k-1); for(int j=0;j<=y;j+=10){ if(x*y+j+one(i)<=n) b[i]++; else continue; } }else b[i]+=buxian(k-3); } for(int i=1;i<=9;i++) b[i]++,ans+=b[i]; for(int i=11;i<=n;i++){ if(!(i%10)) continue; t=i%10; if(one(i)==t){ ans+=b[t]; continue; } if(t<x){ ans+=buxian(k-2); continue; } if(t==x){ ans+=buxian(k-3); int y=pow(10,k-1); for(int j=0;j<=y;j+=10){ if(x*y+j+one(i)<=n) ans++; else continue; } continue; } //t>x ans+=buxian(k-3); } //for(int i=1;i<=9;i++) printf("%d ",b[i]); printf("%lld\\n",ans); return 0;} 时间复杂度的话……大概 $O\\big(9+\\frac{1}{10}\\times (n-9)^2+\\frac{4}{5}\\times (n-9)\\big)$??反正能过,极限数据大概 $1.5$ 秒跑完。 说句闲话1.其实第三种方法根本不需要第二层循环,因为对于中间的那个数来说其实就是每次加一,我们还不如直接看中间那个数是多少,然后判断一下刚好卡着那个数行不行就可以了。这样直接把第二层循环给弄掉。 2.还有更玄学厉害的,我们机房的一群大佬说可以把方法二和方法三结合起来,这样复杂度就是 $O(81)$,直接常数级别了,详见 zqw 大佬的题解链接:点这里。 写在最后送给大家一句来自初三教练的名言: 你思维的深度决定你代码的长度。 这道题体现得淋漓尽致啊。","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"递推,递归","slug":"递推,递归","permalink":"https://blog.liynw.top/tags/%E9%80%92%E6%8E%A8%EF%BC%8C%E9%80%92%E5%BD%92/"}]},{"title":"2021.6.26 考试总结","slug":"「ExamSummary」20210626","date":"2021-08-18T13:52:23.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/c29d3798/","link":"","permalink":"https://blog.liynw.top/posts/c29d3798/","excerpt":"前言最近考试考得挺多的……这次算,考得还行?$\\text{rk16}$。 话说 Peter 崛起了啊,$\\text{rk7}$…… 然鹅我和他都因为数组开小少得了 $\\text{60pts}$。以后别犯这种低级错误了啊…… OJ 最终成绩:$40+100+55+5+0=200$ Lemon 上不知道。","text":"前言最近考试考得挺多的……这次算,考得还行?$\\text{rk16}$。 话说 Peter 崛起了啊,$\\text{rk7}$…… 然鹅我和他都因为数组开小少得了 $\\text{60pts}$。以后别犯这种低级错误了啊…… OJ 最终成绩:$40+100+55+5+0=200$ Lemon 上不知道。 快乐的题解时间T1题意简述: 有 $T$ 组数据,每组数据给定 $n$ 个数字,记为 $a_1,a_2,\\ldots a_n$。在这一组数据中选出 $5$ 个数字使得这 $5$ 个数字的乘积最大。 果然是不开 long long 见祖宗啊…… 还有就是数组开小,后 $10$ 个点全 RE 了…… 我直接 AC $\\text{100pts}→$ WA + RE $\\text{40pts}$…… 法一:贪心我们知道乘积要最大,一定要让它尽量为正数。 那么,首先我们先按照绝对值的大小将数组从大到小排序。然后我们看一看: 如果前 $5$ 个数字中有 $0$,那说明这组数据中不为 $0$ 的数少于 $5$ 个。那么结果一定为 $0$。 如果前 $5$ 个数字的乘积为正数,那么这个乘积一定是最优解。证明很简单,相信大家都明白。 那么如果这 $5$ 个数的乘积是负数呢? 那么首先我们分析最小那个数(即第 $5$ 个数)是否可以被换成一个和它正负相反的数字。因为我们知道,当一个乘积为负的时候,只需要换一个就可以了。找当然是从前往后找,找到一个就立刻输出,然后 return。因为这已经是最优解了。 继续,如果找不到怎么办?那我们就找与 $a_4,a_3,a_2,a_1$ 正负相反的数来代替它(此时的 $a$ 数组已经排过序,还是按照绝对值从大往小找保证找到的是最优解,当然,前 $4$ 个数字要从小往大找)。 那如果还是找不到呢?那么,这组数据一定是不能凑出正数。那么我们就只能找绝对值最小的 $5$ 个数(包括 $0$)来凑乘积了。 Code1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859#include<bits/stdc++.h>#define int long longusing namespace std;int t,n,a[100005];bool f;bool cmp(int x,int y){return abs(x)>abs(y);}bool cmp2(int x,int y){return abs(x)<abs(y);}void s(){ int h=0; bool b[15]={}; for(int i=1;i<=5;i++){ b[i]=(a[i]<0); h+=(int)b[i]; if(!a[i]){ printf("0\\n"); return; } } if(!(h&1)){ printf("%lld\\n",a[1]*a[2]*a[3]*a[4]*a[5]); return; } bool d=!b[5]; for(int i=6;i<=n;i++){ if((a[i]<0)==d){ printf("%lld\\n",a[1]*a[2]*a[3]*a[4]*a[i]); return; } } for(int i=4;i;i--){ if(b[i]!=b[5]){ int y=a[1]*a[2]*a[3]*a[4]*a[5]; y/=a[i]; for(int j=6;j<=n;j++){ if((a[j]<0)!=b[i]){ y*=a[j]; printf("%lld\\n",y); return; } } } } sort(a+1,a+n+1,cmp2); printf("%lld\\n",a[1]*a[2]*a[3]*a[4]*a[5]); return;}signed main(){ freopen("maximum.in","r",stdin); freopen("maximum.out","w",stdout); scanf("%lld",&t); while(t--){ f=0; scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%lld",&a[i]),f=f|(!a[i]); sort(a+1,a+n+1,cmp); s(); } return 0;} 法二:DPAs we all know,最大的乘积是有可能从最小的数(绝对值比较大)乘一个负数得到~ 所以,如果我们使用 DP 来求解这个问题的话,需要开两个 $dp$ 数组来分别存最大值和最小值。 如果要求最大值(至少得是个正数吧),有两种方法来求: 两个正数(其中一个是前面的最大值)相乘; 两个负数(其中一个是前面的最小值)相乘。 求最小值也有两种方法,不过我现在用的这个电脑输入法实在是太不好用了,所以我真的不想写了。。。 见代码…… Code12345678910111213141516171819202122232425#include<bits/stdc++.h>#define ll long longusing namespace std;int t,n,a[100005];ll dp1[100005][6],dp2[100005][6];signed main(){ freopen("maximum.in","r",stdin); freopen("maximum.out","w",stdout); scanf("%d",&t); while(t--){ memset(dp1,128,sizeof(dp1)); memset(dp2,127,sizeof(dp2)); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=0;i<=n;i++) dp1[i][0]=dp2[i][0]=1; for(int i=1;i<=n;i++){ for(int j=1;j<=min(i,(int)5);j++){ dp1[i][j]=max(max(dp1[i-1][j-1]*a[i],dp2[i-1][j-1]*a[i]),dp1[i-1][j]); dp2[i][j]=min(min(dp1[i-1][j-1]*a[i],dp2[i-1][j-1]*a[i]),dp2[i-1][j]); } } printf("%lld\\n",dp1[n][5]); } return 0;} T2水题,打表即可…… Code123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475//有亿点长的打表代码 qwq#include<bits/stdc++.h>using namespace std;int I,V,X,L,C,D,M,n;string sc(int x){ int qian=x/1000; x%=1000; int bai=x/100; x%=100; int shi=x/10; x%=10; string s; //千位 if(qian==1) s="M"; else if(qian==2) s="MM"; else if(qian==3) s="MMM"; //百位 if(bai==1) s+="C"; else if(bai==2) s+="CC"; else if(bai==3) s+="CCC"; else if(bai==4) s+="CD"; else if(bai==5) s+="D"; else if(bai==6) s+="DC"; else if(bai==7) s+="DCC"; else if(bai==8) s+="DCCC"; else if(bai==9) s+="CM"; //十位 if(shi==1) s+="X"; else if(shi==2) s+="XX"; else if(shi==3) s+="XXX"; else if(shi==4) s+="XL"; else if(shi==5) s+="L"; else if(shi==6) s+="LX"; else if(shi==7) s+="LXX"; else if(shi==8) s+="LXXX"; else if(shi==9) s+="XC"; //个位 if(x==1) s+="I"; else if(x==2) s+="II"; else if(x==3) s+="III"; else if(x==4) s+="IV"; else if(x==5) s+="V"; else if(x==6) s+="VI"; else if(x==7) s+="VII"; else if(x==8) s+="VIII"; else if(x==9) s+="IX"; return s;}int main(){ freopen("introduction.in","r",stdin); freopen("introduction.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++){ string s=sc(i); int j=0; while(s[j]){ if(s[j]=='I') I++; else if(s[j]=='V') V++; else if(s[j]=='X') X++; else if(s[j]=='L') L++; else if(s[j]=='C') C++; else if(s[j]=='D') D++; else if(s[j]=='M') M++; j++; } } if(I) printf("I %d\\n",I); if(V) printf("V %d\\n",V); if(X) printf("X %d\\n",X); if(L) printf("L %d\\n",L); if(C) printf("C %d\\n",C); if(D) printf("D %d\\n",D); if(M) printf("M %d\\n",M); return 0;} T3和 LIS 问题的普通版求解差不多,只是需要多开一维记录前一个是上升还是下降。 zmq 说也可以用贪心统计折点的数量再 $+1$,但是我交了之后只有 $\\text{45pts}$,也不知道是我实现错了还是他说错了。 Code1234567891011121314151617181920#include<bits/stdc++.h>using namespace std;int n,num,dp[10005][2],a[10005];bool b[10005];//0为上升,1为下降int main(){ freopen("sawtooth.in","r",stdin); freopen("sawtooth.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); dp[1][0]=dp[1][1]=1; for(int i=2;i<=n;i++){ for(int j=1;j<i;j++){ if(a[i]<a[j]) dp[i][0]=max(dp[i][0],dp[j][1]+1); if(a[i]>a[j]) dp[i][1]=max(dp[i][1],dp[j][0]+1); } num=max(num,max(dp[i][0],dp[i][1])); } printf("%d",num); return 0;} T4$dp_{i,j}$ 表示前 $i$ 只 mjl 马进前 $j$ 个马棚产生的最小不愉快系数。 对于每一只 mjl 马,我们可以把它和它前面的 一些 mjl 马一起装在一个马棚里,说具体点,就是再用一层循环,$q$ 代表从第 $q$ 只 mjl 马开始就和第 $i$ 只 mjl 马装在一个马棚里。这样产生的不愉快系数就是 $dp_{q-1,j-1}$ + 这个马棚里的不愉快系数。那这个不愉快系数怎么算呢?用前缀和统计一下这个区间里黑 mjl 马和白 mjl 马的数量再相乘就可以了。具体看代码。 $dp$ 初始化成 $\\text{INF}$,$dp_{0,0}=0$。 Code123456789101112131415161718192021222324#include<bits/stdc++.h>using namespace std;int n,k,a[505],b[505],w[505],dp[505][505];int main(){ freopen("horse.in","r",stdin); freopen("horse.out","w",stdout); memset(dp,127,sizeof(dp)); dp[0][0]=0; scanf("%d %d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++){ w[i]=w[i-1]+(!a[i]); b[i]=b[i-1]+a[i]; } for(int i=1;i<=n;i++){ for(int j=1;j<=min(i,k);j++){ for(int q=j;q<=i;q++){ dp[i][j]=min(dp[i][j],dp[q-1][j-1]+(w[i]-w[q-1])*(b[i]-b[q-1])); } } } printf("%d",dp[n][k]); return 0;} T5$50$ 分就是一个分组背包。 $100$ 分好像是要开专门记录有或者没有奖励子弹的情况。 没太听明白,放个代码留在这吧。 Code123456789101112131415161718192021222324252627282930313233343536373839404142434445#include<bits/stdc++.h>using namespace std;int m,n,k;int f[205][205];bool h[205][205];char a;int dy[205][205],dn[205][205],dpy[205][205],dpn[205][205];int main(){ scanf("%d %d %d",&m,&n,&k); for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ scanf("%d %c",&f[i][j],&a); if(a=='Y') h[i][j]=1; } } //初始化dy和dn的值 for(int i=1;i<=n;i++){ //列 int tot=0; for(int l=m;l;l--){ //行 if(h[l][i]){ dy[i][tot]+=f[l][i]; //dn[i][tot]=dy[i][tot]; }else{ tot++; dn[i][tot]=dy[i][tot-1]+f[l][i]; dy[i][tot]=dn[i][tot]; } } } //状态转移 for(int i=1;i<=n;i++){ //列 for(int j=0;j<=k;j++){ //子弹的数量 for(int q=0;q<=min(k,j);q++){ //当前这一列子弹的数量 //打第i列最后打到Y,并且把这个奖励子弹用在后面 dpy[i][j]=max(dpy[i][j],dpy[i-1][j-q]+dy[i][q]); //打第i列打到N if(q) dpn[i][j]=max(dpn[i][j],dpy[i-1][j-q]+dn[i][q]); //打第1~i-1列的N if(j-q) dpn[i][j]=max(dpn[i][j],dpn[i-1][j-q]+dy[i][q]); } } } printf("%d",dpn[n][k]); return 0;}","categories":[{"name":"考试总结","slug":"考试总结","permalink":"https://blog.liynw.top/categories/%E8%80%83%E8%AF%95%E6%80%BB%E7%BB%93/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"贪心","slug":"贪心","permalink":"https://blog.liynw.top/tags/%E8%B4%AA%E5%BF%83/"},{"name":"模拟","slug":"模拟","permalink":"https://blog.liynw.top/tags/%E6%A8%A1%E6%8B%9F/"}]},{"title":"中考假 & 端午节做题记录","slug":"「Record」2021中考假","date":"2021-08-18T13:36:40.000Z","updated":"2022-02-01T10:01:01.000Z","comments":true,"path":"posts/a8f57f13/","link":"","permalink":"https://blog.liynw.top/posts/a8f57f13/","excerpt":"前言端午和中考假连在一起了,放六天假,好耶!ヽ(✿゚▽゚)ノ 个人认为难度: T1(难在读题)<T4(代码较长,思路简单)<T2(有一定的思维量,较为经典的要排序的 01 背包问题)<T5(广搜)<T3(难在读题 + 坑点较多的模拟)<T6(线性 DP)<T7(紫题劝退)","text":"前言端午和中考假连在一起了,放六天假,好耶!ヽ(✿゚▽゚)ノ 个人认为难度: T1(难在读题)<T4(代码较长,思路简单)<T2(有一定的思维量,较为经典的要排序的 01 背包问题)<T5(广搜)<T3(难在读题 + 坑点较多的模拟)<T6(线性 DP)<T7(紫题劝退) T1$AC\\ \\ on\\ \\ 2021.06.11$ 这题是来考语文的吗? 题目老长,愣是看了半天才看懂。 读懂题后:这不就是给一串数处理之后加在一起咩?? 处理也很简单 (事实证明这道题最难的地方在读题),直接看灯是什么颜色:如果是绿的就变为 $0$,如果是红的就不变,如果是黄的就加上 $r$。 结果打完之后代码 CE 了后来我才发现 time 是 c++ 的关键词,无语…… Code12345678910111213#include<bits/stdc++.h>using namespace std;int n,a,b,r,y,g,Time; int main(){ scanf("%d %d %d %d",&r,&y,&g,&n); for(int i=1;i<=n;i++){ scanf("%d %d",&a,&b); if(!a||a==1) Time+=b; else if(a==2) Time+=b+r; } printf("%d",Time); return 0;} T2$AC\\ \\ on\\ \\ 2021.06.13$ 吃石头可海星。 背包,考点是怎么排序。 假设要吃两个石头 $x$ 和 $y$,先吃 $x$ 比先吃 $y$ 好。 设 $first$ 为石头的初始能量值,$lost$ 为每秒石头会浪费的能量值,$time$ 为吃一块石头要用的时间。 为了方便我们不考虑石头能量值流完的情况。 那么先吃 $x$ 的表达式为: energy1=x_{first}+y_{first}-x_{time}\\times y_{lost}先吃 $y$ 的表达式为: energy2=y_{first}+x_{first}-y_{time}\\times x_{lost}我们知道 $energy1>energy2$ 代入原式: x_{first}+y_{first}-x_{time}\\times y_{lost}>y_{first}+x_{first}-y_{time}\\times x_{lost}把相同的地方抵消了就变成: -x_{time}\\times y_{lost}>-y_{time}\\times x_{lost}把负号搞掉: x_{time}\\times y_{lost}","categories":[{"name":"做题记录","slug":"做题记录","permalink":"https://blog.liynw.top/categories/%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"模拟","slug":"模拟","permalink":"https://blog.liynw.top/tags/%E6%A8%A1%E6%8B%9F/"},{"name":"搜索","slug":"搜索","permalink":"https://blog.liynw.top/tags/%E6%90%9C%E7%B4%A2/"}]},{"title":"P6855「EZEC-4.5」走方格 题解","slug":"「Solution」Luogu_P6855","date":"2021-08-18T13:29:07.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/c0d0ee68/","link":"","permalink":"https://blog.liynw.top/posts/c0d0ee68/","excerpt":"前言题目传送门 正解:动态规划 难度较大的一道题 qwq。 Problem给你一个 $n\\times m$ 大小的方格阵,可以把方格中的任意一个数改为 $0$,每次从 $(1,1)$ 到 $(n,m)$ 的得分为路上所有数字的和。求每次改动数字后能得到的最大值的最小值。","text":"前言题目传送门 正解:动态规划 难度较大的一道题 qwq。 Problem给你一个 $n\\times m$ 大小的方格阵,可以把方格中的任意一个数改为 $0$,每次从 $(1,1)$ 到 $(n,m)$ 的得分为路上所有数字的和。求每次改动数字后能得到的最大值的最小值。 SolutionPS1:因为此篇题解前后改动较多,如果有什么错误请各位奆佬提出,本蒟蒻感激不尽 ovo。 PS2:因为我习惯用 $m$ 表示行 $n$ 表示列,所以下列题解就会这么写。 有点麻烦。 如果每个点只能遍历一次,那么必须不变值和变为 $0$ 这两种情况需要同时考虑。 首先我们可以考虑先求出从 $(1,1)$ 点出发走到 $(i,j)$ 点和从 $(m,n)$出发走到 $(i,j)$ 点能拿到的最大分数,分别存在 $dp1$ 数组和 $dp2$ 数组里。 这两个数组很好求,按照每个初学 DP 者都要打的取数板子就珂以了。至于为什么要求这两个数组,我先卖个关子,待会儿就知道了(逃。 我们知道,如果你从 $(1,1)$ 出发,在走的时候不经过某个坐标为 $(i,j)$ 的点(也就是绕过这个点),你有两种情况可以绕开它: 从左边绕。 从上边绕。 给这两种方法更严谨的定义 $(i>0)$ : 从左边绕:经过点 $(x,y-i)$。 从上边绕:经过点 $(x-i,y)$。 对于这两种情况,我们可以分别用两个二维数组 $l$ (左边绕)和 $d$ (上边绕)来存: $l_{i,j}$ 表示从左边绕过 $(i,j)$ 点能获得的最大值。 $d_{i,j}$ 表示从上边绕过 $(i,j)$ 点能获得的最大值。 但是如何求这两个数组呢? 直接切入可能比较麻烦。这个时候我们可以先分析这种情况: 不管是往哪边绕,也不管前面怎么走,都紧贴着点 $(i,j)$ 过路方便分析,即,从左绕一定经过点 $(i,j-1)$,从上绕一定经过点 $(i-1,j)$。 首先分析从左边绕的情况。 看图,蓝色区域表示从 $(1,1)$ 出发到点 $(i,j-1)$ 有可能会经过的区域,红色区域表示从点 $(i+1,j-1)$ 到 $(m,n)$ 有可能会经过的区域。至于为什么选这两个点呢,相信大家看图也能明白,因为选择这两个点可以做到可能经过的格子不重不漏,考虑到每种情况。 如果这么算,那么从点 $(i,j-1)$ 绕过去能拿到的最大分数就是: score=dp1_{i,j-1}+dp2_{i+1,j-1}现在大家知道两个 $dp$ 数组的意义了吧,就是用来求某个区域的最大分数的。因为如果每次循环到一个点就计算此点到终点的分数还需要两层循环会超时,基于走方格的方向是可逆的,我们只需要计算终点到每个点的最大分数就可以啦 OvO。 那我们现在只求了最贴近点 $(i,j)$ 的绕法,那我们如何求出往左绕的所有情况的最大值呢? 我们之前不是用了一个数组来存往左边绕的值吗?因为循环的顺序是从上到下,从左到右的,所以在求点 $(i,j)$ 的值时,我们已经把它左上方的所有值都求出来了。现在我们可以利用这些值,每一次,我们求当前 $score$ 与之前最大分数的较大值,那每次都求最大值就是所有情况中的最大值。 那么最后的结果就是: l_{i,j}=\\max(dp1_{i,j-1}+dp2_{i+1,j-1},i_{i,j-1})至于为什么我们利用的是 $l_{i,j-1}$,大家可以自己画图感知,这个格子就位于我们前面求的必须经过的那个格子 $(i,j-1)$,那么绕过它我们就会求必须经过点 $(i,j-2)$,这样我们就又需要考虑绕过这个格子的情况,就又必须经过点 $(i,j-3)$……这么一层一层往左推,最后可以推到点 $(i,1)$,从而把每种情况都考虑到。 从上面绕分析方法也差不多,这里不多说画张图让读者感知一下。 (您看看这两张图多像,连大小都差不多26 KB) 最后求出 $d_{i,j}$ 的式子为: d_{i,j}=\\max(dp1_{i-1,j}+dp2_{i-1,j+1},d_{i-1,j})最后求答案有些麻烦,因为题目要求的是变化后最能获得的最大分数的最小值,所以 $\\max$ 和 $\\min$ 是真的挺容易用混的,这里需要特别注意。 首先每对一个格子进行操作,最后得到的答案会是下列三种情况中的一种: 从左边绕过去得到的最大分数。 从上边绕过去得到的最大分数。 经过这个格子得到的最大分数。 前两个我们已经求解了,但其实第三种情况是灰常简单的!因为要保证经过点 $(i,j)$,所以我们只需要求 $dp1_{i,j}+dp2_{i,j}$ 就可以了。但是上面那个式子算了两次 $(i,j)$ 的值,而我们因为把它变成 $0$ 了,就一次都不能算。所以还需要减去两个 $a_{i,j}$。 所以最后的答案终于可能被更新了 owo: ans=\\min\\big(ans,\\max(l_{i,j},d_{i,j},dp1_{i,j}+dp2_{i,j}-2\\times a_{i,j})\\big)最后再强调一遍要注意最大值和最小值别用反了啊! 别问我为什么知道(悲)。 Code123456789101112131415161718192021222324252627282930313233343536#include<bits/stdc++.h>#define ll long long //记得要开long long哦!using namespace std;//温馨提示细节:因为最后答案还是求的最小值,所以 ans 需要定义极大值ll m,n,ans=LONG_LONG_MAX,a[2005][2005],dp1[2005][2005],dp2[2005][2005],l[2005][2005],d[2005][2005];int main(){ //输入 scanf("%lld %lld",&m,&n); for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ scanf("%lld",&a[i][j]); } } //求两个 dp 数组的值 for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ dp1[i][j]=max(dp1[i-1][j],dp1[i][j-1])+a[i][j]; } } for(int i=m;i;i--){ for(int j=n;j;j--){ dp2[i][j]=max(dp2[i+1][j],dp2[i][j+1])+a[i][j]; } } //核心代码开始 for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ l[i][j]=max(l[i][j-1],dp1[i][j-1]+dp2[i+1][j-1]); d[i][j]=max(d[i-1][j],dp1[i-1][j]+dp2[i-1][j+1]); ans=min(ans,max(max(l[i][j],d[i][j]),dp1[i][j]+dp2[i][j]-2*a[i][j])); } } //核心代码结束,输出答案 printf("%lld",ans); return 0;} 写在最后这真的是一道很好的动态规划题,很考验思维,也有很多需要注意的细节。最后,看在本人写了那么久的份上,就请您随手点一下左下角那个小小的赞吧 qwq。","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"CF201C Fragile Bridges 题解","slug":"「Solution」CF201C","date":"2021-08-18T13:25:24.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/1b222d8e/","link":"","permalink":"https://blog.liynw.top/posts/1b222d8e/","excerpt":"","text":"前言题目链接 正解:动态规划 思路不是很好想,想出来了应该就没有多大问题了,但是需要处理的细节较多,再加上水水的样例,难度应该是偏难的。个人感觉应该是绿到蓝的亚子。 Update: 已经有评级了,绿的,针不戳。 先说说思路的来源 错误解法,不想看请跳过。 中午打开题目,第一反应:这不是一个暴搜部分分到手吗? 打了个无脑暴搜,深搜+回溯,分别向往左走和往右走的方向进行搜索,只是需要注意变量初始化和判断是否可以走。 时间复杂度 $Θ(2^{n})$,校内 OJ $15pts$。 123456789101112131415161718192021222324252627282930#include<bits/stdc++.h>#define int long longusing namespace std;int n,ans,a[100005];int dfs(int i,int sum){ int left=sum,right=sum; if(!a[i-1]&&!a[i+1]) return sum; if(a[i-1]){ //可以往左走 a[i-1]--; //要耗费一次桥的耐久度 left=dfs(i-1,sum+1); a[i-1]++; //回溯 } if(a[i+1]){ //可以往右走 a[i+1]--; //要耗费一次桥的耐久度 right=dfs(i+1,sum+1); a[i+1]++; //回溯 } return max(left,right); //返回最大值}signed main(){ scanf("%lld",&n); for(int i=1;i<=n-1;i++){ scanf("%lld",&a[i]); } for(int i=1;i<=n-1;i++){ ans=max(ans,dfs(i,0)); } printf("%lld",ans); return 0;} 可是 CF 的题都是绑在一个点上的啊,于是还没把正解想出来的我开始想怎么优化。 我先想到了一个假优化:使用一个前缀和 $pre$ 数组统计一下前面最多可能得多少分,如果现在得的分 + 往左/右走能拿到的最多的分都不可以更新答案的话就不用搜这一边了。 程序不做展示,但是我发现样例竟然没过…… 淦,那到底是哪里出问题了呢? 经过短暂的思考我们可以发现,完全有这么一种可能: 假设你现在从第 $i$ 个平台往左走,在左边尽量赚到最多的分数之后回到第 $i$ 个平台,然后再往右边走,再在右边的平台结束游戏。 此时的得分完全是有可能高于 $pre_{i-1}+sum$ 或 $ans$ 的。 正解上面的枝我们减错了,但是事情却有了一点眉目: 这道题好像每次的状态转移都与他离开之后回不回来有关系。 为了方便,我们将第 $i$ 与 $i+1$ 个平台中的桥定义为 $i$ 号桥,这也与输入的数组下标对应。 所以可以定义 $dp$ 数组意义如下: $dp_{i,0}$ 代表往左边走且不回来。 $dp_{i,1}$ 代表往左边走且必须回来。 $dp_{i,2}$ 代表往右边走且不回来。 $dp_{i,3}$ 代表往右边走且必须回来。 PS:关于必须回来这一个点,现有的题解说的是不保证回来,那么就有两种情况,较难判断。所以设置绝对一点较好思考。 首先思考 $dp_{i,0}$ 的情况。如果我们从第 $i$ 个平台往左边走(而且可以走),就一定会走到第 $i-1$ 个桥上。 而因为不回第 $i$ 个平台,所以我们无需考虑到了第 $i-1$ 个平台后再往左走(不考虑直接返回),还能不能回到第 $i-1$ 的平台。因此,对于走到第 $i-1$ 个平台能获得的最大分数,就为 $dp{i-1,0}$ 和 $dp{i-1,1}$ 的最大值。 然后再求从第 $i$ 个平台到第 $i-1$ 个平台能获得的最大分数。 首先要明确,我们是不回来的,所以最优的方式就是把这座桥尽量“榨干”,即能走断就走断,在两个平台之间反复横跳。但是若是要走到对面,需要走桥的次数必须是奇数。所以,如果桥当前还能走的最大次数是偶数,那就必须留一次走的机会(不然就走回去走不过来了)。 所以可以得出,往返这两个平台间能获得的最大分数(暂时叫 $score$)为: score= \\begin{cases} a_{i-1},2\\not\\!|\\;a_{i-1}\\\\ a_{i-1}-1,2\\;|\\;a_{i-1} \\end{cases}在 c++ 中,我们可以用更加简洁的三目运算符表示。 1dp[i][0]=max(dp[i-1][0],dp[i-1][1])+((a[i-1]&1)?a[i-1]:(a[i-1]-1)); 接着看必须回来的情况。如果要保证回到第 $i$ 个平台,那么必须保证要先回到第 $i-1$ 个平台,然后还要能从中间那座桥走过去。所以前面加上的是 $dp_{i-1,1}$,没有别的情况。 像之前那样考虑在中间走能拿到的最大分数。因为要回去,我们需要经过偶数次桥。如果当前桥能走的次数是奇数,那就只能浪费一次机会达到能回来的目的。 score= \\begin{cases} a_{i-1}-1,2\\not\\!|\\;a_{i-1}\\\\ a_{i-1},2\\;|\\;a_{i-1} \\end{cases}同样可以用三目运算符解决。 1dp[i][1]=dp[i-1][1]+((a[i-1]&1)?(a[i-1]-1):a[i-1]); 不过这样还是不对,大家是否注意到这个程序没有关于无法走动的判断?现在这个判断来了! 如果这座桥能走的次数为 $1$,那我走过去这座桥就断了,无法回来。为此,我只能放弃前面的所有分数。 所以上述程序只有在 $a_i>1$ 时才能执行,当 $a_i=1$ 时,$dp_{i,1}$ 的值为 $0$(不用赋值)。 右边同理。只是循环要倒着枚举而已。 最后我们需要知道答案时什么。因为每一个平台都有可能作为出发点,所以需要枚举求解最优解。 那么,对于每一个点,我们可以先往左边走回来再往右边不回来,也可以先往右边回来再往左边不回来。所以答案就为: ans=\\max_{i=1}^{n}\\{dp_{i,0}+dp_{i,3},dp_{i,1}+dp_{i,2}\\}提示:打代码的时候一定要注意你的这个下标到底代表的是平台还是桥! 在这道题中,$dp$ 数组代表的是平台,$a$ 数组代表的是桥。 Code123456789101112131415161718#include<bits/stdc++.h>using namespace std;long long ans,n,a[100005],dp[100005][4];int main(){ scanf("%lld",&n); for(int i=1;i<=n-1;i++) scanf("%lld",&a[i]); for(int i=2;i<=n-1;i++){ dp[i][0]=max(dp[i-1][0],dp[i-1][1])+((a[i-1]&1)?a[i-1]:(a[i-1]-1)); if(a[i-1]>1) dp[i][1]=dp[i-1][1]+((a[i-1]&1)?(a[i-1]-1):a[i-1]); //注意加特判 } for(int i=n;i;i--){ dp[i][2]=max(dp[i+1][2],dp[i+1][3])+((a[i]&1)?a[i]:(a[i]-1)); if(a[i]>1) dp[i][3]=dp[i+1][3]+((a[i]&1)?(a[i]-1):a[i]); } for(int i=1;i<=n;i++) ans=max(ans,max(dp[i][0]+dp[i][3],dp[i][1]+dp[i][2])); printf("%lld",ans); return 0;} 比暴力还短吼 提交记录作证 qwq:link","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"CF877B Nikita and string 题解","slug":"「Solution」CF877B","date":"2021-08-18T13:20:47.000Z","updated":"2022-01-30T18:36:48.000Z","comments":true,"path":"posts/49def204/","link":"","permalink":"https://blog.liynw.top/posts/49def204/","excerpt":"前言如果此题解有什么问题的话欢迎各位大巨佬提出。 题目链接:CF877B 题目类型:dp,一讲就会,一做就废(;′⌒`)。","text":"前言如果此题解有什么问题的话欢迎各位大巨佬提出。 题目链接:CF877B 题目类型:dp,一讲就会,一做就废(;′⌒`)。 更新2021/5/31:因为帮同学调代码添加了新发现的滚动数组问题。 题意简述给定一个只含有 “a” 和 “b” 的字符串,求这个字符串中最长的子序列,子序列满足以下条件之一: 全为 “a”。 全为 “b”。 开头和结尾是连续的 “a”,中间是连续的 “b”。 字符串长度 $\\leq 5000$。 题解算法:动态规划 首先可以发现一个“美丽”的字符串由三个部分组成,所以我们可以开一个二维的 $dp$ 数组。其含义如下: $dp_{i,1}$ 代表字符串只包含第一部分,从 $1\\sim i$ 能取到的最长长度; $dp_{i,2}$ 代表字符串包含第一部分和第二部分,从 $1\\sim i$ 能取到的最长长度; $dp_{i,3}$ 代表字符串包含所有部分,从 $1\\sim i$ 能取到的最长长度。 因为每个部分都可以是空串,所以最后的答案可以为 $dp_{i,1},dp_{i,2},dp_{i,3}$ 的任意一种,取最大值。 然后思考如何转移状态。 首先 $dp_{i,1}$ 是很好求的,我们只需要求解字符串里面有多少个 “a” 就可以了。这个很好想,要求解一个字符串中最长的只包含 “a” 或为空的子序列,只需统计 “a” 的个数,让所有的 “a” 都加入子序列。 当然我们不需要每次都用一个循环求解,可以参照前缀和的方式求解 “a” 的个数: dp_{i,j}=dp_{i-1,1}+(a[i]=='a');(如果这个字符是 “a” 就 $+1$,反之不加) 接着考虑 $dp_{i,2}$ 的求法。实际上,我们可以用另一种方式理解前面求 $dp_{i,1}$,把它当做动态转移方程而并不是一个简单的求前缀和的式子: 如果这个字符是 “a”,那么一定要选,然后再加上前 $i-1$ 个字符能拿到的最长的长度。 那么,$dp_{i,2}$ 的解法就呼之欲出了: dp_{i,2}=\\max\\{dp_{i-1,1},dp_{i-1,2}\\}+(a[i]=='b');因为第一部分和第二部分都可以是空串,所以一个全为 “a” 的字符串同样可以作为第二部分的结果,所以需要求最大值。最后的那个 “b” 也是一样的,因为如果这个字符串不是一个全为 “a” 的字符串,就必须以 “b” 结束。所以只有后面是 “b” 才能增加字符串的长度,若答案全是 “a” 那就是 $dp_{i,1}$,与 $dp_{i,2}$ 的求解是无关的。 $dp_{i,3}$ 方法类似,需要考虑空串的情况,考虑 $dp$ 前两维的情况;也需要根据结尾的 “a” 判断不为空的最大值,读者自证不难。 dp[i][3]=\\max\\{dp_{i-1,1},dp_{i-1,2},dp_{i-1,3}\\}+(a[i]=='a');最后可以看到: 我们只有 “$1,3$ 不空,$2$ 空”和 “$1,3$ 空,$2$ 不空”的情况没有考虑到。但是由于第一个部分和第三个部分本质上是一样的,所以这两种情况可以分别对应到另外两种我们已经考虑过的情况——全为 “a” 或全为 “b”。 Code123456789101112131415#include<bits/stdc++.h>using namespace std;int dp[5005][4],n;char a[5005];int main(){ scanf("%s",a+1); n=strlen(a+1); for(int i=1;i<=n;i++){ dp[i][1]=dp[i-1][1]+(a[i]=='a'); dp[i][2]=max(dp[i-1][1],dp[i-1][2])+(a[i]=='b'); dp[i][3]=max(dp[i-1][1],max(dp[i-1][2],dp[i-1][3]))+(a[i]=='a'); } printf("%d",max(dp[n][1],max(dp[n][2],dp[n][3]))); return 0;} 关于滚动数组我有个同学一直没过让我帮他调代码,然后我发现他用的是滚动数组。 首先要明确这道题是不能在上述代码中直接修改的。因为如果打滚动,例如求 $dp_{i,3}$ 时需要用到 $dp_{i-1,2}$ 的值,但是如果打滚动这个值就已经被更新为 $dp_{i,2}$ 了。 如果要打滚动呢? 很简单,先求 $dp_{i,3}$,再求 $dp_{i,2}$,最后求 $dp_{i,1}$ 就可以了。","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"}]},{"title":"CF474D Flowers 题解","slug":"「Solution」CF474D","date":"2021-08-18T13:16:29.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/e71167b1/","link":"","permalink":"https://blog.liynw.top/posts/e71167b1/","excerpt":"题目:CF474D Flowers传送门","text":"题目:CF474D Flowers传送门 DP?递推?首先可以很快看出这是一道 DP 的题目,但与其说是 DP,还不如说是递推。 大家还记得刚学递推时教练肯定讲过的一道经典例题吗?就是爬楼梯,一个有 $n$ 阶的楼梯,一个人爬上去,每次可以爬一阶也可以爬两阶,问有多少种爬法?其实这道题也是一样的,只不过把 $2$ 换成了 $k$ 而已。 于是我们开始分析,定义 $dp[i]$ 为吃 $i$ 个蛋糕的吃法总数。 很容易看出,如果 $i<k$,就不可以一口气吃掉,只能一个一个吃,方法为 $1$ 种。 如果 $i==k$,就既可以一个一个吃掉,也可以一口气全部吃完,方法为 $2$ 种。 如果 $i>k$,就有两种吃法,既可以先吃 $i-1$ 个,然后再吃一个,也可以先吃 $i-k$ 个,再吃 $k$ 个。方法为 $dp[i-1]+dp[i-k]$ 种。 最后记得要开 long long,而且要一边加一边模 $1000000007$。 核心代码:12345678if(dp[i])continue;if(i<k) dp[i]=1;else if(i==k) dp[i]=2;else dp[i]=(dp[i-1]+dp[i-k])%1000000007;sum[i]=(sum[i-1]+dp[i])%1000000007; 因为一组数据只有一个 $k$,但是有很多组关于这个 $k$ 的测试点,所以可以用一个前缀和数组统计 $dp_1\\sim dp_i$ 的和,然后根据题目中 $mod\\ 1000000007$。 玄学优化其实这个优化也不难想到。既然一组数据中只会有一个 $k$,那么说明不管怎么算,$dp[i]$ 的值算出来都是相等的。那么可以判断一下当前出现的最大 $x_2$,如果一组输入的 $x_2$ 值小于最大值,就说明 $dp[x_2]$ 已经计算过,直接输出即可。 Code 12345678910111213141516171819202122232425262728#include<bits/stdc++.h>#define ll long longusing namespace std;int t,k,x1,x2,Max=1;ll dp[100005],sum[100005];int main(){ scanf("%d %d",&t,&k); while(t--){ scanf("%d %d",&x1,&x2); if(Max>=x2){ //优化:判断x2和max(x2)的大小 printf("%lld\\n",(sum[x2]-sum[x1-1])%1000000007); continue;//直接跳过 } for(int i=Max;i<=x2;i++){//只计算没计算过的 if(dp[i])continue; if(i<k) dp[i]=1; else if(i==k) dp[i]=2; else dp[i]=(dp[i-1]+dp[i-k])%1000000007; sum[i]=(sum[i-1]+dp[i])%1000000007; } printf("%lld\\n",(sum[x2]-sum[x1-1])%1000000007); Max=x2;//更新Max的值 } return 0;} 究竟是什么地方错了?然后你交上去发现WA了! 这也就是一个本蒟蒻在做题时犯的错误。 一般要取余的题都是一边计算一边取模,所以可能会造成dp数组中前面的值大于后面的值的情况。在最终计算 $x_1\\sim x_2$ 的时候做的减法运算可能是负数,负数取模就出事了。 那如何解决呢?其实很简单,只需要在取模之前再加上一个 $1000000007$ 就可以了。 $Code$ 12345678910111213141516171819202122232425262728#include<bits/stdc++.h>#define ll long longusing namespace std;int t,k,x1,x2,Max=1;ll dp[100005],sum[100005];int main(){ scanf("%d %d",&t,&k); while(t--){ scanf("%d %d",&x1,&x2); if(Max>=x2){ printf("%lld\\n",(sum[x2]-sum[x1-1]+1000000007)%1000000007); continue; } for(int i=Max;i<=x2;i++){ if(dp[i])continue; if(i<k) dp[i]=1; else if(i==k) dp[i]=2; else dp[i]=(dp[i-1]+dp[i-k])%1000000007; sum[i]=(sum[i-1]+dp[i])%1000000007; } printf("%lld\\n",(sum[x2]-sum[x1-1]+1000000007)%1000000007); Max=x2; } return 0;} 终于A了!www","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"递推,递归","slug":"递推,递归","permalink":"https://blog.liynw.top/tags/%E9%80%92%E6%8E%A8%EF%BC%8C%E9%80%92%E5%BD%92/"}]},{"title":"高精度算法学习笔记","slug":"「Algorithm」高精度算法","date":"2021-08-17T17:15:20.000Z","updated":"2022-01-30T18:41:46.000Z","comments":true,"path":"posts/74e5e1ec/","link":"","permalink":"https://blog.liynw.top/posts/74e5e1ec/","excerpt":"概念之前做题,有时候会听到老师说:“这道题数据很大,要高精度。” 有些题的数据范围可以毒瘤到连开挂神器 __int128 都救不了你,这个时候我们就会选择: 让电脑跟我们人一样计算!毕竟人只要时间够,不管多大的数都是可以算出来的。 这就是高精度算法。","text":"概念之前做题,有时候会听到老师说:“这道题数据很大,要高精度。” 有些题的数据范围可以毒瘤到连开挂神器 __int128 都救不了你,这个时候我们就会选择: 让电脑跟我们人一样计算!毕竟人只要时间够,不管多大的数都是可以算出来的。 这就是高精度算法。 高精度的那些板子计算说句闲话:高精度算法需要使用到 $string$ 类型的字符串,这里不提,请自行 BDFS。 1.高精度加法要做高精度,首先需要了解人是怎么算的。 小学一年级内容出现了!(雾,大雾,伦敦雾 $$\\ \\ \\ \\ \\ \\ \\ \\ \\ \\ 3\\ 2\\ 8$$$$+\\ \\ \\ \\ \\ \\ \\ 1\\ 3\\ 7$$ ————————$$\\ \\ \\ \\ \\ \\ \\ \\ \\ \\ 4\\ 6\\ 5$$ 先看以上算式: 第一步:从低位往高位算,$8+7=15$。末位为5,往前进一位。 ans 0 1 5 第二步:算十位,$2+3+1=6$(1为进位),十位为6,不进位。 ans 0 6 5 第三步:算最高位,$3+1=4$,最高位为4。 ans 4 6 5 ans就是最终的答案了。 但是有个问题:上面这个例子两个加数的位数是一样的,那要是不一样呢?程序不就弄错了吗? 解决方法很简单:把两个加数倒着存就行了,这样因为数组初始化的原因,后面的数都会变成0,即会在前面自动补0。最后算出来的答案也是倒着的,再进行处理。 具体过程如下:(没错我又在水博客了,巨佬们直接跳过吧【滑稽】) 1.倒序存储123a 8 2 3b 7 3 1c 0 0 02.算第一位,进位直接存在下一位中123a 8 2 3b 7 3 1c 5 1 03.继续算,记得加上数组里本来就有的值(即进位)123a 8 2 3b 7 3 1c 5 6 04.一直循环,直到最后算完出结果123a 8 2 3b 7 3 1c 5 6 45.把答案倒过来输出15 6 4 --> 465 这样就是一个不完整的高精度加法的过程。 为什么不完整呢?因为程序可能会在计算位数的时候出错(程序会把和的位数计算为加数中位数较大的那一个数的位数+1,但是不一定会进位),所以在结束计算之后,需要判断和的第一位是否为0,如果是就需要去除。 这就完整了! 最后给出一份有注释的高精度加法板子代码:123456789101112131415161718192021222324252627282930313233343536373839#include<bits/stdc++.h>using namespace std;int a[5005],b[5005],c[5005];//定义全局,即初始化为0 string add(string a1,string b1){ //前置工作 string c1; int lena=a1.size(); int lenb=b1.size(); //倒序存储并转换为整数类型 for(int i=0;i<lena;i++){ a[lena-i]=a1[i]-'0'; } for(int i=0;i<lenb;i++){ b[lenb-i]=b1[i]-'0'; } //做加法 int lenc=1; //做加法的次数为两个高精度数中较大的位数 while(lenc<=lena||lenc<=lenb){ c[lenc]+=a[lenc]+b[lenc]; //加法 c[lenc+1]=c[lenc]/10; //进位 c[lenc]%=10; //取个位 lenc++; } //去除前导0 if(!c[lenc]) lenc--; //把c存到c1里面 while(lenc>=1) c1+=c[lenc--]+'0'; return c1;}int main(){ string a1,b1; cin>>a1; cin>>b1; cout<<add(a1,b1); return 0;} 2.高精度减法思路和高精度加法差不多,就是把进位换成退位,每次退位就让当前位数上的数加上10,然后让前面那个位数的答案-1。要注意前导0可能不止一个,需要连续去除。 12345678910111213141516171819202122232425262728293031323334353637383940414243#include<bits/stdc++.h>using namespace std;int a[5005],b[5005],c[5005];string sub(string a1,string b1){ //前置工作 if(a1==b1) return "0"; string c1; int lena=a1.size(); int lenb=b1.size(); //倒序存储并转换为整数类型 for(int i=0;i<lena;i++){ a[lena-i]=a1[i]-'0'; } for(int i=0;i<lenb;i++){ b[lenb-i]=b1[i]-'0'; } //做减法 int lenc=1; //做减法的次数为两个高精度数中较大的位数 while(lenc<=lena||lenc<=lenb){ c[lenc]+=a[lenc]-b[lenc]; //减法 if(c[lenc]<0){ //退位 c[lenc]+=10; //借位 c[lenc+1]=-1; //前面的要-1 } lenc++; } //去除前导0 while(!c[lenc]) lenc--; //把c存到c1里面 while(lenc>=1) c1+=c[lenc--]+'0'; return c1;}int main(){ string a1,b1; cin>>a1; cin>>b1; cout<<sub(a1,b1); return 0;} 3.高精度乘法这个就有点意思了。 再来看一个竖式: $$\\ \\ \\ \\ \\ \\ \\ \\ 1\\ 5\\ 3$$$$\\times\\ \\ \\ \\ \\ \\ \\ \\ 2\\ 7$$ ————————$$\\ \\ \\ \\ \\ \\ 1\\ 0\\ 7\\ 1$$$$\\ \\ \\ 3\\ 0\\ 6$$ ————————$$\\ \\ \\ \\ \\ \\ 4 \\ 1\\ 3\\ 1$$ 乘法又是怎么算的呢? 根据日常经验,我们能很快发现:需要先将一个数拆掉,一位一位去乘另一个数,然后把结果加起来,不过加起来的时候需要乘以10^{最小位数-1}。 那事情就变得容易了起来,一个双重循环,每一次循环让被拆掉的那个数的每一位乘以另一个数,加在答案里,进位和加法差不多,不过要注意进位可能不止一位。 代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445#include<bits/stdc++.h>using namespace std;int a[5005],b[5005],c[10005];string mul(string a1,string b1){ //如果有数为0直接返回0 if(a1=="0"||b1=="0") return "0"; //前置工作 string c1; int lena=a1.size(); int lenb=b1.size(); //倒序存储并转换为整数类型 for(int i=0;i<lena;i++){ a[lena-i]=a1[i]-'0'; } for(int i=0;i<lenb;i++){ b[lenb-i]=b1[i]-'0'; } //做乘法 int lenc; for(int i=1;i<=lena;i++){ for(int j=1;j<=lenb;j++){ lenc=i+j-1; //存进去的位数 c[lenc]+=a[i]*b[j]; //乘法 c[lenc+1]+=c[lenc]/10; //进位 c[lenc]%=10; //取个位 lenc++; } } lenc=lena+lenb; //去除前导0 while(!c[lenc]) lenc--; //把c存到c1里面 while(lenc>=1) c1+=c[lenc--]+'0'; return c1;}int main(){ string a1,b1; cin>>a1; cin>>b1; cout<<mul(a1,b1); return 0;} 4.高精度除法之高精除以低精没学高精度之前一直不知道为什么要把除法分成两个部分,学了之后才发现这难度根本不是一个级别的!awa 首先还是思考人是怎么列竖式做除法的。你们自己脑补一个竖式吧。(ˉ▽ˉ;)… 做除法的步骤是: 用现在判断到这一位之前的数除以除数,把商存在答案中; 计算余数; 循环1~2,直到判断完毕。 这应该是一个很正确的思路,但是其实它本来是对的,但是放到程序里就错了。 为什么呢? 首先除以除数(第一步)的代码是这个样子的: 1c[i]=(a[i]+c[i])/b; 第二步求余数的代码是这个样子的: 1c[i+1]=(a[i]+c[i])%b*10; 很容易注意到,在执行第一步操作的时候,$c[i]$的值会发生改变,但是求余数需要用到的$c[i]$却必须是原来的值。 所以在代码的实现中,要先求余数再求商。 代码: 12345678910111213141516171819202122232425262728293031323334353637#include<bits/stdc++.h>using namespace std;int a[10005],b,c[10005];string div(string a1,int b){ if(a1=="0") return "0"; //前置工作 string c1; int lena=a1.size(); //转换为整数类型 for(int i=0;i<lena;i++){ a[i+1]=a1[i]-'0'; //printf("%d",a[i+1]); } //除法 for(int i=1;i<=lena;i++){ /* ※错点:一定要先求下一位余了多少再求现在的位数, 因为求现在的位数时c[i]的值会更新 */ c[i+1]=(a[i]+c[i])%b*10; //求下一位余了多少 c[i]=(a[i]+c[i])/b; //计算当前位数的值 } int lenc=1; while(!c[lenc]) lenc++; for(int i=lenc;i<=lena;i++) c1+=c[i]+'0'; return c1;}int main(){ string a1; cin>>a1; scanf("%d",&b); cout<<div(a1,b); return 0;} 5.高精度除法之高精除以高精emmm…… 直到现在我的高精除高精还是没写出来啊╮(╯▽╰)╭,因为我感觉这个程序不是bug太多,而是整个程序就是一个bug…… 先看思路。 首先高精除高精是不能像低精除法那样直接除的(不然请你告诉我怎么除),这里采取的方式是:用减法模拟除法。 设置一个变量$flag$,代表现在判断到了第几位。在这一位以及这一位之前组成的数就是被减数,除数就是减数。判断一下两个数之间还能否相减(即被减数是否还大于减数),如果不行了就判断下一位,如果还可以就继续减,直到不能减为止。 说得简单,这玩意儿实现起来超级无敌难的。 我的代码一直都是 $\\texttt{TLE}\\ 10$ 分,不过思路应该是对的,也拿出来给大家看一下。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586//一个bug比头发还多的高精除高精代码 #include<bits/stdc++.h>using namespace std;int a[5005],b[5005],c[5005],d[5005];//这是判断是否还能减的函数 bool check(int flag,int lena,int lenb){ if(flag<lenb) return false; int sum=0;//统计前面有多少个前导0 for(int i=lena;i>=flag;i--){ if(!a[i]) sum++; else break; } if(flag-sum<lenb) return false; if(flag-sum>lenb) return true; //对于位数一样的数,判断是否可减 bool f=1;//是否完全相等 for(int i=lena-sum;i>=1;i--){ if(a[i]>b[i]) return true; if(a[i]!=b[i]) f=0; } if(f) return true; else return false;}string div(string a1,string b1){ //前置工作 if(a1=="0") return "0"; string c1; int lena=a1.size(); int lenb=b1.size(); //倒序存储并转换为整数类型 for(int i=0;i<lena;i++){ a[lena-i]=a1[i]-'0'; } for(int i=0;i<lenb;i++){ b[lenb-i]=b1[i]-'0'; } //开始用减法模拟除法 int flag=lena;//指针,现在到第几位了 int lenc=1; while(flag){ //printf("%d\\n",flag); if(!check(flag,lena,lenb)){//现在a已经小于b,需要进行下一位的除法 a[flag]=0; flag--; continue; }else{//a还未小于b,继续减 memset(d,0,sizeof(d)); int lend=flag; while(lend<=lena||lend<=lenb){ d[lend]+=a[lend]-b[lend]; if(d[lend]<0){ d[lend]+=10; d[lend+1]=-1; } lend++; } lenc++; for(int i=flag;i<=lena;i++) a[i]=d[i]; } } //去除前导0 while(!c[lenc]) lenc--; //把c存到c1里面 while(lenc>=1) c1+=c[lenc--]+'0'; return c1;}int main(){ string a1,b1; cin>>a1; cin>>b1; cout<<div(a1,b1); return 0;} (我那个 $10$ 分估计是特判 $0$ 得的?)","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"高精度","slug":"高精度","permalink":"https://blog.liynw.top/tags/%E9%AB%98%E7%B2%BE%E5%BA%A6/"},{"name":"模拟","slug":"模拟","permalink":"https://blog.liynw.top/tags/%E6%A8%A1%E6%8B%9F/"}]},{"title":"书的复制 题解","slug":"「Solution」Luogu_P1281","date":"2021-08-17T17:10:51.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/567a174a/","link":"","permalink":"https://blog.liynw.top/posts/567a174a/","excerpt":"题目传送门 第一道独立做出来的绿题祭(虽然花了我将近一个小时)","text":"题目传送门 第一道独立做出来的绿题祭(虽然花了我将近一个小时) 这是一道二分题目。 为了方便:因为这道题没有明确的规定抄书时间(反正不影响结果),所以我们不妨假设一个人抄一页需要 $1$ 分钟。 确定二分范围确定一下二分的范围: 最优情况:每个人都只抄一本,需要的时间为页数最大的那一本所需时间。 最坏情况:一个人要把所有书抄完,需要的时间为所有书加在一起的时间。 所以二分的范围就是从最大值到和。 核心代码然后二分函数开始验证,注意验证的是当只给那么多时间的时候这些人可不可以完成抄写任务。二分的地方很简单,主要的部分其实是输出答案那一部分和 $check$ 函数。 首先先来看输出答案那一部分。 注意题目中的一句话: 如果有多解,则尽可能让前面的人抄写少的页数。 定义 $l$ 为需要的最小时间(也就是二分出来的答案)。 因为要求让前面的人抄得尽量少,意思就是说让后面的人抄得尽量多(这个点一定要倒着想,如果是从前到后的话需要搜索,这样会 TLE)。那么就写一个循环,只要这个人现在抄的时间不超过 $l$,就让他一直抄,直到再加上一个就会超过为止,这样就可以保证后面的人抄得最多。 打一个有注释的代码(输出部分): 1234567891011121314int flag=n,c=0;//flag代表目前拿到的书的编号,c代表现在这个人抄的时间for(int i=k;i>=1;i--){//枚举每一个人,要倒着枚举 while(c+a[flag]<=l&&flag){//可以继续抄且有书可抄 c+=a[flag];//这个人抄的时间增加 flag--;//枚举下一本书 } s[i]=flag+1;//这个人抄完了,存一下开始的编号 c=0;//清空时间}for(int i=1;i<=k-1;i++){//输出:这个人抄书的结束点一定是下一个人的开始点-1 printf("%d %d\\n",s[i],s[i+1]-1);}printf("%d %d",s[k],n);//最后一个人的结束点是n,需要单独考虑exit(0);//输出后直接结束程序 然后再看有关 $check$ 函数的那一部分。 本人采取的方法是:让每本书都被抄到,看在此时间内能否用小于等于 $k$ 个人完成任务。如果可以,就往小的部分二分,反之往大的方向二分。 写一下 $check$ 函数(也是一个有注释的代码): 1234567891011121314bool check(int x){ //x为当前判断的时间长度 int r=1,c=0;//r:需要的人数 c:这个人当前抄书的时间 for(int i=1;i<=n;i++){ if(c+a[i]<=x)//可以继续抄这本书 c+=a[i]; else{//需要换下一个人抄这本书 r++; c=a[i]; } } if(r<=k)//答案是否小于等于总人数 return true; return false;} 一个问题:关于有人没活可以干那么现在你可能会问一个问题了:题目中不是还有这么一句话吗? 且每个人都必须抄写。 你咋不判断呢? 那么我们来分析一下: 首先题目对人数有个要求:$k≤m≤500$ 可见总人数是小于书的本数的。那么可以发现为了达到最短的时间,每个人都必须抄书,也就是说,最优解情况一定是每个人都会抄的。 可以证明这一点:假设有人没抄,现在必然有人的抄书时间是等于总用时的,如果耗时最大的人只抄了一本书,那么这个没抄的人可以去找一个抄多本书去“借”一本(题目的数据保证了如果有人没抄,那一定会有抄多本书的人),反正也不会影响结果。如果耗时最大的人抄了不止一本书,那么这个人可以帮他“分摊任务”,这样可以减小总用时,说明这一定不是最佳方案。可以得出:最优解一定是所有人都会抄书的。 洛谷的题目已经修正了不存在这种情况,但是鉴于我们 OJ 的毒瘤数据还是需要考虑一下这个点。当然你没考虑到也能 AC。 代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253#include<bits/stdc++.h>using namespace std;int n,k,sum,a[505],s[505];//sum是页数的和,s是输出的答案bool check(int x){ int r=1,c=0; for(int i=1;i<=n;i++){ if(c+a[i]<=x) c+=a[i]; else{ r++; c=a[i]; } } if(r<=k) return true; return false;}void f(int l,int r){ if(l==r){//已经找到满足条件的页数 //计算与输出结果 int flag=n,c=0; for(int i=k;i>=1;i--){ while(c+a[flag]<=l&&flag){ c+=a[flag]; flag--; } s[i]=flag+1; c=0; } for(int i=1;i<=k-1;i++){ printf("%d %d\\n",s[i],s[i+1]-1); } printf("%d %d",s[k],n); exit(0); } int mid=(l+r)>>1; if(check(mid)) f(l,mid);//因为答案就有可能是mid所以mid不能-1 else f(mid+1,r); return;}int main(){ int y=INT_MIN;//y为最大值 scanf("%d %d",&n,&k); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); sum+=a[i]; y=max(y,a[i]); } f(y,sum); return 0;}","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"二分答案","slug":"二分答案","permalink":"https://blog.liynw.top/tags/%E4%BA%8C%E5%88%86%E7%AD%94%E6%A1%88/"},{"name":"思维","slug":"思维","permalink":"https://blog.liynw.top/tags/%E6%80%9D%E7%BB%B4/"}]},{"title":"2021.3.13 考试总结","slug":"「ExamSummary」20210313","date":"2021-08-17T17:03:41.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/a3eddf00/","link":"","permalink":"https://blog.liynw.top/posts/a3eddf00/","excerpt":"今天是2021年3月13日,mjl 告诉我们,今天要考搜索。 我们:什么玩意儿???Σ(っ °Д °;)っ","text":"今天是2021年3月13日,mjl 告诉我们,今天要考搜索。 我们:什么玩意儿???Σ(っ °Д °;)っ 本人最终成绩:$0+0+70+0+86+10=166$ 反正就是挺无语的 qwq。 废话不多说,接下来直接进入 题解时间 T1 某班的学生订了 $N$ 天的午餐,知道他们每天要花多少钱。每天班主任可以选择自己支付或使用班费支付当天午餐费。班主任希望尽可能多地花掉班费,请你帮他计算他最多能够花多少班费。输入包含多个测试数据(不超过 $50$ 个)。$1\\leq N\\leq 30$,$0\\leq M\\leq 10^7$。 《论考试不看题不加多组输入考后直接崩掉》 但是其实就是加了估计也要爆零,只是从 WA 变成 TLE 而已。 这就是一道简化版的 The Robbery(考试题目少了一个较为复杂的优化),这里再简单提一下。 基础的搜索很简单,就是在搜索每一天的情况时继续搜索用班费支付和自己支付两种情况。 接下来是剪枝: 首先是排序,按照从大到小排,因为根据简单的贪心思维,先选大的取到最优解的可能性更大。 然后再求一下后缀和,如果在某个情况时前面已经选了的和后面所有没有选的还小于当前最大值,就直接 return。(最优性剪枝) 最后还要注意一下,因为是多组输入,所以统计后缀和的数组需要清空一下。 代码: 12345678910111213141516171819202122232425262728293031323334353637#include<bits/stdc++.h>using namespace std;int m,n,a[35],ans=0,hzh[35];bool cmp(int x,int y){ if(x<y)return false; return true;}void dfs(int t,int sum){ if(sum+hzh[t]<ans||sum>m)return; if(t==n+1){ ans=max(ans,sum); return; } //用班费 if(sum+a[t]<=m)dfs(t+1,sum+a[t]); //不用班费 dfs(t+1,sum); return;}int main(){ //freopen("lunch.in","r",stdin); //freopen("lunch.out","w",stdout); while(scanf("%d %d",&n,&m)!=EOF){ for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } sort(a+1,a+n+1,cmp); for(int i=n;i>=1;i--){ hzh[i]=hzh[i+1]+a[i]; } dfs(1,0); printf("%d\\n",ans); ans=0; memset(hzh,0,sizeof(hzh)); } return 0;} T2 有一个 $X\\times Y$ 的矩形蛋糕,要分成 $N$ 个面积相同的小块。每一切只能平行于一块蛋糕的一边(任意一边),并且必须把这块蛋糕切成两块。要求 $N$ 块蛋糕的长边与短边的比值的最大值最小。请求出这个比值,输出保留六位小数。$1\\le X,Y\\le 10000$,$1\\le N\\le 10$。 这道题主要难在思路(其实也不难),思路出来了基本上就没多大问题了。 这道题目看起来是个二分,但只要稍微冷静地分析一下就会发现这道题不能用二分,再想一下(其实是因为这是一次搜索测试啦),这道题是可以用 DFS 解决的,其思路和巧克力棒差不多。 首先我们假设这是一块长为 $x$ 宽为 $y$ 的蛋糕,需要把它分给 $n$ 个人。 我们可以横着切(平行于 $X$ 轴)或者竖着切(平行于 $Y$ 轴)。那么切哪里呢? 因为我们要把这个蛋糕分给 $n$ 个人,所以我们就切这条边可以被 $n$ 整除的地方。如图。 最后我们来看一下出口:假如说这个 $n=1$ 了,就说明不需要再切了,这个时候我们返回一下长与宽的比值。 (特别注意:最大比例的最小值,不要把 $\\max$ 和 $\\min$ 用反了) 代码: 1234567891011121314151617181920#include<bits/stdc++.h>using namespace std;double bzmax=INT_MAX;double dfs(double x,double y,int n){ if(n==1)return max(x,y)*1.0/min(x,y); double bzmax=INT_MAX; for(int i=1;i<=n/2;i++){//因为切上面和下面都是一样的,所以只用切一半 bzmax=min(max(dfs(x,i*y/n,i),dfs(x,y-i*y/n,n-i)),bzmax);//x bzmax=min(max(dfs(i*x/n,y,i),dfs(x-i*x/n,y,n-i)),bzmax);//y } return bzmax;}int main(){ //freopen("birthday.in","r",stdin); //freopen("birthday.out","w",stdout); int x,y,n; scanf("%d %d %d",&x,&y,&n); printf("%.6lf",dfs(x,y,n)); return 0;} T3 有一个纸片上的数和一个给定的目标数,你需要把纸片切成若干片,要求切出来的每一个数字加起来不超过目标数且与目标数最接近。如果目标数和输入纸片上的数相同,那么纸片不进行切割,如果不论怎样切割,分割得到的纸片上数的和都大于目标数,那么输出错误信息。如果有多种不同的切割方式可以得到相同的最优结果。那么输出拒绝服务信息。你需要给出对纸片的分割方式。输入包括多组数据。对每一组输入数据,输出相应的输出。有三种不同的输出结果: sum part1 part2 ...(*切割方案) rejected (*拒绝服务) error (*错误信息) 题面很长,但是实质就是添加号问题。 不过添加号真的很难 awa,再加上本人没做对,所以这里就不详细讲了,添加号在我第一周改对了之后直到第二周都还停留在考试的 70 分。 考试 70 分代码(样例倒数第二个有未知玄学错误,大家看看思路就行了): 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253//*#include<bits/stdc++.h>using namespace std;int s,tot=0,len,ans=0,js=1;char paper[8];int a[8],b[8];void dfs(int t,int sum,int last){//判断t和t+1之间的部分 last:上一个数 if(sum+last>s)return; if(t==len+1){ if(sum>ans){ tot=1,ans=sum; int i=1; memset(b,0,sizeof(b)); while(a[i])b[i]=a[i],i++; } else if(sum==ans)tot++; return; } //切割 a[js]=last*10+paper[t]-'0'; js++; dfs(t+1,sum+last*10+paper[t]-'0',0); a[js]=0; js--; //不切割 dfs(t+1,sum,last*10+paper[t]-'0'); return;}int main(){ //freopen("paper.in","r",stdin); //freopen("paper.out","w",stdout); while(1){ scanf("%d %s",&s,paper+1); len=strlen(paper+1); if(!s&&paper[1]=='0')return 0; dfs(1,0,0); if(tot>=2){ printf("rejected\\n"); } else if(tot){ printf("%d ",ans); int i=1; while(b[i]){ printf("%d ",b[i]); i++; } printf("\\n"); }else printf("error\\n"); tot=0,ans=0,js=1; memset(a,0,sizeof(a)); } return 0;} T4 给出两个单词(开始单词和结束单词)以及一个词典。找出从开始单词转换到结束单词,所需要的最短转换序列。转换的规则如下: 每次只能改变一个字母。 转换过程中出现的单词(除开始单词和结束单词)必须存在于词典中,开始单词和结束单词可以不在词典中。如果不能找到这种变换,则输出 0。 《论一个人考试不记得字符串的操作函数然后直接原地爆炸的详细过程》 其实这道题应该不用这些奇奇怪怪的函数也可以做,但是当时看到这道题貌似有点难,想了一会儿思路没出来就去看别的题了。 其实这道题我们可以每一次都统计一下哪些单词和当前的单词可以转换然后在用新的单词继续搜索。 注意一个优化(最优性剪枝),不然会 TLE: 拿一个数组存一下从初始单词到这个单词需要多少步,如果现在我们递归到这个单词时的步数已经大于最优的步数就直接 return。 其实这道题可以用 BFS 做而且据说还好做一点,但是我考试是这么做的就这么写题解吧。 AC 代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546#include<bits/stdc++.h>using namespace std;int len,n,Min=INT_MAX;char a[1005][10],end[10];bool b[1005];int dp[1005];//记忆化数组,表示从开头到这个单词至少需要几步 //判断两个单词是否可以互相转换的 check 函数 bool check(int x,int y){ int sum=0; for(int i=1;i<=len;i++){ if(a[x][i]!=a[y][i])sum++; } if(sum==1)return true; return false; }// dfs 函数 t:需要操作的字符串 l:目前操作的步数 bh:需要操作字符串的编号 void dfs(char t[10],int l,int bh){ //printf("%s %d %d\\n",t,l,bh); if(l>=Min||dp[bh]&&l>=dp[bh])return; dp[bh]=l; if(check(bh,n+1)){ // 两个字符串只有一个字符不一样 Min=min(Min,l+1); return; } if(l==n)return; for(int i=1;i<=n;i++){ if(!b[i]&&check(bh,i)){ b[i]=1; dfs(a[i]+1,l+1,i); b[i]=0; } } return;}int main(){ //freopen("word.in","r",stdin); //freopen("word.out","w",stdout); scanf("%s %s",a[0]+1,end+1); len=strlen(end+1); n=1; while(scanf("%s",a[n]+1)!=EOF)n++; strcpy(a[n+1]+1,end+1); dfs(a[0]+1,1,0); printf("%d",Min); return 0;} T5 在一个 $n$ 行 $m$ 列单元格构成的地图中,每一个格子里都有可能是空地(.),墙(#)或者僵尸(G),且四边肯定是墙。若放置一个炸弹,它会以放置点为中心进行行列延伸炸到同行同列的僵尸,但不能穿墙。有一个人去放一个炸弹,他初始位置在 $(x,y)$,只能在地图中朝上下左右四个方向行进,不能穿墙,也不能穿越僵尸。求最多能炸到的僵尸数量。 考场上打了个广搜+记忆化得了 $80$ 分,后来讲题的时候 mjl 说: 在两堵墙之间,横向或纵向能炸到僵尸的数量相同。 然后我就很 zz 的在函数里面加上了这个优化…… 最终从 80 掉到了 75。 后来我去问 mjl,他给我看了个 AC 代码……(这玩意儿还是要加在主函数里的?!) AC 代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071#include<bits/stdc++.h>using namespace std;char a[2005][2005];bool b[2005][2005];int dp1[2005][2005],dp2[2005][2005],dp3[2005][2005],dp4[2005][2005];int m,n,x,y,ans=-1;int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};struct s{ int x,y;}t1,t2;void bfs(int x,int y){ queue<s> q; t1.x=x,t1.y=y; q.push(t1); int dx,dy; while(!q.empty()){ t1=q.front(); q.pop(); for(int i=0;i<=3;i++){ dx=t1.x+dir[i][0],dy=t1.y+dir[i][1]; if(dx>=1&&dx<=m&&dy>=1&&dy<=n&&!b[dx][dy]&&a[dx][dy]!='#'&&a[dx][dy]!='G'){ b[dx][dy]=1; if(dp1[dx][dy]+dp2[dx][dy]+dp3[dx][dy]+dp4[dx][dy]>ans) ans=dp1[dx][dy]+dp2[dx][dy]+dp3[dx][dy]+dp4[dx][dy]; t2.x=dx,t2.y=dy; q.push(t2); } } } return;}int main(){ //freopen("zombie.in","r",stdin); //freopen("zombie.out","w",stdout); scanf("%d %d %d %d",&m,&n,&x,&y); for(int i=1;i<=m;i++){ scanf("\\n"); for(int j=1;j<=n;j++){ scanf("%c",&a[i][j]); if(a[i][j]=='G'){ dp1[i][j]=dp1[i][j-1]+1; dp2[i][j]=dp2[i-1][j]+1; }else{ dp1[i][j]=dp1[i][j-1]; dp2[i][j]=dp2[i-1][j]; } if(a[i][j]=='#'){ dp1[i][j]=0; dp2[i][j]=0; } } } for(int i=m;i>=1;i--){ for(int j=n;j>=1;j--){ if(a[i][j]=='G'){ dp3[i][j]=dp3[i][j+1]+1; dp4[i][j]=dp4[i+1][j]+1; }else{ dp3[i][j]=dp3[i][j+1]; dp4[i][j]=dp4[i+1][j]; } if(a[i][j]=='#'){ dp3[i][j]=0; dp4[i][j]=0; } } } bfs(x,y); printf("%d",ans); return 0;} T6 一个人进入了一个两层的迷宫,迷宫的入口是 $S(0,0,0)$,里面有时空传输机(用 # 表示),墙(用 * 表示),平地(用 . 表示)。进入时空传输机会被转到另一层的相对位置,但如果被转到的位置是墙,他就会被撞死。他只能在一层中前后左右移动,每移动一格花 $1$ 时刻。层间的移动只能通过时空传输机,且不需要任何时间。如果他能在 $T$ 时刻及以前到 $P$ 点输出 YES,否则输出 NO。多组数据。 $1 <= N,M <=10$。 其实我根本不知道自己考试的代码为什么会错,但是经过了代码的重构与 mjl 的帮助之后我 A 了这道题。 首先思路 BFS,其他的内容和走迷宫的 BFS 经典题目差不多,唯一的区别就是传送门,如果走到了传送门就传送到另外一层,并且这两个传送门都会走一遍。需要特别注意的是不要陷入上下两个都是传送门的死循环。 代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970#include<bits/stdc++.h>using namespace std;int m,n,T;char a[15][15][2];bool b[15][15][2];int dir[4][2]={{-1,0},{0,-1},{1,0},{0,1}};struct s{ int x,y,z,l;}t1,t2;//取相反数:0到1,1到0 int f(int x){ if(x)return 0; return 1;}bool bfs(int x,int y,int z){ queue<s> q; t1.x=x,t1.y=y,t1.z=z,t1.l=0; q.push(t1); int dx,dy; while(!q.empty()){ t1=q.front(); q.pop(); //特判:#和P if(a[t1.x][t1.y][t1.z]=='#'){ if(a[t1.x][t1.y][f(t1.z)]=='P'||a[t1.x][t1.y][f(t1.z)]=='.'){ t1.z=f(t1.z); q.push(t1); } continue; }else if(a[t1.x][t1.y][t1.z]=='P')return true; for(int i=0;i<=3;i++){ dx=t1.x+dir[i][0],dy=t1.y+dir[i][1]; if(dx>=1&&dx<=m&&dy>=1&&dy<=n&&a[dx][dy][t1.z]!='*'&&!b[dx][dy][t1.z]){ b[dx][dy][t1.z]=1; t2.x=dx,t2.y=dy,t2.z=t1.z,t2.l=t1.l+1; if(t2.l>T)continue; q.push(t2); } } } return false;}int main(){ freopen("plan.in","r",stdin); freopen("plan.out","w",stdout); int c,x,y,z; scanf("%d",&c); while(c--){ memset(b,0,sizeof(b)); scanf("%d %d %d",&m,&n,&T); for(int i=1;i<=m;i++){ scanf("\\n"); for(int j=1;j<=n;j++){ scanf("%c",&a[i][j][0]); if(a[i][j][0]=='S')x=i,y=j,z=0; } } scanf("\\n"); for(int i=1;i<=m;i++){ scanf("\\n"); for(int j=1;j<=n;j++){ scanf("%c",&a[i][j][1]); if(a[i][j][1]=='S')x=i,y=j,z=1; } } if(bfs(x,y,z))printf("YES\\n"); else printf("NO\\n"); } return 0;} 总结时间T1 没看到多组输入,以为就是道水题,看到题目的背景之后没有任何联想,也没有想到那道让我犹豫了很久的 The Robbery,导致一道本来会做的题丢掉 $100$ 分。 T2 本来这么水一道题也没有想到思路,这道题应该就是那种思路很难想,但是一想出来就很简单的那种题吧,得亏我们之前还做过一道类似的题目…… T3 出了一个 bug,然后我在那个地方调了很久,其实当时应该先把这个部分分拿到之后就先去看别的题目,因为 bug 不好说什么时候调得出来,可是有些题的部分分是很好得的…… T4 因为不会函数就直接跳过了,其实就相当于我又跳过了一道比较简单的题目,所以事实证明: 函数不会不要虚,有时候手打函数比系统自带的还要快,比如说$\\max$,$\\min$ 等。 要背常用函数。 T5 的话,还是优化方法没有完全掌握吧……平时刷题不够多。 T6 属于那种过完样例就走人行为,因为 $10$ 分的话确实太少了…… 所以经过这次考试,可以发现: 考试比赛如果题目比较难,骗分和部分分永远是王道(*^-^*)。 常用函数是一定要背的(而且不要被错),考试用节约时间,但是如果背不到也不要直接放弃题目,只要你手打得出来就继续做。(当然如果这道题做不出来就另当别论了!) 安排好考试时间,如果一道题有 bug,不管可不可以拿到一部分分,也先不要在这道题死磕(尤其是死磕超过一节课的时间),先去看一下别的题有没有思路了。 认真读题。尤其注意别漏了恶心人的多组输入。 要充分发挥自己的联想能力。要定期复习代码,不要以为 AC 了就万事大吉,这样考试的联想才能发挥作用。 希望这篇博客可以帮助到大家(还有我自己),感谢观看!","categories":[{"name":"考试总结","slug":"考试总结","permalink":"https://blog.liynw.top/categories/%E8%80%83%E8%AF%95%E6%80%BB%E7%BB%93/"}],"tags":[{"name":"搜索","slug":"搜索","permalink":"https://blog.liynw.top/tags/%E6%90%9C%E7%B4%A2/"}]},{"title":"打包装箱 题解","slug":"「Solution」打包装箱","date":"2021-08-17T16:58:08.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/96ec7789/","link":"","permalink":"https://blog.liynw.top/posts/96ec7789/","excerpt":"","text":"Update on 2021/10/1:修复了 $\\LaTeX$ 格式。 1.题目原题链接(CQBZOJ) 题目描述学校组织了爱心捐书活动,同学们纷纷踊跃把自己看过的旧书拿出来捐给贫困山区的孩子们。图书室的马老师把同学们捐献的书打包在了6种纸箱子里(打包好的各类纸箱子有若干个),纸箱子的高相同,但底面积分为 $1\\times 1$,$2\\times 2$,$3\\times 3$,$4\\times 4$,$5\\times 5$,$6\\times 6$。现为了装车方便,需要把这些纸箱子装在若干个 $6\\times 6$ 的木箱子,木箱子的高和打包纸箱子相同,请你帮助马老师,用最少的木箱子打包完所有的纸箱子。 输入一行(六个数)用空格隔开,分别表示 $1\\times 1$,$2\\times 2$,$3\\times 3$,$4\\times 4$,$5\\times 5$,$6\\times 6$ 六类纸箱子的个数(每类箱子的个数小于等于 $100$)。 输出一行,最少的木箱子个数 样例输入16 5 4 3 2 1 样例输出17 是的,为了我们“亲爱”的mjl,我把题目改了(( 2.算法分析用最少的木箱子装完,也就是说让剩余的空间最小,最好的情况就是除了最后一个箱子,其他箱子都装满,所以只需要考虑箱子内部的情况。故这道题可以用贪心算法解决。 3.思路要解决箱子内部如何装满的问题,我们首先需要根据题目条件建立模型。题目告诉我们有六种不同的纸箱子和木箱子的大小,根据这些条件我们可以列出箱子内部装满时的模型如下(PS:最上面的那个数字指的是最大的那个纸箱子): 可以看出,在纸箱子的大小为 $3\\times 3$ 时,情况比较多比较复杂,所以需要着重考虑。 需要注意的是,在装箱的时候我们会优先把所有 $3\\times 3$ 的纸箱子装到一个木箱子里(即 $4$ 个 $3\\times 3$ 的箱子装在一个木箱子里),所以剩下的那三种情况只需要判断一次。(我就是在这个地方栽的) 这里再补充两个编程的技巧(不想看请跳过): (1)向上取整如果一个人问大家怎么向上取整的话,估计大多数人的回答应该都是用 <cmath> 头文件里的 ceil 函数。但是这多少有点麻烦,如果你懒得写为了方便,其实根本不需要什么函数,用整数除法就可以实现。 在 c++ 中,整数的除法是默认为向下取整。我们假设 $n$ 可以被 $k$ 整除且 $n/k=q$,那么: $(n+1)/k=q$ $(n+2)/k=q$ $……$ $(n+k-1)/k=q$ $(n+k)/k=q+1$ 由此可以看出,如果我们要向上取整一个除法 $n/k$ (余数为 $r$ ),就需要把 $n$ 的范围提到 $n+(k-r)$ 到 $n+(k-r)+(k-1)$ 这个范围之内,而当这个被除数的加数取到 $k-1$ 的时候,不管余数怎么变,被除数永远都在这个范围。综上所述,向上取整的公式为: $\\lceil n/k \\rceil=(n+k-1)/k$ (2)辅助数组——为“偷懒”而生在数的计算中,我们通常要根据结果的不同而使用不同的值。这个问题可以用条件选择语句解决,但是当情况太多的时候就会造成程序的内容繁杂。(而且写着也麻烦!)当一个计算结果与一个值可以一一对应的时候,我们就可以用辅助数组简化程序。 (注:如果数与值不是一一对应的,也可以使用辅助数组,但是一定是一个值对应一个结果(也就是说不同的结果可以对应同一个值),但是在本人的学习范围之内辅助数组不能同一个结果对应多个值。) 拿这道题举例,由于 $3\\times 3$ 纸箱子的个数不一定能被 $4$ 整除,这也就意味着当个数除以 $4$ 的时候余数可能是 $0,1,2,3$,不同的值对应边长为 $2\\times 2$的纸箱子在放有这些剩余的 $3\\times 3$ 的箱子的木箱子中能放得下的不同个数,计算的结果与值一一对应。所以我们可以设置一个辅助数组 cnt,从 $0$ 到 $3$ 号位分别存储余数为 $0,1,2,3$ 的不同情况所对应的值,这样我们在使用这个值的时候就可以直接调用数组,不用写条件语句。 4.编写代码终于到了激动人心的时刻了! AC 代码(不要复制,但你要复制我也没办法): 12345678910111213141516#include<cstdio>int cnt[4]={0,5,3,1};//辅助数组int main(){ int a1,a2,a3,a4,a5,a6,sum=0,x,y; scanf("%d %d %d %d %d %d",&a1,&a2,&a3,&a4,&a5,&a6); //边长为6,5,4,3的盒子需要的箱子数 sum=a6+a5+a4+(a3+3)/4; //边长为2的盒子在已经装了的箱子中可以装的数量 x=a4*5+a3*cnt[a3%4]; if(x<a2)sum+=(a2-x+8)/9; //边长为1的盒子在已经装了的箱子中可以装的数量 y=sum*36-a6*36-a5*25-a4*16-a3*9-a2*4; if(y<a1)sum+=(a1-y+35)/36; printf("%d",sum); return 0;} 5.总结这应该算是一道对于本人来说比较难的贪心,但是主要难在思路,代码不难,思路想出来题目就没多大问题了。 但是之前在做的时候一直是 $91$ 分,调了很久的代码才发现x在计算的时候 cnt[a3%4] 不需要乘a[3],还是思路没有彻底理清楚 ,,ԾㅂԾ,,","categories":[{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"}],"tags":[{"name":"贪心","slug":"贪心","permalink":"https://blog.liynw.top/tags/%E8%B4%AA%E5%BF%83/"}]},{"title":"给Dev-C++设置黑暗模式","slug":"「Water」Darkmode_in_Dev_cpp","date":"2021-08-17T16:46:42.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/da971dbf/","link":"","permalink":"https://blog.liynw.top/posts/da971dbf/","excerpt":"多图警告!我把较大的图片传到了另一个图床。如果图片没加载出来请反复刷新! 效果如下图:","text":"多图警告!我把较大的图片传到了另一个图床。如果图片没加载出来请反复刷新! 效果如下图: 首先你需要有一个Dev-c++(我想大家都有)。 点进去,打开自己的程序。 没错因为我打八皇后的时候心态炸了于是变量名…… 点击上面工具栏的【工具】—>【编译器选项】,会弹出来这么一个东西: 找到【高亮显示当前行】,点击【色彩】(如果没有启用先点击启用,然后再选颜色),选择black。 这是为了让当前点击的那一行不那么刺眼。 然后点击上面的【语法】,会出来一个界面: 找到左下角的【预设】,点击那个长条,选择【GSS Hacker】,操作后界面如下: 找到左上角那个写着很多英语单词的那个框框,需要一个一个点击。 调颜色是靠左的两个带颜色的长条,第一个是字体颜色(前景),第二个是背景颜色(背景),如图所示。 1.$Assemblen$ 集合 本人不知道这东西是干什么的,平常编程也没遇到,就先不管它了(反正也是蓝色系的)。 2.$Character$ 字符(即用单引号框起来的字符们) 这里的【背景】我们不用去管它,点击【前景】,然后跳出来一个选颜色的框框。选择最上面的【Custom】。 这个时候会跳出一个更加详细的选颜色的界面。点击下面的【规定自定义颜色】。 然后看到下面的六个参数,把参数的颜色调成这个样子(其实不用调,检查一下): 3.$Comment$ 注释 不用管背景,点击【前景】,不过这次不用点【Custom】,选择【Gray】就可以了。 4.$Float$ 浮点数(小数) 不用管背景,点击【前景】—>【Custom】—>【规定自定义颜色】,把参数调成这样: 5.$Hexadecimal$ 十六进制(的数) 同4 6.$Identifier$ 标识符(如$main$,变量名等) 不用管背景,点击【前景】—>【Custom】—>【规定自定义颜色】,把参数调成这样: 7.$Illegal$ $Char$ 不合法的的字符 不用管(虽然不好看,但是我觉得字符打错了就需要一个很丑的颜色来提醒你……)。 8.$Number$ 数字(整数类型,十进制) 同4、5,一家人就要整整齐齐【大写的滑稽】 9.$Octal$ 八进制(的数) 同4、5、8,得,又来一个 10.$Preprocessor$ 前面带“#”的玩意儿 不用管背景,点击【前景】—>【Sliver】。 11.$Reserved$ $Word$ 保留字符(如$int$,$if$等) 不用管背景,【前景】—>【Custom】,然后选上面表格中第三行第八列的猛男色。 12.$Space$ 空格 不用管。 13.$String$ 字符串(即用双引号框起来的字符们) 不用管背景,点击【前景】—>【Teal】。 14.$Symbol$ 符号(类似于括号) 不用管背景,点击【前景】—>【Sliver】。 15.$Selected$ $text$ 被选中的地方 不用管前景,点击【背景】—>【Gray】。 16.$Gutter$ 旁边那一列数字(列号) 不用管背景,点击【前景】—>【Sliver】。 17.$Breakpoints$ 断点(调试用的) 点击【前景】—>【Black】,【背景】—>【Custom】—>【规定自定义颜色】,把参数调成这样: 18.$Error$ $line$ 编译错误时的那条红杠杠 这个我们不用管,这个颜色还可以。 19.$Active$ $Breakpoints$ 正在执行的断点 也不用管。 20.$Floding$ $lines$ 花括号之间的点点 不用管$ * $3。 作者在偷懒hhhc 然后颜色就调好了。如果大家觉得不好看,可以自己调。 最后点击下方【预设】那个正方形的按键: 然后给这个预设取个名字,最后点一下【OK】。 勾选【使用语法加亮】。 ↓ 不勾选【使用语法加亮】的后果 最后点击确定,你的C++就成开头展示的那样了。 完成! (Tips:调完之后建议把左边的白框框拉掉,不然反差很大) 今天的保姆级教程就到这里了哦~ 顺便附上这个水贴的思路来源","categories":[{"name":"水贴","slug":"水贴","permalink":"https://blog.liynw.top/categories/%E6%B0%B4%E8%B4%B4/"}],"tags":[{"name":"编译器","slug":"编译器","permalink":"https://blog.liynw.top/tags/%E7%BC%96%E8%AF%91%E5%99%A8/"}]},{"title":"递推、递归学习笔记","slug":"「Algorithm」递推与递归","date":"2021-08-17T13:04:24.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/25bca6af/","link":"","permalink":"https://blog.liynw.top/posts/25bca6af/","excerpt":"","text":"Update on 2021/10/1:对文章进行优化,修复 $\\LaTeX$。 前言各位在座的在站的在躺的在趴的以及以各种奇奇怪怪的姿势出现在电脑前的大佬们大家好,虽然我知道你们都是大佬,但是这篇博客,我还是会把你们当成萌新来讲解的(。 好了,废话不多说,下面进入正题。 概念递推mjl:从已知道的若干项出发,利用递推关系依次推算出后面的未知项的方法,我们称为递推算法。 做递推算法最关键的是找出递推式,然后再求出最简单的情况的值并存在数组里。 这个要直接求出值的情况个数要根据递推式来看。 比如说,如果一个递推式是 $f(i)=f(i-3)+f(i-2)$,那么我们需要把 $1,2,3$ 的情况都先写出来,然后从 $4$ 开始循环。 递推模板: 1234567891011#include<cstdio>const int maxn=...;int a[maxn]={需要存的最简单的情况};int main(){ int n; scanf("%d",&n); for(int i=...;i<=n;i++){ 递推式; } printf("%d",a[n]);} 递归mjl:从已知问题的结果出发,用迭代表达式逐步推算出问题的开始的条件,即顺推法的逆过程,称为递归。 当然这是算法方面的解释,递归其实就是套娃函数自己调用自己,只不过比纯粹的套娃要难$10^8$ 些罢了(。 从算法的角度来说,显然无限套娃是没有意义的,所以我们也需要一个递归的出口来结束递归。 因为递归变化较多,模板不太好写,就不写了。 区别与联系之所以把递推和递归放在一起讲说明他们是有共同点的,根据本人为数不多的做题经验(一定要多刷题啊!awa),可以发现递推和递归都是要推出在一个规律下,不同值对应的不同结果之间的关系,从而推出答案的值,也都需要一个出口来结束程序。 mjl:而递推与递归的不同之处在于,递归是从未知到已知,逐步接近解决问题的过程,而递推从已知到未知。 翻译成人话就是:递推是从小到大一点一点推,而递归是一个栈的结构——先从最终结果出发,一点一点往前推,直到推到出口,再根据出口的数值把答案推出来。 最直观的图: (↑以 $f(x)=f(x-1)+x$ 且 $f(1)=1$ 举例,求 $f(5)$,递推与递归的区别与联系) 递推递归的 5 种模型递推递归有一些模型,这些模型可以在递推和递归中通用。 1. 斐波那契数列(Fibonacci)这是一种很简单的递推模型。 这种模型一般都是,此时第 $x$ 项数据与前面的数据有直接的数值关联(一般来说是很明显的倍数关系)。 例题-铺砖1原题链接(CQBZOJ) 有 $2\\times n$ 的一个长方形方格道路,只有一种的 $1\\times 2$ 砖去铺,总共有多少种铺法?($0\\le n\\le 45$) 很明显可以看到末尾的砖块(只是末尾)有两种放法: 竖着放一块; 横着放两块。 我们在放最新的第 $i$ 列,即末尾的砖块时,这两种放法对应着 $i-1$ 和 $i-2$ 的情况,如下图。 所以可以看出这是一道很经典的 Fibonacci 递推的题目。 递推式:$a_i=a_{i-1}+a_{i-2}$ $80$ 分代码(很简单): 123456789101112#include<cstdio>int f(int n){ if(n==1)return 1; if(n==2)return 2; else return f(n-1)+f(n-2);}int main(){ int n; scanf("%d",&n); printf("%d",f(n)); return 0;} 至于为什么是 $80$ 分,这里先不说,看到后面就知道了。 2.汉诺塔(Hanoi)汉诺塔本身的问题是把若干从小到大堆叠的圆盘借助一根辅助的柱子从一根柱子移到另一根柱子上,要求一次只能移动一个,而且大的不能压在小的上面。 我们研究最原始的问题。 例题-汉诺塔问题原题链接(CQBZOJ) 有三个柱子,其中一根上从大到小叠着 $n$ 个圆盘。现在需要移动这些圆盘,规则如下: 一次只许移动一个盘; 任何时候、任何柱子不允许把大盘放在小盘上面; 可使用任一一根立柱暂存圆盘。 问:如何使用最少步数实现 $n$ 个盘子的移动?打印出具体移动方案。 数据范围:$1\\le n\\le 18$ 我们可以假设这三根柱子分别为 $A$,$B$,$C$,我们把 $n$ 个盘子从 $A$ 借助 $B$ 移到 $C$ 上。 如果只有一个盘子,我们就直接移。 如果有两个以上的盘子,我们就分 $3$ 步做: 第一步:把上面 $n-1$ 个盘子从 $A$ 借助 $C$ 移到 $B$ 上,腾出 $C$ 的位置; 第二步:把留在 $A$ 上的最大的圆盘移到 $C$ 上; 第三步:把暂时放在 $B$ 上的其他圆盘从 $B$ 借助 $A$ 移到 $C$ 上。 而第一步和第三步怎么移过去,就需要继续把这个问题分解成最大的和其他的问题,直到只剩下一个圆盘,就直接移过去。 递推式(只求步数不求过程):$a_i=2\\times a_{i-1}+1$ 要求输出过程只能用递归。 AC 代码: 1234567891011121314#include<cstdio>//n:圆盘数量,a、b、c:柱子编号void h(int n,char a,char b,char c){ if(n==0)return; h(n-1,a,c,b); //第一步 printf("%c->%c\\n",a,c); //第二步 h(n-1,b,a,c); //第三步}int main(){ int n; scanf("%d",&n); h(n,'A','B','C'); return 0;} 3.平面分割用一些两两相交但是不会三个及以上相交的圆把平面分成若干个区域。 观察答案,我们会发现: $ans_2-ans_1=2$ $ans_3-ans_2=4$ $ans_4-ans_3=6$ $……$ 很明显的差等差数列。发现这个之后规律就出来了: 递推式:$a_i=a_{i-1}+2\\times (i-1)$ 12345678910#include<cstdio>int a[105]={0,2};int main(){ int n; scanf("%d",&n); for(int i=2;i<=n;i++){ a[i]=a[i-1]+2*(n-1); } printf("%d",a[n]);} 4.卡塔兰数(Catalan)难点来了!这玩意儿很难,主要是规律很难发现,而且做题也不容易看出来。 卡塔兰数本来是将一个 $n$ 边形通过不相交的对角线分成若干个三角形,求这个 $n$ 边形有多少种不同的划分方法。 最原始的题目: 原题链接(CQBZOJ) 要解决这个题目,首先我们需要一个 $n$ 边形: 然后我们再选定一个 $i$($2\\leq i \\leq n-1$)点,把 $1$ 到 $i$ 和 $n$ 到 $i$ 两条对角线连起来,把这个 $n$ 边形分成三个部分:($i$ 以 $3$ 为例) 可见,这个 $n$ 边形被分成了一个 $i$ 边形、一个三角形和一个 $(n-i+1)$ 边形。 而这个 $i$ 边形和 $(n-i+1)$ 边形可以用相同的方法去求解。 每个满足条件的 $i$ 点都可以选一遍。 所以这个递推式就是这样的: $a_i=a_2\\times a_{n-2+1}+a_3\\times a_{n-3+1}+a_4\\times a_{n-4+1}+\\ldots+a_{n-1}\\times a_2$ (当然要记住,$a_2=0$,但是我们在计算的时候,为了计算的准确,我们规定 a[0]=a[1]=a[2]=1,最后再把 $a_2$ 改回去。) 用求和公式表达就是: $a_i=\\sum_{j=2}^{n-1} \\limits a_j\\times a_{n-j+1}$ 作为一个初学者,我并不明白 $\\sum$ 这玩意儿是啥意思,其实它叫”求和符号“,具体大家去网上搜吧。 (这里就用递推了好理解一些,注意开 long long,不然会炸。) 1234567a[0]=a[1]=a[2]=1;for(int i=3;i<=n;i++){//从3开始枚举 for(int j=2;j<=i-1;j++){ a[i]+=a[j]*a[i-j+1]; } } a[2]=0; 好了讲完了理论我们就来看看实际应用。 例题-编程社买书原题链接(CQBZOJ) 为了进一步提高编程能力,编程社的 $2n$ 个同学决定去购买《信息学奥赛一本通》,书的价格为 $50$ 元。卖书的书店没有零钱找补,但是有一个特殊的找零装置,放入这个装置的钱只能从最上面的一张拿。其中,$n$ 个同学手中仅有一张 $50$ 元,另外 $n$ 个同学手中仅有一张 $100$ 元。请问一共有多少种排队方案使得所有的同学都可以买到书?$(1\\leq n\\leq100)$ 这道题的关键是:如何看出这是一道卡塔兰。 样例+打表?显然不是。 首先,不难发现,不管我们遍历到哪个位置,这个位置和他前面的位置中,$50$ 元的数量必须比 $100$ 多或相等。可以看出:第一个人必须拿 $50$。 我们在 $2-2n$ 这个区域中随机选一个人,编号为 $i$(不管他拿的是什么钱)。 如果前 $i$ 个人可以做到拿 $50$ 的数量 $\\ge$ 拿 $100$ 的数量,那么这种情况就可以算一种正确答案。 而我们从第二个人开始,一直选到第 $2n$ 个人,就是所有的情况。 我们把这 $2n$ 个人分成了 $3$ 个部分,这不就是卡塔兰数吗? 代码需要高精度,大家可以自行复制板子:)。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758#include<cstdio>#include<string>#include<iostream>using namespace std;string a[10005];string add(string a1,string b1){ int a[10005]={},b[10005]={},c[10005]={}; string c1; int lena=a1.size(); int lenb=b1.size(); for(int i=0;i<lena;i++) a[lena-i]=a1[i]-'0'; for(int i=0;i<lenb;i++) b[lenb-i]=b1[i]-'0'; int lenc=1; while(lenc<=lena||lenc<=lenb){ c[lenc]+=a[lenc]+b[lenc]; c[lenc+1]=c[lenc]/10; c[lenc]%=10; lenc++; } while(!c[lenc]) lenc--; while(lenc>=1) c1+=c[lenc--]+'0'; return c1;}string mul(string a1,string b1){ int a[10005]={},b[10005]={},c[10005]={}; if(a1=="0"||b1=="0") return "0"; string c1; int lena=a1.size(); int lenb=b1.size(); for(int i=0;i<lena;i++) a[lena-i]=a1[i]-'0'; for(int i=0;i<lenb;i++) b[lenb-i]=b1[i]-'0'; int lenc; for(int i=1;i<=lena;i++){ for(int j=1;j<=lenb;j++){ lenc=i+j-1; c[lenc]+=a[i]*b[j]; c[lenc+1]+=c[lenc]/10; c[lenc]%=10; lenc++; } } lenc=lena+lenb; while(!c[lenc]) lenc--; while(lenc>=1) c1+=c[lenc--]+'0'; return c1;}int main(){ int n; scanf("%d",&n); a[0]=a[1]="1"; for(int i=2;i<=n;i++){ // 注意这个地方还是和板子略微有一些区别,至于为什么,请读者自行思考 for(int j=1;j<=i;j++){ a[i]=add(a[i],mul(a[j-1],a[i-j])); } } cout<<a[n]; return 0;} 5.第二类 Stirling 数这是个二维递推的模型,也就是说,有两个值在影响结果。第二类 Stirling 数的本质是排列组合(个人理解),题目没有固定的解题公式,但是会有非常紧密的联系,大家可以看看下面的三个例子。 例题1-合理放球原题链接(CQBZOJ) $n$ 个各不相同球放入 $m$ 个相同的盒子里,球全部放完后,要求最后没有空盒,求不同的放法总数。($0 < n,m\\leq 20$) 我们可以用一个二维数组存放答案。然后假如说我们要把$i$个球放在$j$个盒子里,那么有三种特殊情况: $i=1$:因为题目要求顺序不算,所以只有在一个盒子里放。$a_{1,j}=1$; $j=0$:因为没有盒子,所以没办法放。$a_{i,0}=0$; $j=1$:因为只有一个盒子,所以只能都放在这个盒子里。$a_{i,1}=1$。 还有三种普通情况: $i<j$ 这种一个就不用说了吧,这种情况肯定是没有了(因为不能有空盒子)。$a_{i,j}=0$; 注意:上面的 $i=1$ 如果这这里满足条件的话也要变成 $0$,所以特殊情况和普通情况需要分开判断。也就是说这里不能再用 else if 了。 $i=j$ 这种情况也很简单,因为不能有空盒子,所以只能每个盒子放一个球。$a_{i_j}=1$; $i>j$ 本题考点。 首先我们分析一下有 $i$ 个球分到 $j$ 个盒子里的情况,可以分析出来两种变成这样的方式: 先把 $i-1$ 个球放到 $j$ 个盒子里面,再往里面加一个球; 先把 $i-1$ 个球放到 $j-1$ 个盒子里面,然后再加一个装了一个球的盒子。 第一种情况因为不管加在哪个盒子里都可以,所以有 $j$种方法。第二种只有一个方法。 所以递推式为:$a_{i,j}=a-{i-1,j-1}+j\\times a_{i-1,j} $ 最后加上其他的东西,组合成 AC 代码~(注意开 long long): 123456789101112131415161718#include<cstdio>long long a[25][25];int main(){ int m,n; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ for(int j=0;j<=m;j++){ if(i==1)a[1][j]=1; else if(j==0)a[i][0]=0; else if(j==1)a[i][1]=1; if(i<j)a[i][j]=0; else if(i==j)a[i][j]=1; else a[i][j]=a[i-1][j-1]+j*a[i-1][j]; } } printf("%lld",a[n][m]); return 0;} 例题2-危险物质有 $n$ 个存放危险物质的坑,坑排列在一条直线,要求有危险物质的两个坑之间至少要有 $m$ 个空坑。对于给定的 $n$ 和 $m$ ,求安全存放危险物质的方案总数 $\\text{mod}\\ 5000011$ 之后的结果。($1\\le m\\le n\\le 10^5$) 这也是有两个数字会影响结果,但是你就算是看看数据范围也可以发现这道题只需要一个一维数组就够了。 首先我们需要 $n$ 个排成一列的坑: 然后我们看向最后一个坑,它有填与不填两种情况。 如果不填的话,那么前面的坑就可以为所欲为,只要放置方法合理就行了。 但是如果填呢? 那么这个坑前面的 $m$ 个坑就不能填了,只有往前到第 $n-m-1$ 个坑的时候才可以随便填。 所以递推式为:$a_i=a_{i-1}+a_{i-m-1}$ (批注:前面那部分表示此坑不填,后面那部分表示此坑要填) 接下来我们要注意一下递推的另一个条件,也就是最简单的情况。 现在 $m$ 不知道,那么我们怎么得出最简单的情况呢? 可以发现,如果 $n$ 小于 $m+2$ 的话,那么最多只能填一个或者不填,也就是 $n+1$ 种情况,所以我们可以用循环来解决这个问题。 AC 代码如下: 123456789101112#include<cstdio>int a[100005]={1,2};int main(){ int m,n; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ if(i<m+2)a[i]=i+1; else a[i]=(a[i-1]+a[i-m-1])%5000011; } printf("%d",a[n]); return 0;} 例题3-核电站一个核电站有 $N$ 个放核物质的坑,坑排列在一条直线上。如果连续 $M$ 个坑中放入核物质,则会发生爆炸,于是,在某些坑中可能不放核物质。对于给定的 $N$ 和 $M$,求不发生爆炸的放置核物质的方案总数。($2≤N≤50,2≤M≤5$) 方法类似上面两道题的融合,一共有三种特殊的情况: 如果坑的个数小于不能连续的个数,直接随便放,就是$2^n$; 如果坑的个数等于不能连续的个数,就只有全放一种不行,为$2^n-1$; 如果坑的个数大于不能连续的个数,那就把它分成两个部分看,放和不放。不放就随便,放的话需要让前面连续 $m+1-1$ 个坑不能连续放(因为前面的有最后一个坑放的可能性所以要 $+1$)。 最后算出来为: $a_i=2\\times a_{i-1}$ $a_i=2\\times a_{i-1}-1$ $2\\times a_{i-1}-a_{i-m-1}$ 往循环里一套 AC 代码就出来了。 12345678910111213#include<cstdio>long long a[55]={1};int main(){ int n,m; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ if(i>m)a[i]=2*a[i-1]-a[i-m-1]; else if(i==m)a[i]=2*a[i-1]-1; else a[i]=2*a[i-1]; } printf("%lld",a[n]); return 0;} 递归的优化——记忆化递归你们还记得那个 $80$ 分的斐波那契数列吗?要是不记得了可以去前面再看看。为什么它是 $80$ 分呢? 因为它超时了……(废话) 那么怎么解决这个问题呢? 用递推就可以了( •̀ ω •́ )y 说得好像有道理……不过这并不妨碍我们介绍另一种方法,就是记忆化递归。(●ˇ∀ˇ●) 记忆化递归是一种典型的以空间换取时间的优化。我们用一个数组储存每一种情况的值,如果我们以后再调用到这个值我们就直接调用,不需要计算了。 除了斐波那契数列,我们还有另外一个记忆化的题目: 例题-递归函数对于一个递归函数$w(a, b, c)$。 如果$a <= 0\\ or\\ b <= 0\\ or\\ c <= 0$就返回值 $1$。 如果$a > 20\\ or\\ b > 20\\ or\\ c > 20$就返回 $W(20,20,20)$。 如果 $a < b$ 并且 $b < c$ 就返回 $w(a,b,c-1)+w(a,b-1,c-1)-w(a,b-1,c)$, 其它别的情况就返回 $w(a-1,b,c)+w(a-1,b-1,c)+w(a-1,b,c-1)-w(a-1,b-1,c-1)$。 这是个简单的递归函数,但实现起来可能会有些问题。 $|a|, |b|, |c| < 30$ 这道题很简单,照着他的写就可以了,主要还是记忆化。 我们定义一个三维数组来存储解(ll 指long long): 1ll s[35][35][35]; 然后每次递归之前都判断一下这个值是不是已经算过了,如果算过了就直接返回: 1if(s[x][y][z]!=0)return s[x][y][z]; 如果没有算过,那么我们算完了之后要把这个值存到数组里(一个例子): 1if(x<y&&y<z)return s[x][y][z]=w(x,y,z-1)+w(x,y-1,z-1)-w(x,y-1,z); 这样我们的代码就轻轻松松的 AC 了: 123456789101112131415161718#include<bits/stdc++.h>#define ll long longusing namespace std;ll a,b,c,s[35][35][35];ll w(ll x,ll y,ll z){ if(x<=0||y<=0||z<=0)return 1; if(x>20||y>20||z>20)return w(20,20,20); if(s[x][y][z]!=0)return s[x][y][z]; if(x<y&&y<z)return s[x][y][z]=w(x,y,z-1)+w(x,y-1,z-1)-w(x,y-1,z); return s[x][y][z]=w(x-1,y,z)+w(x-1,y-1,z)+w(x-1,y,z-1)-w(x-1,y-1,z-1);}int main(){ while(scanf("%lld %lld %lld",&a,&b,&c)){ if(a==-1&&b==-1&&c==-1)return 0; printf("w(%lld,%lld,%lld)=%lld\\n",a,b,c,w(a,b,c)); } return 0;} 结语递推递归是很基础的一个算法,也是我们正式学的第一种算法,以后很多的算法都需要用到它们(尤其是递归),所以这是很重要的一节课。递推递归的题目难起来也会让人很头疼,甚至很多递归都涉及到了以后学的搜索,而递推涉及到了 dp。 不管怎么样,各种算法之间都是有很紧密的联系的。递推与递归之间也有很紧密的联系,甚至它们之间的基本模型也是可以通用的,所以我们就把它们放在一起一起学了。","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"递推,递归","slug":"递推,递归","permalink":"https://blog.liynw.top/tags/%E9%80%92%E6%8E%A8%EF%BC%8C%E9%80%92%E5%BD%92/"}]},{"title":"网站渲染测试","slug":"「Website」网站渲染测试","date":"2021-08-17T09:25:57.000Z","updated":"2023-06-28T09:52:39.423Z","comments":true,"path":"posts/c9c67023/","link":"","permalink":"https://blog.liynw.top/posts/c9c67023/","excerpt":"","text":"网站渲染测试(H1)Markdown 部分(H2)标题测试(H3)四级标题(H4)五级标题(H5)六级标题(H6) 文本测试普通文本 加粗文本 斜体文本 加粗斜体文本 删除线文本(这个功能经常被拿来整活) *使用反斜线开头的*被当做是普通的字符* 代码块测试1234567891011121314151617181920::-webkit-scrollbar { width: 8px; height: 8px}::-webkit-scrollbar-track { background-color: rgba(73, 177, 245, .2); border-radius: 2em}::-webkit-scrollbar-thumb { background-color: #49b1f5; background-image: -webkit-linear-gradient(45deg, hsla(0, 0%, 100%, .4) 25%, transparent 0, transparent 50%, hsla(0, 0%, 100%, .4) 0, hsla(0, 0%, 100%, .4) 75%, transparent 0, transparent); border-radius: 2em}::-webkit-scrollbar-corner { background-color: transparent}::-moz-selection { color: #fff; background-color: #49b1f5} 行内代码:xsc062(可爱的妹纸 qwq) AK IOI膜拜巨佬!!1(夹带私货了属于是) 引用测试 Markdown 标记区块引用的方法是在行的最前面加 >。 也可以只在整个段落的第一行最前面加上 >。 区块引用内部可以嵌套,只要根据层次加上不同数量的 > 即可。 比如说这里可以再加一层。 我是内部嵌套区块,我可以使用其他 Markdown 语法哦。 我是引用区块内使用四级标题语法。(对)(你甚至可以使用 $\\LaTeX$) 12345//在引用区块内可以加入代码块import java.net.URL;import java.util.Arrays;import java.util.Date;import java.util.Set; 注意嵌套完了之后需要留一层空的,否则不会退出嵌套。 列表测试1.无序列表 Red Green Blue RED GREEN BLUE 2.有序列表 Red Green Blue RED GREEN BLUE 链接 & 图片测试Welcome to Liynw’s Blog! This is XSC062’s Blog. 表格测试 Left-Aligned Center Aligned Right Aligned 1 QWQ QWQ 2 (●’◡’●) QAQ 3 o(* ̄▽ ̄*)ブ QWQ 注脚测试You can create footnotes like thisfootnote. footnote. Here is the text of the footnote. ↩ 分割线测试 LaTeX 测试Hexo 博客文章的渲染机制和 CSDN 之类的不同,这些平台在渲染 $\\LaTeX$ 时会和 markdown 一起渲染,但是 Hexo 不同,它会先把 markdown 渲染了才会渲染 $\\LaTeX$,所以会导致 $,*,\\,_ 这四个符号在 markdown 和 $\\LaTeX$ 都有特殊意义的字符渲染出错。为了让它正常渲染,一般的解决办法有两种:在需要 $\\LaTeX$ 渲染的这些符号前加上反斜杠,或者是修改渲染 markdown 插件的源代码。但是第二个方法不太靠谱,毕竟,你可以做到使用 markdown 不用下划线来渲染粗体什么的,但是你能不用星号吗?所以,第一个办法虽然麻烦,但是也没办法,只能这样。 另外,这篇文章的 $\\LaTeX$ 貌似有点鬼畜。这好像是我用了 CSS 调了字体和大小之后出现的问题,我也不知道怎么解决。 $a+b=c$ $$a+b=c$$ $alpha$ $\\alpha$ $pi$ $\\pi$ $\\Gamma$ $a\\equiv b$ $$\\equiv$$ $$a=b$$ $$a\\notin b \\ c\\in d $$ $\\int$ $\\iint$ $\\iiint$ $$C_{1} \\qquad \\int_{x} $$ $$\\Sigma_{C_{i}}\\quad \\Psi$$ $$a_{i} \\ b_{i}$$ $$C_1+C_2$$ $$C_ {m,n} $$ $${C_{i^2}}^2 = a^2+b^{\\int_{x}}$$ $$e^{x^2} \\neq e^{x^2}$$ $${sin\\alpha}^2+{cos\\beta}^2 \\equiv 1$$ $$\\sqrt{x+y}= \\sqrt{\\Sigma_{i=1}^{n} x}$$ $\\sqrt{a}$ $$a=b\\cdot c \\ a=b\\dot c$$ $$lim_{x \\rightarrow 0} \\frac {\\sin x}{x}=1$$ $$\\overline{a} \\quad \\underline{m+n}$$ $$\\underbrace{\\int_{a_1}^{a_2}f_1(x)dx+\\int_{a_2}^{a_3}f_2(x)dx+\\cdots+\\int_{a_{n-1}}^{a_n}f_n(x)dx}_{\\iint_{\\Sigma_{i=1}^{n} g(b_i) dx}}$$ $$y’=3\\widetilde a$$ $$\\overrightarrow{AC}=\\overrightarrow{AB}+\\overrightarrow{BC}$$ $${n\\choose m} \\qquad {x\\atop y+2} \\quad ({x\\atop y+2})$$ $$C_({x\\atop y+2})$$ $${\\int_{0}^{\\frac{\\pi}{2}}}$$ $$\\sum_{i=1}^{n}$$ $$\\prod_ \\epsilon$$ $$1+\\left(\\frac {1}{1-x^2}\\right)^3 \\qquad 1+(\\frac {1}{1-x^2})^2$$ $$\\left(\\underbrace{\\int_{a_1}^{a_2}f_1(x)dx+\\int_{a_2}^{a_3}f_2(x)dx+\\cdots+\\int_{a_{n-1}}^{a_n}f_n(x)dx}_{\\iint_{\\Sigma_{i=1}^{n} g(b_i) dx}}\\right)= \\Psi $$ $$a=b$$ $$\\begin{Bmatrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\end{Bmatrix} \\tag{1} $$ $$\\mathbf{X} = \\left( \\begin{array}{ccc} x_{11} & x_{12} & \\ldots \\\\ x_{21} & x_{22} & \\ldots \\\\ \\vdots & \\vdots & \\ddots \\end{array} \\right) $$ $$\\mathbf{X} = \\left( \\begin{array}{ccc} x_{11} & x_{12} & \\ldots \\\\ x_{21} & x_{22} & \\ldots \\\\ \\vdots & \\vdots & \\ddots \\end{array} \\right) \\tag{2}$$ $\\mathbf{X} = \\left( \\begin{array}{ccc} x_{11} & x_{12} & \\ldots \\\\ x_{21} & x_{22} & \\ldots \\\\ \\vdots & \\vdots & \\ddots \\end{array} \\right) \\tag{1}$ $$ \\left[ \\begin{matrix} 1 & 2 & \\cdots & 4 \\\\ 7 & 6 & \\cdots & 5 \\\\ \\vdots & \\vdots & \\ddots & \\vdots \\\\ 8 & 9 & \\cdots & 0 \\\\ \\end{matrix} \\right] $$ $$ \\left[ \\begin{array}{cc|c} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\end{array} \\right] \\tag{7} $$ $$\\sum_{i=1}^n a_i=0$$ $$f(x)=x^{x^x}$$ $$1=1\\\\2=2$$ $$1=1$$ $$\\sqrt[3]{x}$$ $$f(x_1,x_x,\\ldots,x_n) = x_1^2 + x_2^2 + \\cdots + x_n^2 $$ $$[f(x,y,z) = 3y^2 z \\left( 3 + \\frac{7x+5}{1 + y^2} \\right).]$$ $$\\left. \\frac{du}{dx} \\right|_{x=0}.$$ $$\\begin{eqnarray*}\\cos 2\\theta & = & \\cos^2 \\theta - \\sin^2 \\theta \\\\ & = & 2 \\cos^2 \\theta - 1.\\end{eqnarray*}$$ Butterfly 自带外挂标签Note (Bootstrap Callout)默认 提示块标签 default 提示块标签 primary 提示块标签 success 提示块标签 info 提示块标签 warning 提示块标签 danger 提示块标签 默认 提示块标签 default 提示块标签 primary 提示块标签 success 提示块标签 info 提示块标签 warning 提示块标签 danger 提示块标签 默认 提示块标签 default 提示块标签 primary 提示块标签 success 提示块标签 info 提示块标签 warning 提示块标签 danger 提示块标签 默认 提示块标签 default 提示块标签 primary 提示块标签 success 提示块标签 info 提示块标签 warning 提示块标签 danger 提示块标签 默认 提示块标签 default 提示块标签 primary 提示块标签 success 提示块标签 info 提示块标签 warning 提示块标签 danger 提示块标签 你是刷 Visa 还是 UnionPay 2021年快到了…. 小心开车 安全至上 这是三片呢?还是四片? 你是刷 Visa 还是 UnionPay 剪刀石头布 前端最讨厌的浏览器 你是刷 Visa 还是 UnionPay 2021年快到了…. 小心开车 安全至上 这是三片呢?还是四片? 你是刷 Visa 还是 UnionPay 剪刀石头布 前端最讨厌的浏览器 你是刷 Visa 还是 UnionPay 2021年快到了…. 小心开车 安全至上 这是三片呢?还是四片? 你是刷 Visa 还是 UnionPay 剪刀石头布 前端最讨厌的浏览器 你是刷 Visa 还是 UnionPay 2021年快到了…. 小心开车 安全至上 这是三片呢?还是四片? 你是刷 Visa 还是 UnionPay 剪刀石头布 前端最讨厌的浏览器 你是刷 Visa 还是 UnionPay 2021年快到了…. 小心开车 安全至上 这是三片呢?还是四片? 你是刷 Visa 还是 UnionPay 剪刀石头布 前端最讨厌的浏览器 tag-hide哪个英文字母最酷? 查看答案 因为西装裤(C装酷) 门里站着一个人? Click 闪 查看答案查看答案 怎么可能有呢 qwq Butterfly安装方法在你的博客根目录里 git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/Butterfly 如果想要安装比较新的dev分支,可以 git clone -b dev https://github.com/jerryc127/hexo-theme-butterfly.git themes/Butterfly mermaid pie title Key elements in Product X "Calcium" : 42.96 "Potassium" : 50.05 "Magnesium" : 10.01 "Iron" : 5 Tabstest1 1test1 2test1 3This is Tab 1.This is Tab 2.This is Tab 3. test2 1test2 2test2 3This is Tab 1.This is Tab 2.This is Tab 3. test3 1test3 2test3 3This is Tab 1.This is Tab 2.This is Tab 3. 第一个Tab炸弹tab名字为第一个Tab只有图标 没有Tab名字名字+icon label臣亮言:先帝 创业未半,而中道崩殂 。今天下三分,益州疲敝 ,此诚危急存亡之秋 也!然侍衞之臣,不懈于内;忠志之士 ,忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气;不宜妄自菲薄,引喻失义,以塞忠谏之路也。宫中、府中,俱为一体;陟罚臧否,不宜异同。若有作奸 、犯科 ,及为忠善者,宜付有司,论其刑赏,以昭陛下平明之治;不宜偏私,使内外异法也。 timeline2022 01-02 这是测试页面 2022 01-02 这是测试页面 2022 01-02 这是测试页面 2022 01-02 这是测试页面 2022 01-02 这是测试页面 2022 01-02 这是测试页面 2022 01-02 这是测试页面 Tag Plugins Plus部分转载于:https://akilar.top/posts/615e2dec/ 行内文本样式 text标签语法样式预览示例源码123456{% u 文本内容 %}{% emp 文本内容 %}{% wavy 文本内容 %}{% del 文本内容 %}{% kbd 文本内容 %}{% psw 文本内容 %} 带 下划线 的文本 带 着重号 的文本 带 波浪线 的文本 带 删除线 的文本 键盘样式的文本 command + D 密码样式的文本:这里没有验证码 1234561. 带 {% u 下划线 %} 的文本2. 带 {% emp 着重号 %} 的文本3. 带 {% wavy 波浪线 %} 的文本4. 带 {% del 删除线 %} 的文本5. 键盘样式的文本 {% kbd command %} + {% kbd D %}6. 密码样式的文本:{% psw 这里没有验证码 %} 行内文本 span标签语法配置参数样式预览示例源码1{% span 样式参数(参数以空格划分), 文本内容 %} 字体: logo, code 颜色: red,yellow,green,cyan,blue,gray 大小: small, h4, h3, h2, h1, large, huge, ultra 对齐方向: left, center, right 彩色文字在一段话中方便插入各种颜色的标签,包括:红色、黄色、绿色、青色、蓝色、灰色。 超大号文字文档「开始」页面中的标题部分就是超大号文字。VolantisA Wonderful Theme for Hexo 123456- 彩色文字在一段话中方便插入各种颜色的标签,包括:{% span red, 红色 %}、{% span yellow, 黄色 %}、{% span green, 绿色 %}、{% span cyan, 青色 %}、{% span blue, 蓝色 %}、{% span gray, 灰色 %}。- 超大号文字文档「开始」页面中的标题部分就是超大号文字。{% span center logo large, Volantis %}{% span center small, A Wonderful Theme for Hexo %} 段落文本 p标签语法配置参数样式预览示例源码1{% p 样式参数(参数以空格划分), 文本内容 %} 字体: logo, code 颜色: red,yellow,green,cyan,blue,gray 大小: small, h4, h3, h2, h1, large, huge, ultra 对齐方向: left, center, right 彩色文字在一段话中方便插入各种颜色的标签,包括:红色、黄色、绿色、青色、蓝色、灰色。 超大号文字文档「开始」页面中的标题部分就是超大号文字。Volantis A Wonderful Theme for Hexo 123456- 彩色文字在一段话中方便插入各种颜色的标签,包括:{% p red, 红色 %}、{% p yellow, 黄色 %}、{% p green, 绿色 %}、{% p cyan, 青色 %}、{% p blue, 蓝色 %}、{% p gray, 灰色 %}。- 超大号文字文档「开始」页面中的标题部分就是超大号文字。{% p center logo large, Volantis %}{% p center small, A Wonderful Theme for Hexo %} 上标标签 tip主要样式参考自小康的butterfly渐变背景标签,自己写了个tip.js来渲染标签,精简了一下代码。 标签语法配置参数样式预览示例源码1{% tip [参数,可选] %}文本内容{% endtip %} 样式: success,error,warning,bolt,ban,home,sync,cogs,key,bell 自定义图标: 支持fontawesome。 default info success error warning bolt ban home sync cogs key bell 自定义font awesome图标 12345678910111213{% tip %}default{% endtip %}{% tip info %}info{% endtip %}{% tip success %}success{% endtip %}{% tip error %}error{% endtip %}{% tip warning %}warning{% endtip %}{% tip bolt %}bolt{% endtip %}{% tip ban %}ban{% endtip %}{% tip home %}home{% endtip %}{% tip sync %}sync{% endtip %}{% tip cogs %}cogs{% endtip %}{% tip key %}key{% endtip %}{% tip bell %}bell{% endtip %}{% tip fa-atom %}自定义font awesome图标{% endtip %} 动态标签 anima动态标签的实质是引用了font-awesome-animation的css样式,不一定局限于tip标签,也可以是其他标签。只不过这里tip.js是我自己写的,所以我清楚它会怎么被渲染成html,才用的这个写法。可以熟读文档,使用html语言来编写其他标签类型。 标签语法配置参数样式预览示例源码1{% tip [参数,可选] %}文本内容{% endtip %}更多详情请参看font-awesome-animation文档 将所需的CSS类添加到图标(或DOM中的任何元素)。 对于父级悬停样式,需要给目标元素添加指定CSS类,同时还要给目标元素的父级元素添加CSS类faa-parent animated-hover。(详情见示例及示例源码)You can regulate the speed of the animation by adding the CSS class or . faa-fastfaa-slow 可以通过给目标元素添加CSS类faa-fast或faa-slow来控制动画快慢。 On DOM load当页面加载时显示动画 On hover当鼠标悬停时显示动画 On parent hover当鼠标悬停在父级元素时显示动画 faa-wrench animated faa-wrench animated-hover faa-wrench faa-ring animated faa-ring animated-hover faa-ring faa-horizontal animated faa-horizontal animated-hover faa-horizontal faa-vertical animated faa-vertical animated-hover faa-vertical faa-flash animated faa-flash animated-hover faa-flash faa-bounce animated faa-bounce animated-hover faa-bounce faa-spin animated faa-spin animated-hover faa-spin faa-tada animated faa-tada animated-hover faa-tada faa-pulse animated faa-pulse animated-hover faa-pulse faa-shake animated faa-shake animated-hover faa-shake faa-tada animated faa-tada animated-hover faa-tada faa-passing animated faa-passing animated-hover faa-passing faa-passing-reverse animated faa-passing-reverse animated-hover faa-passing-reverse faa-burst animated faa-burst animated-hover faa-burst faa-falling animated faa-falling animated-hover faa-falling faa-rising animated faa-rising animated-hover faa-rising On DOM load(当页面加载时显示动画) warning ban 调整动画速度。 warning ban On hover(当鼠标悬停时显示动画) warning ban On parent hover(当鼠标悬停在父级元素时显示动画) warning ban On DOM load(当页面加载时显示动画) 12{% tip warning faa-horizontal animated %}warning{% endtip %}{% tip ban faa-flash animated %}ban{% endtip %} 调整动画速度 12{% tip warning faa-horizontal animated faa-fast %}warning{% endtip %}{% tip ban faa-flash animated faa-slow %}ban{% endtip %} On hover(当鼠标悬停时显示动画) 12{% tip warning faa-horizontal animated-hover %}warning{% endtip %}{% tip ban faa-flash animated-hover %}ban{% endtip %} On parent hover(当鼠标悬停在父级元素时显示动画) 12{% tip warning faa-parent animated-hover %}<p class="faa-horizontal">warning</p>{% endtip %}{% tip ban faa-parent animated-hover %}<p class="faa-flash">ban</p>{% endtip %} 复选列表 checkbox标签语法配置参数样式预览示例源码1{% checkbox 样式参数(可选), 文本(支持简单md) %} 样式: plus, minus, times 颜色: red,yellow,green,cyan,blue,gray 选中状态: checked 纯文本测试 支持简单的 markdown 语法 支持自定义颜色 绿色 + 默认选中 黄色 + 默认选中 青色 + 默认选中 蓝色 + 默认选中 增加 减少 叉 12345678910{% checkbox 纯文本测试 %}{% checkbox checked, 支持简单的 [markdown](https://guides.github.com/features/mastering-markdown/) 语法 %}{% checkbox red, 支持自定义颜色 %}{% checkbox green checked, 绿色 + 默认选中 %}{% checkbox yellow checked, 黄色 + 默认选中 %}{% checkbox cyan checked, 青色 + 默认选中 %}{% checkbox blue checked, 蓝色 + 默认选中 %}{% checkbox plus green checked, 增加 %}{% checkbox minus yellow checked, 减少 %}{% checkbox times red checked, 叉 %} 单选列表 radio标签语法配置参数样式预览示例源码1{% radio 样式参数(可选), 文本(支持简单md) %} 颜色: red,yellow,green,cyan,blue,gray 选中状态: checked 纯文本测试 支持简单的 markdown 语法 支持自定义颜色 绿色 黄色 青色 蓝色 1234567{% radio 纯文本测试 %}{% radio checked, 支持简单的 [markdown](https://guides.github.com/features/mastering-markdown/) 语法 %}{% radio red, 支持自定义颜色 %}{% radio green, 绿色 %}{% radio yellow, 黄色 %}{% radio cyan, 青色 %}{% radio blue, 蓝色 %} 链接卡片 link标签语法样式预览示例源码1{% link 标题, 链接, 图片链接(可选) %}Liynw 个人主页https://liynw.top1{% link Liynw 个人主页, https://liynw.top, https://cdn.jsdelivr.net/npm/[email protected]/img/avatar.webp %} 按钮 btnsVolantis的按钮使用的是btn和btns标签。btns和butterfly的button不冲突,但是btn会被强制渲染,导致部分参数失效,而且btn的效果还是butterfly的button更好看些。所以就只适配了btns。 标签语法参数配置样式预览示例源码1234{% btns 样式参数 %}{% cell 标题, 链接, 图片或者图标 %}{% cell 标题, 链接, 图片或者图标 %}{% endbtns %} 圆角样式:rounded, circle 增加文字样式:可以在容器内增加 <b>标题</b>和<p>描述文字</p> 布局方式: 默认为自动宽度,适合视野内只有一两个的情况。 参数 含义 wide 宽一点的按钮 fill 填充布局,自动铺满至少一行,多了会换行 center 居中,按钮之间是固定间距 around 居中分散 grid2 等宽最多2列,屏幕变窄会适当减少列数 grid3 等宽最多3列,屏幕变窄会适当减少列数 grid4 等宽最多4列,屏幕变窄会适当减少列数 grid5 等宽最多5列,屏幕变窄会适当减少列数 如果需要显示类似「团队成员」之类的一组含有头像的链接: xaoxuu xaoxuu xaoxuu xaoxuu xaoxuu 或者含有图标的按钮: 下载源码 查看文档 圆形图标 + 标题 + 描述 + 图片 + 网格5列 + 居中 心率管家 专业版 心率管家 免费版 如果需要显示类似「团队成员」之类的一组含有头像的链接:1234567{% btns circle grid5 %}{% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %}{% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %}{% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %}{% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %}{% cell xaoxuu, https://xaoxuu.com, https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png %}{% endbtns %} 或者含有图标的按钮:1234{% btns rounded grid5 %}{% cell 下载源码, /, fas fa-download %}{% cell 查看文档, /, fas fa-book-open %}{% endbtns %} 圆形图标 + 标题 + 描述 + 图片 + 网格5列 + 居中1234567891011121314{% btns circle center grid5 %}<a href='https://apps.apple.com/cn/app/heart-mate-pro-hrm-utility/id1463348922?ls=1'> <i class='fab fa-apple'></i> <b>心率管家</b> {% p red, 专业版 %} <img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/qrcode/heartmate_pro.png'></a><a href='https://apps.apple.com/cn/app/heart-mate-lite-hrm-utility/id1475747930?ls=1'> <i class='fab fa-apple'></i> <b>心率管家</b> {% p green, 免费版 %} <img src='https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/qrcode/heartmate_lite.png'></a>{% endbtns %} github卡片 ghcardghcard使用了github-readme-stats的API,支持直接使用markdown语法来写。 标签语法配置参数样式预览示例源码12{% ghcard 用户名, 其它参数(可选) %}{% ghcard 用户名/仓库, 其它参数(可选) %}更多参数可以参考:使用,分割各个参数。写法为:参数名=参数值以下只写几个常用参数值。 参数名 取值 释义 hide stars,commits,prs,issues,contribs 隐藏指定统计 count_private true 将私人项目贡献添加到总提交计数中 show_icons true 显示图标 theme 请查阅Available Themes 主题 用户信息卡片 仓库信息卡片 用户信息卡片 12345| {% ghcard xaoxuu %} | {% ghcard xaoxuu, theme=vue %} || -- | -- || {% ghcard xaoxuu, theme=buefy %} | {% ghcard xaoxuu, theme=solarized-light %} || {% ghcard xaoxuu, theme=onedark %} | {% ghcard xaoxuu, theme=solarized-dark %} || {% ghcard xaoxuu, theme=algolia %} | {% ghcard xaoxuu, theme=calm %} | 仓库信息卡片 12345| {% ghcard volantis-x/hexo-theme-volantis %} | {% ghcard volantis-x/hexo-theme-volantis, theme=vue %} || -- | -- || {% ghcard volantis-x/hexo-theme-volantis, theme=buefy %} | {% ghcard volantis-x/hexo-theme-volantis, theme=solarized-light %} || {% ghcard volantis-x/hexo-theme-volantis, theme=onedark %} | {% ghcard volantis-x/hexo-theme-volantis, theme=solarized-dark %} || {% ghcard volantis-x/hexo-theme-volantis, theme=algolia %} | {% ghcard volantis-x/hexo-theme-volantis, theme=calm %} | github徽标 ghbdage标签语法配置参数样式预览示例源码1{% bdage [right],[left],[logo]||[color],[link],[title]||[option] %} left:徽标左边的信息,必选参数。 right: 徽标右边的信息,必选参数, logo:徽标图标,图标名称详见simpleicons,可选参数。 color:徽标右边的颜色,可选参数。 link:指向的链接,可选参数。 title:徽标的额外信息,可选参数。主要用于优化SEO,但object标签不会像a标签一样在鼠标悬停显示title信息。 option:自定义参数,支持shields.io的全部API参数支持,具体参数可以参看上文中的拓展写法示例。形式为name1=value2&name2=value2。 本外挂标签的参数分为三组,用||分割。 基本参数 信息参数 拓展参数 本外挂标签的参数分为三组,用||分割。 基本参数,定义徽标左右文字和图标12{% bdage Theme,Butterfly %}{% bdage Frame,Hexo,hexo %} 信息参数,定义徽标右侧内容背景色,指向链接123{% bdage CDN,JsDelivr,jsDelivr||abcdef,https://metroui.org.ua/index.html,本站使用JsDelivr为静态资源提供CDN加速 %}//如果是跨顺序省略可选参数,仍然需要写个逗号,用作分割{% bdage Source,GitHub,GitHub||,https://github.com/ %} 拓展参数,支持shields的API的全部参数内容123{% bdage Hosted,Vercel,Vercel||brightgreen,https://vercel.com/,本站采用双线部署,默认线路托管于Vercel||style=social&logoWidth=20 %}//如果是跨顺序省略可选参数组,仍然需要写双竖线||用作分割{% bdage Hosted,Vercel,Vercel||||style=social&logoWidth=20&logoColor=violet %} 网站卡片 sites标签语法样式预览示例源码1234{% sitegroup %}{% site 标题, url=链接, screenshot=截图链接, avatar=头像链接(可选), description=描述(可选) %}{% site 标题, url=链接, screenshot=截图链接, avatar=头像链接(可选), description=描述(可选) %}{% endsitegroup %}xaoxuu简约风格 inkss这是一段关于这个网站的描述文字 MHuiG这是一段关于这个网站的描述文字 Colsrch这是一段关于这个网站的描述文字 Linhk1606这是一段关于这个网站的描述文字1234567{% sitegroup %}{% site xaoxuu, url=https://xaoxuu.com, screenshot=https://i.loli.net/2020/08/21/VuSwWZ1xAeUHEBC.jpg, avatar=https://cdn.jsdelivr.net/gh/xaoxuu/cdn-assets/avatar/avatar.png, description=简约风格 %}{% site inkss, url=https://inkss.cn, screenshot=https://i.loli.net/2020/08/21/Vzbu3i8fXs6Nh5Y.jpg, avatar=https://cdn.jsdelivr.net/gh/inkss/common@master/static/web/avatar.jpg, description=这是一段关于这个网站的描述文字 %}{% site MHuiG, url=https://blog.mhuig.top, screenshot=https://i.loli.net/2020/08/22/d24zpPlhLYWX6D1.png, avatar=https://cdn.jsdelivr.net/gh/MHuiG/imgbed@master/data/p.png, description=这是一段关于这个网站的描述文字 %}{% site Colsrch, url=https://colsrch.top, screenshot=https://i.loli.net/2020/08/22/dFRWXm52OVu8qfK.png, avatar=https://cdn.jsdelivr.net/gh/Colsrch/images/Colsrch/avatar.jpg, description=这是一段关于这个网站的描述文字 %}{% site Linhk1606, url=https://linhk1606.github.io, screenshot=https://i.loli.net/2020/08/21/3PmGLCKicnfow1x.png, avatar=https://i.loli.net/2020/02/09/PN7I5RJfFtA93r2.png, description=这是一段关于这个网站的描述文字 %}{% endsitegroup %} 行内图片 inlineimage标签语法参数配置样式预览示例源码1{% inlineimage 图片链接, height=高度(可选) %} 高度:height=20px 这是 一段话。 这又是 一段话。123这是 {% inlineimage https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/aru-l/0000.gif %} 一段话。这又是 {% inlineimage https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/aru-l/5150.gif, height=40px %} 一段话。 单张图片 image标签语法参数配置样式预览示例源码1{% image 链接, width=宽度(可选), height=高度(可选), alt=描述(可选), bg=占位颜色(可选) %} 图片宽度高度:width=300px, height=32px 图片描述:alt=图片描述(butterfly需要在主题配置文件中开启图片描述) 占位背景色:bg=#f2f2f2 添加描述: 每天下课回宿舍的路,没有什么故事。 指定宽度: 指定宽度并添加描述: 每天下课回宿舍的路,没有什么故事。 设置占位背景色: 优化不同宽度浏览的观感 添加描述: 1{% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, alt=每天下课回宿舍的路,没有什么故事。 %} 指定宽度:1{% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, width=400px %} 指定宽度并添加描述:1{% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, width=400px, alt=每天下课回宿舍的路,没有什么故事。 %} 设置占位背景色:1{% image https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg, width=400px, bg=#1D0C04, alt=优化不同宽度浏览的观感 %} 音频 audio标签语法样式预览示例源码1{% audio 音频链接 %}Your browser does not support the audio tag.1{% audio https://npm.elemecdn.com/[email protected]/assets/Tiny_Stars.mp3 %} 视频 video标签语法参数配置样式预览示例源码1{% video 视频链接 %} 对其方向:left, center, right 列数:逗号后面直接写列数,支持 1 ~ 4 列。 100%宽度 Your browser does not support the video tag. 50%宽度 Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. 25%宽度 Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. Your browser does not support the video tag. 100%宽度 1{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %} 50%宽度 123456{% videos, 2 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% endvideos %} 25%宽度 12345678910{% videos, 4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% video https://vdse.bdstatic.com//192d9a98d782d9c74c96f09db9378d93.mp4 %}{% endvideos %} 折叠框 foldingButterfly虽然也有内置折叠框hideToggle标签,但是Volantis的folding折叠框更好看一些。 标签语法配置参数样式预览示例源码123{% folding 参数(可选), 标题 %}![](https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper/abstract/41F215B9-261F-48B4-80B5-4E86E165259E.jpeg){% endfolding %} 颜色:blue, cyan, green, yellow, red 状态:状态填写 open 代表默认打开。 查看图片测试 查看默认打开的折叠框 这是一个默认打开的折叠框。 查看代码测试 假装这里有代码块(代码块没法嵌套代码块) 查看列表测试 qwqqaq 查看嵌套测试 查看嵌套测试2 查看嵌套测试3 hahaha 123456789101112131415161718192021222324252627282930313233343536{% folding 查看图片测试 %}![](https://pic.imgdb.cn/item/618fb67c2ab3f51d9168295e.png){% endfolding %}{% folding cyan open, 查看默认打开的折叠框 %}这是一个默认打开的折叠框。{% endfolding %}{% folding green, 查看代码测试 %}假装这里有代码块(代码块没法嵌套代码块){% endfolding %}{% folding yellow, 查看列表测试 %}- qwq- qaq{% endfolding %}{% folding red, 查看嵌套测试 %}{% folding blue, 查看嵌套测试2 %}{% folding 查看嵌套测试3 %}hahaha <span><img src='https://pic.imgdb.cn/item/618fb67c2ab3f51d9168295e.png' style='height:204px'></span>{% endfolding %}{% endfolding %}{% endfolding %} 诗词标签 poem标签语法参数配置样式预览示例源码123{% poem [title],[author] %}诗词内容{% endpoem %} title:诗词标题 author:作者,可以不写 水调歌头苏轼明月几时有?把酒问青天。不知天上宫阙,今夕是何年?我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间?转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。 123456789101112{% poem 水调歌头,苏轼 %}丙辰中秋,欢饮达旦,大醉,作此篇,兼怀子由。明月几时有?把酒问青天。不知天上宫阙,今夕是何年?我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间?转朱阁,低绮户,照无眠。不应有恨,何事长向别时圆?人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共婵娟。{% endpoem %} 阿里图标 icon标签语法参数释义样式预览示例源码1{% icon [icon-xxxx],[font-size] %} icon-xxxx:表示图标font-class,可以在自己的阿里矢量图标库项目的font-class引用方案内查询并复制。 font-size:表示图标大小,直接填写数字即可,单位为em。图标大小默认值为1em。 1234567891011121314151617181920212223{% icon icon-rat_zi %}{% icon icon-rat,2 %}{% icon icon-ox_chou,3 %}{% icon icon-ox,4 %}{% icon icon-tiger_yin,5 %}{% icon icon-tiger,6 %}{% icon icon-rabbit_mao,1 %}{% icon icon-rabbit,2 %}{% icon icon-dragon_chen,3 %}{% icon icon-dragon,4 %}{% icon icon-snake_si,5 %}{% icon icon-snake,6 %}{% icon icon-horse_wu %}{% icon icon-horse,2 %}{% icon icon-goat_wei,3 %}{% icon icon-goat,4 %}{% icon icon-monkey_shen,5 %}{% icon icon-monkey,6 %}{% icon icon-rooster_you %}{% icon icon-rooster,2 %}{% icon icon-dog_xu,3 %}{% icon icon-dog,4 %}{% icon icon-boar_hai,5 %}{% icon icon-boar,6 %} 特效标签wow标签语法参数配置样式预览示例源码123{% wow [animete],[duration],[delay],[offset],[iteration] %}内容{% endwow %} animate: 动画样式,效果详见animate.css参考文档 duration: 选填项,动画持续时间,单位可以是ms也可以是s。例如3s,700ms。 delay: 选填项,动画开始的延迟时间,单位可以是ms也可以是s。例如3s,700ms。 offset: 选填项,开始动画的距离(相对浏览器底部) iteration: 选填项,动画重复的次数 注意,后面四个虽然是选填项,但是当有跨位选填时,次序不能乱。详见示例。支持嵌套其他外挂标签。 flip动画效果。 flip动画效果。 zoomIn动画效果,持续5s,延时5s,离底部100距离时启动,重复10次。 zoomIn动画效果,持续5s,延时5s,离底部100距离时启动,重复10次 slideInRight动画效果,持续5s,延时5s。 slideInRight动画效果,持续5s,延时5s。 heartBeat动画效果,延时5s,重复10次。 heartBeat动画效果,延时5s,重复10次。 flip动画效果。12345{% wow animate__flip %}{% note green 'fas fa-fan' modern%}`flip`动画效果。{% endnote %}{% endwow %} zoomIn动画效果,持续5s,延时5s,离底部100距离时启动,重复10次。12345{% wow animate__zoomIn,5s,5s,100,10 %}{% note blue 'fas fa-bullhorn' modern%}`zoomIn`动画效果,持续`5s`,延时`5s`,离底部`100`距离时启动,重复`10`次{% endnote %}{% endwow %} slideInRight动画效果,持续5s,延时5s。12345{% wow animate__slideInRight,5s,5s %}{% note orange 'fas fa-car' modern%}`slideInRight`动画效果,持续`5s`,延时`5s`。{% endnote %}{% endwow %} heartBeat动画效果,延时5s,重复10次。此处注意不用的参数位置要留空,用逗号间隔。12345{% wow animate__heartBeat,,5s,,10 %}{% note red 'fas fa-battery-half' modern%}`heartBeat`动画效果,延时`5s`,重复`10`次。{% endnote %}{% endwow %} 进度条 progress标签语法参数配置样式预览示例源码1{% progress [width] [color] [text] %}width: 0到100的阿拉伯数字color: 颜色,取值有red,yellow,green,cyan,blue,graytext:进度条上的文字内容进度条样式预览 进度条样式预览 进度条样式预览 进度条样式预览 进度条样式预览 进度条样式预览123456{% progress 10 red 进度条样式预览 %}{% progress 30 yellow 进度条样式预览 %}{% progress 50 green 进度条样式预览 %}{% progress 70 cyan 进度条样式预览 %}{% progress 90 blue 进度条样式预览 %}{% progress 100 gray 进度条样式预览 %} 注释 notation标签语法参数配置样式预览示例源码1{% nota [label] , [text] %}label: 注释词汇text: 悬停显示的注解内容把鼠标移动到我上面试试1{% nota 把鼠标移动到我上面试试 ,可以看到注解内容出现在顶栏 %} 气泡注释 bubble标签语法参数配置样式预览示例源码1{% bubble [content] , [notation] ,[background-color] %}content: 注释词汇notation: 悬停显示的注解内容background-color: 可选,气泡背景色。默认为“#71a4e3”最近我学到了不少新玩意儿(虽然对很多大佬来说这些已经是旧技术了),比如 CSS 的兄弟相邻选择器例如 h1 + p {margin-top:50px;},flex 布局 Flex 是 Flexible Box 的缩写,意为弹性布局 \",用来为盒状模型提供最大的灵活性\",transform 变换 transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。,animation 的贝塞尔速度曲线贝塞尔曲线 (Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋写法,还有今天刚看到的 clip-pathclip-path 属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。属性。这些对我来说很新颖的概念狠狠的冲击着我以前积累起来的设计思路。1最近我学到了不少新玩意儿(虽然对很多大佬来说这些已经是旧技术了),比如CSS的{% bubble 兄弟相邻选择器,"例如 h1 + p {margin-top:50px;}" %},{% bubble flex布局,"Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性","#ec5830" %},{% bubble transform变换,"transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。","#1db675" %},animation的{% bubble 贝塞尔速度曲线,"贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋","#de4489" %}写法,还有今天刚看到的{% bubble clip-path,"clip-path属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。","#868fd7" %}属性。这些对我来说很新颖的概念狠狠的冲击着我以前积累起来的设计思路。 引用文献 reference标签语法参数配置样式预览示例源码12{% referto [id] , [literature] %}{% referfrom [id] , [literature] , [url] %}考虑到锚点跳转的特性,不建议您将引用出处标签referfrom写在常隐外挂标签(如folding、tab、hideToggle)中,这样能有效避免跳转失败。 referto 引用上标 id: 上标序号内容,需与referfrom标签的id对应才能实现跳转 literature: 引用的参考文献名称 referfrom 引用出处 id: 序号内容,需与referto标签的id对应才能实现跳转 literature: 引用的参考文献名称 url: 引用的参考文献链接,可省略 Akilarの糖果屋(akilar.top)是一个私人性质的博客[1]Akilarの糖果屋群聊简介参考资料,从各类教程至生活点滴,无话不谈。建群的目的是提供一个闲聊的场所。博客采用Hexo框架[2]Hexo中文文档参考资料,Butterfly主题[3]Butterfly 安装文档(一) 快速开始参考资料 本项目参考了Volantis[4]hexo-theme-volantis 标签插件参考资料的标签样式。引入[tag].js,并针对butterfly主题修改了相应的[tag].styl。在此鸣谢Volantis主题众开发者。主要参考内容包括各个volantis的内置标签插件文档[5]Volantis文档:内置标签插件参考资料Butterfly主题的各个衍生魔改[6]Butterfly 安装文档:标签外挂(Tag Plugins参考资料[7]小弋の生活馆全样式预览参考资料[8]l-lin-font-awesome-animation参考资料[9]小康的butterfly主题使用文档参考资料 [1]Akilarの糖果屋群聊简介 [2]Hexo中文文档 [3]Butterfly 安装文档(一) 快速开始 [4]hexo-theme-volantis 标签插件 [5]Volantis文档:内置标签插件 [6]Butterfly 安装文档:标签外挂(Tag Plugins [7]小弋の生活馆全样式预览 [8]l-lin-font-awesome-animation [9]小康的butterfly主题使用文档1234567891011121314151617Akilarの糖果屋(akilar.top)是一个私人性质的博客{% referto '[1]','Akilarの糖果屋群聊简介' %},从各类教程至生活点滴,无话不谈。建群的目的是提供一个闲聊的场所。博客采用Hexo框架{% referto '[2]','Hexo中文文档' %},Butterfly主题{% referto '[3]','Butterfly 安装文档(一) 快速开始' %}本项目参考了Volantis{% referto '[4]','hexo-theme-volantis 标签插件' %}的标签样式。引入`[tag].js`,并针对`butterfly`主题修改了相应的`[tag].styl`。在此鸣谢`Volantis`主题众开发者。主要参考内容包括各个volantis的内置标签插件文档{% referto '[5]','Volantis文档:内置标签插件' %}Butterfly主题的各个衍生魔改{% referto '[6]','Butterfly 安装文档:标签外挂(Tag Plugins' %}{% referto '[7]','小弋の生活馆全样式预览' %}{% referto '[8]','l-lin-font-awesome-animation' %}{% referto '[9]','小康的butterfly主题使用文档' %}{% referfrom '[1]','Akilarの糖果屋群聊简介','https://jq.qq.com/?_wv=1027&k=pGLB2C0N' %}{% referfrom '[2]','Hexo中文文档','https://hexo.io/zh-cn/docs/' %}{% referfrom '[3]','Butterfly 安装文档(一) 快速开始','https://butterfly.js.org/posts/21cfbf15/' %}{% referfrom '[4]','hexo-theme-volantis 标签插件','https://volantis.js.org/v5/tag-plugins/' %}{% referfrom '[5]','Volantis文档:内置标签插件','https://volantis.js.org/tag-plugins/' %}{% referfrom '[6]','Butterfly 安装文档:标签外挂(Tag Plugins','https://butterfly.js.org/posts/4aa8abbe/#%E6%A8%99%E7%B1%A4%E5%A4%96%E6%8E%9B%EF%BC%88Tag-Plugins%EF%BC%89' %}{% referfrom '[7]','小弋の生活馆全样式预览','https://lovelijunyi.gitee.io/posts/c898.html' %}{% referfrom '[8]','l-lin-font-awesome-animation','https://github.com/l-lin/font-awesome-animation' %}{% referfrom '[9]','小康的butterfly主题使用文档','https://www.antmoe.com/posts/3b43914f/' %} 弹窗点击查看1 点击查看2","categories":[{"name":"网站","slug":"网站","permalink":"https://blog.liynw.top/categories/%E7%BD%91%E7%AB%99/"}],"tags":[{"name":"Hexo","slug":"Hexo","permalink":"https://blog.liynw.top/tags/Hexo/"},{"name":"butterfly","slug":"butterfly","permalink":"https://blog.liynw.top/tags/butterfly/"}]},{"title":"01 背包学习笔记","slug":"「Algorithm」01背包","date":"2021-08-16T22:32:24.000Z","updated":"2022-02-20T11:45:35.000Z","comments":true,"path":"posts/7ffdbf9/","link":"","permalink":"https://blog.liynw.top/posts/7ffdbf9/","excerpt":"概念01背包之所以叫“01”背包,就是它需要选择是否将当前这样物品装入背包。$0$ 代表不装,$1$ 代表装。","text":"概念01背包之所以叫“01”背包,就是它需要选择是否将当前这样物品装入背包。$0$ 代表不装,$1$ 代表装。 一、板子题和强化题目描述有一个最多能装 $m$ 千克的背包,有 $n$ 块魔法石,它们的重量分别是$W_1,W_2,…,W_n$ ,它们的价值分别是 $C_1,C_2,…,C_n$。若每种魔法石只有一件,问能装入的最大总价值。 输入格式第一行为两个整数 $m$ 和 $n$,以下 $n$ 行中,每行两个整数 $W_i,C_i$,分别代表第 $i$ 件物品的重量和价值。 输出格式输出一个整数,即最大价值。 样例输入12348 32 35 45 5 样例输出18 1.板子$1\\le m \\le 30,1\\le n\\le 15$. 首先还是分析问题:每个东西都可以选择选或不选。我们假设正在抉择第 $i$ 个物品,如果选的话,我们需要付出的代价就是占据一部分的背包容量。可见有两个因素在影响dp数组的值:$i$ 和背包容量 $m$。 因此可以定义dp数组的意义为: $dp_{i,j}$ 代表在前 $i$ 件物品中做选择,背包容量为 $j$ 时能获得的最大价值。 好,继续分析,每次对于第 $i$ 个物体的抉择,无非是选和不选的两种情况。 如果选的话,那么在选择前 $i-1$ 个物体的时候,可以使用的背包容量就需要减少 $w_i$,但是所获得的价值就可以加上 $c_i$。 如果不选,那就和选择前 $i-1$ 个物品的最优情况是一样的。 可得出动态转移方程: dp_{i,j}=\\max(dp_{i-1,j},dp_{i-1,j-w_i}+c_i)Code把动态转移方程套到程序里面就可以了,注意循环两层从小到大。 12345678910111213141516171819#include<bits/stdc++.h>using namespace std;int n,m,w[20],c[20],dp[20][35];int main(){ scanf("%d %d",&m,&n); for(int i=1;i<=n;i++){ scanf("%d %d",&w[i],&c[i]); } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(w[i]>j) dp[i][j]=dp[i-1][j]; else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]); } } printf("%d",dp[n][m]); return 0;} 2.压缩空间$1\\le m \\le 3\\times 10^6,1\\le n\\le 100$,空限变得极度 duliu。 考虑优化空间复杂度。 上述程序的 $dp$ 数组为二维数组,但是大家注意到动态转移方程时只需要用到 $dp_{i-1,?}$ 的值。 这也就意味着我们可以使用滚动数组优化空间复杂度。(不过时间复杂度就没法变了……) 需要注意的是,因为问号处的值必定小于 $j$,所以我们 $j$ 这一维需要倒着枚举,不然在利用前面的值的时候就会错利用为 $dp_{i,?}$ 而非 $dp_{i-1,?}$ 的值了。 Code12345678910111213141516#include<cstdio>#define max(a,b) (a)>(b)?(a):(b)int m,n,dp[35],w[20],c[20];int main(){ scanf("%d %d",&m,&n); for(int i=1;i<=n;i++){ scanf("%d %d",&w[i],&c[i]); } for(int i=1;i<=n;i++){ for(int j=m;j>=w[i];j--){ dp[j]=max(dp[j],dp[j-w[i]]+c[i]); } } printf("%d",dp[m]); return 0;} 3.如果有其它的限制条件……$1\\le n\\le 100$ $1\\le W_i,m\\le 10^9$ $1\\le C_i\\le 10^7$ 对于每个 $i=2,3,…,n$,满足 $W_1\\le W_i\\le W_1+3$ 如果按照背包的板子做,那么一定会 TLE 和 MLE。那如何优化呢? 我们注意到数据范围里有一句十分特殊的话: 对于每个 $i=2,3,…,n$,满足 $W_1\\le W_i\\le W_1+3$ 那么我们完全可以把每个物品的重量以 $W_1$ 为基准,转化为一个不超过 $3$ 的数。这么可以极大地优化空间复杂度。 用 $h$ 代表 $W_1$ 的真实数字。 但是这样的话,我们就无法得知我们现在装的东西到底有没有超过背包的容量,因为我们并不知道我们选了多少个东西。所以 $dp$ 数组还需要开一维代表选择物品的个数。 所以 $dp_{i,j,k}$ 代表在前 $i$ 个物品中选择 $k$ 个物品,背包容量为 $j$ 时的最大价值。 还需要注意一下循环的范围: $i:1\\sim n$ (这个不需要解释了吧……) $j:0\\sim 3\\times i$ (因为物品的重量被处理过,当原来的 $W_i=W_1$ 时这个物品的重量为 $0$。所以最小的背包容量有可能是 $0$。物品最大的重量不超过 $3$,有 $i$ 个物品,所以最大可能容量为 $3\\times i$。) $k:1\\sim i$ (选择的物品数量不能超过总数。因为后面求答案 $ans$ 的初始值就为 $0$ 已经包括了一个都不选的情况,所以不需要考虑不选物品的情况。) 最后求答案的时候需要枚举 $dp_{n,i,j}$,注意只有 $i+j\\times h\\le m$ 时背包才不超限,可以更新答案。 Code12345678910111213141516171819202122232425262728293031#include<cstdio>#define max(a,b) (a)>(b)?(a):(b) //卡常小技巧:用这个比库函数要快一些#define ll long longll n,m,ans,h,w[105],v[105],dp[105][305][105];int main(){ scanf("%lld %lld",&n,&m); scanf("%lld %lld",&h,&v[1]); //h 代表 W1 的的重量 for(int i=2;i<=n;i++){ scanf("%lld %lld",&w[i],&v[i]); w[i]-=h; //以 W1 为基准转化每一件物品的重量 } for(int i=1;i<=n;i++){ for(int k=1;k<=i;k++){ //选择的物品数量不能大于总数量 for(int j=0;j<=3*i;j++){ //3*i 为被允许的最大容量 if(j<w[i]) dp[i][j][k]=dp[i-1][j][k]; else dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-w[i]][k-1]+v[i]); } } } //在合法的范围内寻找最大值 for(int i=0;i<=3*n;i++){ for(int j=0;j<=n;j++){ if(i+j*h<=m) ans=max(ans,dp[n][i][j]); } } printf("%lld",ans); return 0;} 二、比较简单的应用一些适合背包初学者体会的题目~ 例2-1.采药题目链接 非常板的一道题,把板子搬过来即可。 例2-2.装箱问题题目链接 也是板子题,因为没有价值,而要求剩余空间最小,那么就是让重量尽量大。所以我们可以把重量当作价值,在不超过容量的前提下把剩余空间尽量变小。 Code (非滚动写法)12345678910111213141516171819#include<bits/stdc++.h>using namespace std;int m,n,w[35],dp[35][20005];int main(){ scanf("%d %d",&m,&n); for(int i=1;i<=n;i++){ scanf("%d",&w[i]); } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(w[i]>j) dp[i][j]=dp[i-1][j]; else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+w[i]); } } printf("%d",m-dp[n][m]); //求的是剩余空间所以需要做减法 return 0;} 例2-3.数字分组1题目链接 简化题意:给出若干个数字,把这些数字分为两组使两组数字的和的差距尽量小。 因为要让两组尽量接近,即接近 $\\big (\\sum\\limits_{i=1}^{n}w_i\\big ) ÷2$,所以我们规定一个背包的大小为这个数,然后让一组的重量尽量靠近这个值(也就是例 $2-2$,顺便展示一下例 $2-2$ 的滚动数组写法),再分别输出即可。 Code1234567891011121314151617181920#include<bits/stdc++.h>using namespace std;int m,sum,n,w[35],dp[300005];int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",&w[i]); m+=w[i]; sum+=w[i]; } m=(m+1)/2; //为了以防万一向上取整也是可以的 for(int i=1;i<=n;i++){ for(int j=m;j>=w[i];j--){ dp[j]=max(dp[j],dp[j-w[i]]+w[i]); } } int k=sum-dp[m]; printf("%d",abs(k-dp[m])); return 0;} 三、01背包问题输出方案DP 算法输出方案的思想都是差不多的,即,开一个 $pre$ 数组记录每一步的决策,然后输出的时候根据 $pre$ 数组倒推回去。 我们来看一些例题。 例3-1.01背包输出方案题目即让一个普通的01背包输出方案——输出要选择的物品编号。 那么,在每次求最大值的时候,我们就不能用 $\\max$ 了,而是打一个条件选择,如果能更新 $dp_j$ 的值,那么我们就更新 $dp_j$,而且把 $pre_{i,j}$ 设为 $1$,代表在背包容量为 $j$ 的情况下要选择 $i$ 物品。 最后的输出可以用递归,也可以用循环。这里本人比较偏向于打递归(倒着推,代码较为简短),大家可以结合注释好好理解一下这个递归的输出函数: 12345678void print(int x,int y){ //x代表物品的编号,y代表背包此时的容量 if(!x) return; //物品已经枚举完 if(pre[x][y]){ //要选这个物品 print(x-1,y-w[x]); //往前推,背包的容量需要减少这个物品的重量 printf("%d ",x); //输出(因为要正着输出但是我们是倒着推的,所以在运行完了上面的递归函数后才输出) }else print(x-1,y); //不选的话就不用减少背包容量了 return;//华丽结束~} 有时候,题目会要求我们输出使用背包容量最小时的方案,我们在调用函数的时候 $y$ 参数就不可以直接填 $m$,而是进行一次循环,从小往大枚举,如果 $dp_i=dp_m$(最优解),就把这个 $i$ 值代入函数的 $y$ 参数。 Code123456789101112131415161718192021222324252627282930313233343536#include<cstdio>#define max(a,b) (a)>(b)?(a):(b)int m,n,dp[35],w[20],c[20],pre[20][35];void print(int x,int y){ if(!x) return; if(pre[x][y]){ print(x-1,y-w[x]); printf("%d ",x); }else print(x-1,y); return;}int main(){ scanf("%d %d",&m,&n); for(int i=1;i<=n;i++){ scanf("%d %d",&w[i],&c[i]); } for(int i=1;i<=n;i++){ for(int j=m;j>=w[i];j--){ //dp[j]=max(dp[j],dp[j-w[i]]+c[i]); if(dp[j-w[i]]+c[i]>dp[j]){ dp[j]=dp[j-w[i]]+c[i]; pre[i][j]=1; } } } printf("%d\\n",dp[m]); int t=0; for(int i=1;i<=m;i++){ if(dp[i]==dp[m]){ t=i; break; } } print(n,t); return 0;} 例3-2.CD题目链接 和上述题目差不多,有多组输入,但是输出要求要输出和。 不过因为和是在最后输出,所以我们可以在递归函数的过程中统计选择了多少个 CD,代码实现很简单。 再说一句:如果是要求在输出方案之前输出和,那么我们就需要再准备一个数组,先用此函数统计和(但是把输出语句改为把答案存到数组里的语句),输出 $sum$,然后输出数组。 Code1234567891011121314151617181920212223242526272829303132333435363738394041#include<bits/stdc++.h>using namespace std;int m,n,dp[10005],a[105],pre[105][10005],sum;void print(int x,int y){ bool flag=0; if(!x) return; if(pre[x][y]){ flag=1; print(x-1,y-a[x]); }else print(x-1,y); if(flag){ printf("%d ",a[x]); sum+=a[x]; } return;}int main(){ while(scanf("%d %d",&m,&n)!=EOF){ memset(dp,0,sizeof(dp)); memset(pre,0,sizeof(pre)); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } for(int i=1;i<=n;i++){ for(int j=m;j>=a[i];j--){ if(dp[j-a[i]]+a[i]>m) continue; if(dp[j-a[i]]+a[i]>=dp[j]){ dp[j]=dp[j-a[i]]+a[i]; pre[i][j]=1; } } } print(n,m); printf("sum:%d\\n",sum); sum=0; } return 0;} 四、需要排序的01背包问题到这个阶段,背包问题就开始有难度了。 背包问题本身是不需要排序的。需要排序的背包问题就是说在做某个决策的时候,一些参数(如背包容量等)会发生变化,为了得到最优解,我们需要对背包的物品进行排序。 一般来说,排序的过程涉及到贪心的思想,我们可以使用假设的方法。这种方法会在下面的题解中详细介绍。 还是来看一些例题来加强理解吧。 例4-1.骄傲的商人题目链接 题意简述:有一些商人,每个商人只卖一件商品,价格是 $P_i$,但是如果你的钱少于 $Q_i$,你就不能买这个东西。你评估了每一件商品的价值 $V_i$。而且你只有 $M$ 单位的钱,那么你能得到的最大价值是多少? 其实这道题是非常简单的,就是十分普通的01背包板子,只需要在前面加一个判断总钱数是否大于 $Q_i$ 的程序就好啦~ 结果你交上去会发现错了(( 为什么呢?疑惑(っ´Ι`)っ?? 原因是这样的: 因为每一次购买物品都需要耗钱买,那么有些本来可以买的东西因为枚举较为靠后就没有买到。 所以呢,我们需要对每一件商品进行排序,让每个让我拿到最优解的东西都可以买到。 那么怎么排序呢?这时候就需要假设法(名字我自己取的)来分析了! 使用结构体存储每件商品的信息,然后假设我们要买两件商品 $x$ 和 $y$,而且你的钱 $M$ 大于两个商品耗费的钱之和。 假设如果你先买 $x$ 商品比先买 $y$ 商品方案更优。 那么就只有一种情况:买了 $x$ 后可以继续买 $y$,但是买了 $y$ 之后就不可以买 $x$ 了。 $x\\ \\ \\ \\ x.p\\ \\ \\ x.q\\ \\ \\ x.v$$y\\ \\ \\ \\ y.p\\ \\ \\ y.q\\ \\ \\ y.v$ (把所有条件都列在草稿纸上以便分析,一定要养成好习惯哦!) 所以可以得出: $ \\begin{cases} M-x.p\\ge y.q\\cdots(1)\\\\ M-y.p<x.q\\cdots(2) \\end{cases}$ 整理一下式子可以得到: $ \\begin{cases} M-x.p\\ge y.q\\cdots(1)\\\\ y.p-M>-x.q\\cdots(2) \\end{cases}$ (右边负号别漏了) $(1)+(2)$ 可得: $y.p-x.p>y.q-x.q$ 移项变号: $x.q-x.p<y.q-y.p$ 就得出了最后排序的式子。 使用的时候,$x$(放在前面更优的物品)放在 cmp 传参的前面,$y$ 放在cmp 传参的后面,直接 return 推出来的式子即可。当然,你愿意保险一点像我一样写条件选择也没问题。 Code1234567891011121314151617181920212223242526#include<cstdio>#include<algorithm>using namespace std;int m,n,dp[5005];struct node{ int p,q,v;}a[505];bool cmp(node x,node y){ if(x.q-x.p>=y.q-y.p) return false; return true;}int main(){ scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d %d %d",&a[i].p,&a[i].q,&a[i].v); } sort(a+1,a+n+1,cmp); for(int i=1;i<=n;i++){ for(int j=m;j>=a[i].q;j--){ dp[j]=max(dp[j],dp[j-a[i].p]+a[i].v); } } printf("%d",dp[m]); return 0;} 例4-2.烹调方案题目链接 很明显这道题是需要排序的,因为每一道菜的美味指数与时间有关,所以需要排序安排做每个食物的顺序。不过现在这并不是我们的重点。 问题是:背包容量是啥,重量是啥,价值又是啥呢? 题目中只规定了一个时间,所以重量限制就是做菜的时间;需要获得的是最大的美味指数,所以价值就是每道菜的美味指数。(我说了先暂时不管时间对食物价值的损耗,只是推状态转移方程而已啦!) 外层循环 $i$:枚举每个食物。 内层循环 $j$:枚举过了多少时间。 特别注意! 在背包问题中,需要注意你的内层循环这个数值到底代表的是最多这么多限制还是刚好这么多限制,这道题因为不需要用到非常精准的时间分钟数所以是最多的限制,$dp$ 数组不需要初始化极小值,所有数为 $0$ 即可。 每个食物的重量:$c_i$ 每个食物的价值:$a_i-j\\times b_i$ 照着板子打上去就可以了。现在是考虑排序的时间~ 还是假设要做两个食物 $x$ 和 $y$,先做 $x$ 比先做 $y$ 获得的美味指数多。为了方便,时间从 $0$ 开始且不考虑食物美味指数小于等于 $0$ 的情况。 $x\\ \\ \\ \\ x.a\\ \\ \\ x.b\\ \\ \\ x.c$$y\\ \\ \\ \\ y.a\\ \\ \\ y.b\\ \\ \\ y.c$ 先做 $x$ 的美味指数:$x.a+y.a-y.b\\times x.c$ 先做 $y$ 的美味指数:$y.a+x.a-x.b\\times y.c$ 由假设得出结论: $x.a+y.a-y.b\\times x.c>y.a+x.a-x.b\\times y.c$ 整理得: $-y.b\\times x.c>-x.b\\times y.c$ 去掉负号: $y.b\\times x.c<x.b\\times y.c$ OK,式子推出来了。还是用结构体存储排序打代码。 代码记得开 long long。 Code123456789101112131415161718192021222324252627282930313233343536#include<cstdio>#include<algorithm>#define ll long longusing namespace std;ll T,n,ans,dp[100005];struct food{ ll a,b,c;}w[55];bool cmp(food x,food y){ if(x.c*y.b>=x.b*y.c) return false; return true;}int main(){ scanf("%lld %lld",&T,&n); for(int i=1;i<=n;i++){ scanf("%lld",&w[i].a); } for(int i=1;i<=n;i++){ scanf("%lld",&w[i].b); } for(int i=1;i<=n;i++){ scanf("%lld",&w[i].c); } sort(w+1,w+n+1,cmp); for(int i=1;i<=n;i++){ for(int j=T;j>=w[i].c;j--){ dp[j]=max(dp[j],dp[j-w[i].c]+w[i].a-w[i].b*j); } } for(int i=1;i<=T;i++){ ans=max(ans,dp[i]); } printf("%lld",ans); return 0;} 说句闲话其实上面两道题代表得是本人认为对于需要排序得背包问题的两大类: 骄傲的商人 $→$ 不同的排序方式,一些情况成立,另一些则无法成立。每种情况只要成立创造的价值都是相同的。 列出来的式子通常是 $2$ 个及以上,需要合并,较为麻烦。 烹饪方案 $→$ 不同的排序方式,所有情况都成立,可是创造的价值不同。 通常只会列出一个式子进行推到,相对比较容易。 当然肯定会有两者的结合题目,只不过本人暂时未遇到就先不说了。 五、01背包进阶到了这个阶段,01背包就已经不是单纯的背包问题了,其本质上是每进行一个操作参数的变化。具体表现为:“背包的容量”和物品的“重量”、“价值”不是很好找,而且很有可能根据某些操作变化。 我们来看一些例题。 例5-1.Course Selection System题目链接 题意简述:有 $n$ 个物品,第 $i$ 个物品都有两个权值 $H_i$ 和 $C_i$。现在选出若干个物品(可以不选)$x_1,x_2,\\ldots ,x_m$ 使得 $ans$ 最大。 $ans=\\big(\\sum\\limits_{i=1}^{m}H_{x_i}\\big)^2-\\big(\\sum\\limits_{i=1}^{m}H_{x_i}\\big)\\times \\big(\\sum\\limits_{i=1}^{m}C_{x_i}\\big)-\\big(\\sum\\limits_{i=1}^{m}C_{x_i}\\big)^2$ 这道题乍一看没有什么思路,那我们就需要对这个式子进行处理。 首先,根据观察可得:这道题中 $H_i$ 主要是让答案更大,$C_i$ 是让答案更小。可以说,$C_i$ 是答案的限制。所以,我们可以把 $C_i$ 作为背包的容量和每一件物品的重量。不难看出,背包的最大容量应该是 $\\sum\\limits_{i=1}^{n}C_i$。 那么相应地,$H_i$ 就可以作为物品的价值,所以存在 $dp$ 数组里面的值就是固定 $C_i$ 下的最大 $H_i$ 之和。 在进行一次 01 背包之后,你不要以为就万事大吉了!因为不一定 $C_i$ 大的 $ans$ 值就是最优解,所以我们需要遍历 $dp$ 数组,每次在计算答案的时候按照题目要求的格式计算即可。具体看代码理解。 最后注意开 long long,注意多组数据和每次的初始化。 Code123456789101112131415161718192021222324252627#include<cstdio>#include<cstring>#define max(a,b) (a)>(b)?(a):(b)#define ll long longll t,n,ans,sum,h[505],c[505],dp[50005];int main(){ scanf("%lld",&t); while(t--){ memset(dp,0,sizeof(dp)); //多组数据要初始化 ans=0,sum=0; scanf("%lld",&n); for(int i=1;i<=n;i++){ scanf("%lld %lld",&h[i],&c[i]); sum+=c[i]; } for(int i=1;i<=n;i++){ for(int j=sum;j>=c[i];j--){ dp[j]=max(dp[j],dp[j-c[i]]+h[i]); } } for(ll i=1;i<=sum;i++){ //i遍历的是 Ci 的值,dp 数组里的值代表 Hi ans=max(ans,dp[i]*dp[i]-dp[i]*i-i*i); } printf("%lld\\n",ans); } return 0;} 例5-2.Dima and Salad题目链接 本题解已审核通过,欢迎大家资瓷~link 首先假设我们选了 $m$ 个水果。已知: \\dfrac{\\sum\\limits_{i=1}^{m}a_i}{\\sum\\limits_{i=1}^{m}b_i}=k转换式子后可得: \\sum\\limits_{i=1}^{m}a_i=\\sum\\limits_{i=1}^{m}b_i\\times k再次转换: \\sum\\limits_{i=1}^{m}a_i-\\sum\\limits_{i=1}^{m}b_i\\times k=0可以写成: \\sum\\limits_{i=1}^{m}(a_i-b_i\\times k)=0所以我们可以让第 $i$ 个水果的重量为 $a_i-b_i\\times k$,最后只要让重量总和等于 $0$ 就算满足条件啦。 此时我们需要注意初始值:因为按照上面这么分析,水果的重量和完全有可能是负数。这个时候看到数据范围: $1\\le n\\le 100,1\\le k\\le 10,1\\le a_i,b_i\\le 100$ 最小极限情况:$n=100,k=10,a_i=1,b_i=100$ 那么重量总和就为: $100\\times (1-100\\times 10)\\approx -100000$ 所以 $dp$ 数组中,每一个数字都需要加上 $100000$ 以保证不越界。我们可以定义一个常量 $p=100000$,写代码更加简洁,不过不写也是可以的。 接着我们计算最大极限情况:$n=100,k=1,a_i=100,b_i=1$ 那么重量总和就为: $100\\times (100-1\\times 1)\\approx 10000$ 所以 $dp$ 数组需要开 $110000+5$,这也是最大有可能出现的背包容量。 最后要注意:因为重量有正有负,所以我们不知道循环是从小到大还是从大到小。所以我没有打滚动,如果要打滚动,需要注意判断重量的正负之后判断循环的顺序。 打代码要注意细节,我错了很多次才对。 (PS:我打代码的时候觉得 $a_i-b_i\\times k$ 看着不爽,就改成了 $b_i\\times k-a_i$,这样在分析极限值的时候有变化,需要注意。) Code12345678910111213141516171819202122232425262728293031#include<cstdio>#include<cstring>#define max(a,b) (a)>(b)?(a):(b)int n,k,a[105],b[105],m[105],dp[105][110005];int main(){ memset(dp,-1,sizeof(dp)); dp[0][10000]=0; //最后答案的位置是0 scanf("%d %d",&n,&k); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } for(int i=1;i<=n;i++){ scanf("%d",&b[i]); m[i]=k*b[i]-a[i]; } for(int i=1;i<=n;i++){ for(int j=110000;j>=0;j--){ if(j-m[i]<=110000&&j-m[i]>=0){ //不能越界,越了界后面就没有回来的可能性了,直接跳过此循环 if(dp[i-1][j-m[i]]==-1) dp[i][j]=dp[i-1][j]; //不可能 else dp[i][j]=max(dp[i-1][j],dp[i-1][j-m[i]]+a[i]); } } } if(dp[n][10000]) printf("%d",dp[n][10000]); else printf("-1"); return 0;} 例5-3.多米诺骨牌题目链接 emmm 这也许是一道转化比较复杂的01背包问题了。 首先对于每个骨牌,都有转与不转两种抉择,相当于01背包里的“选和不选”。 其次,要求的是“上下分别之和的差得绝对值”尽量小。为了方便,我们把这个差记为上面减下面的差。假如说我们把第 $i$ 个骨牌的上面点数记为 $a_i$,下面的点数记为 $b_i$,那么每旋转一个骨牌,那么上下和的差就会减少 $2\\times (a_i-b_i)$,这个 $2$ 可以约掉。我们可以把这个东西记为第 $i$ 个骨牌旋转后的重量。 每个骨牌的价值是 $1$,我们要让价值最小。 但是这道题和上一道题有相同的地方,就是万恶的负重量!于是我们又要分析数据的极限值了 qwq。 分析过程省略,反正最后出来是 $-5000\\sim 5000$。 所以 $dp$ 数组关于容量的那个下标需要统一加上 $5000$。 另外还有就是与上面一样,因为重量有正有负,所以打滚动需要注意循环的顺序。 求答案:那如何让在产生最优的重量时求得最小的价值呢?我们可以把 $dp$ 数组先全部初始化为极大值,只把 $dp_{0,5000}$ 定义为 $0$,在求解答案的过程中,从小到大枚举背包占的容量,只要有一个数字不是极大值,就可以直接输出。不过因为让求的是差的绝对值的最小值,所以枚举的时候,两边(重量为正和负)都需要看一下。 Code12345678910111213141516171819202122232425#include<bits/stdc++.h>using namespace std;int ans,n,a[1005],b[1005],c[1005],dp[1005][10005];int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d %d",&a[i],&b[i]); c[i]=a[i]-b[i]; } memset(dp,127,sizeof(dp)); dp[0][5000]=0; for(int i=1;i<=n;i++){ for(int j=10000;j>=c[i];j--){ dp[i][j]=min(dp[i-1][j-c[i]],dp[i-1][j+c[i]]+1); } } for(int i=0;i<=5000;i++){ int ans=min(dp[n][5000-i],dp[n][5000+i]); if(ans!=2139062143){ printf("%d",ans); return 0; } } return 0;} 例5-4.夏季特惠题目链接 题意简述:有 $n$ 个商品,第 $i$ 个商品原价为 $a_i$,打折后的价格为 $b_i$,买到这个东西你的快乐值会增加 $w_i$。你的钱数是无限的,预算为 $m$ 元,只要花的钱不超过 $m$ 或者获得的总优惠金额不低于超过预算的总金额,那么你就不会觉得吃亏。现在请你在感觉不吃亏的前提下获得最多的快乐值。 这道题很明显背包是会根据买的东西变化的。具体表现为:每买一样东西,背包会先减去 $b_i$,再加上 $a_i-b_i$ (获得优惠的金额)。 那么易证只要买的东西价格满足 $a_i\\ge 2\\times b_i$,那背包可用的容量不仅不会减少,而且可能会增加。 所以满足上述条件的商品是一定要买的。 接着考虑剩下的物品。上面已经分析,在买了一个物品后,背包的可用容量 $H$ 就会变成 $H-2\\times b_i+a_i$。所以商品的重量可以规定为 $2\\times b_i-a_i$。而因为剩下的商品不满足上述条件,所以买了背包的容量是一定会减小的,即剩下的商品重量不会为负数。 商品的价值是 $w_i$,这个很明显。 代码实现很简单,哦,对了,记得开 long long。 Code1234567891011121314151617181920212223242526#include<bits/stdc++.h>#define ll long longusing namespace std;ll n,x,tot,ans,a[505],b[505],h[505],c[505],w[505],dp[5000005];int main(){ scanf("%lld %lld",&n,&x); for(int i=1;i<=n;i++){ scanf("%lld %lld %lld",&a[i],&b[i],&h[i]); } for(int i=1;i<=n;i++){ if(a[i]>=2*b[i]){ x+=a[i]-2*b[i]; ans+=h[i]; }else{ w[++tot]=2*b[i]-a[i]; c[tot]=h[i]; } } for(int i=1;i<=n;i++){ for(int j=x;j>=w[i];j--){ dp[j]=max(dp[j],dp[j-w[i]]+c[i]); } } printf("%lld",ans+dp[x]); return 0;} 总结01背包这个算法有非常多的变化形式,唯有多看题、刷题、总结题型才能真正掌握。同时它还是后面其它背包问题的基础,所以学好 01 背包是一个十分重要的版块。 终于写完了!完结撒花!❀╰(*°▽°*)╯❀ Markdown 竟然写了 $900^+$ 行,创历史新高啊 qwq。","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"}],"tags":[{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"贪心","slug":"贪心","permalink":"https://blog.liynw.top/tags/%E8%B4%AA%E5%BF%83/"}]}],"categories":[{"name":"词条","slug":"词条","permalink":"https://blog.liynw.top/categories/%E8%AF%8D%E6%9D%A1/"},{"name":"生活","slug":"生活","permalink":"https://blog.liynw.top/categories/%E7%94%9F%E6%B4%BB/"},{"name":"学习笔记","slug":"学习笔记","permalink":"https://blog.liynw.top/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"},{"name":"网站","slug":"网站","permalink":"https://blog.liynw.top/categories/%E7%BD%91%E7%AB%99/"},{"name":"题解","slug":"题解","permalink":"https://blog.liynw.top/categories/%E9%A2%98%E8%A7%A3/"},{"name":"考试总结","slug":"考试总结","permalink":"https://blog.liynw.top/categories/%E8%80%83%E8%AF%95%E6%80%BB%E7%BB%93/"},{"name":"做题记录","slug":"做题记录","permalink":"https://blog.liynw.top/categories/%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/"},{"name":"证明","slug":"证明","permalink":"https://blog.liynw.top/categories/%E8%AF%81%E6%98%8E/"},{"name":"水贴","slug":"水贴","permalink":"https://blog.liynw.top/categories/%E6%B0%B4%E8%B4%B4/"}],"tags":[{"name":"文学创作","slug":"文学创作","permalink":"https://blog.liynw.top/tags/%E6%96%87%E5%AD%A6%E5%88%9B%E4%BD%9C/"},{"name":"闲聊","slug":"闲聊","permalink":"https://blog.liynw.top/tags/%E9%97%B2%E8%81%8A/"},{"name":"动态规划","slug":"动态规划","permalink":"https://blog.liynw.top/tags/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/"},{"name":"Hexo","slug":"Hexo","permalink":"https://blog.liynw.top/tags/Hexo/"},{"name":"butterfly","slug":"butterfly","permalink":"https://blog.liynw.top/tags/butterfly/"},{"name":"树形结构","slug":"树形结构","permalink":"https://blog.liynw.top/tags/%E6%A0%91%E5%BD%A2%E7%BB%93%E6%9E%84/"},{"name":"链表","slug":"链表","permalink":"https://blog.liynw.top/tags/%E9%93%BE%E8%A1%A8/"},{"name":"数据结构","slug":"数据结构","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"},{"name":"二分答案","slug":"二分答案","permalink":"https://blog.liynw.top/tags/%E4%BA%8C%E5%88%86%E7%AD%94%E6%A1%88/"},{"name":"单调队列","slug":"单调队列","permalink":"https://blog.liynw.top/tags/%E5%8D%95%E8%B0%83%E9%98%9F%E5%88%97/"},{"name":"思维","slug":"思维","permalink":"https://blog.liynw.top/tags/%E6%80%9D%E7%BB%B4/"},{"name":"数学","slug":"数学","permalink":"https://blog.liynw.top/tags/%E6%95%B0%E5%AD%A6/"},{"name":"游记","slug":"游记","permalink":"https://blog.liynw.top/tags/%E6%B8%B8%E8%AE%B0/"},{"name":"贪心","slug":"贪心","permalink":"https://blog.liynw.top/tags/%E8%B4%AA%E5%BF%83/"},{"name":"哈希","slug":"哈希","permalink":"https://blog.liynw.top/tags/%E5%93%88%E5%B8%8C/"},{"name":"STL","slug":"STL","permalink":"https://blog.liynw.top/tags/STL/"},{"name":"高精度","slug":"高精度","permalink":"https://blog.liynw.top/tags/%E9%AB%98%E7%B2%BE%E5%BA%A6/"},{"name":"模拟","slug":"模拟","permalink":"https://blog.liynw.top/tags/%E6%A8%A1%E6%8B%9F/"},{"name":"搜索","slug":"搜索","permalink":"https://blog.liynw.top/tags/%E6%90%9C%E7%B4%A2/"},{"name":"图论","slug":"图论","permalink":"https://blog.liynw.top/tags/%E5%9B%BE%E8%AE%BA/"},{"name":"递推,递归","slug":"递推,递归","permalink":"https://blog.liynw.top/tags/%E9%80%92%E6%8E%A8%EF%BC%8C%E9%80%92%E5%BD%92/"},{"name":"编译器","slug":"编译器","permalink":"https://blog.liynw.top/tags/%E7%BC%96%E8%AF%91%E5%99%A8/"}]}