-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.xml
6785 lines (6785 loc) · 330 KB
/
index.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Senie</title><link>/</link><atom:link href="/index.xml" rel="self" type="application/rss+xml"/><description>Senie</description><generator>Wowchemy (https://wowchemy.com)</generator><language>zh</language><copyright>© 星梦</copyright><lastBuildDate>Sat, 09 Jul 2022 17:20:47 +0800</lastBuildDate><image><url>/media/icon_huf638f59a6dd2257b0add8e0186347459_300061_512x512_fill_lanczos_center_3.png</url><title>Senie</title><link>/</link></image><item><title>C++ STL</title><link>/notes/oiknowledge/others%E6%9D%82%E9%A1%B9/c++-stl/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/others%E6%9D%82%E9%A1%B9/c++-stl/</guid><description><p>当你啥也不会的时候,充分利用C++ STL模板可以让你的暴力多得几分。</p></description></item><item><title>快速幂</title><link>/notes/oiknowledge/math%E6%95%B0%E8%AE%BA/%E5%BF%AB%E9%80%9F%E5%B9%82/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/math%E6%95%B0%E8%AE%BA/%E5%BF%AB%E9%80%9F%E5%B9%82/</guid><description><h1 id="快速幂">快速幂</h1>
<h2 id="幂还有快速">幂还有快速?</h2>
<p>我们首先来思考一个问题,对于 $7^6$ 如何求。</p>
<p>作为一个人类,你的求法一定是先算出 $7\times7=49$,然后接着算出 $49\times7=343$,…… ,$16807\times7=117649$。因为人类并不适合算两位数以上的乘法(一位数还勉强可以瞪眼),所以有些时候我们的思维便会限制在这个里面。</p>
<p>但是,我们了解,对于计算机CPU来说 $7\times7$ 和 $1561564\times1561564$ 几乎没啥差别(毕竟慢的是存储),因此我们可以加快幂运算。</p>
<h2 id="又是二进制">又是二进制</h2>
<p>没错,你没看错,又是二进制!</p>
<p>我们早就知道了,对于任意一个二进制数,我们可以拆分成多个 $2^n$ 相加的形式。如:$6_{10}=(110)_2=(100)_2+(10)_2=2^2+2^1$。</p>
<p>相应的,任意一个数 $x$ 的 $k$ 次方 $x^k$ 都可以拆分成多个 $x^{2^n}$ 相乘的形式。这时候,我们发现,原本的 $O(k)$ 次操作被优化到了 $O(\log{k})$。而我们只需要将 $x^{2^n}$ 不断的自己乘自己,就可以得到所有我们需要的数。</p>
<p>例子:计算 $7^{14}=7^2\times7^4\times7^8$</p>
<h2 id="代码">代码</h2>
<p>[P1226 【模板】快速幂&amp;取余运算][https://www.luogu.com.cn/problem/P1226]</p>
<pre><code class="language-c++">//只需要注意mi函数即可
#include &lt;cstdio&gt;
int a, b, p;
inline int mi(int n, int m, int mod, long long sum = 1, long long ans = 1)
//n表示底数,m表示指数,mod为模数,sum为累加器,ans为结果
{
sum = n; //初始化累加器至n^1
while (m) //保证指数不为0
{
if (m &amp; 1) //如果最后一位是1,则应该乘上
{
ans *= sum;
ans %= mod;
}
sum *= sum; //累加器自乘
sum %= mod;
m &gt;&gt;= 1; //这一位已经处理完毕,开始处理下一位
}
return ans % mod;
}
int main()
{
scanf(&quot;%d%d%d&quot;, &amp;a, &amp;b, &amp;p);
printf(&quot;%d^%d mod %d=%d\n&quot;, a, b, p, mi(a, b, p)); //奇奇怪怪的输出方式
}
</code></pre>
<h2 id="链接">链接</h2>
<p>其他更快的方法,参见[]、[]。</p></description></item><item><title>搜索</title><link>/notes/oiknowledge/graph%E5%9B%BE%E8%AE%BA/%E6%90%9C%E7%B4%A2/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/graph%E5%9B%BE%E8%AE%BA/%E6%90%9C%E7%B4%A2/</guid><description><h1 id="dfs">DFS</h1>
<h2 id="深度优先搜索">深度优先搜索。</h2>
<p>深度优先,即每一次均搜索一条完整的从起点到达终点的路径,然后继续搜索第二条。</p>
<p>原始支持有权图搜索。</p>
<p>一般情况下,通常使用递归的方式实现。</p>
<p>具体流程为,对于当前节点,循环遍历下一个目标节点,对于可能的目标节点调用递归函数,直到终止条件。</p>
<p>状态一般包括step,记录决策的数组等,可以放在全局变量中(注意回溯时需要回退全局变量)。</p>
<h2 id="剪枝">剪枝</h2>
<p>剪枝就是切掉一部分无用的搜索树,起到优化复杂度的作用</p>
<ol>
<li>
<p>**记忆化:**dfs函数中传入相同的状态往往会得到一样的解,所以用数组记录下对应每个状态的答案,若之前已求得直接返回即可。</p>
</li>
<li>
<p>**求代价和最小:**若转移时代价非负,则若目前的代价和已经大于等于之前的最小答案,直接返回。</p>
</li>
<li>
<p><strong>更改转移的枚举顺序</strong></p>
</li>
</ol>
<h2 id="启发式搜索">启发式搜索</h2>
<p>当搜索到一个状态u时,计算估价函数 $f(u)$ 的值:</p>
<ul>
<li>
<p>对于转移需要代价的题目,$f(u)$ 代表转移到末状态的最小代价,如果走到u状态的代价 $g(u)+f(u)&gt;ans$ 则剪枝</p>
</li>
<li>
<p>对于转移得到收益的题目,$f(u)$ 表示可能的最大收益,如果 $g(u)+f(u)≤ans$ 则剪枝</p>
</li>
</ul>
<h2 id="a">A*</h2>
<p>估价函数同启发式搜索,但是我们用优先队列进行类广搜,每次取出 $f(x)+g(x)$ 最小的 $x$,然后更新相邻的状态。</p>
<h2 id="ida">IDA*</h2>
<p>迭代加深A*,仅仅是添加了限制层数。</p>
<h2 id="dfs生成树">DFS生成树</h2>
<p>在一个有向图中,以DFS的方式选取某一节点进行搜索而形成的树+边的结构。</p>
<p>举个栗子:</p>
<p>我们先看一个有向图:</p>
<p>
<figure >
<div class="d-flex justify-content-center">
<div class="w-100" ><img src="graph%20%282%29.png" alt="" loading="lazy" data-zoomable /></div>
</div></figure>
</p>
<p>用DFS跑图,就可以得到一下这个:</p>
<img src="dfs-tree.svg" style="zoom:120%;" />
<p>我们可以以树形结构将图中的边分为四种:</p>
<ul>
<li>
<p>树边:每个节点第一次被访问时经过的边,整体称为搜索树。</p>
</li>
<li>
<p><font color="red">返祖边:从子树中的节点到其父亲节点的边。</font></p>
</li>
<li>
<p><font color="green">前向边:从父亲节点直接指向儿子(也可以是孙子等)节点的边。</font></p>
</li>
<li>
<p><font color="blue">横叉边:搜索的时候遇到了一个已经访问过的结点,但是这个结点<strong>并不是</strong>当前结点的祖先时形成的。(看不懂?流氓定义:如果这条边不是上面三种,那就是横叉边)</font></p>
</li>
</ul>
<h2 id="dfs序">DFS序</h2>
<p>字面意思,很好理解,就是用DFS<strong>前序遍历</strong>一棵树上所有节点和边时访问的顺序。</p>
<p>我们知道,树是一种非线性的数据结构,它的一些数据调用肯定是没有线性结构来得方便的。所以这个时候,dfs站了出来。</p>
<p>如图,这是一棵树:</p>
<img src="graph%20(2)-16321427081993.png" alt="afd" style="zoom:90%;" />
<p>所以,整了一大顿,这DFS序到底有啥子用?这得从DFS的优势来探讨了。</p>
<p>DFS是深度优先的,所以对于一个点,它会先遍历完它的所有子节点,再去遍历他的兄弟节点以及其他子树的节点。</p>
<p>因此DFS序保证了一棵树(子树)的根节点和其子树中所有的节点会被存储在连续的区间之中。</p>
<p>比如,以 $5$ 为根的子树中节点编号为 $5\sim8$。</p>
<p>这样,我们把一个非线性的数据结构——树,成功转化为了一个线性的数据结构,然后……<a href="../data%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84">请自行选择</a>。</p>
<p><strong>But!</strong> 我们现在还有一个问题:</p>
<p>如何直到我的子树区间到哪?别搞出去就不好了。</p>
<p>欲知详情,请接着看 -_-</p>
<h3 id="时间戳">时间戳</h3>
<p>这玩意好比一个标签,贴在每一个点上,记录dfs第一次开始访问这个点的时间以及最后结束访问的时间。</p>
<p>我们发现,节点 $i$ 的DFS序其实是DFS第一次访问节点 $i$ 的时间,因此我们只需要记录最后结束访问的时间即可。</p>
<p>实现很简单,循环完儿子之后(回溯之前)记录一下当前最大DFS序值即可。</p>
<p>这样,我们成功的把一棵树变成了一段段的<strong>区间</strong>:</p>
<img src="graph%20(3).png" alt="fad" style="zoom:90%;" />
<h1 id="bfs">BFS</h1>
<h2 id="广度优先搜索">广度优先搜索。</h2>
<p>广度优先,即每一次都搜索距离起点相同长度的点,然后继续搜索长度 $+1$ 的点,直到搜索到终点。</p>
<p>原始不支持有权图,但可以使用算法优化。</p>
<p>一般情况下,通常使用队列的方式实现。</p>
<p>具体流程为,先将起始节点推入队列,每次循环弹出队首元素,将其加入以搜索的点集,并将与之连接的未加入以搜索的点集的点推入队列尾部,直到找到终点或者队列为空。</p>
<h2 id="双向bfs">双向BFS</h2>
<p>要求 $S$ 状态到 $T$ 状态的最小转移次数,从 $S$,$T$ 分别同时出发进行BFS,直到BFS到的点相遇为止。</p>
<p>理论上将深度为 $n$ 的搜索树拆分成了两棵深度为 $\frac{n}{2}$ 的搜索树 。若决策个数为 $k$,搜索树大小由 $k^n$ 优化为 $2\times k^\frac{n}{2}$。</p>
<p>如何判断一个局面(状态)是否被访问过?可以将全局状态变成一个整数,用<code>map</code>或<code>unordered_map</code>记录该整数是否出现过。</p>
<h1 id="三个要素">三个要素</h1>
<ul>
<li>状态:怎么表示?要求包括全部信息、能够标记是否已访问。</li>
<li>转移:转移后的状态计算,转移代价计算。</li>
<li>优化:剪枝(剪掉无用的搜索子树),修改搜索顺序,估价函数。</li>
</ul></description></item><item><title>并查集</title><link>/notes/oiknowledge/data%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%B9%B6%E6%9F%A5%E9%9B%86/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/data%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%B9%B6%E6%9F%A5%E9%9B%86/</guid><description><h1 id="普通并查集">普通并查集</h1>
<h2 id="引子">引子</h2>
<p><del>脑洞蛮大的一个东西,自己还改了改</del></p>
<p> 江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。
但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。
这样一来,江湖上就形成了一个一个的帮派,通过两两之间的朋友关系串联起来。而不在同一个帮派的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。
但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?</p>
<p> 我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物。这样,每个圈子就可以这样命名“周翡队”“谢允队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。</p>
<p> 但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长(抓狂!)。要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”
这样,想打一架得先问个几十年,饿都饿死了,受不了。这样一来,队长面子上也挂不住了,不仅效率太低,还有可能陷入无限死循环中。
于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,队长就是根节点,下面分别是二级军官、三级小兵……每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。
由于我们关心的只是两个人之间是否是一个帮派的,至于他们是如何通过朋友关系相关联的,以及每个圈子内部的结构是怎样的,甚至队长是谁,都不重要了。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。</p>
<p> 现在,我们用fa[i]数组记录编号为i的大侠上级的编号。如果fa[i]==i,则i号大侠是队长。而find函数这是用来寻找队长的。
我非常喜欢周翡与谢允,他们分别属于四十八寨和皇亲国戚,那明显就是两个阵营了。我不希望他们互相打架 <del>(磕糖没够)</del> ,就对他俩说:“你们两位拉拉勾,做好(x)朋(q)友(l)吧。”
他们看在我的面子上,同意了(我脸真大)。这一同意可非同小可,整个四十八寨和皇帝的人就不能打架了。这么重大的变化,可如何实现呀,要改动多少地方?
其实非常简单,我对谢允说:“大师,麻烦你把你的上级改为周翡吧。这样一来,两派原先的所有人员的终极boss都是周翡,那还打个球啊!”谢允一听肯定火大了:“我艹,凭什么是我变成她手下呀,怎么不反过来?我抗议!”<del>(大笑)(反正我们关心的只是连通性,门派内部的结构不要紧的,人家家事也不好管的。)</del></p>
<p> 于是,两人相约一战,杀的是天昏地暗,风云为之变色啊。但是啊,这场战争终究会有胜负,胜者为王,弱者就被吞并了。反正谁加入谁效果是一样的,门派就由两个变成一个了。而together函数就是用来合并门派的。</p>
<p> 两个互不相识的大侠碰面了,想知道能不能干一场。于是赶紧打电话问自己的上级:“你是不是掌门?”上级说:“我不是呀,我的上级是谁谁谁,你问问他看看。”
一路问下去,原来两人的队长都是周翡。“哎呀呀,原来是自己人,有礼有礼,在下岐兰山盘龙洞白面葫芦娃!”“幸会幸会,在下万仙山暖香阁狗尾(yi)巴花!”两人高高兴兴地手拉手喝酒去了。“等等等等,两位大侠请留步,还有事情没完成呢!”我叫住他俩。“哦,对了,还要做路径压缩。”两人醒悟。(find)
白面葫芦娃打电话给他的上级六掌门:“组长啊,我查过了,其实偶们的掌门是周翡。不如偶们一起结拜在周翡手下吧,省得级别太低,以后查找掌门麻烦。”
“唔,有道理。”白面葫芦娃接着打电话给刚才拜访过的三执事……仙子狗尾巴花也做了同样的事情。
这样一来,整个门派树的层数都会维持在比较低的水平上,便于查找。</p>
<h2 id="开始了">开始了</h2>
<p>乐呵够了,开始了…………</p>
<p>上述玄幻故事生动形象地讲述了一个<del>中华武林的真实故事</del> 啊不,是<strong>并查集</strong>。</p>
<p>提到了几个要点:fa数组,find函数,together函数,路径压缩 <em><del>等等</del></em></p>
<h3 id="fa数组">fa数组</h3>
<p>fa[i]<del>生动形象地</del>记录了你的爹地是谁qwq</p>
<p>是不是很直观、生动?</p>
<p>咳咳,fa[i]表示第$i$个节点的父亲(也就是掌门人)的节点编号</p>
<h3 id="find函数">find函数</h3>
<p>你开始打电话,逐层查找你的上级、你上级的上级…………</p>
<p>实现十分简单,只要不断访问fa[i]即可,直到 $fa[i]=i$;</p>
<h3 id="together函数">together函数</h3>
<p>两个门派经过昏天黑地的战斗后,决定合并…………<del>因为人都打没了</del></p>
<p>只要把find函数返回的值选择一个合并到另一个里即可</p>
<pre><code class="language-c++">fa[find(x)]=find(y);
</code></pre>
<h3 id="路径压缩">路径压缩</h3>
<p>故事最后一段</p>
<p>为了防止储存集合所生成的树过高而采用的方法</p>
<p>具体来说,就是每一次find时,将所访问到的所有节点都挂在根节点下。</p>
<h3 id="按秩合并">按秩合并</h3>
<p><em>(达到和路径压缩几乎<strong>相同</strong>的结果,但是并不更优,还难以理解,<del>放弃吧</del>…………)</em></p>
<p>记录每棵树的树高(根到叶子的最大边数)depi,两棵树合并的时候将dep较小的根挂在dep较大的根下面。</p>
<p>当且仅当两棵树的dep相同时新树的树高为dep+1,否则为dep。</p>
<p>那么dep=1的树有一个结点,dep=2的树至少有两个结点,dep=3的树只能至少由两个dep=2的树合并而来,所以至少有4个结点&hellip;</p>
<p>所以按秩合并保证了树高不超过$\log{n}$,时间复杂度$O(n\log{n})$</p>
<p>贴份代码:</p>
<pre><code class="language-c++">int f[N], s[N]; // 取秩为集合大小
inline void init(int n)
{
for (int i = 1; i &lt;= n; ++i)
f[i] = i, s[i] = 1;
}
int find(int x) { return x == f[x] ? x : f[x] = find(f[x]); } // 路径压缩
inline void merge(int x, int y)
{ // 按秩合并
x = find(x), y = find(y);
if (x == y)
return;
if (s[x] &gt; s[y])
swap(x, y);
f[x] = y, s[y] += s[x];
}
</code></pre>
<h3 id="最后代码-surprise">最后……………代码! surprise!!!</h3>
<p><a href="https://www.luogu.com.cn/problem/P3367" target="_blank" rel="noopener">洛谷P3367</a> <del>就是板子</del></p>
<pre><code class="language-c++">#include &lt;cstdio&gt;
const int maxe = 10009;
int n, m, fa[maxe];
inline int find(int x) //find函数和路径压缩的完美结合,递归思想
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
inline void together(int a, int b) //应该是最短的写法
{
fa[find(a)] = find(b);
}
int main()
{
scanf(&quot;%d%d&quot;, &amp;n, &amp;m);
for (int i = 1; i &lt;= n; i++) //初始化,最开始每个人都是一个门派
{
fa[i] = i;
}
while (m--)
{
int a, b, c;
scanf(&quot;%d%d%d&quot;, &amp;a, &amp;b, &amp;c);
if (a == 1)
together(b, c);
else
printf(&quot;%c\n&quot;, find(b) == find(c) ? 'Y' : 'N');
}
return 0;
}
</code></pre>
<h1 id="带权并查集">带权并查集</h1></description></item><item><title>栈</title><link>/notes/oiknowledge/data%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E6%A0%88/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/data%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E6%A0%88/</guid><description/></item><item><title>背包问题</title><link>/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98/</guid><description><h1 id="背包问题">背包问题</h1>
<h2 id="01背包">01背包</h2>
<h3 id="引题">引题:</h3>
<p><strong>原版:</strong>
有一个背包,可容纳重量为 $k$ 的物品。有 $n$ 个物品(每个物品只有一个),第 $i$ 个物品的重量为 $w[i]$,价值为 $val[i]$。求背包可容纳物品的最大价值。</p>
<p><strong>魔改版:</strong></p>
<p>你有 $n$ 项作业,但是你只有 $k$ 的时间,第 $i$ 项作业需 $val_i$ 个小时完成。但是,因为作业的内容、难度不同,对于每一项作业老师取得的快乐值也不同。</p>
<p>比如:如果你完成了一道全班只有你才完成的数学难题,老师会很高兴,即使你没有把定义练习抄写1000000000遍,老师也不会批评你;然而,如果你只把 $1+1=2$ 这类的题做完了,而稍微难一点儿的题就不做,老师就会很生气,认为你很懒,而让你把定义再抄100000000000遍。</p>
<p>问,在有限的时间内,如何使老师获得的快乐值最大?<del>从而免去抄写</del></p>
<h3 id="心灵的震撼">心灵的震撼</h3>
<p>我相信,每一个第一次接触dp的人都会为之而着迷,毕竟一种优美的暴力手段是每一个有着 $ak\space IOI$ 的人所梦寐以求的。</p>
<p>dp之所以比暴搜快,可以看做是因为记录了中间过程的权值,进而优化掉了DFS中很多重复的子树,也可以看做一种牺牲空间换取时间的做法。</p>
<p>dp的核心思想就是从之前的状态推导当前状,进而在 $O(1\sim n)$ 的时间内找到当前状态的解。</p>
<p>也就是说,dp时无需考虑问题整体,而是化为一个个的状态,只需要关注某个状态如何从之前已经得到的状态中转移过来。</p>
<p>使用dp时很多时候论证其正确性是一个费时费力还费脑子的工作,因此dp题目需要你有一定的经验、大胆的尝试和欧气(毕竟dp的样例一般都很水,可能一道题样例全过就得个 $30+$ 的分数也不是没有可能),因为考试的时候没那么多时间,大多数dp代码都不长,建议有想法就直接写,搞出来后跑样例,然后再做调整。正常情况下,只要能过 $1+$ 个样例,基本上不会暴零。可以先写出一个局部解,然后再完善dp的状态转移方程。</p>
<p><del>上面唠唠叨叨</del></p>
<p>dp最重要的就是<strong>状态</strong>和<strong>转移</strong>。</p>
<p>在01背包中,我们用 $dp[i][j]$ 表示在前 $i$ 件物品中选择,背包容量为 $j$ 时能装的最大价值。</p>
<p>然后,思考这个式子:
$$
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+val[i])
$$
$max$ 表示求取的最大值。因为我们已经知道了前 $i-1$ 个物品在背包容量为 $a\in[1,k]$ 时能装下的最大价值,因此对于第 $i$ 件物品,只有两种选择:</p>
<ol>
<li>装(前提是 $w[i]\le j$,毕竟得能装下才有讨论的必要)</li>
<li>不装 <del>(你能把我怎样?)</del></li>
</ol>
<p>若不选择第 $i$ 件物品,很简单,其在容量为 $j$ 的背包下最大价值 $=$ 从前 $i-1$ 件中选择的最大价值(反正不选第 $i$ 件,所以没有影响) $=\space dp[i-1][j]$。</p>
<p>若选择第 $i$ 件物品,则应在前 $i-1$ 个中选择后至少剩余 $w[i]$ 的背包空间,然后选择第 $i$ 个物品,我们就会获得它的价值 $val[i]$,因此总共获得价值为 $dp[i-1][j-w[i]]+val[i]$。</p>
<p>易证,当物品相同时,大容量的背包装的总价值一定不小于小背包装的,因此只考虑最大的可能背包即可,不必遍历小背包。</p>
<p>当我们把这张表填完的时候,$dp[n][k]$ (表示选择前 $n$ 个物品背包容量为 $k$ 时能装下的最大值)就是我们要求的最终答案。</p>
<pre><code class="language-c++">
</code></pre>
<h3 id="线性优化">线性优化</h3>
<p>我们发现了一个问题,上述01背包问题求解时,时间复杂度为 $O(nk)$,而空间复杂度也为 $O(nk)$。</p>
<p>因为这玩意常数比较小,在 $nk\le10^8$ 内都能跑过,但是,内存肯定不够用啊……<del>人间疑惑</del></p>
<p>可不可以优化一下啊,毕竟好不容易不TLE了要是再MLE可就要<del>砸电脑了</del>螺旋升天了,进而怀疑人生……</p>
<p>好消息!好消息!好消息!空间可以优化!</p>
<p>观察转移方程,我们实际上只用到了 $2\times k$ 个值,其余的我们都不需要,所以,我们搞一个 <code>int dp[2][k]</code>好吗?</p>
<p><strong>当然可以!</strong><del>只要你调的出来,理论上确实可行,只要你不是每搞完一行整一个 O(k) 的复制就行</del></p>
<p>下面,让我们再动脑子想一想,在缩小一下转移方程使用到的值,其实只有 $dp[i-1][a\in[1,j]]$,大于 $j$ 的我们用不到!</p>
<p>所以,我们能不能在一维数组上解决这个问题呢?</p>
<p><strong>当然可以!</strong><del>没得怕的</del></p>
<p>只要我们从后往前遍历即可!即<code>for(int i = k;i &gt; 0;i--)</code>即可。</p>
<p>因为,当我们遍历原二维数组第 $i$ 行时,现在数组中存储的就是原二维数组第 $i-1$ 行的值,并且即使大于 $j$ 的部分被更改,我们需要使用的小于等于$j$ 的部分仍旧是原第 $i-1$ 行的dp值,因此,状态转移方程魔改一下:
$$
dp[j]=max(dp[j],dp[j-w[i]]+val[i])
$$
<del>体会到了人类脑洞之大</del></p>
<p>我们的最终答案,就在跑完之后,存在 $dp[k]$ 的地方。</p>
<pre><code class="language-c++">
</code></pre>
<p>注:</p>
<ul>
<li>如果要 $dp[j] =$ 重量恰好为 $j$ 的最大价值,在DP前将 $dp$ 数组初始化为 $\infty$ 即可。</li>
<li>如果有重量为负值(显然此时要求的是重量恰好为 $j$ 的最大价值),循环顺序要改为正序,且 $dp$ 的下标统一加一个足够大的数以保证全为正数!</li>
<li>一维优化不能重构路径,但是二维数组可以通过一些办法(回溯,又称为逆推)求出咋装的。</li>
</ul>
<h2 id="完全背包">完全背包</h2>
<p>背包问题大多是从01背包的基础上演化而来,简言之就是01背包的增强版。</p>
<p>01背包的问题魔改一下就是完全背包:</p>
<p>我们还有一个背包,可容纳重量为 $k$ 的物品。有 $n$ <strong>种</strong>物品(每种物品有<strong>无限多个</strong>),第 $i$ 种物品的重量为 $w[i]$,价值为 $val[i]$。求背包可容纳物品的最大价值。</p>
<p>我们依然使用 $dp[i][j]$ 表示从前 $i$ 种物品中选择,背包容量为 $j$ 时能装的最大价值。</p>
<p>然后,状态转移方程就变化了:
$$
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+val[i])
$$
不难发现,对于第 $i$ 种物品,我们可以选择 $a\in[0,\frac{j}{w[i]}]$ 个。分为两类:</p>
<ol>
<li>不选第 $i$ 种物品。</li>
<li>选第 $i$ 种物品 $a$ 个。</li>
</ol>
<p>然后,这个问题又可以分为两步:</p>
<ol>
<li>确定选还是不选</li>
<li>如果选,选几个</li>
</ol>
<p>这时候,想一想dp的奥妙,即从已知推导出未知。</p>
<p>不选很好解决,$dp[i-1][j]$ 就是不选第 $i$ 种的最大价值。</p>
<p>那如果选呢?再回想dp数组的意义和01背包对于选物品的空间预留,可以得到 $dp[i][j-w[i]]+val[i]$ 这样的一个式子。我们只需要在可以选择第 $i$ 种的情况下留出 $w[i]$ 的空间来再装下一个第 $i$ 种物品即可。这样,我们无需知道第 $i$ 种物品应该选择几个,因为我们只需要在之前的最优解上进行状态转移即可,换言之我们只需要考虑多装一个是否更优,而不是遍历装几个更优,因为背包容量从小到大,能装下的个数也是从小到大。</p>
<h3 id="线性优化-1">线性优化</h3>
<p>和01背包一样,完全背包依旧时间复杂度 $O(nk)$,空间复杂度 $O(nk)$。</p>
<p>那么,就会出现同样的问题。</p>
<p>既然差不多,完全背包也能线性优化吗?</p>
<p>**当然可以!**只不过需要进行一些调整,来适应新的状态转移方程。</p>
<p>我们发现,新的状态转移方程依赖于 $dp[i][a\in[0,j]]$ 而并不依赖于 $dp[i-1][a\in[0,k]]$ 和 $dp[i][a\in(j,k]]$,因此,我们需要改变遍历的顺序。</p>
<p>上面我们已经理解了线性优化后 $dp[j]$ 在修改前表示 $dp[i-1][j]$,所以<code>for(int i = 0;i &lt; k;i++)</code>改为正序遍历即可先求出第 $i$ 行较小的 $j$ 进而推导出较大的 $j$,完成dp。这样,空间复杂度降为 $O(k)$。</p>
<pre><code class="language-c++">
</code></pre>
<p>同01背包,一维优化不能重构路径,但二维可以。</p>
<h2 id="多重背包">多重背包</h2>
<p>同样,上来还是老问题:</p>
<p>我们又有一个背包,可容纳重量为 $k$ 的物品。有 $n$ 中物品,第 $i$ 种物品有 $c[i]$个,每个重量为 $w[i]$,价值为 $val[i]$。求背包可容纳物品的最大价值。</p>
<p>看,看,看,是不是和01背包又是差不多?</p>
<p>所以,最简单的想法就是将每个物品拆成独立的个体,跑01背包,时间复杂度为 $O(k\times\sum_{i=1}^nc[i])$。</p>
<p>但是,显然,<strong>太慢了!</strong></p>
<p>观察,多重背包与01背包最大的不同,就是有多个物品属性相同, 这也成为优化的入手点。</p>
<p>先说一个题外话:人民币大家都知道吧,有$1$元的、$10$元的还有红色毛爷爷……那么,为什么不全用$1$元的?这样也可以表示任意的金额啊?</p>
<p>但是,前几天刚出现了一个新闻,某男子用一麻袋硬币交房租被告上法庭……所以,体会到了红色毛爷爷的重要性。</p>
<p>因此,我们的多重背包也可以借助这个思想,将 $c[i]$ 个物品划分成几组,从而能且仅能表示出所有 $a\in[0,c[i]]$ 。</p>
<p>问题来了,知道了为什么要分组,接下来就是怎么分组了。</p>
<h3 id="二进制拆分">二进制拆分</h3>
<p>我们先来看 $0\sim19$ 的二进制表:</p>
<table>
<thead>
<tr>
<th>十进制</th>
<th>二进制</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>00000</td>
</tr>
<tr>
<td>1</td>
<td>00001</td>
</tr>
<tr>
<td>2</td>
<td>00010</td>
</tr>
<tr>
<td>3</td>
<td>00011</td>
</tr>
<tr>
<td>4</td>
<td>00100</td>
</tr>
<tr>
<td>5</td>
<td>00101</td>
</tr>
<tr>
<td>6</td>
<td>00110</td>
</tr>
<tr>
<td>7</td>
<td>00111</td>
</tr>
<tr>
<td>8</td>
<td>01000</td>
</tr>
<tr>
<td>9</td>
<td>01001</td>
</tr>
<tr>
<td>10</td>
<td>01010</td>
</tr>
<tr>
<td>11</td>
<td>01011</td>
</tr>
<tr>
<td>12</td>
<td>01100</td>
</tr>
<tr>
<td>13</td>
<td>01101</td>
</tr>
<tr>
<td>14</td>
<td>01110</td>
</tr>
<tr>
<td>15</td>
<td>01111</td>
</tr>
<tr>
<td>16</td>
<td>10000</td>
</tr>
<tr>
<td>17</td>
<td>10001</td>
</tr>
<tr>
<td>18</td>
<td>10010</td>
</tr>
<tr>
<td>19</td>
<td>10011</td>
</tr>
</tbody>
</table>
<p>然后,我们发现,对于任意一个二进制数,均可以用不同的只有一位是 $1$ 的二进制数相加得到,如:</p>
<p>$13$ = $(01101)_2$ = $(01000)_2+(00100)_2+(00001)_2$</p>
<p>而这个规律翻译成十进制就是,对于数 $2^{k-1}&lt;a\le2^k$ 一定可以用 $2^0,2^1,2^2,2^3,\dots,2^{k-1}$ 和 $a-2^{k-1}$ 中的某几个数相加得到。这就构成了我们分组的依据。</p>
<p>我们将 $c[i]$ 拆分成 $2^0,2^1,2^2,\dots,2^{k-1},c[i]-a$,当成一共 $k$ 个物品,第 $x$ 组(个)物品的重量为 $第x组的个数\times w[x]$,价值为 $第x组的个数\times val[x]$。</p>
<p>这样拆分之后,在跑01背包,即可将时间复杂度降至 $O(k\times\sum_{i=1}^n\log{c[i]})$。</p>
<pre><code class="language-c++">
</code></pre>
<h2 id="混合背包">混合背包</h2>
<p>01、完全、多重背包三合一。<del>大锅炖</del></p>
<p>01背包当 $c[i]=1$,完全背包当 $c[i]=\lfloor\frac{k}{w[i]}\rfloor$ 即可。</p>
<h2 id="多限制背包">多限制背包</h2>
<p>物品有更多属性,对每种属性都有限制。</p>
<p>把所有限制都加到状态里即可。</p>
<h2 id="分组背包">分组背包</h2>
<p>描述:有n件物品,分为若干组,现约束,在每组物品里最多取一件物品放入背包,每件物品的重量确定,价值确定,背包容量确定,求在不超过背包容量的情况下,可以存放的最大价值。</p>
<p>$w[i][j]$ 表示第 $i$ 组第 $j$ 件物品的重量,$val[i][j]$ 表示第 $i$ 组第 $j$ 件物品的价值,$dp[a][b]$ 表示在前 $a$ 组中选择重量不大于 $b$ 的最大价值。</p>
<p>对于任意一组物品,共计两类状态:</p>
<ol>
<li>一个都不选</li>
<li>选择第 $i\in[1,len]$ 个</li>
</ol>
<p>因此,在01背包的基础上进行嵌套循环,每次遍历整组,求出最优解即可。</p>
<p>线性空间优化见上文。</p>
<h2 id="背包方案计数">背包方案计数</h2>
<p>把所有的取 $max$ 都改成求和即可。</p>
<p><strong>注意:<strong>多重背包不能用二进制拆分优化了,因为</strong>同样的</strong>数量拆分方式<strong>不唯一</strong>。</p>
<h2 id="依赖性背包">依赖性背包</h2>
<p>依赖关系只有一层:把每个“主件”当做一个背包,然后将各背包合并(也是树形背包的基本思路)。</p>
<p>无循环依赖(树形背包):<a href="./%e6%a0%91%e5%bd%a2dp.md">树形DP</a>。</p>
<p>有循环依赖:<a href="../graph%e5%9b%be%e8%ae%ba/%e5%bc%ba%e8%bf%9e%e9%80%9a%e5%88%86%e9%87%8f.md">Tarjan+缩点</a> + <a href="./%e6%a0%91%e5%bd%a2dp.md">树形DP</a>。</p>
<p>请在相应章节查看。</p></description></item><item><title>递推和递归</title><link>/notes/oiknowledge/basic%E5%9F%BA%E6%9C%AC%E6%80%9D%E6%83%B3/%E9%80%92%E5%BD%92/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/basic%E5%9F%BA%E6%9C%AC%E6%80%9D%E6%83%B3/%E9%80%92%E5%BD%92/</guid><description><h1 id="递推">递推</h1></description></item><item><title>拓扑排序</title><link>/notes/oiknowledge/graph%E5%9B%BE%E8%AE%BA/%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/graph%E5%9B%BE%E8%AE%BA/%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F/</guid><description><h1 id="拓扑排序">拓扑排序</h1>
<p><a href="https://blog.csdn.net/weixin_39970166/article/details/110870115" target="_blank" rel="noopener">一篇博文</a></p>
<h2 id="引子">引子</h2>
<p>下面让我来看一看拓扑序是什么,又有啥子用。</p>
<p>众所周知,万事都要讲究先来后到。比如,你对一个婴儿进行早教,你很自信、很闪耀,觉得此孩儿定是天降奇才,跳过了万水千山,连话都不会说,直接交他拓扑排序,为以后发扬光大OI事业而打下坚实的基础。然后,一段时间以后,你忽然发现,你教授完一万字的长篇大论然而并没有一点用处,甚至人家一眼都没看你。所以,你十分灰心,感受到了生活的无情,决定放下倔强,从头教起。你潜心研究,发现有一些知识需要在某些知识已经掌握的情况之下才可以学习;所以,你编写了一个程序,给出学习各种知识的先后顺序。</p>
<h2 id="为啥是图论">为啥是图论</h2>
<p>看上面的段子,脑海中浮现出各种问号……</p>
<p>这东西为啥子是图论嘞?</p>
<p>我们先搞一下啥是DAG(有向无环图):</p>
<p><strong>一个有向图,且不存在环。</strong> <del>多么简洁明了</del></p>
<img src="104909e17df721889a8ef5386d33263d.png" alt="DAG" style="zoom: 55%;" />
<p>这时候,考虑将边 $u\rightarrow{v}$ 具体为”做 $v$ 之前要先做 $u$ ”,那么拓扑排序能求出每件事应该在什么时候做,也就是说满足每件事的前置事件都做完后它才能开始做的一种安排。</p>
<p><strong>拓扑序不是唯一的,因为有些点之间不存在拓扑关系。</strong></p>
<p>比如,上图中A和E、B和D等就没有拓扑关系。</p>
<p>建图到此结束。</p>
<h2 id="bfs款">BFS款</h2>
<p>入度:指向某个点的有向边条数。</p>
<p>我们发现,对于任意一个DAG,总有入度为$0$的节点。完成这个节点无需依赖其他任何节点的完成情况。</p>
<p>因此,我们维护一个队列(其实栈也不是不行),队列中包含所有入度为$0$的节点。</p>
<p>对于每一个队列中节点,删去其所有初度,并将删边后入度为$0$的节点加入队列。</p>
<p>如需按照字典序大小求拓扑序,改为使用优先队列即可(即优先访问字典序大/小的节点)。</p>
<p>记录已经遍历的节点数,若队列为空并且已经遍历的节点数小于总节点数,则说明存在环。</p>
<img src="6e6578e58c374ddeca7d1ff41d0e8aab.png" alt="huan" style="zoom:65%;" />
<p>如上图,当出现环时,删除一定数量的节点和边后,出现所有节点入度均大于$0$的情况,即队列为空。</p>
<p>两种算法复杂度均为$O(n+m)$。</p>
<p><a href="http://poj.org/problem?id=2367" target="_blank" rel="noopener">POJ2367</a> 外加一点补充</p>
<pre><code class="language-c++">#include &lt;cstdio&gt;
#include &lt;queue&gt;
#include &lt;vector&gt;
using std::vector;
const int maxe = 109; //最大节点个数
int len, in[maxe], cnt; //len总节点数,in[i]第i号节点的入度,cnt已经遍历的节点个数
vector&lt;int&gt; next[maxe]; //邻接链表存图
std::priority_queue&lt;int, vector&lt;int&gt;, std::greater&lt;int&gt; &gt; running; //如无需按字典序输出,请改用queue;注意两个'&gt;'中间有一个空格,看好是小顶堆还是大顶堆
int main()
{
scanf(&quot;%d&quot;, &amp;len);
for (int i = 1; i &lt;= len; i++)
{
int a;
while (1)
{
scanf(&quot;%d&quot;, &amp;a);
if (!a)
break;
next[i].push_back(a);
in[a]++; //每有一条指向节点的边,入度+1
}
}
for (int i = 1; i &lt;= len; i++)
{
if (in[i] == 0) //查找所有入度为0的节点
running.push(i);
}
while (!running.empty())
{
int top = running.top();
running.pop();
printf(&quot;%d &quot;, top);
cnt++; //已经遍历的节点数+1
int n = next[top].size();
for (int i = 0; i &lt; n; i++) //遍历所有出度,进行删边操作
{
int now = next[top][i];
in[now]--;
if (!in[now]) //如果删边后入度为0,则加入队列
running.push(now);
}
}
if (cnt != len) //如果以遍历节点数小于总节点数,说明有环
printf(&quot;error\n&quot;);
return 0;
}
</code></pre>
<h2 id="dfs款">DFS款</h2>
<p>选定一个节点,递归完成他的入度。</p>
<p>对于每个节点循环。</p>
<p><del>还是学上面那个吧</del></p></description></item><item><title>离散化</title><link>/notes/oiknowledge/others%E6%9D%82%E9%A1%B9/%E7%A6%BB%E6%95%A3%E5%8C%96/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/others%E6%9D%82%E9%A1%B9/%E7%A6%BB%E6%95%A3%E5%8C%96/</guid><description><h1 id="离散化">离散化</h1>
<p><del>好高深的鸭子</del></p>
<p><del>其实,用STL水就可以了</del></p>
<h2 id="离散化-1">离……散化?</h2>
<p>离散化,就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。即在不改变数据相对大小的条件下,对数据进行相应的缩小。通俗地讲就是当有些数据因为本身很大或者类型不支持,自身无法作为数组的下标来方便地处理,而影响最终结果的只有元素之间的相对大小关系时,我们可以将原来的数据按照从大到小编号来处理问题,即离散化。</p>
<p><del>好了,上面的多无聊</del></p>
<p>很简单,就是如果你只需要 $k$ 个数,但是可能存在 $n$ 种情况,你开不下下标为 $n$ 的数组,因此你丢出了离散化来解决这个问题。</p>
<h2 id="所以实现">所以……实现?</h2>
<p>如果你不嫌累的话,你大可以学习各种各样奇奇怪怪的离散化的方法(包括但不限于Hash、排序)。</p>
<p>但是,这种非常常见的东西C++ STL里面肯定有啊。</p>
<p>所以,介绍几个:</p>
<h3 id="map">map</h3>
<h3 id="heading"></h3></description></item><item><title>质数、合数、约数</title><link>/notes/oiknowledge/math%E6%95%B0%E8%AE%BA/%E8%B4%A8%E6%95%B0%E5%90%88%E6%95%B0%E7%BA%A6%E6%95%B0%E5%80%8D%E6%95%B0/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/math%E6%95%B0%E8%AE%BA/%E8%B4%A8%E6%95%B0%E5%90%88%E6%95%B0%E7%BA%A6%E6%95%B0%E5%80%8D%E6%95%B0/</guid><description><h1 id="质数">质数</h1>
<p>本标题下所有数 $x\in N^*$ 。</p>
<h2 id="单个质数的判定">单个质数的判定</h2>
<h3 id="思想">思想</h3>
<p>枚举 $i\in{{1\sim\sqrt{P}}}$ ,判断$P\mod{i}$是否为$0$即可。</p>
<p>若数 $P=p_1*p_2$ 并且 $p_2&gt;\sqrt{P}$ ,则 $p_1&lt;\sqrt{P}$ ,所以只需要枚举 $i$ 到 $i&lt;=\sqrt{P}$ 。</p>
<h3 id="代码">代码</h3>
<pre><code class="language-c++">bool prime(int n)
{
int len = sqrt(n);//求i的遍历范围
for (int i = 2; i &lt;= len; i++)
if (!(n % i))//如果i是n的约数,说明n不是质数,返回false
return false;
return true;//n除了1和n以外没有别的约数,证明n是质数,返回true
}
</code></pre>
<p>时间复杂度 $O(\sqrt{n})$。</p>
<h2 id="单个数分解质因数">单个数分解质因数</h2>
<p>想法和上面的一样,只不过找到一个因数就循环除他罢辽。</p>
<pre><code class="language-c++">list&lt;int&gt; prime(int n)
{
list&lt;int&gt; result;
int len = sqrt(n); //求i的遍历范围
for (int i = 2; i &lt;= len; i++)
{
if (N % i == 0)
{ // 如果 i 能够整除 N,说明 i 为 N 的一个质因子。
while (N % i == 0)
N /= i;
result.push_back(i);
}
}
if (N != 1)
{ // 说明再经过操作之后 N 留下了一个素数
result.push_back(N)
}
}
</code></pre>
<h2 id="范围内所有质数的判定">范围内所有质数的判定</h2>
<h3 id="纯暴力">纯暴力</h3>
<p>就把上面那个套个循环,复杂度 $O(n\sqrt n)$。</p>
<h3 id="埃氏筛">埃氏筛</h3>
<p>从 $2$ 开始,不断寻找没有被标记的,并把它的倍数全部标记,复杂度 $O(n\log\log n)$。</p>
<p>但是,我们注意到,某一些合数被标记了不止一次,因此仍有优化空间。</p>
<h3 id="欧拉筛">欧拉筛</h3></description></item><item><title>分治(二分)</title><link>/notes/oiknowledge/basic%E5%9F%BA%E6%9C%AC%E6%80%9D%E6%83%B3/%E5%88%86%E6%B2%BB/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/basic%E5%9F%BA%E6%9C%AC%E6%80%9D%E6%83%B3/%E5%88%86%E6%B2%BB/</guid><description><h2 id="二分查找">二分查找</h2>
<h2 id="二分答案">二分答案</h2></description></item><item><title>线性dp</title><link>/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E7%BA%BF%E6%80%A7dp/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/%E7%BA%BF%E6%80%A7dp/</guid><description><h1 id="线性dp">线性dp</h1>
<h2 id="最基本的dp和dp简介">最基本的dp和dp简介</h2>
<h3 id="一种优美的暴力">一种优美的暴力</h3>
<p>动态规划可以看做在<strong>最优化问题</strong>和<strong>计数问题</strong>中对暴力搜索的优化。</p>
<p>在暴力搜索中,我们要枚举<strong>每一步</strong>决策,枚举所有的方案。</p>
<p>但在多数问题中,暴力搜索其实会做大量重复工作,重复计算大量中间结果。</p>
<p>动态规划就是把这些中间结果用dp数组记录下来,以便后续利用。</p>
<p>最基本的状态和转移方程的设计比较容易,在NOIP题目中一般占分 $30\sim60$。</p>
<p>最基本的方程的设计并没有特别的技巧。一般来说,</p>
<ul>
<li>动态规划问题的“三大要素”:<strong>状态、转移、边界</strong>。</li>
<li>题目中给的条件都可以加入状态中,题目要求最优化的值就是DP值。</li>
<li>转移方程往往根据最后一步决策来设计。</li>
</ul>
<h3 id="线性dp-1">线性dp</h3>
<p>状态均沿<strong>一个方向</strong>转移的dp。</p>
<p>例如最基本的模型——<a href="https://www.luogu.com.cn/problem/P1216" target="_blank" rel="noopener">数字三角形</a>和<a href="http://noi.openjudge.cn/ch0206/1759/" target="_blank" rel="noopener">最长上升子序列(LIS)</a>都属于线性dp。</p>
<p>[洛谷P1216 [USACO1.5][IOI1994] 数字三角形 Number Triangles](<a href="https://www.luogu.com.cn/problem/P1216" target="_blank" rel="noopener">https://www.luogu.com.cn/problem/P1216</a>)</p>
<p>$dp[i][j]$表示以第 $i$ 行第 $j$ 列为终点的最大数字和,转移方程为 $dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])+val[i][j]$。</p>
<p><a href="http://noi.openjudge.cn/ch0206/1759/" target="_blank" rel="noopener">Openjudge 2.6 1759 最长上升子序列</a></p>
<p>一会儿详细的说。</p>
<h2 id="几种优化的方式">几种优化的方式</h2>
<p>因为dp要对于题目而设计转移方程,因此有一些优化方式仅能用例题去表示。</p>
<p>但是,强烈建议能想到dp的题先搞出来最基本的,然后再优化,便于对拍和防止暴零。</p>
<h3 id="无优化">无优化</h3>
<p>以LIS为例子。</p>
<p>求最长上升子序列和最长不降子序列(注意,是子序列以及不降表示可以相等;对于固定的数组,虽然LIS序列不一定唯一,但LIS的长度是唯一的)。</p>
<p>我们用 $dp[i]$ 表示以第 $i$ 位结尾的LIS长度。</p>
<p>转移时每次都向前找比它小的数和比它大的数的位置,将第一个比它大的替换掉。这样操作虽然LIS序列的具体数字可能会变,但是很明显LIS长度还是不变的,因为只是把数替换掉了,并没有改变增加或者减少长度。</p>
<p>因此,转移方程为:</p>
<pre><code class="language-c++">if (num[i - 1] &lt; num[i])
dp[i] = dp[i - 1] + 1;
else
{
int big = 2e9, ji = -1;
for (int j = 1; j &lt; i; j++)
{
if (num[j] &lt; big &amp;&amp; num[j] &gt;= num[i])
{
big = num[j];
ji = j;
}
}
dp[i] = ji == -1 ? 1 : dp[ji];
}
</code></pre>
<p><del>比较复杂</del></p>
<h3 id="减少多余状态">减少多余状态</h3>
<p>打一个恰当的比方,当你的 $dp$ 状态有$4$个,但是其中一个可以由剩下的$3$个推算得来,则这个状态就可以省略(优化掉)。</p>
<h3 id="改变状态设计">改变状态设计</h3>
<p>通俗一点,就是换一个dp的思路。</p>
<h3 id="前后缀和优化dp">前(后)缀和优化DP</h3>
<h2 id="背包问题">背包问题</h2>
<p><a href="./%e8%83%8c%e5%8c%85%e9%97%ae%e9%a2%98.md">背包问题</a>是一类特殊的线性DP问题。其模型应用极为广泛,故单独叙述。</p></description></item><item><title>队列</title><link>/notes/oiknowledge/data%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E9%98%9F%E5%88%97/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/data%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E9%98%9F%E5%88%97/</guid><description/></item><item><title>差分数组</title><link>/notes/oiknowledge/others%E6%9D%82%E9%A1%B9/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/others%E6%9D%82%E9%A1%B9/%E5%B7%AE%E5%88%86%E6%95%B0%E7%BB%84/</guid><description><h1 id="差分数组">差分数组</h1>
<h2 id="定义">定义</h2>
<p>对于第$i$位,记录第$i$位减第$i-1$位的差值。</p>
<p>例子:</p>
<table>
<thead>
<tr>
<th style="text-align:center">下标</th>
<th style="text-align:center">0</th>
<th style="text-align:center">1</th>
<th style="text-align:center">2</th>
<th style="text-align:center">3</th>
<th style="text-align:center">4</th>
<th style="text-align:center">5</th>
<th style="text-align:center">6</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center"><strong>原数组</strong></td>
<td style="text-align:center">0</td>
<td style="text-align:center">6</td>
<td style="text-align:center">9</td>
<td style="text-align:center">5</td>
<td style="text-align:center">4</td>
<td style="text-align:center">7</td>
<td style="text-align:center">3</td>
</tr>
<tr>
<td style="text-align:center"><strong>差分数组</strong></td>
<td style="text-align:center">0</td>
<td style="text-align:center">+6</td>
<td style="text-align:center">+3</td>
<td style="text-align:center">-4</td>
<td style="text-align:center">-1</td>
<td style="text-align:center">+3</td>
<td style="text-align:center">-4</td>
</tr>
</tbody>
</table>
<h2 id="用途">用途</h2>
<p>快速进行区间修改操作。</p>
<p><strong>注意!</strong> 只适用于可被抵消贡献的运算中,如$+/-/\and$(加、减、异或)等。</p>
<p>若将原数组$[l,r]$区间内加$val$,则相当于将差分数组第$l$位加$val$,第$r+1$位减去$val$(将贡献抵消)。</p>
<h6 id="注意-是第r1位而不是第r位"><strong>注意! 是第$r+1$位,而不是第$r$位!!!</strong></h6>
<p>经常和维和区间和的数据结构一起使用。(<a href="../data%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84/%e6%a0%91%e7%8a%b6%e6%95%b0%e7%bb%84.md">树状数组</a>等)</p></description></item><item><title>最小生成树</title><link>/notes/oiknowledge/graph%E5%9B%BE%E8%AE%BA/%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/graph%E5%9B%BE%E8%AE%BA/%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91/</guid><description><h1 id="最小生成树">最小生成树</h1>
<h2 id="问题描述">问题描述</h2>
<p>给出一个有$n$个节点,$m$条边的无向图,从中选取边权之和最小(大)的$n-1$条边,使得图上任意两个顶点有且只有唯一的一条路径可以互相到达。</p>
<h2 id="例子">例子</h2>
<p>
<figure >
<div class="d-flex justify-content-center">
<div class="w-100" ><img src="%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91.png" alt="最小生成树" loading="lazy" data-zoomable /></div>
</div></figure>
<img src="%E6%97%A0%E5%90%91%E5%9B%BE.svg" alt="无向图" style="zoom:80%;" /></p>
<p>如上图,其最小生成树为: <strong>红色部分</strong> 和 <img src="%E6%9C%80%E5%B0%8F%E7%94%9F%E6%88%90%E6%A0%91.dot.svg" alt="最小生成树" style="zoom:80%;" /></p>
<p><strong>注意</strong> 最小生成树可能不唯一(边权相等)</p>
<h2 id="暴力">暴力</h2>
<p>枚举每一种可能的情况,计算最小值(因复杂度过高,不再赘述)。</p>
<h2 id="prim算法">Prim算法</h2>
<p>时间复杂度:$O(nm)\sim O(m\log{n})$ <em>数据结构优化</em></p>
<p><a href="https://blog.csdn.net/weixin_42657313/article/details/103326019?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522162729943216780366585099%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&amp;request_id=162729943216780366585099&amp;biz_id=0&amp;utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-6-103326019.first_rank_v2_pc_rank_v29&amp;utm_term=prim%E7%AE%97%E6%B3%95&amp;spm=1018.2226.3001.4187" target="_blank" rel="noopener">一篇比较详细的博文</a></p>
<p>以<strong>点</strong>为核心,每次选择当前<strong>已选中</strong>的点连接<strong>未选中</strong>的点的边中<strong>权值最小</strong>的一个,将这个点加入<strong>已选中</strong>的点集,并用和这个点连接的边更新<strong>未选中</strong>的点的距离。</p>
<h3 id="例子-1">例子</h3>
<img src="20191130162837932.png" alt="Prim1" style="zoom: 150%;" />
<img src="20191130162907364.png" alt="Prim2" style="zoom: 200%;" />
<p><del>盗图很开心</del></p>
<p><a href="https://www.luogu.com.cn/problem/P3366" target="_blank" rel="noopener">洛谷P3366</a> <del>其实就是板子</del></p>
<pre><code class="language-c++">#include &lt;cstdio&gt;
const int maxe = 6000; //n的最大值
int n, m, head[maxe], cnt;
//n个点,m条边,head[i]表示已i为端点最后读入的一条边的编号,已经读入cnt条边
bool vis[maxe]; //vis[i]标记i节点是否在最小生成树点集中
struct node
{
int value, last, sign; //value表示边权,last表示同一起点的上一条读入的边的编号,sign表示当前节点编号
} edge[400009];
//m最大值*2
inline void add_edge(int start, int end, int value) //链式前向星
{
edge[++cnt].value = value;
edge[cnt].sign = end;
edge[cnt].last = head[start];
head[start] = cnt;
return;
}
inline int prim()
{
int dis[maxe], ans = -1e9;
//dis[i]表示连接未加入点集的i号节点和点集中任意点最短路径长,ans = -1e9抵消选择第一个点溢出
for (int i = 1; i &lt;= n; i++)
{
dis[i] = 2e9; //初始化dis数组
}
int k = 1, min; //k记录所选择的点的编号,min记录最小值,k初始值为第一个加入点集的点的编号
vis[1] = 1; //将第一个点加入点集
for (int i = 0; i &lt; n; i++) //循环n次,将所有点加入点集
{
min = 1e9; //别忘了qwq,注意小于dis初始者,判断是否无法建树
for (int j = 1; j &lt;= n; j++) //暴力搜索最小点,可用堆/平衡树(优先队列、set等)优化
{
if (!vis[j] &amp;&amp; dis[j] &lt; min)
{
min = dis[j];
k = j;
}
}
if (min == 1e9 &amp;&amp; k != 1) //除第一个点外,如果没有未加入点集的点有连接且点集中小于n个点,证明有点无法连同
{
return 0;
}
ans += min;
vis[k] = 1;
for (int i = head[k]; i; i = edge[i].last) //用新加入的点更新dis数组
{
if (!vis[edge[i].sign] &amp;&amp; dis[edge[i].sign] &gt; edge[i].value)
{
dis[edge[i].sign] = edge[i].value;
}
}
}
return ans;
}
int main()
{
scanf(&quot;%d%d&quot;, &amp;n, &amp;m);
for (int i = 0; i &lt; m; i++)
{
int a, b, value;
scanf(&quot;%d%d%d&quot;, &amp;a, &amp;b, &amp;value);
add_edge(a, b, value); //无向边,任意端点可作为起止点
add_edge(b, a, value);
}
int ans = prim();
if (ans)
printf(&quot;%d&quot;, ans);
else
printf(&quot;orz&quot;);//无法连同所有点
return 0;
}
</code></pre>
<h2 id="kruskal算法">Kruskal算法</h2>
<p>时间复杂度: $O(m\log{m})$</p>
<p>想法比较简单、易懂。</p>
<p>将所有边<strong>按权值</strong>从小到大排序,优先选取<strong>权值较小</strong>的边,判断两个端点是否在<strong>同一个集合</strong>中。如果在同一集合中,则<strong>跳过</strong>这条边,遍历下一条;如果不再同一集合中,则将ans加上边权并将<strong>两个点所在的集合合并</strong>。</p>
<p>涉及到判断两个点是否在同一集合中,需要引入<a href="/oiblogs/data%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84/%e5%b9%b6%e6%9f%a5%e9%9b%86/">并查集</a>。</p>
<p><a href="https://www.luogu.com.cn/problem/P3366" target="_blank" rel="noopener">洛谷P3366</a> <del>其实就是板子</del></p>
<pre><code class="language-c++">#include &lt;cstdio&gt;
#include &lt;algorithm&gt;
int n, m, fa[6000], ans, cnt; //n个节点,m条边,已经选择cnt条边
struct edge
{
int a, b, value;
} edges[200009]; //有一条端点是a和b,权重为value的边
inline bool cmp(edge a, edge b) //比较函数
{
return a.value &lt; b.value;
}
inline int find(int x) //并查集
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
inline void together(int x, int y)
{
x = find(x);
y = find(y);
fa[x] = y;
}
int main()
{
scanf(&quot;%d%d&quot;, &amp;n, &amp;m);
for (int i = 1; i &lt;= n; i++)
{
fa[i] = i;
}
for (int i = 0; i &lt; m; i++)
{
scanf(&quot;%d%d%d&quot;, &amp;edges[i].a, &amp;edges[i].b, &amp;edges[i].value);
}
std::sort(edges, edges + m, cmp); //按边权排序
for (int i = 0; i &lt; m; i++)
{
if (find(edges[i].a) != find(edges[i].b)) //如果两个端点不再同一集合
{
ans += edges[i].value; //选中这条边
cnt++;
together(edges[i].a, edges[i].b); //合并两个集合
}
if (cnt == n) //最小生成树边数等于总节点数-1
break;
}
if (cnt == n - 1) //最小生成树边数等于总节点数-1
printf(&quot;%d&quot;, ans);
else
printf(&quot;orz&quot;);
return 0;
}
</code></pre>
<h2 id="总结">总结</h2>
<p>两种算法均用到<a href="/oiblogs/basic%e5%9f%ba%e6%9c%ac%e6%80%9d%e6%83%b3/%e8%b4%aa%e5%bf%83/">贪心</a>的思想。</p>
<p>Prim侧重于点,适合稠密图;Kruskal侧重于边,适合稀疏图;但二者优化后差距并不大,推荐Kruskal,易理解不费手还不容易出奇奇怪怪的问题。<del>(复杂度中可以看出)</del></p></description></item><item><title>KMP</title><link>/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/kmp/</link><pubDate>Sun, 17 Oct 2021 00:00:00 +0000</pubDate><guid>/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/kmp/</guid><description><h1 id="kmp">KMP</h1>
<h2 id="入门第一发">入门第一发</h2>
<p>相信,很多人入门的第一个算法(除去贪心等基本思想),所以,先来略略领会一下dp的奥妙和人类的脑洞叭。</p>
<h3 id="字符串匹配问题">字符串匹配问题:</h3>
<p>一类在一个字符串中查找其他字符串出现情况的问题。称被匹配的串为<strong>主串</strong>;称在主串中寻找匹配位置的串为<strong>模式串</strong>。</p>
<p>其按模式串个数分为<strong>单模</strong>匹配和<strong>多模</strong>匹配问题。而KMP是一种利用 LBorder 来高效解决单模匹配的算法。</p>
<p><strong>单模匹配问题</strong>:给定两个串 $n$ 和 $m$ ,求 $m$在 $n$ 中出现的所有位置。</p>
<p>字符串 $s$ 的公共前后缀(即:$s[i]=s[len-i]$)称作Border。空串与原串也是 Border。</p>
<p><strong>非原串</strong>的最长的 Border 称作 <strong>LBorder</strong> (Longest Border)。</p>
<p>LBorder 的性质使得 KMP 算法在匹配失败时能够直接按 LBorder 将主串与模式串重新对齐并继续尝试匹配,从而有效避免了暴力做法中盲目试错的过程。</p>
<h2 id="预处理">预处理</h2>
<p>为模式串建立 $next$ 数组,$next[i]$ 表示当模式串匹配到第 $i$ 位失配时应该从哪一位开始匹配。</p>
<p>注意,$next[0]$ 和 $next[1]$ 不存在Border(一共长度没有 $2$,上哪来的两个子串?),因此根据其意义规定为 $next[0]=next[1]=0$,在这两位失配时均从头开始匹配。</p>
<p>而构建 $next$ 数组,运用了一点dp的思想,即尽可能的使用现有的去推导已知的。</p>
<p>
<figure >
<div class="d-flex justify-content-center">
<div class="w-100" ><img alt="kmp" srcset="
/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/kmp/KMP_huee9227d8f766b824f535546b7eb39deb_6764_1d4e52a038b05963acd7ba2588ad6993.png 400w,
/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/kmp/KMP_huee9227d8f766b824f535546b7eb39deb_6764_095f616770fbd084713d6a9bf8122c8c.png 760w,
/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/kmp/KMP_huee9227d8f766b824f535546b7eb39deb_6764_1200x1200_fit_lanczos_3.png 1200w"
src="/notes/oiknowledge/dp%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/kmp/KMP_huee9227d8f766b824f535546b7eb39deb_6764_1d4e52a038b05963acd7ba2588ad6993.png"
width="760"
height="77"
loading="lazy" data-zoomable /></div>
</div></figure>
</p>
<p>上图演示:当我们求 $next[j+1]$ 时,已经知道 $next[0]\sim next[j]$ 的值。</p>
<ol>
<li>首先,我们判断 $j+1=k$,若成立则 $next[j+1]=next[j]+1$ 并退出,若不成立执行第 $2$ 步。</li>
<li>再次判断 $j+1=h$,若成立则 $next[j+1]=next[k]+1$ 并退出,若不成立执行第 $3$ 步。</li>
<li>接着判断……</li>
</ol>
<p>那么,问题来了,问什么我们这样做是对的呢?<del>(这题我A了,但是为什么?)</del></p>
<p>看上图,方块表示对应位置的字符,每条颜色对应的横线是相等的子串(其实就是之前求出的LBonder)。</p>
<ol>
<li>
<p>因为 $A_1=A_2$,因此 $next[j]=k$。如果 $j=k$,则 $next[j+1]$ 应该指向 $k+1$ 位(LBonder的长度为 $A_1+第k位$)。</p>
</li>
<li>
<p>若上面的没匹配到,则因为 $A_1=A_2\space&amp;\space b_1=b_2$,我们可以推导出 $b_1=b_3$,这样,我们又重复了上面的那个问题,所以一样的方法再做一遍即可。</p>
</li>
<li>
<p>我们推导可知, $c_1=c_2=c_3=c_4$,所以继续……</p>
</li>
</ol>
<p>下面,我们上代码!</p>
<p>因为通常起始位置可能是下标 $0$ 或者是下标 $1$,构建的时候可以目标第 $j$ 位或者第 $j+1$ 位;因此,经过组合数的计算,我们一共有 $4$ 种代码。</p>
<p>从 $0$ 开始目标 $j$ 位:</p>
<pre><code class="language-c++">inline void get_next(char *in, int *next)
{
int len = strlen(in);
next[0] = next[1] = 0;
for (int i = 2; i &lt;= len; i++)
{
int now = next[i - 1];
while (now &amp;&amp; in[now] != in[i - 1])
now = next[now];
next[i] = in[i - 1] == in[now] ? now + 1 : 0;
}
}
</code></pre>
<p>从 $0$ 开始目标 $j+1$ 位 :</p>
<pre><code class="language-c++">inline void get_next(char *in, int *next)
{
int len = strlen(in);
next[0] = next[1] = 0;
for (int i = 1; i &lt; len; i++)
{
int now = next[i];
while (now &amp;&amp; in[now] != in[i])
now = next[now];
next[i + 1] = in[i] == in[now] ? now + 1 : 0;
}
}
</code></pre>
<p>从 $1$ 开始目标 $j$ 位:</p>
<pre><code class="language-c++">inline void get_next(char *in, int *next)
{
int len = strlen(in);
next[1] = next[2] = 1;
for (int i = 3; i &lt;= len; i++)
{
int now = next[i - 1];
while (now &gt; 1 &amp;&amp; in[now] != in[i - 1])
now = next[now];
next[i] = in[i - 1] == in[now] ? now + 1 : 1;
}
}
</code></pre>
<p>从 $1$ 开始目标 $j+1$ 位:</p>
<pre><code class="language-c++">inline void get_next(char *in, int *next)
{
int len = strlen(in);
next[1] = next[2] = 1;
for (int i = 2; i &lt; len; i++)
{
int now = next[i];
while (now &gt; 1 &amp;&amp; in[now] != in[i])
now = next[now];
next[i + 1] = in[i] == in[now] ? now + 1 : 1;
}
}
</code></pre>
<h2 id="开始匹配">开始匹配</h2>
<p>而接下来的匹配就很好搞了,只要一失配就跳next即可。这样,可以做到主串的 $i$ 不会退,提高了效率。</p>
<p><a href="https://www.luogu.com.cn/problem/P3375" target="_blank" rel="noopener">洛谷 P3375 【模板】KMP字符串匹配</a></p>
<pre><code class="language-c++">#include &lt;cstdio&gt;
#include &lt;cstring&gt;
const int maxe = 1e6 + 9;
char a[maxe], b[maxe];
int nxt[maxe], na, nb;
inline void get_next(char *in, int *next)
{
int len = strlen(in);
next[0] = next[1] = 0;
for (int i = 2; i &lt;= len; i++)
{
int now = next[i - 1];
while (now &amp;&amp; in[now] != in[i - 1])
now = next[now];
next[i] = in[i - 1] == in[now] ? now + 1 : 0;
}
}
int main()
{
scanf(&quot;%s%s&quot;, a, b);
na = strlen(a);
nb = strlen(b);
get_next(b, nxt);
for (int i = 0, j = 0; i &lt; na; i++)
{
while (j &amp;&amp; b[j] != a[i]) //不断失配跳转
{
j = nxt[j];
}
if (b[j] == a[i])
j++;
if (j == nb)
{
printf(&quot;%d\n&quot;, i - j + 2);
}
}
for (int i = 1; i &lt;= nb; i++)
{
printf(&quot;%d &quot;, nxt[i]);
}
return 0;
}
</code></pre>
<h2 id="字符串最小循环节">字符串最小循环节</h2>
<p><strong>证明:字符串最小循环节 = 字符串长度 - LBonder(尾部next数组)</strong></p>
<p>假设字符串 $a$ 是其子串 $b$ 循环 $k$ 次在加一个 $b$ 的前缀 $d$ 得到,$a$ 串的LBonder长 $x$,则我们思考一个问题:</p>
<p>前 $k-1$ 个 $b$ 加一个 $d$ 等于后 $k-1$ 个 $b$ 加一个 $d$。举个栗子:</p>
<table>
<thead>
<tr>
<th style="text-align:center"><strong>a</strong></th>