Codeforces Round #670 (Div. 2) 题解
[CF1406A] Subset Mex 提交地 …
May all the beauty be blessed.
清流真可爱!
昨天晚上 20 点,我在补白天翘掉了的大学物理课,但是非常困,感觉完全听不进去课,于是决定躺床上休息 15 分钟。
然而,我再睁眼的时候已经是第二天 0 点了,此时的我还穿着前一天白天出去买菜的衣服,没有洗漱,随意地躺在床上。我本想就这么睡过去,第二天早起再洗漱换衣服。
其结果是,我并没能睡着,反而非常精神。翻来覆去折腾了 1h,最终放弃睡觉,开始玩手机。就这么一直玩完了躺着,躺够了又开始玩,遍历了手机上每一个有我感兴趣内容的应用,实在无聊至极。
由于最开始的策略是想尽量睡觉,加上不想打扰到还在睡觉的老妈,就一直在床上呆着,最多玩玩手机,并不打算起床干正事。谁能想到我就这么在床上浪费了 6h 的生命呢。
那么问题来了,今天早上 6 点,我总算是熬到头了,今天的一天要怎么办?是等到困了去补觉,继续着这么不规律的作息;还是继续熬着,但按时睡觉?
高三的我每天都非常累,而这是有原因的:每天早上 5:55 起床,坐上 6:05 的出租车,在 6:15 之前到教室,然后读上约 15min 的书,会站着不由自主地进入浅睡眠状态。其后的第一、二节课基本也是如此,只有在吃饭或睡够了的时候才能稍微清醒一些。
由于停课缺课的原因,我比较缺乏训练,做题的速度不太行,每天的作业都写的很慢。每天的数学作业总是在上午布置,下午上课前提交,因此必须在午休的 1.5h 内写完。而我即使是整个午休撑住不睡也写不完这些作业,而且午休的后半部分还会因为实在太困效率进一步下降,写作业效果更差了。
由于午休并没很好地休息,下午的开头几节课仍然很困,听不进去什么东西,一直熬到晚饭后的晚自习。学校只安排了约 1h 晚自习时间用于自由写作业,剩下的约 2h 其实是在讲课或考试,因此每天的作业也基本写不完。
带着这么写写不完的作业回家,我每天都有很强的罪恶感,因此到家之后还是会稍微补一段时间的作业。晚自习通常 22:00 放,到家约 22:30,洗漱完毕就到了 23:00,再写一会作业,很容易就到第二天了。
除了写作业之外,我每天还会整理当天产生的错题,或找以前以往的错题重新做一遍。具体而言,整理错题需要用手机拍照,再打印下来,而重新做错题则是从以往的打印稿中裁一道题出来贴在错题本上,再在空白处重新解答。这其实是相当耗时间的,一方面排版和打印需要一定的时间,另一方面,由于订正时通常比第一遍做更认真详细,所以耗时会比常规做题长一些。
睡前还会玩一会手机,等真正睡着可能已经 1:30 了,而第二天又需要在 5:55 起床,每天的睡眠只有这么可怜的不到 5h,第二天又是困倦的一天呢。
这是一个恶性循环,长此以往,不仅学习效率没有提高,身体也会因为缺乏睡眠而变差,所以应该怎么破解这个循环?
我是一个喜欢水群的人,而群总是在晚上最活跃。每天晚上,我都能水满整个活跃期,并把作业放在这之后完成——因为作业有第二天的 ddl,我同时也是一个拖延症患者。
由于晚睡,第二天显然会晚起,而安排在早 8 的所有课程就会因此错过,只能留到以后再补。补看这些课程通常都是在晚上进行的,而又因为水群,补看的学习率其实并不高,这会导致写作业不顺利,然后晚睡。
这也是一个恶性循环,怎么办?
必须打破这个循环!哪怕是空出一天什么都不干,只用来调整作息到正常的状态,并且睡个好觉,让自己在第二天能够正常学习工作,都是可以接受的。因为既然已经虚度了那么多天的光阴,又怎么会差这一天呢?与其继续浑浑噩噩,不如牺牲一天时间,换取之后的高效学习。
新的一天里,按时起了个大早,然后呢?如果继续像之前一样颓废、水群,那么又会迅速回到上文所述的恶性循环中,所以必须改变!有计划地安排自己的白天时间,把学习或工作主要放在白天完成,而晚上就可以自由安排自习或者娱乐了。此外,必须保持下去合理的生活习惯,不然前面做的这些决心全部木大之后,反而会让自己陷入更深的罪恶感和自责当中。
这件事的根源在于,我没能掌握自己的生活。生活应该是自己的生活,自己对自己的生活要进行经营、管理、负起责任。所以,需要主动一些,尝试去掌握自己的生活,尤其是生活失控的时候,更应该主动地去纠正偏离正轨的部分。当然,强行掰回来也许是不现实的,也可以留有慢慢修正的余地。
不管当前的状况再怎么差,最怕的是失去了信心、失去了动力。缺乏信心的情况下,自己是不愿意做出努力来改变现状的,而放任失控的生活继续这么过分下去。我自己至少在这一点上做的还不错,我常对未来有乐观的预期,但是缺乏行动力,这也是今天早上写这篇随笔进行反思的一个原因。
Case 1 其实就是今天早上发生了的真实事件,而它也是促使我反思自己昨天经历的事情,乃至回忆起自己之前关于恶性循环的观点的原因。写下这篇随笔,不仅是想分享我的观点,更是通过反思和自我批评来鞭策自己进行改变。
我已经如此度过了这个学期的前一半,并因为恶性循环导致了学习率低下、毫无进步、课外项目荒废的严重后果。如果再不进行改变,这半年又跟白过有什么区别呢?
我是 KSkun,请监督我学习/干活。
KSkun
2020/5/19
题目地址:洛谷:P5665 划分 – 洛谷 | 计算机科学教育新生态
2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 $n$ 组数据,数据从 $1 \sim n$ 编号,$i$ 号数据的规模为 $a_i$。
小明对该题设计出了一个暴力程序,对于一组规模为 $u$ 的数据,该程序的运行时间为 $u^2$。然而这个程序运行完一组规模为 $u$ 的数据之后,它将在任何一组规模小于 $u$ 的数据上运行错误。样例中的 $a_i$ 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。
也就是说,小明需要找到一些分界点 $1 \le k_1 < k_2 < \cdots < k_p < n$,使得:
$$
\sum_{i=1}^{k_1} a_i\le \sum_{i=k_1+1}^{k_2} a_i \le \dots \le \sum_{i=k_p+1}^n a_i
$$
注意 $p$ 可以为 $0$ 且此时 $k_0 = 0$,也就是小明可以将所有数据合并在一起运行。
小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化
$$
\left(\sum_{i=1}^{k_1} a_i \right)^2+\left(\sum_{i=k_1}^{k_2} a_i \right)^2+\cdots +\left(\sum_{i=k_p+1}^n a_i \right)^2
$$
小明觉得这个问题非常有趣,并向你请教:给定 $n$ 和 $a_i$,请你求出最优划分方案下,小明的程序的最小运行时间。
给定一个长度为 $n$ 的数列 $\{a_i\}$,求对数列的一个划分 $T = \{ k_1, k_2, \dots, k_p \}$,使得该划分满足 $\sum\limits_{i=1}^{k_1} a_i\le \sum\limits_{i=k_1+1}^{k_2} a_i \le \dots \le \sum\limits_{i=k_p+1}^n a_i$,且最小化 $ \left(\sum\limits_{i=1}^{k_1} a_i \right)^2+\left(\sum\limits_{i=k_1}^{k_2} a_i \right)^2+\cdots +\left(\sum\limits_{i=k_p+1}^n a_i \right)^2 $。
输入格式:
由于本题的数据范围较大,部分测试点的 $a_i$ 将在程序内生成。
第一行两个整数 $n, \text{type}$。$n$ 的意义见题目描述,$\text{type}$ 表示输入方式。
对于 $\text{type} = 1$ 的 $23 \sim 25$ 号测试点,$a_i$ 的生成方式如下:
$$
a_i=\left( b_i \bmod (r_j-l_j+1) \right) +l_j
$$
上述数据生成方式仅是为了减少输入量大小,标准算法不依赖于该生成方式。
输出格式:
输出一行一个整数,表示答案。
输入样例#1:
5 0 5 1 7 9 9
输出样例#1:
247
输入样例#2:
10 0 5 6 7 7 4 6 2 13 19 9
输出样例#2:
1256
输入样例#3:
10000000 1 123 456 789 12345 6789 3 2000000 123456789 987654321 7000000 234567891 876543219 10000000 456789123 567891234
输出样例#3:
4972194419293431240859891640
测试点编号 | $n\le$ | $a_i\le$ | $\text{type}=$ |
---|---|---|---|
$1\sim 3$ | $10$ | $10$ | $0$ |
$4\sim 6$ | $50$ | $10^3$ | $0$ |
$7\sim 9$ | $400$ | $10^4$ | $0$ |
$10\sim 16$ | $5\times 10^3$ | $10^5$ | $0$ |
$17\sim 22$ | $5\times 10^5$ | $10^6$ | $0$ |
$23\sim 25$ | $4\times 10^7$ | $10^9$ | $1$ |
对于 $\text{type} = 0$ 的测试点,保证答案不超过 $4\times 10^{18}$。
所有测试点满足:$\text{type} \in {0, 1} , 2 \le n \le 4 \times 10^7 , 1 \le a_i \le 10^9 , 1 \le m \le 10^5 ,1 \le l_i \le r_i \le 10^9 , 0 \le x, y, z, b_1, b_2 < 2^{30}$。
参考资料:
这个题的部分思路并不难,但最优解法确实具有一定的难度和复杂程度,以至于从开始写到整理思路写这篇题解一共用了三天时间。
首先可以设计出一个比较简单的 DP,状态 $f(i, j)$ 表示对前 $i$ 个数字分段,上一段的结尾为 $j$ 的时候段平方和的最小值。转移时,枚举上一段的结尾 $j$,再枚举上上一段的结尾 $k$,先判断是否满足转移条件 $\sum\limits_{t=k+1}^j a_t \leq \sum\limits_{t’=j+1}^i a_{t’}$,然后进行最小化转移:
$$ f(i, j) = \min_{ \sum\limits_{t=k+1}^j a_t \leq \sum\limits_{t’=j+1}^i a_{t’} } \left\{ f(j, k) + \left( \sum\limits_{t=j+1}^i a_i \right)^2 \right\} $$
显然这个 DP 的复杂度为 $O(n^3)$。
看这个式子:
$$ a^2 + b^2 < (a+b)^2 \ \ \ (a, b > 0) $$
在转移时,最后一段最长肯定越可能满足转移条件。当段较长的时候,可能将段从中间拆成多段也可以满足转移条件。假设切为两段后满足转移条件且前一段之和为 $a$、后一段之和为 $b$,上面的式子告诉我们,这里切成两段后的段平方和更小。因此,段肯定是越短越好的,或者也可以说,最后一段越短越好。事实上,段越短有利于之后分的段也较短。
因此,这里有贪心策略:段分得越短越好。
考虑将贪心应用进 DP 的过程中,既然最后一段越短越好,最优解中的最后一段一定是最短的,因此上一段的结尾不再有多种可能性,而是直接选择使最后一段最短的那一个。将前 $i$ 个数字分段的最优解中,上一段的结尾记为 $lst(i)$,DP 状态也可改为 $f(i)$ 代表前 $i$ 个数字分段的最优解的段平方和。转移方程如下:
$$ f(i) = \min_{ \sum\limits_{t=lst(j)+1}^j a_t \leq \sum\limits_{t’=j+1}^i a_{t’} } \left\{ f(j) + \left( \sum\limits_{t=j+1}^i a_i \right)^2 \right\} $$
复杂度降为 $O(n^2)$,但依然不够。
记 $a_i$ 的前缀和为 $s(i)$,将转移条件写成前缀和的形式:
$$ s(j)-s[lst(j)-1] \leq s(i)-s(j) $$
移项可得
$$ s(i) \geq 2s(j)-s[lst(j)-1] $$
上式的左边只与 $i$ 有关,右边只与 $j$ 有关,我们需要找到满足上述条件的最大的 $j$。
此外,还可以发现,当一个 $j$ 满足上述条件时,根据前缀和的单增性可以得到 $s(i+1) \geq s(i) \geq 2s(j)-s[lst(j)-1]$,即 $j$ 也可以作为 $i+1$ 的决策。因此可行的决策点是一个以 $1$ 为左端点的区间,当 $i$ 增大时,决策点的区间右端点也是单增的(性质 1)。显然,$2s(j)-s[lst(j)-1]$ 的值越小,$j$ 越可能成为合法的决策点,也就是说,记 $A(j) = 2s(j)-s[lst(j)-1]$,当 $j_1 > j_2$ 且 $A(j_1) \leq A(j_2)$ 时,$j_1$ 一定优于 $j_2$(性质 2)。
上述性质决定了我们可以应用单调队列优化 DP 转移的复杂度:维护一个 $A(j)$ 数值单调递增且 $j$ 也单增的单调队列,每次转移时找到队列中最大的满足上述条件的决策点 $j_m$(性质 1),转移后仅保留 $j_m$ 而弹出小于 $j_m$ 的满足条件的决策点(性质 2)。转移完成后,计算 $A(i)$ 再将 $i$ 放入队列中,作为以后的状态的可能决策点。
利用单调队列优化 DP 后,转移的复杂度变为 $O(1)$,则总复杂度为 $O(n)$,可以通过极限数据。
由于最开始的写法使用了大量 STL 和常数略大的写法,在各种 OJ 上都跑不过极限数据,于是开始了卡常。
最后终于在洛谷上 AC 了。不过没卡的这么极端的时候也能在 LOJ、UOJ 之类的地方跑过,可能洛谷跑的会略有点慢。
本题解的说明并不严谨,在说明单调队列优化转移时并未给出相关性质的证明,表述也不尽清晰。本文受本人水平、时间、精力等所限,暂时无法呈现出更好的效果,欢迎提出建议或改写本文部分内容。
关于有关性质的证明,可以在毛爷爷的博客:CSP2019划分的简要题解 – 博客 – matthew99的博客阅读。
#pragma GCC optimize(3) // 手动开 O3
// Code by KSkun, 2019/12
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <utility>
typedef unsigned long long LL;
typedef std::pair<int, int> PII;
inline char fgc() {
static char buf[100000], * p1 = buf, * p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for (; !isdigit(c); c = fgc()) if (c == '-') neg = -1;
for (; isdigit(c); c = fgc()) res = res * 10 + c - '0';
return res * neg;
}
inline char readsingle() {
char c;
while (!isgraph(c = fgc())) {}
return c;
}
const int MAXN = 40000005, MAXM = 100005;;
const LL MASK = (1ull << 30) - 1; // % 2^30 和 & MARK 等价
int n, type, lst[MAXN];
LL s[MAXN];
void gen_data() {
LL x, y, z, m;
x = readint(); y = readint(); z = readint();
s[1] = readint(); s[2] = readint();
m = readint();
int* p = new int[MAXM], * l = new int[MAXM], * r = new int[MAXM];
p[0] = l[0] = r[0] = 0;
for (int i = 1; i <= m; i++) {
p[i] = readint();
l[i] = readint();
r[i] = readint();
}
for (int i = 3; i <= n; i++) {
s[i] = (x * s[i - 1] + y * s[i - 2] + z) & MASK; // 复用 s 数组省空间
}
for (int i = 1; i <= m; i++) {
for (int j = p[i - 1] + 1; j <= p[i]; j++) {
s[j] = (s[j] % (r[i] - l[i] + 1)) + l[i] + s[j - 1]; // 复用 s 数组省空间
}
}
delete[] p; delete[] l; delete[] r; // 内存回收
}
const int BN_MAX = 1e9;
struct BigNum {
LL dat[5];
int len;
BigNum() {
dat[0] = dat[1] = dat[2] = dat[3] = dat[4] = 0; // 手动初始化没用 memset 减小常数
len = 0;
}
BigNum(LL x) {
dat[0] = dat[1] = dat[2] = dat[3] = dat[4] = 0;
len = 0;
while (x > 0) {
dat[len++] = x % BN_MAX;
x /= BN_MAX;
}
}
BigNum operator+(const BigNum& rhs) const {
BigNum res;
if (len == 0) return rhs; // 减少运算次数
if (rhs.len == 0) return *this;
res.len = std::max(len, rhs.len);
for (register int i = 0; i < res.len; i++) {
if (dat[i] == 0 && rhs.dat[i] == 0) continue; // 减少运算次数
res.dat[i] += dat[i] + rhs.dat[i];
if (res.dat[i] > BN_MAX) { // 减少运算次数
res.dat[i + 1] += res.dat[i] / BN_MAX;
res.dat[i] %= BN_MAX;
}
}
if (res.dat[res.len] != 0) res.len++;
return res;
}
BigNum operator*(const BigNum& rhs) const {
BigNum res;
if (len == 0 || rhs.len == 0) return res;
res.len = len + rhs.len - 1;
for (register int i = 0; i < len; i++) {
if (dat[i] == 0) continue; // 减少运算次数
for (register int j = 0; j < rhs.len; j++) {
if (rhs.dat[j] == 0) continue; // 减少运算次数
res.dat[i + j] += dat[i] * rhs.dat[j];
}
}
for (register int i = 0; i < res.len; i++) {
if (res.dat[i] > BN_MAX) { // 减少运算次数
res.dat[i + 1] += res.dat[i] / BN_MAX;
res.dat[i] %= BN_MAX;
}
}
if (res.dat[res.len] != 0) res.len++;
return res;
}
};
int d_dat[MAXN], d_l, d_r;
inline LL A(int idx) {
return 2 * s[idx] - s[lst[idx]];
}
int main() {
n = readint(); type = readint();
if (type == 0) {
for (register int i = 1; i <= n; i++) {
s[i] = s[i - 1] + readint(); // 输入时就把前缀和做了,减小单独计算的常数
}
} else {
gen_data();
}
d_l = d_r = 0; // 单调队列手写而且没封装,常数优化
for (register int i = 1; i <= n; i++) {
int lst_ele = 0;
while (d_l != d_r && s[i] >= A(d_dat[d_l])) {
lst_ele = d_dat[d_l]; d_l++;
}
lst[i] = lst_ele; d_dat[--d_l] = lst_ele;
LL Ai = A(i); // 没空间存 A 数组,所以用函数,这里是避免重复运算减小常数
while (d_l != d_r && Ai <= A(d_dat[d_r - 1])) d_r--;
d_dat[d_r++] = i;
}
BigNum ans;
for (register int i = n; i != 0; i = lst[i]) {
BigNum t = BigNum(s[i] - s[lst[i]]);
ans = ans + t * t;
}
for (register int i = ans.len - 1; i >= 0; i--) {
if (i == ans.len - 1) {
printf("%llu", ans.dat[i]);
} else {
printf("%09llu", ans.dat[i]);
}
}
return 0;
}