251025B. 海啸

251025B. 海啸

\(n\) 个物品,物品 \(i\)\(v_i\) 的价值和 \(2^{w_i}\) 的体积。

以及 \(q\) 次修改,每次给出 \(x\) 并令 \(a_x \leftarrow a_x +1\)

每次修改后求出当总体积 \(\le V\) 时的最大总价值。

\[n\le 2\times 10^5,q\le 10^4, V\le 10^{18} \]


先考虑没有修改怎么做

\(w_i\) 相同的所有物品都在第 \(w_i\) 层。

我们先把每一层内部排序,然后考虑从小往大贪心。

对于第 \(w\) 层,假如 \(V\) 二进制下第 \(w\) 位为 \(1\),那么取出最大的价值 \(v_0\),将 \(v_0\) 累加到答案里,然后将其删去。

对于剩下的数,我们从大到小将每两个相邻的物品捆绑加入下一层。

这个过程可以归并解决。


考虑怎么带上修改?

我们从 \(w_x\) 开始依次枚举层,并考虑他造成的影响。

考虑到我们仅关心价值,不关心具体是哪个物品贡献的,我们在当前层二分找到第一个 \(\le v_x\) 的,然后将他 \(+1\)

然后我们模仿刚才的过程,将这个物品和相邻的捆绑,并在下一层二分找到对应的物品,将权值 \(+1\)

一直重复这个过程直到某一层 \(w\) 上这个价值是该层最大值,并且 \(V\) 二进制下第 \(w\) 位为 \(1\)。那么直接将答案 \(+1\),然后退出这个过程。


时间复杂度 \(\mathcal O(n\log V+q\log n\log V)\)

code
#include <iostream>
#include <algorithm>
#include <cstring>

const int N = 3e5 + 7, W = 60;
typedef long long i64;
#define rep(i,a,b) for(int i(a);i<=(b);++i)

namespace suki {

i64 sp, ans;
int n, q, w[N], v[N];
std::basic_string<i64> g[W]; 
auto cmp = std::greater<i64>();

inline void reset() { ans = 0; for(int i = 0; i < W; ++i) g[i].clear(); }

inline void main() {
	std::cin >> n >> sp >> q;
	rep(i, 1, n) std::cin >> w[i] >> v[i], g[w[i]] += v[i];
	for(int i = 0; i < W; ++i) std::sort(g[i].begin(), g[i].end(), cmp);

	auto work = [&](int k) {
		if(k > 0) {
			int origs = g[k].size(), shift = sp >> (k - 1) & 1;
			for(int i = shift; i + 1 < (int)g[k-1].size(); i += 2)
				g[k] += g[k-1][i+1] + g[k-1][i];
			if((g[k-1].size() - shift) & 1) g[k] += g[k-1].back();
			std::inplace_merge(g[k].begin(), g[k].begin() + origs, g[k].end(), cmp);
		}
		if((sp >> k & 1) && !g[k].empty()) { ans += g[k][0]; }
	};
	for(int i = 0; i < W; ++i) work(i);
	std::cout << ans << "\n";

	for(; q--; ) {
		int x; std::cin >> x;

		auto modify = [](int id) {
			i64 val = v[id]++;
			for(int k = w[id]; k < W; ++k) {
				int p = std::lower_bound(g[k].begin(), g[k].end(), val, cmp) - g[k].begin();
				int shift = sp >> k & 1; g[k][p]++;
				if(shift && (p == 0)) { ++ans; return ; }
				int q = ((p - shift) ^ 1) + shift;
				if(0 <= q && q < g[k].size()) val += g[k][q];
			}
		};
		modify(x);
		std::cout << ans << "\n";
	}
}

};

int main() {
	std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
	int t; std::cin >> t;
	for(; t--; ) { suki::reset(), suki::main(); }
}

之前见过一道相似的题,但是找不到原题了。题意如下:

给定一个大小为 \(n\) 的可重集 \(S={a_i}\)

维护以下两种操作:

  • 将第 \(l\) 大到第 \(r\) 大之间的所有数 \(+1\)

  • 询问第 \(l\) 大到第 \(r\) 大之间的所有数的和。

\[n\le 10^6 \]


我们初始先将 \(a\) 排序,然后直接按顺序放到线段树上,所以初始时整个序列是有序的。

对于一次修改操作 \([l,r]\),令 \(x=a_l,y=a_r\)\(p_0\)\(y\) 第一次出现的位置,\(p_1\)\(y\) 最后一次出现的位置。

假如我们将整个序列拆成三部分:\([1,p_0-1],[p_0,p_1],[p_1+1,n]\)

对于第一部分,我们修改的是一个后缀,所以直接在原位置上加不会破坏有序的性质。

对于中间的部分,由于值全部相同,我们可以将位于前缀的修改平移到后缀上,这个时候仍然不会破坏有序的性质。

对于最后一部分,我们没有修改,所以不会破坏有序的性质。

第一部分的最后一个数修改后 \(\le y\),中间部分的第一个数修改后 \(\ge y\),所以我们将前两段拼起来整体仍然满足有序的性质。

中间部分的最后一个数修改后 \(\le y+1\),最后一部分的第一个数 \(\ge y+1\),所以我们将后两段拼起来整体仍然满足有序的性质。

换句话说我们可以将每个修改拆成最多两个区间,使得直接在原位置上修改后整个序列仍然有序。

例如一开始的序列是 01112223334,我们希望做操作 \([4,8]\)。(011[12223]334

那么我们实际上做的区间加是:011[1222]33[3]4。加完后的结果是:01123333344


总而言之,这两个问题的维护方法,本质上都是利用了 \(+1\) 操作不会造成“交换”的性质,这使得我们直接在原位上对序列进行操作成为可能。

posted @ 2025-10-26 00:13  CuteNess  阅读(13)  评论(0)    收藏  举报