<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>st1vdy&apos;s blog</title><description>No description</description><link>https://fuwari.vercel.app/</link><language>zh_CN</language><item><title>模板元编程2：Typename与类型萃取 - 以迭代器为例</title><link>https://fuwari.vercel.app/posts/cpp_templates2_typename_and_type_traits/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/cpp_templates2_typename_and_type_traits/</guid><description>通过实现一个双向链表迭代器，理解 typename 关键字与类型萃取的设计哲学</description><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文编译环境：clang-21，C++23标准。&lt;/p&gt;
&lt;h1&gt;迭代器&lt;/h1&gt;
&lt;p&gt;迭代器（&lt;code&gt;iterator&lt;/code&gt;）是 STL 的核心抽象之一：它将“如何遍历一个容器”与“容器本身的结构”解耦，让算法可以不关心底层数据结构。&lt;/p&gt;
&lt;p&gt;用过 STL 的话，你一定见过类似这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector vec = {1, 2, 3, 4, 5};
for (std::vector&amp;lt;int&amp;gt;::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::print(&quot;{} &quot;, *it);
}
std::println();

std::map&amp;lt;std::string, int&amp;gt; mp = {{&quot;a&quot;, 1}, {&quot;b&quot;, 2}};
for (std::map&amp;lt;std::string, int&amp;gt;::iterator it = mp.begin(); it != mp.end(); ++it) {
    std::print(&quot;{}:{} &quot;, it-&amp;gt;first, it-&amp;gt;second);
}
std::println();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;vec.begin()&lt;/code&gt; 和 &lt;code&gt;mp.begin()&lt;/code&gt; 返回的都是迭代器，但两者的底层结构截然不同——&lt;code&gt;vector&lt;/code&gt; 是连续内存，&lt;code&gt;map&lt;/code&gt; 是红黑树。迭代器将这种差异隐藏起来，对外统一提供 &lt;code&gt;*&lt;/code&gt;、&lt;code&gt;-&amp;gt;&lt;/code&gt;、&lt;code&gt;++&lt;/code&gt; 这套接口。&lt;/p&gt;
&lt;p&gt;我们平时写的范围 for 循环，本质上也是迭代器的语法糖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for (auto&amp;amp; x : Container)  // 等价于用 begin()/end() 迭代
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不同容器的迭代器，&lt;strong&gt;能力各不相同&lt;/strong&gt;，本章将具体解释迭代器的分类与内部实现。&lt;/p&gt;
&lt;h2&gt;五种迭代器类型&lt;/h2&gt;
&lt;p&gt;C++ 标准库将迭代器按能力分为五个等级，构成一个继承层次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input_iterator_tag / output_iterator_tag
forward_iterator_tag        （继承自 input）
bidirectional_iterator_tag  （继承自 forward）
random_access_iterator_tag  （继承自 bidirectional）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个层次反映的是&quot;能做什么&quot;：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;代表容器&lt;/th&gt;
&lt;th&gt;支持操作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt; / &lt;code&gt;output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;流迭代器（比如输入输出流）&lt;/td&gt;
&lt;td&gt;单次单向遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;forward&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::forward_list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;多次单向遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bidirectional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;双向遍历 (&lt;code&gt;++&lt;/code&gt; / &lt;code&gt;--&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;random_access&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;std::vector&lt;/code&gt;、原生指针&lt;/td&gt;
&lt;td&gt;随机访问（&lt;code&gt;+n&lt;/code&gt;、&lt;code&gt;-n&lt;/code&gt;、&lt;code&gt;[]&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;正如继承关系所示，每一级迭代器的约束都在逐渐增强：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;input_iterator / output_iterator_tag&lt;/code&gt;&lt;/strong&gt;：常见于输入/输出流。因为底层是流，数据读过即消失，无法回头也无法重复读取，不保证迭代器副本独立有效，因此只支持单次的单向遍历。&lt;code&gt;std::istream_iterator&lt;/code&gt; 是典型代表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;forward_iterator_tag&lt;/code&gt;&lt;/strong&gt;：在输入迭代器基础上增加了多遍保证——迭代器可以被复制，两个副本独立前进互不影响，同一位置可以反复解引用，但是只支持向前遍历（&lt;code&gt;++&lt;/code&gt; 操作）。比如单向链表 &lt;code&gt;std::forward_list&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bidirectional_iterator_tag&lt;/code&gt;&lt;/strong&gt;：在前向迭代器基础上增加了 &lt;code&gt;--&lt;/code&gt; 操作，支持向后移动。&lt;code&gt;std::list&lt;/code&gt;、&lt;code&gt;std::map&lt;/code&gt;、&lt;code&gt;std::set&lt;/code&gt; 等的迭代器属于这级，双向链表和红黑树都能在节点间双向跳转，但一次跳n步只能一步步来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;random_access_iterator_tag&lt;/code&gt;&lt;/strong&gt;：在双向迭代器基础上增加了 $O(1)$ 的任意跳跃（即随机访问能力），支持 &lt;code&gt;it + n&lt;/code&gt;、&lt;code&gt;it - n&lt;/code&gt;、&lt;code&gt;it1 - it2&lt;/code&gt; 和 &lt;code&gt;it[n]&lt;/code&gt;，以及迭代器之间的大小比较 &lt;code&gt;&amp;lt;&lt;/code&gt;、&lt;code&gt;&amp;gt;&lt;/code&gt;。&lt;code&gt;std::vector&lt;/code&gt;、&lt;code&gt;std::deque&lt;/code&gt; 的迭代器属于这级。&lt;code&gt;std::sort&lt;/code&gt; 等算法要求这级，因为内部需要 $O(1)$ 的分治跳跃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;contiguous_iterator_tag&lt;/code&gt;&lt;/strong&gt;（C++17 概念引入，C++20 正式有标签）：在随机访问基础上额外保证内存物理连续，&lt;code&gt;std::vector&lt;/code&gt;、&lt;code&gt;std::array&lt;/code&gt; 和裸数组满足这级，&lt;code&gt;std::deque&lt;/code&gt; 虽然支持随机访问但内存不完全连续，因此不满足。这个tag主要在拷贝时有效，如果物理内存完全连续，那么就是直接将 &lt;code&gt;std::copy&lt;/code&gt; 优化为一次 &lt;code&gt;memcpy&lt;/code&gt;，但是某些支持随机访问但不完全连续的容器就不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些 tag 不仅是“标记”，在后面我们会看到，它们也是编译期分发的关键。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;std::iterator&lt;/code&gt; 的五个关联类型&lt;/h2&gt;
&lt;p&gt;一个合规的迭代器需要向外暴露五个类型信息，STL 算法依赖这些信息工作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型名&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;value_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;迭代器指向的元素类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pointer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的指针类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reference&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;元素的引用类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;difference_type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;两个迭代器之间距离的类型（通常是 &lt;code&gt;ptrdiff_t&lt;/code&gt;）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iterator_category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;迭代器类别 tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;看不懂没关系，继续往下看实例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;实现一个自定义迭代器&lt;/h2&gt;
&lt;p&gt;这一节的目标是实现一个带哨兵节点的双向链表 &lt;code&gt;my_list&amp;lt;T&amp;gt;&lt;/code&gt;，并为它配套实现一个符合规范的自定义迭代器。显然，对于一个自己手动实现的双向链表，不能直接通过 &lt;code&gt;std::iterator&lt;/code&gt; 进行遍历，因此我们需要实现一个配套的自定义迭代器。&lt;/p&gt;
&lt;p&gt;双向链表实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template &amp;lt;typename T&amp;gt;
class my_list {
private:
    struct node {
        T data = 0;
        node *prev = nullptr;
        node *next = nullptr;
    };
    node* _head;
    node* _tail;

public:
    my_list() : _head(new node()), _tail(new node()) {
        _head-&amp;gt;next = _tail;
        _tail-&amp;gt;prev = _head;
    }
    
    void push_back(const T &amp;amp;elem) {
        node *new_node = new node{elem, nullptr, _tail};
        new_node-&amp;gt;prev = _tail-&amp;gt;prev;
        _tail-&amp;gt;prev-&amp;gt;next = new_node;
        _tail-&amp;gt;prev = new_node;
    }

    void push_front(const T &amp;amp;elem) {
        node *new_node = new node{elem, _head, nullptr};
        new_node-&amp;gt;next = _head-&amp;gt;next;
        _head-&amp;gt;next-&amp;gt;prev = new_node;
        _head-&amp;gt;next = new_node;
    }

    void print() {
        auto current = _head-&amp;gt;next;
        while (current != _tail) {
            std::print(&quot;{} &quot;, current-&amp;gt;data);
            current = current-&amp;gt;next;
        }
        std::println();
    }

    ~my_list() {
        auto current = _head-&amp;gt;next;
        delete _head;
        while (current != _tail) {
            _head = current;
            current = current-&amp;gt;next;
            delete _head;
        }
        delete _tail;
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;使用哨兵节点（dummy head / tail）的好处是：&lt;code&gt;begin()&lt;/code&gt; 返回 &lt;code&gt;_head-&amp;gt;next&lt;/code&gt;，&lt;code&gt;end()&lt;/code&gt; 返回 &lt;code&gt;_tail&lt;/code&gt;，边界条件统一，无需特判。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;迭代器的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct iterator {
    using value_type        = T;
    using pointer           = T*;
    using reference         = T&amp;amp;;
    using difference_type   = std::ptrdiff_t;
    using iterator_category = std::bidirectional_iterator_tag;  // 双向，不是随机访问

    node* ptr;

    reference operator*()  const { return ptr-&amp;gt;data; }
    pointer   operator-&amp;gt;() const { return &amp;amp;ptr-&amp;gt;data; }

    bool operator==(const iterator&amp;amp; o) const { return ptr == o.ptr; }
    bool operator!=(const iterator&amp;amp; o) const { return ptr != o.ptr; }

    // ++ 的语义是跳到 next 节点，而不是地址 +1
    iterator&amp;amp; operator++() { ptr = ptr-&amp;gt;next; return *this; }
    iterator  operator++(int) { auto tmp = *this; ptr = ptr-&amp;gt;next; return tmp; }

    // 双向迭代器特有：往回走
    iterator&amp;amp; operator--() { ptr = ptr-&amp;gt;prev; return *this; }
    iterator  operator--(int) { auto tmp = *this; ptr = ptr-&amp;gt;prev; return tmp; }

    // 没有 +n、-n、[] - 链表做不到随机访问
};

iterator begin() { return {_head-&amp;gt;next}; }
iterator end()   { return {_tail}; }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关联类型&lt;/h3&gt;
&lt;p&gt;对于一个迭代器而言，我们需要首先指定它的五个关联类型，对于这个双向链表的迭代器，它的五个关联信息分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;value_type = T&lt;/code&gt;&lt;/strong&gt;：迭代器所指向的元素类型，即链表存储的数据类型。解引用 &lt;code&gt;*it&lt;/code&gt; 返回的就是这个类型的值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pointer = T*&lt;/code&gt;&lt;/strong&gt;：元素的指针类型，即 &lt;code&gt;value_type*&lt;/code&gt;。&lt;code&gt;operator-&amp;gt;()&lt;/code&gt; 的返回类型就是它，用于支持 &lt;code&gt;it-&amp;gt;member&lt;/code&gt; 这样的访问语法。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reference = T&amp;amp;&lt;/code&gt;&lt;/strong&gt;：元素的引用类型，即 &lt;code&gt;value_type&amp;amp;&lt;/code&gt;。&lt;code&gt;operator*()&lt;/code&gt; 的返回类型就是它，表示解引用后得到的是元素本身的引用，而不是拷贝，所以 &lt;code&gt;*it = 42&lt;/code&gt; 可以直接修改链表中的元素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;difference_type = std::ptrdiff_t&lt;/code&gt;&lt;/strong&gt;：表示两个迭代器之间距离的类型。对链表来说虽然没有 &lt;code&gt;it1 - it2&lt;/code&gt; 运算符，但 &lt;code&gt;std::distance&lt;/code&gt; 等算法内部需要用这个类型来计数，所以仍然需要声明。&lt;code&gt;std::ptrdiff_t&lt;/code&gt; 是标准的有符号整数类型，基本上固定用它即可。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;iterator_category = std::bidirectional_iterator_tag&lt;/code&gt;&lt;/strong&gt;：声明这个迭代器属于哪个级别。这是五个类型里最关键的一个，STL 算法在编译期通过它做分发——标记为双向迭代器后，&lt;code&gt;std::sort&lt;/code&gt; 等要求随机访问的算法会在编译期直接报错，&lt;code&gt;std::reverse&lt;/code&gt; 等只需要双向迭代器的算法则可以正常使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;必要运算符&lt;/h3&gt;
&lt;p&gt;以下是实现一个双向迭代器所必须的部件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引用与解引用：&lt;code&gt;reference&lt;/code&gt; 和 &lt;code&gt;pointer&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;比较运算符：&lt;code&gt;==&lt;/code&gt; 和 &lt;code&gt;!=&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;前进（前向迭代）：&lt;code&gt;++iter&lt;/code&gt; 和 &lt;code&gt;iter++&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;后退（反向迭代）：&lt;code&gt;--iter&lt;/code&gt; 和 &lt;code&gt;iter--&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;可以补充的是 &lt;code&gt;const_iterator&lt;/code&gt; 相关的代码，本文暂时不需要所以省略。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;我们可以对于这个双向链表类进行一些简单的测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;ranges&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;print&amp;gt;

template &amp;lt;typename T&amp;gt;
class my_list {
private:
    struct node {
        T data = 0;
        node *prev = nullptr;
        node *next = nullptr;
    };
    node* _head;
    node* _tail;

public:
    my_list() : _head(new node()), _tail(new node()) {
        _head-&amp;gt;next = _tail;
        _tail-&amp;gt;prev = _head;
    }
    void push_back(const T &amp;amp;elem) {
        node *new_node = new node{elem, nullptr, _tail};
        new_node-&amp;gt;prev = _tail-&amp;gt;prev;
        _tail-&amp;gt;prev-&amp;gt;next = new_node;
        _tail-&amp;gt;prev = new_node;
    }

    void push_front(const T &amp;amp;elem) {
        node *new_node = new node{elem, _head, nullptr};
        new_node-&amp;gt;next = _head-&amp;gt;next;
        _head-&amp;gt;next-&amp;gt;prev = new_node;
        _head-&amp;gt;next = new_node;
    }

    void print() {
        auto current = _head-&amp;gt;next;
        while (current != _tail) {
            std::print(&quot;{} &quot;, current-&amp;gt;data);
            current = current-&amp;gt;next;
        }
        std::println();
    }

    ~my_list() {
        auto current = _head-&amp;gt;next;
        delete _head;
        while (current != _tail) {
            _head = current;
            current = current-&amp;gt;next;
            delete _head;
        }
        delete _tail;
    }

    struct iterator {
        using value_type        = T;
        using pointer           = T*;
        using reference         = T&amp;amp;;
        using difference_type   = std::ptrdiff_t;
        using iterator_category = std::bidirectional_iterator_tag;  // 注意：双向，不是随机访问

        node* ptr;

        reference operator*()  const { return ptr-&amp;gt;data; }
        pointer   operator-&amp;gt;() const { return &amp;amp;ptr-&amp;gt;data; }

        bool operator==(const iterator&amp;amp; o) const { return ptr == o.ptr; }
        bool operator!=(const iterator&amp;amp; o) const { return ptr != o.ptr; }

        // ++ 的语义是跳到 next 节点，而不是地址 +1
        iterator&amp;amp; operator++() { ptr = ptr-&amp;gt;next; return *this; }
        iterator  operator++(int) { auto tmp = *this; ptr = ptr-&amp;gt;next; return tmp; }

        // 双向迭代器特有：往回走
        iterator&amp;amp; operator--() { ptr = ptr-&amp;gt;prev; return *this; }
        iterator  operator--(int) { auto tmp = *this; ptr = ptr-&amp;gt;prev; return tmp; }

        // 没有 +n、-n、[]，链表做不到 O(1) 跳跃
    };

    iterator begin() { return {_head-&amp;gt;next}; }
    iterator end()   { return {_tail}; }
};

int main() {
    my_list&amp;lt;int&amp;gt; lst;
    lst.push_back(1);
    lst.push_back(2);
    lst.push_back(3);
    lst.push_front(0);
    lst.push_front(-1);
    lst.print();  // 输出: -1 0 1 2 3 
    
    for (auto iter = std::begin(lst); iter != std::end(lst); ++iter) {  // 输出: -1 0 1 2 3 
        std::print(&quot;{} &quot;, *iter);
    }
    std::println();
    for (auto&amp;amp; iter : lst) {  // 输出: -1 0 1 2 3 
        std::print(&quot;{} &quot;, iter);
    }
    std::println();
    for (auto&amp;amp; iter : std::ranges::reverse_view(lst)) {  // 输出: 3 2 1 0 -1 
        std::print(&quot;{} &quot;, iter);
    }
    std::println();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;如何泛化地对接 STL —— 以 &lt;code&gt;std::advance&lt;/code&gt; 为例&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;std::advance(iter, n)&lt;/code&gt; 的作用是让迭代器前进 $n$ 步。对不同类型的迭代器，最优实现截然不同，我们这里以双向迭代器和随机访问迭代器为例说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;random_access&lt;/strong&gt;：直接 &lt;code&gt;iter += n&lt;/code&gt;，$O(1)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bidirectional&lt;/strong&gt;：由于不连续，只能循环 &lt;code&gt;++iter / --iter&lt;/code&gt;，$O(n)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果我们想根据上述标准库的实现方法，自己实现一个可泛化的 &lt;code&gt;my_advance&lt;/code&gt;，可以用于和 &lt;code&gt;std::advance&lt;/code&gt; 对接，最朴素的想法是运行期 if-else：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 反面教材：运行期判断
template&amp;lt;typename Iter&amp;gt;
void my_advance_bad(Iter&amp;amp; iter, int n) {
    if (/* iter 是 random access？*/) {
        iter += n;
    } else {
        while (n--) ++iter;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这有两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;怎么在运行期判断迭代器类型？&lt;/li&gt;
&lt;li&gt;更根本的问题是：对 &lt;code&gt;bidirectional&lt;/code&gt; 迭代器，&lt;code&gt;iter += n&lt;/code&gt; 根本&lt;strong&gt;不能编译&lt;/strong&gt;——这必须是编译期决策，而不是运行期分支。比如我们在双向链表迭代器中完全没有实现 &lt;code&gt;operator+=&lt;/code&gt;，那么对于该迭代器，&lt;code&gt;iter += n&lt;/code&gt; 这行代码在编译阶段一定会报错（尽管我们可以在运行期确保不会走进该分支，但在编译期，编译器完全无法判断 &lt;code&gt;iter&lt;/code&gt; 的类型）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确的做法是&lt;strong&gt;编译期分发（tag dispatch）&lt;/strong&gt;，而这需要知道迭代器的 &lt;code&gt;iterator_category&lt;/code&gt;。因此，自定义的 &lt;code&gt;iterator&lt;/code&gt; 泛化对接STL的核心问题就是：如何在编译期得到迭代器的类型信息，也就是我们前文中提到过的五个关联类型（对于 &lt;code&gt;std::advance&lt;/code&gt;，最关键的类型信息自然是 &lt;code&gt;iterator_category&lt;/code&gt;，这决定了具体调用的方法）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Typename 关键字&lt;/h1&gt;
&lt;p&gt;由于我们在迭代器中已经用五个 &lt;code&gt;using&lt;/code&gt; 声明了对应的类型信息，现在要在模板中访问迭代器的 category，直觉上写法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Iter&amp;gt;
void my_advance(Iter&amp;amp; iter, int n) {
    Iter::iterator_category tag;  // 编译错误？
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这行代码会编译失败，原因涉及 C++ 模板解析的一个核心规则：&lt;strong&gt;dependent name 问题&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;依赖名称与二义性&lt;/h2&gt;
&lt;p&gt;在模板中，&lt;code&gt;Iter::iterator_category&lt;/code&gt; 是一个&lt;strong&gt;依赖名称&lt;/strong&gt;（dependent name）——它的含义依赖于模板参数 &lt;code&gt;Iter&lt;/code&gt;，在模板实例化之前编译器并不知道 &lt;code&gt;Iter&lt;/code&gt; 是什么。&lt;/p&gt;
&lt;p&gt;问题在于：&lt;code&gt;Iter::iterator_category&lt;/code&gt; 既可以是一个&lt;strong&gt;类型&lt;/strong&gt;，也可以是一个&lt;strong&gt;静态成员变量&lt;/strong&gt;。考虑这两种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct A { using iterator_category = std::bidirectional_iterator_tag; };  // 类型
struct B { static int iterator_category; };                               // 变量
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器在解析模板时，面对 &lt;code&gt;T::something&lt;/code&gt;，默认假设它是&lt;strong&gt;值&lt;/strong&gt;，而不是类型。因此：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Iter::iterator_category* tag;  // 编译器认为 Iter::iterator_category 是值, 这里的 * 会被解析为乘法运算符而非指针
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这自然是错误的。我们需要显式告诉编译器：&lt;code&gt;iterator_category&lt;/code&gt; 是一个类型。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;typename&lt;/code&gt; 的作用&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;typename Iter::iterator_category tag;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加上 &lt;code&gt;typename&lt;/code&gt;，编译器才会正确地将 &lt;code&gt;Iter::iterator_category&lt;/code&gt; 解析为类型名（也就是通过 &lt;code&gt;typename&lt;/code&gt; 关键词告诉编译器，这里的 &lt;code&gt;Iter::iterator_category&lt;/code&gt; 不会是变量，一定是类型）。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;类型萃取（Type Traits）&lt;/h1&gt;
&lt;p&gt;现在我们有了 &lt;code&gt;typename&lt;/code&gt;，可以写出 &lt;code&gt;typename Iter::iterator_category&lt;/code&gt; 来表示迭代器的类型。但还有一个场景没有解决：&lt;strong&gt;原生指针&lt;/strong&gt;。比如 &lt;code&gt;int*&lt;/code&gt; 也是合法的随机访问迭代器（可以 &lt;code&gt;ptr + n&lt;/code&gt;、&lt;code&gt;ptr[i]&lt;/code&gt;），但它没有任何成员类型，&lt;code&gt;int*::iterator_category&lt;/code&gt; 根本不存在。&lt;/p&gt;
&lt;p&gt;这就是 &lt;code&gt;std::iterator_traits&lt;/code&gt; 要解决的问题。&lt;/p&gt;
&lt;h2&gt;类型萃取的设计逻辑 - 以 &lt;code&gt;iterator_traits&lt;/code&gt; 为例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 通用版本：直接转发迭代器自身定义的五个关联类型
template &amp;lt;typename Iter&amp;gt;
struct iterator_traits {
    using value_type        = typename Iter::value_type;
    using pointer           = typename Iter::pointer;
    using reference         = typename Iter::reference;
    using difference_type   = typename Iter::difference_type;
    using iterator_category = typename Iter::iterator_category;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里体现了模板编程的核心设计思路：用一个中间层结构体统一对外暴露类型信息，使得外部代码无需关心不同迭代器的内部实现差异，始终通过 &lt;code&gt;iterator_traits&amp;lt;Iter&amp;gt;::xxx&lt;/code&gt; 这同一种方式访问所需的类型。也就是&lt;strong&gt;设置一个中间层，将类型的差异封装在萃取类内部，对外提供统一的访问接口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;my_list::iterator&lt;/code&gt; 这样规范的自定义迭代器，通用版本的 &lt;code&gt;iterator_traits&lt;/code&gt; 直接生效。&lt;/p&gt;
&lt;h2&gt;对原生指针的偏特化&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// 对 T* 的偏特化：手动填充这些信息
template &amp;lt;typename T&amp;gt;
struct iterator_traits&amp;lt;T*&amp;gt; {
    using value_type        = T;
    using pointer           = T*;
    using reference         = T&amp;amp;;
    using difference_type   = std::ptrdiff_t;
    using iterator_category = std::random_access_iterator_tag;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是类型萃取设计的精华所在：通过&lt;strong&gt;模板偏特化&lt;/strong&gt;，为没有内嵌成员类型的类型（比如原生指针）补充元信息，使其融入统一接口。&lt;/p&gt;
&lt;p&gt;调用方永远只需要写 &lt;code&gt;iterator_traits&amp;lt;Iter&amp;gt;::iterator_category&lt;/code&gt;，无论 &lt;code&gt;Iter&lt;/code&gt; 是自定义迭代器还是 &lt;code&gt;int*&lt;/code&gt;，都能正确得到 category。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;别名模板：告别冗长的 &lt;code&gt;typename&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;经过前面的铺垫，我们写出了正确的类型访问方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typename std::iterator_traits&amp;lt;Iter&amp;gt;::iterator_category
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这行代码每次使用都要写这么长，很繁琐。C++11 引入的&lt;strong&gt;别名模板&lt;/strong&gt;（alias template）可以解决这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename Iter&amp;gt;
using iter_category_t = typename std::iterator_traits&amp;lt;Iter&amp;gt;::iterator_category;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此后直接写 &lt;code&gt;iter_category_t&amp;lt;Iter&amp;gt;&lt;/code&gt; 即可，&lt;code&gt;typename&lt;/code&gt; 被封装进别名定义里，使用侧不再需要重复书写。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;_t&lt;/code&gt; 惯例&lt;/h3&gt;
&lt;p&gt;这种把 &lt;code&gt;typename ...::type&lt;/code&gt; 包装成 &lt;code&gt;..._t&lt;/code&gt; 的写法，从 C++14 起被标准库大量采用。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// C++11 写法
typename std::remove_const&amp;lt;T&amp;gt;::type
typename std::decay&amp;lt;T&amp;gt;::type

// C++14 起的 _t 别名
std::remove_const_t&amp;lt;T&amp;gt;
std::decay_t&amp;lt;T&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 &lt;code&gt;iterator_traits&lt;/code&gt;，标准库本身没有直接提供 &lt;code&gt;_t&lt;/code&gt; 别名，但 C++20 引入了一套更现代的迭代器工具：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::iter_value_t&amp;lt;Iter&amp;gt;       // = iterator_traits&amp;lt;Iter&amp;gt;::value_type
std::iter_reference_t&amp;lt;Iter&amp;gt;   // = iterator_traits&amp;lt;Iter&amp;gt;::reference
std::iter_difference_t&amp;lt;Iter&amp;gt;  // = iterator_traits&amp;lt;Iter&amp;gt;::difference_type
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：C++20 的这套别名底层实现比 &lt;code&gt;iterator_traits&lt;/code&gt; 更复杂，能处理更多边缘情况（如 range 的迭代器），但核心思想是一致的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 C++14/17 环境下，按需自定义 &lt;code&gt;_t&lt;/code&gt; 别名是常见做法，也是标准库的编写惯例。&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;&lt;code&gt;my_advance&lt;/code&gt; 的完整实现&lt;/h1&gt;
&lt;p&gt;有了 &lt;code&gt;iterator_traits&lt;/code&gt;，我们可以实现完整的 &lt;code&gt;my_advance&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 针对 bidirectional 的实现：逐步前进
template&amp;lt;typename Iter&amp;gt;
void my_advance_impl(Iter&amp;amp; iter, int dist, std::bidirectional_iterator_tag) {
    std::println(&quot;bidirectional_iterator, step forward {}.&quot;, dist);
    if (dist &amp;gt; 0) {
        while (dist--) ++iter;
    } else {
        while (dist++) --iter;
    }
}

// 针对 random_access 的实现：O(1) 跳跃
template&amp;lt;typename Iter&amp;gt;
void my_advance_impl(Iter&amp;amp; iter, int dist, std::random_access_iterator_tag) {
    std::println(&quot;random_access_iterator, step forward {}.&quot;, dist);
    iter += dist;
}

// 别名模板
template&amp;lt;typename Iter&amp;gt;
using iter_category_t = typename std::iterator_traits&amp;lt;Iter&amp;gt;::iterator_category;

// 对外接口：通过 iterator_traits 萃取 category，构造 tag 对象传入
template&amp;lt;typename Iter&amp;gt;
void my_advance(Iter&amp;amp; iter, int dist) {
    my_advance_impl(iter, dist, iter_category_t&amp;lt;Iter&amp;gt;());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有几个细节值得注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;typename&lt;/code&gt; 再次出现&lt;/strong&gt;：&lt;code&gt;typename std::iterator_traits&amp;lt;Iter&amp;gt;::iterator_category&lt;/code&gt; —— &lt;code&gt;Iter&lt;/code&gt; 是模板参数，所以 &lt;code&gt;iterator_traits&amp;lt;Iter&amp;gt;::iterator_category&lt;/code&gt; 是 dependent name，必须加 &lt;code&gt;typename&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tag 对象&lt;/strong&gt;：&lt;code&gt;iter_category_t&amp;lt;Iter&amp;gt;()&lt;/code&gt; 构造了一个 tag 类型的临时对象，但这个对象本身没有任何数据，它只是作为&lt;strong&gt;类型信息的载体&lt;/strong&gt;传递给重载函数，让编译器选择正确的 &lt;code&gt;my_advance_impl&lt;/code&gt; 版本。整个分发过程发生在&lt;strong&gt;编译期&lt;/strong&gt;，零运行时开销。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;继承的作用&lt;/strong&gt;：&lt;code&gt;random_access_iterator_tag&lt;/code&gt; 继承自 &lt;code&gt;bidirectional_iterator_tag&lt;/code&gt;。如果我们没有为 &lt;code&gt;bidirectional&lt;/code&gt; 提供特化，编译器会自动匹配到 &lt;code&gt;bidirectional&lt;/code&gt; 的版本（因为 random_access tag 可以隐式转换为 bidirectional tag）。这使得 tag dispatch 天然地支持&quot;能力降级&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证一下效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main() {
    my_list&amp;lt;int&amp;gt; lst;
    lst.push_back(1);
    lst.push_back(2);
    lst.push_back(3);
    lst.push_front(0);
    lst.push_front(-1);
    lst.print();
    auto it2 = std::begin(lst);
    my_advance(it2, 2);
    std::println(&quot;{}&quot;, *it2);   // 1
    my_advance(it2, -1);
    std::println(&quot;{}&quot;, *it2);   // 0

    int* arr = new int[10];
    std::iota(arr, arr + 10, 0);
    auto arr_iter = arr;
    my_advance(arr_iter, 5);
    std::println(&quot;{}&quot;, *arr_iter);  // 5
    my_advance(arr_iter, -2);
    std::println(&quot;{}&quot;, *arr_iter);  // 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;my_list&lt;/code&gt; 的迭代器走了 bidirectional 分支，原生指针走了 random_access 分支，完全符合预期。&lt;/p&gt;
&lt;p&gt;用这一套 &lt;code&gt;iterator_traits&lt;/code&gt; 的设计方案，用户只需要定义好五个关联类型，自定义的迭代器就能够对接所有可用的STL方法了。&lt;/p&gt;
</content:encoded></item><item><title>模板元编程1：全特化与偏特化</title><link>https://fuwari.vercel.app/posts/cpp_templates1/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/cpp_templates1/</guid><description>从一个通用的打印类出发，理解模板全特化与偏特化的语法、匹配规则与使用场景</description><pubDate>Wed, 25 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文编译环境：clang-21，C++23标准。&lt;/p&gt;
&lt;p&gt;模板的核心价值在于&quot;通用性&quot;——一份代码适配多种类型。但通用性不是万能的，某些特定类型需要完全不同的实现。&lt;strong&gt;模板特化&lt;/strong&gt;就是为这些特例&quot;开后门&quot;的机制。&lt;/p&gt;
&lt;h2&gt;全特化&lt;/h2&gt;
&lt;p&gt;假设我们有这样一个 &lt;code&gt;my_printer&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T&amp;gt;
class my_printer {
public:
    void print(const T&amp;amp; obj) {
        std::println(&quot;Default print: {}&quot;, obj);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对大部分基础类型，这个模板能直接工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_printer&amp;lt;float&amp;gt; p1;
p1.print(114.514f);       // Default print: 114.514

my_printer&amp;lt;int&amp;gt; p2;
p2.print(1919810);        // Default print: 1919810

my_printer&amp;lt;std::string&amp;gt; p3;
p3.print(&quot;hello&quot;);        // Default print: hello
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但如果 &lt;code&gt;T&lt;/code&gt; 是 &lt;code&gt;std::vector&amp;lt;int&amp;gt;&lt;/code&gt; 呢？C++23之前，&lt;code&gt;vector&lt;/code&gt; 没有直接支持 &lt;code&gt;{}&lt;/code&gt; 格式化，通用模板无法打印它的内容，需要我们单独处理。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C++23中完全支持以下写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;format&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;print&amp;gt;

int main() {
    std::vector v = {1, 2, 3};
    std::println(&quot;{}&quot;, v);  // C++23 输出：[1, 2, 3]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;针对 &lt;code&gt;vector&amp;lt;int&amp;gt;&lt;/code&gt; 的&lt;strong&gt;全特化&lt;/strong&gt;写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;&amp;gt;
class my_printer&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; {
public:
    void print(const std::vector&amp;lt;int&amp;gt;&amp;amp; v) {
        std::print(&quot;Vector&amp;lt;int&amp;gt; print:&quot;);
        for (const auto&amp;amp; i : v) {
            std::print(&quot; {}&quot;, i);
        }
        std::println();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语法要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;template&amp;lt;&amp;gt;&lt;/code&gt; 表示全特化，尖括号内&lt;strong&gt;没有&lt;/strong&gt;任何模板参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;my_printer&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;&lt;/code&gt; 中的 &lt;code&gt;&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;&lt;/code&gt; 精确指定了这套实现生效的类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;调用效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector vec{114, 514, 1919, 810};
my_printer&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt; p4;
p4.print(vec);  // Vector&amp;lt;int&amp;gt; print: 114 514 1919 810
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器遇到 &lt;code&gt;my_printer&amp;lt;std::vector&amp;lt;int&amp;gt;&amp;gt;&lt;/code&gt; 时，会优先匹配全特化版本，而不是通用模板。&lt;/p&gt;
&lt;h2&gt;偏特化&lt;/h2&gt;
&lt;p&gt;全特化解决了 &lt;code&gt;vector&amp;lt;int&amp;gt;&lt;/code&gt; 的问题，但如果我们想打印 &lt;code&gt;vector&amp;lt;float&amp;gt;&lt;/code&gt;、&lt;code&gt;vector&amp;lt;std::string&amp;gt;&lt;/code&gt;……难道要为每种元素类型都写一份全特化吗？&lt;/p&gt;
&lt;p&gt;更合理的做法是&lt;strong&gt;偏特化&lt;/strong&gt;——对 &lt;code&gt;vector&amp;lt;ElemType&amp;gt;&lt;/code&gt; 这一整类类型统一处理，其中 &lt;code&gt;ElemType&lt;/code&gt; 仍然是未确定的模板参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename ElemType&amp;gt;
class my_printer&amp;lt;std::vector&amp;lt;ElemType&amp;gt;&amp;gt; {
public:
    void print(const std::vector&amp;lt;ElemType&amp;gt;&amp;amp; v) {
        std::print(&quot;Vector print:&quot;);
        for (const auto&amp;amp; i : v) {
            std::print(&quot; {}&quot;, i);
        }
        std::println();
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与全特化的语法区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;template&amp;lt;typename ElemType&amp;gt;&lt;/code&gt; 保留了未确定的参数（全特化是 &lt;code&gt;template&amp;lt;&amp;gt;&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;my_printer&amp;lt;std::vector&amp;lt;ElemType&amp;gt;&amp;gt;&lt;/code&gt; 中的 &lt;code&gt;&amp;lt;std::vector&amp;lt;ElemType&amp;gt;&amp;gt;&lt;/code&gt; 描述的是一个&lt;strong&gt;模式&lt;/strong&gt;，而不是一个精确类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在 &lt;code&gt;my_printer&amp;lt;std::vector&amp;lt;ElemType&amp;gt;&amp;gt;&lt;/code&gt; 对任意 &lt;code&gt;ElemType&lt;/code&gt; 都能工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;std::vector&amp;lt;float&amp;gt; vf{1.1f, 2.2f, 3.3f};
my_printer&amp;lt;std::vector&amp;lt;float&amp;gt;&amp;gt; p5;
p5.print(vf);   // Vector print: 1.1 2.2 3.3

std::vector&amp;lt;std::string&amp;gt; vs{&quot;hello&quot;, &quot;world&quot;};
my_printer&amp;lt;std::vector&amp;lt;std::string&amp;gt;&amp;gt; p6;
p6.print(vs);   // Vector print: hello world
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;偏特化的另一种用途：指针模式&lt;/h3&gt;
&lt;p&gt;偏特化不仅能匹配容器类型，还能匹配类型的结构特征（如&quot;是否是指针&quot;）。以下面的 &lt;code&gt;my_pair&lt;/code&gt; 为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename U&amp;gt;
class my_pair {
public:
    T first;
    U second;
    my_pair(T f, U s) : first(f), second(s) {}
    void show() const {
        std::println(&quot;Default show: ({}, {})&quot;, first, second);
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认情况下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_pair pr1(114, 514);
pr1.show();   // Default show: (114, 514)

int x = 810;
my_pair pr2(1919, &amp;amp;x);
pr2.show();   // Default show: (1919, 0x...)  ← 输出的是指针地址，不是值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pr2&lt;/code&gt; 的第二个参数是指针，通用模板直接打印地址。如果我们希望当第二个参数是指针时自动解引用输出值，可以对 &lt;code&gt;&amp;lt;T, U*&amp;gt;&lt;/code&gt; 做偏特化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;typename T, typename U&amp;gt;
class my_pair&amp;lt;T, U*&amp;gt; {
public:
    T first;
    U* second;
    my_pair(T f, U* s) : first(f), second(s) {}
    void show() const {
        std::println(&quot;&amp;lt;T, U*&amp;gt; show: ({}, {})&quot;, first, *second);  // 解引用
    }
};

int main() {
    my_pair pr2(1919, &amp;amp;x);
    pr2.show();   // &amp;lt;T, U*&amp;gt; show: (1919, 810)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里偏特化的匹配条件是&quot;第二个参数是某种类型的指针&quot;，&lt;code&gt;T&lt;/code&gt; 和 &lt;code&gt;U&lt;/code&gt; 仍然是自由的模板参数——这就是偏特化&quot;只对部分参数施加约束&quot;的含义。&lt;/p&gt;
&lt;h2&gt;全特化 vs 偏特化&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;全特化&lt;/th&gt;
&lt;th&gt;偏特化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;语法&lt;/td&gt;
&lt;td&gt;&lt;code&gt;template&amp;lt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;template&amp;lt;typename ...&amp;gt;&lt;/code&gt; 保留部分参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;匹配方式&lt;/td&gt;
&lt;td&gt;精确匹配特定类型&lt;/td&gt;
&lt;td&gt;匹配符合某种模式的一类类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型用途&lt;/td&gt;
&lt;td&gt;针对单一特殊类型的定制实现&lt;/td&gt;
&lt;td&gt;针对指针、容器、模板类等整类类型&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;当同时存在多个候选时，编译器的优先级是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;全特化&lt;/strong&gt; &amp;gt; &lt;strong&gt;最匹配的偏特化&lt;/strong&gt; &amp;gt; &lt;strong&gt;通用模板&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;全特化的匹配最精确，因此优先级最高；多个偏特化之间选择&quot;最具体&quot;的那个（最窄的模式）；通用模板是兜底选项。&lt;/p&gt;
</content:encoded></item><item><title>Backpropagation</title><link>https://fuwari.vercel.app/posts/backpropagation/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/backpropagation/</guid><description>介绍两层神经网络的梯度推导，从链式法则出发推广至一般全连接网络的反向传播算法，给出前向传播与反向传播的完整迭代公式。</description><pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Backpropagation&lt;/h1&gt;
&lt;p&gt;这是&lt;a href=&quot;https://www.youtube.com/watch?v=OyrqSYJs7NQ&amp;amp;t=1468s&quot;&gt;Lecture 3 (Part I) - &quot;Manual&quot; Neural Networks&lt;/a&gt;和&lt;a href=&quot;https://www.youtube.com/watch?v=JLg1HkzDsKI&quot;&gt;Lecture 3 (Part II) - &quot;Manual&quot; Neural Networks&lt;/a&gt;的笔记。&lt;/p&gt;
&lt;h2&gt;The gradients of a two-layer network&lt;/h2&gt;
&lt;h3&gt;two-layer network&lt;/h3&gt;
&lt;p&gt;在实际场景下，linear hypothesis class无法分类所有情况&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;index.assets/image-20250218150519988.png&quot; alt=&quot;image-20250218150519988&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;index.assets/image-20250218150532311.png&quot; alt=&quot;image-20250218150532311&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因此往往采用MLP，也就是线性网络+非线性网络嵌套。一个最简单的例子就是two-layer network，
$$
\sigma(XW_1)W_2
$$
&lt;img src=&quot;index.assets/image-20250218150658714.png&quot; alt=&quot;image-20250218150658714&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里 $\sigma$ 表示一个对逐个元素的非线性变换，比如常见的ReLU、tanh等。&lt;/p&gt;
&lt;p&gt;$X\in\mathbb R^{m\times n},W_1\in\mathbb R^{n\times d}, W_2\in\mathbb R^{d\times k}$&lt;/p&gt;
&lt;h3&gt;gradients&lt;/h3&gt;
&lt;p&gt;令 $X$ 表示输入是batch matrix form。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;目标：计算 $\nabla_{{W_1,W_2}}\ell_{ce}(\sigma(XW_1)W_2,y)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;$W_2$&lt;/h4&gt;
&lt;p&gt;然后我们还是“将一切视作标量”，直接链式求导，
$$
\begin{aligned}
\frac{\partial \ell_{ce}(\sigma(XW_1)W_2,y)}{\partial W_2} &amp;amp;= \frac{\partial \ell_{ce}(\sigma(XW_1)W_2,y)}{\partial \sigma(XW_1)W_2}\cdot \frac{\partial \sigma(XW_1)W_2}{\partial W_2}\
&amp;amp;= (S - I_y)\cdot \sigma(XW_1)\quad(S=\text{normalize}(\exp(\sigma(XW_1)W_2)))
\end{aligned}
$$
然后调整维度，$(S-I_y)\in\mathbb R^{m\times k},\sigma(XW_1)\in R^{m\times d}$，而我们是对 $W_2\in\mathbb R^{d\times k}$ 求偏导，因此
$$
\nabla_{W_2}\ell_{ce}(\sigma(XW_1)W_2,y) = (\sigma(XW_1))^T\cdot (S-I_y)
$$&lt;/p&gt;
&lt;h4&gt;$W_1$&lt;/h4&gt;
&lt;p&gt;再对 $W_1$ 求偏导，
$$
\begin{aligned}
\frac{\partial \ell_{ce}(\sigma(XW_1)W_2,y)}{\partial W_1} &amp;amp;= \frac{\partial \ell_{ce}(\sigma(XW_1)W_2,y)}{\partial \sigma(XW_1)W_2}\cdot \frac{\partial \sigma(XW_1)W_2}{\partial \sigma(XW_1)}\cdot \frac{\partial \sigma(XW_1)}{\partial XW_1}\cdot \frac{\partial XW_1}{\partial W_1}\
&amp;amp;= (S-I_y)\cdot W_2\cdot \sigma^\prime(XW_1)\cdot X
\end{aligned}
$$
这里 $\sigma$ 是一个标量函数，所以 $\sigma^\prime$ 就是这个标量函数的导数，比如ReLU的导数就是一个分段函数 $\sigma&apos;(x)=0,x\le 0;1,x\gt 0$。&lt;/p&gt;
&lt;p&gt;对于上面这一串连乘，我们列出维度：$(S-I_y)\in\mathbb R^{m\times k}, W_2\in\mathbb R^{d\times k}, \sigma^\prime(XW_1)\in\mathbb R^{m\times d}, X\in\mathbb R^{m\times n}$，而 $W_1\in\mathbb R^{n\times d}$，因此
$$
\nabla_{W_1}\ell_{ce}(\sigma(XW_1)W_2,y) = X^T(\sigma^\prime(XW_1) \circ ((S-I_y)W_2^T))
$$
这里 $\circ$ 表示标量积，即逐元素相乘。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的启发就是&lt;s&gt;不要手算了&lt;/s&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Backpropagation &quot;in general&quot;&lt;/h2&gt;
&lt;p&gt;假设我们有一个全连接网络 $Z_{i+1} = \sigma_i(Z_iW_i);i=1,\ldots,L$，如果要求最后一层的偏导，那么就有
$$
\frac{\partial \ell(Z_{L+1},y)}{\partial W_i} = \frac{\partial \ell}{\partial  Z_{L+1}}\cdot \frac{\partial Z_{L+1}}{\partial Z_L}\cdots \frac{\partial Z_{i+2}}{\partial Z_{i+1}}\cdot \frac{\partial Z_{i+1}}{\partial W_i}
$$
注意到这里的链式求导很多都是重复的操作，设
$$
G_{i+1} = \frac{\partial \ell}{\partial  Z_{L+1}}\cdot \frac{\partial Z_{L+1}}{\partial Z_L}\cdots \frac{\partial Z_{i+2}}{\partial Z_{i+1}}
$$
则有一个简单的递推关系
$$
G_i = G_{i+1}\cdot \frac{\partial Z_{i+1}}{\partial Z_i} = G_{i+1}\cdot \frac{\partial \sigma(Z_iW_i)}{\partial Z_iW_i}\cdot\frac{\partial Z_iW_i}{\partial Z_i} = G_{i+1}\cdot \sigma^\prime(Z_iW_i)\cdot W_i
$$
这里 $G_i = \nabla_{Z_i}\ell(Z_{L+1},y)\in\mathbb R^{m\times n_i},Z_i\in\mathbb R^{m\times n_i},W_i\in\mathbb R^{n_i\times n_{i+1}}$。&lt;/p&gt;
&lt;p&gt;因此我们调整 $G_{i+1}\cdot \sigma^\prime(Z_iW_i)\cdot W_i$ 的维度为
$$
G_i = \nabla_{Z_i}\ell(Z_{L+1},y) = (G_{i+1}\circ \sigma^\prime(Z_iW_i)) \cdot W_i^T
$$
最后就可以求出我们实际上需要的梯度 $\nabla_{W_i}\ell(Z_{L+1},y)$，
$$
\begin{aligned}
\frac{\partial \ell(Z_{L+1},y)}{\partial W_i} &amp;amp;= \nabla_{W_i}\ell(Z_{L+1},y)\
&amp;amp;= G_{i+1}\cdot \frac{\partial Z_{i+1}}{\partial W_i}\
&amp;amp;= G_{i+1}\cdot \frac{\partial \sigma_i(Z_iW_i)}{\partial W_i}\
&amp;amp;= G_{i+1}\cdot \frac{\partial \sigma_i(Z_iW_i)}{\partial Z_iW_i}\cdot \frac{\partial Z_iW_i}{\partial W_i}\
&amp;amp;= G_{i+1}\cdot \sigma^\prime(Z_iW_i) \cdot Z_i
\end{aligned}
$$
最后再调整维度为
$$
\frac{\partial \ell(Z_{L+1},y)}{\partial W_i} = \nabla_{W_i}\ell(Z_{L+1},y) = Z_i^T \cdot (G_{i+1}\circ \sigma^\prime(Z_iW_i))
$$&lt;/p&gt;
&lt;h2&gt;Backpropagation: Forward and backward passes&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Forward pass
&lt;ul&gt;
&lt;li&gt;Initialize: $Z_1=X$，&lt;/li&gt;
&lt;li&gt;Iterate: $Z_{i+1} = \sigma_i(Z_iW_i); i=1,\ldots,L$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Backward pass
&lt;ul&gt;
&lt;li&gt;Initialize: $G_{L+1} = \nabla_{Z_{L+1}}\ell(Z_{L+1},y) = S-I_y$，&lt;/li&gt;
&lt;li&gt;Iterate: $G_i = (G_{i+1}\circ \sigma^\prime(Z_iW_i)) \cdot W_i^T; i=L,\ldots,1$，&lt;/li&gt;
&lt;li&gt;Compute gradients: $\nabla_{W_i}\ell(Z_{L+1},y) = Z_i^T \cdot (G_{i+1}\circ \sigma^\prime(Z_iW_i))$。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Softmax Regression</title><link>https://fuwari.vercel.app/posts/softmax_regression/softmax_regression/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/softmax_regression/softmax_regression/</guid><description>从Softmax Regression的基本概念出发，介绍假设类、损失函数与优化方法，推导Cross-Entropy Loss的梯度，并给出Python和C++的MNIST分类实现。</description><pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Softmax Regression&lt;/h1&gt;
&lt;p&gt;这是&lt;a href=&quot;https://www.youtube.com/watch?v=MlivXhZFbNA&quot;&gt;&lt;strong&gt;Deep Learning Systems&lt;/strong&gt; Lecture 2 - ML Refresher / Softmax Regression&lt;/a&gt;的学习笔记。&lt;/p&gt;
&lt;h2&gt;Three Ingredients of ML algorithm&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The hypothesis class&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;也就是将输入映射到输出的程序流程。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Loss function&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;用于衡量hypothesis class表现如何。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;An optimization method&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;减小loss、优化参数的方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Softmax Regression Optimization Problem&lt;/h2&gt;
&lt;h3&gt;Notation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;假设问题的输入是一个长为 $n$ 的一维向量，所求输出是一个长为 $k$ 的一维向量，输入数据一共有 $m$ 个。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;比如，MNIST训练集有60000幅图像，每幅图像都是 $28\times 28$ 的灰度图，需要分为10种数字类别。对应本节描述就是 $n=28\times 28=784,k=10,m=60000$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;hypothesis class $h:\mathbb R^n \to \mathbb R^k$，也就是将输入映射到输出的函数。特别地，在本文中将采用线性算子作为hypothesis class，也就是
$$
h_{\theta}(x) = \theta^T x
$$
这里 $\theta\in\mathbb R^{n\times k}$。（也就是一个简单的线性加权，因为 $x\in \mathbb R^{n\times 1}$）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Matrix batch notation&lt;/p&gt;
&lt;p&gt;在实际情况下，我们往往会将数据打包为矩阵，而不是单独处理一维向量，因此有以下matrix batch notation：
$$
X\in\mathbb{R}^{m\times n} = \begin{pmatrix}
x^{(1)^{T}}\
\vdots\
x^{(m)^{T}}\
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
y\in{1,\ldots,k}^m = \begin{pmatrix}
y^{(1)}\
\vdots\
y^{(m)}
\end{pmatrix}
$$&lt;/p&gt;
&lt;p&gt;这里 $X$ 表示将输入打包后的矩阵，$y$ 表示对应的标签（not one-hot）。将线性假设应用到batch中，就有
$$
h_{\theta}(X) = \begin{pmatrix}
h_{\theta}(x^{(1)^T})\
\vdots\
h_{\theta}(x^{(m)^T})\
\end{pmatrix}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Softmax / Cross-Entropy Loss&lt;/h3&gt;
&lt;p&gt;softmax的作用往往是将hypothesis class的输出转为概率，
$$
z_i = P(label=i) = \frac{\exp(h_i(x))}{\sum_{j=1}^k\exp(h_j(x))}\Leftrightarrow z = \text{normalize}(\exp(h(x)))
$$
这里 $z\in\mathbb{R}^{k}$，表示将输出 $h(x)$ 通过softmax转换为概率分布。&lt;/p&gt;
&lt;p&gt;Cross-Entropy Loss是最常见的loss function，表示为
$$
\begin{aligned}
\ell_{ce}(h(x),y) &amp;amp;= -\log P(label=y)\
&amp;amp;= -\log \frac{\exp(h_y(x))}{\sum_{j=1}^k\exp(h_j(x))}\
&amp;amp;= -\log \exp(h_y(x)) + \log \sum_{j=1}^k \exp(h_j(x))\
&amp;amp;= -h_y(x) + \log \sum_{j=1}^k \exp(h_j(x))
\end{aligned}
$$&lt;/p&gt;
&lt;h3&gt;The Softmax Regression Optimization Problem&lt;/h3&gt;
&lt;p&gt;我们可以将机器学习的三个组成成分简化为一个优化问题：
$$
\min_{\theta} \frac {1}{m}\sum_{i=1}^m \ell(h_\theta(x^{(i)}),y^{(i)})
$$
即最小化hypothesis class的平均loss。&lt;/p&gt;
&lt;p&gt;在本文中则是
$$
\min_{\theta} \frac {1}{m}\sum_{i=1}^m \ell_{ce}(\theta^Tx^{(i)},y^{(i)})
$$&lt;/p&gt;
&lt;h2&gt;Gradient Descent&lt;/h2&gt;
&lt;h3&gt;Gradient Descent(TODO)&lt;/h3&gt;
&lt;p&gt;&amp;lt;img src=&quot;softmax_regression.assets/1920px-Gradient_descent.svg.png&quot; alt=&quot;undefined&quot; style=&quot;zoom: 25%;&quot; /&amp;gt;&lt;/p&gt;
&lt;h3&gt;Stochastic Gradient Descent&lt;/h3&gt;
&lt;p&gt;相较于直接的梯度下降，机器/深度学习中往往采用Stochastic Gradient Descent，即随机梯度下降。这里，Stochastic体现在计算梯度时，并不是使用整个训练集来计算，而是随机选取训练集中的一个小批量样本来估算梯度，从而进行参数更新。&lt;/p&gt;
&lt;p&gt;具体地，传统的梯度下降方法（Batch Gradient Descent）会在每次迭代中使用整个训练集来计算梯度并更新模型参数，这会导致计算成本非常高，尤其是在训练集很大的时候。而在SGD中，每次参数更新是基于单个样本或者小批量样本的梯度。&lt;/p&gt;
&lt;p&gt;这种随机选择的方式带来了两个主要特点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;计算速度更快&lt;/strong&gt;：由于每次迭代只计算一个小批量或单个样本的梯度，计算时间比全量梯度下降要短。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;噪声性&lt;/strong&gt;：由于每次梯度计算只基于部分样本，所以梯度更新会带有噪声，表现为更不稳定的更新路径。这种噪声在短期内可能导致不收敛或震荡，但长远来看，可以帮助跳出局部最优解，从而找到更好的全局最优解。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体可以表示为：&lt;/p&gt;
&lt;p&gt;Repeat:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sample a minibatch of data $X\in \mathbb R^{B\times n}, y\in{1,\ldots,k}^B$&lt;/li&gt;
&lt;li&gt;Update parameters $\theta:= \theta - \frac{\alpha}{B}\sum_{i=1}^B\nabla_\theta\ell(h_{\theta}(x^{(i)}),y^{(i)})$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Gradient of the  Softmax Objective&lt;/h3&gt;
&lt;h4&gt;$\nabla_{\theta}\ell_{ce}(\theta^T x,y)$&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;目标：计算 $\nabla_{\theta}\ell_{ce}(\theta^T x,y)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先我们计算softmax的偏导数，也就是 $\frac{\partial \ell_{ce}(h,y)}{\partial h_i}$，
$$
\begin{aligned}
\frac{\partial \ell_{ce}(h,y)}{\partial h_i} &amp;amp;= \frac{\partial }{\partial  h_i}(-h_y+\log \sum_{j=1}^k \exp(h_j))\
&amp;amp;= -[i=y] + \frac{\frac{\partial }{\partial  h_i}\sum_{j=1}^k \exp(h_j)}{\sum_{j=1}^k \exp(h_j)}\
&amp;amp;= -[i=y] + \frac{\exp(h_i)}{\sum_{j=1}^k \exp(h_j)}
\end{aligned}
$$
不难发现该式的后半部分就是softmax，所以 $\nabla_h \ell_{ce}(h,y)$ 可以表示为
$$
\nabla_h \ell_{ce}(h,y) = z - e_y
$$
$z = \text{normalize}(\exp(h))$，$e_y$ 则是标签 $y$ 的one-hot编码。&lt;/p&gt;
&lt;p&gt;然后我们再计算 $\nabla_{\theta}\ell_{ce}(\theta^T x,y)$，注意这里对矩阵的求导我们假设“一切都是标量”（也就是假设 $\theta$ 就是一个标量而非向量），然后直接对 $\theta$ 应用链式法则，并且按经验重排矩阵的维度，而不是正常的严谨数学推导（多元微积分）。
$$
\begin{aligned}
\frac{\partial}{\partial \theta} \ell_{ce}(\theta^T x,y) &amp;amp;= \frac{\partial \ell_{ce}(\theta^T x,y)}{\partial \theta^T x} \cdot \frac{\partial \theta^T x}{\partial \theta}\
&amp;amp;= (z-e_y)\cdot x
\end{aligned}
$$
注意这个链式法则的左半部就是上方推导的softmax的偏导数，而后半部分就是对线性加权的偏导。&lt;/p&gt;
&lt;p&gt;但是这有个问题，$z-e_y$ 和 $x$ 的维度不能直接点乘，根据维度 $z-e_y\in\mathbb R^{k\times 1}, x\in\mathbb R^{n\times 1}$，然后，不难分析出 $\frac{\partial}{\partial \theta} \ell_{ce}(\theta^T x,y)$ 的维度应该和 $\theta$ 是一样的（$\theta$ 需要直接减去这个gradient），所以我们应该将维度改写成 $x\cdot(z-e_y)^T$，即
$$
\begin{aligned}
\frac{\partial}{\partial \theta} \ell_{ce}(\theta^T x,y) &amp;amp;= x\cdot(z-e_y)^T
\end{aligned}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上就是完全按照经验调整维度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;matrix batch&lt;/h4&gt;
&lt;p&gt;实际上，我们一般是对输入打包进行处理，也就是所谓的matrix batch，这种暴力偏导仍然可以成立。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;目标：计算 $\nabla_{\theta}\ell_{ce}(X\theta,y)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意因为 $X\in \mathbb R^{m\times n}$，所以 $\theta$ 这回为了统一维度变成了矩阵右乘，显然 $\theta\in\mathbb R^{n\times k}$。
$$
\begin{aligned}
\frac{\partial}{\partial \theta} \ell_{ce}(X\theta,y) &amp;amp;= \frac{\partial \ell_{ce}(X\theta,y)}{\partial X\theta} \cdot \frac{\partial X\theta}{\partial \theta}\
&amp;amp;= (Z-I_y)\cdot X
\end{aligned}
$$
这里 $Z-I_y\in\mathbb R^{m\times k}$，可以认为是 $m$ 个向量的堆叠；$X\in \mathbb R^{m\times n}$，因此我们要将这个结果按照维度重排为 $X^T\cdot(Z-I_y)$。
$$
\frac{\partial}{\partial \theta} \ell_{ce}(X\theta,y) = X^T\cdot(Z-I_y)
$$&lt;/p&gt;
&lt;h2&gt;MNIST&lt;/h2&gt;
&lt;h3&gt;Python&lt;/h3&gt;
&lt;p&gt;这是一个linear hypothesis对于MNIST数据集的分类算法的个人实现，测试集 &lt;code&gt;acc = 0.92&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import loguru
import numpy as np
import gzip
from tqdm import tqdm
from copy import copy


def read_idx(filename):
    with gzip.open(filename, &apos;rb&apos;) as f:
        # 读取文件头（前16个字节）
        data = f.read()
        # 解析文件头信息
        magic_number = int.from_bytes(data[0:4], byteorder=&apos;big&apos;)
        if magic_number == 2051:
            # 图像文件，读取图像数据
            num_images = int.from_bytes(data[4:8], byteorder=&apos;big&apos;)
            num_rows = int.from_bytes(data[8:12], byteorder=&apos;big&apos;)
            num_cols = int.from_bytes(data[12:16], byteorder=&apos;big&apos;)
            # 图像数据
            images = np.frombuffer(data[16:], dtype=np.uint8).reshape(num_images, num_rows, num_cols)
            return images
        elif magic_number == 2049:
            # 标签文件，读取标签数据
            num_labels = int.from_bytes(data[4:8], byteorder=&apos;big&apos;)
            labels = np.frombuffer(data[8:], dtype=np.uint8)
            return labels
        else:
            raise ValueError(&quot;Unknown magic number&quot;)


image_file_tr = &apos;train-images-idx3-ubyte.gz&apos;
image_file_ts = &apos;t10k-images-idx3-ubyte.gz&apos;
label_file_tr = &apos;train-labels-idx1-ubyte.gz&apos;
label_file_ts = &apos;t10k-labels-idx1-ubyte.gz&apos;


class linear_model:
    def __init__(self, n=784, k=10, lr=1e-3, batch_size=100):
        self.n = n
        self.k = k
        self.lr = lr
        self.eps = 1e-7
        self.batch_size = batch_size
        self.theta = np.random.randn(self.n, self.k).astype(np.float32)

    def compute_loss(self, x, y):
        # 1. 计算 softmax
        # 为了避免溢出，减去每行的最大值（数值稳定性）
        exp_x = np.exp(x - np.max(x, axis=1, keepdims=True))
        exp_x += self.eps
        softmax_probs = exp_x / np.sum(exp_x, axis=1, keepdims=True)

        # 2. 获取每个样本的正确类别的预测概率
        correct_probs = softmax_probs[np.arange(len(y)), y]

        # 3. 计算交叉熵损失
        loss = -np.mean(np.log(correct_probs))

        return loss

    def compute_gradient(self, x1, x2, y):
        exp_x = np.exp(x2 - np.max(x2, axis=1, keepdims=True))
        exp_x += self.eps
        z = exp_x / np.sum(exp_x, axis=1, keepdims=True)

        e_y = np.zeros((y.shape[0], self.k), dtype=np.float32)
        e_y[np.arange(y.size), y] = 1

        xt = x1.T.astype(np.float32)

        g = np.matmul(xt, z - e_y)
        return g

    def forward(self, x, y, mode=&apos;train&apos;):
        input_x = copy(x)
        x = x @ self.theta
        if mode == &apos;train&apos;:
            loss = self.compute_loss(x, y)
            gradient = self.compute_gradient(input_x, x, y)
            self.theta = self.theta - self.lr * gradient / self.batch_size
            return x, loss
        else:
            return x


if __name__ == &apos;__main__&apos;:
    train_images = read_idx(image_file_tr)
    train_labels = read_idx(label_file_tr)
    test_images = read_idx(image_file_ts)
    test_labels = read_idx(label_file_ts)

    train_images = np.reshape(train_images, (train_images.shape[0], 28 * 28)).astype(np.float32)
    test_images = np.reshape(test_images, (test_images.shape[0], 28 * 28)).astype(np.float32)
    train_images = train_images / 255.0
    test_images = test_images / 255.0
    m = 10
    epochs = 100
    model = linear_model(n=784, k=10, lr=0.1, batch_size=m)
    for e in range(epochs):
        loguru.logger.info(f&quot;Epoch {e+1}/{epochs}&quot;)
        with tqdm(total=len(train_images) // m, desc=f&quot;loss = {0}&quot;) as pbar:
            loss_sum = np.zeros(1)
            for i in range(len(train_images) // m):
                x = train_images[i * m: (i + 1) * m]
                y = train_labels[i * m: (i + 1) * m]
                _, loss = model.forward(x, y)
                loss_sum += loss
                pbar.set_description(f&quot;loss = {loss}&quot;)
                pbar.update()
            pbar.close()
            loguru.logger.info(f&quot;epoch {e+1}, average loss = {loss_sum / (len(train_images) // m)}&quot;)
            preds = model.forward(test_images, test_labels, &apos;test&apos;)
            preds = np.argmax(preds, axis=1)
            acc = np.sum(preds == test_labels) / len(test_labels)
            loguru.logger.info(f&quot;accuracy = {acc}&quot;)
            if (e + 1) % 2 == 0:
                model.lr *= 0.9
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Cpp&lt;/h3&gt;
&lt;p&gt;cpp实现移植于&lt;a href=&quot;https://github.com/st1vdy/dlsys-hw/blob/main/hw0-main/src/simple_ml_ext.cpp&quot;&gt;dlsys hw0&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;

using std::vector;

template&amp;lt;typename ...T&amp;gt;
auto print(T&amp;amp;&amp;amp;... args) {
    ((std::cout &amp;lt;&amp;lt; args &amp;lt;&amp;lt; &quot; &quot;), ...) &amp;lt;&amp;lt; &quot;\n&quot;;
}

template&amp;lt;typename T&amp;gt; struct matrix {
    size_t n, m;
    vector&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; a;

    matrix(size_t n_, size_t m_, int val = 0) : n(n_), m(m_), a(n_, vector&amp;lt;T&amp;gt;(m_, val)) {}

    matrix(const T* x, size_t n_, size_t m_) {
        n = n_, m = m_;
        a.resize(n, vector&amp;lt;T&amp;gt;(m));        
        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; m; j++) {
                a[i][j] = *(x + i * m + j);
            }
        }
    }

    vector&amp;lt;T&amp;gt;&amp;amp; operator[](int k) { return this-&amp;gt;a[k]; }

    matrix operator - (matrix&amp;amp; k) { return matrix(*this) -= k; }

    matrix operator * (matrix&amp;amp; k) { return matrix(*this) *= k; }

    matrix&amp;amp; operator -=(matrix&amp;amp; mat) {
        assert(n == mat.n);
        assert(m == mat.m);
        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; m; j++) {
                a[i][j] -= mat[i][j];
            }
        }
        return *this;
    }

    matrix&amp;amp; operator *= (matrix&amp;amp; mat) {
        assert(m == mat.n);
        int x = n, y = mat.m, z = m;
        matrix&amp;lt;T&amp;gt; c(x, y);
        for (int i = 0; i &amp;lt; x; i++) {
            for (int k = 0; k &amp;lt; z; k++) {
                T r = a[i][k];
                for (int j = 0; j &amp;lt; y; j++) {
                    c[i][j] += mat[k][j] * r;
                }
            }
        }
        return *this = c;
    }

    matrix&amp;lt;T&amp;gt; transpose() {
        matrix&amp;lt;T&amp;gt; result(m, n);
        for (int i = 0; i &amp;lt; m; i++) {
            for (int j = 0; j &amp;lt; n; j++) {
                result[i][j] = a[j][i];
            }
        }
        return result;
    }

    matrix&amp;lt;float&amp;gt; softmax() {
        matrix&amp;lt;float&amp;gt; result(*this);
        for (int i = 0; i &amp;lt; n; i++) {
            auto&amp;amp; v = result.a[i];
            auto mx = *std::max_element(v.begin(), v.end());
            for (auto&amp;amp; j : v) {
                j -= mx;
                j = std::exp(j);
            }
            auto exp_sum = std::accumulate(v.begin(), v.end(), (float)0.0);
            for (auto&amp;amp; j : v) {
                j /= exp_sum;
            }
        }
        return result;
    }
};

float ce_loss(matrix&amp;lt;float&amp;gt;&amp;amp; Z, matrix&amp;lt;uint8_t&amp;gt;&amp;amp; I_y) {
    int batch = Z.n;
    float sum = 0;
    for (int i = 0; i &amp;lt; batch; i++) {
        float v = Z[i][I_y[i][0]];
        v = -std::log(v);
        sum += v;
    }
    return sum / static_cast&amp;lt;float&amp;gt;(batch);
}

float softmax_regression_epoch_cpp(const float* X, const unsigned char* y,
    float* theta, size_t m, size_t n, size_t k,
    float lr, size_t batch)
{
    /**
     * A C++ version of the softmax regression epoch code.  This should run a
     * single epoch over the data defined by X and y (and sizes m,n,k), and
     * modify theta in place.  Your function will probably want to allocate
     * (and then delete) some helper arrays to store the logits and gradients.
     *
     * Args:
     *     X (const float *): pointer to X data, of size m*n, stored in row
     *          major (C) format
     *     y (const unsigned char *): pointer to y data, of size m
     *     theta (float *): pointer to theta data, of size n*k, stored in row
     *          major (C) format
     *     m (size_t): number of examples
     *     n (size_t): input dimension
     *     k (size_t): number of classes
     *     lr (float): learning rate / SGD step size
     *     batch (int): SGD minibatch size
     *
     * Returns:
     *     (None)
     */

     /// BEGIN YOUR CODE
    float alpha = lr / static_cast&amp;lt;float&amp;gt;(batch);
    float loss = 0;
    for (int e = 0; e &amp;lt; m / batch; e++) {
        matrix&amp;lt;float&amp;gt; batch_X(X + e * batch * n, batch, n);
        matrix&amp;lt;unsigned char&amp;gt; batch_y(y + e * batch, batch, 1);
        matrix&amp;lt;float&amp;gt; t(theta, n, k);

        auto&amp;amp;&amp;amp; XW = batch_X * t;
        auto&amp;amp;&amp;amp; XT = batch_X.transpose();
        auto&amp;amp;&amp;amp; Z = XW.softmax();
        matrix&amp;lt;float&amp;gt; I_y(batch, k);
        auto loss_i = ce_loss(Z, batch_y);
        loss += loss_i;

        // X^T @ (Z - I_y)
        for (int i = 0; i &amp;lt; batch; i++) {
            I_y[i][batch_y[i][0]] = 1;
        }
        Z -= I_y;
        auto&amp;amp;&amp;amp; res = XT * Z;

        for (int i = 0; i &amp;lt; n; i++) {
            for (int j = 0; j &amp;lt; k; j++) {
                *(theta + i * k + j) -= res[i][j] * alpha;
            }
        }
    }

    return loss / (m / batch);
    /// END YOUR CODE
}

float predict(const float* X, const unsigned char* y, const float* theta, size_t m, size_t n, size_t k) {
    matrix&amp;lt;float&amp;gt; MX(X, m, n);
    matrix&amp;lt;float&amp;gt; W(theta, n, k);
    auto&amp;amp;&amp;amp; res = MX * W;
    int num_acc = 0;
    for (int i = 0; i &amp;lt; m; i++) {
        auto&amp;amp;&amp;amp; v = res.a[i];
        int pred_class = max_element(v.begin(), v.end()) - v.begin();
        int tp = *(y + i);
        num_acc += (pred_class == tp);
    }
    return static_cast&amp;lt;float&amp;gt;(num_acc) / m;
}

uint32_t swap_endian(uint32_t val) {
    return ((val &amp;gt;&amp;gt; 24) &amp;amp; 0x000000FF) |  // 获取最高字节
        ((val &amp;gt;&amp;gt; 8) &amp;amp; 0x0000FF00) |   // 获取次高字节
        ((val &amp;lt;&amp;lt; 8) &amp;amp; 0x00FF0000) |   // 获取次低字节
        ((val &amp;lt;&amp;lt; 24) &amp;amp; 0xFF000000);   // 获取最低字节
}

void read_mnist_images(const std::string&amp;amp; filepath, std::vector&amp;lt;std::vector&amp;lt;uint8_t&amp;gt;&amp;gt;&amp;amp; images, int&amp;amp; num_images, int&amp;amp; rows, int&amp;amp; cols) {
    std::ifstream file(filepath, std::ios::binary);

    uint32_t magic_number, num_items;
    uint32_t num_rows, num_cols;

    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(&amp;amp;magic_number), 4);
    magic_number = swap_endian(magic_number);  // 转换字节序（小端 -&amp;gt; 大端）

    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(&amp;amp;num_items), 4);
    num_items = swap_endian(num_items);

    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(&amp;amp;num_rows), 4);
    num_rows = swap_endian(num_rows);

    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(&amp;amp;num_cols), 4);
    num_cols = swap_endian(num_cols);

    num_images = num_items;
    rows = num_rows;
    cols = num_cols;
    images.resize(num_images, std::vector&amp;lt;uint8_t&amp;gt;(rows * cols));

    for (int i = 0; i &amp;lt; num_images; ++i) {
        file.read(reinterpret_cast&amp;lt;char*&amp;gt;(images[i].data()), rows * cols);
    }
}

void read_mnist_labels(const std::string&amp;amp; filepath, std::vector&amp;lt;uint8_t&amp;gt;&amp;amp; labels, int&amp;amp; num_labels) {
    std::ifstream file(filepath, std::ios::binary);

    // 读取头部信息
    uint32_t magic_number, num_items;
    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(&amp;amp;magic_number), 4);
    magic_number = swap_endian(magic_number);  // 转换字节序（小端 -&amp;gt; 大端）

    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(&amp;amp;num_items), 4);
    num_items = swap_endian(num_items);

    num_labels = num_items;
    labels.resize(num_labels);

    // 读取所有标签数据
    file.read(reinterpret_cast&amp;lt;char*&amp;gt;(labels.data()), num_labels);
}

float* convert_vec(const std::vector&amp;lt;std::vector&amp;lt;uint8_t&amp;gt;&amp;gt;&amp;amp; images_tr) {
    size_t num_images = images_tr.size();
    if (num_images == 0)
        return nullptr;

    size_t image_size = images_tr[0].size();
    float* flattened = new float[num_images * image_size];

    for (size_t i = 0; i &amp;lt; num_images; i++) {
        const std::vector&amp;lt;uint8_t&amp;gt;&amp;amp; img = images_tr[i];
        for (size_t j = 0; j &amp;lt; image_size; j++) {
            flattened[i * image_size + j] = static_cast&amp;lt;float&amp;gt;(img[j]) / 255.0;
        }
    }

    return flattened;
}

uint8_t* convert_vec(const std::vector&amp;lt;uint8_t&amp;gt;&amp;amp; images_tr) {
    size_t num_images = images_tr.size();
    if (num_images == 0)
        return nullptr;

    uint8_t* flattened = new uint8_t[num_images];

    for (size_t i = 0; i &amp;lt; num_images; i++) {
        flattened[i] = images_tr[i];
    }

    return flattened;
}

std::mt19937 rng(114);
float generateRandomNumber() {
    std::normal_distribution&amp;lt;float&amp;gt; distribution(0.0f, 1.0f);
    return distribution(rng);
}

void solve(std::vector&amp;lt;std::vector&amp;lt;uint8_t&amp;gt;&amp;gt;&amp;amp; images_tr, std::vector&amp;lt;std::vector&amp;lt;uint8_t&amp;gt;&amp;gt;&amp;amp; images_ts,
    std::vector&amp;lt;uint8_t&amp;gt;&amp;amp; labels_tr, std::vector&amp;lt;uint8_t&amp;gt; labels_ts) {
    int num_samples = images_tr.size();
    int batch_size = 50;
    int epochs = 10;
    float* train_images = convert_vec(images_tr);
    float* test_images = convert_vec(images_ts);
    uint8_t* train_labels = convert_vec(labels_tr);
    uint8_t* test_labels = convert_vec(labels_ts);
    
    int m = 60000;
    int n = 784;
    int k = 10;
    float lr = 0.2;
    float* theta = new float[n * k];
    for (int i = 0; i &amp;lt; n * k; i++) {
        theta[i] = generateRandomNumber();
    }
    int tm = 10000;
    for (int e = 0; e &amp;lt; epochs; e++) {
        auto loss = softmax_regression_epoch_cpp(train_images, train_labels, theta, m, n, k, lr, batch_size);
        auto acc = predict(test_images, test_labels, theta, tm, n, k);
        print(&quot;Epoch&quot;, e + 1, &quot;| loss =&quot;, loss, &quot;| acc =&quot;, acc);
    }
}

int main() {
    // 注意这里要解压文件
    std::string images_path_tr = &quot;train-images-idx3-ubyte&quot;;
    std::string images_path_ts = &quot;t10k-images-idx3-ubyte&quot;;
    std::string labels_path_tr = &quot;train-labels-idx1-ubyte&quot;;
    std::string labels_path_ts = &quot;t10k-labels-idx1-ubyte&quot;;
    int num_images_tr, num_labels_tr, rows, cols;
    int num_images_ts, num_labels_ts;
    std::vector&amp;lt;std::vector&amp;lt;uint8_t&amp;gt;&amp;gt; images_tr, images_ts;
    std::vector&amp;lt;uint8_t&amp;gt; labels_tr, labels_ts;

    read_mnist_images(images_path_tr, images_tr, num_images_tr, rows, cols);
    read_mnist_images(images_path_ts, images_ts, num_images_ts, rows, cols);
    read_mnist_labels(labels_path_tr, labels_tr, num_labels_tr);
    read_mnist_labels(labels_path_ts, labels_ts, num_labels_ts);

    solve(images_tr, images_ts, labels_tr, labels_ts);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>线段树基础教程</title><link>https://fuwari.vercel.app/posts/segment_tree_tutorial/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/segment_tree_tutorial/</guid><description>线段树从入门到进阶：单点/区间操作、懒标记、幺半群模板、动态开点及经典应用。</description><pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;区间修改+单点查询&lt;/h1&gt;
&lt;h2&gt;区间加+单点查询&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;给定一个 $n$ 个元素的数组 $a$，处理以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;add(l, r, x)&lt;/code&gt;：对于 $a_i(l\le i\lt r)$ 加上 $x$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(i)&lt;/code&gt;：获取 $a_i$ 的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是最经典和基本的线段树，线段树的核心思想就是：树上的每个节点负责处理原始数组的某一段区间，维护这段区间上发生的修改。如下图给出了一个 $8$ 个元素构成的线段树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/f31d7987f3b8bd8330874fee8bf2abd2c31b6fa7.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;线段树的每个结点维护一段区间 $[l,r]$，每个结点（除了叶子）有两个子结点，左子节点维护 $[l,mid]$，右子节点维护 $[mid+1,r]$（$mid=(l+r)/2$）。当 $l=r$ 时，该节点就是一个叶子结点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在代码中，我们一般认为线段树的根节点是 $1$；对于某个结点 $u$，它的左儿子是 $u&lt;em&gt;2$，右儿子是 $u&lt;/em&gt;2+1$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;区间加&lt;/h3&gt;
&lt;p&gt;对于区间加 &lt;code&gt;add(l, r, x)&lt;/code&gt;，我们的思路就是将 $[l,r)$ 这一段区间的变化拆解到一系列线段树上的小区间的变化。以 &lt;code&gt;add(3, 8, 5)&lt;/code&gt; 为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/65ce992b9f5946350e57bca3f01d1b4796a54a8a.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如图所示，操作1中，我们尝试从覆盖了 $[1,4]$ 区间的结点走向其左子结点，但是左子结点覆盖的区间是 $[1,2]$，与我们需要修改的区间 $[3,7]$ 没有交集，因此返回。操作2中，我们尝试走向覆盖了 $[3,4]$ 的右子结点，而 $[3,4]$ 被 $[3,7]$ 包含，因此这是我们需要修改的区间，我们对该线段修改后返回（不需要继续向下更新了）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/5af9f5f8aba22a20f76d26f2d02b20987328bd4b.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;右半子树同理，找到区间 $[5,6]$ 和 $[7,7]$ 后更新并返回。&lt;/p&gt;
&lt;p&gt;让我们再处理 &lt;code&gt;add(1, 6, 3)&lt;/code&gt; 和 &lt;code&gt;add(4, 7, 1)&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/b051c22894e65d7f7f8b5f73a6d9c56c6fff563b.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/ad1ce080c0e9ff2e3cb52a1a37189cff958262f6.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;总结，对于 $[ql,qr]$ 上的区间修改操作，线段树要做的就是在递归过程中找到覆盖了 $[l,r]$ 的结点（$ql\le l\le r\le qr$），修改这些结点并返回。（这里，我们用 $[ql,qr]$ 表示修改区间，$[l,r]$ 表示线段树某个结点覆盖的区间，后文将沿用这一记法）&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;复杂度分析：对于区间加操作，线段树的复杂度是什么？注意到，对于线段树上的每一层，我们至多找到两个符合 $ql\le l\le r\le qr$ 条件的线段，至多找到两个 $[ql,qr]$ 与 $[l,r]$ 没有交集的线段，因此一次递归的复杂度上限就是 $O(4\log n)$，因此区间加的复杂度为 $O(\log n)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;单点查询&lt;/h3&gt;
&lt;p&gt;对于线段树的单点查询 &lt;code&gt;get(i)&lt;/code&gt;，只需要一路递归找到覆盖了 $[i,i]$ 的结点，然后再回溯过程中将所有经过的区间上的数值相加即可。以 &lt;code&gt;get(4)&lt;/code&gt; 为例，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/cf63a75134efcbe1179a567b511c97e9fb2184e3.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们找到的结点覆盖的区间分别是 $[1,4],[3,4],[4,4]$，因此最终得到 &lt;code&gt;get(4) = 9&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;Code&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/1/practice/contest/279634/problem/A&quot;&gt;模板题 - 区间加+单点查询&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
using ll = long long;

class segTree {
public:
    int _n;
    struct node {
        ll add = 0;
    };
    vector&amp;lt;node&amp;gt; t;
    segTree(int n = 0) {
        _n = n;
        t.resize((_n + 1) * 4, { 0 });
    }
    void range_add(int rt, int l, int r, int ql, int qr, int v) {
        if (ql &amp;lt;= l &amp;amp;&amp;amp; r &amp;lt;= qr) {
            t[rt].add += v;
            return;
        }
        int mid = (l + r) / 2;
        if (ql &amp;lt;= mid) range_add(rt * 2, l, mid, ql, qr, v);
        if (mid + 1 &amp;lt;= qr) range_add(rt * 2 + 1, mid + 1, r, ql, qr, v);
        return;
    }
    ll query(int rt, int l, int r, int pos) {
        if (l == r) {
            return t[rt].add;
        }
        int mid = (l + r) / 2;
        if (pos &amp;lt;= mid) return query(rt * 2, l, mid, pos) + t[rt].add;
        else return query(rt * 2 + 1, mid + 1, r, pos) + t[rt].add;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n, q;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; q;
    segTree st(n);
    while (q--) {
        int type, x, y, z;
        cin &amp;gt;&amp;gt; type &amp;gt;&amp;gt; x;
        x++;
        if (type == 1) {
            cin &amp;gt;&amp;gt; y &amp;gt;&amp;gt; z;
            st.range_add(1, 1, n, x, y, z);
        } else {
            cout &amp;lt;&amp;lt; st.query(1, 1, n, x) &amp;lt;&amp;lt; &quot;\n&quot;;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要实现 &lt;code&gt;range_add()&lt;/code&gt; 和 &lt;code&gt;query()&lt;/code&gt; 这两个函数。&lt;/p&gt;
&lt;h2&gt;同时满足结合律和交换律的运算&lt;/h2&gt;
&lt;p&gt;假设运算 $\otimes$ 表示任意一种运算（比如 $+,-,\times,\gcd$），如果同时满足以下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结合律：对于 $a,b,c\in G$，$a\otimes (b\otimes c) = (a\otimes b)\otimes c$。&lt;/li&gt;
&lt;li&gt;交换律：对于 $a,b\in G$，$a\otimes b = b\otimes a$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;则线段树可以直接维护。&lt;/p&gt;
&lt;p&gt;假设 &lt;code&gt;modify(l, r, x)&lt;/code&gt; 表示对于 $[l,r)$ 中的所有 $a_i$ 都进行 $a_i=a_i\otimes x$ 的运算，且 $\otimes$ 是任意一种同时满足结合律+交换律的运算。假设我们对某一区间进行了两次操作 &lt;code&gt;modify(l, r, x)&lt;/code&gt; 和 &lt;code&gt;modify(l, r, y)&lt;/code&gt;；显然，该区间中的元素应该变成了 $((a_i\otimes x)\otimes y)$，但是线段树维护的是区间上的修改，即 $(a_i\otimes (x\otimes y))$，因此该运算必须满足结合律。&lt;/p&gt;
&lt;p&gt;那为什么要满足交换律？因为我们在进行区间修改时是自顶向下，而单点求值时是自底向上，因此必须有 $x\otimes y=y\otimes x$。&lt;/p&gt;
&lt;p&gt;因此，我们可以用相同的方法解决&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/1/practice/contest/279634/problem/B&quot;&gt;模板题 - 区间最值修改+单点查询&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;只满足结合律的运算&lt;/h2&gt;
&lt;p&gt;如果运算 $\otimes$ 只满足结合律，但并不满足交换律（比如矩阵乘法），我们可以通过懒惰标记（lazy propagation）的技术来处理。懒惰标记的核心思路是：保证所有的旧操作比新操作更深，并且在下推标记时用新操作去更新旧操作。&lt;/p&gt;
&lt;p&gt;假设区间 $[l,r]$ 上已经存在了某种操作 $x$，该节点的两个儿子上已分别有旧操作 $y,z$，当我们需要从 $[l,r]$ 继续向下递归时，我们就需要下推懒惰标记，将两个子结点分别更新为 $y\otimes x,z\otimes x$。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;segtree1-1.assets/5cdec3c87b31bedfc00f207e4a49f7860b735fdf.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如下图，蓝色结点表示当前递归中的根结点，橙色结点表示我们想要访问的结点，懒惰标记 $y$ 如图进行下推。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://espresso.codeforces.com/a08849fb8c634bcd14786a6263f5b3650da3a4ad.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然懒惰标记的一次下推复杂度是 $O(1)$，而线段树的一次区间修改操作和一次单点查询都是 $O(\log n)$，因此使用懒惰标记的线段树的复杂度不变。&lt;/p&gt;
&lt;h2&gt;区间覆盖+单点查询&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/1/practice/contest/279634/problem/C&quot;&gt;模板题 - 区间覆盖+单点查询&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定一个 $n$ 个元素的数组 $a$，处理以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;assign(l, r, v)&lt;/code&gt;：对于 $a_i(l\le i\lt r)$ 赋值为 $v$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(i)&lt;/code&gt;：查询 $a_i$ 的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;区间覆盖是一个典型的只满足结合律，但不满足交换律的运算，因此需要懒惰标记。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
using ll = long long;

class segTree {
public:
    int _n;
    struct node {
        int lazy_cover;
    };
    vector&amp;lt;node&amp;gt; t;
    segTree(int n = 0) {
        _n = n;
        t.resize((_n + 1) * 4, { -1 });
    }
    void pushdown(int rt) {
        if (t[rt].lazy_cover != -1) {
            int lc = rt * 2, rc = rt * 2 + 1;
            t[lc].lazy_cover = t[rt].lazy_cover;
            t[rc].lazy_cover = t[rt].lazy_cover;
            t[rt].lazy_cover = -1;
        }
    }
    void range_cover(int rt, int l, int r, int ql, int qr, int v) {
        if (ql &amp;lt;= l &amp;amp;&amp;amp; r &amp;lt;= qr) {
            t[rt].lazy_cover = v;
            return;
        }
        pushdown(rt);
        int mid = (l + r) / 2;
        if (ql &amp;lt;= mid) range_cover(rt * 2, l, mid, ql, qr, v);
        if (mid + 1 &amp;lt;= qr) range_cover(rt * 2 + 1, mid + 1, r, ql, qr, v);
        return;
    }
    int query(int rt, int l, int r, int pos) {
        if (l == r) {
            return t[rt].lazy_cover;
        }
        pushdown(rt);
        int mid = (l + r) / 2;
        if (pos &amp;lt;= mid) return query(rt * 2, l, mid, pos);
        else return query(rt * 2 + 1, mid + 1, r, pos);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n, q;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; q;
    segTree st(n);
    while (q--) {
        int type, x, y, z;
        cin &amp;gt;&amp;gt; type &amp;gt;&amp;gt; x;
        x++;
        if (type == 1) {
            cin &amp;gt;&amp;gt; y &amp;gt;&amp;gt; z;
            st.range_cover(1, 1, n, x, y, z);
        } else {
            cout &amp;lt;&amp;lt; max(0, st.query(1, 1, n, x)) &amp;lt;&amp;lt; &quot;\n&quot;;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与基础线段树的唯一不同之处在于，我们需要实现一个叫做 &lt;code&gt;pushdown()&lt;/code&gt; 的函数，该函数用于处理懒惰标记的下推。当且仅当我们需要从当前结点向下继续搜索时，我们需要在进入子结点之前先将当前节点的懒惰标记传递到两个子结点。&lt;/p&gt;
&lt;h1&gt;区间修改+区间查询&lt;/h1&gt;
&lt;h2&gt;区间加+区间最值&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/A&quot;&gt;模板题 - 区间加+区间最值&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;给定一个 $n$ 个元素的数组 $a$，处理以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;add(l, r, v)&lt;/code&gt;：对于 $a_i(l\le i\lt r)$ 加上 $x$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min(l, r)&lt;/code&gt;：求区间 $[l,r]$ 中最小的 $a_i$。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;举个例子，对于数组 &lt;code&gt;[3, 4, 2, 5, 6, 8, 1, 3]&lt;/code&gt; 构造一棵维护区间最小值的线段树：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://espresso.codeforces.com/a3651bdf9cbe828edc2877e14608dc02804e9dc0.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在，我们对于每个结点维护两个值：区间加的懒惰标记 &lt;code&gt;lazy_add&lt;/code&gt; 和区间最小值 &lt;code&gt;min_elem&lt;/code&gt;，然后以一次更新 &lt;code&gt;add(3, 8, 2)&lt;/code&gt; 为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://espresso.codeforces.com/3474b56cf6e9d2cea2554429bba646d67c10574c.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同样是递归将大区间划分到线段树上的一系列小区间，然后注意到：假设某一段区间 $[l,r]$ 整体加上了 $v$，那么这一段区间的最小值也会加上 $v$，表现在图中就是值分别为 $4, 8, 3$ 的结点（最小值）都加上了 $2$，并且这三个结点被打上了懒惰标记 &lt;code&gt;lazy_add = 2&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://espresso.codeforces.com/35e2ca666ba946aeb5cd046817b4943baeea2a7d.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于懒惰标记的下推也是同理，当需要从某个结点向下走时，先将父节点的信息下推至子结点，以图中橙色结点为例。&lt;/p&gt;
&lt;p&gt;但是，只更新 $4,8,3$ 这三个结点是不够的，对于一个线段 $[l,r]$，该线段上的最小值是左儿子 $[l,mid]$ 上最小值和右儿子 $[mid+1,r]$ 上最小值中的最小值，即 &lt;code&gt;min(l, r) = min(min(l, mid + 1), min(mid + 1, r))&lt;/code&gt;。也就是说，在回溯过程中，我们需要对于我们访问路径上的所有父结点进行更新，以确保线段树上所有被访问过的结点存储了正确的信息，只有未被访问的结点才可能未被最新的信息更新（这也就是lazy propagation命名的来源）。&lt;/p&gt;
&lt;p&gt;代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
using ll = long long;
const ll INF = 1e18;

class segTree {
public:
    int _n;
    struct node {
        ll lazy_add, min_elem;
    };
    vector&amp;lt;node&amp;gt; t;
    segTree(int n = 0) {
        _n = n;
        t.resize((_n + 1) * 4, { 0,0 });
    }
    void pushdown(int rt) {
        if (t[rt].lazy_add != 0) {
            int lc = rt * 2, rc = rt * 2 + 1;
            t[lc].lazy_add += t[rt].lazy_add;
            t[rc].lazy_add += t[rt].lazy_add;
            t[lc].min_elem += t[rt].lazy_add;
            t[rc].min_elem += t[rt].lazy_add;
            t[rt].lazy_add = 0;
        }
    }
    void pushup(int rt) {
        int lc = rt * 2, rc = rt * 2 + 1;
        t[rt].min_elem = min(t[lc].min_elem, t[rc].min_elem);
    }
    void range_add(int rt, int l, int r, int ql, int qr, int v) {
        if (ql &amp;lt;= l &amp;amp;&amp;amp; r &amp;lt;= qr) {
            t[rt].lazy_add += v;
            t[rt].min_elem += v;
            return;
        }
        pushdown(rt);
        int mid = (l + r) / 2;
        if (ql &amp;lt;= mid) range_add(rt * 2, l, mid, ql, qr, v);
        if (mid + 1 &amp;lt;= qr) range_add(rt * 2 + 1, mid + 1, r, ql, qr, v);
        pushup(rt, l, r);
        return;
    }
    ll range_min(int rt, int l, int r, int ql, int qr) {
        if (ql &amp;lt;= l &amp;amp;&amp;amp; r &amp;lt;= qr) {
            return t[rt].min_elem;
        }
        pushdown(rt);
        int mid = (l + r) / 2;
        ll res = INF;
        if (ql &amp;lt;= mid) res = min(res, range_min(rt * 2, l, mid, ql, qr));
        if (mid + 1 &amp;lt;= qr) res = min(res, range_min(rt * 2 + 1, mid + 1, r, ql, qr));
        pushup(rt);
        return res;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n, q;
    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; q;
    segTree st(n);
    while (q--) {
        int type, x, y, z;
        cin &amp;gt;&amp;gt; type &amp;gt;&amp;gt; x &amp;gt;&amp;gt; y;
        x++;
        if (type == 1) {
            cin &amp;gt;&amp;gt; z;
            st.range_add(1, 1, n, x, y, z);
        } else {
            cout &amp;lt;&amp;lt; st.range_min(1, 1, n, x, y) &amp;lt;&amp;lt; &quot;\n&quot;;
        }
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相较于区间修改+单点查询，这份代码多了一个函数 &lt;code&gt;pushup()&lt;/code&gt;，该函数的作用是在回溯过程中，父节点通过子结点的最新信息进行更新。&lt;/p&gt;
&lt;h2&gt;其他运算&lt;/h2&gt;
&lt;p&gt;假设有以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;modify(l, r, x)&lt;/code&gt;：表示将 $a_i(l\le i\lt r)$ 更新为 $a_i=a_i\otimes x$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calc(l, r)&lt;/code&gt;：表示计算 $a_i\odot a_{i+1}\odot\cdots\odot a_{r-1}$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里 $\otimes$ 和 $\odot$ 满足以下性质：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$\otimes$ 和 $\odot$ 都满足结合律。这是一个&lt;strong&gt;必要条件&lt;/strong&gt;，不过不难理解，因为线段树最核心的性质就是维护满足结合律的运算（根据前文，我们需要计算 $((a_i\otimes x)\otimes y)$，实际上线段树维护了 $(a_i\otimes (x \otimes y))$）。&lt;/li&gt;
&lt;li&gt;$\otimes$ 相对于 $\odot$ 可分配。这是一个&lt;strong&gt;必要条件&lt;/strong&gt;，可以类比分配律 $(a\otimes x)\odot (b\otimes x) = (a\odot b)\otimes x$。例如对于区间加+区间最小值就满足 $\min(a + x, b + x) = \min(a, b) + x$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;根据以上分析，可以解决以下练习题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/B&quot;&gt;区间乘+区间和&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/C&quot;&gt;区间或+区间与&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/D&quot;&gt;区间加+区间和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;区间加+区间求和相对特殊，因为 $(a+x)+(b+x)=(a+b)+2x$，注意到这似乎不满足传统意义上的分配律（本质上，全是加法就不存在分配律），但是实际上我们也只需要处理一下系数即可（对于区间加的标记乘上区间长度）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/E&quot;&gt;区间覆盖+区间最值&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/2/practice/contest/279653/problem/F&quot;&gt;区间覆盖+区间和&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;幺半群与线段树模板&lt;/h2&gt;
&lt;h3&gt;幺半群&lt;/h3&gt;
&lt;p&gt;先看幺半群的定义：一个 &lt;strong&gt;幺半群（Monoid）&lt;/strong&gt; 是一个集合 $M$ 和一个二元运算 $\circ$，满足以下条件：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;封闭性&lt;/strong&gt;：对于任意 $a,b\in M$，有 $a\circ b\in M$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;结合性&lt;/strong&gt;：对于任意 $a,b,c\in M$，有 $(a \circ b) \circ c = a \circ (b \circ c)$&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单位元&lt;/strong&gt;：存在一个元素 $e\in M$，使得对于任意 $a\in M$，有 $e \circ a = a \circ e =a$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而线段树这一数据结构可以维护所有的幺半群。举一些例子，实际上之前的大部分运算都可以用幺半群解释（下面给出的运算都有显然的封闭性，结合性）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$+,-$ 运算：对于加减法，单位元是 $0$。&lt;/li&gt;
&lt;li&gt;$\times$ 运算：乘法的单位元是 $1$。&lt;/li&gt;
&lt;li&gt;矩阵乘法运算：矩阵乘法的单位元是单位矩阵（主对角线为1，其余为0的矩阵）。&lt;/li&gt;
&lt;li&gt;$\min,\max$ 运算：对于 $\min,\max$ 运算，$\min$ 运算的单位元是 $+\infty$，$\max$ 运算的单位元是 $-\infty$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;模板化（区间仿射变换+区间和）&lt;/h3&gt;
&lt;p&gt;通过幺半群的思想，我们可以模板化线段树这一数据结构。例如ac-library中的&lt;a href=&quot;https://github.com/atcoder/ac-library/blob/master/document_en/lazysegtree.md&quot;&gt;Lazy Segtree&lt;/a&gt;，这个模板的线段树采用的是非递归线段树（ZKW线段树），具有更好的常数，但是我还是以递归式线段树为基准。虽然我们不直接采用ac-library的代码，但是可以沿用ac-library的接口定义。&lt;/p&gt;
&lt;p&gt;如下定义一棵线段树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class S, auto op, auto e, class F, auto mapping, auto composition, auto id&amp;gt;
class segTree {
public:
    // ...
private:
    int _n;
    vector&amp;lt;S&amp;gt; data;
    vector&amp;lt;F&amp;gt; lazy;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 $S$ 是一个幺半群，$(S,\cdot):S\times S\to S,e\in S$；$F$ 是一个 $S\to S$ 映射的集合，并且满足&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;$F$ 含有恒等映射 $\text{id}$，即 $\text{id}(x)=x$ 对于任意 $x\in S$ 成立。&lt;/li&gt;
&lt;li&gt;$F$ 在复合映射下封闭，即 $f\circ g\in F$ 对于任意 $f,g\in F$ 成立。&lt;/li&gt;
&lt;li&gt;$f(x\cdot y)=f(x)\cdot f(y)$ 成立（$\cdot$ 是定义幺半群的二元运算）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;引入一个模板题以解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://judge.yosupo.jp/problem/range_affine_range_sum&quot;&gt;区间仿射变换+区间和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;维护两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;0 l r b c&lt;/code&gt;：对于区间 $[l,r)$ 中的元素 $a_i$，赋值为 $a_i\leftarrow b\times a_i+c$（区间仿射变换）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1 l r&lt;/code&gt;：查询 $[l,r)$ 的区间和。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后我们逐个解释模板类的定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;class S&lt;/code&gt;：这是幺半群中元素的定义，以“区间仿射变换+区间和”为例，这里的元素就是每一个结点对应的（区间和，区间长度）这个二元组。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto op&lt;/code&gt;：或者更准确地，&lt;code&gt;S (*op)(S, S)&lt;/code&gt;；这是定义幺半群的二元运算，以“区间仿射变换+区间和”为例，这个二元运算就是区间加（区间和相加，区间长度相加），更准确的说是 &lt;code&gt;push_up&lt;/code&gt; 时两段区间合并的运算。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto e&lt;/code&gt;：单位元；以“区间仿射变换+区间和”为例，单位元就是 $0$，因为二元运算是加法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class F&lt;/code&gt;：映射集合中任意元素的定义，或者说这就是区间修改的定义；以“区间仿射变换+区间和”为例，区间仿射变换就是将区间中的所有元素 $x$ 变成 $cx+d$，因此每个映射可以抽象为一个二元组 $&amp;lt;c,d&amp;gt;$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto mapping&lt;/code&gt;：或者更准确地，&lt;code&gt;S (*mapping)(F, S)&lt;/code&gt;，这是映射的定义；比如仿射变换是 $f(x)=cx+d$，那么 &lt;code&gt;mapping&lt;/code&gt; 就是将 $x$ 映射要 $cx+d$ 的函数。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto composition&lt;/code&gt;：或者更准确地，&lt;code&gt;F (*composition)(F, F)&lt;/code&gt;，这是复合映射的定义；在线段树懒惰标记下推（&lt;code&gt;push_down&lt;/code&gt;）时，我们就会遇到：父节点上新的映射 $f$ 需要与子结点上旧的映射 $g$ 进行复合这一操作，而 &lt;code&gt;composition&lt;/code&gt; 函数就是处理这一过程的。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;auto id&lt;/code&gt;：恒等映射；以“区间仿射变换+区间和”为例，恒等映射就是 $f(x)=1x+0$，即 $c=1,d=0$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上，我们就能写出“区间仿射变换+区间和”这一问题中的所有定义了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct S {
    int sum, size;  // 区间和, 区间长度
};
S op(S l, S r) {
    return { add(l.sum, r.sum), l.size + r.size };
}
S e() {
    return { 0,1 };
}

struct F {
    int c, d;  // f(x) = cx + d.
    bool operator == (const F&amp;amp; k) const { return c == k.c and d == k.d; }
};
S mapping(F f, S x) {  // 注意我们维护的是区间和, 因此f(x) = c * sum + d * size
    return { add(mul(f.c, x.sum), mul(f.d, x.size)),x.size };
}
F composition(F f, F g) {  // f(g(x))
    return { mul(f.c,g.c),add(mul(f.c, g.d), f.d) };
}
F id() {
    return { 1,0 };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后给出维护幺半群的线段树模板（递归版）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class S, auto op, auto e, class F, auto mapping, auto composition, auto id&amp;gt;
class segTree {
public:
    segTree() : segTree(0) {}
    explicit segTree(int n) : segTree(std::vector&amp;lt;S&amp;gt;(n, e())) {}
    explicit segTree(const vector&amp;lt;S&amp;gt;&amp;amp; v) : _n(int(v.size())) {
        data.resize(4 * _n, e());
        lazy.resize(4 * _n, id());

        auto build = [&amp;amp;](auto&amp;amp;&amp;amp; self, int rt, int l, int r) -&amp;gt; void {
            if (l == r) {
                data[rt] = v[l - 1];
                return;
            }
            int mid = (l + r) / 2;
            self(self, rt * 2, l, mid);
            self(self, rt * 2 + 1, mid + 1, r);
            push_up(rt);
        };
        build(build, 1, 1, _n);
    }
    void push_up(int rt) {
        data[rt] = op(data[2 * rt], data[2 * rt + 1]);
    }
    void apply(int rt, F f) {
        data[rt] = mapping(f, data[rt]);
        lazy[rt] = composition(f, lazy[rt]);
    }
    void push_down(int rt) {
        if (lazy[rt] == id()) {
            return;
        }
        apply(2 * rt, lazy[rt]);
        apply(2 * rt + 1, lazy[rt]);
        lazy[rt] = id();
    }
    void range_modify(int rt, int l, int r, int ql, int qr, F f) {
        if (r &amp;lt; ql or l &amp;gt; qr) {
            return;
        }
        if (ql &amp;lt;= l and r &amp;lt;= qr) {
            apply(rt, f);
            return;
        }
        push_down(rt);
        int mid = (l + r) / 2;
        range_modify(rt * 2, l, mid, ql, qr, f);
        range_modify(rt * 2 + 1, mid + 1, r, ql, qr, f);
        push_up(rt);
    }
    S range_query(int rt, int l, int r, int ql, int qr) {
        if (r &amp;lt; ql or l &amp;gt; qr) {
            return e();
        }
        if (ql &amp;lt;= l and r &amp;lt;= qr) {
            return data[rt];
        }
        push_down(rt);
        int mid = (l + r) / 2;
        auto&amp;amp;&amp;amp; res = op(range_query(rt * 2, l, mid, ql, qr), range_query(rt * 2 + 1, mid + 1, r, ql, qr));
        push_up(rt);
        return res;
    }
private:
    int _n;
    vector&amp;lt;S&amp;gt; data;
    vector&amp;lt;F&amp;gt; lazy;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://atcoder.jp/contests/practice2/submissions/61229095&quot;&gt;AC Link&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;总结就是：对于一棵常规的带有懒惰标记的线段树，我们将所有需要实现的部分抽象到了7个部分。&lt;/p&gt;
&lt;p&gt;此外，我们还能把线段树上二分也集成进去，这也可以参考ac-library的定义。但我感觉这个模板化并不好用，大部分情况都要改模板内的代码，代码实现请参考下一节中的线段树上二分。&lt;/p&gt;
&lt;h1&gt;经典问题&lt;/h1&gt;
&lt;h2&gt;线段树维护最大子段和&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/3/practice/contest/280799/problem/A&quot;&gt;区间覆盖+最大子段和&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;维护以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;assign(l, r, v)&lt;/code&gt;：对于 $a_i(l\le i\lt r)$ 赋值为 $v$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max_seg(l, r)&lt;/code&gt;：询问区间 $[l,r)$ 上的最大子段和。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每一个结点（假设当前结点覆盖了 $[l,r]$），维护以下几种信息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;seg&lt;/code&gt;：$[l,r]$ 上的最大子段和&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre&lt;/code&gt;：$[l,r]$ 上的最大前缀子段和&lt;/li&gt;
&lt;li&gt;&lt;code&gt;suf&lt;/code&gt;：$[l,r]$ 上的最大后缀子段和&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sum&lt;/code&gt;：$[l,r]$ 上的元素之和&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在pushup过程中这样合并左右儿子信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// rt是父节点; lc, rc 分别是左右儿子
t[rt].sum = t[lc].sum + t[rc].sum;
t[rt].pre = max(t[lc].pre, t[lc].sum + t[rc].pre);
t[rt].suf = max(t[rc].suf, t[rc].sum + t[lc].suf);
t[rt].seg = max({ t[lc].seg,t[rc].seg,t[lc].suf + t[rc].pre });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为父节点的最大子段和只能是 左儿子的最大子段和 或 右儿子的最大子段和 或 跨越两段区间的最大子段和，如果跨越了左右儿子的区间，那么就一定是左儿子的最大后缀+右儿子的最大前缀。&lt;/p&gt;
&lt;h2&gt;0-1线段树找第K个1&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/3/practice/contest/280799/problem/B&quot;&gt;区间翻转+K-th one&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于一个只含有 $0,1$ 两种元素的数组，维护以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;翻转 $[l,r)$ 中的所有元素（$0$ 变 $1$，$1$ 变 $0$）&lt;/li&gt;
&lt;li&gt;求第 $k$ 个 $1$ 的下标&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于操作2，实际上我们只需要维护一个线段和。假设当前在根节点，左儿子的区间和为 $sum_l$，如果 $k\gt sum_l$，那么显然我们应该走向右儿子，查找第 $k-sum_l$ 个 $1$ 的下标；否则走向左儿子。&lt;/p&gt;
&lt;p&gt;对于操作1，我们维护一个区间翻转的懒惰标记即可，区间被翻转时，新的区间和就是区间总长度减去老的区间和。&lt;/p&gt;
&lt;h2&gt;线段树查找第一个不小于 $x$ 的元素（线段树上二分）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/3/practice/contest/280799/problem/C&quot;&gt;区间加+lower_bound&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;维护以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;add(l, r, v)&lt;/code&gt;：对于 $a_i(l\le i\lt r)$ 加上 $x$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lower_bound(l, x)&lt;/code&gt;：查询第一个满足 $j\ge l,a[j]\ge x$ 的元素的下标。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;实际上很简单，我们维护一下线段的区间最大值即可，如果左儿子的区间最大值超过 $x$ 并且左儿子的区间和 $[l,+\infty)$ 有交集就向左儿子递归，如果左儿子没找到或者符合条件的元素就向右儿子递归。复杂度分析很简单：和区间加的复杂度分析类似，线段树上每一层最多两次失败查询+两次成功查询，因此复杂度是 $O(\log n)$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;线段树上的lower_bound操作甚至不需要序列单调。这是因为我们本质上是在线段树维护的前缀最值数组中进行二分，而这个数组是单调的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 找第一个满足 a[j] &amp;gt;= x and j &amp;gt;= lo 的下标j
// 如果不存在就返回 n + 1
int min_left(int rt, int l, int r, int lo, ll x) {
    if (l == r) {
        return l;
    }
    push_down(rt);
    int mid = (l + r) / 2, res = _n + 1;
    if (mid &amp;gt;= lo and data[rt * 2].max_elem &amp;gt;= x) res = min_left(rt * 2, l, mid, lo, x);
    if (res &amp;gt; _n and data[rt * 2 + 1].max_elem &amp;gt;= x) res = min_left(rt * 2 + 1, mid + 1, r, lo, x);
    push_up(rt);
    return res;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;动态开点&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/range-module/&quot;&gt;715. Range 模块&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;对于长度为 $n(n=1e9)$ 的0-1数组，维护以下几种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;1 l r v&lt;/code&gt;：将区间 $[l,r)$ 内的每个数赋值为 $v$，$v=0$ 或 $1$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2 l r&lt;/code&gt;：查询区间 $[l,r)$ 内的元素是否全是 $1$。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果 $n$ 很小，那么本题用线段树维护非常简单；但是由于 $n=1e9$，所以不能直接上线段树。&lt;/p&gt;
&lt;p&gt;如果离线+离散化可行的话，本题还是很简单；可惜本题强制在线，因此就需要一种叫做“动态开点”的技巧。&lt;/p&gt;
&lt;p&gt;观察线段树的结构，我们不难发现，只有我们需要访问的结点才需要被创建（比如我要遍历到结点 $[l,r]$ 后返回，它的两个子结点实际上并不需要被创建），因此动态开点的思想就是当我们需要走到某一个结点时才声明这个节点。&lt;/p&gt;
&lt;p&gt;在代码实现中，我们可以在 &lt;code&gt;push_down&lt;/code&gt; 时进行子结点的创建（这样就能保证我们一定创建了所有可能走到的结点）。动态开点所需内存更大，注意多开空间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;template&amp;lt;class S, auto op, auto e, class F, auto mapping, auto composition, auto id&amp;gt;
class dynamicSegTree {
public:
    dynamicSegTree() : dynamicSegTree(0) {}
    // 注意这里尽量多开空间 不知道会申请多少结点
    explicit dynamicSegTree(int n, int m = (int)2e6 * 4 + 10) : _n(n) {
        data.reserve(m);
        lazy.reserve(m);
        t.reserve(m);
        t.push_back({ -1,-1,-1,-1 });
        lazy.push_back(id());
        data.push_back(e());
        nodes = 0;
        make_node(1, n);  // root
    }
    int make_node(int l, int r) {
        t.push_back({ l,r,-1,-1 });
        lazy.push_back(id());
        data.push_back(e());
        return ++nodes;
    }
    void push_up(int rt) {
        auto&amp;amp;&amp;amp; [l, r, lc, rc] = t[rt];
        data[rt] = op(data[lc], data[rc]);
    }
    void apply(int rt, F f) {
        data[rt] = mapping(f, data[rt]);
        lazy[rt] = composition(f, lazy[rt]);
    }
    void push_down(int rt) {
        auto&amp;amp;&amp;amp; [l, r, lc, rc] = t[rt];
        int mid = (l + r) / 2;
        if (lc == -1) {
            lc = make_node(l, mid);
        }
        if (rc == -1) {
            rc = make_node(mid + 1, r);
        }
        if (not (lazy[rt] == id())) {
            apply(lc, lazy[rt]);
            apply(rc, lazy[rt]);
        }
        lazy[rt] = id();
    }
    void range_modify(int rt, int ql, int qr, F f) {
        auto&amp;amp;&amp;amp; [l, r, lc, rc] = t[rt];
        if (r &amp;lt; ql or l &amp;gt; qr) {
            return;
        }
        if (ql &amp;lt;= l and r &amp;lt;= qr) {
            apply(rt, f);
            return;
        }
        push_down(rt);
        int mid = (l + r) / 2;
        range_modify(lc, ql, qr, f);
        range_modify(rc, ql, qr, f);
        push_up(rt);
    }
    S range_query(int rt, int ql, int qr) {
        auto&amp;amp;&amp;amp; [l, r, lc, rc] = t[rt];
        if (r &amp;lt; ql or l &amp;gt; qr) {
            return e();
        }
        if (ql &amp;lt;= l and r &amp;lt;= qr) {
            return data[rt];
        }
        push_down(rt);
        int mid = (l + r) / 2;
        auto&amp;amp;&amp;amp; res = op(range_query(lc, ql, qr), range_query(rc, ql, qr));
        push_up(rt);
        return res;
    }
private:
    int _n, nodes;
    vector&amp;lt;tuple&amp;lt;int, int, int, int&amp;gt;&amp;gt; t;  // (l, r, lc, rc)
    vector&amp;lt;S&amp;gt; data;
    vector&amp;lt;F&amp;gt; lazy;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;练习题&lt;/h1&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/4/practice/contest/280801/problem/A&quot;&gt;Assignment, Addition, and Sum&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;维护三种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;区间覆盖&lt;/li&gt;
&lt;li&gt;区间加&lt;/li&gt;
&lt;li&gt;区间求和&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实就是搞清楚区间加和区间覆盖的懒惰标记在下推时的顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct S {
    ll sum, size;
};
S op(S l, S r) {
    return { l.sum + r.sum,l.size + r.size };
}
S e() {
    return { 0,1 };
}
 
struct F {
    ll add, cover;
    bool operator == (const F&amp;amp; k) const { return add == k.add &amp;amp;&amp;amp; cover == k.cover; }
};
S mapping(F f, S x) {
    if (f.cover == -1) {  // 只有区间加时 f(x) += add * size
        return { x.sum + 1LL * x.size * f.add, x.size };
    } else {              // 既有区间覆盖又(可能)有区间加 f(x) = cover * size + add * size
        return { 1LL * x.size * (f.add + f.cover), x.size };
    }
}
F composition(F f, F g) {  // f(g(x))
    auto h = f;            // 如果新操作存在区间覆盖 那么旧操作直接作废
    if (f.cover == -1) {   // 新操作不存在区间覆盖 那就只更新区间加
        h.cover = g.cover;
        h.add = g.add + f.add;
    }
    return h;
}
F id() {
    return { 0,-1 };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/4/practice/contest/280801/problem/B&quot;&gt;Add Arithmetic Progression On Segment&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;维护两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;区间加等差数列&lt;/li&gt;
&lt;li&gt;单点求值&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为只要求单点求值，所以我们维护差分数组 $d_i=a_i-a_{i-1}$ 就行了，在差分数组上区间加等差数列就是普通的区间加公差+末尾单点修改。单点求值就是差分数组前缀和。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/4/practice/contest/280801/problem/C&quot;&gt;Painter&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;对于一个0-1数组维护以下两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;区间覆盖&lt;/li&gt;
&lt;li&gt;查询区间 $[L,R)$ 的区间和以及连续的1有几段。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于每个结点维护：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sum&lt;/code&gt;：区间和&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lb&lt;/code&gt;：左端点是否为1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rb&lt;/code&gt;：右端点是否为1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnt&lt;/code&gt;：区间中连续的1有几段&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;pushup时 &lt;code&gt;t[rt].cnt = t[lc].cnt + t[rc].cnt - (t[lc].rb == 1 &amp;amp;&amp;amp; t[rc].lb == 1)&lt;/code&gt;（当两端区间的交界处都为1时去重）。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/4/practice/contest/280801/problem/D&quot;&gt;Problem About Weighted Sum&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;维护两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;区间加&lt;/li&gt;
&lt;li&gt;区间阶梯查询，给定区间 $[l,r]$，查询 $a[l] \cdot 1 + a[l + 1] \cdot 2 + \dots \ a[r] \cdot (r - l + 1)$&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;维护两个数组：&lt;code&gt;a[1], a[2], a[3], ..., a[n]&lt;/code&gt; 和 &lt;code&gt;a[1], 2*a[2], 3*a[3], ..., n*a[n]&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;&lt;a href=&quot;https://codeforces.com/edu/course/2/lesson/5/4/practice/contest/280801/problem/E&quot;&gt;IOI2014 - Wall&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;维护两种操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;chmax l r x&lt;/code&gt;：将区间 $[l,r)$ 中的元素 $a_i$ 替换为 $\max(a_i,x)$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmin l r x&lt;/code&gt;：将区间 $[l,r)$ 中的元素 $a_i$ 替换为 $\min(a_i,x)$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完成所有操作后，询问最终的数组。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实不难发现，由于这道题并没有区间查询操作，所以 &lt;code&gt;push_up&lt;/code&gt; 是不需要的，因此幺半群 $S$ 也没有维护的必要，我们只需要维护区间修改集合 $F$ 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const int INF = 1e9 + 5;
struct S {
    int val, size;
};
S op(S l, S r) {
    return { l.val + r.val,l.size + r.size };
}
S e() {
    return { 0,1 };
}

struct F {
    int chmin, chmax;
    bool operator == (const F&amp;amp; k) const { return chmin == k.chmin &amp;amp;&amp;amp; chmax == k.chmax; }
};
S mapping(F f, S x) {
    if (x.size != 1) {  // 不是叶子结点就不用更新了
        return x;
    }
    x.val = max(x.val, f.chmax);
    x.val = min(x.val, f.chmin);
    return x;
}
F composition(F f, F g) {  // f(g(x))
    auto h = f;
    h.chmax = max(h.chmax, g.chmax);
    h.chmin = min(h.chmin, g.chmin);
    if (h.chmin &amp;lt; h.chmax) {  // 当两种操作产生矛盾时需要判断取左端点or右端点
        if (f.chmin &amp;lt; g.chmax) h.chmax = h.chmin;
        else h.chmin = h.chmax;
    }
    return h;
}
F id() {
    return { INF,-1 };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://vjudge.net/problem/DMOJ-ioi05p2&quot;&gt;IOI2005 - Mountain&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;对于一个差分数组 $d_i=a_i-a_{i-1}$ ，维护：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;I a b D&lt;/code&gt;：将区间 $[l,r]$ 中的 $d_i$ 修改为 $D$。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Q h&lt;/code&gt;：询问第一个大于 $h$ 的 $a_i$。&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;实际上就是需要维护前缀和最值，令前缀和最值为 &lt;code&gt;max_pre&lt;/code&gt;，则 &lt;code&gt;rt.max_pre = max(lc.max_pre, lc.sum + rc.max_pre)&lt;/code&gt;，在 &lt;code&gt;push_up&lt;/code&gt; 时更新即可。&lt;/p&gt;
&lt;p&gt;此外本题需要动态开点。&lt;/p&gt;
</content:encoded></item><item><title>傅里叶级数</title><link>https://fuwari.vercel.app/posts/fourier_series/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/fourier_series/</guid><description>傅里叶级数的推导与应用：系数确定、收敛定理、正弦/余弦级数及一般周期展开。</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;周期为 $2\pi$ 的傅里叶级数&lt;/h2&gt;
&lt;p&gt;傅里叶级数是一种利用三角函数近似周期函数的方法，本节将以周期为 $2\pi$ 的函数 $f(x)$ 为例，解析傅里叶级数是如何做到拟合的：
$$
f(x) = a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx
$$&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./index.assets/Fourier_series_square_wave_circles_animation.gif&quot; alt=&quot;描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;一个分别采用傅里叶级数的前 1, 2, 3, 4 项近似方波的可视化&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;h3&gt;前置定理&lt;/h3&gt;
&lt;p&gt;:::note[定理1]
$$
\begin{aligned}
\int_{0}^{2\pi}\sin mx \mathrm dx &amp;amp;= 0,\ m为任意整数\
\int_{0}^{2\pi}\cos mx\mathrm dx &amp;amp;=0,\ m为任意非零整数
\end{aligned}
$$
:::&lt;/p&gt;
&lt;p&gt;证明很显然，以 $\int_{0}^{2\pi}\sin mx \mathrm dx = 0$ 为例：
$$
\begin{aligned}
\int_{0}^{2\pi}\sin mx \mathrm dx &amp;amp;=\frac{1}{m}\int_{0}^{2\pi}\sin mx \mathrm d(mx)\
&amp;amp;=\frac{1}{m}\cdot -\cos mx\bigg|_{0}^{2\pi}\
&amp;amp;=0
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;:::note[定理2]
$$
\int_{0}^{2\pi}\sin mx\cos nx\mathrm dx = 0,\ m,n为任意整数
$$
:::&lt;/p&gt;
&lt;p&gt;证明主要利用积化和差公式：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\int_{0}^{2\pi}\sin mx\cos nx\mathrm dx &amp;amp;= \int_{0}^{2\pi}\frac{1}{2}[\sin(mx+nx)+\sin(mx-nx)]\mathrm{d}x\
&amp;amp;=\frac{1}{2}\int_{0}^{2\pi}\sin(m+n)x+\sin(m-n)x\
&amp;amp;=0
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;:::note[定理3]
$$
\int_{0}^{2\pi}\sin mx\sin nx \mathrm dx= \begin{cases}\pi &amp;amp; m=\pm n, m\neq 0\0 &amp;amp; \text{otherwise}\end{cases}
$$
这里 $m,n$ 都是整数。
:::&lt;/p&gt;
&lt;p&gt;首先证明 $m=n$ 的情况：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\int_{0}^{2\pi}\sin mx\sin nx \mathrm dx&amp;amp;= \int_{0}^{2\pi}\sin^2mx\mathrm dx\
&amp;amp;=\int_0^{2\pi}-\frac{1}{2}[\cos 2mx - \cos 0x]\mathrm dx\
&amp;amp;=\frac{1}{2}\int_0^{2\pi}1\mathrm dx\
&amp;amp;=\pi
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;然后 $m=-n$ 的情况是显然的，因此证明剩余部分：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\int_{0}^{2\pi}\sin mx\sin nx \mathrm dx&amp;amp;= \int_{0}^{2\pi}-\frac 1 2[\cos(m+n)x - \cos(m-n)x]\mathrm dx\
&amp;amp;= 0
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;同理，还存在对偶情况&lt;/p&gt;
&lt;p&gt;$$
\int_{0}^{2\pi}\cos mx\cos nx \mathrm dx= \begin{cases}\pi &amp;amp; m=\pm n ,m\neq 0\0 &amp;amp; \text{otherwise}\end{cases}
$$&lt;/p&gt;
&lt;p&gt;证明略。&lt;/p&gt;
&lt;h3&gt;确定系数&lt;/h3&gt;
&lt;p&gt;有了以上的前置定理之后，我们就可以开始确定傅里叶级数的系数 $a_i,b_j$ 了。&lt;/p&gt;
&lt;h4&gt;确定 $a_0$&lt;/h4&gt;
&lt;p&gt;确定系数的思路就是对等式 $f(x) = a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx$ 两边积分，如下&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
f(x) &amp;amp;= a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx\
\int_0^{2\pi}f(x)\mathrm dx&amp;amp;=\int_0^{2\pi}\bigg[a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx \bigg]\mathrm dx\
\int_0^{2\pi}f(x)\mathrm dx&amp;amp;=2\pi a_0 + \sum_{i=1}^{\infty}a_i\int_0^{2\pi}\cos ix\mathrm dx + \sum_{j=1}^{\infty}\int_0^{2\pi}\sin jx\mathrm dx\
\int_0^{2\pi}f(x)\mathrm dx&amp;amp;=2\pi a_0\
a_0&amp;amp;=\frac{1}{2\pi}\int_0^{2\pi}f(x)\mathrm dx
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;容易发现，$a_0$ 的几何意义就是 $f(x)$ 在一个周期上的平均值。&lt;/p&gt;
&lt;h4&gt;确定 $a_i$&lt;/h4&gt;
&lt;p&gt;这里的做法也是等式两边积分，但是应用了一个技巧：两边同乘 $\cos cx$，这样就能消去除了 $a_c$ 以外的项的干扰。&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
f(x) &amp;amp;= a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx\
\int_0^{2\pi}f(x)\cos cx\mathrm dx&amp;amp;=\int_0^{2\pi}\bigg[a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx \bigg]\cos cx\mathrm dx\quad(c是1,2,\cdots,n的常数)\
\int_0^{2\pi}f(x)\cos cx\mathrm dx&amp;amp;= \int_{0}^{2\pi}a_c\cos cx\cos cx\mathrm dx\
\int_0^{2\pi}f(x)\cos cx\mathrm dx&amp;amp;= \pi a_c\
a_c&amp;amp;=\frac{1}{\pi}\int_0^{2\pi}f(x)\cos cx\mathrm dx
\end{aligned}
$$&lt;/p&gt;
&lt;h4&gt;确定 $b_i$&lt;/h4&gt;
&lt;p&gt;和 $a_i$ 做法类似，直接给出结论：&lt;/p&gt;
&lt;p&gt;$$
b_c=\frac{1}{\pi}\int_0^{2\pi}f(x)\sin cx\mathrm dx
$$&lt;/p&gt;
&lt;h3&gt;函数拟合的示例&lt;/h3&gt;
&lt;p&gt;以一个周期为 $2\pi$ 的方波为例，构造傅里叶级数。具体参数如下图所示（波峰为 $3$，波谷为 $0$）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;index.assets/fourierseries1-1024x317.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们逐个确定傅里叶级数的系数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;首先确定 $a_0$，根据 $a_0$ 的几何意义（$f(x)$ 在一个周期上的平均值），直接就能计算出 $a_0=\frac{3}{2}$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后确定 $a_i(i\gt 0)$：
$$
\begin{aligned}
a_i&amp;amp;=\frac {1}{\pi}\int_{0}^{2\pi}f(x)\cos ix\mathrm dx\
&amp;amp;=\frac{1}{\pi}\int_{0}^{\pi}3\cos ix\mathrm dx\
&amp;amp;=\frac{3}{i\pi}\sin ix\bigg|_0^{\pi}\
&amp;amp;=0\
\end{aligned}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后确定 $b_i$：
$$
\begin{aligned}b_i&amp;amp;=\frac {1}{\pi}\int_{0}^{2\pi}f(x)\sin ix\mathrm dx\&amp;amp;=\frac {1}{\pi}\int_{0}^{\pi}3\sin ix\mathrm dx\&amp;amp;=\frac {-3}{i\pi}\cos ix\bigg|_0^{\pi}\&amp;amp;=\frac {-3}{i\pi}(\cos i\pi - 1)\&amp;amp;= \frac{3(1-\cos i\pi)}{i\pi}\end{aligned}
$$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上，我们就能写出这个方波的傅里叶级数了：
$$
b_i=
\begin{cases}
0 &amp;amp; \text{i is even}\
\frac{6}{i\pi} &amp;amp; \text{i is odd}
\end{cases}
$$&lt;/p&gt;
&lt;h3&gt;收敛定理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;当 $t$ 是 $f(x)$ 的连续点时，级数收敛于 $f(t)$。&lt;/li&gt;
&lt;li&gt;当 $t$ 是 $f(x)$ 的第一类间断点时，级数收敛于 $\frac{f(t^+)+f(t^-)}{2}$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;*收敛定理主要的用处是说明第一类间断点在傅里叶级数中的数值。&lt;/p&gt;
&lt;p&gt;:::tip[例题1]
设 $f(x)$ 是周期为 $2$ 的周期函数，它在区间 $(-1,1]$ 上定义为 $f(x)=\begin{cases} 2 &amp;amp; -1\lt x\le 0\ x^3 &amp;amp; 0\lt x\le 1 \end{cases}$，则 $f(x)$ 的傅里叶级数在 $x=1$ 处收敛于（）。
:::&lt;/p&gt;
&lt;p&gt;根据收敛定理，显然收敛于 $\frac{1}{2}[2+1]=\frac 3 2$。&lt;/p&gt;
&lt;h3&gt;正弦级数、余弦级数&lt;/h3&gt;
&lt;p&gt;傅里叶级数 $f(x) = a_0 + \sum_{i=1}^{\infty} a_i\cos ix + \sum_{j=1}^{\infty}b_j\sin jx$ 中虽然既有余弦也有正弦，但是在某些特殊情况下，傅里叶级数会只留下余弦或正弦（比如前文中的方波示例）。这里，实际上有一个比较实用的性质：奇函数的傅里叶级数是正弦级数（只含有正弦项），偶函数的傅里叶级数是余弦级数（只含有余弦项）。&lt;/p&gt;
&lt;p&gt;上文中的方波例子是一个“广义”的奇函数。&lt;/p&gt;
&lt;p&gt;证明实际上比较显然，我们以奇函数为例进行一个简要的说明，设 $f(x)$ 是一个周期为 $2\pi$ 的奇函数，那么&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
a_c&amp;amp;=\frac 1 \pi\int_{-\pi}^{+\pi}f(x)\cos cx\mathrm dx\
b_c&amp;amp;=\frac 1 \pi\int_{-\pi}^{+\pi}f(x)\sin cx\mathrm dx
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;因为 $f(x)$ 是奇函数，$\cos cx$ 是偶函数，所以 $f(x)\cos cx$ 是奇函数，即 $a_c=0$。&lt;/p&gt;
&lt;p&gt;:::tip[例题2]
设 $f(x)$ 是周期为 $2\pi$ 的周期函数，它在 $[-\pi,\pi)$ 上的表达式为 $f(x)=|x|$，将 $f(x)$ 展开成傅里叶级数。
:::&lt;/p&gt;
&lt;p&gt;$f(x)$ 显然是偶函数，因此其傅里叶级数只含有余弦项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先求 $a_0$：
$$a_0 = \frac{1}{2\pi}\int_{-\pi}^{+\pi}|x|\mathrm dx = \frac{\pi}{2}$$&lt;/li&gt;
&lt;li&gt;再求 $a_n(n\gt 0)$：
$$
\begin{aligned}a_n &amp;amp;= \frac{2}{\pi}\int_0^{\pi}x\cos nx \mathrm dx\&amp;amp;= \frac{2}{\pi} \frac{nx\sin (nx) + \cos(nx)}{n^2} \bigg|_0^\pi\&amp;amp;= \frac{2(\cos n\pi - 1)}{\pi n^2}\&amp;amp;= \begin{cases}-\frac{4}{\pi n^2} &amp;amp; n=1,3,5,\cdots\0 &amp;amp; n=2,4,6,\cdots\end{cases}\end{aligned}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是&lt;/p&gt;
&lt;p&gt;$$
f(x) = \frac \pi 2 - \frac 4 \pi \sum_{k=1}^{\infty}\frac{\cos(2k-1)x}{(2k-1)^2}\quad(-\infty\lt x\lt +\infty)
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此时，如果我们令 $x=0$，则有&lt;/p&gt;
&lt;p&gt;$$
f(0) = 0 = \frac{\pi}{2} - \frac{4}{\pi}\sum_{k=1}^{\infty}\frac{\cos 0}{(2k-1)^2}
$$&lt;/p&gt;
&lt;p&gt;可以求出无穷级数 $\sum_{k=1}^{+\infty}\frac{1}{(2k-1)^2}$ 的极限是 $\frac{\pi^2}{8}$。这是傅里叶级数在无穷级数中的一种应用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;一般周期的傅里叶级数&lt;/h2&gt;
&lt;p&gt;这一节我们研究更加一般的傅里叶级数，也就是对于任意周期函数构造傅里叶级数。&lt;/p&gt;
&lt;p&gt;思路实际上很简单，我们直接放缩 $x$ 轴坐标即可。对于周期为 $2\pi$ 的函数，我们构造的三角函数是 $\cos nx, \sin nx$，这是因为这一系列三角函数都具有 $2\pi$ 的周期；那么对于周期为 $2l$ 的函数，我们只需要构造一系列具有周期为 $2l$ 的三角函数即可，也就是 $\cos \frac{n\pi x}{l}, \sin\frac{n\pi x}{l}$。即&lt;/p&gt;
&lt;p&gt;$$
f(x) = a_0 + \sum_{i=1}^{\infty}a_i \cos \frac{i\pi x}{l} + \sum_{j=1}^{\infty} b_j\sin \frac{j\pi x}{l}
$$&lt;/p&gt;
&lt;h3&gt;确定系数&lt;/h3&gt;
&lt;p&gt;一般周期的傅里叶级数确定系数的方法本质上和 $2\pi$ 周期的一样，也就是多了一步 $x$ 轴的缩放。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确定 $a_0$：
$$
a_0 = \frac{1}{2l}\int_0^{2l}f(x)\mathrm dx
$$&lt;/li&gt;
&lt;li&gt;确定 $a_i(i\gt 0)$：
$$
a_i = \frac{1}{l}\int_0^{2l}f(x)\cos\frac{i\pi x}{l} \mathrm dx
$$&lt;/li&gt;
&lt;li&gt;确定 $b_j$：
$$
b_j = \frac{1}{l}\int_0^{2l}f(x)\sin\frac{j\pi x}{l} \mathrm dx
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;例题&lt;/h3&gt;
&lt;p&gt;:::tip[例题3]
设函数 $f(x)=x^2,0\le x\lt 1$，而 $S(x)=\sum_{n=1}^{\infty}b_n\sin n\pi x,-\infty\lt x\lt +\infty$。其中 $b_n=2\int_0^1f(x)\sin n\pi x\mathrm dx,n=1,2,3,\cdots$，则 $S(-\frac 1 2)=$（）。
:::&lt;/p&gt;
&lt;p&gt;看到 $S(x)$ 这个无穷级数的形式就容易联想到傅里叶级数，并且更特殊的是，这是一个正弦级数，于是我们可以认为这个正弦级数的原函数是一个奇函数。&lt;/p&gt;
&lt;p&gt;然后观察系数 $b_n$ 的形式，容易猜测：原函数就是 $f(x)$ 作奇延拓，即 $F(x)=\begin{cases}x^2 &amp;amp; 1\gt x\ge 0\ -x^2 &amp;amp; 0\ge x\gt -1\end{cases}$。此时延拓得到的函数 $F(x)$ 的系数 $b_n$ 就是&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
b_n &amp;amp;=  \frac{1}{l}\int_{-l}^{+l}f(x)\sin\frac{n\pi x}{l} \mathrm dx\
&amp;amp;= 2\int_0^1 f(x)\sin n\pi x \mathrm dx
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;因此，根据收敛定理可得 $S(-\frac 1 2) = -\frac 1 4$。&lt;/p&gt;
&lt;p&gt;:::tip[例题4]
将函数 $f(x)=2+|x|(-1\le x\le 1)$ 展开成以 $2$ 为周期的傅里叶级数，并由此求级数 $\sum_{n=1}^{\infty}\frac{1}{n^2}$ 的和。
:::&lt;/p&gt;
&lt;p&gt;先求傅里叶级数，因为 $f(x)$ 是一个偶函数，所以这是一个余弦级数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;求 $a_0$：
$$
a_0 = \frac{1}{2l}\int_{-l}^{+l}f(x)\mathrm dx = \frac{5}{2}
$$&lt;/li&gt;
&lt;li&gt;求 $a_n(n\gt 0)$：
$$
\begin{aligned}
a_n &amp;amp;= \frac{1}{l}\int_{-l}^{+l}f(x)\cos\frac{n\pi x}{l} \mathrm dx\
&amp;amp;= 2\int_0^1 (2+x)\cos n\pi x\mathrm dx\
&amp;amp;= \frac{2}{n^2\pi^2}[\cos(n\pi)-1]\
&amp;amp;= \begin{cases}
0 &amp;amp; n=2k\
\frac{-4}{(2k-1)^2\pi^2} &amp;amp; n=2k-1
\end{cases}
\end{aligned}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是&lt;/p&gt;
&lt;p&gt;$$
f(x)=\frac 5 2 - \frac{4}{\pi^2}\sum_{k=1}^{\infty}\frac{\cos(2k-1)\pi x}{(2k-1)^2}
$$&lt;/p&gt;
&lt;p&gt;令 $x=0$，则&lt;/p&gt;
&lt;p&gt;$$
f(0)=2=\frac 5 2 - \frac{4}{\pi^2}\sum_{k=1}^{\infty}\frac{\cos(2k-1)\pi x}{(2k-1)^2}
$$&lt;/p&gt;
&lt;p&gt;因此 $\sum_{k=1}^{\infty}\frac{1}{(2k-1)^2}=\frac{\pi^2}{8}$。
$$
\begin{aligned}
\sum_{k=1}^{\infty}\frac{1}{(2k)^2}+\sum_{k=1}^{\infty}\frac{1}{(2k-1)^2} &amp;amp;= \sum_{k=1}^{\infty}\frac{1}{k^2}\
\frac 1 4 \sum_{k=1}^{\infty}\frac{1}{k^2}+\sum_{k=1}^{\infty}\frac{1}{(2k-1)^2} &amp;amp;= \sum_{k=1}^{\infty}\frac{1}{k^2}\
\frac 3 4\sum_{k=1}^{\infty}\frac{1}{k^2} &amp;amp;= \frac{\pi^2}{8}\
\sum_{k=1}^{\infty}\frac{1}{k^2} &amp;amp;= \frac{\pi^2}{6}
\end{aligned}
$$&lt;/p&gt;
</content:encoded></item><item><title>傅里叶变换</title><link>https://fuwari.vercel.app/posts/fourier_transform/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/fourier_transform/</guid><description>从傅里叶级数的复数形式推导傅里叶变换，并介绍离散傅里叶变换（DFT）与FFT的基本思想。</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;傅里叶级数的复数形式&lt;/h2&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;p&gt;在傅里叶级数章节中，我们已知一个周期为 $2l$ 的周期函数可以展开为
$$
f(x) = a_0 + \sum_{i=1}^{\infty}a_i \cos \frac{i\pi x}{l} + \sum_{i=1}^{\infty} b_i\sin \frac{i\pi x}{l}
$$
为了方便起见，我们作如下定义：常数项 $a_0$ 记作 $\frac{a_0}{2}$，设周期 $T=2l$，记 $\omega = \frac{2\pi}{T}$，则有
$$
f(t) = \frac{a_0}{2} + \sum_{n=1}^{\infty}a_n\cos n\omega t + \sum_{n=1}^{\infty}b_n\sin n\omega t
$$
根据欧拉公式 $e^{i\theta} = \cos\theta + i\sin\theta$ 的推论 $\begin{cases}\cos\theta = \frac{1}{2}(e^{i\theta} + e^{-i\theta})\ \sin\theta = -\frac{1}{2}i(e^{i\theta}-e^{-i\theta})\end{cases}$，代入傅里叶级数的表达式中就有
$$
\begin{aligned}
f(t) &amp;amp;= \frac{a_0}{2} + \sum_{n=1}^{\infty} \bigg[\frac{a_n}{2}(e^{in\omega t}+e^{-in\omega t}) - \frac{ib_n}{2}(e^{in\omega t}-e^{-in\omega t})\bigg]\
&amp;amp;= \frac{a_0}{2} + \sum_{n=1}^{\infty} \bigg[\frac{a_n-ib_n}{2}e^{in\omega t} + \frac{a_n+ib_n}{2}e^{-in\omega t}\bigg]\
&amp;amp;= \frac{a_0}{2} + \sum_{n=1}^{\infty}\frac{a_n-ib_n}{2}e^{in\omega t} + \sum_{n=1}^{\infty}\frac{a_n+ib_n}{2}e^{-in\omega t}\
&amp;amp;= \sum_{n=0}^0\frac{a_0}{2}e^{in\omega t} + \sum_{n=1}^{\infty}\frac{a_n-ib_n}{2}e^{in\omega t} + \sum_{n=-\infty}^{-1}\frac{a_{-n}+ib_{-n}}{2}e^{in\omega t}\quad(令n为-n)\
&amp;amp;= \sum_{n=-\infty}^{\infty} c_ne^{in\omega t}
\end{aligned}
$$
这里，$c_n = \begin{cases}\frac{a_0}{2} &amp;amp; n=0\ \frac{a_n-ib_n}{2} &amp;amp; n\gt 0\ \frac{a_{-n}+ib_{-n}}{2} &amp;amp; n\lt 0\end{cases}$，根据常数项 $a_n,b_n$ 在傅里叶级数中的公式可知 $\begin{cases}a_0 = \frac{2}{T}\int_0^T f(t)\mathrm dt\ a_n = \frac{2}{T}\int_0^Tf(t)\cos n\omega t\mathrm dt\ b_n=\frac{2}{T}\int_0^T f(t)\sin n\omega t\mathrm dt\end{cases}$，代入 $c_n$ 的表达式就有
$$
c_n = \begin{cases}
\begin{aligned}
\frac{a_0}{2} &amp;amp;= \frac{1}{2}\cdot \frac{2}{T}\int_0^Tf(t)\mathrm dt\
&amp;amp;= \frac{1}{T}\int_0^Tf(t)\mathrm dt
\end{aligned}&amp;amp; n=0\
\
\begin{aligned}
\frac{a_n-ib_n}{2} &amp;amp;= \frac{1}{2}\cdot\bigg(\frac{2}{T}\int_0^T f(t)\cos n\omega t\mathrm dt - i\frac{2}{T}\int_0^Tf(t)\sin n\omega t \mathrm dt\bigg)\
&amp;amp;= \frac{1}{T}\int_0^Tf(t)(\cos n\omega t - i\sin n\omega t)\mathrm dt\
&amp;amp;= \frac{1}{T}\int_0^Tf(t)(\cos (-n\omega t) + i\sin (-n\omega t))\mathrm dt\quad(奇偶性)\
&amp;amp;= \frac{1}{T}\int_0^Tf(t)e^{-in\omega t}\mathrm dt \quad(欧拉公式)\
\end{aligned} &amp;amp; n\gt 0\
\
\begin{aligned}
\frac{a_{-n}+ib_{-n}}{2} &amp;amp;= \frac 12\cdot\bigg(\frac 2 T \int_0^T f(t)\cos(-n\omega t)\mathrm dt + i\frac 2T \int_0^T f(t)\sin(-n\omega t)\mathrm dt\bigg)\
&amp;amp;= \frac 1T \int_0^T f(t)e^{-in\omega t}\mathrm dt\
\end{aligned} &amp;amp; n\lt 0
\end{cases}
$$
注意到 $n\gt 0,n\lt 0$ 两种情况的积分是完全一致的，所以
$$
c_n=\begin{cases}
\frac{1}{T}\int_0^Tf(t)\mathrm dt &amp;amp; n=0\
\frac{1}{T}\int_0^Tf(t)e^{-in\omega t}\mathrm dt &amp;amp; n\neq0\
\end{cases} = \frac{1}{T}\int_0^Tf(t)e^{-in\omega t}\mathrm dt
$$&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;根据上方推导，我们可以得出结论：对于一个周期为 $T$ 的复变函数 $f(t)$，可以展开为傅里叶级数
$$
f(t) = \sum_{n=-\infty}^{\infty} c_ne^{in\omega t}
$$
其中
$$
c_n=\frac{1}{T}\int_0^Tf(t)e^{-in\omega t}\mathrm dt
$$&lt;/p&gt;
&lt;h2&gt;傅里叶变换&lt;/h2&gt;
&lt;h3&gt;傅里叶变换&lt;/h3&gt;
&lt;p&gt;上文已经得出了傅里叶级数的复数形式，即一个周期为 $T$ 的函数 $f_T(t)$可以展开为
$$
\begin{aligned}
f(t) &amp;amp;= \sum_{n=-\infty}^{\infty} c_n e^{in\omega t}\
&amp;amp;= \sum_{n=-\infty}^{\infty} (\frac{1}{T}\int_{-\frac T 2}^{\frac T 2}f(t)e^{-in\omega t}\mathrm dt) e^{in\omega t}\
\end{aligned}
$$
对于这个式子，我们可以将 $n\omega$ 视作一个整体，也就是说傅里叶级数实际上是在枚举不同频率 $\cdots,1\omega,2\omega,\cdots,n \omega,\cdots$。可以想象一个数轴，该数轴上的点均为 $n\omega$，每个点的值对应一个复数 $c_n$。根据该思想，记 $\omega_0 = \frac{2\pi}{T}$（这个参数也被称为是基频率），那么傅里叶级数就是在枚举基频率 $\cdots,1\omega_0,2\omega_0,\cdots,n \omega_0,\cdots$，于是傅里叶级数就可以用基频率表示为
$$
\begin{aligned}
f(t) &amp;amp;= \sum_{n=-\infty}^{\infty} c_n e^{in\omega_0 t}\
&amp;amp;= \sum_{n=-\infty}^{\infty} (\frac{1}{T}\int_{-\frac T 2}^{\frac T 2}f(t)e^{-in\omega_0 t}\mathrm dt) e^{in\omega_0 t}\
\end{aligned}
$$
对于一个非周期函数 $f(t)$，我们能否也将其展开为傅里叶级数呢？答案就是傅里叶变换，我们可以将这个非周期函数 $f(t)$ 视作一个周期为 $\infty$ 的“周期函数”。当 $T\rightarrow\infty$ 时，$\omega_0\rightarrow 0$，此时对于基频率 $\omega_0$ 的枚举就从一个离散函数变成了一个连续函数。&lt;/p&gt;
&lt;p&gt;记 $\Delta\omega = (n+1)\omega_0 - n\omega_0 = \omega_0 = \frac{2\pi}{T}$，$\omega = n\omega_0$ 则
$$
\begin{aligned}
f(t) &amp;amp;= \sum_{n=-\infty}^{\infty} (\frac{1}{T}\int_{-\frac T 2}^{\frac T 2}f(t)e^{-in\omega_0 t}\mathrm dt) e^{in\omega_0 t}\
&amp;amp;= \sum_{n=-\infty}^{\infty}\frac{\Delta\omega}{2\pi}\int_{-\frac T 2}^{\frac T 2}f(t)e^{-i\omega t}\mathrm dt\cdot e^{i\omega t}\
&amp;amp;= \frac{1}{2\pi}\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}f(t)e^{-i\omega t}\mathrm dt\mathrm\cdot e^{i\omega t} d\omega\quad(T\rightarrow \infty,\omega_0\rightarrow 0)
\end{aligned}
$$
这里可以类比定积分的几何意义（在区间 $[a,b]$ 内取 $n-1$ 个分点 $a=x_0\lt x_1\lt \cdots\lt x_{n}=b$，记 $\Delta x_i = x_i-x_{i-1}$，$\xi_i\in(x_{i-1},x_i)$，则 $\int_a^b f(x)\mathrm dx = \lim_{n\rightarrow\infty}\sum_{i=1}^n f(\xi_i)\Delta x_i$），只不过上下限都变成了无穷（不定积分）。&lt;/p&gt;
&lt;p&gt;记 $F(\omega)=\int_{-\infty}^{\infty}f(t)e^{-i\omega t}\mathrm dt$，该式就是&lt;strong&gt;傅里叶变换&lt;/strong&gt;，而 $f(t) = \frac{1}{2\pi}\int_{-\infty}^{\infty}F(\omega)e^{i\omega t}\mathrm d\omega$ 就是&lt;strong&gt;逆傅里叶变换&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;离散傅里叶变换（DFT）&lt;/h3&gt;
&lt;p&gt;在实际应用中，我们很难做到对连续函数做傅里叶变换，但是可以对函数进行采样，然后做离散傅里叶变换，公式为
$$
X_k = \sum_{n=0}^{N-1}x_ne^{-i \frac{2\pi }{N}nk}
$$
假设我们已知序列 $x_0,x_1,\cdots,x_{N-1}$ 的值，那我们根据离散傅里叶变换公式就能求出对应的 $X_0,X_1,\cdots,X_{N-1}$ 了。这里 $N$ 是所分析函数/信号的长度（采样区间），$x_0,x_1,\cdots,x_{N-1}$ 可以视作是我们对连续信号的离散采样。&lt;/p&gt;
&lt;p&gt;同理存在离散傅里叶逆变换
$$
x_k = \frac 1N \sum_{n=0}^{N-1}X_ne^{i \frac{2\pi}{N}nk}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;对于离散傅里叶变换，如果用 $N$ 阶单位根（$W_{N,k}=e^{-i\frac{2\pi}{N}k}$）代入，则
$$
X_k=\sum_{n=0}^{N-1}x_n W_{N,k}^{n}
$$
实际上，这就是在求一个多项式 $f(t)=x_0+x_1t+x_2t^2+\cdots+x_{N-1}t^{N-1}$ 在 $N$ 阶单位根构成的群上的点值表示，这个问题可以用Cooley–Tukey算法等优化至 $O(N\log N)$ 的复杂度，也就是快速傅里叶变换（FFT）。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>有理多边形格点计数</title><link>https://fuwari.vercel.app/posts/lattice_point_counting_in_rational_polygon/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/lattice_point_counting_in_rational_polygon/</guid><description>从线段格点计数出发，基于 exgcd 和取模求和，推导有理多边形内格点数的 O(n log n) 计算算法。</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文原始论文：&lt;a href=&quot;https://dominoweb.draco.res.ibm.com/998d6b527637a012852572730025e777.html&quot;&gt;链接&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;符号定义&lt;/h2&gt;
&lt;p&gt;$\lfloor x\rfloor$ 表示小于等于 $x$ 的最大整数；${x}=x-\lfloor x\rfloor$，即 $x$ 的小数部分；$%$ 表示取模。&lt;/p&gt;
&lt;h2&gt;线段上的格点数&lt;/h2&gt;
&lt;p&gt;如果你熟悉exgcd和线性同余方程，那么可以直接跳过本节。&lt;/p&gt;
&lt;h3&gt;定义1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $x_1,y_1,x_2,y_2$ 为有理数，定义 $L(x_1,y_1,x_2,y_2)$ 是线段 $(x_1,y_1),(x_2,y_2)$ 上的格点个数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;引理1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;给定非负整数 $a,b$，则可以通过exgcd计算出方程 $ax+by=\gcd(a,b)$ 的一组可行解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;引理2&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;根据裴蜀定理，线性同余方程 $ax+by=c$ 有解当且仅当 $c% \gcd(a,b)=0$。此外，若 $(x_0,y_0)$ 是该方程的一组可行解，则该方程的通解为
$$
x=x_0+\frac{b}{d}k,\ y=y_0-\frac{a}{d}k\quad(k=\cdots,-2,-1,0,1,2,\cdots)
$$
这里 $d=\gcd(a,b)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;定理1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;$L(x_1,y_1,x_2,y_2)$ 可以以 $O(\max{\log |x_1|, \log |y_1|, \log |x_2|, \log |y_2|})$ 的复杂度计算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;证明如下：&lt;/p&gt;
&lt;p&gt;不失一般性，我们假设 $0\le x_1\le x_2, y_1\ge y_2\ge 0$（对于 $x_2\le x_1\lt 0, y_1\ge y_2\gt0$ 的情况，我们令 $(x_1,y_1),(x_2,y_2)$ 为 $(-x_1,y_1),(-x_2,y_2)$ 即可），因为 $x_1,y_1,x_2,y_2$ 都是有理数，所以必定存在直线 $ax+by=c$ 穿过点 $(x_1,y_1),(x_2,y_2)$，且 $a,b,c$ 都是非负整数。&lt;/p&gt;
&lt;p&gt;考虑特殊情况 $a=0$ 或 $b=0$，这是trivial的，我们不展开讨论。此外，根据引理1，我们可以通过exgcd计算出方程 $ax+by=d$（$d=\gcd(a,b)$）的一组可行解 $(p,q)$，也就是 $ap+bq=d$。因为 $(x,y)=(\frac{cp}{d},\frac{cq}{d})$ 满足方程 $ax+by=c$，根据引理2可知方程 $ax+by=c$ 的通解为
$$
x=\frac{c}{d}p + \frac{b}{d}k,\ y=\frac{c}{d}q-\frac{a}{d}k\quad(k=\cdots,-2,-1,0,1,2,\cdots)
$$
因为 $x_1\le x\le x_2$，所以以下不等式必须成立：
$$
\begin{aligned}
x_1\le \frac{c}{d}p + \frac{b}{d}k\le x_2 &amp;amp;\Leftrightarrow \frac{dx_1-cp}{b}\le k\le \frac{dx_2-cp}{b}\
&amp;amp;\Leftrightarrow \bigg\lceil\frac{dx_1-cp}{b}\bigg\rceil\le k\le \bigg\lfloor\frac{dx_2-cp}{b}\bigg\rfloor
\end{aligned}
$$
因此
$$
L(x_1,y_1,x_2,y_2) = \bigg\lfloor\frac{dx_2-cp}{b}\bigg\rfloor - \bigg\lceil\frac{dx_1-cp}{b}\bigg\rceil + 1
$$&lt;/p&gt;
&lt;h2&gt;特殊直角三角形内的格点数&lt;/h2&gt;
&lt;h3&gt;定义2&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这里我们定义的直角三角形 $T(a,b,c)$ 为直线 $ax+by=c$ 与坐标轴交出的三角形，即
$$
T(a,b,c) = {(x,y)\in\mathbb R^2\ |\ ax+by\le c, x\gt 0, y\gt 0}
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;定义3&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $a,b,c$ 都是正整数，定义 $N(a,b,c)$ 为三角形 $T(a,b,c)$ 内部的格点数。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;引理3&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $a,b,c$ 都是正整数，则 $N(a,b,c)=N(b,a,c)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;证明：根据对称性，显然。&lt;/p&gt;
&lt;h3&gt;引理4&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $a,c$ 都是正整数，则 $N(a,a,c)=\lfloor c/a\rfloor (\lfloor c/a\rfloor-1)/2$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;证明：我们枚举每一个整数 $x$ 坐标，则 $N(a,a,c) = (\lfloor c/a\rfloor-1) + (\lfloor c/a\rfloor - 2) + \cdots+ 1$。&lt;/p&gt;
&lt;h3&gt;引理5&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $a,b,c$ 都是正整数，且 $a\gt b$；令 $m = \lfloor c/a \rfloor, h=(c-am)/b, k=\lfloor(a-1)/b\rfloor, c^\prime=c-b(km+\lfloor h\rfloor)$，则以下递推方程成立：
$$
N(a,b,c) = N(a-bk,b,c^\prime) + km(m-1)/2 + m\lfloor h\rfloor
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;证明：我们首先尝试用 $N(a,b,c)$ 来表示 $T(a,b,c)$ 的面积。我们不妨认为 $T(a,b,c)$ 内部的每一个格点都代表了它左下方的单位正方形，那么 $T(a,b,c)$ 内部的所有完整单位正方形就等于其内部的格点数，即 $N(a,b,c)$。然后，我们再将 $T(a,b,c)$ 去除掉单位正方形的剩余部分以 $x=i,i\in N^+$ 划分为 $m$ 个梯形 $S_i$ 和一个三角形 $R$，如下图1所示&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./index.assets/image-20240923185008279.png&quot; alt=&quot;描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;图1. 三角形 T(a,b,c) 的分解&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;然后梯形 $S_i$ 的面积就能用 &lt;code&gt;(上底 + 下底) * 高 / 2&lt;/code&gt; 的公式表达为
$$
\begin{aligned}
|S_i| &amp;amp;= \frac 12\bigg[\bigg(\frac{c-a(i-1)}{b} - \bigg\lfloor \frac{c-ai}{b}\bigg\rfloor \bigg) + \bigg(\frac{c-ai}{b} - \bigg\lfloor \frac{c-ai}{b}\bigg\rfloor \bigg)\bigg]\
&amp;amp;= \frac 12 \bigg(\frac ab + 2\bigg{ \frac{c-ai}{b}\bigg}\bigg)
\end{aligned}
$$
如下图2所示&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./index.assets/image-20240923185415466.png&quot; alt=&quot;描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;图2. 区域Si&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;三角形 $R$ 的面积则可以表示为（利用直线 $ax+by=c$ 必定经过点 $(m,h)$ 的性质）
$$
|R| = \frac 12 \bigg(\frac ca - \bigg\lfloor \frac ca \bigg\rfloor\bigg)\frac{c-am}{b} = \frac 12 \bigg(\frac ca - m\bigg)h
$$
综上，我们得出等式
$$
\begin{aligned}
N(a,b,c) &amp;amp;= |T(a,b,c)| - \sum_{i=1}^m|S_i| - |R|\
&amp;amp;= \frac{c^2}{2ab} - \frac 12 \bigg(\frac ca - m\bigg)h - \sum_{i=1}^m \frac 12 \bigg(\frac ab + 2\bigg{ \frac{c-ai}{b}\bigg}\bigg)\
&amp;amp;= \frac{cm}{2b} + \frac{hm}{2} - \frac 12\sum_{i=1}^m \bigg(\frac ab + 2\bigg{ \frac{c-ai}{b}\bigg}\bigg)
\end{aligned}
$$
根据上方的符号定义可知，直线 $ax+by=c$ 必定经过点 $(m,h)$ 和 $(0,c/b)$；我们不难验证直线 $(a-bk)x+by=c^\prime$ 必定经过点 $(m,{h})$ 和 $(0,c^\prime/b)$。&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./index.assets/image-20240923190818865.png&quot; alt=&quot;描述&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;图3. 直线 ax+by=c 和 (a-bk)x+by=c&apos;&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;p&gt;并且很容易证明 $a-bk\gt 0$，即 $a-b\lfloor(a-1)/b\rfloor\gt 0$。&lt;/p&gt;
&lt;p&gt;此时，我们将 $N(a-bk,b,c^\prime)$ 带入公式可得
$$
N(a-bk,b,c^\prime) = \frac{c^\prime m}{2b} + \frac{{h}m}{2} - \frac 12\sum_{i=1}^m \bigg( \frac{a-bk}{b} + 2\bigg{ \frac{c^\prime-(a-bk)i}{b} \bigg} \bigg)
$$
此时有
$$
\begin{aligned}
&amp;amp;N(a,b,c)-N(a-bk,b,c^\prime)\
= &amp;amp; \frac{cm-c^\prime m}{2b} + \frac{m(h-{h})}{2} - \frac 12\sum_{i=1}^m \bigg[ \bigg(\frac ab + 2\bigg{ \frac{c-ai}{b}\bigg}\bigg) - \bigg(\frac{a-bk}{b}+2\bigg{ \frac{c^\prime-(a-bk)i}{b}\bigg}\bigg) \bigg]\
= &amp;amp; \frac{cm}{2b} - \frac{(c-b(km+\lfloor h\rfloor))m}{2b} +\frac 12m\lfloor h\rfloor - \frac 12\sum_{i=1}^m \bigg[ \bigg(\frac ab + 2\bigg{ \frac{c-ai}{b}\bigg}\bigg) -
\bigg(\frac ab -k+2\bigg{ \frac{c-b(km+\lfloor h\rfloor)-(a-bk)i}{b}\bigg}\bigg)
\bigg]\
\end{aligned}
$$
注意到这里的 ${ \frac{c-b(km+\lfloor h\rfloor)-(a-bk)i}{b}}$ 这一复杂结构可以如下化简
$$
\bigg{ \frac{c-b(km+\lfloor h\rfloor)-(a-bk)i}{b}\bigg} = \bigg{ \frac{c-ai}{b}-(km+\lfloor h\rfloor)+ki\bigg} = \bigg{ \frac{c-ai}{b}\bigg}
$$
所以
$$
\begin{aligned}
&amp;amp;N(a,b,c)-N(a-bk,b,c^\prime)\
= &amp;amp; \frac{(km+\lfloor h\rfloor)m}{2} + \frac 12m\lfloor h\rfloor  \frac{(km+\lfloor h\rfloor)m}{2} + \frac 12m \lfloor h \rfloor - \frac 12 \sum_{i=1}^m \bigg[ \bigg( \frac ab + 2\bigg{ \frac{c-ai}{b} \bigg} \bigg) -
\bigg(\frac ab -k+2\bigg{ \frac{c-ai}{b}\bigg}\bigg)
\bigg]\
=&amp;amp; \frac{(km+\lfloor h\rfloor)m}{2} + \frac 12m\lfloor h\rfloor - \frac 12 \sum_{i=1}^m k\
=&amp;amp; \frac k2m(m-1) + m\lfloor h\rfloor
\end{aligned}
$$&lt;/p&gt;
&lt;h3&gt;定理2&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;设 $a,b,c$ 都是正整数，则 $N(a,b,c)$ 可以在 $O(\max{\log a,\log b})$ 的时间复杂度下计算得到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;根据引理5，令 $k=\lfloor(a-1)/b\rfloor$，则 $a-bk$ 的取值有以下两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若 $a% b = 0$，即 $a=bt,t\in N$。则 $k=\lfloor(a-1)/b\rfloor = t-1$，此时 $a-bk=b$。 当 $a=b$ 时我们用引理4可直接求解。&lt;/li&gt;
&lt;li&gt;若 $a%b\neq 0$，即 $a=bt+r,t\in N, r\in N^+$。则 $k=\lfloor(a-1)/b\rfloor = t$，此时 $a-bk=r=a%b$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上，递推式 $N(a,b,c) = N(a-bk,b,c^\prime) + km(m-1)/2 + m\lfloor h\rfloor$ 的计算复杂度等价于辗转相除法求 $\gcd(a,b)$ 的复杂度。&lt;/p&gt;
&lt;p&gt;于是可以写出以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int64_t count_triangle(int64_t A, int64_t B, int64_t C) {
    if (C &amp;lt; 0) return 0;
    if (A &amp;lt; B) swap(A, B);
    int64_t m = C / A;
    if (A == B) return m * (m - 1) / 2;
    int64_t h = (C - m * A) / B;
    int64_t k = (A - 1) / B;
    return m * h + k * m * (m - 1) / 2 + count_triangle(B, A - B * k, C - B * (k * m + h));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多边形内的格点数&lt;/h2&gt;
&lt;p&gt;为了简单起见，我们这里不考虑多边形边缘上的点以简化讨论。&lt;/p&gt;
&lt;p&gt;设多边形 $P$ 是由 $N$ 个有理数点 $(x_i,y_i),i=0,1,\cdots,n-1$ 构成的，令梯形 $T_i$ 表示点 $(x_i,y_i),(x_{i-1},y_{i-1}),(x_{i-1},0),(x_{i},0)$ 围成的直角梯形，然后就有（定义 $(x_n,y_n)=(x_0,y_0)$）
$$
\text{area}(P) = \sum_{i =1}^N\text{sgn}(x_i-x_{i-1})|D_i|
$$
也就是利用线段的方向计算有向面积。这也可以推广到格点计算上：
$$
\text{num}(P) = \sum_{i=1}^N \text{sgn}(x_i-x_{i-1})\text{num}(D_i)
$$
而梯形的格点计算只需要将直角梯形分成一个矩形+一个直角三角形即可（或者两个直角三角形相减），剩下的都是一些细节上的操作。&lt;/p&gt;
&lt;p&gt;一个例题是 &lt;a href=&quot;https://atcoder.jp/contests/abc372/tasks/abc372_g&quot;&gt; [ABC372G] Ax + By &amp;lt; C&lt;/a&gt;。&lt;/p&gt;
</content:encoded></item><item><title>快速傅里叶变换</title><link>https://fuwari.vercel.app/posts/fast_fourier_transform/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/fast_fourier_transform/</guid><description>从点值表示法出发，推导FFT的分治思想与蝴蝶变换优化，并给出完整的C++实现。</description><pubDate>Thu, 19 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;快速傅里叶变换&lt;/h1&gt;
&lt;h2&gt;点值表示法&lt;/h2&gt;
&lt;p&gt;对于一个 $n-1$ 阶多项式 $P(x)=a_0+a_1x+a_2x^2+\cdots+a_{n-1}x^{n-1}$，如果我们已知一个点集 $S:{(x_0,y_0),(x_1,y_1),\cdots,(x_{n-1},y_{n-1})}$，点集 $S$ 中的所有点都满足 $y_i=P(x_i)$，且 $x_i(i=0,1,\cdots,n-1)$ 各不相同。那么这个点集 $S$ 就是多项式 $P(x)$ 的一个点值表示。&lt;/p&gt;
&lt;h3&gt;插值多项式&lt;/h3&gt;
&lt;p&gt;通过点值表达式还原多项式的操作就是插值，这等价于求解一个线性方程组：
$$
\begin{cases} a_0+a_1x_{0}+a_2x_{0}^2+\cdots+a_{n-1}x_{0}^{n-1}=y_0\ a_0+a_1x_{1}+a_2x_{1}^2+\cdots+a_{n-1}x_{1}^{n-1}=y_1\ \cdots\ a_0+a_1x_{n-1}+a_2x_{n-1}^2+\cdots+a_{n-1}x_{n-1}^{n-1}=y_{n-1}\ \end{cases}
$$
表示成矩阵形式就是
$$
\begin{bmatrix} 1 &amp;amp; x_0 &amp;amp; x_0^2 &amp;amp; \cdots &amp;amp; x_0^{n-1}\ 1 &amp;amp; x_1 &amp;amp; x_1^2 &amp;amp; \cdots &amp;amp; x_1^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ 1 &amp;amp; x_{n-1} &amp;amp; x_{n-1}^2 &amp;amp; \cdots &amp;amp; x_{n-1}^{n-1}\ \end{bmatrix} \begin{bmatrix} a_0\ a_1\ \vdots\ a_{n-1} \end{bmatrix}= \begin{bmatrix} y_0\ y_1\ \vdots\ y_{n-1} \end{bmatrix}
$$
这里系数矩阵 $\begin{bmatrix} 1 &amp;amp; x_0 &amp;amp; x_0^2 &amp;amp; \cdots &amp;amp; x_0^{n-1}\ 1 &amp;amp; x_1 &amp;amp; x_1^2 &amp;amp; \cdots &amp;amp; x_1^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ 1 &amp;amp; x_{n-1} &amp;amp; x_{n-1}^2 &amp;amp; \cdots &amp;amp; x_{n-1}^{n-1}\ \end{bmatrix}$ 是&lt;a href=&quot;https://en.wikipedia.org/wiki/Vandermonde_matrix&quot;&gt;范德蒙德矩阵&lt;/a&gt;，该矩阵的行列式值是 $\prod_{0\le i\lt j\lt n}(x_j-x_i)$，因为 $x_i(i=0,1,\cdots,n-1)$ 各不相同，所以该矩阵的行列式值 $\neq0$，即系数矩阵满秩，该线性方程组只能有唯一解。并且可以计算出多项式系数向量：
$$
\begin{bmatrix} a_0\ a_1\ \vdots\ a_{n-1} \end{bmatrix}= \begin{bmatrix} 1 &amp;amp; x_0 &amp;amp; x_0^2 &amp;amp; \cdots &amp;amp; x_0^{n-1}\ 1 &amp;amp; x_1 &amp;amp; x_1^2 &amp;amp; \cdots &amp;amp; x_1^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ 1 &amp;amp; x_{n-1} &amp;amp; x_{n-1}^2 &amp;amp; \cdots &amp;amp; x_{n-1}^{n-1}\ \end{bmatrix}^{-1} \begin{bmatrix} y_0\ y_1\ \vdots\ y_{n-1} \end{bmatrix}
$$&lt;/p&gt;
&lt;h3&gt;点值表示法下的多项式乘法&lt;/h3&gt;
&lt;p&gt;假设多项式 $C(x)=A(x)B(x)$，其中 $A(x)$ 的度数（最高次幂）为 $n$，$B(x)$ 的度数为 $m$，此时 $C(x)$ 的度数 $\deg C = \deg A + \deg B = n+m$。如果要通过点值表示法插值出原多项式 $C(x)$，显然至少需要 $n+m+2$ 个点。因此，多项式 $A(x),B(x)$ 的点值表示都需要 $n+m+2$ 个点，设点集分别为 $S_A,S_B$：&lt;/p&gt;
&lt;p&gt;$$
\begin{cases}
S_A:{(x_0,y_0),(x_1,y_1),\cdots,(x_{n+m+1},y_{n+m+1})}\
S_B:{(x_0,y^\prime_0),(x_1,y^\prime_1),\cdots,(x_{n+m+1},y^\prime_{n+m+1})}
\end{cases}
$$&lt;/p&gt;
&lt;p&gt;对于任意点 $x_k$，$C(x_k)=A(x_k)B(x_k)$。因此，多项式 $C(x)$ 的点值表示为&lt;/p&gt;
&lt;p&gt;$$
{(x_0,y_0y^\prime_0),(x_1,y_1y^\prime_1),\cdots,(x_{n+m+1},y_{n+m+1}y^\prime_{n+m+1})}
$$&lt;/p&gt;
&lt;p&gt;利用点值表示法求多项式乘法的流程实际上就是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将给定的多项式 $A(x),B(x)$（设度数都是 $O(N)$ 量级）分别转变为点值表示&lt;/li&gt;
&lt;li&gt;通过 $A(x),B(x)$ 的点值表示计算出 $C(x)$ 的点值表示&lt;/li&gt;
&lt;li&gt;通过 $C(x)$ 的点值表示插值还原出系数多项式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其中第2步的时间复杂度显然为 $O(N)$，如果我们可以通过合理的点集选取，使得第一步和第三步都能够在 $O(N\log N)$ 的复杂度下实现，那么我们就得到了一个总复杂度为 $O(N\log N)$ 的多项式乘法算法。&lt;/p&gt;
&lt;h2&gt;快速傅里叶变换&lt;/h2&gt;
&lt;h3&gt;单位复根&lt;/h3&gt;
&lt;p&gt;$n$ &lt;strong&gt;次单位复根&lt;/strong&gt; 是满足 $\omega^n=1$ 的复数 $\omega$，这样的复根恰好有 $n$ 个：$\omega = e^{\frac{2\pi k i}{n}}(k=0,1,\cdots,n-1)$。根据欧拉公式可知，这些复根均匀地分布在复平面的单位圆上。&lt;/p&gt;
&lt;p&gt;&amp;lt;figure&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;index.assets/image-20240325214753702.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;figcaption&amp;gt;8次单位复根的示例&amp;lt;/figcaption&amp;gt;
&amp;lt;/figure&amp;gt;&lt;/p&gt;
&lt;h3&gt;一些引理&lt;/h3&gt;
&lt;h4&gt;消去引理&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;对任意整数 $n\ge 0,k\ge 0$ 以及 $d\gt 0$，
$$
\omega_{dn}^{dk} = \omega_n^k
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;从几何意义考虑，这个引理是显然的，并且由此可以得出一个推论：$\omega_{2n}^n = -1$。&lt;/p&gt;
&lt;h4&gt;折半引理&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;如果 $n\gt 0$ 是偶数，那么 $n$ 个 $n$ 次单位复根的平方的集合就是 $\frac n 2$ 个 $\frac n 2$ 次单位复根的集合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;证明：根据消去引理有 $(\omega_n^k)^2 = \omega_{n/2}^k$。并且注意到，如果对所有 $n$ 次单位复根进行平方，那么获得每个 $\frac n 2$ 次单位根正好 $2$ 次，因为&lt;/p&gt;
&lt;p&gt;$$
(\omega_{n}^{k+n/2})^2 = \omega_n^{2k+n} = \omega_n^{2k}\omega_n^n = (\omega_n^k)^2
$$&lt;/p&gt;
&lt;p&gt;即 $\omega_n^k$ 和 $\omega_{n}^{k+n/2}$ 的平方相同。&lt;/p&gt;
&lt;h4&gt;求和引理&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;对任意整数 $n\ge 1$ 和不能被 $n$ 整除的非负整数 $k$，有
$$
\sum_{j=0}^{n-1} (\omega_n^k)^j = 0
$$&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$$
\sum_{j=0}^{n-1} (\omega_n^k)^j = \frac{(\omega_n^k)^n-1}{\omega_n^k-1} = \frac{(\omega_n^n)^k-1}{\omega_n^k-1} = 0
$$&lt;/p&gt;
&lt;h3&gt;离散傅里叶变换(DFT)&lt;/h3&gt;
&lt;p&gt;回想一下我们的问题：计算多项式乘法 $C(x)=A(x)B(x)$。为了 $O(N\log N)$ 解决这个问题，我们需要将系数表示转换为点值表示，这里我们对于点值表示法的选取就是 $n$ 次单位复根，即我们希望计算多项式&lt;/p&gt;
&lt;p&gt;$$
A(x) = \sum_{j=0}^{n-1} a_jx^j
$$&lt;/p&gt;
&lt;p&gt;在 $\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}$ 处的值（即 $n$ 个 $n$ 次单位复根处）。假设 $A(x)$ 的系数表示用向量 $\vec a = (a_0,a_1,\cdots,a_{n-1})$ 表示，定义：&lt;/p&gt;
&lt;p&gt;$$
y_k = A(\omega_n^k) = \sum_{j=0}^{n-1}a_j\omega_n^{kj}
$$&lt;/p&gt;
&lt;p&gt;则向量 $\vec y = (y_0,y_1,\cdots,y_{n-1})$ 就是系数向量 $\vec a = (a_0,a_1,\cdots,a_{n-1})$ 的&lt;strong&gt;离散傅里叶变换（DFT）&lt;/strong&gt;，记作 $y=\text{DFT}_n(a)$。&lt;/p&gt;
&lt;p&gt;这里的公式可以与&lt;a href=&quot;https://st1vdy.xyz/index.php/2024/03/23/fourier_transform/&quot;&gt;傅里叶变换文中的离散傅里叶变换公式&lt;/a&gt;作比较，不难发现本质上是相同的。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;快速傅里叶变换&lt;/strong&gt;(FFT)&lt;/h3&gt;
&lt;p&gt;本章节中，假设 $n$ 是 $2$ 的整数次幂。&lt;/p&gt;
&lt;p&gt;**快速傅里叶变换（FFT）**实际上就是利用 $n$ 次单位复根的性质，在 $O(n\log n)$ 的时间复杂度下计算出 $n-1$ 阶多项式 $A(x) = \sum_{j=0}^{n-1} a_jx^j$ 的点值表示。按照下标的奇偶性，分别定义两个多项式 $A^{[0]}(x),A^{[1]}(x)$：
$$
\begin{aligned} A^{[0]}(x) &amp;amp;= a_0 + a_2 x + a_4 x^2 + \cdots + a_{n-2}x^{n/2-1}\ A^{[1]}(x) &amp;amp;= a_1 + a_3 x + a_5 x^2 + \cdots + a_{n-1}x^{n/2-1}\ \end{aligned}
$$
原多项式 $A(x) = A^{[0]}(x^2) + xA^{[1]}(x^2)$。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;比如 $7$ 阶多项式 $f(x) = a_0+a_1x+a_2x^2+a_3x^3+a_4x^4+a_5x^5+a_6x^6+a_7x^7$ 就处理为：
$$
f(x) = (a_0+a_2x^2+a_4x^4+a_6x^6)+(a_1x+a_3x^3+a_5x^5+a_7x^7)
$$
其中 $f^{[0]}(x) = a_0 + a_2x+a_4x^2+a_6x^3,f^{[1]}(x) = a_1+a_3x+a_5x^2+a_7x^3$。于是 $f(x) = f^{[0]}(x^2)+xf^{[1]}(x^2)$。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是，求 $A(x)$ 在 $\omega_n^0,\omega_n^1,\cdots,\omega_n^{n-1}$ 处的值就转化为：求两个新的多项式 $A^{[0]}(x),A^{[1]}(x)$ 在 $(\omega_n^0)^2,(\omega_n^1)^2,\cdots,(\omega_n^{n-1})^2$ 处的值，根据折半引理可知这 $n$ 个点的点值实际上只有 $\frac n 2$ 个不同的取值（因为 $(\omega_{n}^{k+n/2})^2 = (\omega_n^k)^2$），这就使得子问题的量级减半了，即求 $\text{DFT}_n$ 转化为了两个 $\text{DFT}{\frac n 2}$ 的子问题，时间复杂度如下：
$$
T(n) = 2T(\frac n 2) + \Theta(n) = \Theta(n\log n)
$$&lt;/p&gt;
&lt;h3&gt;快速傅里叶逆变换(Inverse FFT)&lt;/h3&gt;
&lt;p&gt;求出多项式的点值表示后，我们还需要将点值表示逆运算为常见的系数表示，这一步就是&lt;strong&gt;离散傅里叶逆变换(IDFT)&lt;/strong&gt;。前文中，我们已经提到了IDFT就是插值，即解方程
$$
\begin{bmatrix} 1 &amp;amp; x_0 &amp;amp; x_0^2 &amp;amp; \cdots &amp;amp; x_0^{n-1}\ 1 &amp;amp; x_1 &amp;amp; x_1^2 &amp;amp; \cdots &amp;amp; x_1^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ 1 &amp;amp; x_{n-1} &amp;amp; x_{n-1}^2 &amp;amp; \cdots &amp;amp; x_{n-1}^{n-1}\ \end{bmatrix} \begin{bmatrix} a_0\ a_1\ \vdots\ a_{n-1} \end{bmatrix}= \begin{bmatrix} y_0\ y_1\ \vdots\ y_{n-1} \end{bmatrix}
$$
在FFT中，点值表示选用了 $n$ 次单位复根，上方的方程可以进一步写成
$$
\begin{bmatrix} a_0\ a_1\ \vdots\ a_{n-1} \end{bmatrix}= \begin{bmatrix} \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \cdots &amp;amp; \omega_n^0\ \omega_n^0 &amp;amp; \omega_n^1 &amp;amp; \omega_n^2 &amp;amp; \cdots &amp;amp; \omega_n^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ \omega_n^0 &amp;amp; \omega_n^{n-1} &amp;amp; \omega_n^{2(n-1)} &amp;amp; \cdots &amp;amp; \omega_n^{(n-1)(n-1)}\ \end{bmatrix}^{-1} \begin{bmatrix} y_0\ y_1\ \vdots\ y_{n-1} \end{bmatrix}
$$
这个单位复根构成的逆矩阵可以表示为以下形式：
$$
\begin{bmatrix} \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \cdots &amp;amp; \omega_n^0\ \omega_n^0 &amp;amp; \omega_n^1 &amp;amp; \omega_n^2 &amp;amp; \cdots &amp;amp; \omega_n^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ \omega_n^0 &amp;amp; \omega_n^{n-1} &amp;amp; \omega_n^{2(n-1)} &amp;amp; \cdots &amp;amp; \omega_n^{(n-1)(n-1)}\ \end{bmatrix}^{-1}= \frac 1 n\begin{bmatrix} \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \cdots &amp;amp; \omega_n^0\ \omega_n^0 &amp;amp; \omega_n^{-1} &amp;amp; \omega_n^{-2} &amp;amp; \cdots &amp;amp; \omega_n^{-(n-1)}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ \omega_n^0 &amp;amp; \omega_n^{-(n-1)} &amp;amp; \omega_n^{-2(n-1)} &amp;amp; \cdots &amp;amp; \omega_n^{-(n-1)(n-1)}\ \end{bmatrix}
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我们记 $W=\begin{bmatrix} \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \cdots &amp;amp; \omega_n^0\ \omega_n^0 &amp;amp; \omega_n^1 &amp;amp; \omega_n^2 &amp;amp; \cdots &amp;amp; \omega_n^{n-1}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ \omega_n^0 &amp;amp; \omega_n^{n-1} &amp;amp; \omega_n^{2(n-1)} &amp;amp; \cdots &amp;amp; \omega_n^{(n-1)(n-1)}\ \end{bmatrix},W^{-1}=\frac 1 n\begin{bmatrix} \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \omega_n^0 &amp;amp; \cdots &amp;amp; \omega_n^0\ \omega_n^0 &amp;amp; \omega_n^{-1} &amp;amp; \omega_n^{-2} &amp;amp; \cdots &amp;amp; \omega_n^{-(n-1)}\ \vdots &amp;amp; \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots\ \omega_n^0 &amp;amp; \omega_n^{-(n-1)} &amp;amp; \omega_n^{-2(n-1)} &amp;amp; \cdots &amp;amp; \omega_n^{-(n-1)(n-1)}\ \end{bmatrix}$，只需要验证 $WW^{-1}=I_n$，这里略去计算过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此有
$$
a_k = \frac 1 n\sum_{j=0}^{n-1}\omega_n^{-kj}y_j
$$
与公式 $y_k = \sum_{j=0}^{n-1}a_j\omega_n^{kj}$ 相比较，不难发现这两个问题几乎是一样的（计算 $\text{IFFT}_n(x)$ 只需要将向量 $y,a$ 互换，单位复根 $\omega_n$ 取逆）。&lt;/p&gt;
&lt;h2&gt;算法实现&lt;/h2&gt;
&lt;h3&gt;分治&lt;/h3&gt;
&lt;p&gt;假设我们已知 $y^{[0]}=\text{DFT}(A^{[0]}),y^{[1]}=\text{DFT}(A^{[1]})$（这里 $y^{[0]},y^{[1]}$ 分别是两个长度为 $\frac n 2$ 的向量），求解 $y=\text{DFT}(A)$。也就是我们已知 $y^{[0]} = (A^{[0]}(\omega_{\frac n 2}^{0}), A^{[0]}(\omega_{\frac n 2}^{1}),\cdots, A^{[0]}(\omega_{\frac n 2}^{\frac n 2 - 1}))$ 和 $y^{[1]} = (A^{[1]}(\omega_{\frac n 2}^{0}), A^{[1]}(\omega_{\frac n 2}^{1}),\cdots, A^{[1]}(\omega_{\frac n 2}^{\frac n 2 - 1}))$，求 $y=(A(\omega_n^0),A(\omega_n^1),\cdots,A(\omega_n^{n-1}))$。&lt;/p&gt;
&lt;p&gt;根据前文推导的公式 $A(x) = A^{[0]}(x^2) + xA^{[1]}(x^2)$ 可知 $0\le k\lt \frac n 2$ 时：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned} y_k &amp;amp;= A(\omega_n^k)\ &amp;amp;= A^{[0]}(\omega_n^{2k}) + \omega_n^k A^{[1]}(\omega_n^{2k})\ &amp;amp;= A^{[0]}(\omega_{\frac n 2}^{k}) + \omega_n^k A^{[1]}(\omega_{\frac n 2}^{k})\quad \text{(消去引理)}\ &amp;amp;= y^{[0]}_k + \omega_n^k y^{[1]}_k \end{aligned}
$$&lt;/p&gt;
&lt;p&gt;而后一半的计算则略有不同（$0\le k\lt \frac n 2$）：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned} y_{k+\frac n 2} &amp;amp;= A(\omega_n^{k+\frac n 2})\ &amp;amp;= A^{[0]}(\omega_n^{2k+n}) + \omega_n^{k+\frac n2} A^{[1]}(\omega_n^{2k+n})\ &amp;amp;= A^{[0]}(\omega_n^{2k}\omega_n^n) + \omega_n^{k}\omega_n^{\frac n2} A^{[1]}(\omega_n^{2k}\omega_n^n)\ &amp;amp;= A^{[0]}(\omega_{\frac n2}^{k}) - \omega_n^{k} A^{[1]}(\omega_{\frac n2}^{k})\ &amp;amp;= y^{[0]}_k - \omega_n^ky^{[1]}_k \end{aligned}
$$&lt;/p&gt;
&lt;p&gt;然后就可以写出FFT的代码了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using cp = complex&amp;lt;double&amp;gt;;
void fft(vector&amp;lt;cp&amp;gt;&amp;amp; a, int inv) {
    int n = a.size();
    if (n == 1) return;
    vector&amp;lt;cp&amp;gt; a0(n / 2), a1(n / 2);
    for (int i = 0; i * 2 &amp;lt; n; i++) {
        a0[i] = a[i * 2];
        a1[i] = a[i * 2 + 1];
    }
    fft(a0, inv);
    fft(a1, inv);

    double angle = 2 * pi / n * (inv ? -1 : 1);
    cp w(1, 0), wn(cos(angle), sin(angle));
    for (int i = 0; i * 2 &amp;lt; n; i++) {
        a[i] = a0[i] + w * a1[i];
        a[i + n / 2] = a0[i] - w * a1[i];
        if (inv) {
            a[i] /= 2;
            a[i + n / 2] /= 2;
        }
        w *= wn;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上方函数中 &lt;code&gt;inv&lt;/code&gt; 取 $0$ 时就是一次FFT的正变换；否则是FFT的逆变换。&lt;/p&gt;
&lt;p&gt;下面是模板题&lt;a href=&quot;https://www.spoj.com/problems/POLYMUL/en/&quot;&gt;SPOJ - POLYMUL&lt;/a&gt;的一个AC代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
using db = double;
const db pi = acos(-1);
using cp = complex&amp;lt;db&amp;gt;;

void fft(vector&amp;lt;cp&amp;gt;&amp;amp; a, int inv) {
    int n = a.size();
    if (n == 1) return;
    vector&amp;lt;cp&amp;gt; a0(n / 2), a1(n / 2);
    for (int i = 0; i * 2 &amp;lt; n; i++) {
        a0[i] = a[i * 2];
        a1[i] = a[i * 2 + 1];
    }
    fft(a0, inv);
    fft(a1, inv);

    db angle = 2 * pi / n * (inv ? -1 : 1);
    cp w(1, 0), wn(cos(angle), sin(angle));
    for (int i = 0; i * 2 &amp;lt; n; i++) {
        a[i] = a0[i] + w * a1[i];
        a[i + n / 2] = a0[i] - w * a1[i];
        if (inv) {
            a[i] /= 2;
            a[i + n / 2] /= 2;
        }
        w *= wn;
    }
}

vector&amp;lt;long long&amp;gt; multiply(vector&amp;lt;int&amp;gt;&amp;amp; a, vector&amp;lt;int&amp;gt;&amp;amp; b) {
    int n = 1;
    vector&amp;lt;cp&amp;gt; fa(a.begin(), a.end()), fb(b.begin(), b.end());
    while (n &amp;lt; a.size() + b.size()) n &amp;lt;&amp;lt;= 1;
    fa.resize(n);
    fb.resize(n);
    
    fft(fa, 0), fft(fb, 0);

    for (int i = 0; i &amp;lt; n; i++)
        fa[i] *= fb[i];

    fft(fa, 1);
    vector&amp;lt;long long&amp;gt; res(n);
    for (int i = 0; i &amp;lt; n; i++)
        res[i] = round(fa[i].real());
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int t;
    cin &amp;gt;&amp;gt; t;
    while (t--) {
        int n;
        cin &amp;gt;&amp;gt; n;
        vector&amp;lt;int&amp;gt; a(n + 1), b(n + 1);
        for (auto&amp;amp; i : a)
            cin &amp;gt;&amp;gt; i;
        for (auto&amp;amp; i : b)
            cin &amp;gt;&amp;gt; i;
        auto c = multiply(a, b);
        for (int i = 0; i &amp;lt; n * 2 + 1; i++)
            cout &amp;lt;&amp;lt; c[i] &amp;lt;&amp;lt; &quot; \n&quot;[i == n * 2];
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;优化（倍增FFT）&lt;/h3&gt;
&lt;p&gt;下图展示了一个 $7$ 阶多项式的系数在每一轮递归后的位置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;index.assets/bit_rev_fft-1024x427.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意每个系数的下标在最终状态（树的叶子结点）时的位置，$(0,1,2,3,4,5,6,7)\rightarrow (0,4,2,6,1,5,3,7)$。我们从二进制的角度找找规律：$(000,001,010,011,100,101,110,111)\rightarrow(000,100,010,110,001,101,011,111)$。观察到：把原始序列下标的二进制翻转对称一下，就是最终那个位置的下标。这个性质被称为是&lt;strong&gt;位逆序置换&lt;/strong&gt;（bit-reversal permutation）。&lt;/p&gt;
&lt;p&gt;位逆序置换显然可以在 $O(n\log n)$ 的复杂度下实现，但也有更优的 $O(n)$ 做法，设 $R(x)$ 表示二进制数 $x$ 翻转后的数，从小到大递推 $R(x)$。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先 $R(0)=0$。&lt;/li&gt;
&lt;li&gt;因为我们从小到大递推 $R(x)$ 的值，因此在求 $R(x)$ 时，$R(\lfloor \frac x2\rfloor)$ 的值已知。只要将 $x$ 右移一位，然后翻转，再右移一位，就得到了 $x$ 除了二进制最高位之外所有位的翻转结果，而 $R(x)$ 的最高位可以通过 $x\bmod 2$ 求出。于是
$$
R(x) = \bigg\lfloor\frac{R(\lfloor \frac x2\rfloor)}{2}\bigg\rfloor + (x\bmod 2)\times 2^k
$$
这里 $2^k$ 表示将 $x\bmod 2$ 移动至最高位的偏移量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int bit_reorder(int n, vector&amp;lt;cp&amp;gt;&amp;amp; a) {
    int len = __builtin_ctz(n);
    if ((int)rev.size() != n) {
        rev.assign(n, 0);
        for (int i = 0; i &amp;lt; n; i++) {
            rev[i] = (rev[i &amp;gt;&amp;gt; 1] &amp;gt;&amp;gt; 1) + ((i &amp;amp; 1) &amp;lt;&amp;lt; (len - 1));
        }
    }
    for (int i = 0; i &amp;lt; n; i++) {
        if (i &amp;lt; rev[i]) {
            swap(a[i], a[rev[i]]);
        }
    }
    return len;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说我们能够直接找到FFT递归树最后一层的状态，然后自下而上地合并两个叶子，每一次合并实际上都是在做“已知 $y^{[0]} = (A^{[0]}(\omega_{\frac n 2}^{0}), A^{[0]}(\omega_{\frac n 2}^{1}),\cdots, A^{[0]}(\omega_{\frac n 2}^{\frac n 2 - 1}))$ 和 $y^{[1]} = (A^{[1]}(\omega_{\frac n 2}^{0}), A^{[1]}(\omega_{\frac n 2}^{1}),\cdots, A^{[1]}(\omega_{\frac n 2}^{\frac n 2 - 1}))$，求 $y=(A(\omega_n^0),A(\omega_n^1),\cdots,A(\omega_n^{n-1}))$”这个子问题，从前文已经推出了这个问题的解法：
$$
\begin{cases} y_k = y^{[0]}&lt;em&gt;k + \omega_n^k y^{[1]}&lt;em&gt;k\ y&lt;/em&gt;{k+\frac n 2} = y^{[0]}&lt;em&gt;k - \omega_n^k y^{[1]}&lt;em&gt;k\ \end{cases} \quad (0\le k \lt \frac n2)
$$
注意到，如果我们把向量 $y^{[0]}$ 和 $y^{[1]}$ 依次排列，即 $(y^{[0]},y^{[1]})=(y_0^{[0]},y_1^{[0]},\cdots,y&lt;/em&gt;{\frac n2-1}^{[0]},y_0^{[1]},y_1^{[1]},\cdots,y&lt;/em&gt;{\frac n2-1}^{[1]})$，对比所求向量 $y=(y_0,y_1,\cdots,y&lt;/em&gt;\frac n2,y_{\frac n2+1},\cdots,y_{n-1})$，不难发现 $y_{k}^{[0]},y_{k}^{[1]}$ 和 $y_k,y_{k+\frac n2}$ 的位置两两对应，因此我们可以直接在原数组上计算傅里叶变换，无需递归。这个优化被称为是&lt;strong&gt;蝴蝶变换&lt;/strong&gt;（butterfly transform）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void fft(vector&amp;lt;cp&amp;gt;&amp;amp; a, int inv) {
    int n = a.size();
    int len = bit_reorder(n, a);

    for (int k = 1; k &amp;lt; n; k &amp;lt;&amp;lt;= 1) {
        db angle = 2 * pi / (k * 2) * (inv ? -1 : 1);
        cp wn(cos(angle), sin(angle));
        for (int i = 0; i &amp;lt; n; i += k * 2) {
            cp w(1, 0);
            for (int j = i; j &amp;lt; i + k; j++) {
                cp yk0 = a[j], yk1 = a[j + k] * w;
                a[j] = yk0 + yk1;
                a[j + k] = yk0 - yk1;
                w *= wn;
            }
        }
    }
    if (inv) {
        for (auto&amp;amp; i : a)
            i /= n;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面是模板题&lt;a href=&quot;https://www.spoj.com/problems/POLYMUL/en/&quot;&gt;SPOJ - POLYMUL&lt;/a&gt;的倍增法AC代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;
using db = double;
const db pi = acos(-1);
using cp = complex&amp;lt;db&amp;gt;;
vector&amp;lt;int&amp;gt; rev;
int bit_reorder(int n, vector&amp;lt;cp&amp;gt;&amp;amp; a) {
    int len = __builtin_ctz(n);
    if ((int)rev.size() != n) {
        rev.assign(n, 0);
        for (int i = 0; i &amp;lt; n; i++) {
            rev[i] = (rev[i &amp;gt;&amp;gt; 1] &amp;gt;&amp;gt; 1) + ((i &amp;amp; 1) &amp;lt;&amp;lt; (len - 1));
        }
    }
    for (int i = 0; i &amp;lt; n; i++) {
        if (i &amp;lt; rev[i]) {
            swap(a[i], a[rev[i]]);
        }
    }
    return len;
}

void fft(vector&amp;lt;cp&amp;gt;&amp;amp; a, int inv) {
    int n = a.size();
    int len = bit_reorder(n, a);

    for (int k = 1; k &amp;lt; n; k &amp;lt;&amp;lt;= 1) {
        db angle = 2 * pi / (k * 2) * (inv ? -1 : 1);
        cp wn(cos(angle), sin(angle));
        for (int i = 0; i &amp;lt; n; i += k * 2) {
            cp w(1, 0);
            for (int j = i; j &amp;lt; i + k; j++) {
                cp yk0 = a[j], yk1 = a[j + k] * w;
                a[j] = yk0 + yk1;
                a[j + k] = yk0 - yk1;
                w *= wn;
            }
        }
    }
    if (inv) {
        for (auto&amp;amp; i : a)
            i /= n;
    }
}

vector&amp;lt;long long&amp;gt; multiply(vector&amp;lt;int&amp;gt;&amp;amp; a, vector&amp;lt;int&amp;gt;&amp;amp; b) {
    int n = 1;
    vector&amp;lt;cp&amp;gt; fa(a.begin(), a.end()), fb(b.begin(), b.end());
    while (n &amp;lt; a.size() + b.size()) n &amp;lt;&amp;lt;= 1;
    fa.resize(n);
    fb.resize(n);
    
    fft(fa, 0), fft(fb, 0);

    for (int i = 0; i &amp;lt; n; i++)
        fa[i] *= fb[i];

    fft(fa, 1);
    vector&amp;lt;long long&amp;gt; res(n);
    for (int i = 0; i &amp;lt; n; i++)
        res[i] = round(fa[i].real());
    return res;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int t;
    cin &amp;gt;&amp;gt; t;
    while (t--) {
        int n;
        cin &amp;gt;&amp;gt; n;
        vector&amp;lt;int&amp;gt; a(n + 1), b(n + 1);
        for (auto&amp;amp; i : a)
            cin &amp;gt;&amp;gt; i;
        for (auto&amp;amp; i : b)
            cin &amp;gt;&amp;gt; i;
        auto c = multiply(a, b);
        for (int i = 0; i &amp;lt; n * 2 + 1; i++)
            cout &amp;lt;&amp;lt; c[i] &amp;lt;&amp;lt; &quot; \n&quot;[i == n * 2];
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Hello World</title><link>https://fuwari.vercel.app/posts/hello_world/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/hello_world/</guid><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Hello World&lt;/h1&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;自从我2020年开始构建博客以来已经过了六年，我尝试了博客园、云服务器+wordpress、hexo等方案，现在我计划重新将博客迁移至astro框架+fuwari主题的方案。&lt;/p&gt;
&lt;h2&gt;Hello New World&lt;/h2&gt;
&lt;p&gt;我的上一代自建博客基于 &lt;a href=&quot;https://github.com/WordPress/WordPress&quot;&gt;wordpress&lt;/a&gt; + &lt;a href=&quot;https://github.com/solstice23/argon-theme&quot;&gt;argon theme&lt;/a&gt;，我初次接触argon是在洛谷的主题商店，翻了作者的github发现有wordpress版本的主题就直接决定用这套方案了。wordpress的创作体验类似于博客园，都可以通过一个后台的管理系统进行发文和管理，但是wordpress毕竟是自行搭建的，可定制化程度远高于博客园，适合我这样愿意折腾的人。&lt;/p&gt;
&lt;p&gt;但是过去几年来，我愈发感觉wordpress的发文流相对落后了，这主要是以下几个原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;wordpress原生的Gutenberg编辑器和我常用的markdown（Typora）格式并不完美兼容，这就导致我很难直接将我的markdown笔记复制到服务端；&lt;/li&gt;
&lt;li&gt;在现在ai辅助开发的生产流水线中，ai辅助是至关重要的一环，就算我写好笔记、文章后也会让ai进行校对，但是wordpress的发文流让我不得不多进行一层“人工转译”（Typora to Gutenberg），而不是直接写好markdown就完工；&lt;/li&gt;
&lt;li&gt;wordpress的后台管理界面虽然适合非编程环境下使用，但是对于需要氪金的云服务器运行成本较高；&lt;/li&gt;
&lt;li&gt;argon已不再更新，而我在发文过程中遇到了非常多的bug，包括latex渲染、代码高亮等；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综上所述，我决定迁移到&lt;a href=&quot;https://github.com/withastro/astro&quot;&gt;astro&lt;/a&gt;框架，astro是现代前端框架下的静态站点，不需要像wordpress那种动态网站占用大量资源，完全可以做到在本地开发+上传静态资源到云服务器的发文流。&lt;/p&gt;
&lt;p&gt;迁移以后的发文流将会是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地编写markdown文章&lt;/li&gt;
&lt;li&gt;github作为云端备份&lt;/li&gt;
&lt;li&gt;导出静态页面上传云服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，今后的文章原稿将会在github上完全开源（&lt;code&gt;src/content/posts&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;st1vdy/st1vdy.xyz&quot;}&lt;/p&gt;
&lt;p&gt;由于时间问题，暂时不支持评论系统，如果遇到typo可以直接&lt;a href=&quot;https://qm.qq.com/q/VcpLevlI0c&quot;&gt;QQ&lt;/a&gt;联系，以后有空可能会加上……有生之年……&lt;/p&gt;
</content:encoded></item><item><title>Fuwari使用教程</title><link>https://fuwari.vercel.app/posts/fuwari_guide/</link><guid isPermaLink="true">https://fuwari.vercel.app/posts/fuwari_guide/</guid><description>汇总 Fuwari 主题的完整使用说明，涵盖文章配置、Markdown 语法、代码块增强、扩展功能及视频嵌入。</description><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、文章 Front-matter 字段&lt;/h2&gt;
&lt;p&gt;每篇文章的开头需要包含 YAML front-matter，用于配置文章的元信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: 我的第一篇博客
published: 2024-01-01
description: 这是文章的简短描述，会显示在首页卡片上。
image: ./cover.jpg
tags: [标签1, 标签2]
category: 分类名
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;字段&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章标题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;published&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发布日期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;文章简介，显示在首页卡片&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;image&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;封面图路径。以 &lt;code&gt;http://&lt;/code&gt; 或 &lt;code&gt;https://&lt;/code&gt; 开头则使用网络图片；以 &lt;code&gt;/&lt;/code&gt; 开头则相对于 &lt;code&gt;public/&lt;/code&gt; 目录；否则相对于当前 Markdown 文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;标签列表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;category&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分类&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;draft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是否为草稿。设为 &lt;code&gt;true&lt;/code&gt; 时文章不会对外展示&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;二、文章文件组织&lt;/h2&gt;
&lt;p&gt;所有文章放在 &lt;code&gt;src/content/posts/&lt;/code&gt; 目录下，支持两种形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
├── single-file-post.md       # 单文件形式
└── post-with-assets/         # 目录形式（推荐，方便管理图片等资源）
    ├── cover.png
    └── index.md
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;三、草稿&lt;/h2&gt;
&lt;p&gt;将 front-matter 中的 &lt;code&gt;draft&lt;/code&gt; 设为 &lt;code&gt;true&lt;/code&gt;，文章就不会公开显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: 还没写完的文章
draft: true
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写完后改为 &lt;code&gt;draft: false&lt;/code&gt; 即可发布。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;四、标准 Markdown 语法&lt;/h2&gt;
&lt;h3&gt;基础格式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;_斜体_、**粗体**、`行内代码`

&amp;gt; 引用块

---  （水平分割线）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;- 无序列表项1
- 无序列表项2

1. 有序列表项1
2. 有序列表项2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;链接与图片&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[链接文字](https://example.com)

![图片描述](./image.png)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;| 列1 | 列2 | 列3 |
|-----|-----|-----|
| A   | B   | C   |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数学公式&lt;/h3&gt;
&lt;p&gt;行内公式用单个 &lt;code&gt;$&lt;/code&gt;，块级公式用 &lt;code&gt;$$&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;行内公式：$\omega = d\phi / dt$

块级公式：
$$
I = \int \rho R^{2} dV
$$
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;脚注&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;这里有一个脚注[^1]。

[^1]: 脚注内容写在这里。
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;五、扩展 Markdown 功能&lt;/h2&gt;
&lt;h3&gt;GitHub 仓库卡片&lt;/h3&gt;
&lt;p&gt;自动从 GitHub API 拉取仓库信息并显示为卡片：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;owner/repo-name&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;提示块（Admonitions）&lt;/h3&gt;
&lt;p&gt;支持五种类型：&lt;code&gt;note&lt;/code&gt;、&lt;code&gt;tip&lt;/code&gt;、&lt;code&gt;important&lt;/code&gt;、&lt;code&gt;warning&lt;/code&gt;、&lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note
这是一条注意事项。
:::

:::tip
这是一条小技巧。
:::

:::important
这是重要信息。
:::

:::warning
这是警告信息。
:::

:::caution
这是危险提示。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果预览：&lt;/p&gt;
&lt;p&gt;:::note
这是一条注意事项。
:::&lt;/p&gt;
&lt;p&gt;:::tip
这是一条小技巧。
:::&lt;/p&gt;
&lt;p&gt;:::warning
这是警告信息。
:::&lt;/p&gt;
&lt;h4&gt;自定义标题&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;:::note[自定义标题]
内容...
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;GitHub 风格语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; 也支持 GitHub 风格的提示块语法。

&amp;gt; [!TIP]
&amp;gt; 同样支持。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;剧透遮罩&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;内容 :spoiler[被隐藏的文字] 继续。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：内容 :spoiler[被隐藏的文字] 继续。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;六、代码块增强（Expressive Code）&lt;/h2&gt;
&lt;h3&gt;语法高亮&lt;/h3&gt;
&lt;p&gt;直接在代码块后加语言名即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;Hello World&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;显示文件名&lt;/h3&gt;
&lt;p&gt;通过 &lt;code&gt;title&lt;/code&gt; 属性显示文件名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;带文件名的代码块&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;终端样式&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bash&lt;/code&gt;、&lt;code&gt;sh&lt;/code&gt;、&lt;code&gt;powershell&lt;/code&gt; 等语言会自动渲染为终端样式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;这是终端样式的代码块&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;这是带标题的 PowerShell 终端&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;行高亮标记&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;{行号}&lt;/code&gt; 语法高亮指定行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 第1行高亮
// 第2行普通
// 第3行普通
// 第4行高亮
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;新增/删除标记&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;ins&lt;/code&gt; 和 &lt;code&gt;del&lt;/code&gt; 标记新增/删除行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;这行被标记为删除&apos;)
  // 这行被标记为新增
  console.log(&apos;这也是新增行&apos;)
  return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;diff 语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;```diff lang=&quot;js&quot;
  function demo() {
-   console.log(&apos;旧代码&apos;)
+   console.log(&apos;新代码&apos;)
  }
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果预览：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  function demo() {
-   console.log(&apos;旧代码&apos;)
+   console.log(&apos;新代码&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;行内文字高亮&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;&quot;文字&quot;&lt;/code&gt; 语法高亮行内指定内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 高亮行内某段指定文字
return &apos;这里的 指定文字 会被高亮，所有匹配都会高亮&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以组合 &lt;code&gt;ins&lt;/code&gt; / &lt;code&gt;del&lt;/code&gt; 做行内标记：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;inserted 和 deleted 是行内标记&apos;);
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;折叠代码段&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;collapse={行范围}&lt;/code&gt; 折叠不重要的部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这些 import 会被折叠
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;
const engine = someBoilerplateEngine(evenMoreBoilerplate())

// 这部分默认展开
engine.doSomething(1, 2, 3)

function calcFn() {
  const a = 1, b = 2
  // 这部分也会被折叠
  console.log(`结果: ${a} + ${b} = ${a + b}`)
  return a + b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;显示行号&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 显示行号
console.log(&apos;第2行&apos;)
console.log(&apos;第3行&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定起始行号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;从第10行开始编号&apos;)
console.log(&apos;第11行&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自动换行&lt;/h3&gt;
&lt;p&gt;加 &lt;code&gt;wrap&lt;/code&gt; 参数后超长行会自动换行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 开启换行后，下面这行超长内容不会产生横向滚动条，而是自动折到下一行显示
function getLongString() {
  return &apos;This is a very long string that will most probably not fit into the available space unless the container is extremely wide&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;七、嵌入视频&lt;/h2&gt;
&lt;p&gt;直接在 Markdown 中粘贴 iframe 嵌入代码即可。&lt;/p&gt;
&lt;h3&gt;YouTube&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot;
  src=&quot;https://www.youtube.com/embed/VIDEO_ID&quot;
  title=&quot;YouTube video player&quot;
  frameborder=&quot;0&quot;
  allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot;
  allowfullscreen&amp;gt;
&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Bilibili&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot;
  src=&quot;//player.bilibili.com/player.html?bvid=BV_ID&amp;amp;p=1&quot;
  scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot;
  framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot;&amp;gt;
&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 &lt;code&gt;VIDEO_ID&lt;/code&gt; / &lt;code&gt;BV_ID&lt;/code&gt; 替换为对应视频的 ID 即可。&lt;/p&gt;
</content:encoded></item></channel></rss>