<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>伞的隙间</title>
  
  <subtitle>天终会再明。</subtitle>
  <link href="http://blog.tibrella.space/atom.xml" rel="self"/>
  
  <link href="http://blog.tibrella.space/"/>
  <updated>2025-08-19T01:50:05.237Z</updated>
  <id>http://blog.tibrella.space/</id>
  
  <author>
    <name>Tibrella</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>使用 ZFS 作为 macOS Applications 目录</title>
    <link href="http://blog.tibrella.space/post/applicationz-on-hackintosh/"/>
    <id>http://blog.tibrella.space/post/applicationz-on-hackintosh/</id>
    <published>2025-08-19T01:46:51.000Z</published>
    <updated>2025-08-19T01:50:05.237Z</updated>
    
    <content type="html"><![CDATA[<p>最近折腾黑苹果，但是磁盘空间不够，更遗憾的是 ZFS不支持缩小分区。所以采用 O3X(OpenZFS On OSX) 把 ZFS 分区挂载到 macOS上实现扩展空间。</p><h2 id="创建-dataset">创建 dataset</h2><p>之前的文章写过 zfs 安装 archlinux，创建数据集参考<ahref="https://blog.tibrella.space/post/archlinux-on-zfs-root-with-zbm/index.html#%E5%88%9B%E5%BB%BA-zfs-%E6%B1%A0">原文</a>就行。</p><p>这里 ZFS 池是 <code>zroot</code>，打算把 Applications 目录放在<code>zroot/macOS/Applications</code> 这个位置。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">zfs create -o mountpoint=none zroot/macOS</span><br><span class="line">zfs create -o mountpoint=/Applications zroot/macOS/Applications</span><br></pre></td></tr></table></figure><p>这样会在引入 <code>zroot</code> 的时候把这个数据集自动挂载到<code>/Applications</code>，此前还有一个自动挂载到 <code>/home</code>的数据集，没什么影响。Linux 没有 <code>/Applications</code>，macOS 没有<code>/home</code>。</p><p>然后进行进一步配置。我的 <code>zroot</code> 使用 blake3 作为 checksum的选项，大量写入会导致 macOS 崩溃。由于我是 AMD 的 CPU，而 blake3采用了很多指令集优化，猜测是指令集问题，所以换成了另一个参加了 SHA3竞赛并取得不错成绩的 <ahref="https://en.wikipedia.org/wiki/Skein_%28hash_function%29">skein</a>算法。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">zfs set checksum=skein zroot/macOS/Applications </span><br><span class="line">zfs set dedup=skein zroot/macOS/Applications </span><br></pre></td></tr></table></figure><h2 id="配置-macos">配置 macOS</h2><p>有三件事要干：</p><ul class="task-list"><li><label><input type="checkbox" />把原来的 <code>/Applications</code>内容利用 <code>rsync</code> 复制到 ZFS 分区里</label></li><li><label><input type="checkbox" />自动挂载</label></li><li><label><input type="checkbox" />删除原来的<code>/Applications</code> 目录</label></li></ul><h3 id="迁移数据">迁移数据</h3><p>比较简单，为了方便我们先改一下挂载点：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">zfs set mountpoint=legacy zroot/macOS/Applications</span><br></pre></td></tr></table></figure><p><code>mountpoint=legacy</code>意思就是不自动挂载，并且只能通过系统自带的 <code>mount</code>命令挂载。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mkdir ~/test</span><br><span class="line">sudo mount zroot/macOS/Applications ~/test</span><br><span class="line">sudo rsync -a /Applications ~/test</span><br><span class="line">sudo umount ~/test</span><br></pre></td></tr></table></figure><h3 id="自动挂载">自动挂载</h3><p>在这之前把挂载点改回去</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo zfs set mountpoint=/Applications zroot/macOS/Applications</span><br></pre></td></tr></table></figure><p>我打算使用 <a href="https://launchd.info">launchd</a>实现自动挂载，原因是 OpenZFS on OS X 自己有一个自动挂载所有 zpool的脚本挂在了 launchd 下面。</p><p><del>距离配置已经有半年多了，凭印象写（）</del></p><p><code>/System/Library/LaunchDaemons</code> 或者<code>/Library/LaunchDaemons</code> 里面有一个 ZFS相关的服务配置，复制一份下来（其实挺好读的），之后把里面要执行的命令做一下修改，比如：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">key</span>&gt;</span>ProgramArguments<span class="tag">&lt;/<span class="name">key</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">array</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>/path/to/zfs<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>mount<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">string</span>&gt;</span>zroot/macOS/Applications<span class="tag">&lt;/<span class="name">string</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">array</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="删除-applications">删除 <code>/Applications</code></h2><p>需要关闭系统完整性保护等操作。本来想直接链接到根目录，不过发现 ZFS挂载到根目录不需要直接配置，就懒得整了。</p><p>具体忘了咋回事了，我猜把文件夹里面东西删干净就够用了。</p><p>高三文化课太紧迫了，然后没考好还要复读，估计这篇文章完整版得等明年高考完才能出现了。</p><p>只能提供一个操作思路了，我猜像我一样折腾 zfs和黑苹果的同志看完应该能理解。</p><p>有一些缺点，比如偶尔挂载慢了，LaunchPad 会抽风。</p>]]></content>
    
    
    <summary type="html">如果我硬盘足够大就没有这篇文章了。</summary>
    
    
    
    <category term="macOS" scheme="http://blog.tibrella.space/categories/macOS/"/>
    
    
    <category term="ZFS" scheme="http://blog.tibrella.space/tags/ZFS/"/>
    
    <category term="Launchd" scheme="http://blog.tibrella.space/tags/Launchd/"/>
    
  </entry>
  
  <entry>
    <title>使用 systemd-networkd + iwd 代替 Network Manager</title>
    <link href="http://blog.tibrella.space/post/replace-nm-with-systemd-networkd/"/>
    <id>http://blog.tibrella.space/post/replace-nm-with-systemd-networkd/</id>
    <published>2024-08-28T06:16:58.000Z</published>
    <updated>2024-10-07T07:27:54.153Z</updated>
    
    <content type="html"><![CDATA[<p><ahref="https://wiki.archlinuxcn.org/wiki/Systemd-networkd">参考文档</a></p><p>这两天用 Network Manager总感觉反应很慢，有的时候扫不到网络，有的时候一个网络需要很长时间才能连接上，于是考虑换一套网络管理工具。<br />于是选择了 Systemd 内置的systemd-networkd。但是还得配上一个服务用来管理无线适配器，我选择了比较熟悉的iwd 和一个长得挺好看的 TUI 配置界面 <ahref="https://github.com/pythops/impala">impala</a>。</p><h2 id="安装软件包">安装软件包</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo pacman -S iwd impala</span><br></pre></td></tr></table></figure><p>为了防止没网找不到配置的文档，我们先不要停用/卸载 NetworkManager，等待万事俱备再切换。</p><h2 id="配置-systemd-networkd">配置 systemd-networkd</h2><p>编辑<code>/etc/systemd/network/25-wireless.network</code>，写入：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">[Match]</span><br><span class="line">Name=wl*</span><br><span class="line"></span><br><span class="line">[Network]</span><br><span class="line">DHCP=yes</span><br><span class="line">IgnoreCarrierLoss=3s</span><br></pre></td></tr></table></figure><p>无线适配器的名字应该形如<code>wlp2s0</code>，这里支持通配符，所以写的 <code>wl*</code></p><p>在使用指定 DNS 的时候需要配置 systemd-resolved，它默认的备用 DNS 是Cloudflare 家的，以防万一我们需要给他改成国内其他公共DNS。这里我改的是阿里。</p><p>把 <code>/etc/systemd/resolve.conf</code> 中 <code>[Resolve]</code>字段下的 <code>FallbackDNS</code> 改成自己想改的 DNS。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">FallbackDNS=223.5.5.5</span><br></pre></td></tr></table></figure><h2 id="启用服务">启用服务</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">systemctl disable NetworkManager --now</span><br><span class="line">systemctl enable systemd-networkd systemd-resolved iwd --now</span><br></pre></td></tr></table></figure><p>然后使用 impala 联网就好了。执行 <code>impala</code> 命令，Tab切换到选择网络的地方，按上下键选择网络，按空格输入密码即可。（如果你有DE 的话应该可以直接从 DE 的设置进行网络连接）</p><p>如果还有问题，进行撤销：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">systemdctl disable systemd-networkd systemd-resolved iwd --now</span><br><span class="line">systemctl enable NetworkManager --now</span><br></pre></td></tr></table></figure><p>找文档自己想办法去吧。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;a
href=&quot;https://wiki.archlinuxcn.org/wiki/Systemd-networkd&quot;&gt;参考文档&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这两天用 Network Manager
总感觉反应很慢，有的时候扫不到网络，有的时候一个网络需要很长时间才能连接</summary>
      
    
    
    
    <category term="Linux" scheme="http://blog.tibrella.space/categories/Linux/"/>
    
    
    <category term="网络" scheme="http://blog.tibrella.space/tags/%E7%BD%91%E7%BB%9C/"/>
    
  </entry>
  
  <entry>
    <title>KDE XWayland 体验指南</title>
    <link href="http://blog.tibrella.space/post/kde-xwayland-guide/"/>
    <id>http://blog.tibrella.space/post/kde-xwayland-guide/</id>
    <published>2024-02-16T14:52:32.000Z</published>
    <updated>2024-03-13T02:32:05.640Z</updated>
    
    <content type="html"><![CDATA[<p>KDE 现在支持 XWayland 分数缩放了，实际上本质是 Wayland应用随系统缩放，Xorg 应用自己处理缩放，最终显出来是“完美分数缩放”。</p><h2 id="基本设置">基本设置</h2><p>系统设置——显卡与显示器<br />然后调整成你想要的缩放率<br />旧式应用程序选择“由应用程序进行缩放”<br />调整完可能会糊，注销重进就行了</p><h2 id="常见问题">常见问题</h2><h3 id="fcitx-5-候选窗口闪烁">Fcitx 5 候选窗口闪烁</h3><p>阅读 <ahref="https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland#KDE_Plasma">Fcitx5 Wiki</a></p><p>简单来说：</p><ul><li>Fcitx 5 相关环境变量只需要设置一个<code>XMODIFIERS=@im=fcitx</code>，原先的 <code>GTK_IM_MODULE</code><code>QT_IM_MODULE</code> <code>SDL_IM_MODULE</code>不要设置，空的也不行，必须删掉。<br /></li><li>系统设置——输入设备——虚拟键盘，将虚拟键盘设置为 Fcitx 5。</li></ul><p>如果你环境变量写在 <code>/etc/environment</code>里的话需要重启才能生效。</p><p>然后你可能会遇到一些问题，诸如：</p><ul><li>Xorg 下的应用打字漏字母<br /></li><li>Xorg 应用输入法失效（也许会？）</li></ul><p>解决方案有两个：</p><ul><li>让软件使用 Wayland 模式运行，electron应用可以加选项：<code>--enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime</code><br /></li><li>为 Xorg 软件单独设置 <code>GTK_IM_MODULE</code><code>QT_IM_MODULE</code> <code>SDL_IM_MODULE</code> 这些环境变量</li></ul><h3 id="有些软件缩放不了">有些软件缩放不了</h3><p>有两种可能，一个是软件不听系统指挥，一个是软件根本不支持缩放（这种情况你换纯Xorg 也没用）</p><p>举一些例子：</p><h4 id="openutau">OpenUtau</h4><p>参考<a href="https://github.com/stakira/OpenUtau/issues/245">该issue</a></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">xrandr --listactivemonitors</span> </span><br><span class="line">Monitors: 1</span><br><span class="line"> 0: +*eDP-1 1920/360x1080/200+0+0  eDP-1</span><br></pre></td></tr></table></figure><p>然后你需要给你的 OpenUtau设置环境变量（单独或者全局你随意）<code>AVALONIA_SCREEN_SCALE_FACTORS='eDP-1=1.25'</code>，其中<code>eDP-1</code> 是上面命令输出的显示器名称，<code>1.25</code>是你想要的缩放比例</p><h4 id="reaper">Reaper</h4><p>参考<ahref="https://interfacinglinux.com/2024/01/08/installing-reaper-on-linux/">该文章</a></p><p><code>~/.config/REAPER/reaper.ini</code> 文件修改<code>ui_scale_auto=1</code> 为<code>ui_scale_auto=0</code>，<code>ui_scale=</code>后面填写你想要的缩放比例，如 <code>ui_scale=1.25</code></p><h3 id="字体不清晰">字体不清晰</h3><p><ahref="https://imagi.site/posts/%E4%B8%93%E6%B2%BB%E5%90%84%E7%A7%8D%E7%BC%A9%E6%94%BE%E7%96%91%E9%9A%BE%E6%9D%82%E7%97%87/#wayland-%E5%AD%97%E4%BD%93%E4%B8%8D%E5%A6%82-x11-%E6%B8%85%E6%99%B0">Imagi说的</a> 但是我没遇到过</p><p>他说</p><blockquote><p>把字体换成 Sarasa Gothic UI 能改善。</p></blockquote><p>他还说更新到 Plasma 6 也有改善。</p><h2 id="后记">后记</h2><p>原来我用了好久的系统装了 KDE之后缩放一直有问题，所有软件都得关一次再开才清晰。<br />不知道为什么，重装之后就没问题了。<br /><del>所以问题难以解决的时候放过自己，该换换，该重装重装</del></p>]]></content>
    
    
    <summary type="html">关于 KDE Plasma XWayland 模式下可能遇到的问题</summary>
    
    
    
    <category term="Linux" scheme="http://blog.tibrella.space/categories/Linux/"/>
    
    
    <category term="KDE Plasma" scheme="http://blog.tibrella.space/tags/KDE-Plasma/"/>
    
    <category term="XWayland" scheme="http://blog.tibrella.space/tags/XWayland/"/>
    
    <category term="Fcitx 5" scheme="http://blog.tibrella.space/tags/Fcitx-5/"/>
    
  </entry>
  
  <entry>
    <title>ST Link v2 刷 GNUK 记录</title>
    <link href="http://blog.tibrella.space/post/st-link-2-gnuk/"/>
    <id>http://blog.tibrella.space/post/st-link-2-gnuk/</id>
    <published>2023-11-08T08:44:43.000Z</published>
    <updated>2023-11-08T09:19:40.516Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言">前言</h2><p>看到了<ahref="https://ulyc.github.io/2021/01/13/2021%E5%B9%B4-%E7%94%A8%E6%9B%B4%E7%8E%B0%E4%BB%A3%E7%9A%84%E6%96%B9%E6%B3%95%E4%BD%BF%E7%94%A8PGP-%E4%B8%8A/">这篇文章</a>想搞 PGP 智能卡玩，但是 yubikey 死贵 <del>还涉及到某些傻逼政治问题</del>于是就想找找有无开源实现什么的。</p><p>然后就看见了 <ahref="https://www.liuyusong.cn/2023/04/05/diy-smartgpg/">smartcard的制作教程</a>，可惜能找到的便宜 javacard 都是需要读卡器的。</p><p>本来死心了，但是看见 <ahref="https://techie-s.work/posts/2021/04/homemade-gnuk/">ST-link v2 刷GNUK 教程</a> 突然就想折腾了。项目由 <spanclass="math inline">\(\stackrel{\textsf{Free Software Initiative ofJapan}}{\textsf{FSIJ}}\)</span> 维护。</p><p>这玩意比 yubikey 牛逼，支持最新的 x448 椭圆曲线加密算法（但是已经弃用RSA 了）</p><h2 id="材料准备">材料准备</h2><p>一般 ST-Link v2 及其仿品都只提供 SWDIO SWCLK 而无 RXD TXD这种直接串口就能刷的口，所以最好再买个 ST-Link v2当烧录器（买别的也行，主要是这个便宜），我买的是 PowerWriter Lite2。<del>本来想刷这个的，结果外壳都拆不开</del></p><p>所以：</p><ul><li>一个烧录器（ST-Link v2）</li><li>待烧录的 ST-Link v2（确保主控是 STM32F103C8T6 或STM32F103CBT6，GD32F103 一般不会在仿品里出现，网上有<ahref="https://blog.indexyz.me/diy-gnuk-token/">关于这款主控的刷写教程</a>），购买踩坑这里有<ahref="#fn1" class="footnote-ref" id="fnref1"role="doc-noteref"><sup>1</sup></a>。</li><li>四根杜邦线，有一端是公头用于连接待烧录的ST-Link，另一端连接烧录器，公母视情况而定</li><li>一台电脑</li></ul><p>我采用了 Arch Linux，其他系统编译固件的教程在这里有<a href="#fn2"class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></p><h2 id="为啥主控要求是这两个版本">为啥主控要求是这两个版本</h2><p>首先 GNUK 支持 STM32F103 系列。</p><p>然后 GNUK 需要 128KB 闪存容量。</p><p>最后 STM32F103C8T6 和 STM32F103CBT6 用了一套东西，后者官方给定容量128KB，前者虽然标了 64KB 但是实际上有 128KB 的空间可用。恰好，ST-Link 2官方使用的主控就是前者。仿品中可能出现后者。</p><p>GD32F103 能 pin2pin 替换 STM32F103自然也能用。但是需要换烧录方式。</p><h2 id="编译固件">编译固件</h2><h3 id="安装依赖">安装依赖</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pacman -S arm-none-eabi-gcc</span><br></pre></td></tr></table></figure><p>官网：In 2.5, we added GD32VF103 support. Please note that defaultlibc is now picolibc (instead of newlib).</p><p>老教程会教你安装 newlib，但是现在需要安装picolibc。但是这个东西不仅只在 aur 里，而且过期了。实测直接更改 pkgver也可以正常构建<ahref="https://github.com/picolibc/picolibc/releases">新版本</a>。</p><p>所以：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git clone https://aur.archlinux.org/arm-none-eabi-picolibc.git</span><br><span class="line">cd arm-none-eabi-picolibc.git</span><br><span class="line">vim PKGBUILD # 将 pkgver 改成最新版本号，可以在上面 picolibc 项目 release 页面看</span><br><span class="line">makepkg -sif</span><br></pre></td></tr></table></figure><h3 id="编译固件-1">编译固件</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">git clone --recursive https://salsa.debian.org/gnuk-team/gnuk/gnuk.git gnuk</span><br><span class="line">cd gnuk/src</span><br><span class="line">./configure --vidpid=234b:0000 --target=ST_DONGLE</span><br><span class="line">make build/gnuk-vidpid.bin</span><br></pre></td></tr></table></figure><p>然后 <code>build/gnuk-vidpid.bin</code>就是待烧录的固件文件，我们把它单独取出来待用。</p><h2 id="烧写">烧写</h2><h3 id="安装软件">安装软件</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pacman -S openocd inetutils</span><br></pre></td></tr></table></figure><p><code>inetutils</code> 用来提供 <code>telnet</code>支持。<code>putty</code> 用不了，不知道是不是不兼容 XWayland的问题。</p><h3 id="连接设备">连接设备</h3><p>ST-Link v2 连接系统板教程挺多的，直接搜就行。这个里面也有<ahref="#fn3" class="footnote-ref" id="fnref3"role="doc-noteref"><sup>3</sup></a>。</p><p>PW Lite 2 的话<ahref="https://docs.powerwriter.com/docs/faq/powerwriter/base/connection/#2131%E8%AE%BE%E5%A4%87%E4%B8%BApwlink2%E6%88%96%E8%80%85pwlink2-lite">官方文档</a>有接线教程。</p><p>我的是这样</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/654b44b5c458853aef5f7a34.webp"/></div></div><p>如果像<a href="#fn4" class="footnote-ref" id="fnref4"role="doc-noteref"><sup>4</sup></a>里面这样是非常好的，直接插进去随便固定一下就行。但是我不是这种情况。我买的ST-Link 只提供了四个焊盘！！！</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/654b44afc458853aef5f6c93.webp"/></div></div><p>这意味着你需要用调试排针凑合连接一下，连接刷写过程中需要用手按住！调试排针...就是四根杜邦线粘成一排😋</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/654b44a5c458853aef5f56c7.webp"/></div></div><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/654b44bac458853aef5f8470.webp"/></div></div><p>连接失败了好几次，刷写完了之后杜邦线的头都让我按歪了！</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/654b44aac458853aef5f60e5.webp"/></div></div><h3 id="配置-openocd">配置 openocd</h3><p>随便新建一个 <code>xxx.cfg</code>，内容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">telnet_port 4444</span><br><span class="line">source [find interface/stlink-v2.cfg]</span><br><span class="line">source [find target/stm32f1x.cfg]</span><br><span class="line">set WORKAREASIZE 0x10000</span><br></pre></td></tr></table></figure><p>STM32F103C8T6 需要这句<code>set WORKAREASIZE 0x10000</code>，这句使得程序可以刷写到额外的 64KB空间中。如果存储足够 128KB 就不用这句。</p><p>如果你像我一样用 PWLink 2，需要把<code>source [find interface/stlink-v2.cfg]</code> 换成<code>source [find interface/cmsis-dap.cfg]</code>（在一个求助帖看见的，地址找不到了）</p><p>连接好设备后<code>openocd -f ./xxx.cfg</code>，如果跑起来之后没报错没退出就说明你搞对了。</p><h3 id="刷写固件">刷写固件</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">telnet 127.0.0.1:4444</span><br></pre></td></tr></table></figure><p>然后输入以下指令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">stm32f1x unlock 0</span><br><span class="line">reset halt</span><br><span class="line">flash write_bank 0 ./src/build/gnuk-vidpid.bin 0</span><br><span class="line">stm32f1x lock 0</span><br><span class="line">reset halt</span><br></pre></td></tr></table></figure><p><code>./src/build/gnuk-vidpid.bin</code>这个是你刚才编译出来的固件</p><p>然后就没了，退出 telnet 和 openocd，断开设备连接就行。</p><h2 id="测试">测试</h2><p>插入你刷好的 GNUK 智能卡，输入</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gpg --card-status</span><br></pre></td></tr></table></figure><p>此时应该输出你的智能卡的详细信息。</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/654b4e1cc458853aef7d4c1f.webp"/></div></div><h2 id="刷坏了想重刷">刷坏了/想重刷</h2><p>看<a href="#fn5" class="footnote-ref" id="fnref5"role="doc-noteref"><sup>5</sup></a></p><hr /><p>以下文章我或多或少参考了一些，推荐阅读 <a href="#fn6"class="footnote-ref" id="fnref6" role="doc-noteref"><sup>6</sup></a><ahref="#fn7" class="footnote-ref" id="fnref7"role="doc-noteref"><sup>7</sup></a><a href="#fn8" class="footnote-ref"id="fnref8" role="doc-noteref"><sup>8</sup></a><a href="#fn9"class="footnote-ref" id="fnref9" role="doc-noteref"><sup>9</sup></a></p><p><ahref="https://blog.misaka4e21.science/gnuk-stm32f103-minimum-system/">这篇文章</a>讲述了 64KB 主控如何刷支持 64KB 的旧版 GNUK</p><section id="footnotes" class="footnotes footnotes-end-of-document"role="doc-endnotes"><hr /><ol><li id="fn1"><p>https://blog.dylanwu.space/2020/01/24/stm32-gnuk.html<ahref="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn2"><p>https://nx3d.org/gnuk-st-link-v2/<a href="#fnref2"class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn3"><p>https://nx3d.org/gnuk-st-link-v2/<a href="#fnref3"class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn4"><p>https://nx3d.org/gnuk-st-link-v2/<a href="#fnref4"class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn5"><p>https://nx3d.org/gnuk-st-link-v2/<a href="#fnref5"class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn6"><p>https://nx3d.org/gnuk-st-link-v2/<a href="#fnref6"class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn7"><p>https://blog.dylanwu.space/2020/01/24/stm32-gnuk.html<ahref="#fnref7" class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn8"><p>https://kgame.tw/gnupg/stm32-gnuk/<a href="#fnref8"class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn9"><p>https://techie-s.work/posts/2021/04/homemade-gnuk/<ahref="#fnref9" class="footnote-back" role="doc-backlink">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">年轻人的第一款 OpenPGP 智能卡，基于开源固件制作</summary>
    
    
    
    <category term="电子" scheme="http://blog.tibrella.space/categories/%E7%94%B5%E5%AD%90/"/>
    
    
    <category term="OpenPGP" scheme="http://blog.tibrella.space/tags/OpenPGP/"/>
    
  </entry>
  
  <entry>
    <title>春节十二响 题解</title>
    <link href="http://blog.tibrella.space/post/spring-12-biu/"/>
    <id>http://blog.tibrella.space/post/spring-12-biu/</id>
    <published>2023-09-20T14:05:20.000Z</published>
    <updated>2023-09-20T14:29:09.919Z</updated>
    
    <content type="html"><![CDATA[<h2 id="题意">题意</h2><p>给定 <span class="math inline">\(n\)</span>个程序，每个程序所需内存空间为 <spanclass="math inline">\(m_i\)</span>。</p><p>这些程序的调用关系形成一棵以 <span class="math inline">\(1\)</span>为根的树。</p><p>内存可以分为若干个段，每个段存放若干个程序，这些程序在调用关系树上不能为祖先-后代关系，每个段的大小为存放的程序所需空间的最大值。</p><p><span class="math inline">\(n\leq 2\times 10^5\)</span></p><h2 id="题解">题解</h2><p>做这种题要先从部分分入手。</p><p>它给了一个特殊性质：调用关系为一条链。我们把一个位置的段记到一个集合里，然后分类讨论：</p><ul><li>只有一个儿子：有依赖关系显然没法扔到一个段里面，所以把 <spanclass="math inline">\(m_i\)</span> 扔到集合里面。</li><li>两个儿子：一个容易想到的贪心策略，即把两个儿子的段集合里面最大的几个拿出来合并到一个段里面（即取<span class="math inline">\(\max\)</span>）。<br />我们用堆来维护这个集合，如果是两个儿子的情况，就取两个儿子的堆顶的 <spanclass="math inline">\(\max\)</span>放到当前节点的堆中，然后分别弹堆顶，重复这个过程直到一个儿子的堆为空，再把另一个儿子剩下的东西扔到父节点堆里面去就好了。</li></ul><p>然后我们发现可以用这个方法直接在树上合并，然后就成了一般情况。对于多个儿子的节点，我们将它们按照第二条规则两两合并，然后扔到父节点的集合里面。</p><p>（令 <span class="math inline">\(dep_i\)</span> 表示 <spanclass="math inline">\(i\)</span> 子树内最深的点的深度。）</p><p>发现每次都需要让 <span class="math inline">\(\max_{(u,v)}dep_v\)</span>个程序从一个堆进入到另一个堆里面，一个链带点叶子的结构就会卡到 <spanclass="math inline">\(\Theta(n^2 \log n)\)</span>。</p><p>然后我们发现，用到堆的<strong>根节点</strong>最大的特性的地方，仅有<strong>两个儿子堆顶取最大值</strong>的地方，除此以外增加复杂度的地方是“把另一个儿子剩下的东西扔到父节点堆里面去”的过程。</p><p>后面这个东西相当于合并两个堆，所以如果你已经用了配对堆/斐波那契堆这类<span class="math inline">\(\Theta(1)\)</span>合并的可并堆那你就可以先走了。</p><p>其实我们可以在<strong>两个儿子堆顶取最大值</strong>后扔到一个缓冲区里面，一个堆空了之后直接把缓冲区里面的东西全都扔到另一个非空堆中，这样就省去了合并的过程。这样的话一次合并的复杂度是<span class="math inline">\(\min(dep_x,dep_y)\)</span> 的。</p><p>分析总时间复杂度，发现每一次取 <spanclass="math inline">\(\max\)</span>实际上就是扔掉一个元素，然后每个元素只能被扔掉一次，总共被扔了 <spanclass="math inline">\(\Theta(n)\)</span> 次，加上堆的影响就成了 <spanclass="math inline">\(\Theta(n\log n)\)</span> 次。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;题意&quot;&gt;题意&lt;/h2&gt;
&lt;p&gt;给定 &lt;span class=&quot;math inline&quot;&gt;&#92;(n&#92;)&lt;/span&gt;
个程序，每个程序所需内存空间为 &lt;span
class=&quot;math inline&quot;&gt;&#92;(m_i&#92;)&lt;/span&gt;。&lt;/p&gt;
&lt;p&gt;这些程序的调</summary>
      
    
    
    
    <category term="题解" scheme="http://blog.tibrella.space/categories/%E9%A2%98%E8%A7%A3/"/>
    
    
    <category term="启发式合并" scheme="http://blog.tibrella.space/tags/%E5%90%AF%E5%8F%91%E5%BC%8F%E5%90%88%E5%B9%B6/"/>
    
    <category term="贪心" scheme="http://blog.tibrella.space/tags/%E8%B4%AA%E5%BF%83/"/>
    
  </entry>
  
  <entry>
    <title>Tauri 使用 WebviewWindow 新建窗口</title>
    <link href="http://blog.tibrella.space/post/tauri-react-jsx-dynamic-new-window/"/>
    <id>http://blog.tibrella.space/post/tauri-react-jsx-dynamic-new-window/</id>
    <published>2023-09-14T14:13:11.000Z</published>
    <updated>2023-12-19T16:12:28.224Z</updated>
    
    <content type="html"><![CDATA[<p>我使用的是 create-tauri-app 里面自带的 Vite + React 配置。</p><p>按照<ahref="https://tauri.app/zh-cn/v1/guides/features/multiwindow#%E5%9C%A8-javascript-%E4%B8%AD%E5%88%9B%E5%BB%BA%E7%AA%97%E5%8F%A3">官网说明</a>，你可能这样写了：</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">WebviewWindow</span> &#125; <span class="keyword">from</span> <span class="string">&quot;@tauri-apps/api/window&quot;</span>;</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">App</span>(<span class="params"></span>) &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="title function_">open</span> = (<span class="params"></span>) =&gt; &#123;</span><br><span class="line">        <span class="keyword">const</span> webview = <span class="keyword">new</span> <span class="title class_">WebviewWindow</span>(<span class="string">&quot;my-label&quot;</span>, &#123;</span><br><span class="line">            <span class="attr">url</span>: <span class="string">&quot;https://www.tibrella.space/&quot;</span>,</span><br><span class="line">        &#125;);</span><br><span class="line">        webview.<span class="title function_">once</span>(<span class="string">&quot;tauri://created&quot;</span>, <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;Failed&quot;</span>);</span><br><span class="line">        &#125;);</span><br><span class="line">        webview.<span class="title function_">once</span>(<span class="string">&quot;tauri://error&quot;</span>, <span class="keyword">function</span> (<span class="params">e</span>) &#123;</span><br><span class="line">            <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">&quot;Failed&quot;</span>);</span><br><span class="line">        &#125;);</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">return</span> <span class="language-xml"><span class="tag">&lt;<span class="name">button</span> <span class="attr">onClick</span>=<span class="string">&#123;open&#125;</span>&gt;</span><span class="tag">&lt;/<span class="name">button</span>&gt;</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是你又发现，咋运行都跑不起来，然后控制台里面只有你自己设置好的<code>console.log("Failed");</code>，其他报错一概没有。</p><p>然后搜了好久，我找到了<ahref="https://ithelp.ithome.com.tw/m/articles/10304587">一篇 Tauri教程</a>，里面提到了需要更改<code>/src-tauri/tauri.conf.json</code>，但是官网文档完全没有提到这件事情......</p><p>最终解决方案：<code>/src-tauri/tauri.conf.json</code> 中<code>"allowlist"</code> 字段添加<code>"window": &#123; "create": true &#125;</code> 即可。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;build&quot;: &#123;</span><br><span class="line">        &quot;beforeDevCommand&quot;: &quot;pnpm dev&quot;,</span><br><span class="line">        &quot;beforeBuildCommand&quot;: &quot;pnpm build&quot;,</span><br><span class="line">        &quot;devPath&quot;: &quot;http://localhost:1420&quot;,</span><br><span class="line">        &quot;distDir&quot;: &quot;../dist&quot;,</span><br><span class="line">        &quot;withGlobalTauri&quot;: false</span><br><span class="line">    &#125;,</span><br><span class="line">    &quot;package&quot;: &#123;</span><br><span class="line">        &quot;productName&quot;: &quot;name&quot;,</span><br><span class="line">        &quot;version&quot;: &quot;0.0.0&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &quot;tauri&quot;: &#123;</span><br><span class="line">        &quot;allowlist&quot;: &#123;</span><br><span class="line">            &quot;all&quot;: false,</span><br><span class="line">            &quot;shell&quot;: &#123;</span><br><span class="line">                &quot;all&quot;: false,</span><br><span class="line">                &quot;open&quot;: true</span><br><span class="line"><span class="deletion">-            &#125;</span></span><br><span class="line"><span class="addition">+            &#125;,</span></span><br><span class="line"><span class="addition">+            &quot;window&quot;: &#123;</span></span><br><span class="line"><span class="addition">+                &quot;create&quot;: true</span></span><br><span class="line"><span class="addition">+            &#125;</span></span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;bundle&quot;: &#123;</span><br><span class="line">            &quot;active&quot;: true,</span><br><span class="line">            &quot;targets&quot;: &quot;all&quot;,</span><br><span class="line">            &quot;identifier&quot;: &quot;com.tauri.dev&quot;,</span><br><span class="line">            &quot;icon&quot;: [</span><br><span class="line">                &quot;icons/32x32.png&quot;,</span><br><span class="line">                &quot;icons/128x128.png&quot;,</span><br><span class="line">                &quot;icons/128x128@2x.png&quot;,</span><br><span class="line">                &quot;icons/icon.icns&quot;,</span><br><span class="line">                &quot;icons/icon.ico&quot;</span><br><span class="line">            ]</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;security&quot;: &#123;</span><br><span class="line">            &quot;csp&quot;: null</span><br><span class="line">        &#125;,</span><br><span class="line">        &quot;windows&quot;: [</span><br><span class="line">            &#123;</span><br><span class="line">                &quot;fullscreen&quot;: false,</span><br><span class="line">                &quot;resizable&quot;: true,</span><br><span class="line">                &quot;title&quot;: &quot;name&quot;,</span><br><span class="line">                &quot;width&quot;: 800,</span><br><span class="line">                &quot;height&quot;: 600</span><br><span class="line">            &#125;</span><br><span class="line">        ]</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修改之后就可以按照最开始提到的方法动态创建新窗口了。</p>]]></content>
    
    
    <summary type="html">在学习 Tauri 时一直新建不了窗口...结果新建不了的原因是配置问题。</summary>
    
    
    
    <category term="Tauri" scheme="http://blog.tibrella.space/categories/Tauri/"/>
    
    
  </entry>
  
  <entry>
    <title>如何在使用 Linux 环境的比赛现场配置一个好用的 Visual Studio Code？</title>
    <link href="http://blog.tibrella.space/post/noi-vscode/"/>
    <id>http://blog.tibrella.space/post/noi-vscode/</id>
    <published>2023-09-13T14:21:23.000Z</published>
    <updated>2023-10-10T02:18:13.376Z</updated>
    
    <content type="html"><![CDATA[<div class="tag-plugin colorful note" color="cyan"><div class="body"><p>本文的 vscode 配置无需任何扩展，可以直接在纯 vscode/code-oss/vscodium端使用。</p></div></div><div class="tag-plugin colorful note" color="cyan"><div class="body"><p>视频采用了 AV1 编码 + webm 容器封装，如果观看不了请使用最新版chromium/firefox，或者下载后观看。</p></div></div><div class="tag-plugin colorful note" color="warning"><div class="body"><p>指令有经过修改，请以下文给出的为准。<br /><del>懒得重新录视频了</del></p></div></div><p><ahref="/assets/noi-vscode.webm">配置演示视频，包含快捷键演示</a></p><p>你会写 JSON吗？不会写的话你只需要知道这玩意需要一大堆大括号就行了。</p><p>直接给你一套配置文件：</p><p>位置：<code>.vscode/tasks.json</code>，项目根目录下。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tasks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;My Task&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;shell&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;dirname $&#123;file&#125;/.. &amp;&amp; g++ $&#123;file&#125; -o out.exe -O2 &amp;&amp; ./out.exe &amp;&amp; rm ./out.exe&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;problemMatcher&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>程序配置文件目录下 <code>keybindings.json</code>：<br /><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;F4&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;workbench.action.tasks.runTask&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;args&quot;</span><span class="punctuation">:</span> <span class="string">&quot;My Task&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure> <code>dirname + 文件路径</code>可以提取出这个文件路径的文件夹位置。然后 bash有个特性就是输入文件夹名称相当于 <code>cd</code> +这个文件夹名称。作用是进入 C++ 源文件目录。</p><p><code>$&#123;file&#125;</code> 是文件名变量，<code>&amp;&amp;</code> 是<ahref="/post/noi-linux-2-guide/#关系运算符">关系运算符</a>。</p><p><code>args</code> 和 <code>label</code>是任务的名称，随便取一个就行。这里采用了默认的 "My Task"。</p><p><code>command</code> 部分为了兼容 Windows 而加上了 <code>.exe</code>扩展名，对 Linux 没有影响。</p><p><code>problemMatcher</code> 行可以去掉，这行是 VSCode默认配置带上的。</p><p><code>F4</code>可以改成任意其他键，具体看视频，添加键绑定时按照要求按下想要设置的快捷键组合即可。</p><p>非常短，大部分有自动补全。</p>]]></content>
    
    
    <summary type="html">Linux 里没有 Dev C++？Sublime 配置的自定义性差？Visual Studio Code 不会配置？本文解决你的以上所有痛点，为你带来一个干净快捷的程序设计竞赛编辑环境！</summary>
    
    
    
    <category term="杂文" scheme="http://blog.tibrella.space/categories/%E6%9D%82%E6%96%87/"/>
    
    
  </entry>
  
  <entry>
    <title>手把手带你实现跳表</title>
    <link href="http://blog.tibrella.space/post/skip-list/"/>
    <id>http://blog.tibrella.space/post/skip-list/</id>
    <published>2023-08-21T12:28:31.000Z</published>
    <updated>2023-11-08T08:45:45.830Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引入">引入</h2><p>跳表（跳跃表）能够维护一个数的集合（作用类似普通平衡树），查找时间复杂度为<span class="math inline">\(\Theta(\logn)\)</span>，与平衡树一样基于链表结构。由于不需要平衡树那么多旋转什么的，所以效率比较高，一般认为性能能打红黑树。除此以外，链表的特性使它能够以线性时间遍历某个子段。Redis的有序集合就是用跳表实现的。</p><p>更简单来说，跳表是一个支持 <span class="math inline">\(\Theta(\logn)\)</span> 时间随机访问的链表。</p><h2 id="定义">定义</h2><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64e35f5c661c6c8e548c71fb"/></div></div><p>上面这个东西叫链表。</p><p>我们知道，链表只支持线性时间访问，所以不能二分。我们如果想维护一个有序序列的话，虽然插入删除很快，但是找到一个值对应的位置很慢。</p><p>我们又知道，链表的访问形式实际上是一个一个遍历，而它有 <spanclass="math inline">\(n\)</span> 个元素，这是它 <spanclass="math inline">\(\Theta(n)\)</span> 复杂度随机访问的根源所在。</p><p>那我们是不是可以给链表精简一下呢？比如说，我给链表多加几层，每层减少一半的元素，像这样：</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64e362b4661c6c8e549b0cc6"/></div></div><p>（蓝色方框括起来的是一个节点，实现的时候我们不需要把上面几层显式地建出来，只需要创建对应层的指针即可。）</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64e36359661c6c8e549d534b"/></div></div><p>这样的话，我就能像上图这样找到 <spanclass="math inline">\(78\)</span> 这个节点了。</p><p>橙色路径是原有路径，走了 <span class="math inline">\(4\)</span>次。而上面的绿色路径只走了 <span class="math inline">\(\log_2 4 =2\)</span> 个次。好好好，那我这样建的话，我就能在链表上二分了！<br />实际上这个东西叫完美跳表。</p><p>跳表分两种，一种是上面的完美跳表（暂且这样叫）。这个东西最大的特点就是过于理想化了。如果加上插入删除的话，维护对应层的指针就太难了，每次都得更新。</p><p>另一种是基于随机化的跳表。<br />要随机化的东西叫做 <spanclass="math inline">\(level\)</span>。一个跳表节点的 <spanclass="math inline">\(level\)</span>，代表着这个节点同时存在于 <spanclass="math inline">\(1 \sim level\)</span>层的链表中。比如说，上图的值为 <span class="math inline">\(1\)</span>的节点 <span class="math inline">\(level = 3\)</span>，值为 <spanclass="math inline">\(23\)</span> 的节点 <spanclass="math inline">\(level = 1\)</span>。<br />取 <span class="math inline">\(level\)</span> 的方式类似于抛硬币，计算<span class="math inline">\(level\)</span> 时，如果硬币正面朝上，就<span class="math inline">\(+1\)</span>并继续抛；如果反面朝上，则停止。通过这样定下节点 <spanclass="math inline">\(level\)</span>的跳表就是我们今天要实现的跳表。</p><h2 id="基本实现">基本实现</h2><p>为了方便演示，这里就不再封装跳表了，其实跟着教程一边走一边封装也是可行的。</p><h3 id="一些变量">一些变量</h3><p><code>int level</code> 记录跳表的最高 level。</p><p><code>Node head</code>为了防止过多的边界的分类讨论，建立一个空结点当头节点。</p><h3 id="节点">节点</h3><p>动态分配内存太慢了，如果用动态分配的，我还不如直接 STL。</p><p>所以开好数组作为预分配的空间，然后我们可以开一个指针记录分配到了哪一个位置。需要创建新节点的时候直接返回一个<code>++tot</code> 即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">    <span class="type">int</span> key, level;</span><br><span class="line">    Node* nxt[MAX_LEVEL];</span><br><span class="line">&#125; space[N], *tot = space;</span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="warning"><div class="title"><strong>注意</strong></div><div class="body"><p>此处 <code>level</code> 的含义是：该节点存在于 0 到<code>level</code>-1 层的链表中，与前文定义 1 到 <code>level</code>不同。</p></div></div><p>垃圾回收可以自己实现，待会的整体演示里面会放。</p><p>分配一个新节点空间，返回新节点的指针：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> new_node() (++tot)</span></span><br></pre></td></tr></table></figure><p>创建一个值为 <code>key</code> 高度为 <code>level</code> 的节点：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Node* <span class="title">create_node</span><span class="params">(<span class="type">int</span> level, <span class="type">int</span> key)</span> </span>&#123;</span><br><span class="line">    Node* res = <span class="built_in">new_node</span>();</span><br><span class="line">    res-&gt;level = level;</span><br><span class="line">    res-&gt;key = key;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="随机生成-level">随机生成 level</h3><p>前面说了，是抛硬币。</p><p>所以我们可以直接借用一些<ahref="https://oi.wiki/misc/random/#%E9%A2%84%E5%AE%9A%E4%B9%89%E9%9A%8F%E6%9C%BA%E6%95%B0%E7%94%9F%E6%88%90%E5%99%A8">随机数生成器</a><ahref="#fn1" class="footnote-ref" id="fnref1"role="doc-noteref"><sup>1</sup></a>。</p><p>然后我们肯定不能让层数无限大啊，所以需要设置一个<code>MAX_LEVEL</code> 作为最大层数。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> MAX_LEVEL 12</span></span><br><span class="line"></span><br><span class="line">std::random_device seed;</span><br><span class="line"><span class="function">std::minstd_rand <span class="title">rng</span><span class="params">(seed())</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">random_level</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="type">int</span> res = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (res &lt; MAX_LEVEL &amp;&amp; (<span class="built_in">rng</span>() &amp; <span class="number">1</span>)) &#123;</span><br><span class="line">        ++res;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="插入节点">插入节点</h3><p>声明：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">insert</span><span class="params">(<span class="type">int</span> key)</span></span>;</span><br></pre></td></tr></table></figure><p>三步走：找到需要插入的位置，插入节点，更新对应 level 的链表。</p><p>首先我们直接从高 level 开始跳，跳不了了就跳低一级的 level即可找到需要插入的位置。<br />同时记录每一个 level的当前位置之前的节点。（即可能需要更新后向指针的节点）。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Node *cur = head;  <span class="comment">// current</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> lev = level - <span class="number">1</span>; lev != <span class="number">-1</span>; --lev) &#123;</span><br><span class="line">    <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key)</span><br><span class="line">        cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">    update[lev] = cur;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>细节：可能当前 level 还没有到跳表可能达到的最高level，但是当前这个节点随机到的 level 值在这两个数中间，所以需要将<code>level</code> 到 <code>MAX_LEVEL</code> 这段补全为<code>head</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> lev = <span class="built_in">random_level</span>(); <span class="comment">// 当前节点的 level 值</span></span><br><span class="line"><span class="keyword">if</span> (lev &gt; level) &#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = level; i &lt; lev; ++i)</span><br><span class="line">        update[i] = head;</span><br><span class="line"></span><br><span class="line">    level = lev;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>创建节点：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cur = <span class="built_in">create_node</span>(lev, key);</span><br></pre></td></tr></table></figure><p>执行插入操作，即对于每一层链表，更新前一个节点的指针，并让当前节点的后向指针指向后一个节点。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = lev - <span class="number">1</span>; i &gt; <span class="number">-1</span>; --i) &#123;  <span class="comment">// 普通链表插入操作</span></span><br><span class="line">    cur-&gt;nxt[i] = update[i]-&gt;nxt[i];</span><br><span class="line">    update[i]-&gt;nxt[i] = cur;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="删除节点">删除节点</h3><p>和插入类似。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">erase</span><span class="params">(<span class="type">int</span> key)</span> </span>&#123;</span><br><span class="line">    nodePointer cur = head;  <span class="comment">// current</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> lev = level - <span class="number">1</span>; lev != <span class="number">-1</span>; --lev) &#123;</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key)</span><br><span class="line">            cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">        update[lev] = cur;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cur = cur-&gt;nxt[<span class="number">0</span>];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; level; ++i)</span><br><span class="line">        <span class="keyword">if</span> (update[i]-&gt;nxt[i] == cur)</span><br><span class="line">            update[i]-&gt;nxt[i] = cur-&gt;nxt[i];</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (level &gt; <span class="number">1</span> &amp;&amp; !head-&gt;nxt[level - <span class="number">1</span>])  <span class="comment">// 更新当前最大层数</span></span><br><span class="line">        --level;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>额外要注意的是，可能跳表的最高层就这一个节点，删了就没了，所以要判断并更新最大层数。</p><h3 id="查找结点">查找结点</h3><p>实际上上面两个函数的第一部分就相当于查找。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">find</span><span class="params">(<span class="type">int</span> key)</span> </span>&#123;</span><br><span class="line">    Node* cur;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> lev = level - <span class="number">1</span>; lev &gt; <span class="number">-1</span>; --lev)</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key)</span><br><span class="line">            cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> cur-&gt;nxt[<span class="number">0</span>] ? cur-&gt;nxt[<span class="number">0</span>]-&gt;key == key : <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>查找前驱后继的方法也差不多，前驱就是查找后直接返回 <code>cur</code>而不是 <code>cur-&gt;nxt[0]</code>，后继可以跳到<code>cur-&gt;nxt[lev]-&gt;key &lt;= key</code> 的位置之后返回<code>cur-&gt;nxt[0]-&gt;key</code>。最后的代码中有体现。</p><h2 id="随机访问">随机访问</h2><p>上面其实已经实现了跳表的基本功能了，但是显然，目前实现的功能都可以用平衡树替代，而且平衡树还能够按照数的排名查询。</p><p>由于维护的是有序序列，所以按照数的排名查询相当于随机访问。</p><p>接下来我们来实现跳表的随机访问。具体方法：维护每个后向指针的“跨度”（span），即它跳了几个节点。</p><div class="tag-plugin colorful note" color="cyan"><div class="title"><strong>形式化定义</strong></div><div class="body"><p>设指针 <span class="math inline">\(ptr\)</span> 从第 <spanclass="math inline">\(a\)</span> 个节点指向第 <spanclass="math inline">\(b\)</span> 个节点，则 <spanclass="math inline">\(ptr\)</span> 的跨度为 <spanclass="math inline">\(b-a\)</span></p></div></div><p>除此以外，我们还需要维护一个长度 <code>length</code>，在每次<code>erase</code> 和 <code>insert</code> 的时候加减一下就好了。</p><h3 id="重写智能指针">重写智能指针</h3><p><del>啥是智能指针？不太清楚，但是我感觉维护一个 span的指针实在太智能了！</del></p><p>我们需要给指针记录一个“跨度”，那就维护一个结构体作为指针，存原来的裸指针和跨度。</p><p>总的来说，需要构造函数并重载一个运算符，一个类型转换。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">nodePointer</span> &#123;</span><br><span class="line">    <span class="type">int</span> span;</span><br><span class="line">    Node* pointer;</span><br><span class="line">    <span class="built_in">nodePointer</span>() &#123;</span><br><span class="line">        <span class="keyword">this</span>-&gt;pointer = <span class="literal">nullptr</span>; <span class="comment">// 构造函数，将指针初始化为空</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">nodePointer</span>(Node* node) &#123;</span><br><span class="line">        <span class="keyword">this</span>-&gt;pointer = node;  <span class="comment">// 如果提供了指针就用提供的</span></span><br><span class="line">    &#125;</span><br><span class="line">    Node* <span class="keyword">operator</span>-&gt;() &#123;</span><br><span class="line">        <span class="keyword">return</span> pointer; <span class="comment">// 指针原有的箭头运算符，访问 nodePointer-&gt;x 相当于访问 pointer-&gt;x</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">operator</span> Node*() <span class="type">const</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> pointer; <span class="comment">// 智能指针转换为裸指针，直接返回 pointer 就好了</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><div class="tag-plugin colorful note" color="warning"><div class="title"><strong>注意</strong></div><div class="body"><p>不要在所有地方都使用<code>nodePointer</code>，我们只在需要维护跨度的地方使用就好了。<br />编写代码时一定要注意类型的使用，比如说 <code>unsigned</code><code>long</code> 不应乱用之类的。如果错误地更新<code>span</code>，而你滥用了<code>nodePointer</code>，可能就没那么容易找到问题了。</p><p>博主因为滥用 <code>unsigned</code>，跳表调了两天多。</p></div></div><p>需要维护跨度的地方只有跳转用的指针，即 <code>nxt[]</code>。</p><details class="tag-plugin colorful folding" color="cyan" child="codeblock"><summary><span>更改后的代码</span></summary><div class="body"><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">    <span class="type">int</span> key, level;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">nodePointer</span> &#123;</span><br><span class="line">        <span class="type">int</span> span;</span><br><span class="line">        Node* pointer;</span><br><span class="line">        <span class="built_in">nodePointer</span>() &#123;</span><br><span class="line">            <span class="keyword">this</span>-&gt;pointer = <span class="literal">nullptr</span>; <span class="comment">// 构造函数，将指针初始化为空</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">nodePointer</span>(Node* node) &#123;</span><br><span class="line">            <span class="keyword">this</span>-&gt;pointer = node;  <span class="comment">// 如果提供了指针就用提供的</span></span><br><span class="line">        &#125;</span><br><span class="line">        Node* <span class="keyword">operator</span>-&gt;() &#123;</span><br><span class="line">            <span class="keyword">return</span> pointer; <span class="comment">// 指针原有的箭头运算符，访问 nodePointer-&gt;x 相当于访问 pointer-&gt;x</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">operator</span> Node*() <span class="type">const</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> pointer; <span class="comment">// 智能指针转换为裸指针，直接返回 pointer 就好了</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    nodePointer nxt[MAX_LEVEL];</span><br><span class="line">&#125; space[N];</span><br><span class="line"><span class="keyword">using</span> nodePointer = <span class="keyword">typename</span> Node::nodePointer; <span class="comment">// 为了方便书写，缩一下</span></span><br></pre></td></tr></table></figure></div></details><h3 id="重写插入函数">重写插入函数</h3><p>开一个数组记录每一层“上一个节点”的位置（利用跨度）。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> lst_pos[MAX_LEVEL];</span><br></pre></td></tr></table></figure><p>然后在函数开头找位置的时候顺便把它处理出来：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> lev = level - <span class="number">1</span>; lev &gt; <span class="number">-1</span>; --lev) &#123;</span><br><span class="line">    <span class="comment">// 更新 lst_pos</span></span><br><span class="line">    <span class="keyword">if</span> (lev == level - <span class="number">1</span>)</span><br><span class="line">        lst_pos[lev] = <span class="number">0</span>; <span class="comment">// 默认得是 0</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        lst_pos[lev] = lst_pos[lev + <span class="number">1</span>]; <span class="comment">// 否则从上一层继承</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key) &#123;</span><br><span class="line">        lst_pos[lev] += cur-&gt;nxt[lev].span; <span class="comment">// 更新</span></span><br><span class="line">        cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">    &#125;</span><br><span class="line">    update[lev] = cur;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>插入的时候计算一下就好了。如图：</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64ecae10661c6c8e54e969bb.webp"/></div></div><p>然后 <code>level</code> 大于这个节点的指针跨度要加一。</p><p>结合代码理解。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; lev; ++i) &#123;  <span class="comment">// 普通链表插入操作</span></span><br><span class="line">    cur-&gt;nxt[i] = update[i]-&gt;nxt[i];</span><br><span class="line">    update[i]-&gt;nxt[i].pointer = cur; <span class="comment">// 这里不要直接让 nxt[i] = cur，因为后面还要用到 nxt[i].span</span></span><br><span class="line">    cur-&gt;nxt[i].span = update[i]-&gt;nxt[i].span - (lst_pos[<span class="number">0</span>] - lst_pos[i]); <span class="comment">// lst_pos[0] 实际上就是上一个节点的位置</span></span><br><span class="line">    update[i]-&gt;nxt[i].span = lst_pos[<span class="number">0</span>] - lst_pos[i] + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = lev; i &lt; level; ++i) ++update[i]-&gt;nxt[i].span; <span class="comment">// 维护高于新节点的指针的跨度</span></span><br></pre></td></tr></table></figure><p>别忘了 <code>++length</code>。</p><h3 id="重写删除函数">重写删除函数</h3><p>把要删掉的指针的 <code>span</code> 加起来赋值给新指针就好了。</p><p>和 <code>insert</code> 一样，别忘记比当前节点高的指针跨度要<code>-1</code>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; level; ++i)</span><br><span class="line">    <span class="keyword">if</span> (update[i]-&gt;nxt[i] == cur)</span><br><span class="line">        update[i]-&gt;nxt[i].pointer = cur-&gt;nxt[i], update[i]-&gt;nxt[i].span += cur-&gt;nxt[i].span - <span class="number">1</span>;  <span class="comment">// 跨度直接扔给前面那个指针就行了</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        --update[i]-&gt;nxt[i].span;</span><br></pre></td></tr></table></figure><h3 id="随机访问按照排名查询">随机访问（按照排名查询）</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">findrk</span><span class="params">(<span class="type">int</span> k)</span> </span>&#123;</span><br><span class="line">    <span class="built_in">assert</span>(k &lt;= length &amp;&amp; k); <span class="comment">// k 不满足要求就异常</span></span><br><span class="line">    Node* cur = head;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> lev = level - <span class="number">1</span>; lev &gt; <span class="number">-1</span> ; --lev)</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; k - cur-&gt;nxt[lev].span &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            k -= cur-&gt;nxt[lev].span;</span><br><span class="line">            cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">        &#125;</span><br><span class="line">    <span class="keyword">return</span> cur-&gt;nxt[<span class="number">0</span>]-&gt;key;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="微调">微调</h3><p>我们可以对 <code>MAX_LEVEL</code> 和选取 <code>level</code>的概率进行微调。</p><p>比如说下面的普通平衡树代码，把选取层数的 <spanclass="math inline">\(p\)</span> 改成了 <spanclass="math inline">\(\frac 14\)</span>，即<code>(rng() &amp; 1) &amp;&amp; (rng() &amp; 1)</code>，<code>MAX_LEVEL</code>设为了 <spanclass="math inline">\(7\)</span>，经测试这样比较快，在无快读不开 O2的情况下吊打 Splay/FHQ/Treap，加了快读 O2之后不知道为啥跑不过我之前写的指针 FHQ 了。另外数组 Treap始终被吊打。<del>这就是指针带给我的自信</del></p><h2 id="后记">后记</h2><p>实际上跳表最大的优点是能够顺序访问，这点是很多平衡树做不到的，FHQTreap 分裂区间之后中序遍历是可以的，但是常数太大。</p><p><strong>等我把跳表模板题搞出来，他们都得死！</strong></p><h2 id="完整代码">完整代码</h2><p>含类型泛化和封装成类。</p><p>另外实现了一些输入输出操作，自己看应该能看懂了。</p><details class="tag-plugin colorful folding" color="cyan" child="codeblock"><summary><span>代码</span></summary><div class="body"><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;random&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;cassert&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;cstdlib&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> std::cin;</span><br><span class="line"><span class="keyword">using</span> std::cout;</span><br><span class="line"></span><br><span class="line">std::random_device seed;</span><br><span class="line"><span class="function">std::minstd_rand <span class="title">rng</span><span class="params">(seed())</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> N 106</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MAX_LEVEL 32</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> i32 = <span class="type">signed</span> <span class="type">int</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">skiplist</span> &#123;</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">    i32 level;</span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">        T key;</span><br><span class="line">        i32 level; <span class="comment">// 千万的别用 unsigned</span></span><br><span class="line">        <span class="keyword">struct</span> <span class="title class_">nodePointer</span> &#123;</span><br><span class="line">            i32 span; <span class="comment">// 这个也千万他妈的别用 unsigned</span></span><br><span class="line">            Node* pointer;</span><br><span class="line">            <span class="built_in">nodePointer</span>() &#123;</span><br><span class="line">                <span class="keyword">this</span>-&gt;pointer = <span class="literal">nullptr</span>; <span class="comment">// 构造函数，将指针初始化为空</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">nodePointer</span>(Node* node) &#123;</span><br><span class="line">                <span class="keyword">this</span>-&gt;pointer = node;  <span class="comment">// 如果提供了指针就用提供的</span></span><br><span class="line">            &#125;</span><br><span class="line">            Node* <span class="keyword">operator</span>-&gt;() &#123;</span><br><span class="line">                <span class="keyword">return</span> pointer; <span class="comment">// 指针原有的箭头运算符，访问 nodePointer-&gt;x 相当于访问 pointer-&gt;x</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">operator</span> Node*() <span class="type">const</span> &#123;</span><br><span class="line">                <span class="keyword">return</span> pointer; <span class="comment">// 智能指针转换为裸指针，直接返回 pointer 就好了</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">        nodePointer nxt[MAX_LEVEL];</span><br><span class="line">    &#125; space[N];</span><br><span class="line">    i32 bintop;</span><br><span class="line">    <span class="keyword">using</span> nodePointer = <span class="keyword">typename</span> Node::nodePointer;</span><br><span class="line">    Node *head, *tail, *tot, *rubbin[N / <span class="number">4</span> * <span class="number">3</span>];</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> new_node() (bintop ? rubbin[bintop--] : ++tot)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> del_node(x) (rubbin[++bintop] = (x))</span></span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">create_node</span><span class="params">(<span class="type">const</span> i32&amp; level, <span class="type">const</span> T&amp; key)</span> </span>&#123;</span><br><span class="line">        Node* res = <span class="built_in">new_node</span>();</span><br><span class="line">        res-&gt;level = level;</span><br><span class="line">        res-&gt;key = key;</span><br><span class="line">        <span class="keyword">return</span> res;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">i32 <span class="title">random_level</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        i32 res = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">while</span> (res &lt; MAX_LEVEL &amp;&amp; (<span class="built_in">rng</span>() &amp; <span class="number">1</span>)) &#123;</span><br><span class="line">            ++res;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> res;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    Node* update[MAX_LEVEL];</span><br><span class="line">    i32 lst_pos[MAX_LEVEL + <span class="number">1</span>];  <span class="comment">// 每个 level 遍历到的最后一个元素的位置</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="built_in">skiplist</span>() &#123;</span><br><span class="line">        tail = <span class="literal">nullptr</span>;</span><br><span class="line">        level = <span class="number">0</span>;</span><br><span class="line">        head = tot = space;</span><br><span class="line">        bintop = <span class="number">0</span>;</span><br><span class="line">        length = <span class="number">0</span>;</span><br><span class="line">        lst_pos[MAX_LEVEL] = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; MAX_LEVEL; ++i)</span><br><span class="line">            head-&gt;nxt[i] = <span class="literal">nullptr</span>, head-&gt;nxt[i].span = <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">insert</span><span class="params">(<span class="type">const</span> T&amp; key)</span> </span>&#123;</span><br><span class="line">        Node* cur = head;  <span class="comment">// current</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (i32 lev = level - <span class="number">1</span>; lev &gt; <span class="number">-1</span>; --lev) &#123;</span><br><span class="line">            <span class="comment">// 更新 lst_pos，这里由于已经把 lst_pos[MAX_LEVEL] 设为 0 了，所以不需要像上文一样特判</span></span><br><span class="line">            lst_pos[lev] = lst_pos[lev + <span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key) &#123;</span><br><span class="line">                lst_pos[lev] += cur-&gt;nxt[lev].span;</span><br><span class="line">                cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">            &#125;</span><br><span class="line">            update[lev] = cur;</span><br><span class="line">        &#125;</span><br><span class="line">        i32 lev = <span class="built_in">random_level</span>();</span><br><span class="line">        <span class="keyword">if</span> (lev &gt; level) &#123;</span><br><span class="line">            <span class="keyword">for</span> (i32 i = level; i &lt; lev; ++i) &#123;</span><br><span class="line">                update[i] = head;</span><br><span class="line">                update[i]-&gt;nxt[i].span = length;  <span class="comment">// 这层都还没有节点，直接从 head 指向尾部（nullptr），跨度为 length</span></span><br><span class="line">            &#125;</span><br><span class="line">            level = lev;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        cur = <span class="built_in">create_node</span>(lev, key);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; lev; ++i) &#123;  <span class="comment">// 普通链表插入操作</span></span><br><span class="line">            cur-&gt;nxt[i] = update[i]-&gt;nxt[i];</span><br><span class="line">            update[i]-&gt;nxt[i].pointer = cur; <span class="comment">// 这里不要直接让 nxt[i] = cur，因为后面还要用到 nxt[i].span</span></span><br><span class="line">            cur-&gt;nxt[i].span = update[i]-&gt;nxt[i].span - (lst_pos[<span class="number">0</span>] - lst_pos[i]); <span class="comment">// lst_pos[0] 实际上就是上一个节点的位置</span></span><br><span class="line">            update[i]-&gt;nxt[i].span = lst_pos[<span class="number">0</span>] - lst_pos[i] + <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">for</span> (i32 i = lev; i &lt; level; ++i) ++update[i]-&gt;nxt[i].span;</span><br><span class="line"></span><br><span class="line">        ++length;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">erase</span><span class="params">(<span class="type">const</span> T&amp; key)</span> </span>&#123;</span><br><span class="line">        Node* cur = head;  <span class="comment">// current</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (i32 lev = level - <span class="number">1</span>; lev != <span class="number">-1</span>; --lev) &#123;</span><br><span class="line">            <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key)</span><br><span class="line">                cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">            update[lev] = cur;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        cur = cur-&gt;nxt[<span class="number">0</span>];</span><br><span class="line">        <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; level; ++i)</span><br><span class="line">            <span class="keyword">if</span> (update[i]-&gt;nxt[i] == cur)</span><br><span class="line">                update[i]-&gt;nxt[i].pointer = cur-&gt;nxt[i].pointer, update[i]-&gt;nxt[i].span += cur-&gt;nxt[i].span - <span class="number">1</span>;  <span class="comment">// 跨度直接扔给前面那个指针就行了</span></span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">                --update[i]-&gt;nxt[i].span;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (level &gt; <span class="number">1</span> &amp;&amp; !head-&gt;nxt[level - <span class="number">1</span>])  <span class="comment">// 更新当前最大层数</span></span><br><span class="line">            --level;</span><br><span class="line">        <span class="built_in">del_node</span>(cur);</span><br><span class="line">        --length;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">find</span><span class="params">(<span class="type">const</span> T&amp; key)</span> </span>&#123;</span><br><span class="line">        Node* cur = head;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (i32 lev = level - <span class="number">1</span>; lev &gt; <span class="number">-1</span>; --lev)</span><br><span class="line">            <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; cur-&gt;nxt[lev]-&gt;key &lt; key)</span><br><span class="line">                cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> cur-&gt;nxt[<span class="number">0</span>] ? cur-&gt;nxt[<span class="number">0</span>]-&gt;key == key : <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">T <span class="title">findrk</span><span class="params">(i32 k)</span> </span>&#123;</span><br><span class="line">        <span class="built_in">assert</span>(k &lt;= length &amp;&amp; k); <span class="comment">// k 不满足要求就异常</span></span><br><span class="line">        Node* cur = head;</span><br><span class="line">        <span class="keyword">for</span> (i32 lev = level - <span class="number">1</span>; lev &gt; <span class="number">-1</span> ; --lev)</span><br><span class="line">            <span class="keyword">while</span> (cur-&gt;nxt[lev] &amp;&amp; k - cur-&gt;nxt[lev].span &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                k -= cur-&gt;nxt[lev].span;</span><br><span class="line">                cur = cur-&gt;nxt[lev];  <span class="comment">// 存在满足要求的点就跳</span></span><br><span class="line">            &#125;</span><br><span class="line">        <span class="keyword">return</span> cur-&gt;nxt[<span class="number">0</span>]-&gt;key;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    i32 length;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">skiplist&lt;i32&gt; list;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    std::ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line">    cin.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line">    cout.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line"></span><br><span class="line">    i32 n, tx;</span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">1</span>; i &lt;= n; ++i) &#123;</span><br><span class="line">        cin &gt;&gt; tx;</span><br><span class="line">        list.<span class="built_in">insert</span>(tx);</span><br><span class="line">    &#125;</span><br><span class="line">    std::string s;</span><br><span class="line">    <span class="keyword">while</span> (cin &gt;&gt; s) &#123;</span><br><span class="line">        <span class="keyword">switch</span> (s[<span class="number">0</span>]) &#123;</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;l&#x27;</span>): cout &lt;&lt; list.length &lt;&lt; std::endl; <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;i&#x27;</span>):</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;a&#x27;</span>):</span><br><span class="line">                cin &gt;&gt; tx;</span><br><span class="line">                list.<span class="built_in">insert</span>(tx);</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;d&#x27;</span>):</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;r&#x27;</span>): &#123;</span><br><span class="line">                cin &gt;&gt; tx;</span><br><span class="line">                <span class="keyword">if</span> (list.<span class="built_in">find</span>(tx))</span><br><span class="line">                    list.<span class="built_in">erase</span>(tx);</span><br><span class="line">                <span class="keyword">else</span></span><br><span class="line">                    cout &lt;&lt; <span class="string">&quot;该值不存在&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;f&#x27;</span>):</span><br><span class="line">                cin &gt;&gt; tx;</span><br><span class="line">                cout &lt;&lt; (list.<span class="built_in">find</span>(tx) ? <span class="string">&quot;存在&quot;</span> : <span class="string">&quot;不存在&quot;</span>) &lt;&lt; std::endl;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="string">&#x27;g&#x27;</span>):</span><br><span class="line">                cin &gt;&gt; tx;</span><br><span class="line">                cout &lt;&lt; list.<span class="built_in">findrk</span>(tx) &lt;&lt; std::endl;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">default</span>: cout &lt;&lt; <span class="string">&quot;未知命令&quot;</span> &lt;&lt; std::endl;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></details><details class="tag-plugin colorful folding" color="cyan" child="codeblock"><summary><span>普通平衡树</span></summary><div class="body"><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;random&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> std::cin;</span><br><span class="line"><span class="keyword">using</span> std::cout;</span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> i32 = <span class="type">int</span>;</span><br><span class="line"><span class="keyword">using</span> i64 = <span class="type">long</span> <span class="type">long</span>;</span><br><span class="line"></span><br><span class="line">std::random_device seed;</span><br><span class="line"><span class="function">std::minstd_rand <span class="title">rng</span><span class="params">(seed())</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="type">const</span> i32 MAX_LEVEL = <span class="number">7</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> N 100005</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">    i32 level;</span><br><span class="line">    i64 key;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">struct</span> <span class="title class_">ptr</span> &#123;</span><br><span class="line">        Node* pointer;</span><br><span class="line">        i32 span;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">ptr</span>() &#123;</span><br><span class="line">            pointer = <span class="literal">nullptr</span>;</span><br><span class="line">            span = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">ptr</span>(Node* x) &#123;</span><br><span class="line">            pointer = x;</span><br><span class="line">            span = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">operator</span> Node*() <span class="type">const</span>&amp; &#123;</span><br><span class="line">            <span class="keyword">return</span> pointer;</span><br><span class="line">        &#125;</span><br><span class="line">        Node* <span class="keyword">operator</span>-&gt;() <span class="type">const</span>&amp; &#123;</span><br><span class="line">            <span class="keyword">return</span> pointer;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; nxt[MAX_LEVEL];</span><br><span class="line"></span><br><span class="line">&#125; space[N], *rubbin[N];</span><br><span class="line">Node* tot = space;</span><br><span class="line">Node* head = space;</span><br><span class="line">i32 bintop;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> new_node() (bintop ? rubbin[bintop--] : ++tot)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> del_node(x) (rubbin[++bintop] = (x))</span></span><br><span class="line"></span><br><span class="line">i32 level;  <span class="comment">// global max level now</span></span><br><span class="line">i32 length;</span><br><span class="line"></span><br><span class="line"><span class="function">i32 <span class="title">random_level</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    i32 res = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (res &lt; MAX_LEVEL &amp;&amp; (<span class="built_in">rng</span>() &amp; <span class="number">1</span>) &amp;&amp; (<span class="built_in">rng</span>() &amp; <span class="number">1</span>))</span><br><span class="line">        ++res;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Node* <span class="title">create_node</span><span class="params">(<span class="type">const</span> i32&amp; level, <span class="type">const</span> i64&amp; key)</span> </span>&#123;</span><br><span class="line">    Node* res = <span class="built_in">new_node</span>();</span><br><span class="line">    res-&gt;key = key;</span><br><span class="line">    res-&gt;level = level;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">insert</span><span class="params">(<span class="type">const</span> i64&amp; key)</span> </span>&#123;</span><br><span class="line">    Node* cur = head;</span><br><span class="line">    Node::ptr update[MAX_LEVEL];</span><br><span class="line">    i32 lst_pos[MAX_LEVEL + <span class="number">1</span>];</span><br><span class="line">    lst_pos[level] = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i32 l = level - <span class="number">1</span>; l &gt; <span class="number">-1</span>; --l) &#123;</span><br><span class="line">        lst_pos[l] = lst_pos[l + <span class="number">1</span>];</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[l] &amp;&amp; cur-&gt;nxt[l]-&gt;key &lt; key) &#123;</span><br><span class="line">            lst_pos[l] += cur-&gt;nxt[l].span;</span><br><span class="line">            cur = cur-&gt;nxt[l];</span><br><span class="line">        &#125;</span><br><span class="line">        update[l] = cur;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    i32 lev = <span class="built_in">random_level</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (lev &gt; level) &#123;</span><br><span class="line">        <span class="keyword">for</span> (i32 i = level; i &lt; lev; ++i) &#123;</span><br><span class="line">            update[i] = head;</span><br><span class="line">            update[i]-&gt;nxt[i].span = length;</span><br><span class="line">            lst_pos[i] = <span class="number">0</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        level = lev;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cur = <span class="built_in">create_node</span>(lev, key);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; lev; ++i) &#123;</span><br><span class="line">        cur-&gt;nxt[i] = update[i]-&gt;nxt[i];</span><br><span class="line">        cur-&gt;nxt[i].span = update[i]-&gt;nxt[i].span - (lst_pos[<span class="number">0</span>] - lst_pos[i]);</span><br><span class="line">        update[i]-&gt;nxt[i].pointer = cur;</span><br><span class="line">        update[i]-&gt;nxt[i].span = lst_pos[<span class="number">0</span>] - lst_pos[i] + <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i32 i = lev; i &lt; level; ++i)</span><br><span class="line">        ++update[i]-&gt;nxt[i].span;</span><br><span class="line"></span><br><span class="line">    ++length;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">erase</span><span class="params">(<span class="type">const</span> i64&amp; key)</span> </span>&#123;</span><br><span class="line">    Node* cur = head;</span><br><span class="line">    Node::ptr update[MAX_LEVEL];</span><br><span class="line">    <span class="keyword">for</span> (i32 l = level - <span class="number">1</span>; l &gt; <span class="number">-1</span>; --l) &#123;</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[l] &amp;&amp; cur-&gt;nxt[l]-&gt;key &lt; key)</span><br><span class="line">            cur = cur-&gt;nxt[l];</span><br><span class="line">        update[l] = cur;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    cur = cur-&gt;nxt[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; level; ++i)</span><br><span class="line">        <span class="keyword">if</span> (update[i]-&gt;nxt[i] == cur)</span><br><span class="line">            update[i]-&gt;nxt[i].span += cur-&gt;nxt[i].span - <span class="number">1</span>, update[i]-&gt;nxt[i].pointer = cur-&gt;nxt[i].pointer;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            --update[i]-&gt;nxt[i].span;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (level &gt; <span class="number">1</span> &amp;&amp; !head-&gt;nxt[level - <span class="number">1</span>])</span><br><span class="line">        --level;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">del_node</span>(cur);</span><br><span class="line">    --length;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">i32 <span class="title">get_rk</span><span class="params">(<span class="type">const</span> i64&amp; key)</span> </span>&#123;</span><br><span class="line">    Node* cur = head;</span><br><span class="line">    i32 res = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (i32 l = level - <span class="number">1</span>; l &gt; <span class="number">-1</span>; --l) &#123;</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[l] &amp;&amp; cur-&gt;nxt[l]-&gt;key &lt; key) &#123;</span><br><span class="line">            res += cur-&gt;nxt[l].span;</span><br><span class="line">            cur = cur-&gt;nxt[l];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res + <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">i64 <span class="title">find_by_rk</span><span class="params">(i32 k)</span> </span>&#123;</span><br><span class="line">    Node* cur = head;</span><br><span class="line">    <span class="keyword">for</span> (i32 l = level - <span class="number">1</span>; l &gt; <span class="number">-1</span>; --l) &#123;</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[l] &amp;&amp; k - cur-&gt;nxt[l].span &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            k -= cur-&gt;nxt[l].span;</span><br><span class="line">            cur = cur-&gt;nxt[l];</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> cur-&gt;nxt[<span class="number">0</span>]-&gt;key;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Node* <span class="title">prev</span><span class="params">(<span class="type">const</span> i64&amp; key)</span> </span>&#123;</span><br><span class="line">    Node* cur = head;</span><br><span class="line">    <span class="keyword">for</span> (i32 l = level - <span class="number">1</span>; l &gt; <span class="number">-1</span>; --l) &#123;</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[l] &amp;&amp; cur-&gt;nxt[l]-&gt;key &lt; key)</span><br><span class="line">            cur = cur-&gt;nxt[l];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> cur;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Node* <span class="title">next</span><span class="params">(<span class="type">const</span> i64&amp; key)</span> </span>&#123;</span><br><span class="line">    Node* cur = head;</span><br><span class="line">    <span class="keyword">for</span> (i32 l = level - <span class="number">1</span>; l &gt; <span class="number">-1</span>; --l) &#123;</span><br><span class="line">        <span class="keyword">while</span> (cur-&gt;nxt[l] &amp;&amp; cur-&gt;nxt[l]-&gt;key &lt;= key)</span><br><span class="line">            cur = cur-&gt;nxt[l];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> cur-&gt;nxt[<span class="number">0</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    std::ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line">    cin.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line">    cout.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line"></span><br><span class="line">    i32 n;</span><br><span class="line">    i32 op, x;</span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">while</span> (n--) &#123;</span><br><span class="line">        cin &gt;&gt; op &gt;&gt; x;</span><br><span class="line">        <span class="keyword">switch</span> (op) &#123;</span><br><span class="line">            <span class="built_in">case</span> (<span class="number">1</span>): <span class="built_in">insert</span>(x); <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="number">2</span>): <span class="built_in">erase</span>(x); <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="number">3</span>): cout &lt;&lt; <span class="built_in">get_rk</span>(x) &lt;&lt; <span class="string">&#x27;\n&#x27;</span>; <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="number">4</span>): cout &lt;&lt; <span class="built_in">find_by_rk</span>(x) &lt;&lt; <span class="string">&#x27;\n&#x27;</span>; <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="number">5</span>): cout &lt;&lt; <span class="built_in">prev</span>(x)-&gt;key &lt;&lt; <span class="string">&#x27;\n&#x27;</span>; <span class="keyword">break</span>;</span><br><span class="line">            <span class="built_in">case</span> (<span class="number">6</span>): cout &lt;&lt; <span class="built_in">next</span>(x)-&gt;key &lt;&lt; <span class="string">&#x27;\n&#x27;</span>; <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// cout.flush();</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></details><h2 id="复杂度分析">复杂度分析</h2><h3 id="空间复杂度">空间复杂度</h3><p>设定了最高 <code>level</code>，所以空间复杂度只能是 <spanclass="math inline">\(\Theta(n)\)</span> 的。</p><h3 id="时间复杂度">时间复杂度</h3><p>见 <ahref="https://oi.wiki/ds/skiplist/#%E6%97%B6%E9%97%B4%E5%A4%8D%E6%9D%82%E5%BA%A6">OIWiki</a></p><h2 id="参考文献">参考文献</h2><p><ahref="https://juejin.cn/post/7186285617259479098?searchId=202308281935209342639207B2B0163904#heading-5">《跳跃表数据结构与算法分析》</a>纪卓志 George</p><p><a href="https://oi.wiki/ds/skiplist/">《跳表》</a> OI Wiki</p><h2 id="推荐阅读">推荐阅读</h2><p><ahref="https://www.luogu.com.cn/blog/DPair2005/guan-yu-skip-list-di-yi-suo-kuo-zhan-xiang-fa">《关于skip list 的一些扩展想法》</a></p><section id="footnotes" class="footnotes footnotes-end-of-document"role="doc-endnotes"><hr /><ol><liid="fn1"><p>有的朋友可能会有疑问啊，为什么能够直接取随机数的某一位呢？这不会分布地不够均匀吗？为什么用线性同余而不用统计意义更好的<code>mt19937</code> 呢？我想用 <code>std::bernoulli_distribution</code>可以吗？<br />本人做了测试，在随机种子 + 1000次取值的测试下，梅森缠绕和线性同余两种算法通过 <code>&amp; 1</code>求出来的平均值基本上就是 <span class="math inline">\(0.5\pm0.03\)</span>，而且线性同余性能远比梅森缠绕高。然后用 PCG算法测试了一下，发现 PCG 官方给的 C++ 实现能够做到 <spanclass="math inline">\(0.5\pm0.02\)</span>，性能接近线性同余，但是这玩意考场上得自己实现，所以还是用线性同余吧。<br />至于 <code>std::bernoulli_distribution</code>，这东西是取<code>std::uniform_real_distribution</code> 和设定概率比较做出来的，而<code>std::uniform_real_distribution</code>是取随机数之后做一堆浮点运算，效率太低了，而且最后得出来的结果并没有比直接<code>&amp; 1</code> 强到哪去。<a href="#fnref1" class="footnote-back"role="doc-backlink">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">跳表是一个基于随机化的多层索引链表，大部分操作有对数级别的期望复杂度，同时能像链表一样线性时间遍历。效率与红黑树平齐。</summary>
    
    
    
    <category term="数据结构" scheme="http://blog.tibrella.space/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="随机化" scheme="http://blog.tibrella.space/tags/%E9%9A%8F%E6%9C%BA%E5%8C%96/"/>
    
    <category term="跳表" scheme="http://blog.tibrella.space/tags/%E8%B7%B3%E8%A1%A8/"/>
    
  </entry>
  
  <entry>
    <title>计数杂题</title>
    <link href="http://blog.tibrella.space/post/count-misc-questions/"/>
    <id>http://blog.tibrella.space/post/count-misc-questions/</id>
    <published>2023-08-17T11:54:05.000Z</published>
    <updated>2023-09-07T11:15:53.864Z</updated>
    
    <content type="html"><![CDATA[<p>部分题目来自<ahref="https://www.luogu.com.cn/training/2019#problems">这个题单</a>。</p><h2 id="luogu-p6075-jsoi-2015-子集选取">Luogu P6075 [JSOI 2015]子集选取</h2><p>首先，发现可以把元素单独拎出来考虑，假设对于一个元素来说有 <spanclass="math inline">\(x\)</span> 种放置方案，则答案为 <spanclass="math inline">\(x^n\)</span>。</p><p>继续转化题意。</p><p>对于 <spanclass="math inline">\(A_{i,j}\)</span>，它必须是它左边集合的子集，也得是上边集合的子集。<br />也就是说，如果 <span class="math inline">\(A_{i,j}\)</span> 包含元素<spanclass="math inline">\(x\)</span>，则在它正上方、左上方、左侧的所有集合都包含<span class="math inline">\(x\)</span>。（形式化来说，即 <spanclass="math inline">\(A_{a,b},1\leqslant a \leqslant i,1\leqslant b\leqslant j\)</span> 均包含 <span class="math inline">\(x\)</span>）</p><p>然后想象一种合法方案，容易发现每种方案就相当于把整个三角形分成两半，左上一半右下一半。</p><p>每两种方案不同当且仅当左上右下的分界线不同。</p><p>于是我们就能很轻松地求方案数了，即一个分界线从左下走到右上的方案。每一步可以向左或者向右走，即<span class="math inline">\(2^k\)</span>。</p><p>快速幂即可。</p><h2 id="luogu-p6146-usaco-20-feb-help-yourself-g">Luogu P6146 [USACO 20FEB] Help Yourself G</h2><p>首先为了划分未求解和已求解的部分，我们需要把线段按照左端点排序。</p><p>给一个不排序会遇到错误的例子：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">---- 1  ------3</span><br><span class="line">  --------- 2</span><br></pre></td></tr></table></figure><p>用下面的做法做的话，先算 <span class="math inline">\(1,3\)</span>和先算 <span class="math inline">\(1,2\)</span>是不一样的，可以自己体会一下。</p><p>然后设 <span class="math inline">\(f_i\)</span> 为前 <spanclass="math inline">\(i\)</span>个线段的答案。为什么只有一维？因为容易发现子集这个东西很难用来划分阶段。另外前面的排序已经使得这种状态划分是唯一的了。</p><p>考虑如何推出 <span class="math inline">\(f_i\)</span>。</p><p><span class="math inline">\(f_i\)</span> 的答案由两部分组成：</p><ul><li>前 <span class="math inline">\(i-1\)</span> 个线段的答案 <spanclass="math inline">\(f_{i-1}\)</span></li><li>前面所有子集加上第 <span class="math inline">\(i\)</span>条线段的答案。</li></ul><p>发现后者实际上也是分两部分的：</p><ul><li>新增的连通块（复杂度）</li><li>除去新增，也就是原有的复杂度 <spanclass="math inline">\(f_{i-1}\)</span></li></ul><p>好了，那么新增的连通块怎么算？实际上就是与第 <spanclass="math inline">\(i\)</span> 条线段不相交的前 <spanclass="math inline">\(i-1\)</span> 条线段的子集的数量。为啥呢？<br />那我们可以把前 <span class="math inline">\(i-1\)</span>条线段的子集分两类：与 <span class="math inline">\(i\)</span>相交与不相交。显然相交的子集不会贡献答案，不相交的子集会且仅会贡献 <spanclass="math inline">\(1\)</span> 的答案。</p><p>然后问题就转化为：对前 <span class="math inline">\(i-1\)</span>条线段与线段 <span class="math inline">\(i\)</span>不相交的子集计数。<br />容易发现就是与线段 <span class="math inline">\(i\)</span>不相交的所有线段集合 <span class="math inline">\(\mathbb X\)</span>的子集数量，计数就是 <span class="math inline">\(2^{\lvert \mathbb X\rvert}\)</span></p><p>得到递推式：</p><p><span class="math display">\[f_i = 2f_{i-1}+2^{\lvert \mathbb X \rvert}\]</span></p><p><span class="math inline">\(\mathbb X\)</span>的大小我们可以预处理出来：统计每一个右端点出现的次数 <spanclass="math inline">\(s_i\)</span>，然后做一个前缀和，最后 <spanclass="math inline">\(s_{i-1}\)</span> 就是不覆盖点 <spanclass="math inline">\(i\)</span> 的线段数量。</p><p>用快速幂优化，复杂度 <spanclass="math inline">\(\operatorname{O}(n\log n+n\logn)\)</span>，如果用光速幂可以干到 <spanclass="math inline">\(\operatorname{O}(n\log n+n+\sqrt n)\)</span>。</p><h2 id="p6008-usaco-20-jan-cave-paintings-p">P6008 [USACO 20 JAN] CavePaintings P</h2><p>首先发现答案就是把每个连通块的方案数乘起来。</p><p>那对于一个连通块怎么算答案？一个比较蛋疼的事实是，即使是同一个连通块，我们也不能保证它每个部分的水位是一样的，比如下面这个例子：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">########</span><br><span class="line">###  ###</span><br><span class="line">#      #</span><br><span class="line">#  ##  #</span><br><span class="line">#  ##  #</span><br><span class="line">########  </span><br></pre></td></tr></table></figure><p>显然左右侧在分割开的时候可以水位高度不同。</p><p>那我们能不能对分割开的两块分别算呢？答案是不行，因为连通器原理。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">########</span><br><span class="line">#  ##  #</span><br><span class="line">#  ##  #</span><br><span class="line">#      #</span><br><span class="line">#      #</span><br><span class="line">########  </span><br></pre></td></tr></table></figure><p>初中物理可知，左右两侧水位必须一样高。</p><p>所以我们需要动态维护这个关系，用啥呢？并查集。</p><p>初始化每个格子答案为1，然后对于每个连通块从下往上涨水，过程如下：</p><ul><li>把这一行相邻的能放水的位置连在一起，表示他们必须是一个水位的。</li><li>遍历该行每一个空位，如果这个位置下面也是空位且二者目前没有相连，那么当前位置方案数<code>*=</code>正下方一格方案数，同时把两个格子相连，表示两个连通块接在一起了。</li><li>遍历该行每一个空位，每遇到一个连通块（<code>find(x) == x</code>）就把该格方案数加一。</li></ul><p>最后用前面提到过的判断连通块的方式把答案乘起来就好了。</p><h2 id="luogu-p1350-车的放置">Luogu P1350 车的放置</h2><p>同样是蓝题，但是比上一道水多了。</p><p>每一行每一列都只能放一个棋子，那直接从上往下遍历行，同时枚举放几颗棋子递推计数即可。</p><p>具体来说，设 <span class="math inline">\(f_{i,j}\)</span> 为前 <spanclass="math inline">\(i\)</span> 行放了 <spanclass="math inline">\(j\)</span> 个棋子的方案数量，则转移方程如下：</p><p><span class="math display">\[f_{i,j} = f_{i-1,j} + f_{i-1,j-1} (len-j+1)\]</span></p><p>其中 <span class="math inline">\(len\)</span> 是行长度。</p><p>记得把 <span class="math inline">\(f_{0\sim b+d, 0}\)</span> 设为<span class="math inline">\(1\)</span>。</p><h2 id="luogu-p3223-hnoi-2012-排队">Luogu P3223 [HNOI 2012] 排队</h2><p>基本上是纯数学题。</p><p>大概计数题可以分两种大方向思考：正推、容斥。</p><p>而容斥也大概分两种：手动容斥和套式子反演。</p><p>想直接拆分算，感觉是一个非常恶心难想的分类讨论。</p><p>递推，设 <span class="math inline">\(f_{n,m}\)</span> 为 <spanclass="math inline">\(n\)</span> 男生 <spanclass="math inline">\(m\)</span> 女生的方案数，发现推不了。</p><p>那就手动容斥。<br />容易发现把老师的限制扔掉之后是可以轻松算出来答案的：<spanclass="math inline">\(A^m_m A^{n+2}_{n+2}\binom{n+3}m\)</span>，即把老师当成男生随便放置，然后在他们中间的空隙中选出来<span class="math inline">\(m\)</span> 个用来放女生。</p><p>然后不合法方案，即两个老师靠在一起的方案，把两个老师绑在一起当一个男生求出来所有合法方案即可。别忘了两个老师顺序可以变，所以加一个<span class="math inline">\(A^2_2\)</span>。</p><p>最终答案：<span class="math inline">\(A^m_m A^{n+2}_{n+2}\binom{n+3}m - A^m_m A^{n+1}_{n+1}\binom {n+2}m A^2_2\)</span></p><p>为啥说基本上是纯数学题而不是完全的数学题？因为这题需要打高精。</p><p>然后我用 python 写的，python整数除法返回浮点值，所以要强制整除，即使用 <code>//</code>。</p><p>举个例子，算 <span class="math inline">\(\binom nm\)</span>：<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> math</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">binom</span>(<span class="params">n, m</span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">    <span class="keyword">if</span> n &lt; m:</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="keyword">return</span> math.factorial(n)//math.factorial(m)//math.factorial(n-m)</span><br></pre></td></tr></table></figure></p><h2 id="csp-s-2019-emiya-家今天的饭">[CSP-S 2019] Emiya 家今天的饭</h2><p>挺难的题。</p><p>首先我们可以把选菜看成从方阵里面选数，每行只能选一个，每列最多选<span class="math inline">\(\lfloor \frac k2 \rfloor\)</span> 个。</p><p>如果我们分列来考虑，发现合法的列有很多，但是不合法的列只会出现一个。（显然选的数超过总数一半的列只会有一个吧）<br />换句话说，如果我们正着算，记录合法方案总数，是一件非常困难的事情，因为每一列的合法情况我们都要记录。但是相对的，对于不合法方案计数，我们可以枚举不合法的列进行递推。当我们保证这一列不合法时，其他列一定合法，这样我们就不用记太多状态了。</p><p>枚举到第 <span class="math inline">\(col\)</span> 行，设 <spanclass="math inline">\(f(col)_{i,j,k}\)</span> 为第 <spanclass="math inline">\(col\)</span> 列为不合法列，前 <spanclass="math inline">\(i\)</span> 行，第 <spanclass="math inline">\(col\)</span> 行选了 <spanclass="math inline">\(j\)</span> 个数，其他行选了 <spanclass="math inline">\(k\)</span> 个数的方案数量，<spanclass="math inline">\(s_{i}\)</span> 为第 <spanclass="math inline">\(i\)</span> 行 <spanclass="math inline">\(a\)</span> 的总和，则有转移方程：</p><p><span class="math display">\[f_{i,j,k} = f_{i-1,j,k}+f_{i-1,j-1,k}\cdot a_{i,col}+f_{i-1,j,k-1}\cdot(s_i-a_{i,col})\]</span></p><p>然后不合法方案数即 <span class="math inline">\(\sum_{j&gt;k}f(col)_{n,j,k}\)</span></p><p>复杂度 <spanclass="math inline">\(\Theta(mn^3)\)</span>，想办法减少状态。</p><p>根据刚才不合法方案数的计算，我们发现，一个方案不合法，当且仅当有一列选的数大于其他列选的数之和。</p><p>换句话说，刚才对不合法方案数的统计，基于 <spanclass="math inline">\(j&gt;k\)</span> 这个条件，那我们是不是可以只记录<span class="math inline">\(j-k\)</span> 这个东西呢？</p><p>设 <span class="math inline">\(f(col)_{i,j}\)</span> 为第 <spanclass="math inline">\(col\)</span> 列为不合法列，前 <spanclass="math inline">\(i\)</span> 行，当前行选的数比别的行多 <spanclass="math inline">\(j\)</span>个的总方案数，转移方程和上面的差不多：</p><p><span class="math display">\[f_{i,j} = f_{i-1,j} + f_{i,j-1}\cdot a_{i,col} +f_{i,j+1}\cdot(s_i-a_{i,col})\]</span></p><p>不合法方案数：<span class="math inline">\(\sum_{j&gt;0}f(col)_{n,j}\)</span></p><p>容易发现这个东西可能是负数，所以实现的时候要给 <spanclass="math inline">\(j\)</span> 这一维加一个 <spanclass="math inline">\(n\)</span>。</p><p>然后是计算总方案数，设 <span class="math inline">\(g_{i,j}\)</span>为前 <span class="math inline">\(i\)</span> 行选了 <spanclass="math inline">\(j\)</span> 个数的方案数，转移方程：</p><p><span class="math display">\[g_{i,j} = g_{i-1,j} + g_{i-1,j-1} \cdot s_i\]</span></p><p>最终答案：<span class="math inline">\(\sum_{i=1}^n g_{n,i} -\sum_{col=1}^m\sum_{j&gt;0}^n  f(col)_{n,j}\)</span></p><h2 id="luogu-p3214-hnoi-2011-卡农">Luogu P3214 [HNOI 2011] 卡农</h2><p>题意要求选取子集有三条限制：<br />- 非空 - 选出的子集两两不同 - 所有元素的出现次数为偶数</p><p>转化 1：将“子集”转化为“二进制数”，然后选取子集变成从 <spanclass="math inline">\(0,1\dots 2^n-1\)</span> 个数里面选出 <spanclass="math inline">\(m\)</span> 个，然后限制就变成了：<br />- 这些数不等于 <span class="math inline">\(0\)</span> - 这些数异或和为<span class="math inline">\(0\)</span> - 这些数两两不同</p><p>转化 2：答案是“无序”的方案数，我们发现这个 <spanclass="math inline">\(m\)</span> 个数无序的方案数乘一个排列 <spanclass="math inline">\(A_m^m\)</span>就是有序的方案数，显然后者比较容易划分阶段，所以我们可以算出有序的方案数再除以<span class="math inline">\(A_m^m\)</span>。</p><p>然后我们发现这上面几条限制挺难同时记录并递推的，那我们可以考虑一下排除不合法方案。</p><p>设 <span class="math inline">\(f_i\)</span> 为选 <spanclass="math inline">\(i\)</span>个数的方案数，先考虑第二条限制。在这条限制下，当 <spanclass="math inline">\(1\sim i-1\)</span> 这些数选完之后，第 <spanclass="math inline">\(i\)</span>个数是确定的。换句话说，满足第二条限制的方案数，即为前 <spanclass="math inline">\(i-1\)</span> 个数随意选择的方案数（非空）：<spanclass="math inline">\(A_{2^n-1}^{i-1}\)</span>。</p><p>然后考虑第一条限制，发现第 <span class="math inline">\(i\)</span>个集合为空且总异或和为 <span class="math inline">\(0\)</span>，把第<span class="math inline">\(i\)</span>个数（空）去掉依然合法。不满足第一条限制的方案数即 <spanclass="math inline">\(f_{i-1}\)</span>。</p><p>最后考虑第三条性质：假设第 <span class="math inline">\(i\)</span>个数与前面第 <span class="math inline">\(j\)</span>个数相同了，那么这个第 <span class="math inline">\(i\)</span> 个数有<span class="math inline">\(2^n-1-(i-2)\)</span> 种取值，这个第 <spanclass="math inline">\(j\)</span> 个数的位置有 <spanclass="math inline">\(i-1\)</span>种可能性。除此以外的地方合法（前面已经保证异或和为 <spanclass="math inline">\(0\)</span>了，现在再异或两个相同的数结果不变）且长度为 <spanclass="math inline">\(i-2\)</span>。因此不满足这个要求的方案数有 <spanclass="math inline">\(f_{i-2}(2^n-1-(i-2))(i-1)\)</span></p><p>然后就可以递推求解了，<span class="math inline">\(f_i =A_{2^n-1}^{i-1} - f_{i-1} - f_{i-2}(2^n-1-(i-2))(i-1)\)</span>，答案是<span class="math inline">\(f_m\)</span>。</p><p><span class="math inline">\(A_{2^n-1}^{i-1}\)</span>可以预处理，<span class="math inline">\(A_{2^n-1}^1 =2^n-1\)</span>，剩下的挨个乘就好了。预处理到 <spanclass="math inline">\(m\)</span> 即可。</p><h2 id="cf-932-e">CF 932 E</h2><p><del>模拟赛写不动题了过来写这玩意挺合理的吧</del></p><p>原题意是给定 <span class="math inline">\(\lvert \mathbbS\rvert,k\)</span>，求 <span class="math inline">\(\sum_{\mathbbT\subseteq \mathbb S} \lvert \mathbb T \rvert ^ k\)</span>。</p><p>然后我们发现这个东西只与 <span class="math inline">\(\mathbb{S,T}\)</span> 的大小有关，所以改为枚举大小，u设 <spanclass="math inline">\(n = \mathbb S\)</span>，原式化为：</p><p><span class="math display">\[\sum_{i=1}^n \binom ni i^k\]</span></p><p>把 <span class="math inline">\(i^k\)</span>用组合意义干掉。发现这玩意就是 <span class="math inline">\(k\)</span>个不同小球放进 <span class="math inline">\(i\)</span>个不同盒子的方案数。</p><p>继续组合意义分析，上面那个式子相当于是：从 <spanclass="math inline">\(n\)</span> 个盒子里选出 <spanclass="math inline">\(i\)</span> 个盒子，然后把 <spanclass="math inline">\(k\)</span> 个小球放在这 <spanclass="math inline">\(i\)</span> 个盒子里面的方案数。</p><p><del>继续发现</del>发现这 <span class="math inline">\(k\)</span>个小球最多只能放满 <span class="math inline">\(k\)</span>个盒子，所以我们转头去枚举“哪些盒子有小球”。设 <spanclass="math inline">\(f_{i,j}\)</span> 为从 <spanclass="math inline">\(n\)</span> 个盒子选出 <spanclass="math inline">\(j\)</span> 个，<spanclass="math inline">\(i\)</span> 个小球放入这 <spanclass="math inline">\(j\)</span>个盒子，盒子非空的方案数，然后我们要求的东西就变成了：</p><p><span class="math display">\[\sum_{i=1}^k f_{k,i} 2^{n-i}\]</span></p><p>（确定 <span class="math inline">\(j\)</span>个盒子要放入小球之后，剩下 <span class="math inline">\(n-j\)</span>个小球可能会被选入最初选的 <span class="math inline">\(i\)</span>个盒子中也有可能不被选入，所以有一个 <spanclass="math inline">\(2^{n-i}\)</span>）。<span class="math inline">\(k\leqslant 5\times 10^3\)</span>，<spanclass="math inline">\(\Theta(k^2)\)</span> 能过。</p><p><span class="math inline">\(i\)</span> 个小球放入 <spanclass="math inline">\(j\)</span> 个盒子，盒子非空的方案数，相当于 <spanclass="math inline">\(i\)</span> 个元素被划分成 <spanclass="math inline">\(j\)</span>个非空子集的方案数，联想到第二类斯特林数。又因为选出来的 <spanclass="math inline">\(j\)</span>个盒子不一定是哪几个，而且两两不同，我们还得乘进去一个排列。即 <spanclass="math inline">\(f_{k,i} = {k \bracei}A_n^i\)</span>，最终答案即为：</p><p><span class="math display">\[\sum_{i=1}^k {k \brace i} A_n^i 2^{n-i}\]</span></p><p>第二类斯特林数可以 <span class="math inline">\(\Theta(k^2)\)</span>预处理，递推式为 <span class="math inline">\({i \brace j} = {i-1 \bracej-1}+j{i-1 \brace j}\)</span>。<spanclass="math inline">\(A_n^i\)</span> 实际上就是 <spanclass="math inline">\(n\)</span> 的下降幂，这玩意也是直接预处理到 <spanclass="math inline">\(A_n^k\)</span> 就好。后面的 <spanclass="math inline">\(2^{n-i}\)</span>可以用快速幂或者光速幂。光速幂没啥太大必要，毕竟复杂度瓶颈不在这。</p><h2 id="cf-1850-g">CF 1850 G</h2><p>感觉这题没啥必要放，毕竟 CF 1500，但是我写的 <spanclass="math inline">\(\Theta(n \log n)\)</span>大概算是常数比较小的版本吧。</p><p>发现满足要求的两个点只有四种可能： - 横坐标相等 - 纵坐标相等 -横纵坐标和相等 - 横纵坐标差相等</p><p>所以我们把所有点按照上面四个元素作为关键字排序四次，每次分别统计相等的个数<span class="math inline">\(x\)</span>，让答案加上 <spanclass="math inline">\(x(x-1)\)</span> 就好了。</p><p>感觉比他们写的 <code>std::map</code> 做法常数要小。</p><h2 id="cf-1485-f">CF 1485 F</h2><details class="tag-plugin colorful folding" ><summary><span>简化题意</span></summary><div class="body"><p>给定数组 <span class="math inline">\(b\)</span>，对合法的 <spanclass="math inline">\(a\)</span> 计数。<br />合法的 <span class="math inline">\(a\)</span> 即 <spanclass="math inline">\(\forall i \in [1,n],b_i = a_i\)</span> 或 <spanclass="math inline">\(b_i = \sum_{j=1}^i a_j\)</span></p></div></details><p>我们一个一个数填，发现对于每一个 <spanclass="math inline">\(a_i\)</span>，有两种可能： - <spanclass="math inline">\(a_i = b_i\)</span> - <spanclass="math inline">\(a_i = b_i - \sum_{j=1}^{i-1}a_i\)</span></p><p>然后设 <span class="math inline">\(f_s\)</span> 为当前和等于 <spanclass="math inline">\(s\)</span> 的方案数。然后上面两个可能就变成了： -<span class="math inline">\(f_{x+b_i} = f_{x}\)</span> - <spanclass="math inline">\(f_{b_i} = \sum_{j\in \mathbb Z} f_j\)</span></p><p>发现，第一个操作就是把整个数组向右移动了 <spanclass="math inline">\(b_i\)</span>，所以我们只需要记录一下偏移量每次加一个<span class="math inline">\(b_i\)</span> 就完成了 <spanclass="math inline">\(\Theta(1)\)</span> 转移。</p><p>第二个实际上等于一个位置前面所有元素的和，<spanclass="math inline">\(f_x\)</span> 变成了 <spanclass="math inline">\(ans\)</span>，然后原来的 <spanclass="math inline">\(f_x\)</span> 没了，于是 <spanclass="math inline">\(ans = ans - f_x + ans\)</span>。</p><p>值域很大，所以要用 <code>std::map</code> 或者<code>std::unordered_map</code>，后者需要重写哈希函数。</p><details class="tag-plugin colorful folding" child="codeblock"><summary><span>示例代码</span></summary><div class="body"><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">cin &gt;&gt; n;</span><br><span class="line"><span class="keyword">for</span> (i32 i = <span class="number">1</span>; i &lt;= n; ++ i) cin &gt;&gt; b[i];</span><br><span class="line">f.<span class="built_in">clear</span>();</span><br><span class="line"></span><br><span class="line">i32 ans = f[<span class="number">0</span>] = <span class="number">1</span>, delta = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span> (i32 i = <span class="number">1</span>; i &lt;= n; ++ i) &#123;</span><br><span class="line">    delta += b[i];</span><br><span class="line">    i32 add = (ans - f[b[i] - delta] + mod) % mod;</span><br><span class="line">    f[b[i] - delta] = ans;</span><br><span class="line">    ans = (ans + add) % mod;</span><br><span class="line">&#125;</span><br><span class="line">cout &lt;&lt; ans &lt;&lt; <span class="string">&#x27;\n&#x27;</span>;</span><br></pre></td></tr></table></figure></div></details><h2 id="cf-449-d">CF 449 D</h2><details class="tag-plugin colorful folding" ><summary><span>简化题意</span></summary><div class="body"><p>给定数组 <span class="math inline">\(a\)</span>，从 <spanclass="math inline">\(a\)</span> 选出非空子集使其按位与和结果为 <spanclass="math inline">\(0\)</span>。求方案数</p></div></details><p>设 <span class="math inline">\(f_s\)</span> 为与和为 <spanclass="math inline">\(s\)</span> 的方案数，<spanclass="math inline">\(g_s\)</span> 为与和是 <spanclass="math inline">\(s\)</span> 的超集的方案数。</p><p>显然 <span class="math inline">\(f\)</span> 做高维后缀和就得到了<span class="math inline">\(g\)</span>。</p><p>但是发现这个 <span class="math inline">\(f\)</span> 和 <spanclass="math inline">\(g\)</span> 都不好直接求，然后发现如果设 <spanclass="math inline">\(h_s\)</span> 为数列中 <spanclass="math inline">\(s\)</span> 的超集的数量，那么 <spanclass="math inline">\(g_s = 2^{h_s} -1\)</span>（从这些超集里面随便几个取并集，显然结果一定仍然是 <spanclass="math inline">\(s\)</span> 的超集）。</p><p>然后继续发现，<span class="math inline">\(h_s\)</span> 可以对 <spanclass="math inline">\(s\)</span> 计数之后直接高位后缀和得到。</p><p>总体流程：先对 <span class="math inline">\(s\)</span>用桶计数，然后高维后缀和，然后 <span class="math inline">\(g_s = 2^{h_s}- 1\)</span>，然后对 <span class="math inline">\(g\)</span>反着（加法变减法）做一遍高维后缀和就好了。</p><details class="tag-plugin colorful folding" child="codeblock"><summary><span>示例代码</span></summary><div class="body"><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">i32 f[N];</span><br><span class="line">i32 n;</span><br><span class="line"><span class="keyword">constexpr</span> i32 mod = <span class="number">1e9</span><span class="number">+7</span>;</span><br><span class="line"><span class="keyword">constexpr</span> i32 s = (<span class="number">1</span> &lt;&lt; <span class="number">20</span>) - <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">sos</span><span class="params">(<span class="type">const</span> i64&amp; x)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; <span class="number">20</span>; ++ i)</span><br><span class="line">        <span class="keyword">for</span> (i32 j = s; j &gt;= <span class="number">0</span>; -- j) </span><br><span class="line">            <span class="keyword">if</span>((j &amp; (<span class="number">1</span> &lt;&lt; i)) == <span class="number">0</span>)</span><br><span class="line">                f[j] = (f[j] + f[j | (<span class="number">1</span> &lt;&lt; i)] * x % mod + mod) % mod; <span class="comment">// SOS DP</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    std::ios::<span class="built_in">sync_with_stdio</span>(<span class="literal">false</span>);</span><br><span class="line">    cin.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line">    cout.<span class="built_in">tie</span>(<span class="literal">nullptr</span>);</span><br><span class="line">   </span><br><span class="line">    i32 x;</span><br><span class="line"></span><br><span class="line">    cin &gt;&gt; n;</span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">1</span>; i &lt;= n; ++ i) &#123;</span><br><span class="line">        cin &gt;&gt; x;</span><br><span class="line">        ++f[x];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">sos</span>(<span class="number">1</span>);</span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt;= s; ++ i) f[i] = <span class="built_in">binpow</span>(f[i]) - <span class="number">1</span>;</span><br><span class="line">    <span class="built_in">sos</span>(<span class="number">-1</span>);</span><br><span class="line"></span><br><span class="line">    cout &lt;&lt; f[<span class="number">0</span>];</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div></details>]]></content>
    
    
    <summary type="html">计数太烂了，多写几道题吧...</summary>
    
    
    
    <category term="题解" scheme="http://blog.tibrella.space/categories/%E9%A2%98%E8%A7%A3/"/>
    
    
    <category term="组合数学" scheme="http://blog.tibrella.space/tags/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/"/>
    
    <category term="计数" scheme="http://blog.tibrella.space/tags/%E8%AE%A1%E6%95%B0/"/>
    
  </entry>
  
  <entry>
    <title>（转载）什么是 P 问题、NP 问题和 NPC 问题</title>
    <link href="http://blog.tibrella.space/post/p-np-npc/"/>
    <id>http://blog.tibrella.space/post/p-np-npc/</id>
    <published>2023-08-05T23:36:57.000Z</published>
    <updated>2023-08-17T11:40:07.504Z</updated>
    
    <content type="html"><![CDATA[<p><ahref="https://www.matrix67.com/blog/archives/7084">原文传送门</a>，本人对其进行了格式上的改动。</p><p>这或许是众多 OIer 最大的误区之一。你会经常看到网上出现“这怎么做，这不是 NP问题吗”、“这个只有搜了，这已经被证明是 NP问题了”之类的话。你要知道，大多数人此时所说的 NP 问题其实都是指的 NPC问题。他们没有搞清楚 NP 问题和 NPC 问题的概念。NP问题并不是那种“只有搜才行”的问题，NPC问题才是。好，行了，基本上这个误解已经被澄清了。下面的内容都是在讲什么是P 问题，什么是 NP 问题，什么是 NPC问题，你如果不是很感兴趣就可以不看了。接下来你可以看到，把 NP 问题当成是NPC 问题是一个多大的错误。</p><p>还是先用几句话简单说明一下时间复杂度。时间复杂度并不是表示一个程序解决问题需要花多少时间，而是当问题规模扩大后，程序需要的时间长度增长得有多快。也就是说，对于高速处理数据的计算机来说，处理某一个特定数据的效率不能衡量一个程序的好坏，而应该看当这个数据的规模变大到数百倍后，程序运行时间是否还是一样，或者也跟着慢了数百倍，或者变慢了数万倍。不管数据有多大，程序处理花的时间始终是那么多的，我们就说这个程序很好，具有<span class="math inline">\(\Theta(1)\)</span>的时间复杂度，也称常数级复杂度；数据规模变得有多大，花的时间也跟着变得有多长，这个程序的时间复杂度就是<span class="math inline">\(\Theta(n)\)</span>，比如找 <spanclass="math inline">\(n\)</span>个数中的最大值；而像冒泡排序、插入排序等，数据扩大 <spanclass="math inline">\(2\)</span> 倍，时间变慢 <spanclass="math inline">\(4\)</span> 倍的，属于 <spanclass="math inline">\(\Theta(n^2)\)</span>的复杂度。还有一些穷举类的算法，所需时间长度成几何阶数上涨，这就是 <spanclass="math inline">\(\Theta(a^n)\)</span> 的指数级复杂度，甚至 <spanclass="math inline">\(\Theta(n!)\)</span> 的阶乘级复杂度。不会存在 <spanclass="math inline">\(\Theta(2n^2)\)</span>的复杂度，因为前面的那个“<spanclass="math inline">\(2\)</span>”是系数，根本不会影响到整个程序的时间增长。同样地，<spanclass="math inline">\(\Theta(n^3+n^2)\)</span> 的复杂度也就是 <spanclass="math inline">\(\Theta(n^3)\)</span>的复杂度。因此，我们会说，一个 <spanclass="math inline">\(\Theta(0.01n^3)\)</span> 的程序的效率比 <spanclass="math inline">\(\Theta(100n^2)\)</span> 的效率低，尽管在 <spanclass="math inline">\(n\)</span>很小的时候，前者优于后者，但后者时间随数据规模增长得慢，最终 <spanclass="math inline">\(\Theta(n^3)\)</span> 的复杂度将远远超过 <spanclass="math inline">\(\Theta(n^2)\)</span>。我们也说，<spanclass="math inline">\(\Theta(n^{100})\)</span> 的复杂度小于 <spanclass="math inline">\(\Theta(1.01^n)\)</span> 的复杂度。<br />容易看出，前面的几类复杂度被分为两种级别，其中后者的复杂度无论如何都远远大于前者：一种是<span class="math inline">\(\Theta(1),\Theta(\logn),\Theta(n^a)\)</span> 等，我们把它叫做多项式级的复杂度，因为它的规模<span class="math inline">\(n\)</span> 出现在底数的位置；另一种是 <spanclass="math inline">\(\Theta(a^n)\)</span> 和 <spanclass="math inline">\(\Theta(n!)\)</span>型复杂度，它是非多项式级的，其复杂度计算机往往不能承受。当我们在解决一个问题时，我们选择的算法通常都需要是多项式级的复杂度，非多项式级的复杂度需要的时间太多，往往会超时，除非是数据规模非常小。</p><p>自然地，人们会想到一个问题：会不会所有的问题都可以找到复杂度为多项式级的算法呢？很遗憾，答案是否定的。有些问题甚至根本不可能找到一个正确的算法来，这称之为“不可解问题”（UndecidableDecision Problem）。The Halting Problem 就是一个著名的不可解问题，在我的Blog 上有过专门的介绍和证明<a href="#fn1" class="footnote-ref"id="fnref1" role="doc-noteref"><sup>1</sup></a>。再比如，输出从 <spanclass="math inline">\(1\)</span> 到 <spanclass="math inline">\(n\)</span> 这 <spanclass="math inline">\(n\)</span>个数的全排列。不管你用什么方法，你的复杂度都是阶乘级，因为你总得用阶乘级的时间打印出结果来。有人说，这样的“问题”不是一个“正规”的问题，正规的问题是让程序解决一个问题，输出一个“YES”或“NO”（这被称为判定性问题），或者一个什么什么的最优值（这被称为最优化问题）。那么，根据这个定义，我也能举出一个不大可能会有多项式级算法的问题来：Hamilton回路。问题是这样的：给你一个图，问你能否找到一条经过每个顶点一次且恰好一次（不遗漏也不重复）最后又走回来的路（满足这个条件的路径叫做Hamilton回路）。这个问题现在还没有找到多项式级的算法。事实上，这个问题就是我们后面要说的NPC 问题。</p><p>下面引入 P类问题的概念：如果一个问题可以找到一个能在多项式的时间里解决它的算法，那么这个问题就属于P 问题。P 是英文单词多项式的第一个字母。哪些问题是 P 类问题呢？通常 NOI和 NOIP 不会出不属于 P 类问题的题目。我们常见到的一些信息奥赛的题目都是P问题。道理很简单，一个用穷举换来的非多项式级时间的超时程序不会涵盖任何有价值的算法。<br />接下来引入 NP问题的概念。这个就有点难理解了，或者说容易理解错误。在这里强调（回到我竭力想澄清的误区上），NP问题不是非 P 类问题。NP 问题是指可以在多项式的时间里验证一个解的问题。NP问题的另一个定义是，可以在多项式的时间里猜出一个解的问题。比方说，我 RP很好，在程序中需要枚举时，我可以一猜一个准。现在某人拿到了一个求最短路径的问题，问从起点到终点是否有一条小于<span class="math inline">\(100\)</span>个单位长度的路线。它根据数据画好了图，但怎么也算不出来，于是来问我：你看怎么选条路走得最少？我说，我RP很好，肯定能随便给你指条很短的路出来。然后我就胡乱画了几条线，说就这条吧。那人按我指的这条把权值加起来一看，嘿，神了，路径长度<span class="math inline">\(98\)</span>，比 <spanclass="math inline">\(100\)</span> 小。于是答案出来了，存在比 <spanclass="math inline">\(100\)</span>小的路径。别人会问他这题怎么做出来的，他就可以说，因为我找到了一个比<span class="math inline">\(100\)</span>小的解。在这个题中，找一个解很困难，但验证一个解很容易。验证一个解只需要<span class="math inline">\(\Theta(n)\)</span>的时间复杂度，也就是说我可以花 <spanclass="math inline">\(\Theta(n)\)</span>的时间把我猜的路径的长度加出来。那么，只要我 RP好，猜得准，我一定能在多项式的时间里解决这个问题。我猜到的方案总是最优的，不满足题意的方案也不会来骗我去选它。这就是NP 问题。当然有不是 NP问题的问题，即你猜到了解但是没用，因为你不能在多项式的时间里去验证它。下面我要举的例子是一个经典的例子，它指出了一个目前还没有办法在多项式的时间里验证一个解的问题。很显然，前面所说的Hamilton 回路是 NP问题，因为验证一条路是否恰好经过了每一个顶点非常容易。但我要把问题换成这样：试问一个图中是否不存在Hamilton回路。这样问题就没法在多项式的时间里进行验证了，因为除非你试过所有的路，否则你不敢断定它“没有Hamilton 回路”。<br />之所以要定义 NP 问题，是因为通常只有 NP问题才可能找到多项式的算法。我们不会指望一个连多项式地验证一个解都不行的问题存在一个解决它的多项式级的算法。相信读者很快明白，信息学中的号称最困难的问题——“NP问题”，实际上是在探讨 NP 问题与 P 类问题的关系。</p><p>很显然，所有的 P 类问题都是 NP问题。也就是说，能多项式地解决一个问题，必然能多项式地验证一个问题的解——既然正解都出来了，验证任意给定的解也只需要比较一下就可以了。关键是，人们想知道，是否所有的NP 问题都是 P 类问题。我们可以再用集合的观点来说明。如果把所有 P类问题归为一个集合 <span class="math inline">\(\mathbb P\)</span>中，把所有 NP 问题划进另一个集合 <span class="math inline">\(\mathbb{NP}\)</span> 中，那么，显然有 <span class="math inline">\(\mathbb P \in\mathbb {NP}\)</span>。现在，所有对 NP问题的研究都集中在一个问题上，即究竟是否有 <spanclass="math inline">\(P=NP\)</span>？通常所谓的“NP问题”，其实就一句话：证明或推翻 <spanclass="math inline">\(P=NP\)</span>。<br />NP问题一直都是信息学的巅峰。巅峰，意即很引人注目但难以解决。在信息学研究中，这是一个耗费了很多时间和精力也没有解决的终极问题，好比物理学中的大统一和数学中的歌德巴赫猜想等。<br />目前为止这个问题还“啃不动”。但是，一个总的趋势、一个大方向是有的。人们普遍认为，<spanclass="math inline">\(P=NP\)</span>不成立，也就是说，多数人相信，存在至少一个不可能有多项式级复杂度的算法的NP 问题。人们如此坚信 <span class="math inline">\(P\neq NP\)</span>是有原因的，就是在研究 NP 问题的过程中找出了一类非常特殊的 NP 问题叫做NP-完全问题，也即所谓的 NPC 问题。C 是英文单词“完全”的第一个字母。正是NPC 问题的存在，使人们相信 <span class="math inline">\(P\neqNP\)</span>。下文将花大量篇幅介绍 NPC 问题，你从中可以体会到 NPC 问题使<span class="math inline">\(P=NP\)</span> 变得多么不可思议。</p><p>为了说明 NPC问题，我们先引入一个概念——约化（Reducibility，有的资料上叫“归约”）。<br />简单地说，一个问题 A 可以约化为问题 B 的含义即是，可以用问题 B的解法解决问题 A，或者说，问题 A 可以“变成”问题B。《算法导论》上举了这么一个例子。比如说，现在有两个问题：求解一个一元一次方程和求解一个一元二次方程。那么我们说，前者可以约化为后者，意即知道如何解一个一元二次方程那么一定能解出一元一次方程。我们可以写出两个程序分别对应两个问题，那么我们能找到一个“规则”，按照这个规则把解一元一次方程程序的输入数据变一下，用在解一元二次方程的程序上，两个程序总能得到一样的结果。这个规则即是：两个方程的对应项系数不变，一元二次方程的二次项系数为<spanclass="math inline">\(0\)</span>。按照这个规则把前一个问题转换成后一个问题，两个问题就等价了。同样地，我们可以说，Hamilton回路可以约化为 TSP 问题（Travelling Salesman Problem，旅行商问题）：在Hamilton 回路问题中，两点相连即这两点距离为 <spanclass="math inline">\(0\)</span>，两点不直接相连则令其距离为 <spanclass="math inline">\(1\)</span>，于是问题转化为在 TSP问题中，是否存在一条长为 <span class="math inline">\(0\)</span>的路径。Hamilton 回路存在当且仅当 TSP 问题中存在长为 <spanclass="math inline">\(0\)</span> 的回路。<br />“问题 A 可约化为问题 B”有一个重要的直观意义：B 的时间复杂度高于或者等于A 的时间复杂度。也就是说，问题 A 不比问题 B 难。这很容易理解。既然问题 A能用问题 B 来解决，倘若 B 的时间复杂度比 A 的时间复杂度还低了，那 A的算法就可以改进为 B的算法，两者的时间复杂度还是相同。正如解一元二次方程比解一元一次方程难，因为解决前者的方法可以用来解决后者。<br />很显然，约化具有一项重要的性质：约化具有传递性。如果问题 A 可约化为问题B，问题 B 可约化为问题 C，则问题 A 一定可约化为问题C。这个道理非常简单，就不必阐述了。现在再来说一下约化的标准概念就不难理解了：如果能找到这样一个变化法则，对任意一个程序A 的输入，都能按这个法则变换成程序 B的输入，使两程序的输出相同，那么我们说，问题 A 可约化为问题 B。<br />当然，我们所说的“可约化”是指的可“多项式地”约化（Polynomial-timeReducible），即变换输入的方法是能在多项式的时间里完成的。约化的过程只有用多项式的时间完成才有意义。</p><p>好了，从约化的定义中我们看到，一个问题约化为另一个问题，时间复杂度增加了，问题的应用范围也增大了。通过对某些问题的不断约化，我们能够不断寻找复杂度更高，但应用范围更广的算法来代替复杂度虽然低，但只能用于很小的一类问题的算法。再回想前面讲的P 和 NP问题，联想起约化的传递性，自然地，我们会想问，如果不断地约化上去，不断找到能“通吃”若干小NP 问题的一个稍复杂的大 NP问题，那么最后是否有可能找到一个时间复杂度最高，并且能“通吃”所有的 NP问题的这样一个超级 NP 问题？答案居然是肯定的。也就是说，存在这样一个 NP问题，所有的 NP问题都可以约化成它。换句话说，只要解决了这个问题，那么所有的 NP问题都解决了。这种问题的存在难以置信，并且更加不可思议的是，这种问题不只一个，它有很多个，它是一类问题。这一类问题就是传说中的NPC 问题，也就是 NP-完全问题。NPC 问题的出现使整个 NP问题的研究得到了飞跃式的发展。我们有理由相信，NPC问题是最复杂的问题。再次回到全文开头，我们可以看到，人们想表达一个问题不存在多项式的高效算法时应该说它“属于NPC 问题”。此时，我的目的终于达到了，我已经把 NP 问题和 NPC问题区别开了。到此为止，本文已经写了近 5000字了，我佩服你还能看到这里来，同时也佩服一下自己能写到这里来。</p><p>NPC 问题的定义非常简单。同时满足下面两个条件的问题就是 NPC问题。首先，它得是一个 NP 问题；然后，所有的 NP问题都可以约化到它。证明一个问题是 NPC 问题也很简单。先证明它至少是一个NP 问题，再证明其中一个已知的 NPC 问题能约化到它（由约化的传递性，则 NPC问题定义的第二条也得以满足；至于第一个 NPC问题是怎么来的，下文将介绍），这样就可以说它是 NPC 问题了。<br />既然所有的 NP 问题都能约化成 NPC 问题，那么只要任意一个 NPC问题找到了一个多项式的算法，那么所有的 NP 问题都能用这个算法解决了，NP也就等于 P 了。因此，给 NPC找一个多项式算法太不可思议了。因此，前文才说，“正是 NPC问题的存在，使人们相信 <span class="math inline">\(P\neqNP\)</span>”。我们可以就此直观地理解，NPC问题目前没有多项式的有效算法，只能用指数级甚至阶乘级复杂度的搜索。</p><p>顺便讲一下 NP-Hard 问题。NP-Hard 问题是这样一种问题，它满足 NPC问题定义的第二条但不一定要满足第一条（就是说，NP-Hard 问题要比 NPC问题的范围广）。NP-Hard问题同样难以找到多项式的算法，但它不列入我们的研究范围，因为它不一定是NP 问题。即使 NPC 问题发现了多项式级的算法，NP-Hard问题有可能仍然无法得到多项式级的算法。事实上，由于 NP-Hard放宽了限定条件，它将有可能比所有的 NPC问题的时间复杂度更高从而更难以解决。</p><p>不要以为 NPC 问题是一纸空谈。NPC问题是存在的。确实有这么一个非常具体的问题属于 NPC问题。下文即将介绍它。<br />下文即将介绍逻辑电路问题。这是第一个 NPC 问题。其它的 NPC问题都是由这个问题约化而来的。因此，逻辑电路问题是 NPC类问题的“鼻祖”。<br />逻辑电路问题是指的这样一个问题：给定一个逻辑电路，问是否存在一种输入使输出为True。<br />什么叫做逻辑电路呢？一个逻辑电路由若干个输入，一个输出，若干“逻辑门”和密密麻麻的线组成。看下面一例，不需要解释你马上就明白了。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">┌────┐</span><br><span class="line">│IN 1├──┐  ┌────┐</span><br><span class="line">└────┘  └─→┤    │</span><br><span class="line">           │ or ├→───┐</span><br><span class="line">┌────┐  ┌─→┤    │    │  ┌────┐</span><br><span class="line">│IN 2├──┘  └────┘    └─→┤    │</span><br><span class="line">└────┘                  │ AND├──→OUT</span><br><span class="line">                  ┌────→┤    │</span><br><span class="line">┌────┐  ┌────┐    │     └────┘</span><br><span class="line">│IN 3├─→┤NOT ├─→──┘</span><br><span class="line">└────┘  └────┘</span><br></pre></td></tr></table></figure><p>这是个较简单的逻辑电路，当输入 1、输入 2、输入 3 分别为True、True、False 或 False、True、False 时，输出为 True。有输出无论如何都不可能为 True的逻辑电路吗？有。下面就是一个简单的例子。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">┌────┐</span><br><span class="line">│IN 1├→─┐  ┌────┐</span><br><span class="line">└────┘  └─→┤    │</span><br><span class="line">           │AND ├─→┐</span><br><span class="line">        ┌─→┤    │  │</span><br><span class="line">        │  └────┘  │ ┌────┐</span><br><span class="line">        │          └→┤    │</span><br><span class="line">┌────┐  │            │AND ├─→OUT</span><br><span class="line">│IN 2├→─┤ ┌────┐   ┌→┤    │</span><br><span class="line">└────┘  └→┤NOT ├→──┘ └────┘</span><br><span class="line">          └────┘</span><br></pre></td></tr></table></figure><p>上面这个逻辑电路中，无论输入是什么，输出都是False。我们就说，这个逻辑电路不存在使输出为 True 的一组输入。回到上文，给定一个逻辑电路，问是否存在一种输入使输出为True，这即逻辑电路问题。 逻辑电路问题属于 NPC问题。这是有严格证明的。它显然属于 NP 问题，并且可以直接证明所有的 NP问题都可以约化到它（不要以为 NP问题有无穷多个将给证明造成不可逾越的困难）。证明过程相当复杂，其大概意思是说任意一个NP问题的输入和输出都可以转换成逻辑电路的输入和输出（想想计算机内部也不过是一些0 和 1 的运算），因此对于一个 NP 问题来说，问题转化为了求出满足结果为True 的一个输入（即一个可行解）。</p><p>有了第一个 NPC 问题后，一大堆 NPC 问题就出现了，因为再证明一个新的NPC 问题只需要将一个已知的 NPC 问题约化到它就行了。后来，Hamilton回路成了 NPC 问题，TSP 问题也成了 NPC 问题。现在被证明是 NPC问题的有很多，任何一个找到了多项式算法的话所有的 NP问题都可以完美解决了。因此说，正是因为 NPC 问题的存在，<spanclass="math inline">\(P=NP\)</span> 变得难以置信。<spanclass="math inline">\(P=NP\)</span>问题还有许多有趣的东西，有待大家自己进一步的挖掘。攀登这个信息学的巅峰是我们这一代的终极目标。现在我们需要做的，至少是不要把概念弄混淆了。</p><section id="footnotes" class="footnotes footnotes-end-of-document"role="doc-endnotes"><hr /><ol><liid="fn1"><p>编者注：即停机问题。大体来说就是判断一个程序是否是死循环。本文原作者博客的证明：<ahref="https://www.matrix67.com/blog/archives/55">传送门</a><ahref="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">对 P NP NPC NP-Hard 问题的解释，深度好文</summary>
    
    
    
    <category term="杂文" scheme="http://blog.tibrella.space/categories/%E6%9D%82%E6%96%87/"/>
    
    
  </entry>
  
  <entry>
    <title>KMP 详解</title>
    <link href="http://blog.tibrella.space/post/KMP/"/>
    <id>http://blog.tibrella.space/post/KMP/</id>
    <published>2023-08-04T03:49:41.000Z</published>
    <updated>2023-10-05T09:55:05.281Z</updated>
    
    <content type="html"><![CDATA[<p>今天是 8 月 4 日喵，距离退役还有 3 个月左右喵。<br />写字符串的时候感觉突然悟了，于是来写笔记。</p><h2 id="前缀函数">前缀函数</h2><p>实际上看了 OI Wiki 才知道有这个名词……但是也应该有，因为 KMP只是一个匹配算法。</p><h3 id="定义">定义</h3><p>约定：为了方便表示，此处字符串下标从 <spanclass="math inline">\(1\)</span> 开始；对于串 <spanclass="math inline">\(X\)</span>，<spanclass="math inline">\(X(i,j)\)</span> 表示截取 <spanclass="math inline">\(X\)</span> 的 <spanclass="math inline">\([i,j]\)</span> 段。</p><p>对于串 <span class="math inline">\(S\)</span>，其前缀函数 <spanclass="math inline">\(P\)</span> 为一个数组，<spanclass="math inline">\(P_i\)</span> 表示 <spanclass="math inline">\(S(1,i)\)</span> 这段前缀字符串的最长border（不包含 <span class="math inline">\(S(1,i)\)</span> 本身）。</p><p>呃，什么是 border？简单来说，串 <spanclass="math inline">\(X\)</span> 的一个 border 就是 <spanclass="math inline">\(X\)</span> 的一对相等的前缀后缀。</p><p>举个例子，设 <span class="math inline">\(X =\texttt{abcssfabc}\)</span>，则 <spanclass="math inline">\(\texttt{abc}\)</span> 是 <spanclass="math inline">\(X\)</span> 的一个 border。同样的，对于 <spanclass="math inline">\(\texttt{abbabba}\)</span> 来说，存在三个border，即 <spanclass="math inline">\(\texttt{a},\texttt{abba},\texttt{abbabba}\)</span><ahref="#fn1" class="footnote-ref" id="fnref1"role="doc-noteref"><sup>1</sup></a>。</p><p>好了，现在我们设 <spanclass="math inline">\(S=\texttt{abcabc}\)</span>，尝试一下求 <spanclass="math inline">\(S\)</span>的前缀函数？如果上面的概念你还没有理解，可以直接参考下图。</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64cced901ddac507ccfe0344"/></div></div><p>于是，<span class="math inline">\(S\)</span> 的前缀函数则为 <spanclass="math inline">\(\left[0,0,0,1,2,3\right]\)</span></p><h3 id="性质">性质</h3><p>遇到 border 相关要对“跳 border”敏感一些。啥叫跳 border 呢？</p><p>拿 <span class="math inline">\(\texttt{abbabba}\)</span>来举例子，假设其前缀函数为 <spanclass="math inline">\(P\)</span>，容易求得 <spanclass="math inline">\(P=[0,0,0,1,2,3,4]\)</span>。<br />末尾的 <span class="math inline">\(P_7 = 4\)</span>，代表的是 <spanclass="math inline">\(\texttt{abba}\)</span> 这个border。然后就出现了一个神奇的事情：如果把该串所有合法的 border按照长度排序，则 <span class="math inline">\(\texttt{abba}\)</span>的上一个 border 就是 <span class="math inline">\(\text a\)</span>。</p><p>那这个 <span class="math inline">\(\texttt a\)</span> 是什么呢？就是<span class="math inline">\(P_{P_7}\)</span> 代表的 border 喵。</p><p>换句话说，如果 <span class="math inline">\(X\)</span> 的前缀函数<span class="math inline">\(P\)</span> 已知，则：令 <spanclass="math inline">\(m\gets X.length\)</span>，反复让 <spanclass="math inline">\(m \getsP_m\)</span>，则能够按长度从大到小遍历完原串所有的合法border。这就是所谓“跳 border”。</p><h3 id="线性递推求前缀函数">线性递推求前缀函数</h3><p>实际上如果前面的东西你都明白了，这个部分是可以轻松想出来的。</p><p>以防万一，我还是写一份思考过程。</p><p>假设目前位置是 <span class="math inline">\(i\)</span>，<spanclass="math inline">\(i-1\)</span> 以及之前的 <spanclass="math inline">\(P\)</span> 都已知，再令 <spanclass="math inline">\(j\gets P_{i-1}\)</span>，想办法递推求出 <spanclass="math inline">\(P_i\)</span>。</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64ccf7bb1ddac507cc16d2e7"/></div></div><p>如果红色部分是 <span class="math inline">\(S(1,i)\)</span> 的合法border 且长度不为 <spanclass="math inline">\(1\)</span>，则蓝色部分必然是 <spanclass="math inline">\(S(1,i-1)\)</span> 的合法 border。</p><p>因此，我们可以枚举 <span class="math inline">\(S(1,i-1)\)</span>的所有合法 border，看看能不能接上 <spanclass="math inline">\(S_i\)</span> 这个字母形成 <spanclass="math inline">\(S(1,i)\)</span> 的 border。遍历 border可以采用上面说过的跳 border实现。由于这个过程是从长到短遍历，因此找到的合法 border 必然是最长的合法border。</p><p>于是可以反复使 <span class="math inline">\(j \gets P_j\)</span> 直到<span class="math inline">\(S(1,j+1)\)</span> 成为 <spanclass="math inline">\(S(1,i)\)</span> 的合法 border。</p><p>细节见代码实现。代码实现中认为下标从 <spanclass="math inline">\(0\)</span> 开始，<code>nxt</code> 数组即为 <spanclass="math inline">\(P\)</span>。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">get_nxt</span><span class="params">(string&amp; str)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>, j = <span class="number">0</span>; i &lt; str.<span class="built_in">size</span>(); ++i) &#123;</span><br><span class="line">        <span class="keyword">while</span> (j &amp;&amp; str[i - <span class="number">1</span>] != str[j])</span><br><span class="line">            j = nxt[j];  <span class="comment">// 如果 border 不合法就跳，直到没 border 为止</span></span><br><span class="line">        <span class="keyword">if</span> (str[i - <span class="number">1</span>] == str[j])</span><br><span class="line">            ++j;  <span class="comment">/* 实际上上面循环完之后的 j 可能是合法 border</span></span><br><span class="line"><span class="comment">                   的结尾也有可能是无解（0），同时如果 border 合</span></span><br><span class="line"><span class="comment">                   法，需要 +1（因为跳 border 的时候跳的是之前的</span></span><br><span class="line"><span class="comment">                   border）*/</span></span><br><span class="line"></span><br><span class="line">        nxt[i] = j;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><details class="tag-plugin colorful folding" color="cyan"><summary><span>证明</span></summary><div class="body"><p>口胡一个。</p><p>每次 <span class="math inline">\(j\)</span> 指针最多右移 <spanclass="math inline">\(1\)</span>，除此以外 <spanclass="math inline">\(j\)</span> 指针只会左移。<br />容易发现右移最多 <span class="math inline">\(\Theta(n)\)</span>次，则左移也不会超过 <span class="math inline">\(\Theta(n)\)</span>次。<br />计算前缀函数的过程只涉及 <span class="math inline">\(j\)</span>指针的左移右移，所以复杂度 <spanclass="math inline">\(\Theta(n)\)</span>。</p></div></details><h2 id="kmp-字符串匹配算法">KMP 字符串匹配算法</h2><p>总的来说：借助前缀函数实现时空复杂度均为 <spanclass="math inline">\(\operatorname{O}(n+m)\)</span> 的字符串匹配。</p><p>过程和上面递推前缀函数差不多。</p><p>具体来讲，为什么要借助前缀函数呢？</p><p>假设我们有一个文本串 <spanclass="math inline">\(S\)</span>，一个模式串 <spanclass="math inline">\(T\)</span>，接下来需要找出 <spanclass="math inline">\(S\)</span> 中 <spanclass="math inline">\(T\)</span> 出现的所有位置。一旦到 <spanclass="math inline">\(T\)</span> 的 <spanclass="math inline">\(i\)</span> 这个位置匹配失败，我们难道一定就要返回<span class="math inline">\(T_1\)</span> 从头匹配吗？</p><p>并不是，我们可以利用 <span class="math inline">\(T(1,i)\)</span> 的border 来减少匹配次数。border 的定义是“相等的前缀和后缀”，那 <spanclass="math inline">\(T(1,i)\)</span>的这个后缀匹配成功，其对应的前缀也一定能匹配成功，只需要从这个前缀结束的位置继续匹配即可。</p><p>做了个动画： <div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64cd02b21ddac507cc33adee"/></div></div></p><p>代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>, j = <span class="number">0</span>; i &lt; s.<span class="built_in">size</span>(); ++i) &#123;</span><br><span class="line">    <span class="keyword">while</span> (j &amp;&amp; s[i] != t[j])</span><br><span class="line">        j = nxt[j];</span><br><span class="line">    <span class="keyword">if</span> (s[i] == t[j]) ++j;</span><br><span class="line">    <span class="keyword">if</span> (j == t.<span class="built_in">size</span>()) <span class="comment">// 匹配成功</span></span><br><span class="line">        cout &lt;&lt; i - (<span class="type">int</span>)t.<span class="built_in">size</span>() + <span class="number">2</span> &lt;&lt; <span class="string">&#x27;\n&#x27;</span>; <span class="comment">// 答案要求</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><section id="footnotes" class="footnotes footnotes-end-of-document"role="doc-endnotes"><hr /><ol><li id="fn1"><p>border 在能不能包含原串这方面没有权威定义，实际上 border这个概念就只是有一部分人在用，仅用于理解。<a href="#fnref1"class="footnote-back" role="doc-backlink">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">线性时间复杂度的字符串匹配。这玩意实际上比 AC 自动机简单。</summary>
    
    
    
    <category term="字符串" scheme="http://blog.tibrella.space/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    
    
    <category term="KMP" scheme="http://blog.tibrella.space/tags/KMP/"/>
    
    <category term="exKMP" scheme="http://blog.tibrella.space/tags/exKMP/"/>
    
  </entry>
  
  <entry>
    <title>二项式反演</title>
    <link href="http://blog.tibrella.space/post/binomial-inversion/"/>
    <id>http://blog.tibrella.space/post/binomial-inversion/</id>
    <published>2023-07-21T04:55:23.000Z</published>
    <updated>2023-08-19T03:09:16.374Z</updated>
    
    <content type="html"><![CDATA[<h2 id="定义">定义</h2><p>直接给出式子：</p><p><span class="math display">\[g_i=\sum_{k\geqslant i}\binom ki f_k \rightarrow f_i=\sum_{k\geqslanti}\binom ki (-1)^{k-i}g_k\]</span></p><p>看起来不知道咋用？待会再说，我们先证明。</p><h2 id="证明">证明</h2><h3 id="前置知识">前置知识</h3><p><a href="/post/basic-combinatorics#常用记号">组合数学常用记号</a></p><p><a href="/post/basic-combinatorics#二项式定理">二项式定理</a></p><h3 id="正文">正文</h3><p>已知：<span class="math inline">\(g_i=\sum_{k\geqslant i}\binom kif_k\)</span><br />求证：<span class="math inline">\(f_i=\sum_{k\geqslant i}\binom ki(-1)^{k-i}g_k\)</span></p><p>把求证右边那个式子单独拿出来，想办法把它导成 <spanclass="math inline">\(f_i\)</span>。</p><p><span class="math display">\[\begin{aligned}&amp;\sum_{k\geqslant i}\binom ki (-1)^{k-i}g_k \newline=&amp;\sum_{k\geqslant i}\binom ki (-1)^{k-i} \sum_{j\geqslant k}\binomjk f_j\end{aligned}\]</span></p><p>把第二个求和号以及 <span class="math inline">\(f_j\)</span>挪动一下，感性理解一下，发现最后每一项出现的次数还是不变的，所以正确。</p><p><span class="math display">\[\begin{aligned}&amp;\sum_{k\geqslant i}\binom ki (-1)^{k-i} \sum_{j\geqslant k}\binomjk f_j \newline=&amp;\sum_{j\geqslant i}f_j \sum_{j\geqslant k \geqslant i}\binom jk\binom ki (-1)^{k-i} \newline=&amp;\sum_{j\geqslant i}f_j \sum_{j\geqslant k \geqslanti}\frac{j!k!}{k!(j-k)!i!(k-i)!} (-1)^{k-i} \newline=&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!}\sum_{j\geqslant k \geqslanti}\frac 1{(j-k)!(k-i)!} (-1)^{k-i}\end{aligned}\]</span></p><p>展开之后不知道怎么往下算了，想办法把那个大分式化成点什么，比如二项式（组合数）。</p><p>上下同乘 <span class="math inline">\((j-i)!\)</span>，然后 <spanclass="math inline">\((j-k)=[(j-i)-(k-i)]\)</span>，然后就可以轻松化成组合数形式继续往下导公式了。</p><p><span class="math display">\[\begin{aligned}&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!} \sum_{j\geqslant k \geqslanti}\frac 1{(j-k)!(k-i)!} (-1)^{k-i} \newline=&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!} \sum_{j\geqslant k \geqslanti}\frac {(j-i)!}{[(j-i)-(k-i)]!(k-i)!(j-i)!} (-1)^{k-i} \newline=&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!} \sum_{j\geqslant k \geqslanti}\frac {\binom{j-i}{k-i}}{(j-i)!} (-1)^{k-i} \newline\end{aligned}\]</span></p><p>换个形式</p><p><span class="math display">\[\sum_{j\geqslant i}f_j\frac{j!}{i!} \cdot \frac{\sum_{j\geqslant k\geqslant i } \binom{j-i}{k-i} (-1)^{k-i}}{(j-i)!}\]</span></p><p>大分式上面的分母可以用 <ahref="/post/basic-combinatorics#二项式定理">二项式定理</a>处理掉。想不出来可以直接看下面式子。</p><p><span class="math display">\[\begin{aligned}&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!} \cdot \frac{\sum_{j\geqslant k\geqslant i } \binom{j-i}{k-i} (-1)^{k-i}}{(j-i)!} \newline=&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!} \cdot\frac{(1-1)^{j-i}}{(j-i)!}\end{aligned}\]</span></p><p>右下方的分母和左侧分数扔一起是一个非常典型的组合数。</p><p><span class="math display">\[\begin{aligned}&amp;\sum_{j\geqslant i}f_j\frac{j!}{i!} \cdot\frac{(1-1)^{j-i}}{(j-i)!} \newline=&amp;\sum_{j\geqslant i}f_j\binom ji 0^{j-i}\end{aligned}\]</span></p><p>注意，在组合数学中，<spanclass="math inline">\(0^0=1\)</span>。<br />于是很容易地发现，求和号右边的式子值非零，当且仅当 <spanclass="math inline">\(i=j\)</span>。</p><p>于是就出来了：</p><p><span class="math display">\[\begin{aligned}&amp;\sum_{j\geqslant i}f_j\binom ji 0^{j-i}\newline=&amp;f_i\binom ii 0^0 \newline=&amp;f_i\end{aligned}\]</span></p><p>证毕。</p><h2 id="例题">例题</h2><h3 id="分特产">分特产</h3><p><a href="https://www.luogu.com.cn/problem/P5505">题目链接</a></p><h4 id="题意">题意</h4><p><span class="math inline">\(m\)</span> 种特产，<spanclass="math inline">\(n\)</span> 个人，每种特产 <spanclass="math inline">\(s_i\)</span> 个。</p><p>人有标号，两个特产相同当且仅当其种类相同。</p><p>求让每个人都拿到至少一份特产的分配方案总数，答案对 <spanclass="math inline">\(10^9+7\)</span> 取模。</p><h4 id="题解">题解</h4><p>首先，记住开头给你的式子。</p><p>然后把下面的东西看一遍，这应该是二项式反演的基本套路。</p><p>大体上设两个东西，<span class="math inline">\(g_i\)</span> 和 <spanclass="math inline">\(f_i\)</span>。这里设 <spanclass="math inline">\(g_i\)</span> 为<strong>钦定</strong> <spanclass="math inline">\(i\)</span>个人不满足要求（即没有特产），其他人随意分配（可能也没有）的方案数，然后<span class="math inline">\(f_i\)</span> 为<strong>恰好</strong> <spanclass="math inline">\(i\)</span>个人没有特产，其他人都有特产的方案总数。</p><p>然后模仿条件式，用 <span class="math inline">\(f_i\)</span> 表示<span class="math inline">\(g_i\)</span>。</p><p>容易发现：</p><ol type="1"><li>对于 <span class="math inline">\(g_i\)</span>，每一个 <spanclass="math inline">\(f_j,j\in [i,n]\)</span> 都一定被包含在里面。</li><li>对于每一个包含在里面的 <spanclass="math inline">\(f_j\)</span>，被钦定的集合会不同，一共有 <spanclass="math inline">\(\binom ji\)</span> 个，所以每个 <spanclass="math inline">\(f_j\)</span> 被算了 <spanclass="math inline">\(\binom ji\)</span> 次。</li></ol><p>综上，<span class="math inline">\(g_i = \sum_{j=i}^n \binom jif_j\)</span>。</p><p>二项式反演的常见入手点就是设 <spanclass="math inline">\(g,f\)</span>。其中 <spanclass="math inline">\(g_i\)</span> 表示钦定集合中 <spanclass="math inline">\(i\)</span> 个元素满足/不满足要求，其他不管；<spanclass="math inline">\(f_i\)</span> 表示集合中恰好 <spanclass="math inline">\(i\)</span> 个元素满足/不满足要求（是否满足与 <spanclass="math inline">\(g\)</span> 一致）。在此之后用 <spanclass="math inline">\(f_i\)</span> 表示 <spanclass="math inline">\(g_i\)</span>，二项式反演之后问题就变化成了求 <spanclass="math inline">\(g\)</span>。</p><p>然后大力二项式反演，套一下式子就能得出来：</p><p><span class="math display">\[f_i = \sum_{j=i}^n \binom ji g_j (-1)^{j-i}\]</span></p><p>答案就是恰好没有不合法元素的方案总数，于是：</p><p><span class="math display">\[\begin{aligned}ans &amp;= f_0 \newline&amp;= \sum_{j=0}^n \binom j0 g_j (-1)^{j-0} \newline&amp;= \sum_{j=0}^n g_j (-1)^j\end{aligned}\]</span></p><p>问题被转化成了求 <span class="math inline">\(g\)</span>。</p><p>假设目前正在求 <span class="math inline">\(g_i\)</span>，显然只需要有<span class="math inline">\(n-i\)</span> 个人需要考虑。由于剩下 <spanclass="math inline">\(i-1\)</span>是钦定不合法的，我们不用考虑；但是非常魔法的事情来了：由于这 <spanclass="math inline">\(n-i\)</span>个人是随便排列的，我们只需要把特产随意分发就行。</p><p>怎么个随意分发法？对于每一种特产 <spanclass="math inline">\(x\)</span>，可以转化为：有 <spanclass="math inline">\(s_x\)</span> 个无标号小球，装进 <spanclass="math inline">\(n-i\)</span>个有标号盒子里的方案数。直接插板法解决就好啦！显然有 <spanclass="math inline">\(s_x-1\)</span>个空，除此以外，盒子可以为空，所以就是 <spanclass="math inline">\(s_x-1\)</span> 加 <spanclass="math inline">\(n-i\)</span> 个空，插 <spanclass="math inline">\(n-i-1\)</span> 个板的方案数，即 <spanclass="math inline">\(\binom{s_x-1+n-i}{n-i-1}\)</span>。</p><p>每一种特产的分配方案数互不影响，所以乘起来；同时 <spanclass="math inline">\(n\)</span> 个人里面选出来 <spanclass="math inline">\(i\)</span> 个钦定不合法，再乘一个 <spanclass="math inline">\(\binom ni\)</span>，即：</p><p><span class="math display">\[g_i = \binom ni \prod_{j=1}^m \binom{s_x-1+n-i}{n-i-1}\]</span></p><p>然后代入上面计算答案的式子算就完了。</p><h4 id="代码">代码</h4><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">using</span> std::cin;</span><br><span class="line"><span class="keyword">using</span> std::cout;</span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> tkgs &#123;</span><br><span class="line">    <span class="keyword">using</span> i32 = <span class="type">int</span>;</span><br><span class="line">    <span class="keyword">using</span> i64 = <span class="type">long</span> <span class="type">long</span>;</span><br><span class="line">    <span class="type">const</span> i64 mod = <span class="number">1e9</span> + <span class="number">7</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> N 1005</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> NUM 2005</span></span><br><span class="line">    <span class="keyword">namespace</span> mat &#123;</span><br><span class="line">        i64 x, y, tmp;</span><br><span class="line">        <span class="function"><span class="type">void</span> <span class="title">exgcd</span><span class="params">(<span class="type">const</span> i64&amp; a, <span class="type">const</span> i64&amp; b)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">if</span> (!b) &#123;</span><br><span class="line">                x = <span class="number">1</span>, y = <span class="number">0</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                <span class="built_in">exgcd</span>(b, a % b);</span><br><span class="line">                tmp = x;</span><br><span class="line">                x = y;</span><br><span class="line">                y = tmp - a / b * y;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function">i64 <span class="title">inv</span><span class="params">(<span class="type">const</span> i64&amp; a, <span class="type">const</span> i64&amp; b)</span> </span>&#123;</span><br><span class="line">            <span class="built_in">exgcd</span>(a, b);</span><br><span class="line">            <span class="keyword">return</span> x;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        i64 fact[NUM], ifact[NUM];</span><br><span class="line"></span><br><span class="line">        <span class="function"><span class="type">void</span> <span class="title">init</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="type">const</span> i32 n = <span class="number">2000</span>;</span><br><span class="line">            fact[<span class="number">0</span>] = ifact[<span class="number">0</span>] = <span class="number">1</span>;</span><br><span class="line">            <span class="keyword">for</span> (i32 i = <span class="number">1</span>; i &lt;= n; ++i)</span><br><span class="line">                fact[i] = fact[i - <span class="number">1</span>] * i % mod;</span><br><span class="line">            ifact[n] = <span class="built_in">inv</span>(fact[n], mod);</span><br><span class="line">            <span class="keyword">for</span> (i32 i = n - <span class="number">1</span>; i; --i)</span><br><span class="line">                ifact[i] = ifact[i + <span class="number">1</span>] * (i + <span class="number">1</span>) % mod;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="function">i64 <span class="title">getC</span><span class="params">(i32 a, i32 b)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">if</span> (a &lt; b)</span><br><span class="line">                <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">else</span></span><br><span class="line">                <span class="keyword">return</span> fact[a] * ifact[b] % mod * ifact[a - b] % mod;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;  <span class="comment">// namespace mat 实现了初始化阶乘与阶乘逆元，求组合数</span></span><br><span class="line"></span><br><span class="line">    i32 s[N], n, m;</span><br><span class="line"></span><br><span class="line">    <span class="function">i64 <span class="title">g</span><span class="params">(i64 i)</span> </span>&#123;</span><br><span class="line">        i64 res = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">for</span> (i32 j = <span class="number">1</span>; j &lt;= m; ++j)</span><br><span class="line">            res = res * mat::<span class="built_in">getC</span>(s[j] + n - i - <span class="number">1</span>, n - i - <span class="number">1</span>) % mod;</span><br><span class="line">        res = res * mat::<span class="built_in">getC</span>(n, i) % mod;</span><br><span class="line">        <span class="keyword">return</span> res;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        cin &gt;&gt; n &gt;&gt; m;</span><br><span class="line">        <span class="keyword">for</span> (i32 i = <span class="number">1</span>; i &lt;= m; ++i)</span><br><span class="line">            cin &gt;&gt; s[i];</span><br><span class="line">        mat::<span class="built_in">init</span>();</span><br><span class="line">        i64 ans = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">for</span> (i32 j = <span class="number">0</span>; j &lt; n; ++j)</span><br><span class="line">            (ans += <span class="built_in">g</span>(j) * ((j &amp; <span class="number">1</span>) ? <span class="number">-1</span> : <span class="number">1</span>)) %= mod;</span><br><span class="line">        cout &lt;&lt; (ans % mod + mod) % mod;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;  <span class="comment">// namespace tkgs</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    tkgs::<span class="built_in">main</span>();</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="abc309g"><a href="/post/abc309g-solution">ABC309G</a></h3>]]></content>
    
    
    <summary type="html">炫酷二项式反演魔法。</summary>
    
    
    
    <category term="数学" scheme="http://blog.tibrella.space/categories/%E6%95%B0%E5%AD%A6/"/>
    
    
    <category term="组合数学" scheme="http://blog.tibrella.space/tags/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/"/>
    
    <category term="容斥" scheme="http://blog.tibrella.space/tags/%E5%AE%B9%E6%96%A5/"/>
    
    <category term="二项式反演" scheme="http://blog.tibrella.space/tags/%E4%BA%8C%E9%A1%B9%E5%BC%8F%E5%8F%8D%E6%BC%94/"/>
    
  </entry>
  
  <entry>
    <title>ABC309G Ban Permutation 题解</title>
    <link href="http://blog.tibrella.space/post/abc309g-solution/"/>
    <id>http://blog.tibrella.space/post/abc309g-solution/</id>
    <published>2023-07-19T05:06:28.000Z</published>
    <updated>2023-11-08T08:49:42.134Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前置知识">前置知识</h2><ul><li>二项式反演（以后会补上笔记的！）</li><li>状压 DP</li></ul><h2 id="符号约定">符号约定</h2><p><span class="math inline">\(\lor\)</span> 为逻辑或，相当于 C++ 中的<code>|</code> 运算符。</p><h2 id="题意简化">题意简化</h2><p>求 <span class="math inline">\(n\)</span> 的排列 <spanclass="math inline">\(P\)</span> 的方案数，要求 <spanclass="math inline">\(\lvert P_i - i \rvert \geqslant X\)</span>。</p><h2 id="分析">分析</h2><p>排列问题有点抽象，于是有套路：把一个排列看成 <spanclass="math inline">\(n\times n\)</span> 的棋盘上放车的方案——数字 <spanclass="math inline">\(p_i\)</span> 在 <spanclass="math inline">\(i\)</span> 的位置上，相当于棋盘的 <spanclass="math inline">\((i,p_i)\)</span> 位置放了一个车。显然 <spanclass="math inline">\(i,p_i\)</span> 都不会重复，所以正确。</p><p>然后发现是个计数题，给出了<strong>不合法</strong>的条件，而且条件与位置相关，往二项式反演方向思考。</p><p>按照套路，设 <span class="math inline">\(g_i\)</span> 为钦定 <spanclass="math inline">\(i\)</span> 个数不合法，其他随便排的方案数，<spanclass="math inline">\(f_i\)</span> 为恰好 <spanclass="math inline">\(i\)</span> 个数不合法的方案数。发现每个 <spanclass="math inline">\(g_i\)</span> 包含 <spanclass="math inline">\(f_j,j\in[i,n]\)</span>，然后每次选择不同的 <spanclass="math inline">\(i\)</span> 个数进行钦定，所以一个 <spanclass="math inline">\(f_j\)</span> 要算 <spanclass="math inline">\(\binom ji\)</span> 次。于是有：</p><p><span class="math display">\[g_i=\sum_{j=i}^n f_i \binom ji\]</span></p><p>脸上都写上二项式反演这五个字了，套一下式子，有：</p><p><span class="math display">\[f_i = \sum_{j=i}^n(-1)^{j-i}\binom ji g_j\]</span></p><p>答案即为：</p><p><span class="math display">\[\begin{aligned}ans &amp;= f_0 \newline    &amp;= \sum_{j=0}^n(-1)^{j-0}\binom j0 g_j \newline    &amp;= \sum_{j=0}^n(-1)^j g_j\end{aligned}\]</span></p><p>考虑怎么求 <span class="math inline">\(g_i\)</span>。</p><p>回过头来看不合法的条件：<span class="math inline">\(\lvert P_i - i\rvert &lt; X\)</span>，转化为 <span class="math inline">\(P_i\)</span>这个棋子不能放在 <span class="math inline">\([i-X+1,i+X-1]\)</span>这些横行上。</p><p>发现 <span class="math inline">\(X\leqslant5\)</span>，非常小，也许可以状压？结合上面的转化，如果要状压的话，状态可以直接表示<span class="math inline">\([i-X+1,i+X-1]\)</span>中不合法棋子的集合。</p><p>然后 DP 状态设计就有了（这里我们开出来一个新的 <spanclass="math inline">\(f\)</span> 数组）：<spanclass="math inline">\(f_{i,j,s}\)</span> 表示棋盘前 <spanclass="math inline">\(i\)</span> 行，放了 <spanclass="math inline">\(j\)</span> 个不合法棋子，状态为 <spanclass="math inline">\(s\)</span> 的方案数量。</p><p>怎么转移？假设当前是 <spanclass="math inline">\(f_{i,j,s}\)</span>，然后分类讨论：</p><ul><li>不放新的不合法棋子。 发现这种情况下转移到 <spanclass="math inline">\(i+1\)</span> 时，新状态就是 <spanclass="math inline">\(s\)</span> 右移一位，即 <spanclass="math inline">\(2^{-1}s\)</span>。 为了方便，我们设这个状态为<span class="math inline">\(ns\)</span>，表示什么都不放时转移到 <spanclass="math inline">\(i+1\)</span> 时的状态。 转移方程也就简单了：<spanclass="math inline">\(f_{i,j,ns} \gets(f_{i,j,ns}+f_{i,j,s})\)</span></li><li>放新的不合法棋子。 可以枚举一下这个不合法棋子能放到的位置（就是<span class="math inline">\([i-X+1,i+X-1]\)</span>区间的空位），直接转移即可。但是注意，当前要做的是向后转移，所以应当枚举的是<span class="math inline">\(ns\)</span> 这个状态的空位。假设 <spanclass="math inline">\(p\)</span> 是一个空位，则有转移方程：<spanclass="math inline">\(f_{i,j,(ns\lor 2^p)} \gets (f_{i,j,(ns\lor2^p)}+f_{i,j,s})\)</span>。</li></ul><p>对于 <span class="math inline">\(g_i\)</span>，由于是钦定了 <spanclass="math inline">\(i\)</span>个位置不合法，其他位置随便排，所以我们对 <spanclass="math inline">\(f_{n,i,s}\)</span> 枚举 <spanclass="math inline">\(s\)</span> 求和之后，还要乘一个 <spanclass="math inline">\((n-i)!\)</span> 满足“剩下位置随便排列”的要求。</p><p>按照前面 <span class="math inline">\(ans\)</span>的式子求和即可。</p><p>时间复杂度 <spanclass="math inline">\(\operatorname\Theta(4^Xn^2)\)</span>。</p><p>Code:</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> std::io;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">valid</span>(pos: <span class="type">usize</span>, i: <span class="type">usize</span>, n: <span class="type">usize</span>, x: <span class="type">usize</span>) <span class="punctuation">-&gt;</span> <span class="type">bool</span> &#123;</span><br><span class="line">    ((i + pos + <span class="number">1</span> - x) &lt;= n &amp;&amp; (i + pos + <span class="number">1</span> - x) &gt; <span class="number">0</span>) <span class="keyword">as</span> <span class="type">bool</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">const</span> MD: <span class="type">usize</span> = <span class="number">998244353</span>;</span><br><span class="line">    <span class="keyword">const</span> N: <span class="type">usize</span> = <span class="number">205</span>;</span><br><span class="line">    <span class="keyword">const</span> ST: <span class="type">usize</span> = <span class="number">600</span>;</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">input</span> = <span class="type">String</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line">    io::<span class="title function_ invoke__">stdin</span>().<span class="title function_ invoke__">read_line</span>(&amp;<span class="keyword">mut</span> input).<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">s</span> = input.<span class="title function_ invoke__">trim</span>().<span class="title function_ invoke__">split</span>(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="variable">n</span>: <span class="type">usize</span> = s.<span class="title function_ invoke__">next</span>().<span class="title function_ invoke__">unwrap</span>().<span class="title function_ invoke__">parse</span>().<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">x</span>: <span class="type">usize</span> = s.<span class="title function_ invoke__">next</span>().<span class="title function_ invoke__">unwrap</span>().<span class="title function_ invoke__">parse</span>().<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">f</span> = [[[<span class="number">0</span>; ST]; N]; N];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="variable">mxst</span>: <span class="type">usize</span> = (<span class="number">1</span> &lt;&lt; ((x &lt;&lt; <span class="number">1</span>) - <span class="number">2</span> + <span class="number">1</span>)) - <span class="number">1</span>; <span class="comment">// 区间长度本来是 (x &lt;&lt; 1) - 2，但是还要包含 i 本身，所以 + 1</span></span><br><span class="line"></span><br><span class="line">    f[<span class="number">0</span>][<span class="number">0</span>][<span class="number">0</span>] = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> <span class="number">1</span>..=n &#123;</span><br><span class="line">        <span class="keyword">for</span> <span class="variable">j</span> <span class="keyword">in</span> <span class="number">0</span>..=i - <span class="number">1</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> <span class="variable">s</span> <span class="keyword">in</span> <span class="number">0</span>..=mxst &#123;</span><br><span class="line">                <span class="keyword">let</span> <span class="variable">ns</span>: <span class="type">usize</span> = s &gt;&gt; <span class="number">1</span>;</span><br><span class="line">                f[i][j][ns] = (f[i][j][ns] + f[i - <span class="number">1</span>][j][s]) % MD;</span><br><span class="line">                <span class="keyword">for</span> <span class="variable">p</span> <span class="keyword">in</span> <span class="number">0</span>..=(x &lt;&lt; <span class="number">1</span>) - <span class="number">2</span> &#123;</span><br><span class="line">                    <span class="keyword">if</span> ((<span class="number">1</span> &lt;&lt; p) &amp; ns) == <span class="number">0</span> &amp;&amp; <span class="title function_ invoke__">valid</span>(p, i, n, x) &#123;</span><br><span class="line">                        f[i][j + <span class="number">1</span>][ns | (<span class="number">1</span> &lt;&lt; p)] =</span><br><span class="line">                            (f[i][j + <span class="number">1</span>][ns | (<span class="number">1</span> &lt;&lt; p)] + f[i - <span class="number">1</span>][j][s]) % MD</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">fact</span> = [<span class="number">0</span>; N];</span><br><span class="line">    fact[<span class="number">0</span>] = <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> <span class="number">1</span>..=n &#123;</span><br><span class="line">        fact[i] = fact[i - <span class="number">1</span>] * i % MD;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">ans</span>: <span class="type">i64</span> = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> <span class="number">0</span>..=n &#123;</span><br><span class="line">        <span class="keyword">for</span> <span class="variable">s</span> <span class="keyword">in</span> <span class="number">0</span>..=mxst &#123;</span><br><span class="line">            <span class="keyword">if</span> i &amp; <span class="number">1</span> == <span class="number">1</span> &#123;</span><br><span class="line">                ans -= (f[n][i][s] * fact[n - i] % MD) <span class="keyword">as</span> <span class="type">i64</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                ans += (f[n][i][s] * fact[n - i] % MD) <span class="keyword">as</span> <span class="type">i64</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            ans %= MD <span class="keyword">as</span> <span class="type">i64</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    ans = (ans % MD <span class="keyword">as</span> <span class="type">i64</span> + MD <span class="keyword">as</span> <span class="type">i64</span>) % MD <span class="keyword">as</span> <span class="type">i64</span>;</span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, ans);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">容斥大哥组合大哥我错了我滚出 OI</summary>
    
    
    
    <category term="题解" scheme="http://blog.tibrella.space/categories/%E9%A2%98%E8%A7%A3/"/>
    
    
    <category term="组合数学" scheme="http://blog.tibrella.space/tags/%E7%BB%84%E5%90%88%E6%95%B0%E5%AD%A6/"/>
    
    <category term="容斥" scheme="http://blog.tibrella.space/tags/%E5%AE%B9%E6%96%A5/"/>
    
    <category term="二项式反演" scheme="http://blog.tibrella.space/tags/%E4%BA%8C%E9%A1%B9%E5%BC%8F%E5%8F%8D%E6%BC%94/"/>
    
  </entry>
  
  <entry>
    <title>AMD P-STATE 折腾历程</title>
    <link href="http://blog.tibrella.space/post/amd-p-state-honor/"/>
    <id>http://blog.tibrella.space/post/amd-p-state-honor/</id>
    <published>2023-06-19T22:00:34.000Z</published>
    <updated>2023-11-08T08:51:29.283Z</updated>
    
    <content type="html"><![CDATA[<h2 id="介绍">介绍</h2><blockquote><p><code>amd-pstate</code> 是 AMD CPU 性能扩展驱动程序，它在 Linux内核的现代 AMD APU 和 CPU 系列上引入了新的 CPU频率控制机制。新机制基于协作处理器性能控制 (CPPC)，它提供比传统 ACPI硬件 P-States 更精细的频率管理。当前的 AMD CPU/APU 平台使用 ACPIP-states 驱动程序来管理 CPU 频率和时钟，仅在 3 个 P-states 中切换。 CPPC取代了 ACPI P-states 控件，并允许 Linux内核使用灵活、低延迟的接口直接将性能提示传达给硬件 （摘自 <ahref="https://www.kernel.org/doc/html/latest/admin-guide/pm/amd-pstate.html#:~:text=amd-pstate%20is%20the%20AMD%20CPU%20performance%20scaling%20driver,grain%20frequency%20management%20than%20legacy%20ACPI%20hardware%20P-States.">KernelDocs</a>）</p></blockquote><h2 id="折腾">折腾</h2><p>这玩意支持 AMD ZEN 2 和更新版本，我的荣耀本使用R7-4800H，理论上可以跑，于是按照 <ahref="https://note.ay1.us/#/15-amd-cpufreq">Aya 的博客</a>添加了内核参数</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">zfs <span class="built_in">set</span> org.zfsbootmenu:commandline=<span class="string">&quot;rw amd_pstate=passive&quot;</span> zroot/ROOT</span></span><br></pre></td></tr></table></figure><p>然后就没管了。</p><p>有一天忘记从哪里又看见了 AMDP-State，然后发现一个验证方法，跑了一下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">cat</span> /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver</span><br><span class="line"></span><br><span class="line">acpi-cpufreq</span><br></pre></td></tr></table></figure><p>我去，这我可忍不了。于是我先检查内核 module 有没有 AMD P-State存在，结果是没有。但是检查内核 config</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ zcat /proc/config.gz | grep PSTATE</span><br><span class="line"></span><br><span class="line">CONFIG_X86_AMD_PSTATE=y</span><br></pre></td></tr></table></figure><p>那得了，这玩意都 built-in 了咋可能不存在？于是检查日志：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ journalctl -b 0 | grep pstate</span><br></pre></td></tr></table></figure><p><del>经过群友提醒，我使用翻译发现第一次设置的 passive可能出了些幽灵字符导致无法识别，再设置一次才出现了根本问题</del></p><p>得到</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_CPC object is not present in SBIOS</span><br></pre></td></tr></table></figure><p>哈哈荣耀操你妈的，这问题不就一眼顶针了？傻逼厂商的傻逼主板又没有把CPPC 的选项留给我自己设置捏。</p><p>tg 群里：<br />我：好像是硬件的问题，傻逼荣耀。<br />群友：试试 <ahref="https://github.com/DavidS95/Smokeless_UMAF">Smokeless_UMAF</a>？discussion里有 CPPC 相关的东西。</p><p>随后我想起来当时用 Smokeless_UMAF 把傻逼荣耀设置的 512M 显存干到 4G的过程，心想这玩意也能设置？事实上，是的。</p><p>我手上有一个现成的 ventoy 盘，于是直接拿来使用，扔进去 <ahref="https://github.com/mio-19/UniversalAMDFormBrowser-Ventoy">为ventoy 提前制作好的 img 镜像</a> 就能启动了。</p><p>另外提醒一句，UMAF基于一个漏洞，这个漏洞貌似联想在去年三月份就修复了，所以不一定能正常启动。蛤蛤，但是傻逼荣耀已经三年没更新过任何驱动了，所以我怕个吊，直接开干。<br />同时，这玩意不开源，恶意代码存在可能性待议，但是利用漏洞 Hack主板设置肯定有变砖的风险存在。因此 USE AT YOUR OWNRISK。有条件的建议备份一下BIOS（有刷写工具的情况下），或者直接买<del>六七个</del> BIOS芯片，坏了直接换上。</p><p>但是我电脑太冷门了，没有卖的，手上还没有刷写工具，但是保修期内，大不了送回去修，我还有机房电脑可以用。</p><p>按照 <ahref="https://github.com/DavidS95/Smokeless_UMAF/discussions/29#discussioncomment-6143331">discussion#29</a> 的做法，进入 Device Manager - AMD CBS - NBIO - SMU page，启用CPPC，直接一路 esc 出来，它会提醒你选择是否保存，保存后会直接重启。</p><p>再次</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">cat</span> /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver</span><br><span class="line"></span><br><span class="line">amd-pstate</span><br></pre></td></tr></table></figure><p>就这样，没了，感谢阅读。</p>]]></content>
    
    
    <summary type="html">AMD 的一个频率控制工具</summary>
    
    
    <content src="https://pic.imgdb.cn/item/639b2768b1fccdcd36e13360.jpg" type="image"/>
    
    
    <category term="Linux" scheme="http://blog.tibrella.space/categories/Linux/"/>
    
    
    <category term="内核" scheme="http://blog.tibrella.space/tags/%E5%86%85%E6%A0%B8/"/>
    
  </entry>
  
  <entry>
    <title>AC 自动机</title>
    <link href="http://blog.tibrella.space/post/ac-automaton/"/>
    <id>http://blog.tibrella.space/post/ac-automaton/</id>
    <published>2023-06-06T19:43:04.000Z</published>
    <updated>2023-08-19T02:55:30.575Z</updated>
    
    <content type="html"><![CDATA[<p>真的比 KMP 简单！（指好理解）</p><h2 id="前置知识">前置知识</h2><p>trie 树（必会）<br />KMP（知道一点思想就行，不用详细学习）</p><h2 id="用途">用途</h2><p>我们知道 KMP 能够 <spanclass="math inline">\(\operatorname{O}(|T|+|S|)\)</span> 匹配单个文本串<span class="math inline">\(T\)</span> 与模式串 <spanclass="math inline">\(S\)</span>。但是如果有 <spanclass="math inline">\(n \leq 10^5\)</span>个模式串（小串）匹配一个文本串（大串）呢？AC 自动机能够在 <spanclass="math inline">\(\operatorname{O}(|T|+\sum |S|)\)</span>的复杂度内求出每一个模式串的出现次数。</p><p>核心思想和 KMP相似，除去多余的匹配，即在当前匹配状态的基础上转移着匹配。（我也不知道我说了啥）</p><h2 id="基本结构">基本结构</h2><p>多个模式串，先去重前缀，建立前缀树（即 trie 树）。假设我们当前有四个模式串：<spanclass="math inline">\(\texttt{abc,ac,bc,bd}\)</span></p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64813e2a1ddac507cc19c8df"/></div></div><p>对每个模式串终止字符记录其出现的次数作为其在 trie树上对应节点的权值，然后遍历文本串的每一个字符，在 trie树上向下走，<span class="math inline">\(ans\)</span>加上当前点的权值，如果走不了了就把文本串的当前指针回跳到上一个有多个子节点的节点，从trie树根节点继续匹配。（当然这个不重要，理解不理解无所谓）（理解不了就自己匹配一下试试）</p><p>合并了一些模式串的前缀，于是有了一定优化，但是优化还不够，考虑对后缀优化。</p><h2 id="fail-边">fail 边</h2><h3 id="含义">含义</h3><p>AC 自动机的核心就是 fail 边。</p><p>fail 边的含义：如果 <span class="math inline">\(u\)</span> 点的 fail边指向 <span class="math inline">\(v\)</span>，则 <spanclass="math inline">\(v\)</span> 点所代表的字符串<a href="#fn1"class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a>是<span class="math inline">\(u\)</span>点所代表的字符串的后缀，且前者是除后者自身以外满足要求的最长字符串。</p><p>先不管怎样建 fail 边，按照要求人脑建立一下，如下图：</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/648140721ddac507cc1d77e5"/></div></div><p>然后假设有文本串 <spanclass="math inline">\(\texttt{abc}\)</span>，进行匹配。</p><p>为什么上一次匹配的时候，我们需要把文本串的指针回跳？因为没有办法除去后缀的影响——一个模式串是另一个模式串的后缀，或前者的一部分是后者的后缀。意味着：后者匹配失败不代表前者也会匹配失败。于是我们可以记录一下合法后缀，如果匹配失败就跳到自己最长后缀所在节点上（没有的话就跳根节点），容易发现这样一直跳下去，能把最开始的串的合法后缀全部跳完！说明后缀的影响已经排除掉了。</p><p>为了方便，我们把没有合法后缀的节点的 fail 边指向根节点。</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/6481466b1ddac507cc291a6d"/></div></div><p>实际上此时的 fail边的含义：无法向下一个子节点匹配（没有子节点或匹配失败）时要走的边。</p><p>动画演示：</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/64814ec31ddac507cc38599e"/></div></div><h3 id="建立">建立</h3><p>BFS 的过程。</p><ol type="1"><li><p>第一层（根节点为第零层）的 fail 全部指向根节点<a href="#fn2"class="footnote-ref" id="fnref2"role="doc-noteref"><sup>2</sup></a>，压入队列。</p></li><li><p>取出队头，遍历子节点，如果子节点存在，假设这个子节点代表字符<span class="math inline">\(c\)</span>，当前节点 fail 指针指向 <spanclass="math inline">\(lf\)</span>，<spanclass="math inline">\(lf\)</span> 的子节点集合为 <spanclass="math inline">\(son\)</span>，则将其 fail 指针指向 <spanclass="math inline">\(son_c\)</span>。</p></li></ol><p>没了。</p><h2 id="优化">优化</h2><p>我们一般把 trie 树的一个节点的儿子用长度为 26的数组存下，导致遍历子节点的时候会遍历到傻逼空节点。</p><p>对于当前节点的空子节点指针，可以直接指向当前 fail指针指向的节点的子节点集合中，对应的子节点。</p><p>参考代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">build</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    std::queue&lt;i32&gt; q;</span><br><span class="line">    <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; <span class="number">26</span>; ++i)</span><br><span class="line">        <span class="keyword">if</span> (trie[<span class="number">0</span>].son[i]) q.<span class="built_in">push</span>(trie[<span class="number">0</span>].son[i]);</span><br><span class="line">    <span class="keyword">while</span> (!q.<span class="built_in">empty</span>()) &#123;</span><br><span class="line">        i32 nod = q.<span class="built_in">front</span>(), lfail = trie[q.<span class="built_in">front</span>()].fail; <span class="comment">// 当前节点和当前节点的 fail 节点</span></span><br><span class="line">        q.<span class="built_in">pop</span>();</span><br><span class="line">        <span class="keyword">for</span> (i32 i = <span class="number">0</span>; i &lt; <span class="number">26</span>; ++i) &#123;</span><br><span class="line">            i32&amp; to = trie[nod].son[i];</span><br><span class="line">            <span class="keyword">if</span> (to) &#123;</span><br><span class="line">                trie[to].fail = trie[lfail].son[i];</span><br><span class="line">                q.<span class="built_in">push</span>(to);</span><br><span class="line">            &#125; <span class="keyword">else</span></span><br><span class="line">                to = trie[lfail].son[i]; <span class="comment">// 节点为空</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><section id="footnotes" class="footnotes footnotes-end-of-document"role="doc-endnotes"><hr /><ol><liid="fn1"><p>字典树上除根节点外的每一个节点到根的路径都代表一个字符串。<ahref="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li><li id="fn2"><p>模拟一下会发现：如果 fail还按照正常情况定义来建的话会死循环。<a href="#fnref2"class="footnote-back" role="doc-backlink">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">这玩意比 KMP 简单多了。</summary>
    
    
    
    <category term="字符串" scheme="http://blog.tibrella.space/categories/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    
    
    <category term="AC 自动机" scheme="http://blog.tibrella.space/tags/AC-%E8%87%AA%E5%8A%A8%E6%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>SPOJ 4060 题解</title>
    <link href="http://blog.tibrella.space/post/spoj-4060-solution/"/>
    <id>http://blog.tibrella.space/post/spoj-4060-solution/</id>
    <published>2023-05-30T01:25:03.000Z</published>
    <updated>2023-08-17T11:28:45.854Z</updated>
    
    <content type="html"><![CDATA[<h2 id="题意">题意</h2><p>给定一定数量的石子，两个人抛硬币，抛到正面则拿走一颗石子，否则什么都不做。拿走最后一颗石子的人胜利。<br />已知石子数量，假设双方都采用最优方案，两个人分别抛到自己想要的那一面的概率分别为<span class="math inline">\(0.5 \leqslant q,p &lt; 1\)</span><del>这不唯物</del>，求先手的获胜概率。</p><h2 id="题解">题解</h2><p>要点：对概率的熟悉程度，对 DP的熟练程度，奇怪的缩小数据范围的方法</p><p>首先考虑直接硬算，发现希望掷得的面会变化，没法直接算，考虑 DP。</p><h3 id="设计状态">设计状态</h3><ul><li>表示出当前到达第几颗石子</li><li>表示出当前谁拿石子</li></ul><p>于是得到状态：<span class="math inline">\(f_{i,0/1}\)</span> 表示<span class="math inline">\(i\)</span> 个石子，第 <spanclass="math inline">\(1\)</span> 或第 <spanclass="math inline">\(2\)</span> 个人获胜的概率</p><p>显然 <span class="math inline">\(f_{0,0}\)</span> 和 <spanclass="math inline">\(f_{0,1}\)</span> 是两种确定结果，则概率分别为<span class="math inline">\(1,0\)</span>。于是考虑倒推。</p><p>看概率的取值，说明只要我们希望抛到一个面，抛到这个面概率就会更大。<br />由于很容易判断出当前状态下应该希望取得/不取得石子（通过前一个状态的概率大小），所以不用单独表示出希望取得/不取得石子。</p><p>状态是否可行？根据 lzp 大佬的教诲，我们需要判断该状态：</p><ul><li>是否能表示单独状态：显然可以，上面解释过了。</li><li>是否可递推：直觉是可以递推，但是需要列柿子看一下</li></ul><h3 id="转移方程">转移方程</h3><p>开始写柿子：<br />希望取得这颗石子（正面）：</p><p><span class="math display">\[\begin{aligned}f_{i,0} &amp;= f_{i-1,1}\cdot p + f_{i,1} \cdot (1-p)\newlinef_{i,1} &amp;= f_{i-1,0}\cdot q + f_{i,0} \cdot (1-q)\end {aligned}\]</span></p><p>发现这玩意没法递推，但是发现可以运用初中二元一次方程的思路，代入消元。</p><p><span class="math display">\[\begin{aligned}f_{i,0} &amp;= f_{i-1,1}\cdot p + f_{i,1} \cdot (1-p) \newlinef_{i,0} &amp;= f_{i-1,1} \cdot p + (f_{i-1,0}\cdot q + f_{i,0} \cdot(1-q))(1-p) \newlinef_{i,0} &amp;= f_{i-1,1} \cdot p + f_{i-1,0}\cdot (1-p)q + f_{i,0} \cdot(1-q)(1-p) \newline(1-(1-q)(1-p))f_{i,0} &amp;= f_{i-1,1} \cdot p + f_{i-1,0}\cdot (1-p)q\newlinef_{i,0} &amp;= \frac {p\cdot f_{i-1,1} + (1-p)q\cdotf_{i-1,0}}{1-(1-q)(1-p)}\end{aligned}\]</span></p><p>发现这玩意就可以递推了，再依照同样的方式能够列出 <spanclass="math inline">\(f_{i,1}\)</span>的转移方程和不希望取得这颗石子的两个转移方程。不再赘述过程（基本上和上面完全一样），给出柿子希望取得：</p><p><span class="math display">\[\begin{aligned}f_{i,0} &amp;= \frac {p\cdot f_{i-1,1} + (1-p)q\cdotf_{i-1,0}}{1-(1-q)(1-p)} \newlinef_{i,1} &amp;= \frac {q\cdot f_{i-1,0} + (1-q)p\cdotf_{i-1,1}}{1-(1-p)(1-q)}\end {aligned}\]</span></p><p>不希望取得：</p><p><span class="math display">\[\begin{aligned}f_{i,0} = \frac{p(1-q)\cdot f_{i-1,0} + (1-p)\cdot f_{i-1,1}}{1-qp}\newlinef_{i,1} = \frac{q(1-p)\cdot f_{i-1,1} + (1-q)\cdot f_{i-1,0}}{1-qp}\end{aligned}\]</span></p><p>对比一下容易发现：希望取得与不希望取得的转移方程中，区别仅仅是 <spanclass="math inline">\(p,(1-p)\)</span> 和 <spanclass="math inline">\(q,(1-q)\)</span>。于是，当取石子比不取石子优的时候，把<span class="math inline">\(p,q\)</span> 把 <spanclass="math inline">\(1\)</span> 减一下就行了。</p><p>于是 DP 部分解决，复杂度 <spanclass="math inline">\(\operatorname{O}(n)\)</span></p><h3 id="优化">优化</h3><p>但是题目中 <span class="math inline">\(n\)</span>的范围比较申必，<span class="math inline">\(n \leqslant10^9-1\)</span>，还有多测，显然过不了。怎么办？开始考虑减少递推次数</p><ul><li>“自身简化专业玄 —— lzp”矩阵快速幂之类的优化...不好评价，转移方程貌似有点巨大；单调队列或者斜率？已经线性了，这种方法貌似没啥用。</li><li>概率还可以考虑是否会出现<strong>趋于稳定</strong>的情况。可以打表测一下<del>（或者提交试一下）</del>，<del>试一下发现确实可以</del><br /><span class="math inline">\(n = \min(n,100)\)</span> 即可。</li></ul><h3 id="易错提示">易错提示</h3><ol type="1"><li>要确定好自己选择的是正推还是逆推。（从这个状态推到下一个状态还是从上一个状态推到当前状态），换句话说，注意<span class="math inline">\(i-1,i,i+1\)</span> 的区别。</li><li>要确定好自己递推使用的是哪一个柿子，不要写反（谁因为这个调了半天我不说</li></ol><h2 id="示例代码">示例代码</h2><p>不得不说 rust 某些方面比 C++严格，读入也比较麻烦，但是从另一方面来说非常人性化...</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">use</span> std::io;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">solution</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">input</span> = <span class="type">String</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line">    io::<span class="title function_ invoke__">stdin</span>().<span class="title function_ invoke__">read_line</span>(&amp;<span class="keyword">mut</span> input).<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">lis</span> = input.<span class="title function_ invoke__">trim</span>().<span class="title function_ invoke__">split</span>(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">n</span>: <span class="type">usize</span> = lis.<span class="title function_ invoke__">next</span>().<span class="title function_ invoke__">unwrap</span>().<span class="title function_ invoke__">parse</span>().<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">p</span>: <span class="type">f64</span> = lis.<span class="title function_ invoke__">next</span>().<span class="title function_ invoke__">unwrap</span>().<span class="title function_ invoke__">parse</span>().<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="variable">q</span>: <span class="type">f64</span> = lis.<span class="title function_ invoke__">next</span>().<span class="title function_ invoke__">unwrap</span>().<span class="title function_ invoke__">parse</span>().<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">f</span>: [<span class="type">Vec</span>&lt;<span class="type">f64</span>&gt;; <span class="number">2</span>] = [<span class="type">Vec</span>::<span class="title function_ invoke__">new</span>(), <span class="type">Vec</span>::<span class="title function_ invoke__">new</span>()];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> n &gt; <span class="number">100</span> &#123;</span><br><span class="line">        <span class="comment">// 推到第 100 个石子就够了</span></span><br><span class="line">        n = <span class="number">100</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    f[<span class="number">0</span>].<span class="title function_ invoke__">push</span>(<span class="number">0.0</span>);</span><br><span class="line">    f[<span class="number">1</span>].<span class="title function_ invoke__">push</span>(<span class="number">1.0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> <span class="variable">i</span> <span class="keyword">in</span> <span class="number">1</span>..=n &#123;</span><br><span class="line">        <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">pnow</span> = p;</span><br><span class="line">        <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">qnow</span> = q;</span><br><span class="line">        <span class="keyword">if</span> f[<span class="number">1</span>][i - <span class="number">1</span>] &gt; f[<span class="number">0</span>][i - <span class="number">1</span>] &#123;</span><br><span class="line">            <span class="comment">// 如果上一颗石子不应当让自己取</span></span><br><span class="line">            <span class="comment">// 这里取不取等号无所谓</span></span><br><span class="line">            pnow = <span class="number">1.0</span> - p;</span><br><span class="line">            qnow = <span class="number">1.0</span> - q;</span><br><span class="line">        &#125;</span><br><span class="line">        f[<span class="number">0</span>].<span class="title function_ invoke__">push</span>(</span><br><span class="line">            (pnow * (<span class="number">1.0</span> - qnow) * f[<span class="number">0</span>][i - <span class="number">1</span>] + (<span class="number">1.0</span> - pnow) * f[<span class="number">1</span>][i - <span class="number">1</span>]) / (<span class="number">1.0</span> - pnow * qnow),</span><br><span class="line">        );</span><br><span class="line">        f[<span class="number">1</span>].<span class="title function_ invoke__">push</span>(</span><br><span class="line">            (qnow * (<span class="number">1.0</span> - pnow) * f[<span class="number">1</span>][i - <span class="number">1</span>] + (<span class="number">1.0</span> - qnow) * f[<span class="number">0</span>][i - <span class="number">1</span>]) / (<span class="number">1.0</span> - pnow * qnow),</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, f[<span class="number">0</span>][n]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() &#123;</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">input</span> = <span class="type">String</span>::<span class="title function_ invoke__">new</span>();</span><br><span class="line">    io::<span class="title function_ invoke__">stdin</span>().<span class="title function_ invoke__">read_line</span>(&amp;<span class="keyword">mut</span> input).<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">s</span> = input.<span class="title function_ invoke__">trim</span>().<span class="title function_ invoke__">split</span>(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line">    <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">t</span>: <span class="type">i32</span> = s.<span class="title function_ invoke__">next</span>().<span class="title function_ invoke__">unwrap</span>().<span class="title function_ invoke__">parse</span>().<span class="title function_ invoke__">unwrap</span>();</span><br><span class="line">    <span class="keyword">while</span> t != <span class="number">0</span> &#123;</span><br><span class="line">        <span class="title function_ invoke__">solution</span>();</span><br><span class="line">        t -= <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;题意&quot;&gt;题意&lt;/h2&gt;
&lt;p&gt;给定一定数量的石子，两个人抛硬币，抛到正面则拿走一颗石子，否则什么都不做。拿走最后一颗石子的人胜利。&lt;br /&gt;
已知石子数量，假设双方都采用最优方案，两个人分别抛到自己想要的那一面的概率分别为
&lt;span class=&quot;mat</summary>
      
    
    
    <content src="https://pic.imgdb.cn/item/6416687ca682492fccfb973a.jpg" type="image"/>
    
    
    <category term="题解" scheme="http://blog.tibrella.space/categories/%E9%A2%98%E8%A7%A3/"/>
    
    
    <category term="概率" scheme="http://blog.tibrella.space/tags/%E6%A6%82%E7%8E%87/"/>
    
  </entry>
  
  <entry>
    <title>概率杂题</title>
    <link href="http://blog.tibrella.space/post/probability-misc-questions/"/>
    <id>http://blog.tibrella.space/post/probability-misc-questions/</id>
    <published>2023-05-22T05:06:14.000Z</published>
    <updated>2023-09-07T13:22:49.867Z</updated>
    
    <content type="html"><![CDATA[<h2 id="p4562-jxoi2018游戏"><ahref="https://www.luogu.com.cn/problem/P4562">P4562[JXOI2018]游戏</a></h2><p>给定区间 <spanclass="math inline">\([l,r]\)</span>，每次选择一个未选择过的数，将这个数以及它的所有倍数标记，每一种方案全部标记所用次数记为<span class="math inline">\(t\)</span>，求所有可能 <spanclass="math inline">\(t\)</span> 的和。</p><p>首先想到每次将所有点标记的实质，就是将 <spanclass="math inline">\(\left[l,r\right]\)</span>中<strong>没有</strong>在<strong>此区间内</strong>的<strong>非自身的因数</strong>的数标记完。称这样的数为关键数，设关键数有<span class="math inline">\(k\)</span>个，则实际上标记次数就是<strong>最后一个关键数</strong>的位置。</p><p>最后一个关键数的位置...不沾头不沾尾，所以换个方向考虑——最后一个关键数的位置可以替换为<strong>它后面的非关键数的数量</strong>。</p><p>实际上，<span class="math inline">\(k\)</span>个关键数把整个区间分成了 <span class="math inline">\(k+1\)</span>份。设区间长度为 <spanclass="math inline">\(1\)</span>，则一个非关键数在最后一段区间上的期望就是<span class="math inline">\(\cfrac 1 {k+1}\)</span>（<spanclass="math inline">\(k+1\)</span>种情况，每种情况都在段区间上，只有一种情况在目标区间上），<spanclass="math inline">\(n-k\)</span> 个非关键点在最后一段区间上的期望就是<spanclass="math inline">\(\cfrac{n-k}{k+1}\)</span>。（注意这里概率到期望的转换，要想想为什么期望成了求解目标）</p><p>于是，最后一个点的位置的期望，即 <spanclass="math inline">\(t\)</span> 的期望为下式</p><p><span class="math display">\[\begin{aligned}&amp;\ \ \ \ \  n-\cfrac{n-k}{k+1} \newline&amp;= \cfrac{nk+n-n+k}{k+1} \newline&amp;= \cfrac{(n+1)k}{k+1}\end{aligned}\]</span></p><p>求所有可能的 <span class="math inline">\(t\)</span>的值的和，乘一个阶乘就好了。</p><p><span class="math display">\[\begin{aligned}&amp;\ \ \ \ \  \cfrac{(n+1)k}{k+1}n! \newline&amp;= \cfrac{k}{k+1}(n+1)!\end{aligned}\]</span></p><p>取模的话，求完阶乘 <span class="math inline">\(\times k\)</span>再乘个 <a href="/post/multi-inverse-element">逆元</a> 就行了。</p><h2 id="p4284-shoi2014-概率充电器"><ahref="https://www.luogu.com.cn/problem/P4284">P4284 [SHOI2014]概率充电器</a></h2><p>给定一棵树，每条边能导电的概率为 <spanclass="math inline">\(p_i\)</span>，每个点自己带电的概率为 <spanclass="math inline">\(q_i\)</span>，求带电点个数的期望。</p><p>根据期望的线性性，考虑求每个点有电的期望。因为只有 <spanclass="math inline">\(0,1\)</span> 两种情况，因此求概率就行了。</p><p>考虑单个点有电的情况：</p><ul><li>自己有电</li><li>儿子有电，和儿子连边导电</li><li>父亲有电，和父亲连边导电</li></ul><p>设 <span class="math inline">\(f_u\)</span> 为点 <spanclass="math inline">\(u\)</span> 带电的期望：</p><ul><li>第一种情况的概率就是 <spanclass="math inline">\(q_u\)</span>，所以直接 <spanclass="math inline">\(f_u = q_u\)</span> 就行。</li><li>第二种情况非常像树形 DP 的样子，容易想到从叶子节点向上推就行。<ul><li>考虑两个独立事件发生其中一件即可的概率： <spanclass="math inline">\(P(A+B)=P(A)+(1-P(A))P(B)\)</span>，即 <spanclass="math inline">\(A\)</span> 事件的概率 <spanclass="math inline">\(+\)</span> <span class="math inline">\((B\)</span>事件 <span class="math inline">\(+\)</span> <spanclass="math inline">\(A\)</span> 事件未发生 <spanclass="math inline">\()\)</span> 的概率。</li><li>进行 dfs，回溯的时候统计：<ul><li>设当前点为 <span class="math inline">\(u\)</span>，儿子为 <spanclass="math inline">\(v\)</span>，则有 <span class="math inline">\(f_u =f_u + (1-f_u)p_{u,v}f_v\)</span></li></ul></li></ul></li><li>第三种情况实际上也是树形 DP，只不过变成了正推，从 <spanclass="math inline">\(u\)</span> 计算对 <spanclass="math inline">\(v\)</span> 的贡献。<ul><li>假设当前到达 <span class="math inline">\(u\)</span>，且 <spanclass="math inline">\(f_u\)</span> 已经算好了，考虑如何把贡献传给 <spanclass="math inline">\(f_v\)</span>。</li><li>容易想到 <span class="math inline">\(u\)</span> 能贡献给 <spanclass="math inline">\(v\)</span> 电流的那部分概率，实际上是 <spanclass="math inline">\(f_u\)</span> 扔掉 <spanclass="math inline">\(v\)</span> 这棵子树的部分。</li><li>换句话说，贡献 <span class="math inline">\(=\)</span> <spanclass="math inline">\(u\)</span> 有电的概率 <spanclass="math inline">\(-\)</span> <span class="math inline">\(u\)</span>不考虑 <span class="math inline">\(v\)</span>的有电的概率，再乘边的概率之类的常数即可。</li><li>设 <span class="math inline">\(\langle u,v \rangle=e\)</span>，<spanclass="math inline">\(u\)</span> 不考虑 <spanclass="math inline">\(v\)</span> 的有电的概率为 <spanclass="math inline">\(g_u\)</span>，<spanclass="math inline">\(v\)</span> 只考虑下方来电有电的概率为 <spanclass="math inline">\(l_v\)</span>，有柿子： <spanclass="math display">\[\begin{aligned}f_u &amp;= g_u + l_vp_e(1-g_u) \newlinef_u &amp;= g_u + l_vp_e-l_vp_eg_u \newlinef_u - l_vp_e &amp;= g_u(1-l_vp_e) \newlineg_u &amp;= \cfrac {f_u-l_vp_e}{1-l_vp_e} \newlinef_u - g_u &amp;= f_u - \cfrac {f_u-l_vp_e}{1-l_vp_e}\end{aligned}\]</span></li><li>考虑完 <span class="math inline">\(u\)</span>有电的贡献，乘一下边的概率和 <span class="math inline">\(v\)</span>没电的概率就行了 <span class="math display">\[f_v = f_v + (1-f_v)\cdot p_e \cdot(f_u - \cfrac {f_u-l_vp_e}{1-l_vp_e})\]</span></li></ul></li></ul><p>柿子推完了，写两个 dfs 算答案即可。</p><h2 id="spoj-4060-kpgame">SPOJ 4060 KPGAME</h2><p><a href="/post/spoj-4060-solution">传送门</a></p><h2 id="cf-1316-f">CF 1316 F</h2><p>又是一个重量级题目。</p><p>首先我们发现他这个数列是假的，因为求权值的时候是“排序”后再求，子序列也不需要连续，所以我们直接把它当一个数的集合做，不需要维护他们在原数列中的顺序。下面所说的下标都是排序后的下标。</p><p>好，接下来考虑一个点对对期望的贡献。<br />假设有点对 <spanclass="math inline">\(a_i,a_j,i&lt;j\)</span>，显然权值的贡献是 <spanclass="math inline">\(a_ia_j\)</span>。<br />然后来看概率，由于是相邻的，所以能随便挑选的只有 <spanclass="math inline">\(i\)</span> 左边，<spanclass="math inline">\(j\)</span> 右边的点，总共有 <spanclass="math inline">\((i-1)+(m-j)\)</span>个，满足两个点相邻的子序列数量即为 <spanclass="math inline">\(2^{(i-1)+(m-j)}\)</span>，而子序列的总数有 <spanclass="math inline">\(2^n\)</span> 个。因此，这两个点相邻的概率为：</p><p><span class="math display">\[\frac {2^{(i-1)+(m-j)}}{2^n} = \frac {1}{2^{n-i+1-j-n}} = \frac1{2^{j-i+1}}\]</span></p><p>于是得出点对 <span class="math inline">\(a_i,a_j,i&lt;j\)</span>对期望的贡献：<span class="math inline">\(\frac{a_ia_j}{2^{j-i+1}}\)</span>。</p><p>然后给定一个数列，其权值的期望直接枚举点对计算就是答案：<spanclass="math inline">\(\sum_{i=1}^n \sum_{j=i+1}^n \frac{a_ia_j}{2^{j-i+1}}\)</span>。</p><p>但是这玩意带单点修改，直接用这个式子的话就是一个非常优秀的 <spanclass="math inline">\(\Theta(qn^2)\)</span> 的做法。</p><p>单点修改。考虑分治。</p><p>把一个序列分成两段：<spanclass="math inline">\([1,m],[m+1,n]\)</span>，那它的贡献可以分成三部分：</p><ul><li><span class="math inline">\([1,m]\)</span> 的贡献</li><li><span class="math inline">\([m+1,n]\)</span> 的贡献</li><li>跨越两个区间的点对的贡献</li></ul><p>前两个可以分治解决，问题在于第三个东西。<br />首先这个东西的式子是很好写的：<span class="math inline">\(\sum_{i=1}^m\sum_{j=m+1}^n \frac {a_ia_j}{2^{j-i+1}}\)</span><br />为了分治，我们得把这个式子拆成左右两半。欸，这 <spanclass="math inline">\(1\sim m\)</span> 和 <spanclass="math inline">\(m+1\sim n\)</span>根本就没交集，那就直接拆开就好了！</p><p><span class="math display">\[\begin{aligned}&amp;\ \ \ \ \ \sum_{i=1}^m \sum_{j=m+1}^n \frac {a_ia_j}{2^{j-i+1}}\newline&amp;= \sum_{i=1}^m \sum_{j=m+1}^n \frac {a_ia_j}{2^{j-m+m-i+1}}\newline&amp;= (\sum_{i=1}^m \frac {a_i}{2^{m-i+1}})(\sum_{j=m+1}^n \frac{a_j}{2^{j-m}})\end{aligned}\]</span></p><p>然后我们就可以直接分治解决了。</p><p>设一个区间的答案为 <spanclass="math inline">\(f\)</span>，上面拆成的两个部分一个是 <spanclass="math inline">\(ls\)</span> 一个是 <spanclass="math inline">\(rs\)</span>，<span class="math inline">\(len(x,y)= y-x+1\)</span> 表示一个区间的长度。</p><p>对于一个区间 <spanclass="math inline">\([l,r]\)</span>，有如下一堆东西：</p><p><span class="math display">\[\begin{cases}f(i,i) = 0 \newlinels(i,i) = rs(i,i) = 1 \newlinef(l,r) = f(l,m) + f(m+1,r) + ls(l,m)rs(m+1,r)\newlinels(l,r) = ls(l,m) + \frac {rs(m+1,r)}{2^{len(l,m)}} \newliners(l,r) = rs(m+1,r) + \frac {rs(l,m)}{2^{len(m+1,r)}}\end{cases}\]</span></p><p>最终答案是 <span class="math inline">\(f(1,n)\)</span>。</p><p>这样的分治，最终复杂度是 <spanclass="math inline">\(\Theta(qn)\)</span> 的，还是不够。</p><p>这个分治实际上很容易用线段树维护，但是修改权值怎么办？答案是使用权值线段树。</p><p>我们将询问离线下来，把原数列和询问中的数字全部离散化，然后存到权值线段树里面,<strong>这样显然就把排序的部分做完了</strong>。<br />然后对于每个线段树节点，维护当前区间里面的<strong>有效点个数</strong>，以及上面那一堆东西，但是<span class="math inline">\(len\)</span>就可以替换成刚才提到的点数了。</p><p><code>push_up</code> 就用上面的式子就好。</p><p>这样的做法时间复杂度为 <span class="math inline">\(\Theta(n+q\logn)\)</span>，非常能过。</p><p>但是另一个细节的点来了：离散化。由于两个相同的数作为点对也会产生贡献，所以在离散化的时候给他们分配不同的编号。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;p4562-jxoi2018游戏&quot;&gt;&lt;a
href=&quot;https://www.luogu.com.cn/problem/P4562&quot;&gt;P4562
[JXOI2018]游戏&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;给定区间 &lt;span
class=&quot;math inline&quot;</summary>
      
    
    
    <content src="https://pic.imgdb.cn/item/6416687ca682492fccfb973a.jpg" type="image"/>
    
    
    <category term="题解" scheme="http://blog.tibrella.space/categories/%E9%A2%98%E8%A7%A3/"/>
    
    
    <category term="概率" scheme="http://blog.tibrella.space/tags/%E6%A6%82%E7%8E%87/"/>
    
  </entry>
  
  <entry>
    <title>最大流要点</title>
    <link href="http://blog.tibrella.space/post/ff-network-maxflow/"/>
    <id>http://blog.tibrella.space/post/ff-network-maxflow/</id>
    <published>2023-05-20T09:23:16.000Z</published>
    <updated>2023-08-16T14:09:01.889Z</updated>
    
    <content type="html"><![CDATA[<h2 id="什么是最大流问题">什么是最大流问题</h2><p>总体是一张有向图，定一个源点，一个汇点。单位时间内源点能流出无限水，汇点能接受无限水。图中每条边相当于一个单位时间流量有限的管道。</p><p>最大流问题：单位时间内汇点最多能接收到多少水流？</p><h2 id="解法">解法</h2><p>考虑贪心去做：反复 dfs找到仍能流水的一条从源点到汇点的路径，路径中每个边的容量减去路径的最小容量（水管模型能流多少显然受最小容量限制吧...）。<br />但是好像不太对，有可能当前的一个流让它从另一个路径流出去更合适，借鉴反悔贪心的思想，我们可以给每条边加一条反向边。当一条边的容量被占据了<span class="math inline">\(f\)</span> 时，其容量减去 <spanclass="math inline">\(f\)</span>，然后给它的反向边加上 <spanclass="math inline">\(f\)</span>。</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic.imgdb.cn/item/6496477d1ddac507cce1334e.jpg"/></div></div><p>上面只是简单介绍了一下增广路的主要思想，下面是三种具体的求法。</p><p>OI 中网络流貌似不需要去管他的复杂度之类，尤其是最大流，卡 Dinic的题可能只有 HLPP 模板题那一道。</p><p>主要是感性理解，重点都在建模。</p><h3 id="ek">EK</h3><p>不会，上界 <spanclass="math inline">\(\operatorname{O}(nm^2)\)</span>，懒得学了</p><h3 id="dinic">Dinic</h3><p>推荐结合这个博客看：https://www.cnblogs.com/SYCstudio/p/7260613.html</p><p>继续上面的贪心想法</p><p>每一次 dfs 可能得遍历整张图，只找到 1 条增广路，感觉不太值。</p><p>考虑多路增广。</p><p>dfs 每次传入两个参数：当前点编号和流过来了多少流量</p><p>原来的 dfs 过程：</p><ol type="1"><li>判断当前是否到达汇点，如果到达汇点，返回当前流量。</li><li>否则枚举有流量的边，继续 dfs。</li><li>dfs 如果返回正数，说明找到一条增广路，返回。</li></ol><p>我们实际上可以在第 3 步找到增广路时不立即停止，而是将当前流量减去 dfs返回的流量，表示有这么多的水已经流出去了。然后继续枚举边，枚举完毕/当前流量减到0 则停止。</p><p>貌似会死循环，我们可以用 bfs 分层。每次 bfs从源点开始，只走有流量的边，一个点的深度为到源点的最短距离。如果图不连通，说明找不到增广路了。</p><p>如果图连通，则反复 dfs。向下一个点搜索的前提是下一个点的深度 -1等于当前点深度。</p><p>然后加一个当前弧优化：dfs可能反复到达同一个点，而这个点的某些边可能已经被增广过了。因此在第一次到达该点的过程中，可以把链式前向星的fir[nod]修改为当前增广过但是还可能继续增广的边——之前的边增广过，流量没跑完，说明走那条边已经找不到增广路了。</p><p>每次分层情况都不一样，所以开一个 cur 数组当 fir 数组用，每次分层将cur 数组的每个值都初始化成原 fir 数组的值。</p><p>示例代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">edge</span> &#123;</span><br><span class="line">    i64 u, v;</span><br><span class="line">    edge *nex, *opp;</span><br><span class="line">    i64 w;</span><br><span class="line">&#125; graph[M];</span><br><span class="line">edge* tot = graph;</span><br><span class="line">edge *fir[N], *cur[N];</span><br><span class="line">i64 depth[N];</span><br><span class="line">i64 n, m, s, t; <span class="comment">// 点数，边数，源点，汇点</span></span><br><span class="line">queue&lt;i64&gt; q;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">add</span><span class="params">(i64 a, i64 b, i64 c)</span> </span>&#123;</span><br><span class="line">    ++tot;</span><br><span class="line">    tot-&gt;u = a;</span><br><span class="line">    tot-&gt;v = b;</span><br><span class="line">    tot-&gt;w = c;</span><br><span class="line">    tot-&gt;opp = tot + <span class="number">1</span>;</span><br><span class="line">    tot-&gt;nex = fir[a];</span><br><span class="line">    fir[a] = tot;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 反向边</span></span><br><span class="line">    ++tot;</span><br><span class="line">    tot-&gt;u = b;</span><br><span class="line">    tot-&gt;v = a;</span><br><span class="line">    tot-&gt;w = <span class="number">0</span>;</span><br><span class="line">    tot-&gt;opp = tot - <span class="number">1</span>;</span><br><span class="line">    tot-&gt;nex = fir[b];</span><br><span class="line">    fir[b] = tot;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">bfs</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">while</span> (q.<span class="built_in">size</span>())</span><br><span class="line">        q.<span class="built_in">pop</span>();</span><br><span class="line">    <span class="comment">// memset(depth, 0, sizeof depth);</span></span><br><span class="line">    cur[s] = fir[s];</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i &lt;= n; ++i)</span><br><span class="line">        depth[i] = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    depth[s] = <span class="number">1</span>;</span><br><span class="line">    q.<span class="built_in">push</span>(s);</span><br><span class="line">    <span class="keyword">while</span> (q.<span class="built_in">size</span>()) &#123;</span><br><span class="line">        i64 idx = q.<span class="built_in">front</span>();</span><br><span class="line">        q.<span class="built_in">pop</span>();</span><br><span class="line">        <span class="keyword">for</span> (edge* e = fir[idx]; e; e = e-&gt;nex) &#123;</span><br><span class="line">            <span class="keyword">if</span> (e-&gt;w &gt; <span class="number">0</span> &amp;&amp; !depth[e-&gt;v]) &#123;  <span class="comment">// 该边容量大于零且还没有被设置深度</span></span><br><span class="line">                depth[e-&gt;v] = depth[idx] + <span class="number">1</span>;</span><br><span class="line">                q.<span class="built_in">push</span>(e-&gt;v);</span><br><span class="line">                cur[e-&gt;v] = fir[e-&gt;v]; <span class="comment">// 当前弧优化</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!depth[t]) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">i64 <span class="title">dfs</span><span class="params">(i64 nod, i64 val)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (nod == t) <span class="keyword">return</span> val;  <span class="comment">// 边界即到达汇点</span></span><br><span class="line"></span><br><span class="line">    i64 res = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span> (edge* e = cur[nod]; e &amp;&amp; val; e = e-&gt;nex) &#123;</span><br><span class="line">        cur[nod] = e;</span><br><span class="line">        <span class="keyword">if</span> (depth[nod] + <span class="number">1</span> == depth[e-&gt;v] &amp;&amp; e-&gt;w) &#123;  <span class="comment">// 第一个条件判断分层图，第二个条件判断边的残量</span></span><br><span class="line">            i64 tmp = <span class="built_in">dfs</span>(e-&gt;v, <span class="built_in">min</span>(val, e-&gt;w));</span><br><span class="line">            <span class="keyword">if</span> (tmp &gt; <span class="number">0</span>) &#123;  <span class="comment">// 找到增广路</span></span><br><span class="line">                e-&gt;w -= tmp;</span><br><span class="line">                e-&gt;opp-&gt;w += tmp;</span><br><span class="line">                res += tmp;</span><br><span class="line">                val -= tmp;</span><br><span class="line">                <span class="comment">// return res;</span></span><br><span class="line">            &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!tmp)</span><br><span class="line">                depth[e-&gt;v] = <span class="number">-1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">i64 <span class="title">dinic</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    i64 res = <span class="number">0</span>;</span><br><span class="line">    i64 d = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">while</span> () &#123;</span><br><span class="line">        <span class="keyword">while</span> (d = <span class="built_in">dfs</span>(s, (i64)LLONG_MAX)) &#123;</span><br><span class="line">            res += d;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="isap">ISAP</h2><p>TODO<!-- Dinic 需要 bfs 和 dfs 好多次，有没有办法能够动态维护 dep 数组而不用再次 bfs 分层？ 回顾整个 dinic 过程，感性理解一下 dep 数组的变化，发现相当于除了源点以外的点在不断远离源点 --></p>]]></content>
    
    
    <summary type="html">增广路求最大流，但是拣重点写</summary>
    
    
    <content src="https://pic.imgdb.cn/item/6416687ca682492fccfb973a.jpg" type="image"/>
    
    
    <category term="图论" scheme="http://blog.tibrella.space/categories/%E5%9B%BE%E8%AE%BA/"/>
    
    
    <category term="网络流" scheme="http://blog.tibrella.space/tags/%E7%BD%91%E7%BB%9C%E6%B5%81/"/>
    
    <category term="最大流" scheme="http://blog.tibrella.space/tags/%E6%9C%80%E5%A4%A7%E6%B5%81/"/>
    
  </entry>
  
  <entry>
    <title>配对堆基本介绍</title>
    <link href="http://blog.tibrella.space/post/pairing-heap/"/>
    <id>http://blog.tibrella.space/post/pairing-heap/</id>
    <published>2023-05-20T03:42:35.000Z</published>
    <updated>2023-08-21T14:36:14.156Z</updated>
    
    <content type="html"><![CDATA[<p>飞快的可并堆</p><h2 id="时间复杂度">时间复杂度</h2><p>还没有完全严格的证明，但一般认为配对堆时间复杂度如下：<br />删除堆顶均摊 <span class="math inline">\(\operatorname{O}(\logn)\)</span><br />插入元素 <span class="math inline">\(\operatorname{O}(1)\)</span> 合并<span class="math inline">\(\operatorname{O}(1)\)</span></p><p>貌似这个可能比斐波那契堆快（</p><h2 id="节点存储">节点存储</h2><p>配对堆不是一个二叉树，一般采用“左儿子右兄弟”表示法，一个节点存储自己第一个儿子与右侧第一个兄弟。<br />有点抽象？实际上相当于每一个节点的儿子使用链表维护，然后该节点连着第一个子节点。<br /><div class="tag-plugin image"><div class="image-bg"><img src="https://pic1.imgdb.cn/item/64688453e03e90d874db5ef4.jpg"/></div></div></p><p>代码就非常好写了<br /><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> &#123;</span><br><span class="line"><span class="type">int</span> val;</span><br><span class="line">Node *child, *nex; <span class="comment">// 第一个儿子，下一个兄弟</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></p><h2 id="合并两个堆">合并两个堆</h2><p>比较根节点大小，把根节点大的堆直接改成根节点小的堆根节点的儿子即可。<br />图示中还是使用树本身形态。</p><div class="tag-plugin image"><div class="image-bg"><img src="https://pic1.imgdb.cn/item/64688720e03e90d874def660.jpg"/></div></div><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Node* <span class="title">meld</span><span class="params">(Node* x, Node* y)</span> </span>&#123;  <span class="comment">// 传入两个根节点</span></span><br><span class="line"><span class="keyword">if</span> (x == null) <span class="comment">// null 为空节点</span></span><br><span class="line"><span class="keyword">return</span> y;</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (y == null)</span><br><span class="line"><span class="keyword">return</span> x;</span><br><span class="line"><span class="keyword">if</span> (x-&gt;val &lt; y-&gt;val) &#123; <span class="comment">// x 比 y 小，y 当 x 的儿子</span></span><br><span class="line">y-&gt;nex = x-&gt;child; <span class="comment">// 旧儿子是新儿子的兄弟</span></span><br><span class="line">x-&gt;child = y; <span class="comment">// 新儿子是第一个儿子</span></span><br><span class="line"><span class="keyword">return</span> x;</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">x-&gt;nex = y-&gt;child;</span><br><span class="line">y-&gt;child = x;</span><br><span class="line"><span class="keyword">return</span> y;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="插入新节点">插入新节点</h2><p>新建节点，然后当成两个堆合并即可。 <figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">push</span><span class="params">(<span class="type">int</span> x)</span> </span>&#123;</span><br><span class="line">Node* y = <span class="built_in">new_node</span>(); <span class="comment">// 获取新节点</span></span><br><span class="line">y-&gt;val = x;</span><br><span class="line"><span class="keyword">if</span> (root == null) <span class="comment">// 当前堆为空的话，新节点为根</span></span><br><span class="line">root = y;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">root = <span class="built_in">meld</span>(root, y); <span class="comment">// 否则当成两个堆合并</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="弹出堆顶">弹出堆顶</h2><p>貌似要写完了，但是前面几个操作都没有对堆的性质进行维护，因此重头戏在这个部分...</p><p>删除根节点之后，遇到的是原来的子节点们组成的森林，想办法把它们合并到一起。<br />有了上面的合并操作，很容易想到将这些森林里的树一棵一棵合并的方法，但是这样时间复杂度就奔<span class="math inline">\(\operatorname{O}(n)\)</span>去了，因为每次删除根节点都只是单纯地把子节点比较一遍，非常缓慢。<br />于是说到了该数据结构名称的由来——“配对”。<br />我们把这个森林两两配对之后再合并成一个新堆，于是理想状态下，多次删除根结点，新根节点的子节点数量将为<span class="math inline">\(2\)</span>，均摊时间复杂度大约是 <spanclass="math inline">\(\operatorname{O}(\log n)\)</span> 的。</p><p>图示过程：<br /><div class="tag-plugin image"><div class="image-bg"><img src="https://pic1.imgdb.cn/item/64688db3e03e90d874e6d98a.jpg"/></div></div></p><p>首先实现一个用于配对的函数。<br /><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Node* <span class="title">merges</span><span class="params">(Node* x)</span> </span>&#123;</span><br><span class="line"><span class="keyword">if</span> (x == null || x-&gt;nex == null) <span class="keyword">return</span> x;  <span class="comment">// x 为空或者 x 没有下一个兄弟节点</span></span><br><span class="line">Node* y = x-&gt;nex;                           <span class="comment">// y 是 x 的下一个兄弟</span></span><br><span class="line">Node* c = y-&gt;nex;                           <span class="comment">// c 是再下一个兄弟</span></span><br><span class="line">x-&gt;nex = y-&gt;nex = null;                     <span class="comment">// 拆散</span></span><br><span class="line"><span class="keyword">return</span> <span class="built_in">meld</span>(<span class="built_in">merges</span>(c), <span class="built_in">meld</span>(x, y));         <span class="comment">// x 与 y 配对在一起，剩下的继续配对</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure> 然后弹出堆顶的操作就很好写了。<br /><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">pop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">Node* t = <span class="built_in">merges</span>(root-&gt;child);</span><br><span class="line"><span class="built_in">remove</span>(root);</span><br><span class="line">root = t;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></p><h2 id="模板">模板</h2><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> i64 = <span class="type">long</span> <span class="type">long</span>;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __SIZ 2000006</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Node</span> &#123;</span><br><span class="line">    i64 val;</span><br><span class="line">    Node *child, *nex;</span><br><span class="line">&#125; tree[__SIZ], *rubbish_bin[__SIZ]; <span class="comment">// 提早分配内存，new 太慢了</span></span><br><span class="line">Node *null = tree, *tot = tree;</span><br><span class="line">i64 bintop;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">pairing_heap</span> &#123;</span><br><span class="line">    Node* root;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">new_node</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Node* nod;</span><br><span class="line">        <span class="keyword">if</span> (bintop)</span><br><span class="line">            nod = rubbish_bin[bintop--]; <span class="comment">// 垃圾回收</span></span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            nod = ++tot;</span><br><span class="line">        nod-&gt;child = null;</span><br><span class="line">        nod-&gt;nex = null;</span><br><span class="line">        <span class="keyword">return</span> nod;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">remove</span><span class="params">(Node* x)</span> </span>&#123;</span><br><span class="line">        rubbish_bin[++bintop] = x;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">meld</span><span class="params">(Node* x, Node* y)</span> </span>&#123;  <span class="comment">// 合并两个堆</span></span><br><span class="line">        <span class="keyword">if</span> (x == null)</span><br><span class="line">            <span class="keyword">return</span> y;</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (y == null)</span><br><span class="line">            <span class="keyword">return</span> x;</span><br><span class="line">        <span class="keyword">if</span> (x-&gt;val &lt; y-&gt;val) &#123;</span><br><span class="line">            y-&gt;nex = x-&gt;child;</span><br><span class="line">            x-&gt;child = y;</span><br><span class="line">            <span class="keyword">return</span> x;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            x-&gt;nex = y-&gt;child;</span><br><span class="line">            y-&gt;child = x;</span><br><span class="line">            <span class="keyword">return</span> y;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">Node* <span class="title">merges</span><span class="params">(Node* x)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (x == null || x-&gt;nex == null) <span class="keyword">return</span> x;  <span class="comment">// x 为空或者 x 没有下一个兄弟节点</span></span><br><span class="line">        Node* y = x-&gt;nex;                           <span class="comment">// y 是 x 的下一个兄弟</span></span><br><span class="line">        Node* c = y-&gt;nex;                           <span class="comment">// c 是下一个兄弟</span></span><br><span class="line">        x-&gt;nex = y-&gt;nex = null;                     <span class="comment">// 拆散</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">meld</span>(<span class="built_in">merges</span>(c), <span class="built_in">meld</span>(x, y));         <span class="comment">// x 与 y 配对在一起，剩下的继续配对</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="built_in">pairing_heap</span>() &#123;</span><br><span class="line">        root = null;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">push</span><span class="params">(i64 x)</span> </span>&#123;</span><br><span class="line">        Node* y = <span class="built_in">new_node</span>();</span><br><span class="line">        y-&gt;val = x;</span><br><span class="line">        <span class="keyword">if</span> (root == null)</span><br><span class="line">            root = y;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">            root = <span class="built_in">meld</span>(root, y);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function">i64 <span class="title">top</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> root-&gt;val;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">pop</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        Node* t = <span class="built_in">merges</span>(root-&gt;child);</span><br><span class="line">        <span class="built_in">remove</span>(root);</span><br><span class="line">        root = t;</span><br><span class="line">    &#125;</span><br><span class="line">&#125; q;</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">配对堆，一种基于均摊复杂度的飞快的可并堆，写于 5.20</summary>
    
    
    
    <category term="数据结构" scheme="http://blog.tibrella.space/categories/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
    
    
    <category term="堆" scheme="http://blog.tibrella.space/tags/%E5%A0%86/"/>
    
  </entry>
  
  <entry>
    <title>线性代数</title>
    <link href="http://blog.tibrella.space/post/basic-linear-algebra/"/>
    <id>http://blog.tibrella.space/post/basic-linear-algebra/</id>
    <published>2023-05-01T16:00:00.000Z</published>
    <updated>2023-08-17T11:27:03.655Z</updated>
    
    <content type="html"><![CDATA[<h2 id="向量">向量</h2><blockquote><p>类比一维数组 —— zzz</p></blockquote><p>分为行向量与列向量。<br />行向量形如 <span class="math inline">\(\left[ 1,2,3\right]\)</span><br />列向量形如 <span class="math inline">\(\left[ \begin{array}{l} 1\newline 2 \newline 3\newline 4 \end{array} \right]\)</span><br />实际上高中 whk 里面的向量是二维向量，OI中用的向量大部分为多维向量，数组中每一个元素就是一维的坐标。<br />运算的前提很容易想到：同维度，也就是“数组”长度得一样长。</p><p>加法：分量分别相加 <spanclass="math inline">\([a,b,c]+[a,b,c]=[2a,2b,2c]\)</span><br />减法：加法逆运算。<br />数乘：把系数乘到分量上 <spanclass="math inline">\(3[a,b,c]=[3a,3b,3c]\)</span><br />数量积（内积/点乘）：分量分别相乘并相加，顾名思义，得到一个数值 <spanclass="math inline">\([a,b] \cdot [c,d] = ac+bd\)</span><br />向量积（外积/叉乘）：实际上没卵用，<del>所以我不会</del></p><h2 id="线性基">线性基</h2><p>线性代数中的定义：<span class="math inline">\(\mathbf V\)</span>中的极小线性无关向量集合 <span class="math inline">\(\{\mathbf a,\mathbfb,\mathbf c\dots \}\)</span> 能够表示出 <spanclass="math inline">\(\mathbf V\)</span> 中所有向量。<br />在 OI 中使用的线性基与上面定义略有不同：</p><h3 id="异或线性基">异或线性基</h3><p>给定一数列 <span class="math inline">\(a\)</span>，其线性基 <spanclass="math inline">\(s\)</span> 是满足以下性质的数列：</p><ol type="1"><li><span class="math inline">\(a\)</span> 中任意一个数都可以用 <spanclass="math inline">\(s\)</span> 中的数异或得到</li><li><span class="math inline">\(s\)</span> 中任意一些数异或和不为 <spanclass="math inline">\(0\)</span></li><li><span class="math inline">\(s\)</span>里面的数个数一定，并且在满足性质 <span class="math inline">\(1\)</span>的条件下数最少</li></ol><p>如果还是不能理解，别忘了线性基<strong>不一定</strong>是原数列的子集。</p>]]></content>
    
    
    <summary type="html">向量，矩阵，线性基</summary>
    
    
    
    <category term="数学" scheme="http://blog.tibrella.space/categories/%E6%95%B0%E5%AD%A6/"/>
    
    
    <category term="向量" scheme="http://blog.tibrella.space/tags/%E5%90%91%E9%87%8F/"/>
    
    <category term="线性代数" scheme="http://blog.tibrella.space/tags/%E7%BA%BF%E6%80%A7%E4%BB%A3%E6%95%B0/"/>
    
    <category term="矩阵" scheme="http://blog.tibrella.space/tags/%E7%9F%A9%E9%98%B5/"/>
    
    <category term="线性基" scheme="http://blog.tibrella.space/tags/%E7%BA%BF%E6%80%A7%E5%9F%BA/"/>
    
  </entry>
  
</feed>
