Loading

【HT-063-NOI】核桃 NOI 组周赛解题报告

A - PVZ

题目描述

游戏在一个数轴上进行,初始没有僵尸。对于每个 \(i\ge 0\),在第 \(i\) 时刻会依次发生如下事件:

  • 场上所有僵尸同时向 \(x\) 轴正方向移动一单位长度。
  • \(x=0\) 处生成一个血量为 \(h_{i \bmod n}\) 的僵尸。
  • 第一棵植物对 \(x\) 坐标最大的僵尸造成 \(A\) 点伤害,第二棵植物对所有僵尸均造成 \(B\) 点伤害。一个僵尸血量 \(\le 0\) 时就会消失。

上述过程会不断地进行下去,求最大的被僵尸到达过的位置。

数据范围

对于 \(100\%\) 的数据,\(1\le T\le 2\times 10^5\)\(n\ge 1\)\(\sum n\le 2\times 10^5\)\(1\le h_i,A,B\le 10^9\)

\(\text{Subtask 1(10%)}\)\(\sum n\le 100\)\(h_i\le 100\)

\(\text{Subtask 2(20%)}\)\(h_i\le 300\)

\(\text{Subtask 3(30%)}\)\(\sum n\le 5\times 10^3\)

\(\text{Subtask 4(40%)}\):无特殊限制。

解题思路

\(\text{Subtask 1,2}\)

给定的游戏过程是无穷无尽的,但最终的答案(最大的被僵尸经过的位置)必然会收敛至某个值,因为在任意情况下,僵尸均会被造成 \(B\) 点伤害,则至多答案为 \(\lceil\frac{V}{B}\rceil\)

那么,游戏至多进行多少轮(一个时刻视作一轮)答案会收敛至最佳值呢?由于每 \(n\) 次答案必然会增加 \(1\),否则答案收敛,而答案至多增加 \(\lceil\frac{V}{B}\rceil\) 次,每次会至多有 \(n\) 轮,故至多进行 \(\lceil\frac{V}{B}\rceil n\) 轮。

考虑模拟游戏的过程,维护双端队列,按顺序存储僵尸的血量,每次在队尾加入 \(h_{i\bmod n}\) 血量的僵尸,并将队头血量已经为 \(0\) 的僵尸弹出。接下来,维护攻击操作,将队头的血量减少 \(A\),并将全局减少 \(B\)。这里,全局减少 \(B\) 可以通过打 \(\tt tag\) 维护,每个僵尸的实际值为 \(队列中的值 + {\tt tag}\)。全局减少 \(B\) 只需更新 \(\tt tag\),插入 \(v\) 只需要在队列中插入 \(v - {\tt tag}\)

时间复杂度:\(O(Vn)\)

实际得分:\(\bf {\color{#FF782C}{30}}\ /\ \color{#00D88D}{100}\)

\(\text{Subtask 3,4}\)

上述模拟的过程难以优化,维护的信息过于冗杂。考虑更换维护信息量少的做法,发现可递推出答案。令 \(f_i\) 表示前 \(i\) 只僵尸为第 \(i+1\) 只僵尸抵挡了多少次单体伤害,则第 \(i\) 只僵尸最远走到的位置为

\[f_{i-1}+\max\left(\left\lceil \frac{h_{i\bmod n}-B\cdot f_{i-1}}{A + B} \right\rceil, 0\right) \]

\(f_i\) 的值便是第 \(i\) 只僵尸走的步数减 \(1\),因为第 \(i+1\) 只僵尸会在第 \(i\) 只僵尸走了一步之后才出现。即,

\[f_i =f_{i-1}+\max\left(\left\lceil \frac{h_{i\bmod n}-B\cdot f_{i-1}}{A + B} \right\rceil, 0\right)-1 \]

不过,这样的时间复杂度还是 \(O(Vn)\) 的。分析题目中的性质,对于第 \(i\) 只僵尸(更准确地说是 \(h_i\) 血量的僵尸)每 \(n\) 轮会出现一次,而最远走到的位置必然是单调不降的,因为前面抵挡伤害的僵尸越来越多。

所以说,只要知道最后收敛至答案时的那 \(n\) 个僵尸最远走到的距离即可。而根据 \(f\) 的转移式,只需要知道 \(f_{kn-1}\) 的值,即可推出 \(f_{kn},\dots,f_{(k+1)n-1}\) 的值。如何快速计算 \(f_{kn-1}\) 的值呢?

注意到,\(f_{kn-1}\) 的值具有的二分性,若大于等于收敛的值 \(t\),则最终的 \(f_{(k+1)n-1}\le f_{kn-1}\),这是容易证明的,因为比原先的值增加了 \(f_{kn-1}-t\),且中间的过程只会比原来小而不会再增加,限制也增加了 \(f_{kn-1}-t\),所以是具有二分性的。

时间复杂度:\(O(n\log V)\)

实际得分:\(\bf {\color{#00D88D}{100}}\ /\ \color{#00D88D}{100}\)

参考代码

void solve() {
	int n, a, b;
	cin >> n >> a >> b;

	std::vector<int> h(n);
	for (int i = 0; i < n; i ++)
		cin >> h[i];

	int m = *max_element(h.begin(), h.end()) * n;
	auto calc = [&](int dp) -> int {
		for (int i = 0; i < n; i ++) {
			dp = dp + max((h[i] - b * dp + a + b - 1) / (a + b), 0ll);
			dp --;
		}
		return dp;
	};

	int lo = 0, ro = 1e9;
	while (lo <= ro) {
		int mid = lo + ro >> 1;
		if (calc(mid) <= mid) ro = mid - 1;
		else lo = mid + 1;
	}
	int dp = ro + 1, res = 0;
	for (int i = 0; i < n; i ++) {
		dp = dp + max((h[i] - b * dp + a + b - 1) / (a + b), 0ll);
		res = max(res, dp);
		dp --;
	}

	cout << res - 1 << endl;
}

B - 背包问题 1

题目描述

\(n\) 个物品,重量依次为 \(a_1,\dots, a_n\)

定义一个二元组 \((x,y)\) 合法,当且仅当对于任意 \(0\le x'\le x,0\le y'\le y\),都可以将物品分为三组,使得第一组重量之和为 \(x'\),第二组重量之和为 \(y'\)

进行 \(m\) 次修改,每次给定 \(p,w\),将 \(a_p\) 修改为 \(w\)

在所有修改之前及每次修改后求合法二元组 \((x,y)\) 的数量,答案对 \(998244353\) 取模。

数据范围

对于 \(100\%\) 的数据,\(1 \leq n \leq 2 \times 10^5\)\(0 \leq m \leq 2 \times 10^5\)\(1 \leq a_i, w \leq 10^{12}\)\(1 \leq p \leq n\)

  • \(\text{Subtask 1(10%)}\)\(n, a_i \leq 50, m = 0\)
  • \(\text{Subtask 2(20%)}\)\(n, a_i \leq 500, m = 0\)
  • \(\text{Subtask 3(25%)}\)\(m = 0\)
  • \(\text{Subtask 4(15%)}\):保证 \(a_i, p, w\) 均在 \(1 \sim n\) 中等概率随机。
  • \(\text{Subtask 5(30%)}\):无特殊限制。

解题思路

\(\text{Subtask 1}\)

总共的重量之和至多为 \(nV\),令 \(f_{i,j,k}\) 表示前 \(i\) 个物品,第一组重量之和为 \(j\),第二组重量之和为 \(k\) 是否可行。由于 \(f\) 只有 \(01\),可使用 bitset 优化。

时间复杂度:\(O(n^3V^2/w)\)

实际得分:\(\bf {\color{#FF1D27}{10}}\ /\ \color{#00D88D}{100}\)

\(\text{Subtask 2}\)

观察合法二元组的形态,构成阶梯状。比如说,\(a=(1,1,3)\),合法的二元组如 图 1 所示:

图 1

而答案只关系这样的一个阶梯状,更进一步地,只关心这个阶梯的轮廓线,因为有了轮廓线,内部均是合法的。轮廓线在 图 2 中标注红色。

图 2

是否 DP 的时候也只需要关注轮廓线,而无需处理轮廓线以外的点呢?答案是肯定的。唯一的问题在于在加入物品 \(i\) 的时候,可能会使某列或某行的长度增加 \(a_i\),且新增的区域与之前的区域有重叠,从而不止增加 \(a_i\)。但是,只需要将 \(a_i\) 从小到大排序,即可避免这种情况。

\(f_{i,j}\) 表示前 \(i\) 个物品,第一组重量之和为 \(j\) 的情况下的最大值,使得小于等于该值的第二组重量之和均能被凑出来。转移如下:

\[f_{i,j}=\max\begin{cases} f_{i-1,j}+a_i & f_{i-1,j}+1\ge a_i\\ f_{i-1,j-a_i} \end{cases} \]

时间复杂度:\(O(n^2V)\)

实际得分:\(\bf {\color{#FF782C}{30}}\ /\ \color{#00D88D}{100}\)

\(\text{Subtask 3}\)

背包 DP 无法与值域无关,所以为了与值域无关考虑如何不 DP。每次加入物品 \(i\) 相当于将长度大于等于 \(a_i\) 的列增加 \(a_i\),将长度大于等于 \(a_i\) 的行增加 \(a_i\)。不过,注意列和行之间还可能产生影响,即增加列的长度会使行的长度 \(+1\),增加行的长度同样会使列的长度 \(+1\),而 \(+1\) 可能刚好满足了大于等于 \(a_i\) 的条件。不过,聪明的你会发现这种情况只会发生在当前处理的物品总和为 \(S\),所有满足 \(x+y=S\)\((x,y)\) 均合法。

也就是说,初始的形态(图 3)是所有 \(x+y=S\)\((x,y)\) 均能凑出来,接下来一旦加入的 \(a_i\)\(\lceil L/2\rceil\) 大了,则会把行列分开,之后行列之间不再会有影响。

图 3:序列为 (1,1,1) 构成的形态

如果接下来加入 \(3\),即比 \(\lceil L/2\rceil=2\) 大,则后面行列不再会互相影响了,因为列所导致行增加 \(1\) 的值不可能再比 \(a_i\) 大了(\(a_i\) 是升序排序的)。于是,只需要先处理出初始形态的 \(L\),并将初始形态的可行二元组数 \((L+1)L/2\) 加入贡献。

接下来,继续加入剩余的物品,并维护前缀哪些列仍可增加 \(a_i\),每次对应加入贡献即可。由于行和列独立之后是对称的,所以不需要分别做,只需要将加的贡献乘 \(2\) 即可。

时间复杂度:\(O(n\log n)\)

实际得分:\(\bf {\color{#FFB800}{55}}\ /\ \color{#00D88D}{100}\)

核心代码:

const int mod = 998244353, I2 = mod + 1 >> 1;
i128 st = 1, i = 0;
for (; i < n; i ++)
    if ((st + 1) / 2 >= a[i]) st += a[i];
    else break;

if (i == n) cout << (int)(st % mod * (st + 1) % mod * I2 % mod) << endl;
else {
    i128 pos = st >> 1, j = i;
    int res = st % mod * (st + 1) % mod * I2 % mod;
    for (; i < n; i ++) {
        pos = min(st - a[i], pos);
        if (pos < 0) break;
        (res += (pos + 1) % mod * a[i] % mod * 2 % mod) %= mod;
        st += a[i];
    }

    cout << res << endl;
}

\(\text{Subtask 4,5}\)

带修怎么做?分析一下,只有在 \(a_i\) 比长度大的时候,才会不更新该长度,并将大于它的长度更新。这说明,每一次更新的前缀范围变化,长度便会乘 \(2\),也就是说前缀范围变化至多变化 \(O(\log V)\) 次。

那就好办了,只需要维护三棵线段树,一棵用于初始形态的二分,一棵用于中间前缀范围变化的二分,一棵用于更新答案。

时间复杂度:\(O(n\log n\log V)\)

实际得分:\(\bf {\color{#00D88D}{100}}\ /\ \color{#00D88D}{100}\)

参考代码

笔者时限的常数较大,在时限边缘徘徊,仅供参考。

#include <bits/stdc++.h>
#include <atcoder/lazysegtree>
using namespace std;
using namespace atcoder;
using i64 = long long;
using i128 = __int128;

i128 inf = 1;
i128 op(i128 x, i128 y) { return min(x, y); }
i128 e() { return inf; }
i128 mapping(i128 f, i128 x) { return x + f; }
i128 composition(i128 f, i128 g) { return f + g; }
i128 id() { return 0; }

int main() {
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);

	for (int i = 1; i <= 120; i ++, inf *= 2);

	int n, m;
	cin >> n >> m;

	std::vector<i64> a(n), to(n);
	std::vector<pair<i64, int>> in(n);
	std::vector<pair<i64, int>> dct;
	for (int i = 0; i < n; i ++)
		cin >> in[i].first, in[i].second = i;
	sort(in.begin(), in.end());

	for (int i = 0; i < n; i ++) {
		to[in[i].second] = i;
		a[i] = in[i].first;
	}

	for (int i = 0; i < n; i ++) {
		dct.emplace_back(a[i], i);
	}

	std::vector<pair<int, i64>> b(m);
	for (int i = 0; i < m; i ++) {
		cin >> b[i].first >> b[i].second;
		dct.emplace_back(b[i].second, i + n);
	}
	sort(dct.begin(), dct.end());

	std::vector<int> idx(n + m);
	for (int i = 0; i < n + m; i ++)
		idx[dct[i].second] = i;

	lazy_segtree<i128, op, e, i128, mapping, composition, id> S1(n + m), S2(n + m), S3(n + m);
	i128 sum = 1;
	for (int i = 0; i < n; i ++) {
		S1.set(idx[i], sum - 2 * a[i] + 1);
		S2.set(idx[i], sum - a[i]);
		sum += a[i];
	}
	sum = 1;
	for (int i = 0; i < n + m; i ++) {
		sum += dct[i].first * (dct[i].second < n);
		S3.set(i, sum);
	}

	const int mod = 998244353, I2 = mod + 1 >> 1;
	auto calc = [&]() -> int {
		int i = S1.max_right(0, [&](i128 x) { return x >= 0; });
		i128 st = !i ? 1 : S3.get(i - 1);

		int res = st % mod * ((st + 1) % mod) % mod * I2 % mod;
		i128 pos = st >> 1;
		for (; i < n + m; ) {
			pos = st - dct[i].first;
			if (pos < 0) break;
			int j = S2.max_right(i, [&](i128 x) { return x >= pos; });
			i128 t = S3.get(j - 1);
			(res += (pos + 1) % mod * ((t - st) % mod) % mod * 2 % mod) %= mod;
			i = j, st = t;
		}
		return res;
	};

	cout << calc() << '\n';

	for (int i = 0; i < n; i ++)
        a[i] = idx[i];
    for (int i = 0; i < m; i ++) {
		int j = to[b[i].first - 1];
        S1.set(a[j], inf);
        S2.set(a[j], inf);
        S1.apply(a[j] + 1, n + m, -dct[a[j]].first);
        S2.apply(a[j] + 1, n + m, -dct[a[j]].first);
        S3.apply(a[j], n + m, -dct[a[j]].first);

        a[j] = idx[i + n];

        S1.set(a[j], (!a[j] ? 1 : S3.get(a[j] - 1)) - 2 * dct[a[j]].first + 1);
        S2.set(a[j], (!a[j] ? 1 : S3.get(a[j] - 1)) - dct[a[j]].first);
        S1.apply(a[j] + 1, n + m, dct[a[j]].first);
        S2.apply(a[j] + 1, n + m, dct[a[j]].first);
        S3.apply(a[j], n + m, dct[a[j]].first);

        cout << calc() << '\n';
    }

	return 0;
}
posted @ 2025-06-09 20:22  Pigsyy  阅读(69)  评论(0)    收藏  举报