综合算法题
收录一些平时做的比较综合的题目. 考察的知识点不止一点, 各种优化方式简直使出毕生所学.
大概是蓝桥杯国赛, ACM银, 提高组以上难度, 是比赛你不会轻易去碰的题目, 讲解起来也是额外麻烦. 虽然说比赛能保证签到题全对就已经很了不起了, 但这种题目就像高考数学的压轴题, 还是要有所接触才能有所突破嘛.
裁剪序列
题目
导航: AcWing 299. 裁剪序列
题目描述
给定一个长度为 \(N\) 的序列 \(A\),要求把该序列分成若干段,在满足“每段中所有数的和”不超过 \(M\) 的前提下,让“每段中所有数的最大值”之和最小。
试计算这个最小值.
输入格式
第一行包含两个整数 \(N\) 和 \(M\).
第二行包含 \(N\) 个整数,表示完整的序列 \(A\).
输出格式
输出一个整数,表示结果。
如果结果不存在,则输出-1。
数据范围
\[0 \leqslant N \leqslant 10^5\]
\[0 \leqslant M \leqslant 10^{11}\]
序列 \(A\) 中的数非负, 且不超过 \(10^6\).
输入样例:
1 | 8 17 |
输出样例:
1 | 12 |
样例解释:
按照如此分组, 第一组中的最大值为2, 第二组中的最大值为8, 第三组的最大值为2, 总和 2+8+2=12, 为最小值.
题解
TAG: DP 双指针 贪心 单调队列 堆 STL Multiset
朴素DP
本题可以考虑区间DP的处理方式:
- 状态表示:
- 集合:
f[i]
表示前i
个数中所有的划分区间中的最大值之和. - 状态:
min
- 集合:
- 状态计算:
- 划分区间:
1~j
和j+1~i
1~j
不管- 第二段的和要不超过
M
- 前一段区间很好表示:
f[j]
- 后一段区间: 是
j+1~i
这一段区间上的最大值 f[i] = min(f[j] + max A[k]), k in [j+1, i]
f[n]
就是答案.
- 划分区间:
例如 i = 6
, 可能的分组有 2 2 2 8 1
8
, 2 2 2 8
1 8
,
2 2 2
8 1 8
...而 f[i]
是这些方案中的最小值, 也即 2 2 2
8 1 8
分组下的 10
.
枚举 i
, 再枚举 j
, 还要枚举
j+1~i
这段上的 k
, 这种方法的时间复杂度是
O(n^3)
. 必定会TLE. 所以得考虑优化.
如果想不出优化方式过几个数据就作罢.
注: 在进一步优化之前请务必理解DP计算的正确性.
优化
j
可以从小到大枚举, 开一个变量就可以存最大的
a[k]
, 这样复杂度能压到 O(n^2)
. 但还是会
TLE.
定理 1 如果 j
能够成为
f[i]
的唯一最优解, 那么 j
必须满足以下条件之一:
A[j] = max(A[j], A[j+1], ..., A[i])
A[j] + ... + A[i] > M
且A[j+1] + ... + A[i] <= M
证明 考虑反证法. 假设 \(j\) 上述两个条件都不满足. 对于决策
j-1
:
A[j] + ... + A[i] <= M
, 考虑两种情况:A[j-1] + ... + A[i] > M
, 此时j-1
满足上述条件之一.A[j-1] + ... + A[i] <= M
:
f[i]
单调递增.- 同时
A[j]
不是A[j...i]
中的最大值.- 那么
f[j-1] + max A[j...i] <= f[j] + max A[j+1...i]
- 那么
j-1
是一个优于j
或和j
一样优的方案.
由假设可知不成立. 因此 j
必须满足上面条件之一. \(\Box\)
那么我们的问题就转换为: 对于一个 i
,
找到一个满足上述条件的 j
, 且 j
距离
i
最远. i
和 j
是随同变化的,
用双指针就可以压到一维.
不过使用双指针之前还要验证单调性, 也就是移动 i
,
j
也会相应向后移动(无回溯). 不过这不是什么大的问题,
我们给出一个简单的证明:
定理 2 j
在 i
变大的过程中同步增大.
证明 考虑反证法: 如果 i
移动到
i'
而 j
不往后移动, 因为 j
是满足
A[j+1] + ... + A[i] <= M
最小的一个点, 必然有
A[j+1] + ... + A[i'] > M
不满足条件, 因此 j
必须往后移动, 即 j' > j
. \(\Box\)
有了这一点, 我们又得到一个性质: [j, i]
这段区间实际上是一个滑动区间, 尽管长度不定,
但最主要的是, i
和 j
都是单调的. 并且,
我们实现的核心,
是在这个区间内寻找区间最值——本质上这就是一个单调队列!
也即, 我们可以用一个单调队列来维护滑动窗口中出现的最大值,
并使得这段区间的总和小于 m
.
同时我们需要维护一个方案的集合, 使得集合能够动态求最小值, 添加一个数,
删除一个数(头节点和尾节点). 你可能会想到使用堆存储,
但堆不太方便同时存储头节点和尾节点, 最好用一个平衡树来维护,
C++STL里可以用 multiset
.
代码
1 |
|
送信
导航: B 送信
题目描述
某人写了 \(n\) 封信和 \(n\) 个信封,如果所有的信都装错了信封。求所有信都装错信封共有多少种不同情况。
输入描述
一行一个 \(n\). \(n \leqslant 10^9\).
输出描述
输出方案数对 \(10^9+7\) 取模.
样例 1
输入
1 | 2 |
输出
1 | 1 |
样例 2
输入
1 | 114514 |
输出
1 | 977005016 |
题解
TAG: 组合数学 DP优化 分段打表
朴素做法
经典的错排问题.
打表程序:
1 |
|
直接交 TLE. \(O(n)\) 的复杂度, 但是常数还是挺大的, 1s 跑不完 1e9 的数据. 显然我们要用打表优化.
分块打表
关键是 n
离谱的大, 朴素打表不适用, 只能用分段打表,
大体思路是: 把数据范围分成多份, 预处理每一块的信息,
不满一块就暴力计算.
我们把 1e9 的数据分成 10 份 1e8 的块. 如果你有力气, 可以分块成 1000
份 1e6 的块, 然后打表. 预处理之后, 要计算例如 3e8 + 3
,
那么直接从表里找到 3e8 + 1
和
3e8 + 2
(你也可以写 10 个 elif), 然后按公式计算即可.
这题还有个坑: 考场上有一个样例 n = 0
, 这不欺负人嘛.
代码
1 |
|