<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Guoweiyi]]></title><description><![CDATA[不知何时春日悄来临， 不知何日春花已落尽。]]></description><link>https://gwy.fun</link><image><url>https://www.gwy.fun/zhan/logoxin.svg</url><title>Guoweiyi</title><link>https://gwy.fun</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Tue, 14 Apr 2026 21:52:03 GMT</lastBuildDate><atom:link href="https://gwy.fun/feed" rel="self" type="application/rss+xml"/><pubDate>Tue, 14 Apr 2026 21:52:03 GMT</pubDate><language><![CDATA[zh-CN]]></language><follow_challenge><feedId>164352244660916224</feedId><userId>164351361005535232</userId></follow_challenge><item><title><![CDATA[数据结构-树]]></title><description><![CDATA[<link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307163451215.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307163814970.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307165151204.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307165544562.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307165941559.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307170028920.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307192535785.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260307195143754.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/ACM/ds-1">https://gwy.fun/posts/ACM/ds-1</a></blockquote><div><p>鸣谢·B站UP主<a href="https://space.bilibili.com/401399175/">蓝不过海呀</a>精彩的课程 本文为课程的笔记</p><h2 id="">树</h2><h3 id="base">Base</h3><ul><li>父节点（双亲节点） <strong>根节点无双亲</strong> / 孩子</li><li>有同一个父结点 为 兄弟</li><li>祖先 向上推至根节点 都为其祖先</li><li>子孙</li></ul><p>树的定义：n 个结点构成的优先集合</p><p><strong>n = 0 时称为空树</strong> n &gt; 0 时 有且仅有一个根节点 其余结点可分为 m 个<strong>不相交子集</strong></p><ul><li>树的高度 = 树的深度 = 树的层数 = 1</li><li>结点的深度：他所在的层次（从上往下数）</li><li>结点的高度：以他为根的字数的层数</li><li>结点的度：孩子的个数（分支的个数） 度为0的结点为叶节点</li><li>树的度：树中结点的最大度数</li><li>度为 m 的树与 m 叉树
<ul><li>相同点:树里面结点的度最多为m</li><li>不同点:
<ul><li>度为m的树至少要有一个结点的度等于m（<strong>对此时此刻情况的描述</strong>）</li><li>m叉树所有结点的度可以都小于m，甚至可以是空树（<strong>一种约束</strong> 只限制最多有 m 个分支）</li></ul></li></ul></li><li>有序树与无序树（可调换）</li><li>路径从上往下 长度 = 经过边的个数</li></ul><h3 id="">性质</h3><ul><li><p>节点数 n = 变数 + 1 = 所有结点度数之和 = $n<em>1 + 2n</em>2+ ···+mn_m$</p></li><li><p>总结点数 n = $n<em>0 + n</em>1 + n <em> 2 + n</em>k$  加到度为 k 的节点数  $n_0$为叶节点数</p></li><li><p>m 叉树的第 i 层最多有$m^{i-1}$个结点（度为 m 的数也一样）</p></li><li><p>高度为 h 的 m 叉树最多有多少个结点（度为 m 的数也一样）
$$
m^{0}+m^{1}+m^{2}+m^{3}+\ldots+m^{h-1}
$$</p></li></ul><p>$$
 \frac{m^{h}-1}{m-1}
$$</p><ul><li>高度为 h 的 m 叉树最少有 h 结点</li><li>h 层度为 m 的树最少有 $h-1+m$个结点（至少保证有一个节点度=m）</li><li>n 个结点的 m 叉树的最小高度（每一层尽可能填满）（度为 m 的数也一样）</li></ul><p>$$
\frac{m^{h-1}-1}{m-1}+1 \leq n \leq \frac{m^{h}-1}{m-1}
$$</p><p>$$
m^{h-1}&lt;n(m-1)+1 \leq m^{h}
$$</p><p>$$
h-1&lt;\log_{m}\left(n(m-1)+1\right)\leq h
$$</p><p>$$
\log<em>m(n(m-1)+1)\leq h&lt;\log</em>m(n(m-1)+1)+1()
$$</p><p>$$
h=\lceil\log_m(n(m-1)+1)\rceil
$$</p><p>(只能对左侧进行上取整)</p><ul><li>具有n个结点的m叉树的最大高度为n</li><li>度为m具有n个结点的树的最大高度为n-m+1</li></ul><h2 id="">二叉树</h2><h3 id="base">Base</h3><ul><li>度为 2 的树 - 至少有一个结点的度 = 2 - 不区分</li><li>二叉树 - 所有的结点的度可以都 &lt; 2 - 严格区分左右子树</li><li>满二叉树</li><li>完全二叉树 - 除最后一层其余层都是满的（最后一层可以不满 但必须从左向右排列）- 排满就是满二叉树</li></ul><h3 id="">性质</h3><ul><li><p>叶节点数 = 双分支节点数  + 1</p><p>$n<em>0 = n </em> 2 + 1$</p>
<p>$n = n<em>1 + 2n</em>2 + 1$</p></li></ul><h3 id="">完全二叉树的性质</h3><p>1.对于 i 结点：$\begin{aligned}&amp;\text{左孩子:}2i\text{ 右孩子:}2i+1\&amp;\text{父亲:}\left\lfloor\frac{i}{2}\right\rfloor\end{aligned}$</p><p>2.$\text{最后一个分支结点的编号是}\left\lfloor\frac{n}{2}\right\rfloor$ 后面的都是叶节点</p><p>3.最多只可能有一个度为 1 的结点</p><p>4.叶结点只可能在最后两层出现</p><p>5.高度 节点数为 n</p>
<p>$$
\begin{aligned} &amp; 2^{h-1}\leq n\leq2^{h}-1\  &amp; 2^{h-1}\leq n&lt;2^{h}\  &amp; h-1\leq\log<em>2n&lt;h\  &amp; \  &amp; \log</em>2n&lt;h\leq\log_2n+1\end{aligned}
$$</p>
<p>$$
\begin{aligned}&amp;2^{h-1}\leq n\leq2^h-1\&amp;2^{h-1}-1&lt;n\leq2^h-1\&amp;2^{h-1}&lt;n+1\leq2^h\&amp;h-1&lt;\log<em>2(n+1)\leq h\&amp;\log</em>2(n+1)\leq h&lt;\log_2(n+1)+1\end{aligned}
$$</p><p>$\begin{aligned}&amp;\text{具有}n\text{ 个结点的完全二叉树的高度为 }\&amp;\text{具有}n\text{ 个结点的二叉树的最小高度为 }\end{aligned}[\log<em>{2}(n+1)]\text{ 或 }\lfloor\log</em>{2}n\rfloor+1$</p><h3 id="">存储</h3><h4 id="">顺序存储</h4><ul><li>对于非完全二叉树 不存在结点占位 具有相同规律 但会有空间的浪费</li><li>最坏情况 - 左右单支数</li></ul><h4 id="---">链式存储 - 二叉链表</h4><ul><li>空指针的个数 = 节点数 + 1</li><li>（n个结点2n个指针，除了根结点其它n-1个结点被指针指着，所以有n-1个指针指向了实际结点）</li></ul><pre class="language-C++ lang-C++"><code class="language-C++ lang-C++">struct BTNode{
    char data;
    BTNode *lchild,*rchild;
  //C语言中 struct BTNode *lchild,*rchild;
}BTNode;
</code></pre>
<ul><li>若要经常找父亲 - 三叉链表</li></ul><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">struct BTNode{
    char data;
    BTNode *lchild,*parent,*rchild;
}BTNode;
</code></pre>
<h3 id="">遍历</h3><ul><li>前序遍历（先序遍历） - 根左右 - 深度优先搜索</li><li>中序遍历 - 左根右</li><li>后序遍历 - 左右根</li><li>层序遍历 - 广度优先搜索</li></ul><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307163451215.png" alt="image-1" height="794" width="1812"/></p><ul><li>先序序列/后序序列 只可确定根结点</li><li>中序序列 只要知道了根 就可以划分左右字数</li><li>层序序列 可确定根节点</li></ul><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307163814970.png" alt="image-2" height="804" width="2160"/></p><h3 id="">线索二叉树</h3><ul><li>正常使用二叉链表 不方便寻找一个节点的前驱和后继 只能通过遍历记录</li><li>但是有大量(n+1)的空指针被浪费了 可以将其利用</li><li>左空指针指向前驱 右空指针指向后继</li></ul><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307165151204.png" alt="image-3" height="942" width="2078"/></p><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">struct BTNode{
    char data;
    BTNode *lchild,*rchild;
  bool ltag,rtag;//true时表示指向前驱后继
}BTNode;
</code></pre>
<ul><li><p>先序线索化
<img src="https://www.gwy.fun/blog_ima/blog/image-20260307165544562.png" alt="image-4"/></p></li><li><p>中序线索化</p></li></ul><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307165941559.png" alt="image-5" height="764" width="2016"/></p><ul><li>后序线索化</li></ul><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307170028920.png" alt="image-6"/></p>
<ul><li>找中序前驱
<ul><li>ltag = 1</li><li>ltag = 0 从左子树的根开始沿着右孩子不断向下直到没有右孩子（不一定是叶节点）</li></ul></li><li>找中序后继
<ul><li>rtag = 1</li><li>rtag = 0 右子树最左下结点</li></ul></li><li>中序/先序优点 省去栈空间</li><li>找先序前驱
<ul><li>ltag = 1</li><li>ltag = 0 
<ul><li>如果 root 无前驱</li><li>如果是其父的左孩子 前驱为其父</li><li>如果是其父的右孩子且没左兄弟 前驱为其父</li><li>如果是其父的右孩子且有左兄弟 前驱为左兄弟子树中最后一个访问的</li><li>（都需要找父亲 故都需要遍历 无法直接找到）</li></ul></li></ul></li><li>找先序后继
<ul><li>rtag = 1</li><li>rtag = 0
<ul><li>有左孩子 后继为左孩子</li><li>无左孩子 后继为其右孩子<strong>（必有右孩子 要不rtag = 1）</strong></li></ul></li></ul></li><li>找后序前驱
<ul><li>ltag = 1</li><li>ltag = 0 
<ul><li>有右孩子 前驱为右孩子</li><li>没有右孩子 前驱为左孩子<strong>（必有左孩子 要不ltag = 1）</strong></li></ul></li></ul></li><li>找后序后继
<ul><li>rtag = 1</li><li>rtag = 0
<ul><li>root 无后继</li><li>是其父的右孩子 后继为其父</li><li>是其父的左孩子且没有右兄弟 后继为其父</li><li>是其父的左孩子且有右兄弟 后继为其父右子树后序遍历的第一个结点</li><li>（都需要找父亲 故都需要遍历 无法直接找到）</li></ul></li></ul></li></ul><p><strong>先序线索二叉树无法有效查找先序前驱</strong>
<strong>后序线索二叉树无法有效查找后序后继</strong>
<strong>后序线索二叉树的遍历仍需要栈的支持</strong></p><h2 id="">树的存储</h2><h4 id="">双亲表示法</h4><p><strong>找父亲容易</strong></p><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">struct Node{
    char data;
    int parent;/
}BTNode;
</code></pre>
<h4 id="">孩子表示法</h4><p>顺序存储 + 链式存储（类似图的领接表）</p><h4 id="">孩子兄弟表示法/二叉树表示法</h4><p>用二叉链表的形式存储</p><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">struct Node{
    char data;
    Node *firstchild;
  Node *nextsibling;//右兄弟
}Node;
</code></pre>
<p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307192535785.png" alt="image-66"/></p><h3 id="">树转换成二叉树</h3><p>如上 使用孩子兄弟表示法</p><h3 id="">森林转换成二叉树</h3><p>如上 先一次把每棵树转换成二叉树 再把每棵二叉树的根接到前一个根的右指针上 （统一）</p><p><strong>相反（沿着右指针）拆成若干二叉树</strong></p><h2 id="">树的遍历</h2><h3 id="">先根遍历</h3><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260307195143754.png" alt="image-9"/></p><h3 id="">后根遍历</h3><p><strong>先依次遍历每个子树 在访问根</strong></p><p><strong>对应相应的二叉树的中序遍历</strong></p><h3 id="">森林遍历</h3><ul><li>先序遍历 同上</li><li>中序（后序）遍历 等价于对应二叉树的中序遍历</li></ul><h2 id="">并查集</h2><ul><li><p>Find操作:查询元素属于哪个集合</p></li><li>Union操作:合并两个元素所属的集合</li><li>可以用树表示 根节点的编号就代表集合</li><li>其他节点指向父亲 根节点指向自己</li></ul><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">vector&lt;int&gt; p;

void Init(int n){
    p.resize(n);
    for(int i = 0;i &lt; n;i++) p[i] = i;//s[i] = 1,h[i] = 1;
    //p[i] = -1;
}

int find(int x){
    if(p[x] == x) return x;//p[x] &lt; 0
    //路径压缩
    else return p[x] = Find(p[x]);//优化效率find(p[x])
}

void Union(int x,int y){
    int rootx = Find(x);
    int rooty = Find(y);
    if(rootx != rooty){
        //p[rootx] = rooty;
        //按秩合并
        //按树的高度合并
        //其中路径压缩可能会改变树的高度
        //此时 h 数组记录的高度是估计的高度 仅使用按高度合并可以把树的高度控制在 logn 以内
        if(h[rootx] &lt; h[rooty]) p[rootx] = rooty;// = rootx
        else if(h[rootx] &gt; h[rooty]) p[rooty] = rootx; // = rooty
        else{
            p[rootx] = rooty;
            h[rooty]++;//--
        }
        //按树的大小合并size
        //优先将小树合并到大树上 合并后 s 叠加
        if(s[rootx] &lt;= s[rooty]){
            p[rootx] = rooty;
            s[rooty] += s[rootx];
        }else{
            p[rooty] = rootx;
            s[rootx] += s[rooty];
        }
    }
}
</code></pre></div><p style="text-align:right"><a href="https://gwy.fun/posts/ACM/ds-1#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/ACM/ds-1</link><guid isPermaLink="true">https://gwy.fun/posts/ACM/ds-1</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Sat, 07 Mar 2026 12:54:54 GMT</pubDate></item><item><title><![CDATA[计算机取证与流量分析]]></title><description><![CDATA[<link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260122170343815.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20260122170330372.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/CTF/quzheng">https://gwy.fun/posts/CTF/quzheng</a></blockquote><div><h1 id="">计算机取证</h1><h2 id="">简介</h2><ul><li><p>取证例题文件格式：zip、iso、vmdk、vhd、raw、dmp、dump、e01、vmem、mem、img、npbk（夜神模拟器）、evtx（windows日志）、reg（注册表）</p></li><li><p><strong>【关于注册表reg文件，千万不要使用本机直接打开】</strong></p><p>取证系统类型：</p><ul><li>Windows：NTFS/FAT/exFAT</li><li>Linux：Ext2/3/4</li><li>MacOS：HFS/HFS+</li></ul></li></ul><h3 id="">取证工具</h3><p>取证工具：volatility(2/3)、DiskGenius、（取证大师、火眼）付费</p><p>注册表工具：RegistryExplorer、<strong>MiTeC Windows Registry Recovery（WRR）</strong>、（RegistryWorkshop）付费</p><p>挂载工具：AccessData FTK Imager、CnCrypt、VeraCrypt、夜神模拟器、ArsenalImageMounterwindows</p><h2 id="web">WEB取证</h2><p>常见WEB网站源码路径Apache:/var/www/html，配置文件/etc/httpd/conf/httpd.conf</p><p>Weblogic:/xxx/domains/base_domain/sercers/AdminServer,配置文件/conf/server.xml</p><p>Nginx:/home/wwwroot/default，配置文件/etc/nginx/conf/nginx.conf</p><p>IIS:C:\inetpub\wwwroottomcat:server.xml war包在tomcat的webapps下，直接检索，获取war包</p><h2 id="">内存取证</h2><h3 id="">采集</h3><p>通过工具将内存中的全部数据完整拷贝为镜像文件<strong>（常见格式：raw、elf、dump、mem）</strong>，核心要求是不修改原内存数据。</p><p>采集工具：Windows：FTK Imager、DumpIt、WinPMEM</p><p>Linux/macOS：LiME（Linux Memory Extractor）、dd（简单场景，可能存在数据不完整风险）</p><p>通用开源工具：Volatility Framework（支持采集 + 分析）</p><p>注意事项：优先使用只读模式采集，避免工具自身进程写入内存破坏证据。</p><p><strong>系统层→进程层→数据层</strong></p><p>商业工具：FTK Imager、EnCase Forensic（司法取证首选）</p><p>开源工具：dd（Linux 命令行工具，简单易用）、dcfldd（dd 的增强版，支持哈希实时校验）、Guymager（图形化开源工具）。</p><p>指令示例（dd 工具）：# 将/dev/sda磁盘完整镜像为disk_image.raw，并实时生成MD5哈希</p><pre class=""><code class="">dd if=/dev/sda of=disk_image.raw bs=4M conv=noerror,syncmd5sum disk_image.raw &gt; disk_image.md5
</code></pre>
<h3 id="">思路与实操</h3><p>一般内存取证的题目会给一个镜像文件，可以首先尝试一下strings之类的命令看看有没有附加隐写或者有没有hints和非预期存放的flag，如果没有的话就只能老老实实取证了</p><ul><li>使用imageinfo获取镜像Profile信息， WinXPSP2x86之类的信息；</li><li>使用filescan对系统文件做全盘扫描，一般会在桌面上放压缩包图片之类的hints。可以结合系统命令做筛选findstr或者grep；</li><li>使用dumpfiles或者memdump对数据进行导出，前者导出文件后者是进程，然后可以分析这个文件或者进程；</li><li>前面导出的文件可能会存在密码或者只能拿到一部分有价值的数据，这个时候我们可以使用pstree、psscan、pslist、cmdscan等命令检查可疑项；</li><li>结合可疑项和前面导出的文件做文件分析，可能可疑项是一个key之类的的或者需要从可疑项中读取某个值；一般只要知道简单的vol的命令，然后能够结合前面的图片分析压缩包分析来对导出的文件做分析就差不多能拿到flag。</li></ul><h2 id="">注册表相关</h2><ul><li><p>SOFTWARE：包含系统软件和应用程序的配置信息。</p></li><li><p>SYSTEM：包含系统硬件配置及其驱动程序的关键信息。</p></li><li><p>SECURITY：有关系统安全设置的信息。</p></li><li><p>DEFAULT：包含系统默认用户配置的数据。</p></li><li><p>SAM：用于存储系统用户和组信息。</p></li><li><p>注册表位置：C:\Windows\System32\config</p></li></ul><h3 id="">关机时间</h3><p>HKEY<em>LOCAL</em>MACHINE\SYSTEM\ControlSet001\Control\Windows 的ShutdownTime键值，以64位Windows/FILETIME时间格式保存。</p><h3 id="">计算机名称</h3><p>HKEY<em>LOCAL</em>MACHINE\SYSTEM\ControlSet001\Control\ComputerName\ComputerName 的ComputerName键值</p><h3 id="">本地用户</h3><p>HKEY<em>LOCAL</em>MACHINE\SAM\SAM\Domains\Account\Users\Names</p><h3 id="">最后登录的用户</h3><p>HKEY<em>LOCAL</em>MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI</p><h3 id="">当前登录用户</h3><p>HKEY<em>LOCAL</em>MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI\SessionData\1</p><h1 id="">流量分析</h1><pre class=""><code class="">http.request.uri contains &quot;login&quot;
</code></pre>
<pre class=""><code class="">ip.src 源 / ip.dist 出 / ip.addr
tcp.port tcp.srcport tcp.dstport
</code></pre>
<pre class=""><code class="">tshark -r  -T fields  -e dns.qry.name | sort | uniq &gt; test.txt
</code></pre>
<h2 id="usb">USB</h2><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260122170343815.png" alt="image1" height="429" width="481"/></p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20260122170330372.png" alt="image2" height="447" width="524"/></p></div><p style="text-align:right"><a href="https://gwy.fun/posts/CTF/quzheng#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/CTF/quzheng</link><guid isPermaLink="true">https://gwy.fun/posts/CTF/quzheng</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Sat, 07 Feb 2026 08:09:27 GMT</pubDate></item><item><title><![CDATA[纪念《念奴娇·东方欲晓》]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/notes/15">https://gwy.fun/notes/15</a></blockquote><div><p>岁在乙巳冬月，值人民领袖毛泽东诞辰日，星垂平野，长风振衣，遥思不绝</p><p>魂兮归矣，瞰人间换了，此天此地。黎庶长怀开路人，泪化红旗星雨。笔起惊雷，弓鸣裂月，山海皆铭誓。沉浮谁主？苍茫万里澄霁。</p><p>遥想故国寒宵，冻云欲锁，赤焰燃荒砌。一叶红船犁暗夜，终得春回潮沸。雪岭魂雕，窑灯鬓影，俱是峥嵘气。乾坤重整，初心犹在心底。</p><p>山河列阵如碑，思君岁岁，肝胆依然洗。大道如砥通皓月，清风两袖未徙。禾下乘凉，云端牧箭，皆念当时意。东方既白，千峰披曙而起。</p></div><p style="text-align:right"><a href="https://gwy.fun/notes/15#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/notes/15</link><guid isPermaLink="true">https://gwy.fun/notes/15</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Thu, 25 Dec 2025 16:16:27 GMT</pubDate></item><item><title><![CDATA[网安新星赛WP]]></title><description><![CDATA[<link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109162850791.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109162928921.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109163118981.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109163328182.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109163818471.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/449b86a176aeee80c214b8c9102c550e.jpg"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109165654719.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109165828268.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109165843229.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109165949336.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109170652281.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109171344391.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109171504121.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/image-20251109175433711.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/CTF/wp-1">https://gwy.fun/posts/CTF/wp-1</a></blockquote><div><h1 id="crypto">Crypto</h1><h2 id="rsa2">RSA2</h2><p>使用<a href="http://factordb.com/">factor.db</a>分解质因数 n</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109162850791.png" alt="image" height="497" width="861"/></p><p>编写 Py 脚本</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109162928921.png" alt="image" height="631" width="1207"/></p>
<h2 id="base">少见的base</h2><p>通过尝试发现为Base58编码 解密</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109163118981.png" alt="image" height="501" width="862"/></p><h2 id="">签到题</h2><p>将最后一个字符更改为 }</p><h1 id="misc">Misc</h1><h2 id="fakezip">fakezip</h2><p>一开始打开压缩包提示输入密码 但其实是伪加密</p><p>故用 010 Editor 打开压缩包 删除 zip 加密标志</p><p>需要进入General purpose bit flag</p><p>将中心目录和本地头同时修改 以免解压报错</p><p>正常解压后有一张图片便是 flag</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109163328182.png" alt="image" height="760" width="1164"/></p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109163818471.png" alt="image"/></p><h1 id="reverse">Reverse</h1><h2 id="lucknum">lucknum</h2><p>使用 64 位 IDA打开文件 进行反编译 在 main 中发现 flag</p><p><img src="https://www.gwy.fun/blog_ima/blog/449b86a176aeee80c214b8c9102c550e.jpg" alt="449b86a176aeee80c214b8c9102c550e"/></p><h1 id="web">web</h1><h2 id="weakauth">weak_auth</h2><p>弱密码爆破</p><p>爆破前尝试最常见的 admin/123456 成功拿到 flag</p><h2 id="easyweb">Easy_web</h2><p>通过尝试用户名User does not exist 测试到管理员用户名为 admin</p><p>尝试爆破无果后注册账号</p><p>发现需要提权 尝试 changePwd 接口无果</p><p>后发现忘记密码修改接口可能存在漏洞 使用 Burp Suite 修改请求</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109165654719.png" alt="image"/></p><p>后成功登录管理账号 点击 Manage 提示IP Not allowed</p><p>加入头X-Forwarded-For:127.0.0.1</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109165828268.png" alt="image"/></p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109165843229.png" alt="image"/></p><p>进入源码后发现注释 指向文件管理 推测上传点</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109165949336.png" alt="image"/></p><p>需要伪装图片格式文件注入 记事本创建文件<code>&lt;?php phpinfo(); ?&gt;</code>更改后缀名为 jpg</p><p>尝试探测信息 看能不能调取环境变量等</p><p>使用Burp Suite 拦截发送到 Repeater更改 filename 为 php4</p><p>后失败 换为<code>&lt;script language=&quot;php&quot;&gt; phpinfo()&lt;/script&gt;</code>成功获取 flag</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109170652281.png" alt="image"/></p><h2 id="babypython">baby_python</h2><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109171344391.png" alt="image"/></p><p>第一次尝试确认可以执行表达式 大概率是 Jinja2</p><p>测试常见的方式 {{config}}   {{self}} {{[].class}}会被拦截</p><p>故需要转换 将[b] 改为 .getitem(b)</p><p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109171504121.png" alt="image"/></p>
<p>尝试使用object.<strong>subclasses</strong>() 列举所有类</p><p>点属性链可能被拦截 额外过滤了 base 或 pop</p><p>改为getattr链 打包成普通函数和字符串参数</p><p>但不同环境中 subclasses 的顺序不同 试一下</p><p><strong>获得起点<code>{{getattr(getattr(getattr(0,&#x27;__class__&#x27;),&#x27;__mro__&#x27;),&#x27;__getitem__&#x27;)(1)}}</code></strong></p><p><strong>得到长度<code>{{getattr(getattr(getattr(0,&#x27;__class__&#x27;),&#x27;__mro__&#x27;),&#x27;__getitem__&#x27;)(1).__subclasses__().__len__()}}</code></strong></p><p>为 225</p><p><strong>挨个遍历<code>{{getattr(getattr(getattr(0,&#x27;__class__&#x27;),&#x27;__mro__&#x27;),&#x27;__getitem__&#x27;)(1).__subclasses__().__getitem__(40).__name__}}</code></strong></p>
<p><img src="https://www.gwy.fun/blog_ima/blog/image-20251109175433711.png" alt="image"/></p>
<p><strong>得到 flag<code>{{getattr(getattr(getattr(0,&#x27;__class__&#x27;),&#x27;__mro__&#x27;),&#x27;__getitem__&#x27;)(1).__subclasses__().__getitem__(40)(&#x27;/flag&#x27;).read()}}</code></strong></p></div><p style="text-align:right"><a href="https://gwy.fun/posts/CTF/wp-1#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/CTF/wp-1</link><guid isPermaLink="true">https://gwy.fun/posts/CTF/wp-1</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Mon, 08 Dec 2025 02:52:46 GMT</pubDate></item><item><title><![CDATA[基础算法-前缀和与差分]]></title><description><![CDATA[<link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/58-02.webp"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/58-03.webp"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/58-03-20251127102451220.webp"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/58-04.webp"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/ACM/qianzhuihe">https://gwy.fun/posts/ACM/qianzhuihe</a></blockquote><div><p><strong>原数组：</strong>$[3,1,4,1,5,9]$
<strong>前缀和：</strong>$[3,4,8,9,14,23]$
<strong>差分：</strong>$[3,-2,3,-3,4,4]$</p><h3 id="">一维前缀和</h3><p>构建 $ S<em>i = S</em>{i - 1} + a_i$</p><p>快速求$[l,r]$ 的和 $S<em>r -S</em>{l-1}$</p><h3 id="">二维前缀和</h3><p><img src="https://www.gwy.fun/blog_ima/blog/58-02.webp" alt="img" height="476" width="525"/></p><p>类比一维的情形，$S<em>{i,j}$ 应该可以基于 $S</em>{i-1,j}$ 或 $S<em>{i,j-1}$ 计算，从而避免重复计算前面若干项的和。但是，如果直接将 $S</em>{i-1,j}$ 和 $S<em>{i,j-1}$ 相加，再加上 $A</em>{i,j}$，会导致重复计算 $S_{i-1,j-1}$ 这一重叠部分的前缀和，所以还需要再将这部分减掉。</p><p>$$
S<em>{i,j} = A</em>{i,j} + S<em>{i-1,j} + S</em>{i,j-1} - S_{i-1,j-1}.
$$</p><p>在已经预处理出二维前缀和后，要查询左上角为 $(i<em>1,j</em>1)$、右下角为 $(i<em>2,j</em>2)$ 的子矩阵的和，可以计算</p><p>$$
S<em>{i</em>2,j<em>2} - S</em>{i<em>1-1,j</em>2} - S<em>{i</em>2,j<em>1-1} + S</em>{i<em>1-1,j</em>1-1}.
$$</p><p>这可以在 $O(1)$ 时间内完成</p><blockquote><p>在二维的情形，以上算法的时间复杂度可以简单认为是 $O(mn)$，即与给定数组的大小成线性关系。但是，当维度 $k$ 增大时，由于容斥原理涉及的项数以指数级的速度增长，时间复杂度会成为 $O(2^kN)$，其中 $k$ 是数组维度，而 $N$ 是给定数组大小。因此，该算法不再适用。</p></blockquote>
<h2 id="">一维差分</h2><p><img src="https://www.gwy.fun/blog_ima/blog/58-03.webp" alt="img"/></p><p>对于序列 ${a<em>i}$，它的差分序列 ${D</em>i}$ 是指</p><p>$$
D<em>i = a</em>i - a<em>{i-1},~ a</em>0 = 0.
$$</p><h3 id=""><strong>构造与修改可以统一</strong></h3><p><code>insert(i,i,a[i]);</code></p><p><code>insert(l,r,c);</code></p><p>给区间 $[l,r]$ 中每个数 $+c$：
$$
\begin{align}
b[l]\ &amp;+!=c\
b[r+1]\ &amp;-!=c
\end{align}
$$
<img src="https://www.gwy.fun/blog_ima/blog/58-03-20251127102451220.webp"/></p><p>在所有修改操作结束后，可以通过前缀和操作恢复更新后的 ${a_i}$ 的值。单次修改是 $O(1)$ 的。查询时，需要做一次 $O(n)$ 的前缀和操作，随后每次查询都是 $O(1)$ 的。</p><h2 id="">二维差分</h2><p><img src="https://www.gwy.fun/blog_ima/blog/58-04.webp" alt="img"/></p><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">void insert(int x1,int y1,int x2,int y2){
    b[x1][y1] += 1;
    b[x2 + 1][y1] -=1;
    b[x1][y2 + 1] -=1;
    b[x2 + 1][y2 + 1] +=1;
}
</code></pre>
<pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
</code></pre></div><p style="text-align:right"><a href="https://gwy.fun/posts/ACM/qianzhuihe#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/ACM/qianzhuihe</link><guid isPermaLink="true">https://gwy.fun/posts/ACM/qianzhuihe</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Mon, 08 Dec 2025 02:51:35 GMT</pubDate></item><item><title><![CDATA[记录～自组服务器的新位置]]></title><description><![CDATA[<link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/2025-10-05_214522_747.jpg"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/2025-10-05_214515_436.jpg"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/notes/14">https://gwy.fun/notes/14</a></blockquote><div><p>自组服务器的最好安放位置--&gt;自己学校<em>(:з」∠)</em></p><p>准备与原有的几台服务器 装 PVE 组集群mua</p><p>跟我组一辈子服务器吧（）偶内盖！瓦达西！红豆泥！</p><p><img src="https://www.gwy.fun/blog_ima/blog/2025-10-05_214522_747.jpg" alt="p1" height="1920" width="1080"/></p><p><img src="https://www.gwy.fun/blog_ima/blog/2025-10-05_214515_436.jpg" alt="p2"/></p></div><p style="text-align:right"><a href="https://gwy.fun/notes/14#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/notes/14</link><guid isPermaLink="true">https://gwy.fun/notes/14</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Sun, 05 Oct 2025 13:58:15 GMT</pubDate></item><item><title><![CDATA[基础算法-STL]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/ACM/algorithm-strong-foundation-p3stl">https://gwy.fun/posts/ACM/algorithm-strong-foundation-p3stl</a></blockquote><div><h1 id="">壹·引用与致谢</h1><p>本文由https://io.zouht.com/154.html 补充与精简而来 遵守<a href="https://github.com/guoweiyi/notes-backup?tab=readme-ov-file#">CC-BY-SA-4.0 license</a></p><p>同时对作者@<a href="https://space.bilibili.com/23986264">ChrisKim</a>表示感谢 您的视频教程非常详细与易懂</p><p><strong>C++ 标准模板库 (STL, Standard Template Library)</strong>：包含一些常用数据结构与算法的模板的 C++ 软件库。</p><h1 id="">贰·常用容器</h1><h2 id="21--vector">2.1 向量 vector</h2><p><strong><code>#include &lt;vector&gt;</code></strong></p><p>连续的顺序的储存结构（和数组一样的类别），但是有长度可变的特性。</p><p>时间复杂度：$O(n)$</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr;
arr.push_back(3);// 尾接
arr.pop_back();  // 尾删

arr.size()

arr.resize(5,3); //改长赋初值 改短直接删

arr.empty(); // 如果是空返回 `true` 反之返回 `false`.

arr.clear();

for (auto ele : arr) cout &lt;&lt; ele &lt;&lt; endl;
for (int i = 0; i &lt; arr.size(); i++) cout &lt;&lt; a[i] &lt;&lt; endl;
</code></pre>
<p>尾接 &amp; 尾删 时间复杂度：均摊 $O(1)$</p><p>清空 时间复杂度：$O(n)$</p><p>改变长度 时间复杂度：$O(n)$</p><p>一般情况 <code>vector</code> 可以替换掉普通数组，除非该题<strong>卡常（指卡时间复杂度常数</strong>。</p><h3 id="">注意事项</h3><h4 id="">提前指定长度</h4><p>如果长度已经确定，那么应当直接在构造函数指定长度，而不是一个一个 <code>.push_back()</code>. 因为 <code>vector</code> 额外内存耗尽后的重分配是有时间开销的，直接指定长度就不会出现重分配了。</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">// 优化前: 522ms
vector&lt;int&gt; a;
for (int i = 0; i &lt; 1e8; i++)
    a.push_back(i);
// 优化后: 259ms
vector&lt;int&gt; a(1e8);
for (int i = 0; i &lt; a.size(); i++)
    a[i] = i;
</code></pre>
<h4 id="-sizet-">当心 size_t 溢出</h4><p>vector 获取长度的方法 <code>.size()</code> 返回值类型为 <code>size_t</code>，通常 OJ 平台使用的是 32 位编译器（有些平台例如 cf 可选 64 位），那么该类型范围为 $[0,2^{32})$.</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; a(65536);
long long a = a.size() * a.size(); // 直接溢出变成0了
</code></pre>
<h2 id="22--stack">2.2 栈 stack</h2><p><strong><code>#include &lt;stack&gt;</code></strong></p><p>通过二次封装双端队列 (deque) 容器，实现先进后出的栈数据结构。</p><h3 id="">常用方法</h3><table><thead><tr><th> 作用                   </th><th> 用法              </th><th style="text-align:left"> 示例                 </th></tr></thead><tbody><tr><td> 构造                   </td><td> <code>stack&lt;类型&gt; stk</code> </td><td style="text-align:left"> <code>stack&lt;int&gt; stk;</code>    </td></tr><tr><td> 进栈                   </td><td> <code>.push(元素)</code>     </td><td style="text-align:left"> <code>stk.push(1);</code>       </td></tr><tr><td> 出栈                   </td><td> <code>.pop()</code>          </td><td style="text-align:left"> <code>stk.pop();</code>         </td></tr><tr><td> 取栈顶                 </td><td> <code>.top()</code>          </td><td style="text-align:left"> <code>int a = stk.top();</code> </td></tr><tr><td> 查看大小 / 清空 / 判空 </td><td> 同                </td><td style="text-align:left"> 同                   </td></tr><tr><td> Stk.clear()            </td><td> 错误              </td><td style="text-align:left"> 错误                 </td></tr></tbody></table><h3 id="">注意事项</h3><h4 id="">不可访问内部元素！</h4><p><strong>下面都是错误用法</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (int i = 0; i &lt; stk.size(); i++)
    cout &lt;&lt; stk[i] &lt;&lt; endl;
for (auto ele : stk)
    cout &lt;&lt; stk &lt;&lt; endl;
</code></pre>
<h2 id="23--queue">2.3 队列 queue</h2><p><strong><code>#include &lt;queue&gt;</code></strong></p><p>通过二次封装双端队列 (deque) 容器，实现先进先出的队列数据结构。</p><h3 id="">常用方法</h3><table><thead><tr><th> 作用                   </th><th> 用法              </th><th> 示例                   </th></tr></thead><tbody><tr><td> 构造                   </td><td> <code>queue&lt;类型&gt; que</code> </td><td> <code>queue&lt;int&gt; que;</code>      </td></tr><tr><td> 进队                   </td><td> <code>.push(元素)</code>     </td><td> <code>que.push(1);</code>         </td></tr><tr><td> 出队（出的是队首       </td><td> <code>.pop()</code>          </td><td> <code>que.pop();</code>           </td></tr><tr><td> 取队首                 </td><td> <code>.front()</code>        </td><td> <code>int a = que.front();</code> </td></tr><tr><td> 取队尾                 </td><td> <code>.back()</code>         </td><td> <code>int a = que.back();</code>  </td></tr><tr><td> 查看大小 / 清空 / 判空 </td><td> 同                </td><td> 同                     </td></tr></tbody></table><h3 id="">注意事项</h3><p>不可访问内部元素！<strong>同栈stack</strong></p><h2 id="24--priorityqueue">2.4 优先队列 priority_queue</h2><p><strong><code>#include &lt;queue&gt;</code></strong></p><p>提供常数时间的最大元素查找，对数时间的插入与提取，底层原理是二叉堆。</p><h3 id="">常用方法</h3><p><strong><code>priority_queue&lt;类型, 容器, 比较器&gt; pque</code></strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">priority_queue&lt;int&gt; pque1;//堆顶 1 3 4 4 最大的在堆顶  储存int的大顶堆
priority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; pque2; // 最小的在堆顶 储存int的小顶堆

//二维写法
priority_queue&lt;pair&lt;int,int&gt;, vector&lt;pair&lt;int,int&gt;&gt;, cmp&gt; arr;

//比较器
struct cmp {
    bool operator()(const pair&lt;int,int&gt; &amp;a, const pair&lt;int,int&gt; &amp;b) const {
        return a.second &lt; b.second;
    }
};
</code></pre>
<table><thead><tr><th> 作用            </th><th> 用法          </th><th> 示例                 </th></tr></thead><tbody><tr><td> 进堆            </td><td> <code>.push(元素)</code> </td><td> <code>que.push(1);</code>       </td></tr><tr><td> 出堆            </td><td> <code>.pop()</code>      </td><td> <code>que.pop();</code>         </td></tr><tr><td> 取堆顶          </td><td> <code>.top()</code>      </td><td> <code>int a = que.top();</code> </td></tr><tr><td> 查看大小 / 判空 </td><td> 略            </td><td> 略                   </td></tr></tbody></table><p><strong>进出队复杂度 $O(\log n)$</strong>，取堆顶 $O(1)$.</p><h3 id="">适用情形</h3><p>持续维护元素的有序性：每次向队列插入大小不定的元素，或者每次从队列里取出大小最小/最大的元素，元素数量 $n$，插入操作数量 $k$.</p><ul><li>每次插入后进行快速排序：$k\cdot n\log n$</li><li>使用优先队列维护：$k\cdot\log n$</li></ul><h3 id="">注意事项</h3><h4 id="">仅堆顶可读</h4><p>只可访问堆顶，其他元素都无法读取到。<strong>下面是错误用法：</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">cout &lt;&lt; pque[1] &lt;&lt; endl;
</code></pre>
<h4 id="">所有元素不可写</h4><p>堆中所有元素是不可修改的。</p><p>如果你恰好要修改的是堆顶元素，那么是可以完成的：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">int tp = pque.top();
pque.pop();
pque.push(tp + 1);
</code></pre>
<h2 id="25--sethttpszhcppreferencecomwcppcontainerset">2.5 集合 <a href="https://zh.cppreference.com/w/cpp/container/set">set</a></h2><p><strong><code>#include &lt;set&gt;</code></strong></p><p>提供对数时间的插入、删除、查找的集合数据结构。底层原理是红黑树。</p><table><thead><tr><th> 集合三要素 </th><th> 解释                           </th><th> set           </th><th> multiset      </th><th> unordered_set </th></tr></thead><tbody><tr><td> 确定性     </td><td> 一个元素要么在集合中，要么不在 </td><td> ✔             </td><td> ✔             </td><td> ✔             </td></tr><tr><td> 互异性     </td><td> 一个元素仅可以在集合中出现一次 </td><td> ✔             </td><td> ❌（任意次）   </td><td> ✔             </td></tr><tr><td> 无序性     </td><td> 集合中的元素是没有顺序的       </td><td> ❌（从小到大） </td><td> ❌（从小到大） </td><td> ✔             </td></tr></tbody></table><h3 id="">常用方法</h3><p><strong><code>set&lt;类型, 比较器&gt; st</code></strong></p><ul><li>类型：要储存的数据类型</li><li>比较器：比较大小使用的比较器，默认为 <code>less&lt;类型&gt;</code>，可自定义</li></ul><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">set&lt;int&gt; st1;               // 储存int的集合（从小到大）
set&lt;int, greater&lt;int&gt;&gt; st2; // 储存int的集合（从大到小）

if(st.find(1) != st.end()) cout &lt;&lt; &quot;yes&quot; &lt;&lt; endl; // 查找
</code></pre>
<p>可使用迭代器进行遍历：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (set&lt;int&gt;::iterator it = st.begin(); it != st.end(); ++it) cout &lt;&lt; *it &lt;&lt; endl;
for (auto it = st.begin(); it != st.end(); ++it) cout &lt;&lt; *it &lt;&lt; endl;
</code></pre>
<p>基于范围的循环（C++ 11）：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (auto &amp;ele : st) cout &lt;&lt; ele &lt;&lt; endl;
</code></pre>
<table><thead><tr><th> 作用                        </th><th> 用法            </th><th> 示例                    </th></tr></thead><tbody><tr><td> 插入元素                    </td><td> <code>.insert(元素)</code> </td><td> <code>st.insert(1);</code>         </td></tr><tr><td> 删除元素                    </td><td> <code>.erase(元素)</code>  </td><td> <code>st.erase(2);</code>          </td></tr><tr><td> 查找元素                    </td><td> <code>.find(元素)</code>   </td><td> <code>auto it = st.find(1);</code> </td></tr><tr><td> 判断元素是否存在（只有1/0） </td><td> <code>.count(元素)</code>  </td><td> <code>st.count(3);</code>          </td></tr><tr><td> 查看大小 / 清空 / 判空      </td><td> 同              </td><td> 同                      </td></tr></tbody></table><p>增删查时间复杂度均为 $O(\log n)$</p><h3 id="">适用情形</h3><ul><li>元素去重：$[1,1,3,2,4,4]\to[1,2,3,4]$</li><li>维护顺序：$[1,5,3,7,9]\to[1,3,5,7,9]$</li></ul><h3 id="">注意事项</h3><h4 id="">不存在下标索引</h4><p>set 虽说可遍历，但仅可使用迭代器进行遍历，它不存在下标这一概念，无法通过下标访问到数据。</p><h4 id="">元素只读</h4><p>set 的迭代器取到的元素是只读的（因为是 const 迭代器），不可修改其值。如果要改，需要先 erase 再 insert. <strong>下面是错误用法：</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">cout &lt;&lt; *st.begin() &lt;&lt; endl; // 正确。可读。
*st.begin() = 1;             // 错误！不可写！
</code></pre>
<h4 id="">不可用迭代器计算下标</h4><p>set 的迭代器不能像 vector 一样相减得到下标。<strong>下面是错误用法：</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">auto it = st.find(2);      // 正确，返回2所在位置的迭代器。
int idx = it - st.begin(); // 错误！不可相减得到下标。
</code></pre>
<h2 id="26--map">2.6 映射 map</h2><p><strong><code>#include &lt;map&gt;</code></strong></p><p>提供对数时间的有序键值对结构。底层原理是红黑树。</p><table><thead><tr><th> 性质   </th><th> 解释                         </th><th> map           </th><th> multimap      </th><th> unordered_map </th></tr></thead><tbody><tr><td> 互异性 </td><td> 一个键仅可以在映射中出现一次 </td><td> ✔             </td><td> ❌（任意次）   </td><td> ✔             </td></tr><tr><td> 无序性 </td><td> 键是没有顺序的               </td><td> ❌（从小到大） </td><td> ❌（从小到大） </td><td> ✔             </td></tr></tbody></table><h3 id="">常用方法</h3><p><strong><code>map&lt;键类型, 值类型, 比较器&gt; mp</code></strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">map&lt;int, int&gt; mp1;               // int-&gt;int 的映射（键从小到大）
map&lt;int, int, greater&lt;int&gt;&gt; st2; // int-&gt;int 的映射（键从大到小）
</code></pre>
<p>可使用迭代器进行遍历：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (map&lt;int, int&gt;::iterator it = mp.begin(); it != mp.end(); ++it)
    cout &lt;&lt; it-&gt;first &lt;&lt; &#x27; &#x27; &lt;&lt; it-&gt;second &lt;&lt; endl;
</code></pre>
<p>基于范围的循环（C++ 11）：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (auto &amp;pr : mp) cout &lt;&lt; pr.first &lt;&lt; &#x27; &#x27; &lt;&lt; pr.second &lt;&lt; endl;
</code></pre>
<table><thead><tr><th> 作用                                 </th><th> 用法           </th><th> 示例                    </th></tr></thead><tbody><tr><td> 增 / 改 / 查元素                     </td><td> 中括号         </td><td> <code>mp[1] = 2;</code>            </td></tr><tr><td> 查元素（返回迭代器）                 </td><td> <code>.find(元素)</code>  </td><td> <code>auto it = mp.find(1);</code> </td></tr><tr><td> 删除元素                             </td><td> <code>.erase(元素)</code> </td><td> <code>mp.erase(2);</code>          </td></tr><tr><td> 存在？普通 map 有互异性 只能返回 1/0 </td><td> <code>.count(元素)</code> </td><td> <code>mp.count(3);</code>          </td></tr><tr><td> 查看大小 / 清空 / 判空               </td><td> 略             </td><td> 略                      </td></tr></tbody></table><p>增删改查时间复杂度均为 $O(\log n)$</p><h3 id="">适用情形</h3><p>需要维护映射的场景可以使用：输入若干字符串，统计每种字符串的出现次数。(<code>map&lt;string, int&gt; mp</code>)</p><h3 id="">注意事项</h3><h4 id="">中括号访问时默认值</h4><p>如果使用中括号访问 map 时对应的键不存在，那么会新增这个键，并且值为默认值，因此中括号会影响键的存在性。</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">map&lt;char, int&gt; mp;
cout &lt;&lt; mp.count(&#x27;a&#x27;) &lt;&lt; endl; // 0
mp[&#x27;a&#x27;];                       // 即使什么都没做，此时mp[&#x27;a&#x27;]=0已经插入了
cout &lt;&lt; mp.count(&#x27;a&#x27;) &lt;&lt; endl; // 1
cout &lt;&lt; mp[&#x27;a&#x27;] &lt;&lt; endl;       // 0
</code></pre>
<h4 id="">不可用迭代器计算下标</h4><p>map 的迭代器不能像 vector 一样相减得到下标。<strong>下面是错误用法：</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">auto it = mp.find(&#x27;a&#x27;);      // 正确，返回2所在位置的迭代器。
int idx = it - mp.begin();   // 错误！不可相减得到下标。
</code></pre>
<h2 id="27--string">2.7 字符串 string</h2><p><strong><code>#include &lt;string&gt;</code></strong></p><h3 id="">常用方法</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">string s1;           // 构造字符串，为空
string s2 = &quot;awa!&quot;;  // 构造字符串，并赋值awa!
string s3(10, &#x27;6&#x27;);  // 构造字符串，通过构造函数构造为6666666666
</code></pre>
<table><thead><tr><th> 作用                   </th><th> 用法                          </th><th> 示例                            </th></tr></thead><tbody><tr><td> 修改、查询指定下标字符 </td><td> <code>[]</code>                          </td><td> <code>s[1] = &#x27;a&#x27;;</code>                   </td></tr><tr><td> 是否相同               </td><td> <code>==</code>                          </td><td> <code>if (s1 == s2) ...</code>             </td></tr><tr><td> 字符串连接             </td><td> <code>+</code>                           </td><td> <code>string s = s1 + s2;</code>           </td></tr><tr><td> 尾接字符串             </td><td> <code>+=</code>                          </td><td> <code>s += &quot;awa&quot;;</code>                   </td></tr><tr><td> 取子串                 </td><td> <code>.substr(起始下标, 子串长度)</code> </td><td> <code>string sub = s.substr(2, 10);</code> </td></tr><tr><td> 查找字符串             </td><td> <code>.find(字符串, 起始下标)</code>     </td><td> <code>int pos = s.find(&quot;awa&quot;);</code>      </td></tr></tbody></table><p>数值与字符串互转（C++11）</p><table><thead><tr><th> 源                                             </th><th> 目的        </th><th> 函数        </th></tr></thead><tbody><tr><td> int / long long / float / double / long double </td><td> string      </td><td> to_string() </td></tr><tr><td> string                                         </td><td> int         </td><td> stoi()      </td></tr><tr><td> string                                         </td><td> long long   </td><td> stoll()     </td></tr><tr><td> string                                         </td><td> float       </td><td> stof()      </td></tr><tr><td> string                                         </td><td> double      </td><td> stod()      </td></tr><tr><td> string                                         </td><td> long double </td><td> stold()     </td></tr></tbody></table><h3 id="">注意事项</h3><h4 id="-">尾接字符串一定要用 <code>+=</code></h4><p>string 的 += 运算符，将会在原字符串原地尾接字符串。而 + 了再 = 赋值，会先生成一个临时变量，在复制给 string.</p><p>通常字符串长度可以很长，如果使用 + 字符串很容易就 TLE 了。</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">// 优化前: 15139ms
string s;
for (int i = 0; i &lt; 5e5; i++)
    s = s + &quot;a&quot;;

// 优化后: &lt; 1ms (计时器显示0)
string s;
for (int i = 0; i &lt; 5e5; i++)
    s += &quot;a&quot;;
</code></pre>
<h4 id="substr-"><code>.substr()</code> 方法的奇葩参数</h4><p><strong>一定要注意，C++ string 的取子串的第一个参数是子串起点下标，第二个参数是子串长度。</strong></p><p><strong>第二个参数不是子串终点！不是子串终点！要与 java 等其他语言区分开来。</strong></p><h4 id="find-"><code>.find()</code> 方法的复杂度</h4><p>该方法实现为暴力实现，时间复杂度为 $O(n^2)$.</p><h2 id="28--pair">2.8 二元组 pair</h2><p><strong><code>#include &lt;utility&gt;</code></strong></p><p>顾名思义，就是储存二元组的。</p><h3 id="">常用方法</h3><p><strong><code>pair&lt;第一个值类型, 第二个值类型&gt; pr</code></strong></p><ul><li>第一个值类型：要储存的第一个值的数据类型</li><li>第二个值类型：要储存的第二个值的数据类型</li></ul><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">pair&lt;int, int&gt; p1;
pair&lt;int, long long&gt; p2;
pair&lt;char, int&gt; p3;
// ...
</code></pre>
<p>赋值</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">pair&lt;int, char&gt; pr = {1, &#x27;a&#x27;};
</code></pre>
<p>直接取值</p><ul><li>取第一个值：<code>.first</code></li><li>取第二个值：<code>.second</code></li></ul><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">pair&lt;int, char&gt; pr = {1, &#x27;a&#x27;};
int awa = pr.first;
char bwb = pr.second;
</code></pre>
<p>结构化绑定 C++17</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">pair&lt;int, char&gt; pr = {1, &#x27;a&#x27;};
auto &amp;[awa, bwb] = pr;
</code></pre>
<p>判同 直接用 <code>==</code> 运算符</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">pair&lt;int, int&gt; p1 = {1, 2};
pair&lt;int, int&gt; p2 = {1, 3};
if (p1 == p2) { ... } // false
</code></pre>
<h1 id="">叁·迭代器简介</h1><h2 id="31">3.1迭代器认识</h2><p>对于一个 vector，我们可以用下标遍历：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (int i = 0; i &lt; a.size(); i++)
    cout &lt;&lt; a[i] &lt;&lt; endl;
</code></pre>
<p>我们同时也可以用迭代器来遍历：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (vector&lt;int&gt;::iterator it = a.begin(); it != a.end(); ++it)
    cout &lt;&lt; *it &lt;&lt; endl;
</code></pre>
<ul><li><code>a.begin()</code> 是一个迭代器，指向的是第一个元素</li><li><code>a.end()</code> 是一个迭代器，指向的是最后一个元素<strong>再后面一位</strong></li><li>上述迭代器具有自增运算符，自增则迭代器向下一个元素移动</li><li>迭代器与指针相似，如果对它使用解引用运算符，即 <code>*it</code>，就能取到对应值了</li></ul><h2 id="32-">3.2 为何需要迭代器？</h2><p>很多数据结构并不是线性的（例如红黑树），对于非线性数据结构，下标是无意义的。无法使用下标来遍历整个数据结构。</p><p>迭代器的作用就是定义某个数据结构的遍历方式，通过迭代器的增减，代表遍历到的位置，通过迭代器便能成功遍历非线性结构了。</p><p>例如，set 的实现是红黑树，我们是没法用下标来访问元素的。但是通过迭代器，我们就能遍历 set 中的元素了：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for (auto it = st.begin(); it != st.end(); ++it) cout &lt;&lt; *it &lt;&lt; endl;
</code></pre>
<h2 id="33-">3.3 迭代器用法</h2><p>对于 vector 容器，它的迭代器功能比较完整，以它举例：</p><ul><li><code>.begin()</code>：头迭代器</li><li><code>.end()</code>：尾迭代器</li><li><code>.rbegin()</code>：反向头迭代器</li><li><code>.rend()</code>：反向尾迭代器</li><li>迭代器 <code>+</code> 整型：将迭代器向后移动</li><li>迭代器 <code>-</code> 整型：将迭代器向前移动</li><li>迭代器 <code>++</code>：将迭代器向后移动 1 位</li><li>迭代器 <code>--</code>：将迭代器向前移动 1 位</li><li>迭代器 <code>-</code> 迭代器：两个迭代器的距离// set 不可用</li><li><code>prev(it)</code>：返回 it 的前一个迭代器</li><li><code>next(it)</code>：返回 it 的后一个迭代器</li></ul><h2 id="34-">3.4 常见问题</h2><p><strong><code>.end()</code> 和 <code>.rend()</code> 指向的位置是无意义的值</strong></p><p>对于一个长度为 10 的数组：<code>for (int i = 0; i &lt; 10; i++)</code>，第 10 位是不可访问的</p><p>对于一个长度为 10 的容器：<code>for (auto it = a.begin(); it != a.end(); ++it)</code>，.end 是不可访问的</p><p><strong>不同容器的迭代器功能可能不一样</strong></p><p>迭代器细化的话有正向、反向、双向，每个容器的迭代器支持的运算符也可能不同，因此不同容器的迭代器细节很有可能是不一样的。</p><p><strong>删除操作时需要警惕</strong></p><h1 id="">肆·常用算法</h1><h2 id="41-swap">4.1 <code>swap()</code></h2><p>交换两个变量的值</p><h2 id="42-sort">4.2 <code>sort()</code></h2><p>默认排序从小到大</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr{1, 9, 1, 9, 8, 1, 0};
sort(arr.begin(), arr.end());
// arr = [0, 1, 1, 1, 8, 9, 9]
</code></pre>
<p>如果要从大到小，则需要传比较器进去。</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr{1, 9, 1, 9, 8, 1, 0};
sort(arr.begin(), arr.end(), greater&lt;int&gt;());
// arr = [9, 9, 8, 1, 1, 1, 0]
</code></pre>
<p>如果需要完成特殊比较，则需要手写比较器。</p><p>比较器函数返回值是 bool 类型，传参是需要比较的两个元素。记我们定义的该比较操作为 $\star$：</p><ul><li>若 $a\star b$，则比较器函数应当返回 <code>true</code></li><li>若 $a\not\star b$，则比较器函数应当返回 <code>false</code></li></ul><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">bool cmp(pair&lt;int, int&gt; a,pair&lt;int, int&gt; b){
    //第二位从小到大
    if(a.second != b.second) return a.second &lt; b.second;
    //第一位从大到小
    return a.first &gt; b.first;
}
//如果传入cmp的两个元素相同 则他一定会返回false 与 sort 函数实现有关
// return a.first &gt;= b.first 错误写法 会 RE
int main()
{
    vector&lt;pair&lt;int, int&gt;&gt; arr{{1, 9}, {2, 9}, {8, 1}, {0, 0}};
    sort(arr.begin(), arr.end(), cmp);//手写比较器
    // arr = [(0, 0), (8, 1), (2, 9), (1, 9)]
    for (auto ele : arr) cout &lt;&lt; ele.first &lt;&lt; &quot; &quot; &lt;&lt; ele.second &lt;&lt; endl;
    return 0;
}
</code></pre>
<h2 id="43-lowerbound--upperbound">4.3 <code>lower_bound()</code> / <code>upper_bound()</code></h2><p>在<strong>已升序排序</strong>的元素中，应用二分查找检索指定元素，返回对应元素迭代器位置。<strong>找不到则返回尾迭代器。</strong></p><ul><li><code>lower_bound()</code>: 寻找 $\geq x$ 的第一个元素的位置</li><li><code>upper_bound()</code>: 寻找 $&gt;x$ 的第一个元素的位置</li></ul><p>怎么找 $\leq x$ / $&lt; x$ 的第一个元素呢？</p><ul><li>$&gt;x$ 的第一个元素的前一个元素（如果有）便是 $\leq x$ 的第一个元素</li><li>$\geq x$ 的第一个元素的前一个元素（如果有）便是 $&lt;x$ 的第一个元素</li></ul><p><strong>返回的是迭代器，如何转成下标索引呢？减去头迭代器即可。</strong></p><p><strong>用法示例</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr{0, 1, 1, 1, 8, 9, 9};
vector&lt;int&gt;::iterator it = lower_bound(arr.begin(), arr.end(), 7);
int idx = it - arr.begin();
// idx = 4
</code></pre>
<p>我们通常写成一行：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr{0, 1, 1, 1, 8, 9, 9};
idx = lower_bound(arr.begin(), arr.end(), 7) - arr.begin(); // 4
idx = lower_bound(arr.begin(), arr.end(), 8) - arr.begin(); // 4
idx = upper_bound(arr.begin(), arr.end(), 7) - arr.begin(); // 4
idx = upper_bound(arr.begin(), arr.end(), 8) - arr.begin(); // 5
</code></pre>
<h2 id="44-reverse">4.4 <code>reverse()</code></h2><p>反转一个可迭代对象的元素顺序</p><p><strong>用法示例</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr(10);
iota(arr.begin(), arr.end(), 1);
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
reverse(arr.begin(), arr.end());
// 10, 9, 8, 7, 6, 5, 4, 3, 2, 1
reverse(arr.begin() + 2,arr.begin() + 6);//反转 为左闭右开区间 [2,6) 反转 2345
</code></pre>
<h2 id="45-max--min">4.5 <code>max()</code> / <code>min()</code></h2><p>返回最大值 / 最小值的**数值</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">int mx = max({1, 2, 3, 4}); // 4
int mn = min({1, 2, 3, 4}); // 1
</code></pre>
<h2 id="46-unique">4.6 <code>unique()</code></h2><p>消除数组的重复<strong>相邻</strong>元素，数组长度不变，但是有效数据缩短，返回的是有效数据位置的结尾迭代器。</p><p>例如：$[1,1,4,5,1,4]\to[1,4,5,1,4,\underline?]$，下划线位置为返回的迭代器指向。</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">template&lt; class ForwardIt &gt;
ForwardIt unique( ForwardIt first, ForwardIt last );
</code></pre>
<p><strong>用法示例</strong></p><p>单独使用 unique 并不能达成去重效果，因为它只消除<strong>相邻</strong>的重复元素。但是如果序列有序，那么它就能去重了。</p><p>但是它去重后，序列尾部会产生一些无效数据：$[1,1,2,4,4,4,5]\to[1,2,4,5,\underline?,?,?]$，为了删掉这些无效数据，我们需要结合 erase.</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;int&gt; arr1{1,2,1,4,5,4,4};
//1 1 2 4 4 4 5
//1 2 4 5 *(unique返回*的位置)
sort(arr1.begin(),arr1.end());
arr1.erase(unique(arr1.begin(),arr1.end()), arr1.end());
</code></pre>
<h2 id="47-">4.7 数学函数</h2><p>所有函数参数均支持 <code>int</code> / <code>long long</code> / <code>float</code> / <code>double</code> / <code>long double</code></p><table><thead><tr><th> 公式                           </th><th> 示例         </th></tr></thead><tbody><tr><td> $f(x)=\lvert x\rvert$          </td><td> <code>abs(-1.0)</code>  </td></tr><tr><td> $f(x)=e^x$                     </td><td> <code>exp(2)</code>     </td></tr><tr><td> $f(x)=\ln x$                   </td><td> <code>log(3)</code>     </td></tr><tr><td> $f(x,y)=x^y$                   </td><td> <code>pow(2, 3)</code>  </td></tr><tr><td> $f(x)=\sqrt x$   根号          </td><td> <code>sqrt(2)</code>    </td></tr><tr><td> $f(x)=\lceil x\rceil$   上取整 </td><td> <code>ceil(2.1)</code>  </td></tr><tr><td> $f(x)=\lfloor x\rfloor$ 下取整 </td><td> <code>floor(2.1)</code> </td></tr><tr><td> $f(x)=\left&lt;x\right&gt;$          </td><td> <code>round(2.1)</code> </td></tr></tbody></table><p><strong>注意事项</strong></p><p>由于浮点误差，有些的数学函数的行为可能与预期不符，导致 WA。如果你的操作数都是整型，那么用下面的写法会更稳妥。</p><blockquote><p>原文地址：https://codeforces.com/blog/entry/107717</p></blockquote>
<ul><li>$\lfloor\frac{a}{b}\rfloor$
<ul><li>别用：<code>floor(1.0 * a / b)</code></li><li>要用：<code>a / b</code></li></ul></li><li>$\lceil\frac{a}{b}\rceil$
<ul><li>别用：<code>ceil(1.0 * a / b)</code></li><li>要用：<code>(a + b - 1) / b</code>  （$\lceil\frac{a}{b}\rceil=\lfloor\frac{a+b-1}{b}\rfloor$）</li></ul></li><li>$\lfloor\sqrt a\rfloor$
<ul><li>别用：<code>(int) sqrt(a)</code></li><li>要用：二分查找 <a href="https://io.zouht.com/7.html">https://io.zouht.com/7.html</a></li></ul></li><li>$a^b$
<ul><li>别用：<code>pow(a, b)</code></li><li>要用：快速幂 <a href="https://io.zouht.com/18.html">https://io.zouht.com/18.html</a></li></ul></li><li>$\lfloor\log<em>2 a\rfloor$
<ul><li>别用：<code>log2(a)</code></li><li>要用：<code>__lg</code> （不规范，但是这是竞赛）/ `bit</li></ul></em>width`（C++20 可用）</li></ul><h2 id="48-gcd--lcm">4.8 <code>gcd()</code> / <code>lcm()</code></h2><p>（C++17）返回最大公因数 / 最小公倍数</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">int x = gcd(8, 12); // 4
int y = lcm(8, 12); // 24
</code></pre>
<p>如果不是 C++17，但是是 GNU 编译器（g++），那么可以用内置函数 <code>__gcd()</code>.</p><p>当然，<code>gcd</code> / <code>lcm</code> 函数也挺好写，直接写也行（欧几里得算法）：</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">int gcd(int a, int b)
{
    if (!b)
        return a;
    return gcd(b, a % b);
}

int lcm(int a, int b)
{
    return a / gcd(a, b) * b;
}
</code></pre>
<h1 id="stl">STL题目练习</h1><ul><li>字符相关补充 
<ul><li>cin.getline(buf, n, &#x27;@&#x27;);  遇到‘@’或者读满 n-1 结束</li><li>getchar(); 去换行</li><li>getline(cin, s, &#x27;@&#x27;)遇到‘@’停止</li></ul></li></ul><h2 id="51-vector">5.1 Vector</h2><h3 id="-p1047httpswwwluogucomcnproblemp1047">洛谷 <a href="https://www.luogu.com.cn/problem/P1047">P1047</a></h3><p>用  bool 向量记录每个位置的状态</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">vector&lt;bool&gt; arr(l + 1,false);
for(int a = 0;a &lt; m;a++){
    int i,j;
    cin &gt;&gt; i &gt;&gt; j;
    for(int b = i;b &lt;= j;b++) arr[b] = true;
}
</code></pre>
<h2 id="52-stackqueue">5.2 Stack/Queue</h2><h3 id="-p1739httpswwwluogucomcnproblemp1739">洛谷 <a href="https://www.luogu.com.cn/problem/P1739">P1739</a>表达式括号匹配</h3><p>检查()左右括号匹配</p><p>遇到左括号就入一次栈 右括号就抵消一次 最后判空</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">stack&lt;bool&gt; st;
for (char ch : s){
    if(ch == &#x27;(&#x27;) st.push(true);
    else if(ch == &#x27;)&#x27;){
        if(st.empty()){
            cout &lt;&lt; &quot;NO&quot;;
            return 0;
        }
        st.pop();//抵消一次
    }
}
if(st.empty()) cout &lt;&lt; &quot;YES&quot;;
else cout &lt;&lt; &quot;NO&quot;;
</code></pre>
<h3 id="-p1449httpswwwluogucomcnproblemp1449">洛谷 <a href="https://www.luogu.com.cn/problem/P1449">P1449</a>后缀表达式</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">stack&lt;int&gt; arr;//栈
string s;
int num = 0;//临时 用来存数
getline(cin, s, &#x27;@&#x27;);
for( char ch : s){
    if(ch == &#x27;.&#x27;){//将数压入栈
        arr.push(num);
        num = 0;
    }
    else if(ch == &#x27;+&#x27;){
        int a = arr.top(); arr.pop();
        int b = arr.top(); arr.pop();
        arr.push(a+b);
    }
    else if(ch == &#x27;-&#x27;){
        int a = arr.top(); arr.pop();
        int b = arr.top(); arr.pop();
        arr.push(b-a);
    }
    else if(ch == &#x27;*&#x27;){
        int a = arr.top(); arr.pop();
        int b = arr.top(); arr.pop();
        arr.push(a*b);
    }
    else if(ch == &#x27;/&#x27;){
        int a = arr.top(); arr.pop();
        int b = arr.top(); arr.pop();
        arr.push(b/a);
    }
    else num = num * 10 + (ch - &#x27;0&#x27;);//叠数字
}
cout &lt;&lt; arr.top();//栈顶即是答案
</code></pre>
<h3 id="-p1996httpswwwluogucomcnproblemp1996-">洛谷 <a href="https://www.luogu.com.cn/problem/P1996">P1996</a> 约瑟夫问题</h3><p>一个圆圈点到人出局 使用队列解决问题 每次安排 m-1 个人出列到队尾 第 m 个人报数出列</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">for(int i = 1;i &lt;= n;i++) arr.push(i);
while(!arr.empty()){
    for(int i = 1;i &lt; m;i++){
        int x = arr.front();
        arr.pop();
        arr.push(x);
    }
    cout &lt;&lt; arr.front();
    arr.pop();
    if(!arr.empty()) cout &lt;&lt; &quot; &quot;;
}
</code></pre>
<h2 id="53-priorityqueue">5.3 Priority_queue</h2><h3 id="-p1090httpswwwluogucomcnproblemp1090-">洛谷 <a href="https://www.luogu.com.cn/problem/P1090">P1090</a> 合并果子</h3><p>贪心（建哈弗曼树），就是每一次选取最小的两堆果子进行合并</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">priority_queue&lt;int, vector&lt;int&gt; ,greater&lt;int&gt;&gt; arr;
int n,ans = 0;
cin &gt;&gt; n;
for(int i = 1;i &lt;= n;i++){
    int tmp;
    cin &gt;&gt; tmp;
    arr.push(tmp);
}
while(arr.size() &gt; 1){
    int x = arr.top(); arr.pop();
    int y = arr.top(); arr.pop();
    int z = x + y;
    arr.push(z);
    ans += z;
}
cout &lt;&lt; ans;
</code></pre>
<p>可以知道用 priority_queue 逐个 push n 个数，每次 push 是 O(log n)，合计 O(n log n)。</p><p>循环执行 n−1 次，每次包含 2 次 pop 和 1 次 push，堆操作都是 O(log n)，因此总计 O((n−1) log n) ≈ O(n log n)。</p><h3 id="-p6033httpswwwluogucomcnproblemp6033--">洛谷 <a href="https://www.luogu.com.cn/problem/P6033">P6033</a> 合并果子 加强数据版</h3><p>将所有变量开long long可以通过Subtask #3 $n = 10^5$</p><p>但对于$n = 10^7$ 目前插入的效率太低了 只能重新优化算法 我们需要双队列</p><ul><li><code>q1</code>：存放<strong>初始排序好的果子堆大小</strong></li><li><code>q2</code>：存放<strong>新合并出来的堆</strong>（也是递增的，因为每次插入的都是比取出来的更大的）</li></ul><p>每次合并要做的事情：</p><ol start="1"><li>从这两个队列的队首中，<strong>取出两个最小的数</strong>（分别是 <code>x</code> 和 <code>y</code>）分两次取 第一小 第二小</li><li>把 <code>x+y</code> 加到总代价 <code>ans</code> 里 把 <code>x+y</code> 再放回 <code>q2</code>（保持有序）</li></ol><p>但仍要在push的时候优化速度 使用<strong>桶排序</strong></p><p>（本题洛谷卡输入速度 上快读）</p><p><strong>经验积累（ $ 5 <em> 10^5 $ 以上 上同步流 $ 2 </em> 10^6 $ 以上的考虑输入速度）</strong></p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">queue&lt;long long&gt; q1, q2;
long long ans = 0;
const int N = 100001;
vector&lt;int&gt; cnt(N, 0);
int maxA = 0;

#define rd read()
inline long long read()
{
    long long x = 0, y = 1;
    char c = getchar();
    while (c &gt; &#x27;9&#x27; || c &lt; &#x27;0&#x27;)
    {
        if (c == &#x27;-&#x27;)
            y = -1;
        c = getchar();
    }
    while (c &gt;= &#x27;0&#x27; &amp;&amp; c &lt;= &#x27;9&#x27;)
        x = x * 10 + c - &#x27;0&#x27;, c = getchar();
    return x * y;
}

long long getMin() {
    long long x;
    if (q1.empty()) {
        long long x = q2.front(); q2.pop();
        return x;
    }
    if (q2.empty()) {
        long long x = q1.front(); q1.pop();
        return x;
    }
    if (q1.front() &lt; q2.front()) {
        long long x = q1.front(); q1.pop();
        return x;
    } else {
        long long x = q2.front(); q2.pop();
        return x;
    }
    return x;
}

int main() {
    long long n = rd;
    //桶排序
    for (int i = 0; i &lt; n; i++) {
        int x = rd;
        cnt[x]++;
        if (x &gt; maxA) maxA = x;  
    }
    for (int v = 1; v &lt;= maxA; v++) while (cnt[v]--) q1.push(v);

    for (int i = 1; i &lt; n; i++) {
        long long x = getMin();
        long long y = getMin();
        ans += x + y;
        q2.push(x + y);
    }
    cout &lt;&lt; ans &lt;&lt; &quot;\n&quot;;
    return 0;
}
</code></pre>
<h3 id="-p1168httpswwwluogucomcnproblemp1168-">洛谷 <a href="https://www.luogu.com.cn/problem/P1168">P1168</a> 中位数</h3><ul><li>维持动态中位数</li></ul><p>每次排序整个数组复杂度高，对于 <code>N=100000</code> 会超时（O(N² log N)）。</p><p>对于大数据，使用 <strong>优先队列</strong> 维护中位数，左右两个堆</p><ul><li><strong>lefta（大根堆）</strong>：存储较小的一半数据，堆顶是这一半的最大值</li><li><strong>righta（小根堆）</strong>：存储较大的一半数据，堆顶是这一半的最小值</li><li>因为 lefta 始终不少于 righta，所以 <strong>lefta.top() 就是中位数</strong></li><li>对于奇数个元素：lefta.top() 是真正的中位数</li><li>对于偶数个元素：lefta.top() 是较小的那个中位数</li></ul><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">priority_queue&lt;int&gt; lefta; // 大顶堆
priority_queue&lt;int, vector&lt;int&gt;, greater&lt;int&gt;&gt; righta; // 小顶堆
int N;
cin &gt;&gt; N;

for(int i = 0; i &lt; N; i++) {
    int x;
    cin &gt;&gt; x;

    if(lefta.empty() || x &lt;= lefta.top()) lefta.push(x);
    else righta.push(x);

    // 保持 lefta 堆大小 &gt;= righta 堆大小，且 lefta 堆最多比 righta 多 1 个
    if(lefta.size() &lt; righta.size()) {
        lefta.push(righta.top());
        righta.pop();
    } else if(lefta.size() &gt; righta.size() + 1) {
        righta.push(lefta.top());
        lefta.pop();
    }

    if(i % 2 == 0) { 
        cout &lt;&lt; lefta.top() &lt;&lt; endl;
    }
}
</code></pre>
<h3 id="-p3871httpswwwluogucomcnproblemp3871-tjoi2010-">洛谷 <a href="https://www.luogu.com.cn/problem/P3871">P3871</a> [TJOI2010] 中位数</h3><p>原理与上方相同 但增加了实时添加 实时调取这些特性</p><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">priority_queue&lt;int&gt; lefta; //大顶堆
priority_queue&lt;int ,vector&lt;int&gt;,greater&lt;int&gt;&gt; righta; //小顶堆bug1

void insert(int x){
    if(lefta.empty() || x &lt;= lefta.top())  lefta.push(x); //bug2
    else righta.push(x);

    if(righta.size() &gt; lefta.size()){
        lefta.push(righta.top());
        righta.pop();
    }
    else if(righta.size() + 1 &lt; lefta.size()){//bug3
        righta.push(lefta.top());
        lefta.pop();
    }
}
int main(){
    cin &gt;&gt; N;
    for(int i = 0;i &lt; N;i++){
        int x; cin &gt;&gt; x;
        insert(x);
    }
    cin &gt;&gt; M;
    while(M--){
        cin &gt;&gt; s;
        if(s == &quot;add&quot;){
            int x; cin &gt;&gt; x;
            insert(x);
        }
        else cout &lt;&lt; lefta.top() &lt;&lt; endl;
    }
    return 0;
}
</code></pre>
<h2 id="54-set--multiset">5.4 Set / Multiset</h2><h3 id="-p2085httpswwwluogucomcnproblemp2085-">洛谷 <a href="https://www.luogu.com.cn/problem/P2085">P2085</a> 最小函数值</h3><h3 id="-p1160httpswwwluogucomcnproblemp1160-">洛谷 <a href="https://www.luogu.com.cn/problem/P1160">P1160</a> 队列安排</h3><h2 id="55-map--unorderedmap">5.5 map / unordered_map</h2><p>P1309 瑞士轮</p><p>P5318 查找文献</p><p>P1830 轰炸</p><h2 id="56-string">5.6 <strong>String</strong></h2><p>P1308 统计单词数</p><p>P1914 小书童——密码</p><p>P3375 【模板】KMP算法</p></div><p style="text-align:right"><a href="https://gwy.fun/posts/ACM/algorithm-strong-foundation-p3stl#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/ACM/algorithm-strong-foundation-p3stl</link><guid isPermaLink="true">https://gwy.fun/posts/ACM/algorithm-strong-foundation-p3stl</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Sun, 05 Oct 2025 13:43:27 GMT</pubDate></item><item><title><![CDATA[基础算法-二分]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/ACM/binary-search-algorithm-foundation">https://gwy.fun/posts/ACM/binary-search-algorithm-foundation</a></blockquote><div><h2 id="update">二分查找的再梳理Update</h2><p>分清两种情况 男左女右</p><ul><li>$ [mid,r]$  $l = mid,r = mid - 1$; mid = (l + r + 1) &gt;&gt; 1;</li><li>$[l,mid]$  $r = mid,l = mid + 1$; mid = (l + r) &gt;&gt; 1;</li></ul><pre class="language-c++ lang-c++"><code class="language-c++ lang-c++">//找有序数组的起始位置
vector&lt;int&gt; a(n + 1);
for(int i = 0;i &lt; n;i++) cin &gt;&gt; a[i];
while(q--){
    int x;cin &gt;&gt; x;
    int l = 0,r = n - 1;
    while(l &lt; r){
        int mid = (l + r) &gt;&gt; 1;
        if(a[mid] &gt;= x) r = mid;
        else l = mid + 1;
    }
    if(a[l] != x) cout &lt;&lt; &quot;-1 -1&quot; &lt;&lt; endl;
    else{
        cout &lt;&lt; l &lt;&lt; &quot; &quot;;
        l = 0,r = n - 1;
        while(l &lt; r){
            int mid = (l + r + 1) &gt;&gt; 1;
            if(a[mid] &lt;= x) l = mid;
            else r = mid - 1;
        }
        cout &lt;&lt; l &lt;&lt; endl;
    }
}
</code></pre>
<h2 id="">二分查找算法的应用范围</h2><ul><li>在<strong>有序数组</strong>中进行查找一个数（二分下标）</li></ul><blockquote><p>如果数据存放在链表中，访问一个元素我们都得通过遍历，有遍历的功夫我们早就找到了这个元素，因此，在链表中不适合使用二分查找。</p></blockquote>
<p><strong>其中「有序」这个条件可以放宽，半有序数组或者是山脉数组里都可以应用二分查找算法。</strong></p><p>旋转和山脉数组的值都有规律可循，元素的值不是随机出现的，在这个特点下，「减治思想」就可以应用在旋转数组和山脉数组里的一些问题上。我们可以把这两类数组统一归纳为部分有序数组</p>
<p>这种二分法用于查找一个有范围的数，也被称为「二分答案」，或者「二分结果」，也就是在「答案区间」里或者是「结果区间」里逐渐缩小目标元素的范围；</p><h2 id="">二分查找算法的两种思路</h2><p>思路 1：在循环体中查找元素
思路 2：在循环体中排除目标元素一定不存在的区间</p><h2 id="">二分查找的时间复杂度</h2><blockquote><p><strong>大 O 不在乎常数差距，只在乎增长趋势</strong>。</p><p>故 O(2⋅logn)=O(logn)</p></blockquote>
<p>推导<strong>$\frac{n}{2^{k}}=1$</strong></p><p><strong>$k=\log_{2}{n} $</strong></p><h2 id="-">二分查找例用-二分下标</h2><h3 id="1-">1基础-搜索元素位置</h3><pre class=""><code class="">while(left &lt;= right){
    int mid = left + (right - left)/2;
    if (nums[mid] == target){
        return mid;
    }
    else if (nums[mid] &gt; target){
        right = mid - 1;
    }
    else{
        left = mid + 1;
    }
}
</code></pre>
<h3 id="2--">2基础-搜索元素位置 递归写法</h3><pre class=""><code class="">public static int search(int[] nums, int target) {
    return binarySearch(nums, target, 0, nums.length - 1);
}

private static int binarySearch(int[] nums, int target, int left, int right) {
    if (left &gt; right) {
        return -1;
    }

    int mid = left + (right - left) / 2;

    if (nums[mid] == target) {
        return mid;
    } else if (nums[mid] &gt; target) {
        return binarySearch(nums, target, left, mid - 1);
    } else {
        return binarySearch(nums, target, mid + 1, right);
    }
}
</code></pre>
<h3 id="3-httpsleetcodecnproblemsfind-first-and-last-position-of-element-in-sorted-array">3基础-<a href="https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/">在有序数组中查找元素的第一个和最后一个位置</a></h3><table><thead><tr><th> 方法           </th><th> 思路                       </th><th> 时间复杂度 </th><th> 是否符合题目要求 </th></tr></thead><tbody><tr><td> <strong>线性扩展法</strong> </td><td> 找到一个目标后向左右扩展   </td><td> O(n)       </td><td> 不符合           </td></tr><tr><td> <strong>二分两次法</strong> </td><td> 分别用二分找左边界和右边界 </td><td> O(log n)   </td><td> 符合             </td></tr></tbody></table><pre class=""><code class="">public int[] searchRange(int[] nums, int target) {
    int left = find(nums, target, true);  // 左
    int right = find(nums, target, false); // 右
    return new int[]{left, right};
}
private int find(int[] nums,int target,boolean isLeft){
    int right = nums.length - 1,left = 0;
    int bound = -1;
    while(left &lt;= right){
        int mid = left + (right - left)/2;
        if(nums[mid] == target){
            bound = mid;
            if(isLeft){
                right = mid - 1;
            }
            else{
                left = mid + 1;
            }
        }
        else if(nums[mid] &gt; target){
            right = mid - 1;
        }
        else{
            left = mid + 1;
        }
    }
    return bound;
}
</code></pre>
<h3 id="4-httpsleetcodecnproblemsfind-minimum-in-rotated-sorted-array">4旋转数组-无重复<a href="https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/">寻找最小值</a></h3><h4 id="">边界处理</h4><p> 情况一：<code>nums[mid] &gt; nums[right]</code></p><p>说明 <strong>最小值一定在右边</strong>（mid 不可能是最小值）。
因为 <code>nums[mid]</code> 比 <code>nums[right]</code> 大，说明从 <code>mid+1</code> 到 <code>right</code> 这段区间里一定有更小的值。</p><p>所以我们可以放心地写：</p><pre class=""><code class="">left = mid + 1;
</code></pre>
<p>情况二：<code>nums[mid] &lt; nums[right]</code></p><p>说明 <strong>最小值在左边</strong>，而且 <code>mid</code> 本身可能就是最小值。</p><p>所以不能丢掉 <code>mid</code>，只能写：</p><pre class=""><code class="">right = mid;
</code></pre>
<p>保留 <code>mid</code> 继续判断。</p><p>  当 <code>left == right</code> 时，就是最小值的下标。</p><p>-&gt; 绕路 mid左右加和 不影响复杂度 但只需要判断 <strong>哪一边有序</strong>，或者 <strong>最小值在哪一边</strong>，就能决定收缩方向</p><pre class=""><code class="">public int findMin(int[] nums) {
     int left = 0;
     int right = nums.length - 1;

     while(left &lt; right){
         int mid = left + (right - left)/2;
         if(nums[mid] &gt; nums[right]){
            left = mid + 1;
         } else{
            right = mid;
         }
     }
     return nums[right];
}
</code></pre>
<h4 id="-iihttpsleetcodecnproblemsfind-minimum-in-rotated-sorted-array-ii">旋转数组有重复<a href="https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array-ii/">最小值 II</a></h4><pre class=""><code class="">while(left &lt; right){
     int mid = left + (right - left)/2;
     if(nums[mid] &gt; nums[right]){
        left = mid + 1;
     } else if(nums[mid] &lt; nums[right]){
        right = mid;
     } else{
        right--;
     }
 }
 return nums[right];
</code></pre>
<h3 id="5httpsleetcodecnproblemssearch-in-rotated-sorted-array">5搜索旋转数组<a href="https://leetcode.cn/problems/search-in-rotated-sorted-array/">无重复</a></h3><h4 id="-">法一 先找最小值，将数组拆分</h4><pre class=""><code class="">public int search(int[] nums, int target) {
    int mainIndex = searchMin(nums);

    int leftIndex = getBound(nums,target,0,mainIndex - 1);//左面
    int rightIndex = getBound(nums,target,mainIndex,nums.length - 1);//右面

    if (leftIndex != rightIndex) return Math.max(leftIndex,rightIndex);
    else return -1;

}
private int getBound(int[] nums,int target,int left,int right){
    while(left &lt;= right){
        int mid = left + (right - left)/2;
        if(nums[mid] == target){
            return mid;
        }
        else if(nums[mid] &gt; target){
            right = mid - 1;
        }
        else{
            left = mid + 1;
        }
    }
    return -1;
}
private int searchMin(int[] nums){
    int left = 0;
    int right = nums.length - 1;

    while(left &lt; right){
        int mid = left + (right - left)/2;

        if (nums[mid] &lt; nums[right]){
            right = mid;
        }
        else if (nums[mid] &gt; nums[right]){
            left = mid + 1;
        }
    }
    return right;
}
</code></pre>
<h4 id="-">法二 一步二分法</h4><p><strong>每次二分时，至少有一半区间是有序的</strong>。</p><ul><li><code>if nums[left] &lt;= nums[mid] → 左边有序</code></li><li>否则右边有序。</li></ul><p><strong>判断 target 是否在有序区间里</strong>。</p><ul><li>如果在，就缩小到有序区间；</li><li>如果不在，就缩小到另一半。</li></ul><pre class=""><code class="">while (left &lt;= right) {
    int mid = left + (right - left) / 2;

    if (nums[mid] == target) return mid;

    // 左边有序
    if (nums[left] &lt;= nums[mid]) {
        if (target &gt;= nums[left] &amp;&amp; target &lt; nums[mid]) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    } 
    // 右边有序
    else {
        if (target &gt; nums[mid] &amp;&amp; target &lt;= nums[right]) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
}
</code></pre>
<h3 id="6-iihttpsleetcodecnproblemssearch-in-rotated-sorted-array-ii">6搜索旋转数组有重复<a href="https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/">数组 II</a></h3><h4 id="-">法一 两步失效</h4><blockquote><p>小云同学还期望改造刚才的无重复数组 但她考虑到时间复杂度在最坏情况下退化为 <strong>O(n)</strong> 故给搜索最小数的时候也加上了 right--;但很不幸无法 AC（280/282）</p></blockquote>
<p>样例是</p><p>nums = [1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1]
target = 2</p><p>她发现在有大量重复元素的情况下（如样例），<code>searchMin</code> 找到的 <strong>旋转点位置不唯一</strong>，从而导致分段二分出错 她终于发现是算法思路限制 无法通过修补通过这个样例</p><h4 id="">法二</h4><pre class=""><code class="">while(left &lt;= right){ //错点 1
    int mid = left + (right - left)/2;

    if(nums[mid] == target) return true;

    // 遇到重复元素无法判断哪边有序
    if(nums[left] == nums[mid] &amp;&amp; nums[mid] == nums[right]){
        left++;
        right--;
    }
    // 左半边有序
    else if(nums[mid] &gt;= nums[left]){
        if(target &gt;= nums[left] &amp;&amp; target &lt; nums[mid]){
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    // 右半边有序
    else {
        if(target &gt; nums[mid] &amp;&amp; target &lt;= nums[right]){
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
}
</code></pre>
<h3 id="7">7山脉数组找峰顶索引</h3><pre class=""><code class="">while(left &lt; right){
    int mid = left + (right - left)/2;
    if(arr[mid] &lt; arr[mid+1]){
        left = mid + 1;
    }
    else{
        right = mid;
    }
}
</code></pre>
<h3 id="8">8山脉数组中查找目标值</h3><p>使用相对熟悉的多步二分直接 AC 但请 GPT 大人纠正了一些错误代码习惯 摘录在这里</p><ol start="1"><li><strong>减少重复调用 <code>mountainArr.get(mid)</code></strong></li></ol><p>在 <code>find</code> 和 <code>getmax</code> 中，每次判断都调用了两次 <code>mountainArr.get(mid)</code> 或 <code>mountainArr.get(mid+1)</code>。</p><ol start="2"><li><strong>二分查找时使用 <code>left &lt;= right</code> 而不是 <code>left &lt; right</code></strong></li></ol><p>原来的 <code>while(left &lt; right)</code> 循环会在循环结束后再单独判断 <code>mountainArr.get(left)</code>，加上上面的优化可以直接写成标准二分查找形式 <code>left &lt;= right</code>，逻辑更干净。</p><pre class=""><code class="">private int getPeak(MountainArray mountainArr){
    int left = 0, right = mountainArr.length() - 1;
    while(left &lt; right){
        int mid = left + (right - left)/2;
        int midVal = mountainArr.get(mid);
        int midNextVal = mountainArr.get(mid + 1);
        if(midVal &lt; midNextVal) left = mid + 1;
        else right = mid;
    }
    return left;
}

private int binarySearch(MountainArray mountainArr, int target, int left, int right, boolean increasing){
    while(left &lt;= right){
        int mid = left + (right - left)/2;
        int midVal = mountainArr.get(mid);
        if(midVal == target) return mid;
        if(increasing){
            if(midVal &lt; target) left = mid + 1;
            else right = mid - 1;
        } else {
            if(midVal &gt; target) left = mid + 1;
            else right = mid - 1;
        }
    }
    return -1;
}
</code></pre>
<p>但通过刚才经验了解不是很熟练一次二分 故写了一下 有些小问题记录一下</p><p><code>int m2 = mountainArr.get(mid+1);</code> 可能越界</p><ul><li>当 <code>mid == mountainArr.length() - 1</code> 时，<code>mid+1</code> 会越界。</li><li>在取 <code>m2</code> 前先判断 <code>mid &lt; length - 1</code>，否则不要去访问 <code>mid+1</code>。</li></ul><p>但仍不能 AC 对于 <strong>样例点 输入 [0,5,1] 0 输出 -1</strong> 有问题</p><p><strong>这才意识到这种做法无法保证返回最小下标 故放弃</strong></p><pre class=""><code class="">public int findInMountainArray(int target, MountainArray mountainArr) {
        int left = 0;
        int right = mountainArr.length() - 1;
        while(left &lt;= right){
            int mid = left + (right - left)/2;
            int m1 = mountainArr.get(mid);

            if(m1 == target) return mid;

            if(mid &lt; mountainArr.length() - 1){ //越界检查
                int m2 = mountainArr.get(mid + 1);
                if(m1 &lt; m2) { //上升区间,m1上升 
                    if(m1 &lt; target) left = mid + 1; //m1小于目标值 去掉左边
                    else right = mid - 1;
                }
                else{ //下降区间
                    if(m1 &lt; target) right = mid - 1;
                    else left = mid + 1;
                }   
            }
            else right = mid - 1; // mid 已经在数组最后，直接往左缩
        }
        return -1;
    }
</code></pre>
<h3 id="9bosshttpsleetcodecnproblemsmedian-of-two-sorted-arrays">9小结Boss<a href="https://leetcode.cn/problems/median-of-two-sorted-arrays/">寻找两个正序数组的中位数</a></h3><p><a href="https://leetcode.cn/problems/median-of-two-sorted-arrays/solutions/258842/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/">参考题解</a></p><h4 id="">法一：划分法</h4><p>给定两个正序数组 <code>nums1</code> 和 <code>nums2</code>，求它们合并后的中位数，时间复杂度 O(log(m+n)) 暗示我们不能真的合并数组，而要用「二分法」。</p><h5 id="step1"><strong>Step1思考</strong></h5><p>中位数把一个有序数组分成两部分：</p><ol start="1"><li><strong>左边部分的元素全部 ≤ 右边部分的元素</strong></li><li><strong>左边部分和右边部分长度相等（或左边多 1 个）</strong></li></ol><p>如果我们能把两个数组合并后的「左边」和「右边」拆开，就能直接得到中位数。</p><p>我们希望 <strong>left<em>A + left</em>B</strong> 和 <strong>right<em>A + right</em>B</strong>：</p><ul><li>左边所有元素 ≤ 右边所有元素</li><li>左边长度 = 右边长度（或左边多 1）</li></ul><p>记：</p><pre class=""><code class="">i + j = (m + n + 1) / 2
</code></pre>
<p>这个公式保证左右两边长度关系正确（总长度奇偶都通用）</p><p>然后要满足：</p><pre class=""><code class="">A[i-1] ≤ B[j]   // A左最大 ≤ B右最小
B[j-1] ≤ A[i]   // B左最大 ≤ A右最小
</code></pre>
<h5 id="step2-i"><strong>Step2二分法找 i</strong></h5><p>为了避免处理太多边界情况，我们规定 <strong>A 是短数组</strong>（m ≤ n）。</p><ul><li>在 <code>[0, m]</code> 范围里二分查找 i</li><li>通过指针 left right 算出 i</li><li>计算 j = (m + n + 1)/2 - i （有一种向上取整的意味）</li><li>检查：
<ul><li>如果 <code>A[i-1] &gt; B[j]</code> → i 太大，向左移动</li><li>如果 <code>B[j-1] &gt; A[i]</code> → i 太小，向右移动</li></ul></li><li>找到满足条件的 i，即划分完成</li></ul><p><strong>注意边界处理</strong>：</p><ul><li>i = 0 → left_A 为空 → A[i-1] = -∞</li><li>i = m → right_A 为空 → A[i] = ∞</li><li>j = 0 或 j = n 同理</li></ul><p>另在 Java 中：</p><ul><li><strong>正无穷</strong> → <code>Integer.MAX_VALUE</code></li><li><strong>负无穷</strong> → <code>Integer.MIN_VALUE</code></li></ul><p>在C++中</p><ul><li><p>正无穷 → <code>INT_MAX</code>（<code>&lt;climits&gt;</code> 中定义）</p></li><li><p>负无穷 → <code>INT_MIN</code></p></li></ul><h5 id="step3"><strong>Step3计算中位数</strong></h5><p>划分完成后：</p><pre class=""><code class="">max_of_left = max(A[i-1], B[j-1])
min_of_right = min(A[i], B[j])
</code></pre>
<ul><li>如果总长度是奇数 → 中位数 = max<em>of</em>left</li><li>如果总长度是偶数 → 中位数 = (max<em>of</em>left + min<em>of</em>right) / 2</li></ul><pre class="language-java lang-java"><code class="language-java lang-java">public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    if (nums1.length &gt; nums2.length) { //保证nums1是短数组
        return findMedianSortedArrays(nums2, nums1);
    }
    int m = nums1.length;
    int n = nums2.length;
    int left = 0,right = m;//表示可选择的空位

    while(left &lt;= right){
        //i,j 表示切点
        int i = left + (right - left)/2;
        int j = (m + n + 1)/2 - i;

        int A_left = (i == 0) ? Integer.MIN_VALUE : nums1[i-1];// 左面没有元素
        int A_right = (i == m) ? Integer.MAX_VALUE : nums1[i]; // 右面没有元素
        int B_left = (j == 0) ? Integer.MIN_VALUE : nums2[j-1];
        int B_right = (j == n) ? Integer.MAX_VALUE : nums2[j];

        if( A_left &lt;= B_right &amp;&amp; B_left &lt;= A_right){ //划分正确
            if ((m + n) % 2 == 0) {
                return (Math.max(A_left, B_left) + Math.min(A_right, B_right)) / 2.0;
            } else {
                return Math.max(A_left, B_left);
            }
        }
        if(A_left &gt; B_right){ //A左最大 &gt; B右最小 i太大 向左移
            right = i - 1; //通过i来影响right
        }
        if(B_left &gt; A_right){// B左最大 &gt; A右最小 i小 向右移
            left = i + 1;
        } 
    }
    return -1;
}
</code></pre>
<h4 id="-">法二 直接二分</h4><p>数学难度较大 写了一个还没 AC 的代码 比较头晕 后清清脑子再补 <a href="https://leetcode.cn/problems/median-of-two-sorted-arrays/solutions/258842/xun-zhao-liang-ge-you-xu-shu-zu-de-zhong-wei-s-114/">先贴一个题解</a></p><h2 id="-">二分查找例用-二分答案</h2><h3 id="">特征</h3><p>答案空间可枚举 + 存在单调性函数 + 能通过函数值判断往左还是往右。</p><ol start="1"><li><strong>答案在一个范围内，范围可枚举（通常是整数区间）</strong></li><li><strong>可以定义一个单调性函数 f(x)</strong>，用来判断当前 <code>x</code> 是否「可行」或「过大/过小」。
<ul><li>在2题：
<ul><li>定义 <code>count = # {nums[i] ≤ mid}</code>。</li><li>若 <code>count &gt; mid</code> → 说明重复数一定在左半边。</li><li>单调性：随着 mid 增大，<code>count</code> 只会变大或相等。</li></ul></li><li>在3题：
<ul><li>定义 <code>f(x) = sum(min(a, x))</code>。</li><li>单调性：随着 x 增大，f(x) 只会变大或相等。</li></ul></li></ul></li><li><strong>目标是找到某个最优点/最接近点</strong>，而不是直接找到某个精确位置。
<ul><li>在2题：我们最终要精确锁定唯一的重复数。</li><li>在3题：我们要让 <code>f(x)</code> 最接近 <code>target</code>，可能会停在两个候选解之间 → 需要比较。</li></ul></li></ol><h3 id="1">1计算<strong>算术平方根</strong></h3><pre class="language-java lang-java"><code class="language-java lang-java">public int mySqrt(int x) {
    if (x == 0) {
        return 0;
    }
    int left = 1;
    int right = x / 2;

    while (left &lt; right){
        int mid = (left + right + 1) / 2; //向上取整

        if (mid &gt; x / mid) {
            // 大于等于mid的数一定不是解，下一轮搜索的区间为 [left, mid - 1]
            right = mid - 1;
        } else {
            left = mid;
        }
    }
    return left;
}
</code></pre>
<h3 id="2">2寻找重复数</h3><h4 id="">二分法</h4><p>  -&gt; 在 <strong>值的范围</strong> 上做二分，而不是在数组下标上</p><pre class="language-java lang-java"><code class="language-java lang-java">public int findDuplicate(int[] nums) {
    int left = 1, right = nums.length - 1; // 搜索的值域范围 [1, n]
    while (left &lt; right) {
        int mid = left + (right - left) / 2;
        int count = 0;
        // 统计 &lt;= mid 的元素个数
        for (int i = 0; i &lt; nums.length; i++) {
            if (nums[i] &lt;= mid) count++;
        }
        if (count &gt; mid) {
            // 重复数在 [left, mid]
            right = mid;
        } else {
            // 重复数在 [mid+1, right]
            left = mid + 1;
        }
    }
    return left;
}
</code></pre>
<p>在没有重复的情况下：
 在区间 <code>[1, mid]</code> 里，最多应该有 <code>mid</code> 个不同的数。
 所以 <code>count ≤ mid</code>。</p><p>如果 <code>count &gt; mid</code>：
 说明在 <code>[1, mid]</code> 这个区间里，元素的数量超过了它本应能容纳的数量，这区间里一定有重复数。
 → 因此我们缩小搜索区间到 <code>[left, mid]</code>。</p><p>如果 <code>count ≤ mid</code>：
 说明 <code>[1, mid]</code> 内没有超过容量，重复数只能出现在 <code>[mid+1, right]</code>。</p><pre class="language-java lang-java"><code class="language-java lang-java">public int findDuplicate(int[] nums) {
    int left = 1, right = nums.length - 1; // 搜索的值域范围 [1, n]
    while (left &lt; right) {
        int mid = left + (right - left) / 2;
        int count = 0;
        // 统计 &lt;= mid 的元素个数
        for (int i = 0; i &lt; nums.length; i++) {
            if (nums[i] &lt;= mid) count++;
        }
        if (count &gt; mid) {
            // 重复数在 [left, mid]
            right = mid;
        } else {
            // 重复数在 [mid+1, right]
            left = mid + 1;
        }
    }
    return left;
}
</code></pre>
<h4 id="-">链表思路-快慢指针</h4><ul><li>见链表章节(后续更新)</li></ul><h3 id="3">3转变数组后最接近目标值的数组和</h3><ul><li>第一遍写没有打上调试发现不太对劲 有两个错误 已标注在代码中</li></ul><pre class="language-java lang-java"><code class="language-java lang-java">public int findBestValue(int[] arr, int target) {
    int len = arr.length;
    int left = 0,right = findMax(arr,len);
    while(left &lt; right){
        int mid = left + (right - left)/2;
        int count = js(arr,len,mid);
        System.out.println(mid);
        System.out.println(count);
        if(target &lt;= count) right = mid; 
        //错点一 原为right = mid - 1在「最接近目标」的题里，我们不是要严格排除 mid，而是要保留它作为候选值。
        else left = mid + 1;
    }
    int c1 = Math.abs(js(arr,len,left) - target);
    int c2 = (left &gt; 0) ? Math.abs(js(arr, len, left - 1) - target) : Integer.MAX_VALUE;
    //错点二 int c2 = Math.abs(js(arr,len,left - 1) - target); left-1可能越界
    if(c1 == c2) return Math.min(left,left - 1);
    else if(c1 &gt; c2) return left - 1;
    else return left;
}
private int js(int[] nums,int len,int mid){
    int count = 0;
    for(int i = 0;i &lt; len;i++){
        count += Math.min(nums[i],mid);
    }
    return count;
}
private int findMax(int[] nums,int len){
    int max = 0;
    for(int i = 0;i &lt; len;i++){
        max = Math.max(max, nums[i]);
    }
    return max;
}
</code></pre></div><p style="text-align:right"><a href="https://gwy.fun/posts/ACM/binary-search-algorithm-foundation#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/ACM/binary-search-algorithm-foundation</link><guid isPermaLink="true">https://gwy.fun/posts/ACM/binary-search-algorithm-foundation</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Wed, 10 Sep 2025 13:49:25 GMT</pubDate></item><item><title><![CDATA[基础算法-总览]]></title><description><![CDATA[<link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/1646367420-ilEeUZ-image.png"/><link rel="preload" as="image" href="https://www.gwy.fun/blog_ima/blog/wechat_2025-08-23_154619_176.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/ACM/algorithm-foundation-p1-introduction">https://gwy.fun/posts/ACM/algorithm-foundation-p1-introduction</a></blockquote><div><p>::: warning
<em>本文持续更新中～</em>
:::</p><h1 id="">数据结构</h1><p>在解决问题的时候，我们通常不是一下子把数据处理完，更多的时候需要先把它们放在一个容器里，等到一定的时刻再把它们拿出来。使用「数据结构」是一种「空间换时间」思想的体现，</p><table><thead><tr><th> 数据结构 </th><th> 应用场景 </th></tr></thead><tbody><tr><td> 栈 </td><td> 符合「后进先出」的规律 </td></tr><tr><td> 队列 </td><td> 符合「先进先出」的规律 </td></tr><tr><td> 哈希表（散列表）</td><td> 实现「快速存取」数据的功能 </td></tr><tr><td> 二分搜索树（红黑树、B 树系列）</td><td> 维护了一组数据的顺序性，得到一个数据的上下界 </td></tr><tr><td> 并查集    </td><td> 用于处理不相交集合的「动态」连接问题 </td></tr><tr><td> 优先队列 </td><td> 有「动态」添加、删除数据且需要获得最值的场景 </td></tr><tr><td> 字典树（Trie）</td><td> 用于保存和统计大量的字符串和相关的信息 </td></tr><tr><td> 线段树 </td><td> 处理数组的区间信息的汇总（求和、最值等）、单点更新、区间更新问题 </td></tr><tr><td> 树状数组 </td><td> 处理数组的前缀和、单点更新、区间更新问题 </td></tr></tbody></table><h1 id="">时间复杂度与空间复杂度</h1><p>g(N) 关于规模 N 的表达式</p><p>O(g(N)) 表达考虑最坏情况</p><p>时间复杂度的计算（估算）只有在输入规模特别大的时候才有意义。「输入规模特别大」往往是我们初学的时候不可感知的。大 𝑂表示法是一种事前估计法，得到一个函数表达式，它表示了：随着输入规模的扩大，程序的执行消耗会扩大的程度。这个程度不是直接从表达式上直接看出来的，需要在一个动态增加的过程中去理解程序的执行消耗会扩大的程度。</p><p><img src="https://www.gwy.fun/blog_ima/blog/1646367420-ilEeUZ-image.png" alt="t1" height="562" width="2128"/></p><p>$ y=\log_{10}{n}$ 这个函数的图形随着 𝑛的成倍增长，纵轴数值的增长较缓慢（这已经是一种很好的时间复杂度了），到了 100000这个数量级，函数值也才到 5，「二分查找」就具有这样的时间复杂度：随着输入数据规模的增加，函数的执行次数增长较缓慢</p>
<p>$ y=1$ 的函数图像表示了程序的运行时间与输入数据的规模无关；</p>
<p>$ y=\frac{1}{10000} n^{2} $  随着的成倍增长，函数值也成比例地增长，这样的时间复杂度是性能相对较差的，这样的算法常常是「朴素解法」和「暴力解法」</p><p>在 50000 以后  $ y=\frac{1}{10000}  n^{2} $  有着更高算法复杂度。</p>
<p><img src="https://www.gwy.fun/blog_ima/blog/wechat_2025-08-23_154619_176.png" alt="t2"/></p><p>参考学习：力扣</p><h1 id="">其他知识补盲</h1><h2 id="">三元运算符</h2><pre class=""><code class="">条件表达式 ? 表达式1 : 表达式2
</code></pre>
<p>意思是：</p><ul><li>如果 <strong>条件表达式为真</strong>（即非 0），结果就是 <strong>表达式1</strong>；</li><li>否则，结果就是 <strong>表达式2</strong>。</li></ul><p>它相当于一个简写版的 <code>if...else...</code>。</p><h2 id="printf-"><code>printf</code> 控制小数输出</h2><pre class=""><code class="">printf(&quot;%.nf&quot;, x);
</code></pre>
<ul><li><code>%f</code>：输出浮点数（float / double）</li><li><code>%.n</code>：表示保留 <strong>n 位小数</strong>，会<strong>四舍五入</strong></li><li>输出宽度（可选）：<code>%m.nf</code> → 总宽度 m，小数 n</li></ul><pre class=""><code class="">double x = 3.14159;
printf(&quot;%.2f\n&quot;, x);  // 输出：3.14
printf(&quot;%.3f\n&quot;, x);  // 输出：3.142
printf(&quot;%8.2f\n&quot;, x); // 输出：   3.14 （总宽8位，右对齐）
</code></pre>
<ul><li>只显示指定的小数位（四舍五入）</li><li>不自动切换科学计数法</li><li>精确控制格式，非常适合输出表格或文件格式数据</li></ul><hr/><h2 id="cout-"><code>cout</code> 控制小数输出</h2><pre class=""><code class="">#include &lt;iomanip&gt;
</code></pre>
<table><thead><tr><th> 控制符            </th><th> 作用                                                       </th></tr></thead><tbody><tr><td> <code>setprecision(n)</code> </td><td> 设置<strong>有效数字</strong>个数（默认）或<strong>小数位数</strong>（配合 <code>fixed</code>） </td></tr><tr><td> <code>fixed</code>           </td><td> 改变 <code>setprecision</code> 的含义：表示<strong>小数点后位数</strong>           </td></tr><tr><td> <code>scientific</code>      </td><td> 科学计数法输出                                             </td></tr><tr><td> <code>showpoint</code>       </td><td> 强制显示小数点与小数                                       </td></tr><tr><td> <code>setw(n)</code>         </td><td> 设置最小输出宽度                                           </td></tr><tr><td> <code>setfill(c)</code>      </td><td> 设置填充字符                                               </td></tr></tbody></table><hr/><h3 id="-1">例 1：默认行为（有效数字）</h3><pre class=""><code class="">double x = 3.14159;
cout &lt;&lt; setprecision(3) &lt;&lt; x;  // 输出：3.14（有效数字3位）
</code></pre>
<p>解释：默认模式下，<code>setprecision</code> 控制<strong>有效数字</strong>，而不是小数点后几位。</p><hr/><h3 id="-2-fixed">例 2：保留小数位数（配合 fixed）</h3><pre class=""><code class="">cout &lt;&lt; fixed &lt;&lt; setprecision(3) &lt;&lt; x;  // 输出：3.142
</code></pre>
<p>解释：<code>fixed</code> 改变了 <code>setprecision</code> 的含义，使它控制“小数点后几位”。</p><hr/><h3 id="-3">例 3：科学计数法</h3><pre class=""><code class="">cout &lt;&lt; scientific &lt;&lt; setprecision(2) &lt;&lt; x;
// 输出：3.14e+00
</code></pre>
<hr/><h3 id="-4">例 4：组合使用</h3><pre class=""><code class="">cout &lt;&lt; setw(8) &lt;&lt; setfill(&#x27;*&#x27;) &lt;&lt; fixed &lt;&lt; setprecision(2) &lt;&lt; x;
// 输出：***3.14
</code></pre>
<hr/><h3 id="printf-vs-cout">比较：<code>printf</code> vs. <code>cout</code></h3><table><thead><tr><th> 功能         </th><th> <code>printf</code> </th><th> <code>cout</code>                          </th></tr></thead><tbody><tr><td> 输出小数位   </td><td> <code>%.2f</code>   </td><td> <code>fixed &lt;&lt; setprecision(2)</code>      </td></tr><tr><td> 输出有效数字 </td><td> <code>%.2g</code>   </td><td> <code>setprecision(2)</code>（默认）       </td></tr><tr><td> 科学计数法   </td><td> <code>%.2e</code>   </td><td> <code>scientific &lt;&lt; setprecision(2)</code> </td></tr><tr><td> 显示小数点   </td><td> <code>%#.f</code>   </td><td> <code>showpoint</code>                     </td></tr><tr><td> 输出宽度     </td><td> <code>%8.2f</code>  </td><td> <code>setw(8)</code>                       </td></tr></tbody></table><table><thead><tr><th> 需求                  </th><th> printf 写法           </th><th> cout 写法                                           </th></tr></thead><tbody><tr><td> 保留 2 位小数         </td><td> <code>printf(&quot;%.2f&quot;, x);</code>  </td><td> <code>cout &lt;&lt; fixed &lt;&lt; setprecision(2) &lt;&lt; x;</code>            </td></tr><tr><td> 输出 3 位有效数字     </td><td> <code>printf(&quot;%.3g&quot;, x);</code>  </td><td> <code>cout &lt;&lt; setprecision(3) &lt;&lt; x;</code>                     </td></tr><tr><td> 科学计数法            </td><td> <code>printf(&quot;%.3e&quot;, x);</code>  </td><td> <code>cout &lt;&lt; scientific &lt;&lt; setprecision(3) &lt;&lt; x;</code>       </td></tr><tr><td> 输出宽度 8，小数 2 位 </td><td> <code>printf(&quot;%8.2f&quot;, x);</code> </td><td> <code>cout &lt;&lt; setw(8) &lt;&lt; fixed &lt;&lt; setprecision(2) &lt;&lt; x;</code> </td></tr></tbody></table><h2 id="acm">ACM数据类型选择</h2><h3 id="">一、整型类型选择</h3><table><thead><tr><th> 类型                 </th><th> 字节数（C++标准） </th><th> 取值范围                             </th><th> 常用场景                             </th></tr></thead><tbody><tr><td> <code>int</code>                </td><td> 4字节             </td><td> ±2,147,483,647 ≈ ±2×10⁹              </td><td> 普通下标、计数器、循环变量           </td></tr><tr><td> <code>long long</code>          </td><td> 8字节             </td><td> ±9,223,372,036,854,775,807 ≈ ±9×10¹⁸ </td><td> 大整数范围计算（如10¹², 10¹⁵）       </td></tr><tr><td> <code>unsigned long long</code> </td><td> 8字节             </td><td> 0 ~ 1.8×10¹⁹                         </td><td> 非负大数、位运算、哈希、组合数模运算 </td></tr></tbody></table><table><thead><tr><th> 数据范围 </th><th> 推荐类型    </th><th> 举例                       </th></tr></thead><tbody><tr><td> ≤10⁹     </td><td> <code>int</code>       </td><td> n ≤ 10⁸，数组大小、索引    </td></tr><tr><td> ≤10¹²    </td><td> <code>long long</code> </td><td> n² 或和的累计，∑a[i]       </td></tr><tr><td> ≤10¹⁸    </td><td> <code>long long</code> </td><td> 涉及乘法、阶乘近似、模运算 </td></tr><tr><td> &gt;10¹⁸    </td><td> <code>__int128</code>  </td><td> 大整数乘除、斐波那契巨大项 </td></tr></tbody></table><hr/><h4 id="">注意乘法溢出</h4><p>如果题中：</p><ul><li><code>a</code>、<code>b</code> 都 ≤ 10⁹</li><li>但出现 <code>a * b</code>
 那么 <code>a * b</code> 可能达 10¹⁸，要 <strong>提前转 long long</strong>。</li></ul><h4 id="">大整数取模</h4><p>使用unsigned long long刚好多那一点就能确保long long数据能被取模了</p><hr/><h3 id="">二、浮点类型选择</h3><table><thead><tr><th> 类型          </th><th> 字节数               </th><th> 有效数字位数   </th><th> 常见误差     </th><th> 常用场景                     </th></tr></thead><tbody><tr><td> <code>float</code>       </td><td> 4字节                </td><td> 约6位有效数字  </td><td> 误差较大     </td><td> 一般不用                     </td></tr><tr><td> <code>double</code>      </td><td> 8字节                </td><td> 约15位有效数字 </td><td> 10⁻¹⁵        </td><td> <strong>主力</strong>                     </td></tr><tr><td> <code>long double</code> </td><td> 16字节（部分编译器） </td><td> 约18~19位      </td><td> 更高精度但慢 </td><td> <strong>高精度几何、概率、积分题</strong> </td></tr></tbody></table><hr/><h4 id=""><strong>浮点类型选取规则</strong></h4><table><thead><tr><th> 精度需求 / 数据范围       </th><th> 推荐类型            </th><th> 举例                     </th></tr></thead><tbody><tr><td> 1e-3 或输出 2~3 位小数    </td><td> <code>double</code>            </td><td> 平均数、几何长度         </td></tr><tr><td> 高精度计算（误差 &lt; 1e-9） </td><td> <code>double</code>            </td><td> All                      </td></tr><tr><td> 极端精度（1e-15 以内）    </td><td> <code>long double</code>       </td><td> 几何交点、积分、牛顿迭代 </td></tr><tr><td> 无需浮点，能整算          </td><td> 用 <code>long long</code> 代替 </td><td> 避免精度误差             </td></tr></tbody></table><hr/><p>浮点比较时不要用 <code>==</code>，而是：</p><pre class=""><code class="">if (fabs(a - b) &lt; 1e-9)
</code></pre>
<hr/><h3 id="">三、实际题目举例</h3><table><thead><tr><th> 题目数据范围                   </th><th> 推荐类型                  </th><th> 说明                           </th></tr></thead><tbody><tr><td> n ≤ 10⁵，aᵢ ≤ 10⁹              </td><td> <code>long long</code>               </td><td> 可能求和到 10¹⁴                </td></tr><tr><td> n ≤ 10⁶，aᵢ ≤ 10⁶              </td><td> <code>int</code>                     </td><td> 总和 10¹² 仍安全在 <code>long long</code> </td></tr><tr><td> 坐标 ≤ 1e6，有平方根或几何距离 </td><td> <code>double</code>                  </td><td> 因为要开根号 sqrt              </td></tr><tr><td> 坐标 ≤ 1e9，要计算角度或面积   </td><td> <code>long double</code>             </td><td> 防止误差积累                   </td></tr><tr><td> 模数 1e9+7, 998244353          </td><td> <code>long long</code>               </td><td> 模乘必须防止溢出               </td></tr><tr><td> 阶乘 / 组合数 / 大数幂         </td><td> <code>long long</code> 或 <code>__int128</code> </td><td> 有乘法放大                     </td></tr></tbody></table><hr/><h3 id="int128-">四、__int128 的应用</h3><p>当题中要求：</p><ul><li>计算 <code>a*b</code>，而 <code>a,b</code> 都可能到 1e18；</li><li>或 <code>(a*b)%mod</code> 中 <code>a*b</code> 可能溢出 <code>long long</code>;</li></ul><p>可以使用：</p><pre class=""><code class="">__int128 mul(__int128 a, __int128 b, long long mod) {
    return (a * b) % mod;
}
</code></pre>
<p>输出时要手动转换：</p><pre class=""><code class="">void print(__int128 x) {
    if (x == 0) return;
    print(x / 10);
    putchar(x % 10 + &#x27;0&#x27;);
}
</code></pre>
<table><thead><tr><th> 场景                                             </th><th> 建议                               </th></tr></thead><tbody><tr><td> 一律使用 <code>long long</code> 存整数，除非确定不会溢出    </td><td> 防止隐藏错误                       </td></tr><tr><td> 所有浮点题默认用 <code>double</code>                        </td><td> 兼顾精度与速度                     </td></tr><tr><td> 输出时明确格式控制                               </td><td> <code>setprecision</code> 或 <code>printf(&quot;%.6f&quot;)</code> </td></tr><tr><td> 出现“平方”、“乘积”、“累积和”关键字时立刻提升精度 </td><td> 经验法则                           </td></tr><tr><td> 模数题（1e9+7, 998244353）                       </td><td> 仍用 <code>long long</code>                   </td></tr><tr><td> 几何题或误差题                                   </td><td> double / long double + eps 比较    </td></tr></tbody></table><h2 id="">读入速度相关</h2><table><thead><tr><th> 数据规模（输入量级） </th><th> 做法                                                         </th><th> 原因               </th></tr></thead><tbody><tr><td> ≤ 1×10⁵              </td><td> 普通 <code>cin &gt;&gt;</code>、<code>cout &lt;&lt;</code> 即可                                </td><td> -                  </td></tr><tr><td> 1×10⁵ ～ 5×10⁵       </td><td> <strong>关闭同步流</strong>（<code>ios::sync_with_stdio(false); cin.tie(nullptr);</code>） </td><td> 提升约 2～3 倍性能 </td></tr><tr><td> 5×10⁵ ～ 2×10⁶       </td><td> <strong>使用 scanf/printf 或快读模板</strong>                             </td><td> <code>cin</code> 可能开始卡   </td></tr><tr><td> ≥ 2×10⁶              </td><td> <strong>必须使用快读</strong>                                             </td><td> TLE                </td></tr></tbody></table><table><thead><tr><th> 题型                   </th><th> 通常输入量 </th><th> 是否要优化 </th></tr></thead><tbody><tr><td> 普通数据结构、数学题   </td><td> ≤ 1e5      </td><td> 否         </td></tr><tr><td> 排序、扫描、模拟       </td><td> 1e5～1e6   </td><td> 关闭同步流 </td></tr><tr><td> 离散化、前缀和、差分   </td><td> 1e6        </td><td> 使用快读   </td></tr><tr><td> 大规模统计、字符串处理 </td><td> ≥ 1e6      </td><td> 必须快读   </td></tr></tbody></table><h3 id="">快读板子</h3><pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#define rd read()
inline long long read()
{
    long long x = 0, y = 1;
    char c = getchar();
    while (c &gt; &#x27;9&#x27; || c &lt; &#x27;0&#x27;)
    {
        if (c == &#x27;-&#x27;)
            y = -1;
        c = getchar();
    }
    while (c &gt;= &#x27;0&#x27; &amp;&amp; c &lt;= &#x27;9&#x27;)
        x = x * 10 + c - &#x27;0&#x27;, c = getchar();
    return x * y;
}

//使用long long n = rd;
</code></pre>
<h2 id="">字符处理相关</h2><h3 id="">字符与整数的相互转换</h3><table><thead><tr><th> 表达式                </th><th> 含义                                 </th><th> 示例                                </th></tr></thead><tbody><tr><td> <code>&#x27;a&#x27;</code>                 </td><td> 字符常量（ASCII 值为 97）            </td><td> <code>&#x27;a&#x27; + 1</code> → <code>&#x27;b&#x27;</code>                   </td></tr><tr><td> <code>s[i] - &#x27;a&#x27;</code>          </td><td> 把小写字母映射到 0～25               </td><td> <code>&#x27;a&#x27; - &#x27;a&#x27; = 0</code>, <code>&#x27;z&#x27; - &#x27;a&#x27; = 25</code>   </td></tr><tr><td> <code>(unsigned char)s[i]</code> </td><td> 把字符安全地转为无符号整数，防止负值 </td><td> 常用于 <code>cnt[(unsigned char)s[i]]++</code> </td></tr><tr><td> <code>(int)s[i]</code>           </td><td> 查看字符对应的整数（ASCII）          </td><td> <code>&#x27;A&#x27; → 65</code>, <code>&#x27;a&#x27; → 97</code>              </td></tr></tbody></table><pre class=""><code class="">string s = &quot;abc&quot;;
int num[26] = {0};
for (char c : s)
    num[c - &#x27;a&#x27;]++; // 统计字母出现次数
int cnt[256] = {0};
for (unsigned char c : s)
    cnt[c]++; // 统计所有 ASCII 字符出现次数
</code></pre>
<hr/><h3 id="">字符串输入相关语法</h3><table><thead><tr><th> 用法                 </th><th> 功能                                 </th><th> 特点                                    </th></tr></thead><tbody><tr><td> <code>cin &gt;&gt; s;</code>          </td><td> 输入一个字符串（自动跳过空格、换行） </td><td> 读到空格或换行就停                      </td></tr><tr><td> <code>getline(cin, s);</code>   </td><td> 输入整行（包括空格）                 </td><td> 读到换行符才停                          </td></tr><tr><td> <code>cin.getline(c, n);</code> </td><td> 读取一整行到 C 风格字符数组中        </td><td> <code>c</code> 是 <code>char[]</code>，会自动加上 <code>&#x27;\0&#x27;</code> 结尾 </td></tr><tr><td> <code>getchar();</code>         </td><td> 读取一个字符                         </td><td> 常用于“吃掉”换行符或分隔符              </td></tr><tr><td> <code>cin.get();</code>         </td><td> 同样读取一个字符（可读空格）         </td><td> 返回类型是 <code>int</code>（EOF 安全）            </td></tr></tbody></table><hr/><h3 id="-cin--getline-">注意 cin 和 getline 混用问题</h3><p>当用过 <code>cin &gt;&gt; n;</code> 之后，缓冲区里 <strong>还残留一个换行符 <code>&#x27;\n&#x27;</code></strong>。
 如果立刻用 <code>getline(cin, s)</code>，那它会把这个换行符当作空行。</p><p>所以需要：</p><pre class=""><code class="">cin &gt;&gt; n;
getchar(); // 或 cin.ignore();
getline(cin, s); // 现在才能正确读入整行
</code></pre>
<h3 id="c-scanf--printf">C 风格输入输出（scanf / printf）</h3><table><thead><tr><th> 函数                    </th><th> 功能                     </th><th> 特点               </th></tr></thead><tbody><tr><td> <code>scanf(&quot;%d&quot;, &amp;n);</code>      </td><td> 输入一个整数             </td><td> 快，但格式严格     </td></tr><tr><td> <code>scanf(&quot;%s&quot;, str);</code>     </td><td> 读取一个不含空格的字符串 </td><td> 自动加 <code>\0</code>        </td></tr><tr><td> <code>scanf(&quot;%[^\n]&quot;, str);</code> </td><td> 读取整行直到换行         </td><td> 注意需要清理缓冲区 </td></tr><tr><td> <code>printf(&quot;%d&quot;, n);</code>      </td><td> 输出整数                 </td><td> 格式控制灵活       </td></tr><tr><td> <code>getchar()</code>             </td><td> 从标准输入读取一个字符   </td><td> 常用于吸收换行符   </td></tr></tbody></table><hr/><h3 id="">常见字符串处理操作</h3><table><thead><tr><th> 操作     </th><th> 语法                       </th><th> 含义                                  </th></tr></thead><tbody><tr><td> 获取长度 </td><td> <code>s.size()</code> 或 <code>s.length()</code> </td><td> 返回字符个数                          </td></tr><tr><td> 访问字符 </td><td> <code>s[i]</code>                     </td><td> 第 i 个字符（0 起）                   </td></tr><tr><td> 拼接     </td><td> <code>s += t;</code>                  </td><td> 拼接字符串                            </td></tr><tr><td> 截取     </td><td> <code>s.substr(pos, len)</code>       </td><td> 从 <code>pos</code> 开始取 <code>len</code> 个字符          </td></tr><tr><td> 查找     </td><td> <code>s.find(&quot;abc&quot;)</code>            </td><td> 找到返回位置，否则返回 <code>string::npos</code> </td></tr><tr><td> 转数字   </td><td> <code>stoi(s)</code>                  </td><td> 字符串 → 整数                         </td></tr><tr><td> 转字符串 </td><td> <code>to_string(x)</code>             </td><td> 整数 → 字符串                         </td></tr></tbody></table><table><thead><tr><th> 类别        </th><th> 示例                         </th><th> 说明                </th></tr></thead><tbody><tr><td> 字符统计    </td><td> <code>cnt[(unsigned char)s[i]]++</code> </td><td> 防止负下标          </td></tr><tr><td> 字符映射    </td><td> <code>num[s[i] - &#x27;a&#x27;]</code>            </td><td> 字母映射到数组索引  </td></tr><tr><td> 输入整行    </td><td> <code>getline(cin, s)</code>            </td><td> 包含空格            </td></tr><tr><td> 吸收换行    </td><td> <code>getchar()</code> / <code>cin.ignore()</code> </td><td> 避免 getline 读空行 </td></tr><tr><td> C风格行读入 </td><td> <code>cin.getline(c, n+1)</code>        </td><td> 填充 <code>char[]</code>       </td></tr><tr><td> 判断类型    </td><td> <code>isalpha</code>, <code>isdigit</code>, …      </td><td> 判断字符类型        </td></tr><tr><td> 转换        </td><td> <code>toupper</code>, <code>tolower</code>         </td><td> 大小写转换          </td></tr></tbody></table></div><p style="text-align:right"><a href="https://gwy.fun/posts/ACM/algorithm-foundation-p1-introduction#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/ACM/algorithm-foundation-p1-introduction</link><guid isPermaLink="true">https://gwy.fun/posts/ACM/algorithm-foundation-p1-introduction</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Tue, 02 Sep 2025 08:58:43 GMT</pubDate></item><item><title><![CDATA[学习日志·一个优秀的 Git 工作流]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://gwy.fun/posts/building/learning-log-git-workflow">https://gwy.fun/posts/building/learning-log-git-workflow</a></blockquote><div><p>三个仓库 Remote  Local Disk 将本地仓库想象成两个部分</p><h2 id="">流程</h2><h3 id="">正常步骤</h3><p><code>git clone</code> 三仓库统一</p><p><code>git checkout -b my-feature</code> 创建 feature branch 而不是往 main push code</p><p>“硬盘 Disk 不在意是源文件来自哪个 branch”</p><p><code>git diff</code> 查看暂存区与disk区文件的差异</p><p><code>git add &lt;chanssged_file&gt;</code></p><p><code>git commit</code> 将暂存区内容添加到local区的当前分支中</p><p><code>git push origin my-feature</code> 提交</p><h3 id="">另外情况</h3><p><code>git checkout main</code> 不是修改状态</p><p><code>git fetch origin</code> 拉取远端最新分支</p><p><code>git rebase main</code> 这一步会让 Git 把在 my-feature 上的提交“搬到”origin/main 最新提交之后</p><blockquote><p>假设当前分支与 main分支存在共同部分common，该指令用 main分支包括common在内的整体替换当前分支的common部分（原先xxx分支内容为common-&gt;diversityA，当前分支内容为common-&gt;diversityB，执行完该指令后当前分支内容为common-&gt;diversityA-&gt;diversityB）。</p></blockquote>
<p>然后在 Github create pull request -&gt;Squash and merge</p><p>删除远端 branch</p><p>删除本地 先切换<code>git checkout main</code> 再 <code>git branch -D my-feature</code></p><p><code>git pull origin master</code></p><h2 id="">实际项目情况</h2><p>main：生产环境，也就是你们在网上可以下载到的版本，是经过了很多轮测试得到的稳定版本。</p><p>release：开发内部发版，也就是测试环境。</p><p>dev：所有的feature都要从dev上checkout。</p><p>feature：每个需求新创建的分支。</p><p>1.从dev分支上checkout -b new-feature，进行开发</p><p>2.开发完后，经过自测没问题了，准备发版</p><p>3.merge到release分支上进行发版，并打tag</p><p>4.有bug就直接在release上进行修改，改完再次发版，打tag，直到测试人员验证完毕</p><p>5.这时可以将release分支合并到dev上，也可以删除掉feature分支了，并等待通知是否将此功能上线（在正式服务器上运行），如果上线，那就merge到main（master）分支。</p><ul><li>参考 <a href="https://www.bilibili.com/video/BV19e4y1q7JJ">https://www.bilibili.com/video/BV19e4y1q7JJ</a></li></ul></div><p style="text-align:right"><a href="https://gwy.fun/posts/building/learning-log-git-workflow#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://gwy.fun/posts/building/learning-log-git-workflow</link><guid isPermaLink="true">https://gwy.fun/posts/building/learning-log-git-workflow</guid><dc:creator><![CDATA[Guoweiyi]]></dc:creator><pubDate>Sat, 23 Aug 2025 02:32:28 GMT</pubDate></item></channel></rss>