[CSP-S2019] 划分
题目描述
2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 n n n 组数据,数据从 1 ∼ n 1 \sim n 1∼n 编号, i i i 号数据的规模为 a i a_i ai。
小明对该题设计出了一个暴力程序,对于一组规模为 u u u 的数据,该程序的运行时间为 u 2 u^2 u2。然而这个程序运行完一组规模为 u u u 的数据之后,它将在任何一组规模小于 u u u 的数据上运行错误。样例中的 a i a_i ai 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。
也就是说,小明需要找到一些分界点 1 ≤ k 1 < k 2 < ⋯ < k p < n 1 \leq k_1 \lt k_2 \lt \cdots \lt k_p \lt n 1≤k1<k2<⋯<kp<n,使得
∑ i = 1 k 1 a i ≤ ∑ i = k 1 + 1 k 2 a i ≤ ⋯ ≤ ∑ i = k p + 1 n a i \sum_{i=1}^{k_1} a_i \leq \sum_{i=k_1+1}^{k_2} a_i \leq \cdots \leq \sum_{i=k_p+1}^{n} a_i i=1∑k1ai≤i=k1+1∑k2ai≤⋯≤i=kp+1∑nai
注意 p p p 可以为 0 0 0 且此时 k 0 = 0 k_0 = 0 k0=0,也就是小明可以将所有数据合并在一起运行。
小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化
( ∑ i = 1 k 1 a i ) 2 + ( ∑ i = k 1 + 1 k 2 a i ) 2 + ⋯ + ( ∑ i = k p + 1 n a i ) 2 (\sum_{i=1}^{k_1} a_i)^2 + (\sum_{i=k_1+1}^{k_2} a_i)^2 + \cdots + (\sum_{i=k_p+1}^{n} a_i)^2 (i=1∑k1ai)2+(i=k1+1∑k2ai)2+⋯+(i=kp+1∑nai)2
小明觉得这个问题非常有趣,并向你请教:给定 n n n 和 a i a_i ai,请你求出最优划分方案下,小明的程序的最小运行时间。
输入格式
由于本题的数据范围较大,部分测试点的 a i a_i ai 将在程序内生成。
第一行两个整数 n , t y p e n, type n,type。 n n n 的意义见题目描述, t y p e type type 表示输入方式。
- 若 t y p e = 0 type = 0 type=0,则该测试点的 a i a_i ai 直接给出。输入文件接下来:第二行 n n n 个以空格分隔的整数 a i a_i ai,表示每组数据的规模。
- 若 t y p e = 1 type = 1 type=1,则该测试点的 a i a_i ai 将特殊生成,生成方式见后文。输入文件接下来:第二行六个以空格分隔的整数 x , y , z , b 1 , b 2 , m x, y, z, b_1, b_2, m x,y,z,b1,b2,m。接下来 m m m 行中,第 i ( 1 ≤ i ≤ m ) i (1 \leq i \leq m) i(1≤i≤m) 行包含三个以空格分隔的正整数 p i , l i , r i p_i, l_i, r_i pi,li,ri。
对于 t y p e = 1 type = 1 type=1 的 23~25 号测试点, a i a_i ai 的生成方式如下:
给定整数 x , y , z , b 1 , b 2 , m x, y, z, b_1, b_2, m x,y,z,b1,b2,m,以及 m m m 个三元组 ( p i , l i , r i ) (p_i, l_i, r_i) (pi,li,ri)。
保证 n ≥ 2 n \geq 2 n≥2。若 n > 2 n \gt 2 n>2,则 ∀ 3 ≤ i ≤ n , b i = ( x × b i − 1 + y × b i − 2 + z ) m o d 2 30 \forall 3 \leq i \leq n, b_i = (x \times b_{i−1} + y \times b_{i−2} + z) \mod 2^{30} ∀3≤i≤n,bi=(x×bi−1+y×bi−2+z)mod230。
保证 1 ≤ p i ≤ n , p m = n 1 \leq p_i \leq n, p_m = n 1≤pi≤n,pm=n。令 p 0 = 0 p_0 = 0 p0=0,则 p i p_i pi 还满足 ∀ 0 ≤ i < m \forall 0 \leq i \lt m ∀0≤i<m 有 p i < p i + 1 p_i \lt p_{i+1} pi<pi+1。
对于所有 1 ≤ j ≤ m 1 \leq j \leq m 1≤j≤m,若下标值 i ( 1 ≤ i ≤ n ) i (1 \leq i \leq n) i(1≤i≤n)满足 p j − 1 < i ≤ p j p_{j−1} \lt i \leq p_j pj−1<i≤pj,则有
a i = ( b i m o d ( r j − l j + 1 ) ) + l j a_i = \left(b_i \mod \left( r_j − l_j + 1 \right) \right) + l_j ai=(bimod(rj−lj+1))+lj
上述数据生成方式仅是为了减少输入量大小,标准算法不依赖于该生成方式。
输出格式
输出一行一个整数,表示答案。
样例 #1
样例输入 #1
5 0
5 1 7 9 9
样例输出 #1
247
样例 #2
样例输入 #2
10 0
5 6 7 7 4 6 2 13 19 9
样例输出 #2
1256
样例 #3
样例输入 #3
10000000 1
123 456 789 12345 6789 3
2000000 123456789 987654321
7000000 234567891 876543219
10000000 456789123 567891234
样例输出 #3
4972194419293431240859891640
提示
【样例 1 解释】
最优的划分方案为 { 5 , 1 } , { 7 } , { 9 } , { 9 } \{5,1\}, \{7\}, \{9\}, \{9\} {5,1},{7},{9},{9}。由 5 + 1 ≤ 7 ≤ 9 ≤ 9 5 + 1 \leq 7 \leq 9 \leq 9 5+1≤7≤9≤9 知该方案合法。
答案为 ( 5 + 1 ) 2 + 7 2 + 9 2 + 9 2 = 247 (5 + 1)^2 + 7^2 + 9^2 + 9^2 = 247 (5+1)2+72+92+92=247。
虽然划分方案 { 5 } , { 1 } , { 7 } , { 9 } , { 9 } \{5\}, \{1\}, \{7\}, \{9\}, \{9\} {5},{1},{7},{9},{9} 对应的运行时间比 247 247 247 小,但它不是一组合法方案,因为 5 > 1 5 \gt 1 5>1。
虽然划分方案 { 5 } , { 1 , 7 } , { 9 } , { 9 } \{5\}, \{1,7\}, \{9\}, \{9\} {5},{1,7},{9},{9} 合法,但该方案对应的运行时间为 251 251 251,比 247 247 247 大。
【样例 2 解释】
最优的划分方案为 { 5 } , { 6 } , { 7 } , { 7 } , { 4 , 6 , 2 } , { 13 } , { 19 , 9 } \{5\}, \{6\}, \{7\}, \{7\}, \{4,6,2\}, \{13\}, \{19,9\} {5},{6},{7},{7},{4,6,2},{13},{19,9}。
【数据范围】
测试点编号 | n ≤ n \leq n≤ | a i ≤ a_i \leq ai≤ | t y p e = type = type= |
---|---|---|---|
1 ∼ 3 1 \sim 3 1∼3 | 10 10 10 | 10 10 10 | 0 |
4 ∼ 6 4 \sim 6 4∼6 | 50 50 50 | 1 0 3 10^3 103 | 0 |
7 ∼ 9 7 \sim 9 7∼9 | 400 400 400 | 1 0 4 10^4 104 | 0 |
10 ∼ 16 10 \sim 16 10∼16 | 5000 5000 5000 | 1 0 5 10^5 105 | 0 |
17 ∼ 22 17 \sim 22 17∼22 | 5 × 1 0 5 5 \times 10^5 5×105 | 1 0 6 10^6 106 | 0 |
23 ∼ 25 23 \sim 25 23∼25 | 4 × 1 0 7 4 \times 10^7 4×107 | 1 0 9 10^9 109 | 1 |
对于 t y p e = 0 type=0 type=0的所有测试点,保证最后输出的答案 ≤ 4 × 1 0 18 \leq 4 \times 10^{18} ≤4×1018
所有测试点满足: t y p e ∈ { 0 , 1 } type \in \{0,1\} type∈{0,1}, 2 ≤ n ≤ 4 × 1 0 7 2 \leq n \leq 4 \times 10^7 2≤n≤4×107, 1 ≤ a i ≤ 1 0 9 1 \leq a_i \leq 10^9 1≤ai≤109, 1 ≤ m ≤ 1 0 5 1 \leq m \leq 10^5 1≤m≤105, 1 ≤ l i ≤ r i ≤ 1 0 9 1 \leq l_i \leq r_i \leq 10^9 1≤li≤ri≤109, 0 ≤ x , y , z , b 1 , b 2 < 2 30 0 \leq x,y,z,b_1,b_2 \lt 2^{30} 0≤x,y,z,b1,b2<230。
贪心、dp - TLE 64pts
一个简单的想法是状态 ( i , j ) (i,j) (i,j)表示前i个数,最后一个区间和为j的最小平方和。需优化。
见数据量猜测是贪心,研究样例能猜测:前i个数的最优方案,若最后一个区间是
[
j
,
i
]
[j,i]
[j,i],则前面的划分方法与前j-1个数的方案相同。
如果没有递增的限制,这个猜想显然成立。但它害怕一种情况:前j-1个数其他方案与最优方案相比,最后一个区间和更小,也就是对后面的限制更少。
考虑最优方案是.../xx/xxx
,另有方案2.../xxx/xx
. 利用“最优”这一性质。为何倒数第三个x被分到后一个会更好?
考察将新的数x加入
x
1
,
.
.
.
,
x
n
x_1,...,x_n
x1,...,xn中的和平方增量,为
x
2
+
2
x
∑
x
i
x^2+2x\sum{x_i}
x2+2x∑xi. 所以上面的实例中,前两个数和比后两个数和更大。方案2不成立。
所以,最优方案同时也是对后面限制最小的方案。状态减少一个维度。
d
p
(
i
)
dp(i)
dp(i)为前i个数的最小答案,
f
(
i
)
f(i)
f(i)为前i个数最优方案中最后一个区间和。
d
p
(
i
)
=
m
i
n
{
d
p
(
j
)
+
s
2
(
j
+
1
,
i
)
}
,
f
(
j
)
≤
s
(
j
+
1
,
i
)
=
f
(
i
)
.
dp(i)=min\{dp(j)+s^2(j+1,i)\}, f(j)\leq s(j+1,i)=f(i).
dp(i)=min{dp(j)+s2(j+1,i)},f(j)≤s(j+1,i)=f(i).
memset(dp, 0x3f, sizeof(dp));
f[0] = 0; dp[0] = 0;
f[1] = a[1]; dp[1] = a[1] * a[1];
for (int i = 2; i <= n; ++i) {
for (int j = 0; j < i; ++j) {
if (f[j] <= a[i] - a[j] && ckmin(dp[i], dp[j] + (a[i] - a[j]) * (a[i] - a[j]))) f[i] = a[i] - a[j];
}
}
printf("%lld\n", dp[n]);
时间复杂度 O ( n 2 ) O(n^2) O(n2).
分析 - TLE 64pts
对于保证答案不超过long long的数据量,计算过程中可能涉及非最优解,dp数组可能爆long long.
刚刚的结论,也是在说,决策点j是使
f
(
j
)
≤
s
(
j
+
1
,
i
)
f(j)\leq s(j+1,i)
f(j)≤s(j+1,i),即
f
(
j
)
+
s
(
j
)
≤
s
(
i
)
f(j)+s(j)\leq s(i)
f(j)+s(j)≤s(i)的最大的j.
所以不再需要打擂台。可以保存
p
o
s
(
i
)
pos(i)
pos(i)为
d
p
(
i
)
dp(i)
dp(i)转移到的决策点j, 最后简单地统计答案。
f[0] = 0;
f[1] = a[1];
for (int i = 2; i <= n; ++i) {
for (int j = i - 1; ~j; --j) {
if (f[j] <= a[i] - a[j]) {
f[i] = a[i] - a[j];
pos[i] = j;
break;
}
}
}
for (int i = n; i; i = pos[i]) {
ans += f[i] * f[i];
}
printf("%lld\n", ans);
因为s(前缀和)递增,所以pos也递增。找 p o s ( i ) pos(i) pos(i),可以从 p o s ( i − 1 ) pos(i-1) pos(i−1)往后找,但这并没有优化算法复杂度。
f[0] = 0;
f[1] = a[1];
for (int i = 2; i <= n; ++i) {
for (int j = pos[i - 1]; j < i; ++j) {
if (f[j] <= a[i] - a[j]) {
f[i] = a[i] - a[j];
pos[i] = j;
}
}
}
for (int i = n; i; i = pos[i]) {
ans += f[i] * f[i];
}
printf("%lld\n", ans);
时间复杂度 O ( n 2 ) O(n^2) O(n2)。
单调队列 - 缺高精 88pts
瓶颈在求满足
f
(
j
)
+
s
(
j
)
≤
s
(
i
)
f(j)+s(j)\leq s(i)
f(j)+s(j)≤s(i)的最大的j.
右边递增,随机生成数据发现左边没有单调性。
对于一个j, f ( j ) + s ( j ) f(j)+s(j) f(j)+s(j)越小,越有竞争力(当然还要考虑j的大小)。如果一个j较小,f(j)+s(j)却较大(也就是逆序对),那么这个j就失去了竞争力。这正是单调队列的特点。
时间复杂度 O ( n ) O(n) O(n).
f[0] = 0;
f[1] = a[1];
q.push_back(0); q.push_back(1);
for (int i = 2; i <= n; ++i) {
int j;
while (!q.empty() && getval(q.front()) <= a[i]) {
j = q.front();
q.pop_front();
}
q.push_front(j);
f[i] = a[i] - a[j]; pos[i] = j;
while (!q.empty() && getval(q.back()) >= getval(i)) q.pop_back();
q.push_back(i);
}
for (int i = n; i; i = pos[i]) {
ans += f[i] * f[i];
}
printf("%lld\n", ans);
高精度 - MLE
如果把a, f数组改为高精度,会爆空间。
空间优化 - TLE、MLE 88pts (补)
仔细想一想,需要高精度的只有最后的统计答案。只需要在最后用临时高精度变量,不用高精度数组。
大数据量依旧MLE,可能是源于deque. 奇怪的是,若把生成数据使用的b数组从long long改成int,则MLE变成TLE.
struct Huge {
ll num[MAXL];
Huge() {
memset(num, 0, sizeof(num));
num[0] = 1;
}
void put() {
for (int i = num[0]; i; --i) {
printf("%lld", num[i]);
}
}
Huge operator + (const ll &x) const {
Huge ans;
for (int i = 1; i <= num[0]; ++i) ans.num[i] = num[i];
ans.num[1] += x;
while (ans.num[ans.num[0]] > 10) {
ans.num[ans.num[0] + 1] = ans.num[ans.num[0]] / 10;
ans.num[ans.num[0]] %= 10;
++ans.num[0];
}
return ans;
}
Huge operator + (const Huge &x) const {
Huge ans;
int len = max(num[0], x.num[0]);
for (int i = 1; i <= len; ++i) {
ans.num[i] += num[i] + x.num[i];
ans.num[i + 1] += ans.num[i] / 10;
ans.num[i] %= 10;
}
if (ans.num[len + 1]) ++len;
ans.num[0] = len;
return ans;
}
Huge operator * (const Huge &x) const {
Huge ans;
int len = num[0] + x.num[0] - 1;
for (int i = 1; i <= num[0]; ++i) {
for (int j = 1; j <= x.num[0]; ++j) {
ans.num[i + j - 1] += num[i] * x.num[j];
ans.num[i + j] += ans.num[i + j - 1] / 10;
ans.num[i + j - 1] %= 10;
}
}
while (ans.num[len + 1]) ++len;
while (len > 1 && !ans.num[len]) --len;
ans.num[0] = len;
return ans;
}
};
int n, type, m, pos[MAXN];
ll a[MAXN], f[MAXN], b[MAXN];
Huge ans;
deque<int> q;
inline ll getval(int i) {
return f[i] + a[i];
}
int main() {
scanf("%d%d", &n, &type);
if (type) {
ll x, y, z;
scanf("%lld%lld%lld%lld%lld%d", &x, &y, &z, b + 1, b + 2, &m);
for (int i = 3; i <= n; ++i) b[i] = (x * b[i - 1] % MOD + y * b[i - 2] % MOD + z) % MOD;
for (int i = 1, j = 1; i <= m; ++i) {
int p, l, r; scanf("%d%d%d", &p, &l, &r);
for (; j <= p; ++j) {
a[j] = b[j] % (r - l + 1) + l;
a[j] += a[j - 1];
}
}
}
else {
for (int i = 1; i <= n; ++i) {
scanf("%lld", a + i);
a[i] += a[i - 1];
}
}
f[0] = 0;
f[1] = a[1];
q.push_back(0); q.push_back(1);
for (int i = 2; i <= n; ++i) {
int j;
while (!q.empty() && getval(q.front()) <= a[i]) {
j = q.front();
q.pop_front();
}
q.push_front(j);
f[i] = a[i] - a[j]; pos[i] = j;
while (!q.empty() && getval(q.back()) >= getval(i)) q.pop_back();
q.push_back(i);
}
for (int i = n; i; i = pos[i]) {
Huge tmp; tmp = tmp + f[i];
ans = ans + tmp * tmp;
}
ans.put(); printf("\n");
return 0;
}
滚动数组、压位 - AC
生成数据使用的b可以滚动数组。
高精度压位。
struct Huge {
ll num[MAXL];
Huge() {
memset(num, 0, sizeof(num));
num[0] = 1;
}
void put() {
printf("%lld", num[num[0]]);
for (int i = num[0] - 1; i; --i) {
printf("%09lld", num[i]); //中间的0
}
}
Huge operator + (const ll &x) const {
Huge ans;
for (int i = 1; i <= num[0]; ++i) ans.num[i] = num[i];
ans.num[1] += x;
while (ans.num[ans.num[0]] > BASE) {
ans.num[ans.num[0] + 1] = ans.num[ans.num[0]] / BASE;
ans.num[ans.num[0]] %= BASE;
++ans.num[0];
}
return ans;
}
Huge operator + (const Huge &x) const {
Huge ans;
int len = max(num[0], x.num[0]);
for (int i = 1; i <= len; ++i) {
ans.num[i] += num[i] + x.num[i];
ans.num[i + 1] += ans.num[i] / BASE;
ans.num[i] %= BASE;
}
if (ans.num[len + 1]) ++len;
ans.num[0] = len;
return ans;
}
Huge operator * (const Huge &x) const {
Huge ans;
int len = num[0] + x.num[0] - 1;
for (int i = 1; i <= num[0]; ++i) {
for (int j = 1; j <= x.num[0]; ++j) {
ans.num[i + j - 1] += num[i] * x.num[j];
ans.num[i + j] += ans.num[i + j - 1] / BASE;
ans.num[i + j - 1] %= BASE;
}
}
while (ans.num[len + 1]) ++len;
while (len > 1 && !ans.num[len]) --len;
ans.num[0] = len;
return ans;
}
};
int n, type, m, pos[MAXN];
ll a[MAXN], f[MAXN], b[3];
Huge ans;
deque<int> q;
inline ll getval(int i) {
return f[i] + a[i];
}
int main() {
scanf("%d%d", &n, &type);
if (type) {
ll x, y, z;
scanf("%lld%lld%lld%lld%lld%d", &x, &y, &z, b + 1, b + 2, &m);
for (register int i = 1, j = 1; i <= m; ++i) {
int p, l, r; scanf("%d%d%d", &p, &l, &r);
for (; j <= p; ++j) {
if (j > 2) b[j % 3] = (x * b[(j + 2) % 3] % MOD + y * b[(j + 1) % 3] % MOD + z) % MOD;
a[j] = b[j % 3] % (r - l + 1) + l;
a[j] += a[j - 1];
}
}
}
else {
for (int i = 1; i <= n; ++i) {
scanf("%lld", a + i);
a[i] += a[i - 1];
}
}
f[0] = 0;
f[1] = a[1];
q.push_back(0); q.push_back(1);
for (register int i = 2; i <= n; ++i) {
int j;
while (!q.empty() && getval(q.front()) <= a[i]) {
j = q.front();
q.pop_front();
}
q.push_front(j);
f[i] = a[i] - a[j]; pos[i] = j;
while (!q.empty() && getval(q.back()) >= getval(i)) q.pop_back();
q.push_back(i);
}
for (register int i = n; i; i = pos[i]) {
Huge tmp; tmp = tmp + f[i];
ans = ans + tmp * tmp;
}
ans.put(); printf("\n");
return 0;
}