2025 暑期 mx 集训 7.17
T1
https://www.mxoj.net/problem/P60404?contestId=35
题意
你有 \(n\) 个点,有 \(26\) 种颜色,从浅到深为 A 到 Z。
你可以给一段连续的区间上色,深颜色会覆盖浅颜色。
给你 \(n\) 个点的目标颜色。
每次询问 \([l, r]\),求涂 \([1, l)\) 和 \((r, n]\) 的区间最少需要上多少次色。
\(n,q \leq 10^5\)。
Solution
考虑这两段无关,所以分开计算。
考虑合并贡献,比如:x,...,x。
那么如果这两个 x 之间,没有比他更小的点了,那么就可以一次涂完这两个。
所以我们分别维护一个前缀和后缀即可。
具体可以维护每个颜色上一次出现的位置,然后查询是否有 \(<\) 当前颜色的上一次出现位置 \(\ge\) 我这个颜色的上一次出现位置。
因为只有 \(26\) 种,所以可以暴力。当然也可以线段树。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, inf = 0x3f3f3f3f;
int n, q, pre[N], suf[N], p[N];
char a[N];
int main()
{
cin.tie(0)->ios::sync_with_stdio(false);
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1];
if (!p[a[i]]) pre[i]++;
else {
bool ok = 0;
for (int j = 'A'; j < a[i]; j++) if (p[j] >= p[a[i]]) ok = 1;
pre[i] += ok;
}
p[a[i]] = i;
}
for (int i = 'A'; i <= 'Z'; i++) p[i] = n + 1;
for (int i = n; i >= 1; i--) {
suf[i] = suf[i + 1];
if (p[a[i]] == n + 1) suf[i]++;
else {
bool ok = 0;
for (int j = 'A'; j < a[i]; j++) if (p[j] <= p[a[i]]) ok = 1;
suf[i] += ok;
}
p[a[i]] = i;
}
for (int i = 1; i <= q; i++) {
int l, r; cin >> l >> r;
cout << pre[l - 1] + suf[r + 1] << "\n";
}
return 0;
}
T2
https://www.mxoj.net/problem/P60447
题意
给你一个点,然后 \(q\) 次操作。
- 添加一个点,他的父亲为 \(fa\),权值为 \(w\)。
- 查询 \(\sum_{u = 1}^n \sum_{v = 1}^u w_{lca(u,v)}\)。
\(q \leq 10^5\)。
Solution
首先考虑先把树离线下来。
然后我们可以考虑每个点作为 \(lca\) 的次数。
那么有 \(\sum_{i = 1}^n w_i \times (sz_i^2 - \sum_{v\in son_i} sz_v^2)\)。
因为这题要保证 \(v \leq u\) 所以其实应该是 \(\frac{sz(sz + 1)}{2}\) 但是这样写太麻烦了,所以就简写成上面的形式。
然后这个式子可以用线段树维护,然后动态修改。巨麻烦。
所以有一个比较简单的,维护增量的做法。
考虑新增一个点,那么只有他到根的路径上这些点会作为 \(lca\)。
然后考虑每个点新的贡献是:\(w_u \times (sz_u - sz_v)\) 其中 \(v\) 是 \(u\) 的儿子且包含新增的点。
我们把新增的点叫 \(n\),那么除了 \(n\) 之外的点对的贡献我们都算过了,现在只算与 \(n\) 有关的贡献。
然后这个 \(u\) 的子树内,\(n\) 所在的子树不能选,因为这样他和 \(n\) 的 \(lca\) 就不是 \(u\) 了。
然后还有一个非常牛的东西,我们把每个点的权值变成 \(w_u - w_{fa}\)。
然后假如说:\(u, fa\) 本来原先的贡献是:\(w_u \times (sz_u - sz_v) + w_{fa} \times (sz_{fa} - sz_{u})\)
然后把换成新贡献:\((w_u - w_{fa}) \times (sz_u - sz_v) + (w_{fa} - w_{fa_{fa}}) \times (sz_{fa} - sz_u)\)
新贡献里 \(sz_v,sz_u\) 这里多减了,把他们去掉。
整体的全加起来,发现:对于原来的贡献,我们有一个 \(w_u \times (-sz_v)\),现在我们这里恰好全部抵消掉了。
于是只需要维护 \(c_u \times sz_u\) 的和即可。
感性理解就是,原来所有父亲都要减掉一部分儿子的贡献,现在在儿子那里直接加上一个负贡献直接抵消掉父亲那里的减。
Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 10, inf = 0x3f3f3f3f;
int t, n, a[N], c[N];
int dep[N], sz[N], son[N], f[N], top[N], dfn[N], rnk[N], cnt;
struct qry { int opt, u; } q[N];
vector<int> e[N];
void dfs(int u, int fa)
{
c[u] = a[u] - a[fa];
dep[u] = dep[(f[u] = fa)] + 1; sz[u] = 1;
for (auto v : e[u]) {
if (v == fa) continue;
dfs(v, u);
sz[u] += sz[v];
if (sz[son[u]] < sz[v]) son[u] = v;
}
}
void dfs1(int u, int t)
{
top[u] = t;
dfn[u] = ++cnt, rnk[cnt] = u;
if (son[u]) dfs1(son[u], t);
for (auto v : e[u]) if (v != f[u] && v != son[u]) dfs1(v, v);
}
struct node {
ll ans, tag;
ll sum;
} tr[N << 2];
#define lson (rt << 1)
#define rson (rt << 1 | 1)
#define mid (l + r) / 2
void build(int rt, int l, int r)
{
if (l == r) return tr[rt].sum = c[rnk[l]], void();
build(lson, l, mid), build(rson, mid + 1, r);
tr[rt].sum = tr[lson].sum + tr[rson].sum;
}
void push_down(int rt)
{
ll &u = tr[rt].tag;
if (!u) return;
tr[lson].ans += u * tr[lson].sum;
tr[rson].ans += u * tr[rson].sum;
tr[lson].tag += u, tr[rson].tag += u;
u = 0;
}
void update(int rt, int l, int r, int sp, int ep, int v)
{
if (sp <= l && r <= ep) {
tr[rt].ans += v * tr[rt].sum;
tr[rt].tag += v;
return;
}
push_down(rt);
if (sp <= mid) update(lson, l, mid, sp, ep, v);
if (ep > mid) update(rson, mid + 1, r, sp, ep, v);
tr[rt].ans = tr[lson].ans + tr[rson].ans;
}
ll query(int rt, int l, int r, int sp, int ep)
{
if (sp <= l && r <= ep) return tr[rt].ans;
push_down(rt);
ll res = 0;
if (sp <= mid) res = query(lson, l, mid, sp, ep);
if (ep > mid) res += query(rson, mid + 1, r, sp, ep);
return res;
}
void addpath(int u) { while (u) update(1, 1, n, dfn[top[u]], dfn[u], 1), u = f[top[u]]; }
ll qrypath(int u) { ll res = 0; while (u) res += query(1, 1, n, dfn[top[u]], dfn[u]), u = f[top[u]]; return res; }
int main()
{
cin.tie(0)->ios::sync_with_stdio(false);
int w; cin >> t >> w;
q[0] = { 1, ++n };
a[n] = w;
for (int i = 1; i <= t; i++) {
int opt; cin >> opt;
if (opt == 1) {
int fa, w; cin >> w >> fa;
q[i] = { opt, ++n };
a[n] = w;
e[fa].push_back(n), e[n].push_back(fa);
} else q[i] = { opt, 0 };
}
dfs(1, 0), dfs1(1, 1);
build(1, 1, n);
ll sum = 0;
for (int i = 0; i <= t; i++) {
if (q[i].opt == 1) addpath(q[i].u), sum += qrypath(q[i].u);
else cout << sum << "\n";
}
return 0;
}
T3
https://www.mxoj.net/problem/P60406
题意
你有 \(n\) 个点,每个点有颜色 \(a_i\)。
一开始所有点都是 \(0\)。
有 \(q\) 次操作,每次把所有颜色为 \(x\) 的点的状态反转,求有多少极长 \(1\) 的连续段。
\(n,q\leq 2\times 10^5\)。
Solution
对于这种题,考虑根号分治。
如果你这个颜色有 \(>B\) 个,那么最多只会有 \(\frac{n}{B}\) 个颜色会是这种情况。
考虑答案如何计算:\(1\) 的个数 \(-\) 连续 \(11\) 的个数。
前面这个是好算的,考虑如何计算后面这个。
我们可以把相邻的两个点的颜色连边,边的边权就是两个颜色之间有多少点是相邻的,这个是好维护的。
然后如果你要修改的颜色的点数 \(\leq B\) 那么就可以通过这个暴力修改,因为至多才 \(2B\) 条边。
现在考虑如果修改的颜色的点数 \(> B\) 怎么办。
首先对于其他的和这个颜色相邻的大点,我们可以预处理他们的贡献。像暴力那样,因为只有 \(\frac{n}{B}\) 个大点,所以直接暴力就行。
然后考虑和这个颜色相邻的小点,我们可以在修改小点的时候把他们和大点的贡献记下来,挂到大点上去,这样修改大点的时候就可以直接处理掉这些小点的贡献。
时间复杂度 \(O(n\sqrt n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, inf = 0x3f3f3f3f;
int n, q, k, a[N], sz[N], st[N], tag[N];
unordered_map<int, int> to[N];
vector<pair<int, int>> e[N], g[N];
int main()
{
cin.tie(0)->ios::sync_with_stdio(false);
cin >> n >> q >> k;
const int B = sqrt(n);
for (int i = 1; i <= n; i++) cin >> a[i], sz[a[i]]++;
for (int i = 1; i < n; i++) {
if (a[i] == a[i + 1]) sz[a[i]]--;
else to[a[i]][a[i + 1]]++, to[a[i + 1]][a[i]]++;
}
for (int i = 1; i <= k; i++) for (auto j : to[i]) {
int v, w; tie(v, w) = j;
e[i].emplace_back(v, w);
}
for (int i = 1; i <= k; i++) if (e[i].size() > B) {
for (auto j : e[i]) {
int v, w; tie(v, w) = j;
if (e[v].size() > B) g[i].emplace_back(v, w);
}
}
for (int i = 1; i <= k; i++) st[i] = 1;
int ans = 0;
for (int i = 1; i <= q; i++) {
int x; cin >> x;
ans += st[x] * sz[x]; st[x] *= -1;
if (e[x].size() <= B) {
for (auto j : e[x]) {
int v, w; tie(v, w) = j;
if (st[v] == -1) ans -= st[x] * st[v] * w;
tag[v] -= st[x] * w;
}
} else {
ans += tag[x] * st[x];
for (auto j : g[x]) {
int v, w; tie(v, w) = j;
if (st[v] == -1) ans -= st[x] * st[v] * w;
}
}
cout << ans << "\n";
}
return 0;
}
T4
https://www.mxoj.net/problem/P14228?contestId=35
题意
给你一个 \(n\) 个点 \(m\) 条边的无向图。保证联通,没有重边自环。
你可以选择若干个联通子图,并留下这个子图的并(点的并,边的并)。
求最后最多留下多少连通块。
\(n \leq 10^5, m\leq 2\times 10^5\)。
Solution
首先考虑一棵树,只能留下一个连通块。因为路径唯一。
考虑一个环,我们可以通过每次选择环上除了一条边的其他所有边和点,来把所有边的删掉。答案为环的大小。
考虑把图缩边双,那么每个边双都会贡献大小个连通块。考虑缩完边双后,整个图形成一颗树,每连接一条边就会使得答案 \(-1\)。
那么设边双个数为 \(x\),则答案为 \(n - (x - 1)\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, inf = 0x3f3f3f3f;
int n, m, dfn[N], low[N], cnt;
bool f[N << 1], vis[N];
vector<pair<int, int>> e[N];
void dfs(int u, int fa)
{
dfn[u] = low[u] = ++cnt;
for (auto t : e[u]) {
int v = t.first, id = t.second;
if (!dfn[v]) {
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) f[id] = 1;
}
else if (v != fa) low[u] = min(low[u], dfn[v]);
}
}
int main()
{
cin.tie(0)->ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
e[u].emplace_back(v, i), e[v].emplace_back(u, i);
}
for (int i = 1; i <= n; i++) if (!dfn[i]) dfs(i, 0);
auto dfs = [&](auto &&self, int u) -> void {
vis[u] = 1;
for (auto t : e[u]) {
int v = t.first, id = t.second;
if (vis[v] || f[id]) continue;
self(self, v);
}
};
int c = 0;
for (int i = 1; i <= n; i++) if (!vis[i]) {
c++;
dfs(dfs, i);
}
cout << n - (c - 1);
return 0;
}

浙公网安备 33010602011771号