forked from EndlessCheng/codeforces-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdp.go
2093 lines (1970 loc) · 75.5 KB
/
dp.go
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
package copypasta
import (
"container/heap"
"math/bits"
"sort"
)
/* 动态规划
首先请透彻理解何为问题的「状态空间」,见 search.go 开头的注释
思考过程:
1.1 如何把问题形式化为状态空间?(可以从边界、子集的角度思考)
1.2 子问题是如何重叠的?
1.3 子问题是怎么逐层递进的?(题目描述、隐含的顺序)
2.1 如何定义状态?需要用几个维度表示?
2.2 状态的范围是多少?起点状态和终点状态是什么?
2.3 哪些状态是相邻的?(即通过一次转移就能得到)
2.4 状态转移时要计算哪些内容?
2.5 对于转移来的相邻状态(入边),怎么决策?(简单的有取最值取和,复杂的有组合决策)
3.1 若复杂度过高,如何优化决策?
* 状态不好确定时,尝试转化问题模型、逆序思考、增加维度等等
* 对于计数问题或概率问题来说,状态定义和状态转移要做到不重不漏
如何设计状态:
https://codeforces.com/problemset/problem/360/B
https://codeforces.com/problemset/problem/461/B
https://codeforces.com/problemset/problem/553/A
https://codeforces.com/problemset/problem/687/C
https://codeforces.com/problemset/problem/1025/D
https://codeforces.com/problemset/problem/1027/E
https://codeforces.com/problemset/problem/1408/D
SEERC05,紫书例题 9-3,UVa 1347 https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=446&page=show_problem&problem=4093
Daejeon11,紫书例题 9-8,UVa 1625 https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=825&page=show_problem&problem=4500
LC956/周赛114D https://leetcode-cn.com/problems/tallest-billboard/ https://leetcode-cn.com/contest/weekly-contest-114/
涉及到相邻状态先后关系的 DP(喂兔子)https://codeforces.com/problemset/problem/358/D
戳气球 LC312 https://leetcode-cn.com/problems/burst-balloons/
消消乐 LC546/周赛25D https://leetcode-cn.com/problems/remove-boxes/ https://leetcode.com/contest/leetcode-weekly-contest-25
谁来当 DP 对象 LC1434/双周赛25D https://leetcode-cn.com/problems/number-of-ways-to-wear-different-hats-to-each-other/ https://leetcode-cn.com/contest/biweekly-contest-25/
扔蛋问题 LC887/周赛97D https://leetcode-cn.com/problems/super-egg-drop/ https://www.bilibili.com/video/BV1KE41137PK https://leetcode-cn.com/contest/weekly-contest-97/
LC920* https://leetcode-cn.com/problems/number-of-music-playlists/ 注:官方题解给出了一种生成函数的做法
状态优化 https://codeforces.com/problemset/problem/838/E
「排序」题的转换 https://codeforces.com/problemset/problem/1223/D
NOTE: 无后效性是指当前的决策只与过去的结果有关,而与过去的决策无关
NOTE: 若状态转移不构成 DAG,请尝试建图+BFS,见:
https://ac.nowcoder.com/acm/contest/6218/B
https://codeforces.com/problemset/problem/283/B 活用 012 染色
NOTE: 若使用滚动数组,注意复用时可能要初始化
NOTE:(区间 DP)正向计算不易时,试着反向计算
TIPS: 若转移是若干相邻项之和,可以考虑 f(p) - f(p-1) 的值,用滑动窗口来维护区间和,从而优化转移
例题 LC837 https://leetcode-cn.com/problems/new-21-game/
递归打印路径:https://codeforces.com/problemset/problem/2/B
需要补充额外的状态 https://codeforces.com/problemset/problem/682/D
todo Non-trivial DP Tricks and Techniques https://codeforces.com/blog/entry/47764
参考书籍推荐:
《算法竞赛进阶指南》- 介绍了大量且全面的 DP 内容,是目前市面上讲解 DP 最好的一本书
视频讲解:
https://www.bilibili.com/video/BV1gf4y1i78H 动态规划的套路 by wisdompeak
https://www.bilibili.com/video/av70148899 DP 入门,01 背包,完全背包,多重背包
https://www.bilibili.com/video/av77393700 LCS LIS
https://www.bilibili.com/video/av83939419 区间 DP
https://www.bilibili.com/video/av93356551 状态压缩 DP
https://www.bilibili.com/video/av98090640 树形 DP
https://www.bilibili.com/video/BV1MT4y1376C 数位 DP
https://www.bilibili.com/video/av85636122 动态规划 · 零 - Introduction
https://www.bilibili.com/video/av86983419 动态规划 · 一 - 序列型
https://www.bilibili.com/video/av89052674 动态规划 · 二 - 坐标、双序列、划分 & 状态压缩
套题/总结:
《挑战程序设计竞赛》上的练习题(均为 POJ)
2.3 节
3176 https://www.luogu.com.cn/problem/P1216 数字三角形
2229 https://www.luogu.com.cn/problem/P6065 将 n 分拆为若干个 2 的次幂的和的方法数 https://oeis.org/A018819
2385 https://www.luogu.com.cn/problem/P2690 dp[i分钟][j移动次数] = max(dp[i-1][j], dp[i-1][j-1]) + 当前分钟是否有苹果落在 j 次移动后的位置 最后答案为 max{dp[n-1]}
3616 https://www.luogu.com.cn/problem/P2889 DAG 最长路
3280 https://www.luogu.com.cn/problem/P2890 增删取 min,跑区间 DP
1742 http://acm.hdu.edu.cn/showproblem.php?pid=2844 多重背包
3046 http://poj.org/problem?id=3046 todo
3181 https://www.luogu.com.cn/problem/P6205 完全背包
1065 http://acm.hdu.edu.cn/showproblem.php?pid=1051 n 轮 LIS
1631 http://acm.hdu.edu.cn/showproblem.php?pid=1950 转换成 LIS
3666 https://www.luogu.com.cn/problem/P2893
https://codeforces.com/problemset/problem/13/C
https://codeforces.com/problemset/problem/713/C
https://www.luogu.com.cn/problem/P4597 加强版
2392 https://www.luogu.com.cn/problem/P6771 多重背包,按高度限制排序。高度既是价值也是体积
2184 https://www.luogu.com.cn/problem/P2340 把 IQ 看成体积,EQ 看成价值,注意把负数偏移到非负数,以及负数的转移写法
todo 3.4 节
2686 https://www.luogu.com.cn/problem/SP1700
1769 https://www.luogu.com.cn/problem/SP90 https://www.luogu.com.cn/problem/UVA1322
2441
3254 https://www.luogu.com.cn/problem/P1879
2836
1795 https://www.luogu.com.cn/problem/SP1776
3411 https://www.luogu.com.cn/problem/SP3953
3420
3735
3171 https://www.luogu.com.cn/problem/P4644 见 graph.shortestPathDijkstra
CSES DP section editorial https://codeforces.com/blog/entry/70018
力扣上的 DP 问题
分类汇总 https://zhuanlan.zhihu.com/p/126546914
https://leetcode.com/discuss/general-discussion/458695/dynamic-programming-patterns
https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20%E9%A2%98%E8%A7%A3%20-%20%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92.md
https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/discuss/108870/Most-consistent-ways-of-dealing-with-the-series-of-stock-problems
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/yi-ge-tong-yong-fang-fa-tuan-mie-6-dao-gu-piao-w-5/
https://leetcode-cn.com/tag/dynamic-programming/
AT 经典 DP 场 https://atcoder.jp/contests/dp
题解 https://www.cnblogs.com/shanxieng/p/10232228.html
https://codeforces.com/blog/entry/92170
https://www.hamayanhamayan.com/entry/2019/01/12/163853
信息学奥赛一本通 第二部分 基础算法 --> 第九章 动态规划 http://ybt.ssoier.cn:8088/index.php
算法竞赛专题解析(11):DP概述和常见DP面试题 https://blog.csdn.net/weixin_43914593/article/details/105444090
todo 题目推荐 https://www.luogu.com.cn/blog/wyy2020/dp-qian-tan
https://www.cnblogs.com/flashhu/p/9480669.html
其他资料:
https://github.com/hzwer/shareOI/tree/master/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92
https://oi-wiki.org/dp/
https://cp-algorithms.com/dynamic_programming/divide-and-conquer-dp.html
https://wenku.baidu.com/view/7c9de809581b6bd97f19ea72.html 算法合集之《从《鹰蛋》一题浅析对动态规划算法的优化》
*/
func dpCollections() {
min := func(a, b int) int {
if a < b {
return a
}
return b
}
max := func(a, b int) int {
if a > b {
return a
}
return b
}
abs := func(x int) int {
if x < 0 {
return -x
}
return x
}
// 涉及到前缀和/子数组和的问题
// 定义 dp[i] 表示前缀 a[:i] 中子数组和为 targetSum 的最短子数组长度
// 下面的代码来自 LC1477/双周赛28C https://leetcode-cn.com/problems/find-two-non-overlapping-sub-arrays-each-with-target-sum/
prefixSumDP := func(a []int, targetSum int) int {
n := len(a)
const inf int = 1e9
ans := inf
dp := make([]int, n+1)
for _i := range dp {
dp[_i] = inf
}
preSumPos := map[int]int{0: -1} // int64
sum := 0
for i, v := range a {
dp[i+1] = dp[i]
sum += v
if p, ok := preSumPos[sum-targetSum]; ok {
// sum_[p+1,i] == targetSum
l := i - p
if dp[p+1] < inf {
ans = min(ans, dp[p+1]+l)
}
dp[i+1] = min(dp[i+1], l)
}
preSumPos[sum] = i
}
if ans == inf {
ans = -1
}
return ans
}
// https://codeforces.com/problemset/problem/510/D
// 由于数据范围的原因,采用 map 记忆化 dpMap
mapDP := func(n int) {
{
dp := map[int]int{}
var f func(int) int
f = func(x int) (res int) {
//if x == 0 {
// return
//}
if v, ok := dp[x]; ok {
return v
}
defer func() { dp[x] = res }()
return
}
f(n)
}
{
type pair struct{ x, y int }
dp := map[pair]int{}
var f func(int, int) int
f = func(x, y int) (res int) {
//if x == n {
// return
//}
p := pair{x, y}
if v, ok := dp[p]; ok {
return v
}
defer func() { dp[p] = res }()
return
}
f(0, 0)
}
}
/* 线性 DP
① 前缀/后缀之间的转移,例如从 dp[i-1] 转移到 dp[i],或者从 dp[j] 转移到 dp[i] (j<i),这里 dp[i] 可以表示一个状态或一组状态等
力扣上有大量这类题目,例如:
198,213,123,309,376,276,931 (从dp[i-1] 转移到 dp[i])
487,1186 (从 dp[i-1] 转移到 dp[i],带一个额外的决策维度,长度一般是 2-4)
300,368,1105* (从 dp[j] 转移到 dp[i])
903/周赛101D https://leetcode-cn.com/problems/valid-permutations-for-di-sequence/ https://leetcode-cn.com/contest/weekly-contest-101/
② 双序列问题,一般定义 dp[i][j] 表示对子问题 (s1[:i],s2[:j]) 的求解结果
力扣题目 1143,1092,72,97,115,727,583,712,1035,1216,1312
983/周赛121C https://leetcode-cn.com/problems/minimum-cost-for-tickets/ https://leetcode-cn.com/contest/weekly-contest-121/
双周赛38D https://leetcode-cn.com/contest/biweekly-contest-38/problems/number-of-ways-to-form-a-target-string-given-a-dictionary/
③ 一些题目
最大整除子集 LC368 https://leetcode-cn.com/problems/largest-divisible-subset/
编辑距离 LC72 https://leetcode-cn.com/problems/edit-distance/
最高的广告牌 LC956/周赛114D https://leetcode-cn.com/problems/tallest-billboard/ https://leetcode-cn.com/contest/weekly-contest-114/
数字三角形 https://www.luogu.com.cn/problem/P1216
贪心+abs https://atcoder.jp/contests/abc163/tasks/abc163_e
LC1477/双周赛28C https://leetcode-cn.com/problems/find-two-non-overlapping-sub-arrays-each-with-target-sum/
看起来是区间 DP,仔细分析后是线性 DP https://leetcode-cn.com/contest/weekly-contest-199/problems/string-compression-ii/
好题:涉及到相邻状态先后关系的 DP(喂兔子) https://codeforces.com/problemset/problem/358/D
期望 DP https://codeforces.com/problemset/problem/235/B
期望 DP https://codeforces.com/problemset/problem/1097/D
https://codeforces.com/problemset/problem/446/A
https://codeforces.com/problemset/problem/603/A
https://codeforces.com/problemset/problem/1120/C
与 KMP 结合 https://codeforces.com/problemset/problem/1163/D
*/
// 最大子段和 https://www.luogu.com.cn/problem/P1115
// 有两种思路
// - 定义状态 dp[i] 表示以 a[i] 结尾的最大子段和,则有状态转移方程 dp[i]=max(dp[i−1],0)+a[i]
// - 遍历 a 的同时维护前缀和的最小值,则遍历到 a[i] 时,当前最大子段和为 sum[i]-min(sum[j]), j<i
// 算法导论 练习4.1-5
// [题型总结] 关于最大子段和及其变式 https://www.luogu.com.cn/blog/wey-yzyl/zui-tai-zi-duan-hu-ji-ji-bian-shi-di-qi-shi
// 子段长度有上限的最大子段和:见单调队列,题目为 https://ac.nowcoder.com/acm/contest/1006/D
// 子段长度有下限的最大子段和:转换为前缀和之差 sum[i]-sum[j],i-j>=K,维护 mi=min(sum[j]),同时更新 sum[i]-mi 的最大值(题目见 sort.go 中的 0-1 分数规划)
// 子段和有上限的最大子段和:转换为前缀和之差 sum[i]-sum[j]<=K,在平衡树上二分 sum[j] LC363 https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/
// 最大两段子段和:求每个位置上的前缀最大子段和和后缀最大子段和 https://www.luogu.com.cn/problem/P2642
// 最大 m 段子段和 http://acm.hdu.edu.cn/showproblem.php?pid=1024
// 环状最大子段和:转换为 max(最大子段和, 总和减去最小子段和) LC918 https://leetcode-cn.com/problems/maximum-sum-circular-subarray/
// 环状最大两段子段和:思路类似,注意取反后需要传入 a[1:n-1] https://www.luogu.com.cn/problem/P1121 https://ac.nowcoder.com/acm/contest/7738/B
// 变形题 https://codeforces.com/problemset/problem/788/A
// https://codeforces.com/problemset/problem/1155/D
// https://codeforces.com/problemset/problem/1373/D
// 多个小数组合并 https://codeforces.com/problemset/problem/75/D
// 这题做法需要用到上面说到的第二种思路
maxSubArraySum := func(a []int) int {
if len(a) == 0 {
return 0
}
dp, maxSubSum := a[0], a[0] // int64
for _, v := range a[1:] {
dp = max(dp, 0) + v
maxSubSum = max(maxSubSum, dp)
}
return max(maxSubSum, 0) // 若不允许非空,返回 maxSum
}
// 最大两段子段和(两段必须间隔至少 gap 个数)
maxTwoSubArraySum := func(a []int, gap int) int {
// 注意下界
n := len(a)
suf := make([]int, n) // int64
suf[n-1] = a[n-1]
curSum := a[n-1]
for i := n - 2; i >= 0; i-- {
v := a[i]
curSum = max(curSum+v, v)
suf[i] = max(suf[i+1], curSum)
}
curSum, pre := a[0], a[0]
ans := pre + suf[1+gap]
for i := 1; i < n-1-gap; i++ {
v := a[i]
curSum = max(curSum+v, v)
pre = max(pre, curSum)
ans = max(ans, pre+suf[i+1+gap])
}
return ans
}
maxSubArrayAbsSum := func(a []int) int {
if len(a) == 0 {
return 0
}
//min, max, abs := math.Min, math.Max, math.Abs
curMaxSum, maxSum := a[0], a[0]
curMinSum, minSum := a[0], a[0]
for _, v := range a[1:] {
curMaxSum = max(curMaxSum+v, v)
maxSum = max(maxSum, curMaxSum)
curMinSum = min(curMinSum+v, v)
minSum = min(minSum, curMinSum)
}
return max(abs(maxSum), abs(minSum))
}
// 最大子序列交替和(买卖股票)
// 有两种思路:
// - 动态规划,具体见我的题解 https://leetcode-cn.com/problems/maximum-alternating-subsequence-sum/solution/dong-tai-gui-hua-by-endlesscheng-d92a/
// - 贪心,由于第一个值需要取正,将开头补上 0,就变成买卖股票问题了,只需关心波峰和波谷的值,即 ∑max(0,a[i+1]-a[i])
// LC1911/双周赛55C https://leetcode-cn.com/problems/maximum-alternating-subsequence-sum/
// LC122 https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
// 扩展:O(1) 回答交换其中两个元素后的最大子序列交替和 https://codeforces.com/problemset/problem/1420/C2
maxAlternatingSumDP := func(a []int) int {
dp := [2]int{0, -1e9} // int64
for _, v := range a {
dp = [2]int{max(dp[0], dp[1]-v), max(dp[1], dp[0]+v)}
}
return dp[1]
}
maxAlternatingSumGreedy := func(a []int) (ans int) {
a = append([]int{0}, a...)
for i := 1; i < len(a); i++ {
ans += max(0, a[i]-a[i-1]) // int64
}
return
}
// 修改序列为非降或非增的最小修改次数
// 单次修改可以把某个数 +1 或 -1
// https://www.luogu.com.cn/problem/solution/P4597
// 通过一个例子来解释这个基于堆的算法:1 5 10 4 2 2 2 2
// 假设当前维护的是非降序列,前三个数直接插入,不需要任何修改
// 插入 4 的时候,可以修改为 1 5 5 5,或 1 5 6 6,或... 1 5 10 10,修改次数均为 6
// 但我们也可以把修改后的序列视作 1 5 4 4,虽然序列不为非降序列,但修改的次数仍然为 6
// 接下来插入 2,基于 1 5 5 5 的话,修改后的序列就是 1 5 5 5 5,总的修改次数为 9
// 但我们也可以把修改后的序列视作 1 2 4 4 2,总的修改次数仍然为 9
// 接下来插入 2,如果基于 1 5 5 5 5 变成 1 5 5 5 5 5,会得到错误的修改次数 12
// 但是实际上有更优的修改 1 4 4 4 4 4,总的修改次数为 11
// 同上,把这个序列视作 1 2 2 4 2 2,总的修改次数仍然为 11
// ...
// https://www.luogu.com.cn/problem/P2893 http://poj.org/problem?id=3666
// https://codeforces.com/problemset/problem/13/C
// https://codeforces.com/problemset/problem/713/C 严格单调递增 https://codeforces.com/blog/entry/47094?#comment-315161
// 这道题做了一个 a[i]-=i 的操作(i 从 1 开始),把严格单调递增变成了非降的情况,从而可以应用该算法
// 这一技巧的原理是,对于整数来说,单调递增的最小情况是 y=x+C,减去这一函数,就得到了非降序列的最小情况 y=C
// https://www.luogu.com.cn/problem/P4597 (加强版)
minCostSorted := func(a []int) int64 {
h := hp{} // 大根堆
ans := int64(0)
for _, v := range a {
h.push(v)
if d := h.IntSlice[0] - v; d > 0 {
ans += int64(d)
h.IntSlice[0] = v
heap.Fix(&h, 0)
}
}
return ans
}
// 最长公共子序列 (LCS)
// 有向无环图:s1[i] == s2[j] (i-1,j-1) -> (i,j) $ 1
// s1[i] != s2[j] (i-1,j) -> (i,j) $ 0
// (i,j-1) -> (i,j) $ 0
// 例题 LC1143 https://leetcode-cn.com/problems/longest-common-subsequence/
// EXTRA: 最短公共超序列 (SCS) LC1092 https://leetcode-cn.com/problems/shortest-common-supersequence/
// 变种 LC97 https://leetcode-cn.com/problems/interleaving-string/
// LC115 https://leetcode-cn.com/problems/distinct-subsequences/
// LC583 https://leetcode-cn.com/problems/delete-operation-for-two-strings/
// LC712 https://leetcode-cn.com/problems/minimum-ascii-delete-sum-for-two-strings/
// LC1035 https://leetcode-cn.com/problems/uncrossed-lines/
// LC1312 https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/ https://www.luogu.com.cn/problem/P1435
// 其中一个改为子串 https://codeforces.com/problemset/problem/163/A
// https://codeforces.com/problemset/problem/1446/B
// 与 KMP 结合 https://codeforces.com/problemset/problem/346/B
// 若其中一个序列无重复元素,可以转换成 LIS https://www.luogu.com.cn/problem/P1439 LC1713/周赛222D https://leetcode-cn.com/contest/weekly-contest-222/problems/minimum-operations-to-make-a-subsequence/
lcs := func(s, t []byte) int {
// dp[i][j] = LCS(s[:i], t[:j])
n, m := len(s), len(t)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, m+1)
}
for i, v := range s {
for j, w := range t {
if v == w {
// ignore values from dp[i][j+1] and dp[i+1][j]
dp[i+1][j+1] = dp[i][j] + 1
} else {
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j])
}
}
}
{
// EXTRA: 某些 dp 非单调性的题目需要计算全局最值
allMax := 0
for _, row := range dp {
for _, v := range row {
allMax = max(allMax, v)
}
}
}
return dp[n][m]
}
lcsPath := func(s, t []byte) []byte {
n, m := len(s), len(t)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, m+1)
}
fa := make([][]int8, n+1)
for i := range fa {
fa[i] = make([]int8, m+1)
}
for i, v := range s {
for j, w := range t {
if v == w {
dp[i+1][j+1] = dp[i][j] + 1
fa[i+1][j+1] = 1
} else {
if dp[i][j+1] > dp[i+1][j] {
dp[i+1][j+1] = dp[i][j+1]
fa[i+1][j+1] = 2
} else {
dp[i+1][j+1] = dp[i+1][j]
fa[i+1][j+1] = 3
}
}
}
}
lcs := make([]byte, 0, dp[n][m])
var makeLCS func(i, j int)
makeLCS = func(i, j int) {
if i == 0 || j == 0 {
return
}
if fa[i][j] == 1 {
makeLCS(i-1, j-1)
lcs = append(lcs, s[i-1])
} else if fa[i][j] == 2 {
makeLCS(i-1, j)
} else {
makeLCS(i, j-1)
}
}
makeLCS(n, m)
return lcs
}
// 最长回文子序列 (LPS)
// LC516 https://leetcode-cn.com/problems/longest-palindromic-subsequence/
// LC1216/双周赛10D https://leetcode-cn.com/contest/biweekly-contest-10/problems/valid-palindrome-iii/
longestPalindromeSubsequence := func(s string) int {
n := len(s)
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
}
for i := n - 1; i >= 0; i-- {
dp[i][i] = 1
for j := i + 1; j < n; j++ {
if s[i] == s[j] {
dp[i][j] = dp[i+1][j-1] + 2
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
}
}
}
return dp[0][n-1]
}
// 最长上升子序列 (LIS)
// O(n^2) - 定义 dp[i] 为以 a[i] 为末尾的 LIS 的长度
// 可以把此问题想象成一个「跳跃游戏」,任选一个初始位置向右跳跃,每次只能跳到比当前位置更高的位置,问最多能跳多少次(最后答案加一)
// 这样能更容易地看出转移的顺序,然后变成一个 DAG 上求最长路的问题
// 转换 http://acm.hdu.edu.cn/showproblem.php?pid=1950
// 变体 https://codeforces.com/problemset/problem/1350/B
//【网络流 24 题】能取出多少个长为 len(LIS) 的不相交子序列 https://loj.ac/p/6005 https://www.luogu.com.cn/problem/P2766
lisSlow := func(a []int) (ans int) {
n := len(a)
dp := make([]int, n)
for i, v := range a {
dp[i] = 1
for j, w := range a[:i] {
if w < v { // 改成 <= 为非降
dp[i] = max(dp[i], dp[j]+1)
}
}
ans = max(ans, dp[i])
}
return
}
// 最长上升子序列 (LIS)
// O(nlogn) - 定义 dp[i] 为长度为 i+1 的 LIS 末尾元素的最小值
// 求下降,可以考虑取相反数
// https://oi-wiki.org/dp/basic/#_12
// 最小划分数 / 狄尔沃斯定理(Dilworth's theorem)https://en.wikipedia.org/wiki/Dilworth%27s_theorem
// 偏序集的最少反链划分数等于最长链的长度
// 随机排列 LIS 的长度期望 https://www.zhihu.com/question/266958886
//
// 最小划分数(导弹拦截)https://www.luogu.com.cn/problem/P1020
// 转化成最小划分数+打印划分方案 https://codeforces.com/problemset/problem/1296/E2
// 例题 LC300 https://leetcode-cn.com/problems/longest-increasing-subsequence/
// 建模 https://codeforces.com/problemset/problem/269/B
// 方案数 LC673 https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/
// https://www.zhihu.com/question/34905638
// 合唱队形 https://www.luogu.com.cn/problem/P1091
// 合唱队形(至少有升有降)https://leetcode-cn.com/contest/biweekly-contest-40/problems/minimum-number-of-removals-to-make-mountain-array/
// LC354 俄罗斯套娃信封问题 https://leetcode-cn.com/problems/russian-doll-envelopes/
// 将所有元素分成三类:不在任何 LIS / 在至少一个 LIS / 在所有 LIS https://codeforces.com/problemset/problem/486/E
// 重复 T 次的 LIS 问题 https://codeforces.com/problemset/problem/582/B
// 若其中一个序列无重复元素,LCS 可以转换成 LIS https://www.luogu.com.cn/problem/P1439 LC1713/周赛222D https://leetcode-cn.com/contest/weekly-contest-222/problems/minimum-operations-to-make-a-subsequence/
// 二维 LIS:在一维 LIS 的基础上,a[i] 可以从多个数中选一个,问 LIS 最长可以多长
// 思路:将各个 a[i] 的可选项从大到小排序,然后拼接成一个序列,求 LIS 即可(关键:从大到小排序避免了在同一个可选项中选择多个元素)
lis := func(a []int) int {
dp := []int{}
for _, v := range a {
if p := sort.SearchInts(dp, v); p < len(dp) { // 改成 v+1 为非降
dp[p] = v
} else {
dp = append(dp, v)
}
}
return len(dp)
}
// 每个前缀的 LIS
lisAll := func(a []int) []int {
n := len(a)
lis := make([]int, n)
dp := make([]int, 0, n)
for i, v := range a {
p := sort.SearchInts(dp, v)
if p < len(dp) { // 改成 v+1 为非降
dp[p] = v
} else {
dp = append(dp, v)
}
lis[i] = p + 1
}
return lis
}
// LIS 相关构造题
// https://codeforces.com/problemset/problem/1304/D
// https://atcoder.jp/contests/arc091/tasks/arc091_c
// 最大上升子序列和
// 按值从小到大排序,值相同的下标从大到小排序
// 然后用树状数组或线段树:单点更新,维护前缀最大值
// https://www.acwing.com/problem/content/3665/
// 最长公共上升子序列 (LCIS)
// https://www.acwing.com/problem/content/274/
// https://codeforces.com/problemset/problem/10/D
lcis := func(a, b []int) int {
n, m := len(a), len(b)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, m)
}
for i, v := range a {
mx := 0
for j, w := range b {
if v == w {
dp[i+1][j] = mx + 1
} else {
dp[i+1][j] = dp[i][j]
}
if w < v {
mx = max(mx, dp[i][j])
}
}
}
ans := 0
for _, v := range dp[n] {
ans = max(ans, v)
}
return ans
}
// LCIS 打印方案
lcisPath := func(a, b []int) (ans int, lcis []int) {
n, m := len(a), len(b)
dp := make([][]int, n+1)
fa := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, m)
fa[i] = make([]int, m)
}
for i, v := range a {
mx, k := 0, -1
for j, w := range b {
if v == w {
dp[i+1][j] = mx + 1
fa[i+1][j] = k // k < j
} else {
dp[i+1][j] = dp[i][j]
fa[i+1][j] = j
}
if w < v && dp[i][j] > mx {
mx, k = dp[i][j], j
}
}
}
ansJ := 0
for j, dv := range dp[n] {
if dv > dp[n][ansJ] {
ansJ = j
}
}
ans = dp[n][ansJ]
var getLCIS func(i, j int)
getLCIS = func(i, j int) {
if i == 0 || j < 0 {
return
}
getLCIS(i-1, fa[i][j])
if fa[i][j] < j {
lcis = append(lcis, b[j])
}
}
getLCIS(n, ansJ)
return
}
// 长度为 m 的 LIS 个数
// 赤壁之战 https://www.acwing.com/problem/content/299/
// 定义 dp[i][j] 表示 a[:j+1] 的长度为 i 且以 a[j] 结尾的 LIS
// 则有 dp[i][j] = ∑dp[i-1][k] (k<j && a[k]<a[j])
// 注意到当 j 增加 1 时,只多了 k=j 这一个新决策,这样可以用树状数组来维护
// 复杂度 O(mnlogn)
countLIS := func(a []int, m int) int {
// 将 a 离散化成从 2 开始的序列
b := append([]int(nil), a...)
sort.Ints(b)
for i, v := range a {
a[i] = sort.SearchInts(b, v) + 2
}
n := len(a)
const mod int = 1e9 + 7
tree := make([]int, n+2)
add := func(i, val int) {
for ; i < n+2; i += i & -i {
tree[i] = (tree[i] + val) % mod
}
}
sum := func(i int) (res int) {
for ; i > 0; i &= i - 1 {
res = (res + tree[i]) % mod
}
return
}
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n)
}
for i := 1; i <= m; i++ {
tree = make([]int, n+2)
if i == 1 {
add(1, 1)
}
for j, v := range a {
dp[i][j] = sum(v - 1)
add(v, dp[i-1][j])
}
}
ans := 0
for _, v := range dp[m] {
ans = (ans + v) % mod
}
return ans
}
// 本质不同子序列个数
// 定义 dp[i][j] 表示前 i 个字符中长度为 j 的本质不同子序列个数
// 转移 dp[i][j] = dp[i-1][j](不选第 i 个字符)+ dp[i-1][j-1] - dp[prev[i]-1][j-1](选第 i 个字符)
// 其中 prev[i] 为 s[i] 的上一个相同字符位置
// https://ac.nowcoder.com/acm/contest/4853/C 题解 https://ac.nowcoder.com/discuss/394080
// https://codeforces.com/problemset/problem/1183/H
distinctSubsequence := func(s string) int64 {
n := len(s)
prev := [26]int{}
dp := make([][]int64, n+1)
for i := range dp {
dp[i] = make([]int64, n+1)
}
dp[0][0] = 1
for i := 1; i <= n; i++ {
c := s[i-1] - 'a'
dp[i][0] = 1
for j := 1; j <= i; j++ {
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
if p := prev[c]; p > 0 {
dp[i][j] -= dp[p-1][j-1]
}
}
prev[c] = i
}
sum := int64(0)
for _, cnt := range dp[n][1:] { // 不计入空字符串
sum += cnt
}
return sum
}
// 回文串最小分割次数
// 紫书例题 9-7,UVa 11584 https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=27&page=show_problem&problem=2631
// LC132 https://leetcode-cn.com/problems/palindrome-partitioning-ii/
minPalindromeCut := func(s string) int {
n := len(s)
g := make([][]bool, n)
for i := range g {
g[i] = make([]bool, n)
for j := range g[i] {
g[i][j] = true
}
}
for i := n - 1; i >= 0; i-- {
for j := i + 1; j < n; j++ {
g[i][j] = s[i] == s[j] && g[i+1][j-1]
}
}
f := make([]int, n)
for i := range f {
if g[0][i] { // f[i] = 0
continue
}
f[i] = int(1e9)
for j := 0; j < i; j++ {
if g[j+1][i] {
f[i] = min(f[i], f[j]+1)
}
}
}
return f[n-1]
}
/* 背包问题
这类问题可以从物品选择次序的无后效性入手
子区间 -> 前缀和
子序列 -> 背包
https://en.wikipedia.org/wiki/Knapsack_problem
https://codeforces.com/blog/entry/59606
浅谈 ZKP 问题 https://www.luogu.com.cn/blog/xww666/qian-tan-zkp-wen-ti-gai-post
另见 math_ntt.go 中的生成函数
NOTE: 若求能否凑成 1,2,3,...,M,只需判断 dp[i] 是否为正 LC1049 https://leetcode-cn.com/problems/last-stone-weight-ii/
套题 https://www.acwing.com/problem/
混合背包 https://www.luogu.com.cn/problem/P1833
*/
// 0-1 背包 (n 个物品,背包容量为 maxW)
// 状态:从前 i 个物品中选择若干个,当容量限制为 j 时能获得的最大价值和 i∈[0,n-1], j∈[0,maxW]
// 初始值:f(0,j)=0 j∈[0,maxW]
// 除开初始状态,每个状态有两个来源,决策为 max:
// - 不选第 i 个物品:f(i-1,j) -> f(i,j)
// - 选第 i 个物品:f(i-1,j-wi)+vi -> f(i,j) j≥wi
// 最优解为 f(n-1,maxW)
// https://oi-wiki.org/dp/knapsack/
// 模板题 https://www.luogu.com.cn/problem/P1048 https://atcoder.jp/contests/dp/tasks/dp_d
// 转换 LC1049 https://leetcode-cn.com/problems/last-stone-weight-ii/
// 转换 https://codeforces.com/problemset/problem/1381/B
// 打印方案 https://codeforces.com/problemset/problem/864/E
// NOIP06·提高 金明的预算方案(也可以用树上背包做)https://www.luogu.com.cn/problem/P1064
// EXTRA: 恰好装满(相当于方案数不为 0)LC416 https://leetcode-cn.com/problems/partition-equal-subset-sum/
// 必须定义成恰好装满(紫书例题 9-5,UVa 12563)https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=441&page=show_problem&problem=4008
// EXTRA: 背包容量为 0 https://codeforces.com/problemset/problem/366/C
// EXTRA: 二维费用 https://www.acwing.com/problem/content/8/ https://www.luogu.com.cn/problem/P1507 LC474 https://leetcode-cn.com/problems/ones-and-zeroes/
// EXTRA: 把一个维度转换成 DP 的定义 https://codeforces.com/problemset/problem/837/D
// EXTRA: 离散化背包 https://codeforces.com/contest/366/submission/61452111
zeroOneKnapsack := func(values, weights []int, maxW int) int {
dp := make([]int, maxW+1) // int64
for i, v := range values {
w := weights[i]
for j := maxW; j >= w; j-- {
dp[j] = max(dp[j], dp[j-w]+v)
}
}
return dp[maxW]
}
// 0-1 背包 EXTRA: 至少装满 https://www.luogu.com.cn/problem/P4377
// 二维费用的情况+价值最小 https://ac.nowcoder.com/acm/contest/6218/C
zeroOneKnapsackAtLeastFillUp := func(values, weights []int, maxW int) int {
dp := make([]int, maxW+1) // int64
for i := range dp {
dp[i] = -1e18 // 价值最小改成 1e18
}
dp[0] = 0
for i, v := range values {
w := weights[i]
for j := maxW; j >= 0; j-- {
dp[j] = max(dp[j], dp[max(j-w, 0)]+v) // max(j-w, 0) 蕴含了「至少」
}
}
{
// 另一种写法
for i, v := range values {
w := weights[i]
for j := maxW; j >= 0; j-- {
k := min(j+w, maxW)
dp[k] = max(dp[k], dp[j]+v)
}
}
}
return dp[maxW]
}
// 0-1 背包 EXTRA: 从序列 a 中选若干个数,使其总和为 sum 的方案数
// NOTE: 1,1,1,...1(32个1),s-32,s-31,...,s 可以让方案数恰好为 2^32
// 二维+上限+下限 LC879/周赛95D https://leetcode-cn.com/contest/weekly-contest-95/problems/profitable-schemes/
// 转换 https://atcoder.jp/contests/abc169/tasks/abc169_f
// 转换 https://codeforces.com/problemset/problem/478/D
// 转换 LC494 https://leetcode-cn.com/problems/target-sum/
// 转换 LC1434 https://leetcode-cn.com/problems/number-of-ways-to-wear-different-hats-to-each-other/
// 由于顺序不同也算方案,所以这题需要正序递推 LC377 https://leetcode-cn.com/problems/combination-sum-iv/
zeroOneWaysToSum := func(a []int, sum int) int {
dp := make([]int, sum+1) // int64
dp[0] = 1
for _, v := range a {
for s := sum; s >= v; s-- {
dp[s] += dp[s-v] // % mod
}
}
return dp[sum]
}
// 0-1 背包 EXTRA: 打印字典序最小的方案
// 倒序遍历物品,同时用 fa 数组记录转移来源,这样跑完 DP 后,从第一个物品开始即可得到字典序最小的方案
// https://www.acwing.com/problem/content/description/12/
zeroOneKnapsackLexicographicallySmallestResult := func(values, weights []int, maxW int) (ans []int) {
n := len(values)
dp := make([]int, maxW+1) // int64 fill
//dp[0] = 0
fa := make([][]int, n)
for i := n - 1; i >= 0; i-- {
fa[i] = make([]int, maxW+1)
for j := range fa[i] {
fa[i][j] = j
}
v, w := values[i], weights[i]
for j := maxW; j >= w; j-- {
if dp[j-w]+v >= dp[j] { // 注意这里要取等号,从而保证尽可能地从字典序最小的方案转移过来
dp[j] = dp[j-w] + v
fa[i][j] = j - w
}
}
}
for i, j := 0, maxW; i < n; {
if fa[i][j] == j {
i++
} else {
ans = append(ans, i+1) // 下标从 1 开始
j = fa[i][j]
i++ // 完全背包的情况,这行去掉
}
}
return
}
// 0-1 背包 EXTRA: 价值主导的 0-1 背包
// 把重量看成价值,价值看成重量,求同等价值下能得到的最小重量,若该最小重量不超过背包容量,则该价值合法。所有合法价值的最大值即为答案
// https://atcoder.jp/contests/dp/tasks/dp_e
// 完全背包
// 转换 LC322 https://leetcode-cn.com/problems/coin-change/
// EXTRA: 恰好装满+打印方案 LC1449/双周赛26D https://leetcode-cn.com/contest/biweekly-contest-26/problems/form-largest-integer-with-digits-that-add-up-to-target/
unboundedKnapsack := func(values, weights []int, maxW int) int {
dp := make([]int, maxW+1) // int64 fill
//dp[0] = 0
for i, v := range values {
w := weights[i]
for j := w; j <= maxW; j++ {
dp[j] = max(dp[j], dp[j-w]+v)
}
}
return dp[maxW]
}
// 完全背包 EXTRA: 方案数
// LC518 https://leetcode-cn.com/problems/coin-change-2/
// https://www.luogu.com.cn/problem/P1832
// https://www.luogu.com.cn/problem/P6205(需要高精)
// 类似完全背包但是枚举的思路不一样 LC377 https://leetcode-cn.com/problems/combination-sum-iv/
unboundedWaysToSum := func(a []int, sum int) int {
dp := make([]int, sum+1) // int64
dp[0] = 1
for _, v := range a {
for s := v; s <= sum; s++ {
dp[s] += dp[s-v] // % mod
}
}
return dp[sum]
}
// 完全背包 EXTRA: 二维费用方案数
// 注意:「恰好使用 m 个物品」这个条件要当成一种费用来看待
// https://codeforces.com/problemset/problem/543/A
// 多重背包计数(可以用前缀和优化)
// https://www.luogu.com.cn/problem/P1077
// 多重背包 - 未优化
boundedKnapsack := func(values, stocks, weights []int, maxW int) int {
n := len(values)
dp := make([][]int, n+1) // int64
for i := range dp {
dp[i] = make([]int, maxW+1)
}
for i, vi := range values {
si, wi := stocks[i], weights[i]
for j := range dp[i] {
for k := 0; k <= si && k*wi <= j; k++ {
dp[i+1][j] = max(dp[i+1][j], dp[i][j-k*wi]+k*vi)
}
}
}
return dp[n][maxW]
}
// 多重背包 - 优化 1 - 二进制优化
// 模板题 https://codeforces.com/problemset/problem/106/C
// todo 多重背包+完全背包 https://www.luogu.com.cn/problem/P1782 https://www.luogu.com.cn/problem/P1833 https://www.luogu.com.cn/problem/P2851
// http://acm.hdu.edu.cn/showproblem.php?pid=2844 http://poj.org/problem?id=1742
// https://www.luogu.com.cn/problem/P6771 http://poj.org/problem?id=2392
// https://codeforces.com/contest/999/problem/F
boundedKnapsackBinary := func(values, stocks, weights []int, maxW int) int {
dp := make([]int, maxW+1) // int64
for i, v := range values {
num, w := stocks[i], weights[i]
for k := 1; num > 0; k <<= 1 {
K := min(k, num)
for j := maxW; j >= K*w; j-- {
dp[j] = max(dp[j], dp[j-K*w]+K*v)
}
num -= K
}
}
return dp[maxW]
}
// 多重背包 - 优化 2 - 单调队列优化
// todo 挑战 P340
// 分组背包
// https://www.acwing.com/problem/content/9/
// https://www.luogu.com.cn/problem/P1757
type item struct{ v, w int }
groupKnapsack := func(groups [][]item, maxW int) int {
dp := make([]int, maxW+1) // int64
for _, g := range groups {
for j := maxW; j >= 0; j-- {
for _, it := range g {
if v, w := it.v, it.w; w <= j {
dp[j] = max(dp[j], dp[j-w]+v)
}
}
}
}
return dp[maxW]
}
// 树上背包/树形背包/依赖背包
// todo 树上背包的上下界优化 https://ouuan.gitee.io/post/%E6%A0%91%E4%B8%8A%E8%83%8C%E5%8C%85%E7%9A%84%E4%B8%8A%E4%B8%8B%E7%95%8C%E4%BC%98%E5%8C%96/
// 子树合并背包的复杂度证明 https://blog.csdn.net/lyd_7_29/article/details/79854245
// 复杂度优化 https://loj.ac/d/3144
// https://zhuanlan.zhihu.com/p/103813542
//
// todo https://loj.ac/p/160
// https://www.luogu.com.cn/problem/P2014 https://www.acwing.com/problem/content/10/ https://www.acwing.com/problem/content/288/
// 加强版 https://www.luogu.com.cn/problem/U53204
// https://www.luogu.com.cn/problem/P1272
// 加强版 https://www.luogu.com.cn/problem/U53878
// https://www.luogu.com.cn/problem/P3177
// NOIP06·提高 金明的预算方案 https://www.luogu.com.cn/problem/P1064
treeKnapsack := func(g [][]int, items []item, root, maxW int) int {
var f func(int) []int
f = func(v int) []int {
it := items[v]
dp := make([]int, maxW+1) // int64
for i := it.w; i <= maxW; i++ {