abc467EFG

image

E

题意:给你一堆数 \(a_i\) 和一堆数 \(b_i\) 还有一个数 \(m\),让你每次操作给某个 \(a_i\) 加一,使得最后满足所有的 \(a_i + a_{i + 1}\equiv b_i\mod m\)
求最小操作次数。
做法:
我们给每个 \(b_i\) 减去 \(a_i + a_{i + 1}\) 后取模,就只用考虑每个 \(a_i\) 的增量之和了。
假设 \(a_1\) 增了 \(k\),则

\[\begin{align*} a_1&\equiv k\mod m\\ a_2&\equiv b_1 - k\mod m\\ a_3&\equiv b_2 - (b_1 - k)\equiv b_2 - b_1 + k\mod m\\ a_4&\equiv b_3 - b_2 + b_1 - k\mod m\\ &\cdots \end{align*} \]

于是我们令 \(c_1 = 0\)\(c_i\equiv b_{i - 1} - c_{i - 1}\mod m\)
\(a_i\equiv c_i + (-1)^{i + 1}k\mod m\)
答案是 \(\sum ((c_i + (-1)^{i + 1})\mod m)\)
显然我们这么做是想要枚举 \(k\),但这样的话就是 \(\mathcal O(m)\),无法通过。
结论:我们只需要考虑使得某些 \(c_i + (-1)^{i + 1}k\equiv 0\mod m\)\(k\) 就行。
为什么呢?
因为整个答案的函数(以 \(k\) 为自变量)为分段函数,且在同一段内单调不降,而段的起始点恰好是某些使得 \(c_i + (-1)^{i + 1}k\equiv 0\mod m\)\(k\),因为假设现在的 \(k\) 就是这样的 \(k\),如果这一项 \(k\) 的系数是 \(1\),则 \(k\) 变为 \(k - 1\) 的时候这一项的贡献会由 \(0\) 变为 \(m - 1\),有一个很大的跨度。
明白了这一点之后,我们就可以 \(\mathcal O(n)\) 地统计答案了。
记录每个特殊 \(k\) 来自哪里,并统计这里的贡献即可。
代码细节很多,我调了 \(2h\) 😥

点击查看代码
#include <iostream>
#include <vector>
#include <algorithm>
#define int long long

using std::cin;
using std::cout;
const int N = 2e5 + 10;
struct Node
{
	int k, i;
};

int a[N];
int b[N];
int val[N];
int c[N];
Node t[N];
std::vector<int> id[N];

int lam(int n)
{
	return (n & 1) ? 1 : -1;
}
int lam2(int n)
{
	return (n & 1) ? 1 : 0;
}

signed main()
{
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; ++i)
		cin >> a[i];
	for (int i = 1; i < n; ++i)
		cin >> b[i];
	for (int i = 1; i < n; ++i)
		b[i] = ((b[i] - (a[i] + a[i + 1])) % m + m) % m;
	c[1] = 0;
	for (int i = 2; i <= n; ++i)
		c[i] = ((b[i - 1] - c[i - 1]) % m + m) % m;
	int ans = 0;
	for (int i = 1; i <= n; ++i)
		ans += c[i];
	for (int i = 1; i <= n; ++i)
		t[i] = {(int)(((-c[i]) / lam(i) % m + m) % m), i};
	std::sort(t + 1, t + n + 1, [&](const Node &a, const Node &b){return a.k < b.k;});
	int tot = 0;
	for (int i = 1; i <= n; ++i)
	{
		if (i == 1 || t[i].k != t[i - 1].k)
		{
			++tot;
			id[tot].push_back(t[i].i);
			val[tot] = t[i].k;
		}
		else
			id[tot].push_back(t[i].i);
	}
	int now = ans;
	for (int i = 1; i <= tot; ++i)
	{
		now += lam2(n) * (val[i] - val[i - 1]);	
		for (auto j : id[i])
			now = now - (c[j] + lam(j) * val[i]);
		ans = std::min(ans, now);
		for (auto j : id[i])
		{
			if (lam(j) == -1)
				now += m - 1 - (-1);
		}
	}
	cout << ans << '\n';
	return 0;
}

F

image
查询我们贪心地做。
把每组 \((A_i, B_i)\) 从按照 \(B_i\) 从大到小排序后按顺序发邮件一定是最优的。
证明:假设存在 \((A_i, B_i)\)\((A_{i + 1}, B_{i + 1})\) 满足 \(B_i < B_{i + 1}\),并且我们是按照这个顺序做的。
那么此时的贡献为 \(A_i + A_{i + 1} + B_{i + 1}\)
考虑交换 \(i,i + 1\) 之后,贡献为 \(\max(A_{i + 1} + B_{i + 1},A_i + A_{i + 1} + B_i)\) 一定小于原来的贡献,所以要交换。
得证。
那接下来就很简单了,我们可以搞个动态开点值域线段树,也可以上平衡树,只需要考虑每个值域上的后缀的贡献。
我这里写的是平衡树(FHQ-Treap)。

点击查看代码
#include <iostream>
#include <random>

using std::cin;
using std::cout;
const int N = 2e5 + 10;
typedef long long ll;
struct Node
{
	int a, b, id, key;
	ll ans;
	ll sum;
	int l, r;
	friend bool operator<(const Node &a, const Node &b)
	{
		return (a.b ^ b.b) ? a.b < b.b : a.id < b.id;
	}
	friend bool operator<=(const Node &a, const Node &b)
	{
		return (a.b ^ b.b) ? a.b < b.b : a.id <= b.id;
	}
} z[N];

int tot;
int root;
int a[N], b[N];
std::mt19937 rnd(time(0));

void push_up(int x)
{
	int l = z[x].l;
	int r = z[x].r;
	z[x].ans = z[x].a + z[x].b;
	z[x].sum = z[x].a;
	if (r)
	{
		z[x].ans = std::max(z[r].ans, z[x].ans + z[r].sum);
		z[x].sum += z[r].sum;
	}
	if (l)
	{
		z[x].ans = std::max(z[x].ans, z[x].sum + z[l].ans);
		z[x].sum += z[l].sum;
	}
}
void split(int x, Node v, int &L, int &R)
{
	if (!x)
	{
		L = R = 0;
		return;
	}
	if (z[x] <= v)
	{
		L = x;
		split(z[x].r, v, z[x].r, R);
	}
	else
	{
		R = x;
		split(z[x].l, v, L, z[x].l);
	}
	push_up(x);
}
int merge(int L, int R)
{
	if (!L || !R)
		return L | R;
	if (z[L].key > z[R].key)
	{
		z[L].r = merge(z[L].r, R);
		push_up(L);
		return L;
	}
	else
	{
		z[R].l = merge(L, z[R].l);
		push_up(R);
		return R;
	}
}
void del(int b, int i)
{
	int L, M, R;
	split(root, {0, b, i - 1, 0, 0, 0, 0, 0}, L, M);
	split(M, {0, b, i, 0, 0, 0, 0, 0}, M, R);
	root = merge(L, R);
}
int nwp(int a, int b, int i)
{
	++tot;
	z[tot] = {a, b, i, rnd() % (998244353), a + b, a, 0, 0};
	return tot;
}
void insert(int a, int b, int i)
{
	int L, R;
	split(root, {0, b, i, 0, 0, 0, 0, 0}, L, R);
	root = merge(merge(L, nwp(a, b, i)), R);
}

int main()
{
	int n, q;
	cin >> n >> q;
	for (int i = 1; i <= n; ++i)
		cin >> a[i];
	for (int i = 1; i <= n; ++i)
		cin >> b[i], insert(a[i], b[i], i);
	for (int i = 1; i <= q; ++i)
	{
		int opt;
		cin >> opt;
		int id, x;
		cin >> id >> x;
		del(b[id], id);
		if (opt == 1)
			a[id] = x;
		else
			b[id] = x;
		insert(a[id], b[id], id);
		cout << z[root].ans << '\n';
	}
	return 0;
}

G

题意:给你一堆数 \(a_i\),每次单点修改;修改完后,对于 \(a_{l\sim r}\),每次取最大的数,问至少多少次后取出的数的总和 \(\ge\) \(k\),如果怎么也取不出总和 \(\ge\) \(k\) 的,输出 \(-1\)
查询不改变原序列。

整体二分做法

对于一个 \(mid\),我们进行值 \(> mid\) 的修改操作,对于每个查询,如果操作后 \(l\sim r\) 的总和(其实也就是 \(l\sim r\) 中值 \(> mid\) 的数的总和)大于等于 \(k\),那么显然值在 \([mid + 1, R]\) 的操作就已经足够了,我们直接把这个查询放到右面的区间中。
否则(总和小于 \(k\)),那说明单单取值 \(>mid\) 的数是不够的,我们把当前查询的 \(k\) 减等于这个总和,然后给这个查询的 \(ans\) 加等于这些数的个数并把当前查询放到左面的区间(\([L, mid]\))中即可。
维护区间加、区间和直接用树状数组即可。
时间复杂度 \(\mathcal O(n\log n\log V)\),如果离散化可以做到 \(\mathcal O(n\log^2n)\)

点击查看代码
#include <iostream>
#define lowbit(x) x & (-x)

using std::cin;
using std::cout;
const int N = 4e5 + 10;
typedef long long ll;
struct Node
{
	int opt;
	int l, r;
	int val;
	ll k;
	int id;
};

int n, q;
int tot;
int a[N];
int cnt[N];
int ans[N];
ll sum[N];
Node t[N];
Node t1[N];
Node t2[N];

void add(int x, int c, int s)
{
	for (; x <= n; x += lowbit(x))
	{
		sum[x] += s;
		cnt[x] += c;
	}
}
ll qs(int x)
{
	ll ret = 0;
	for (; x; x -= lowbit(x))
		ret += sum[x];
	return ret;
}
int qc(int x)
{
	int ret = 0;
	for (; x; x -= lowbit(x))
		ret += cnt[x];
	return ret;
}
void get(int L, int R, int ql, int qr)
{
	if (ql > qr)
		return;
	if (L == R)
	{
		for (int i = ql; i <= qr; ++i)
		{
			if (t[i].opt)
				add(t[i].l, t[i].opt, t[i].val * t[i].opt);
			else
			{
				ll nows = qs(t[i].r) - qs(t[i].l - 1);
				if (nows < t[i].k)
					ans[t[i].id] = -1;
				else
					ans[t[i].id] += (t[i].k + L - 1) / L;
			}
		}
		for (int i = ql; i <= qr; ++i)
		{
			if (t[i].opt)
				add(t[i].l, -t[i].opt, -t[i].val * t[i].opt);
		}
		return;
	}
	int M = (L + R) >> 1;
	int p1 = 0, p2 = 0;
	for (int i = ql; i <= qr; ++i)
	{
		if (t[i].opt)
		{
			if (t[i].val > M)
			{
				add(t[i].l, t[i].opt, t[i].val * t[i].opt);
				t2[++p2] = t[i];
			}
			else
				t1[++p1] = t[i];
		}
		else
		{
			ll nows = qs(t[i].r) - qs(t[i].l - 1);
			int nowc = qc(t[i].r) - qc(t[i].l - 1);
			if (nows >= t[i].k)
				t2[++p2] = t[i];
			else
			{
				t[i].k -= nows;
				ans[t[i].id] += nowc;
				t1[++p1] = t[i];
			}
		}
	}
	for (int i = ql; i <= qr; ++i)
	{
		if (t[i].opt && t[i].val > M)
			add(t[i].l, -t[i].opt, -t[i].val * t[i].opt);
	}
	int p = ql - 1;
	for (int i = 1; i <= p1; ++i)
		t[++p] = t1[i];
	for (int i = 1; i <= p2; ++i)
		t[++p] = t2[i];
	get(L, M, ql, ql + p1 - 1);
	get(M + 1, R, ql + p1, qr);
}

int main()
{
	cin >> n >> q;
	for (int i = 1; i <= n; ++i)
		cin >> a[i], t[++tot] = {1, i, 0, a[i], 0, 0};
	for (int i = 1; i <= q; ++i)
	{
		int c, x, l, r;
		ll k;
		cin >> c >> x >> l >> r >> k;
		t[++tot] = {-1, c, 0, a[c], 0, 0};
		a[c] = x;
		t[++tot] = {1, c, 0, a[c], 0, 0};
		t[++tot] = {0, l, r, 0, k, i};
	}
	get(1, 1e9, 1, tot);
	for (int i = 1; i <= q; ++i)
		cout << ans[i] << '\n';
	return 0;
}

分块做法

我们对原序列和值域都进行分块。
\(s[i][j]\) 表示前 \(i\) 个块内离散化后值在第 \(j\) 个块内的 \(a\) 的总和。
\(c[i][j]\) 表示前 \(i\) 个块内离散化后值在第 \(j\) 个块内的数的个数。
\(ss[i][j]\) 表示前 \(i\) 个块内离散化后的值恰好等于 \(j\) 的数的总和。
\(cc[i][j]\) 表示前 \(i\) 个块内离散化后的值恰好等于 \(j\) 的数个个数。
每次修改我们直接暴力修改,复杂度 \(\mathcal O(\sqrt n)\)
查询是平凡的。
时间复杂度 \(\mathcal O(q\sqrt n + n\sqrt n)\)

posted @ 2026-07-22 14:27  SigmaToT  阅读(0)  评论(0)    收藏  举报