P5665 [CSP-S2019] 划分

文章描述了一道CSP比赛中的问题,要求在不修改程序的前提下,通过划分数据段来确保程序在不同规模数据上正确运行并最小化运行时间。提出了一个基于暴力程序的解决方案,通过寻找最优数据段划分以降低平方和运行时间。文章探讨了贪心算法作为解决思路,并给出了样例解释和处理大数据量的方法,包括单调队列优化和高精度计算的空间优化等。

[CSP-S2019] 划分

题目描述

2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 nnn 组数据,数据从 1∼n1 \sim n1n 编号,iii 号数据的规模为 aia_iai

小明对该题设计出了一个暴力程序,对于一组规模为 uuu 的数据,该程序的运行时间u2u^2u2。然而这个程序运行完一组规模为 uuu 的数据之后,它将在任何一组规模小于 uuu 的数据上运行错误。样例中的 aia_iai 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。

也就是说,小明需要找到一些分界点 1≤k1<k2<⋯<kp<n1 \leq k_1 \lt k_2 \lt \cdots \lt k_p \lt n1k1<k2<<kp<n,使得

∑i=1k1ai≤∑i=k1+1k2ai≤⋯≤∑i=kp+1nai \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=1k1aii=k1+1k2aii=kp+1nai

注意 ppp 可以为 000 且此时 k0=0k_0 = 0k0=0,也就是小明可以将所有数据合并在一起运行。

小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化

(∑i=1k1ai)2+(∑i=k1+1k2ai)2+⋯+(∑i=kp+1nai)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=1k1ai)2+(i=k1+1k2ai)2++(i=kp+1nai)2

小明觉得这个问题非常有趣,并向你请教:给定 nnnaia_iai,请你求出最优划分方案下,小明的程序的最小运行时间。

输入格式

由于本题的数据范围较大,部分测试点的 aia_iai 将在程序内生成。

第一行两个整数 n,typen, typen,typennn 的意义见题目描述,typetypetype 表示输入方式。

  1. type=0type = 0type=0,则该测试点的 aia_iai 直接给出。输入文件接下来:第二行 nnn 个以空格分隔的整数 aia_iai,表示每组数据的规模。
  2. type=1type = 1type=1,则该测试点的 aia_iai特殊生成,生成方式见后文。输入文件接下来:第二行六个以空格分隔的整数 x,y,z,b1,b2,mx, y, z, b_1, b_2, mx,y,z,b1,b2,m。接下来 mmm 行中,第 i(1≤i≤m)i (1 \leq i \leq m)i(1im) 行包含三个以空格分隔的正整数 pi,li,rip_i, l_i, r_ipi,li,ri

对于 type=1type = 1type=1 的 23~25 号测试点,aia_iai 的生成方式如下:

给定整数 x,y,z,b1,b2,mx, y, z, b_1, b_2, mx,y,z,b1,b2,m,以及 mmm 个三元组 (pi,li,ri)(p_i, l_i, r_i)(pi,li,ri)

保证 n≥2n \geq 2n2。若 n>2n \gt 2n>2,则 ∀3≤i≤n,bi=(x×bi−1+y×bi−2+z)mod  230\forall 3 \leq i \leq n, b_i = (x \times b_{i−1} + y \times b_{i−2} + z) \mod 2^{30}∀3in,bi=(x×bi1+y×bi2+z)mod230

保证 1≤pi≤n,pm=n1 \leq p_i \leq n, p_m = n1pin,pm=n。令 p0=0p_0 = 0p0=0,则 pip_ipi 还满足 ∀0≤i<m\forall 0 \leq i \lt m∀0i<mpi<pi+1p_i \lt p_{i+1}pi<pi+1

对于所有 1≤j≤m1 \leq j \leq m1jm,若下标值 i(1≤i≤n)i (1 \leq i \leq n)i(1in)满足 pj−1<i≤pjp_{j−1} \lt i \leq p_jpj1<ipj,则有

ai=(bimod  (rj−lj+1))+lja_i = \left(b_i \mod \left( r_j − l_j + 1 \right) \right) + l_jai=(bimod(rjlj+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≤95 + 1 \leq 7 \leq 9 \leq 95+1799 知该方案合法。

答案为 (5+1)2+72+92+92=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} 对应的运行时间比 247247247 小,但它不是一组合法方案,因为 5>15 \gt 15>1

虽然划分方案 {5},{1,7},{9},{9}\{5\}, \{1,7\}, \{9\}, \{9\}{5},{1,7},{9},{9} 合法,但该方案对应的运行时间为 251251251,比 247247247 大。

【样例 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 \leqnai≤a_i \leqaitype=type =type=
1∼31 \sim 3131010101010100
4∼64 \sim 64650505010310^31030
7∼97 \sim 97940040040010410^41040
10∼1610 \sim 16101650005000500010510^51050
17∼2217 \sim 2217225×1055 \times 10^55×10510610^61060
23∼2523 \sim 2523254×1074 \times 10^74×10710910^91091

对于type=0type=0type=0的所有测试点,保证最后输出的答案≤4×1018\leq 4 \times 10^{18}4×1018

所有测试点满足:type∈{0,1}type \in \{0,1\}type{0,1}2≤n≤4×1072 \leq n \leq 4 \times 10^72n4×1071≤ai≤1091 \leq a_i \leq 10^91ai1091≤m≤1051 \leq m \leq 10^51m1051≤li≤ri≤1091 \leq l_i \leq r_i \leq 10^91liri1090≤x,y,z,b1,b2<2300 \leq x,y,z,b_1,b_2 \lt 2^{30}0x,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加入x1,...,xnx_1,...,x_nx1,...,xn中的和平方增量,为x2+2x∑xix^2+2x\sum{x_i}x2+2xxi. 所以上面的实例中,前两个数和比后两个数和更大。方案2不成立。
所以,最优方案同时也是对后面限制最小的方案。状态减少一个维度。

dp(i)dp(i)dp(i)为前i个数的最小答案,f(i)f(i)f(i)为前i个数最优方案中最后一个区间和。
dp(i)=min{dp(j)+s2(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(n2)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.
所以不再需要打擂台。可以保存pos(i)pos(i)pos(i)dp(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也递增。找pos(i)pos(i)pos(i),可以从pos(i−1)pos(i-1)pos(i1)往后找,但这并没有优化算法复杂度。

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(n2)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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值