【CSP-S2019】D2T2 划分

CSP-S2019 D2T2 划分

题目

题目描述

2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 n n n 组数据,数据从 1 ∼ n 1 \sim n 1n 编号, 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 1k1<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\limits_{i=1}^{k_1}a_i \le \sum\limits_{i = k_1 + 1}^{k_2}a_i\le \cdots \le \sum\limits_{i = k_p + 1}^{n}a_i i=1k1aii=k1+1k2aii=kp+1nai

注意 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 = p + 1 n a i ) 2 \left( \sum\limits_{i = 1}^{k_1}a_i \right)^2 + \left( \sum\limits_{i = k_1 + 1}^{k_2}a_i \right)^2 + \cdots + \left( \sum\limits_{i = p + 1}^{n}a_i \right)^2 (i=1k1ai)2+(i=k1+1k2ai)2++(i=p+1nai)2

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

分析

结论: 最后取出的一段的长度要尽可能地小。

证明: 不会。。。如果想要看理性的证明可以去看 matthew99 的博客

然后这道题就变成水题了。。。


我还是说一下我的感性理解吧。。。

s i s_i si a i a_i ai 的前缀和。

首先不难想到一个 36 分的 DP:(这是我考场上想到的)

f ( i , j ) f(i, j) f(i,j) 为把前 i i i 个数分成 j j j 段所产生的最小代价, g ( i , j ) g(i, j) g(i,j) 为最后一段的右端点的位置。那么显然有转移:

f ( i , j ) = min ⁡ ( k < i ) ∧ ( s k − s g ( k , j − 1 ) ≤ s i − s k ) { f ( k , j − 1 ) + ( s i − s k ) 2 } f(i, j) = \min\limits_{(k < i) \land (s_k - s_{g(k, j - 1)} \le s_i - s_k)} \{ f(k, j - 1) + (s_i - s_k)^2 \} f(i,j)=(k<i)(sksg(k,j1)sisk)min{f(k,j1)+(sisk)2}

最后答案为 min ⁡ 1 ≤ i ≤ n f ( n , i ) \min\limits_{1 \le i \le n} f(n, i) 1inminf(n,i)

然后我们发现题目中对分成的段数并没有限制,于是去掉第二维,状态定义变成 f ( i ) f(i) f(i) 为表示前 i i i 个数分成若干段的最小答案。转移和上面那个差不多。

然而这种方法似乎精度很容易爆炸。。。我们必须换一种想法。

我们令 b i b_i bi 等于一个合法的划分中每段 a i a_i ai 的和。根据我们初中的数学知识可以得到,对于一个数列 b b b 中的任意一个子区间 [ l , r ] [l, r] [l,r],有:

( ∑ i = l r b i ) 2 > ∑ i = l r b i 2 \left( \sum\limits_{i=l}^rb_i \right)^2 > \sum\limits_{i=l}^{r}{b_i}^2 (i=lrbi)2>i=lrbi2

意思就是把一段长的区间分成尽可能多的短的区间之后,答案一定会变优。感性理解一下就是了。。。 这意味着我们的每次选的最后一个区间要尽可能地短。

然后我们记 g ( i ) g(i) g(i) 为最后一个区间所选的右端点的位置,显然有:

g ( i ) = max ⁡ { j ∣ ( j < i ) ∧ ( s j − s g ( j ) ≤ s i − s j ) } g(i) = \max \{ j | (j < i) \land (s_j - s_{g(j)} \le s_i - s_j)\} g(i)=max{j(j<i)(sjsg(j)sisj)}

最后我们再沿着 g ( i ) g(i) g(i) 倒回去算就可以了,当然也可以边转移边算。

然而这个转移是 O ( n 2 ) O(n^2) O(n2) 的,无法通过这道题。我们考虑优化。

看到转移式子后面的那个不等式,我们将它变一下形就可以得到:

s i ≥ 2 s j − s g ( j ) s_i \geq 2s_j - s_{g(j)} si2sjsg(j)

不难 看出这个式子有单调性,于是用一个单调队列优化一下就是了。

然而我们并不能边转移边算了,因为这道题要用到 高精度 ,这样一做显然是要炸空间的。所以我们必须先处理出整个 g ( i ) g(i) g(i) 再倒回去算。

总时间复杂度为 O ( n ) O(n) O(n),另注意这道题卡空间,数组要想办法压掉几个。。。高精度也要做压位的。 不压位的我也不知道能不能过。。。

参考代码

其实这份代码的长度主要还是高精度。。。核心程序也就那么几行。。。

#include 
#include 
#include 
#include 
#include 
using namespace std;

typedef long long ll;
const int Maxn = 4e7;

struct BigInteger {
	static const int BASE = 100000000;
	int num[15], len;
	BigInteger (ll x = 0) {
		*this = x;
	}
	inline void clear(BigInteger &x) {
		while(x.num[x.len] == 0 && x.len > 1)
			x.len--;
	}
	BigInteger operator = (ll rhs) {
		memset(num, 0, sizeof num);
		len = 0;
		do {
			num[++len] = rhs % BASE;
			rhs /= BASE;
		} while(rhs != 0);
		return *this;
	}
	BigInteger operator + (const BigInteger &rhs) {
		BigInteger ret;
		memset(ret.num, 0, sizeof ret.num), ret.len = 0;
		for(int i = 1, tmp = 0; tmp != 0 || i <= len || i <= rhs.len; i++) {
			tmp += num[i] + rhs.num[i];
			ret.num[++ret.len] = tmp % BASE;
			tmp /= BASE;
		}
		return ret;
	}
	BigInteger operator * (const BigInteger &rhs) {
		BigInteger ret;
		memset(ret.num, 0, sizeof ret.num), ret.len = len + rhs.len;
		for(int i = 1; i <= len; i++) {
			ll tmp = 0;
			for(int j = 1; j <= rhs.len || tmp != 0; j++) {
				tmp += 1LL * num[i] * rhs.num[j] + ret.num[i + j - 1];
				ret.num[i + j - 1] = tmp % BASE;
				tmp /= BASE;
			}
		}
		clear(ret);
		return ret;
	}
	BigInteger operator += (const BigInteger &rhs) {
		*this = *this + rhs;
		return *this;
	}
	BigInteger operator *= (const BigInteger &rhs) {
		*this = *this * rhs;
		return *this;
	}
};
ostream &operator << (ostream &out, const BigInteger &x) {
	out << x.num[x.len];
	for(int i = x.len - 1; i >= 1; i--) {
		char buf[20];
		sprintf(buf, "%08d", x.num[i]);
		out << buf;
	}
	return out;
}

int N;
ll sum[Maxn + 5];

ll B[Maxn + 5];
const ll mod = (1 << 30);

inline void ReadIn() {
	int typ;
	scanf("%d %d", &N, &typ);
	if(typ == 0) {
		for(int i = 1; i <= N; i++)
			scanf("%lld", &sum[i]);
	} else {
		ll x, y, z;
		scanf("%lld %lld %lld %lld %lld", &x, &y, &z, &B[1], &B[2]);
		for(int i = 3; i <= N; i++)
			B[i] = ((x * B[i - 1] % mod) + (y * B[i - 2] % mod) + z) % mod;
		int m, las = 0, p;
		scanf("%d", &m);
		for(int i = 1; i <= m; i++) {
			int l, r;
			scanf("%d %d %d", &p, &l, &r);
			for(int j = las + 1; j <= p; j++)
				sum[j] = (B[j] % (r - l + 1)) + l;
			las = p;
		}
	}
}

int f[Maxn + 5];
inline ll calc_val(int pos) {
	return sum[pos] * 2 - sum[f[pos]];
}

int q[Maxn + 5], head, tail;

int main() {
#ifdef LOACL
	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);
#endif
	ReadIn();
	for(int i = 1; i <= N; i++)
		sum[i] += sum[i - 1];
	head = tail = 1;
	for(int i = 1; i <= N; i++) {
		while(head < tail && calc_val(q[head + 1]) <= sum[i])
			head++;
		f[i] = q[head];
		while(head <= tail && calc_val(q[tail]) >= calc_val(i))
			tail--;
		q[++tail] = i;
	}
	BigInteger ans = 0;
	int p = N;
	while(p != 0) {
		BigInteger tmp(sum[p] - sum[f[p]]);
		ans += (tmp * tmp);
		p = f[p];
	}
	cout << ans;
	return 0;
}

你可能感兴趣的:(#,NOIP系列,#,单调队列及斜率优化,CSP-S2019,D2T2,划分)