codeforces-1706-D1-D2

D1 (\(O(na_n\log(n))\))

链接:https://codeforces.com/contest/1706/problem/D1

滑动窗口,每个\(p_i\)从1开始逐渐递增,维护窗口内的最大的和最小的\(\lfloor\frac{a_i}{p_i}\rfloor\)。每次滑动复杂度\(O(log(n))\),每个\(a_i\)\(a_n\)\(p_i\)需要遍历,所以复杂度\(O(na_n\log(n))\)

代码:

#include <cstdio>
#include <queue>

using namespace std;

constexpr int MAXN = 3011;

int main() {
	int T;
	static int a[MAXN];
	static int p[MAXN];

	scanf("%d", &T);
	while (T--) {
		int n, k;
		scanf("%d%d", &n, &k);
		for (int i = 1; i <= n; ++i) {
			scanf("%d", a + i);
		}
		int ans = 0x3f3f3f3f;
		int mn = 0x3f3f3f3f;
		priority_queue<pair<int, int> > pq;
		for (int i = 1; i <= n; ++i) {
			p[i] = 1;
		}
		for (int i = 1; i <= n; ++i) {
			pq.push(make_pair(a[i], i));
			mn = min(mn, a[i]);
		}
		while (1) {
			int mx = pq.top().first;
			int i = pq.top().second;
			pq.pop();
			ans = min(ans, mx - mn);
			if (p[i] == k) {
				break;
			}
			p[i] += 1;
			int v = a[i] / p[i];
			pq.push(make_pair(v, i));
			mn = min(mn, v);
		}
		printf("%d\n", ans);
	}

	return 0;
}

D1 (\(O(n\sqrt{a_n}\log(n))\))

注意到\(\lfloor\frac{a_i}{p_i}\rfloor\)只有\(O(\sqrt{a_i})\)个可能取值,因此可以用数论分块遍历这\(O(\sqrt{a_i})\)个可以让\(\lfloor\frac{a_i}{p_i}\rfloor\)取不同值的\(p_i\)。复杂度降为\(O(n\sqrt{v}\log(n))\)

代码:

#include <cstdio>
#include <queue>

using namespace std;

constexpr int MAXN = 3011;

int main() {
	int T;
	static int a[MAXN];
	static int p[MAXN];

	scanf("%d", &T);
	while (T--) {
		int n, k;
		scanf("%d%d", &n, &k);
		for (int i = 1; i <= n; ++i) {
			scanf("%d", a + i);
		}
		int ans = 0x3f3f3f3f;
		int mn = 0x3f3f3f3f;
		priority_queue<pair<int, int> > pq;
		for (int i = 1; i <= n; ++i) {
			p[i] = 1;
		}
		for (int i = 1; i <= n; ++i) {
			pq.push(make_pair(a[i], i));
			mn = min(mn, a[i]);
		}
		while (!pq.empty()) {
			int mx = pq.top().first;
			int i = pq.top().second;
			pq.pop();
			ans = min(ans, mx - mn);
			if (mx == 0)
				break;
			p[i] = a[i] / mx + 1;
			if (p[i] > k)
				break;
			int v = a[i] / p[i];
			pq.push(make_pair(v, i));
			mn = min(mn, v);
		}
		printf("%d\n", ans);
	}

	return 0;
}

D2 (\(O(n\sqrt{a_n})\))

D2的n和\(a_n\)的范围都变成了1e5。考虑如何将维护窗口内的最大的\(\lfloor\frac{a_i}{p_i}\rfloor\)的复杂度降至\(O(1)\)

注意到值域仅为1e5,因此可以采用类似桶排序的思路,搞一个数组\(v\)\(v_x\)表示满足\(\lfloor\frac{a_i}{p_i}\rfloor = x\)\(i\)构成的集合。然后从大到小遍历这个数组,当前遍历到的下标就是当前窗口的\(\lfloor\frac{a_i}{p_i}\rfloor\)的最大值。

代码:

#include <cstdio>
#include <vector>

using namespace std;

constexpr int MAXN = 100011;
constexpr int MAXV = 100011;

int main() {
	int T;
	static int a[MAXN];
	static int p[MAXN];

	scanf("%d", &T);
	while (T--) {
		int n, k;
		scanf("%d%d", &n, &k);
		for (int i = 1; i <= n; ++i) {
			scanf("%d", a + i);
		}
		int ans = 0x3f3f3f3f;
		int mn = 0x3f3f3f3f;
		vector<int> v[MAXV];
		for (int i = 1; i <= n; ++i) {
			p[i] = 1;
		}
		int mx = a[1];
		for (int i = 1; i <= n; ++i) {
			v[a[i]].push_back(i);
			mn = min(mn, a[i]);
			mx = max(mx, a[i]);
		}
		for (; mx; mx -= 1) {
			if (v[mx].empty())
				continue;
			for (int i : v[mx]) {
				ans = min(ans, mx - mn);
				p[i] = a[i] / mx + 1;
				if (p[i] > k)
					goto out;
				int val = a[i] / p[i];
				v[val].push_back(i);
				mn = min(mn, val);
			}
			v[mx] = vector<int>();
		}
out:
		printf("%d\n", ans);
	}

	return 0;
}

D2 (\(O(a_n \log(a_n))\))

假设已知\(v = \max_{i}(\lfloor\frac{a_i}{p_i}\rfloor)\),那么我们就要最大化(但不大于\(v\))每个\(\lfloor\frac{a_i}{p_i}\rfloor\)。对于每个给定的\(a_i\),我们其实可以算出\(p_i\)

\(\lfloor\frac{a_i}{p_i}\rfloor \le v \Leftrightarrow \frac{a_i}{p_i} < v + 1 \Leftrightarrow p_i > \frac{a_i}{v+1} \Leftrightarrow p_i > \lfloor\frac{a_i}{v+1}\rfloor\)

要最大化\(\lfloor\frac{a_i}{p_i}\rfloor\),显然\(p_i\)越小越好,所以\(p_i\)应该取\(\lfloor\frac{a_i}{v+1}\rfloor + 1\)

换句话说,假如\(p_i\)的取值应该为\(u\),那么这个\(a_i\)应该满足:

\(\lfloor\frac{a_i}{v+1}\rfloor + 1 = u \Leftrightarrow u \le \frac{a_i}{v+1} + 1 < u + 1 \Leftrightarrow (u-1)(v+1) \le a_i < u (v+1)\)

这里官方题解\(a_i\)的右边界是闭合的:https://codeforces.com/blog/entry/105008。我觉得不对,不然边界上的u就有两种取值了。

这样,我们遍历\(u = 1, 2, ..., k\),把\([(u-1)(v+1), u(v+1))\)范围内的\(a_i\)\(p_i\)全部设为\(u\),即可最大化\(\lfloor\frac{a_i}{p_i}\rfloor\)的最小值。

但是怎么求出这个最小值呢?注意到对每个\(u\)\(\lfloor\frac{a_i}{u}\rfloor\)的最小值是用\([(u-1)(v+1), u(v+1))\)范围内最小的\(a_i\)取到的,因此维护区间最小值即可。注意到\([(u-1)(v+1), u(v+1))\)范围内的区间最小值(如果有的话)和\([(u-1)(v+1), +\infty)\)范围内的区间最小值相同,因此可以直接预计算出后缀最大值。遍历u的时候,查看\([(u-1)(v+1), +\infty)\)范围内的区间最小值,如果其小于\(u(v+1)\),那么它就是要求的区间最小值,算出对应的\(\lfloor\frac{a_i}{u}\rfloor\),更新全局最小值。

总的算法流程是枚举最大值\(v\),然后枚举\(u\),通过找出对应区间最小的\(a_i\)算出\(\lfloor\frac{a_i}{u}\rfloor\)的最小值,进而找到全局最小值,从而算出对当前\(v\)的值的答案。

注意到\(v\)到底是不是真的全局最大值其实对答案没有影响,因为如果\(v\)其实不是全局最大值,那么算出来的答案是偏大的。因此直接遍历\(v\)的值即可。

由于\(u\)最大为\(k\),而遍历\(u\)时,所有的\(a_i\)都应该在这里面的某个区间中。所有\(u\)的区间并为\([0, k(v+1))\),因此应该满足\(a_n < k(v+1)\),即\(v > \frac{a_n}{k} - 1 \Leftrightarrow v > \lfloor\frac{a_n}{k}\rfloor - 1 \Leftrightarrow v \ge \lfloor\frac{a_n}{k}\rfloor\)。因此遍历\(v\)的值时,应该是从\(\lfloor\frac{a_n}{k}\rfloor\)遍历到\(a_n\)

遍历\(u\)时,当\(a_n < (u-1)(v+1) \Leftrightarrow u > \frac{a_n}{v+1} + 1 \Leftrightarrow u > \lfloor\frac{a_n}{v+1}\rfloor + 1\)时,所有\(a_i\)都已经被区间包含了,这时就可以得到全局最小的\(\lfloor\frac{a_i}{p_i}\rfloor\)了。因此遍历\(u\)时应该从1到\(\lfloor\frac{a_n}{v+1}\rfloor + 1\)

因此总的复杂度为

\(O(\sum_{v=\lfloor\frac{a_n}{k}\rfloor}^{a_n} (\lfloor\frac{a_n}{v+1}\rfloor + 1)) = O(sum_{v=1}^{a_n} \frac{a_n}{v}) = O(a_n \log(a_n))\)

代码:

#include <cstdio>
#include <vector>

using namespace std;

constexpr int MAXN = 100011;
constexpr int MAXV = 100011;

int main() {
	int T;
	static int a[MAXN];
	static int ge[MAXV];

	scanf("%d", &T);
	while (T--) {
		int n, k;
		scanf("%d%d", &n, &k);
		a[0] = 0;
		for (int i = 1; i <= n; ++i) {
			scanf("%d", a + i);
		}
		for (int i = n; i; --i) {
			for (int j = a[i]; j > a[i-1]; --j) {
				ge[j] = a[i];
			}
		}
		ge[0] = a[1];
		int ans = 0x3f3f3f3f;
		for (int v = a[n] / k; v <= a[n]; ++v) {
			int mn = 0x3f3f3f3f;
			for (int u = 1; u <= a[n] / (v + 1) + 1; ++u) {
				int ai = ge[(u - 1) * (v + 1)];
				if (ai < u * (v + 1)) {
					mn = min(mn, ai / u);
				}
			}
			ans = min(ans, v - mn);
		}
		printf("%d\n", ans);
	}

	return 0;
}
posted @ 2024-09-28 14:12  寻找繁星  阅读(0)  评论(0编辑  收藏  举报