abc467EFG

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\),则
于是我们令 \(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

查询我们贪心地做。
把每组 \((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)\)

浙公网安备 33010602011771号