2025/10/20~2025/10/26 做题笔记
2025/10/20
AT_arc181_d Prefix Bubble Sort
很显然的有每一次交换都会恰好减少一个逆序对,于是题目转化为每次会产生多少次交换。
那么考虑如何统计交换次数
- 发现当前缀 max 变化时不会产生答案,但是这个折线非常困难维护,不考虑这种做法
- 考虑每个数往后交换多少次是难的,那么考虑有多少个数会往前交换。可以发现当前面有数大于这个数时,这个数一定会产生一次交换,那么直接统计有多少个数前面有大于它的数即可。由于题目限制了 \(A_i\) 单调不减,直接线段树简单维护即可
没有想到可以换一种贡献方式,老是在一棵树上吊死。
Code
#include <iostream>
using namespace std;
using ll = long long;
const int kN = 2e5 + 1;
int n, m, a[kN];
ll ans;
struct BIT {
int tr[kN];
void Update(int x, int k) {
for (; x < kN; x += x & -x)
tr[x] += k;
}
int Query(int x) {
int res = 0;
for (; x; x -= x & -x)
res += tr[x];
return res;
}
} Bit;
struct Point {
int mn, cnt, tag;
} tr[kN << 2];
void Func(int x, int k) {
tr[x].mn += k, tr[x].tag += k;
}
void Pushdown(int x) {
Func(x * 2, tr[x].tag), Func(x * 2 + 1, tr[x].tag), tr[x].tag = 0;
}
void Find(int x, int l, int r) {
if (tr[x].mn > 0)
return;
if (l == r)
return tr[x].mn = tr[x].tag = 1e9, tr[x].cnt = 0, void();
Pushdown(x);
int m = (l + r) / 2;
Find(x * 2, l, m), Find(x * 2 + 1, m + 1, r);
tr[x].mn = min(tr[x * 2].mn, tr[x * 2 + 1].mn), tr[x].cnt = tr[x * 2].cnt + tr[x * 2 + 1].cnt;
}
void Update(int nl, int nr, int x = 1, int l = 1, int r = n) {
if (nl <= l && r <= nr)
return Func(x, -1), Find(x, l, r);
Pushdown(x);
int m = (l + r) / 2;
if (nl <= m)
Update(nl, nr, x * 2, l, m);
if (nr > m)
Update(nl, nr, x * 2 + 1, m + 1, r);
tr[x].mn = min(tr[x * 2].mn, tr[x * 2 + 1].mn), tr[x].cnt = tr[x * 2].cnt + tr[x * 2 + 1].cnt;
}
void Build(int x = 1, int l = 1, int r = n) {
if (l == r)
return tr[x].cnt = a[l] > 0, tr[x].mn = a[l] ? a[l] : 1e9, void();
int m = (l + r) / 2;
Build(x * 2, l, m), Build(x * 2 + 1, m + 1, r);
tr[x].mn = min(tr[x * 2].mn, tr[x * 2 + 1].mn), tr[x].cnt = tr[x * 2].cnt + tr[x * 2 + 1].cnt;
}
int Query(int nl, int nr, int x = 1, int l = 1, int r = n) {
if (nl <= l && r <= nr)
return tr[x].cnt;
Pushdown(x);
int m = (l + r) / 2, res = 0;
if (nl <= m)
res = Query(nl, nr, x * 2, l, m);
if (nr > m)
res += Query(nl, nr, x * 2 + 1, m + 1, r);
return res;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, x; i <= n; i++) {
cin >> x;
a[i] = Bit.Query(n - x + 1), Bit.Update(n - x + 1, 1), ans += a[i];
}
Build();
cin >> m;
for (int i = 1, x; i <= m; i++) {
cin >> x;
ans -= Query(1, x), Update(1, x);
cout << ans << '\n';
}
return 0;
}
2025/10/21
A. makise
非常容易发现如果两个数加起来是奇数,则会有 1 的损耗。那么可以知道先手的策略一定是选取两个奇数,后手的策略一定是选取一奇一偶。以一轮先手后手操作为组,每轮消耗 3 个奇数,那么损耗就是 \(\lfloor \dfrac{cnt}{3} \rfloor + [cnt \mod 3 == 1]\),其中 \(cnt\) 表示奇数的个数
Code
#include <iostream>
using namespace std;
using ll = long long;
int n, odd;
ll s;
int main() {
#ifndef ONLINE_JUDGE
freopen("makise.in", "r", stdin);
freopen("makise.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, x; i <= n; i++) {
cin >> x, odd += x & 1, s += x;
if (i == 1)
cout << s << ' ';
else
cout << s - (odd / 3 + (odd % 3 == 1)) << ' ';
}
return 0;
}
B. shop
可以发现最后的答案一定是只选择一个 \(b_i\),那么对于所有的 \(a_j \ge b_i\) 都要选择。判断一下 \(a_i\) 有没有被选择和选择的 \(a\) 的个数是否大于 \(m\) 即可。
双指针没有限制 \(j \le n\),给我糖丸了
Code
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int kN = 3e5 + 1;
priority_queue<int, vector<int>, greater<int>> qe;
int T, n, m, od[kN];
pii a[kN];
ll ans, s;
int main() {
#ifndef ONLINE_JUDGE
freopen("shop.in", "r", stdin);
freopen("shop.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> T;
while (T--) {
cin >> m >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].first >> a[i].second;
od[i] = i;
}
sort(a + 1, a + n + 1, greater<pii>()), sort(od + 1, od + n + 1, [](int x, int y) { return a[x].second > a[y].second; });
for (int i = 1, j = 1; i <= n; i++) {
for (; j <= n && a[j].first >= a[od[i]].second; j++) {
if ((int)qe.size() < m)
qe.push(a[j].first), s += a[j].first;
else if (a[j].first > qe.top())
s += qe.top() - a[j].first, qe.pop(), qe.push(a[j].first);
}
if (m < j) {
ans = max(ans, s);
continue;
}
if (a[od[i]].first >= a[od[i]].second)
ans = max(ans, s + 1ll * (m - j + 1) * a[od[i]].second);
else
ans = max(ans, s + a[od[i]].first + 1ll * (m - j) * a[od[i]].second);
}
cout << ans << '\n';
ans = s = 0;
for (int i = 1; i <= n; i++)
a[i].first = a[i].second = 0;
priority_queue<int, vector<int>, greater<int>>().swap(qe);
}
return 0;
}
C. divide
注意到我这次没有打错名字()
才做了一道严格加强版 CF1175G Yet Another Partiton Problem 这道题还要上斜率优化。
除了普通的单调栈做法,还可以使用 CDQ 分治。具体来说考虑左边转移到右边,令 \(mn_i\) 表示 \([i, mid]/[mid + 1, i]\) 的 后缀/前缀 \(a_i\) 的最小值,分两种情况考虑 \(mn_l > mn_r\) 和 \(mn_l < mn_r\) 即可,由于 \(mn\) 在两边是单调的,所以对于一个区间做一次 dp 可以做到线性,总体时间复杂度 \(\mathcal {O}(n \log n)\)
现在看起来好装逼啊,完全没必要用 CDQ 分治。
Code
#include <algorithm>
#include <iostream>
#include <map>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int kN = 1e5 + 1;
int n, tp, a[kN], b[kN], c[kN], d[kN], st[kN];
ll f[kN];
void CDQ(int l, int r) {
if (l == r)
return f[l] = max(f[l], f[l - 1] + b[l]), void();
int m = (l + r) / 2;
CDQ(l, m);
c[m] = a[m], c[m + 1] = a[m + 1], d[m] = b[m], d[m + 1] = b[m + 1];
for (int i = m - 1; i >= l; i--) {
c[i] = min(c[i + 1], a[i]);
if (a[i] < c[i + 1])
d[i] = b[i];
else
d[i] = d[i + 1];
}
for (int i = m + 2; i <= r; i++) {
c[i] = min(c[i - 1], a[i]);
if (a[i] < c[i - 1])
d[i] = b[i];
else
d[i] = d[i - 1];
}
ll s = -1e18;
for (int i = m + 1, j = m + 1; i <= r; i++) {
for (; j >= l && c[j] >= c[i]; j--)
s = max(s, f[j - 1]);
f[i] = max(f[i], s + d[i]);
}
s = -1e18;
for (int i = r, j = l; i >= m + 1; i--) {
for (; j <= m + 1 && c[j] <= c[i]; j++)
s = max(s, f[j - 1] + d[j]);
f[i] = max(f[i], s);
}
CDQ(m + 1, r);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("divide.in", "r", stdin);
freopen("divide.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
fill(f + 1, f + kN, -1e18);
CDQ(1, n);
cout << f[n];
return 0;
}
D. Hot-Hotels 加强版
这道题和 Hot-Hotels 完全一样,在 dongxi 题单之前见到过。但是之前没有补,考场上手推了一下。
在 \(lca(x, y, z)\) 统计答案是最方便的,那么符合条件的图长这样:
并且 \(x、y、z\) 到 \(lca(x, y)\) 的距离都一样,设为 \(d\)。这个 \(d\) 不是很重要,我们只需要知道合法的 \(x、y\) 点对数量和符合条件的 \(z\) 数量即可。因此可设计状态:\(f_{i, j}\) 表示在 \(i\) 子树中,距离 \(i\) 为 \(j\) 的点数量、\(g_{i, j}\) 表示 \(i\) 子树 \(lca(x, y)\) 中距离 \(i\) 距离为 \(d - j\) 的点对数量,或者说和一个与 \(i\) 距离为 \(j\) 的点能构成答案的 \(x、y\) 点对数量。
理解了状态设计,答案统计和 \(dp\) 转移是非常简单的(之前就是没有能理解状态设计才不会)
这个东西非常明显可以使用长链剖分转移,然而我长剖题就没写过几道,之前以一种诡异的方法实现的长剖,导致我这次对于 \(g_{i, j}\) 的答案继承上出了很大的问题,赛后才改出来。
Code
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
const int kN = 1e5 + 1;
int n, tp[kN], s[kN], hs[kN], dep[kN];
ll ans, *f[kN], *g[kN], _[kN * 8], *p = _ + 1;
vector<int> e[kN];
void Dfs(int x, int fa) {
s[x] = 1;
for (int i : e[x]) {
if (i == fa)
continue;
Dfs(i, x);
if (s[i] + 1 > s[x])
s[x] = s[i] + 1, hs[x] = i;
}
}
void Dp(int x, int fa, bool t) {
if (t)
f[x] = f[fa] + 1, g[x] = g[fa] - 1;
else
f[x] = p, g[x] = p + 2 * (s[x] + 1), p += 4 * (s[x] + 1);
if (hs[x])
Dp(hs[x], x, 1);
f[x][0] = 1, ans += g[x][0];
for (int i : e[x]) {
if (i == fa || i == hs[x])
continue;
Dp(i, x, 0);
for (int j = 0; j < s[i]; j++)
ans += g[x][j + 1] * f[i][j] + f[x][j - 1] * g[i][j];
for (int j = 0; j < s[i]; j++) {
g[x][j] += g[i][j + 1], g[x][j + 1] += f[x][j + 1] * f[i][j];
f[x][j + 1] += f[i][j];
}
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("tree.in", "r", stdin);
freopen("tree.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, x, y; i < n; i++)
cin >> x >> y, e[x].push_back(y), e[y].push_back(x);
Dfs(1, 0), Dp(1, 0, 0);
cout << ans;
return 0;
}
2025/10/22 做题笔记
P6773 [NOI2020] 命运
我感觉这个 dp 应该比较的典,发现每个点子树内未满足的限制中只有最深的点有效,那么设计状态 \(f_{i, j}\) 表示子树 \(i\) 中,限制最深的点深度为 \(j\),设 \(b_i\) 表示每个点最深的限制,设 \(g_{i, j}\) 表示子树 \(i\) 中 \(f_i\) 的前缀和。
设当前子树为 \(i\),儿子为 \(x\),考虑 dp 转移。
- \(x\) 的父亲边染色,那么对于儿子内的所有限制都满足
- \(x\) 的父亲边不染色,那么考虑是我这个点的限制更深还是子树内的限制更深
综上有 \(f_{i, j} = f_{i, j} \cdot g_{x, dep_x} + f_{i, j} \cdot g_{x, j - 1} + g_{i, j} \cdot f_{x, j - 1} + f_{i, j} \cdot f_{x, j}\),进行小处理 \(f_{i, j} = f_{i, j}(g_{x, dep_x} + g_{x, j - 1} + f_{x, j}) + f_{x, j} \cdot g_{i, j - 1}\)
发现是 \(f_{i, j} \times a + f_{x, j} \times b\),考虑进行线段树合并优化。考虑线段树维护一段区间内的 \(f_{i}\) 的值,那么在线段树合并的时候顺便维护一下两个序列的前缀和,再 \(rt_x, rt_y\) 其中一个为空的时候打上乘法 tag 即可。tips: 要先递归合并右子树再合并左子树,因为合并左子树的时候会覆盖左边子树的答案,导致前缀和维护出错。
Code
#include <iostream>
#include <vector>
using namespace std;
const int kN = 5e5 + 1, kM = 998244353;
int n, m, cnt, b[kN], rt[kN], dep[kN];
vector<int> e[kN];
struct Seg {
int l, r, tag, sum;
} tr[kN * 20];
void Func(int x, int k) {
tr[x].sum = 1ll * tr[x].sum * k % kM, tr[x].tag = 1ll * tr[x].tag * k % kM;
}
void Pushdown(int x) {
Func(tr[x].l, tr[x].tag), Func(tr[x].r, tr[x].tag), tr[x].tag = 1;
}
void Update(int& x, int l, int r, int p) {
x = ++cnt;
tr[x].sum++, tr[x].tag = 1;
if (l == r)
return;
Pushdown(x);
int m = (l + r) / 2;
if (p <= m)
return Update(tr[x].l, l, m, p);
Update(tr[x].r, m + 1, r, p);
}
void Merge(int& x, int y, int l, int r, int sumx, int sumy) {
if (!x && !y)
return;
Pushdown(x), Pushdown(y);
if (!x || !y) {
!x && (swap(x, y), swap(sumx, sumy), 0);
return Func(x, sumy);
}
if (l == r)
return tr[x].sum = (1ll * tr[x].sum * sumy % kM + 1ll * tr[x].sum * tr[y].sum % kM + 1ll * tr[y].sum * sumx % kM) % kM, void();
int m = (l + r) / 2;
Merge(tr[x].r, tr[y].r, m + 1, r, (sumx + tr[tr[x].l].sum) % kM, (sumy + tr[tr[y].l].sum) % kM);
Merge(tr[x].l, tr[y].l, l, m, sumx, sumy);
tr[x].sum = (tr[tr[x].l].sum + tr[tr[x].r].sum) % kM;
}
int Query(int x, int l, int r, int nl, int nr) {
if (nl <= l && r <= nr)
return tr[x].sum;
Pushdown(x);
int m = (l + r) / 2, res = 0;
if (nl <= m)
res = Query(tr[x].l, l, m, nl, nr);
if (nr > m)
res = (res + Query(tr[x].r, m + 1, r, nl, nr)) % kM;
return res;
}
void Init(int x, int fa) {
dep[x] = dep[fa] + 1;
for (int i : e[x])
if (i != fa)
Init(i, x);
}
void Dfs(int x, int fa) {
Update(rt[x], 0, n, b[x]);
for (int i : e[x]) {
if (i == fa)
continue;
Dfs(i, x);
Merge(rt[x], rt[i], 0, n, 0, Query(rt[i], 0, n, 0, dep[x]));
}
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, x, y; i < n; i++)
cin >> x >> y, e[x].push_back(y), e[y].push_back(x);
Init(1, 0);
cin >> m;
for (int i = 1, x, y; i <= m; i++) {
cin >> x >> y;
if (dep[y] > dep[x])
swap(x, y);
b[x] = max(b[x], dep[y]);
}
Dfs(1, 0);
cout << Query(rt[1], 0, n, 0, 0);
return 0;
}
P5298 [PKUWC2018] Minimax
很明显的设 \(f_{i, j}\) 表示 \(i\) 子树内,权值为 \(j\) 的概率,额外设 \(g_{i, j}\) 和 \(h_{i, j}\) 分别表示后缀和和前缀和。
那么有转移式
化简得
和上一题一样的线段树合并技巧,不过这个需要多维护一个后缀
Code
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
const int kN = 3e5 + 1, kM = 998244353, kInv = 796898467;
int n, ans, cnt, p[kN], c[kN], rt[kN], s[kN][2];
struct Tr {
int l, r, sum, tag;
} tr[kN * 20];
int Pow(int x, int y) {
int res = 1;
for (; y; y >>= 1, x = 1ll * x * x % kM)
(y & 1) && (res = 1ll * res * x % kM);
return res;
}
void Func(int x, int k) {
tr[x].sum = 1ll * tr[x].sum * k % kM, tr[x].tag = 1ll * tr[x].tag * k % kM;
}
void Pushdown(int x) {
Func(tr[x].l, tr[x].tag), Func(tr[x].r, tr[x].tag), tr[x].tag = 1;
}
void Set(int& x, int p, int l = 1, int r = n) {
tr[x = ++cnt].sum = 1, tr[x].tag = 1;
if (l == r)
return;
int m = (l + r) / 2;
if (p <= m)
return Set(tr[x].l, p, l, m);
Set(tr[x].r, p, m + 1, r);
}
void Merge(int& x, int y, int p, int gx = 0, int gy = 0, int hx = 0, int hy = 0, int l = 1, int r = n) { // g 表示后缀,h 表示后缀
gx %= kM, gy %= kM, hx %= kM, hy %= kM;
if (!x && !y)
return;
Pushdown(x), Pushdown(y);
if (!x || !y) {
!x && (swap(x, y), swap(gx, gy), swap(hx, hy), 0);
return Func(x, 1ll * p * hy % kM + 1ll * (1 - p + kM) % kM * gy % kM);
}
int m = (l + r) / 2, _hx = tr[tr[x].l].sum, _hy = tr[tr[y].l].sum;
Merge(tr[x].l, tr[y].l, p, gx + tr[tr[x].r].sum, gy + tr[tr[y].r].sum, hx, hy, l, m);
Merge(tr[x].r, tr[y].r, p, gx, gy, hx + _hx, hy + _hy, m + 1, r);
tr[x].sum = (tr[tr[x].l].sum + tr[tr[x].r].sum) % kM;
}
int Query(int& x, int p, int l = 1, int r = n) {
if (!x)
return 0;
if (l == r)
return tr[x].sum;
Pushdown(x);
int m = (l + r) / 2;
return (p <= m) ? Query(tr[x].l, p, l, m) : Query(tr[x].r, p, m + 1, r);
}
void Dfs(int x) {
if (!s[x][0])
Set(rt[x], p[x]);
if (s[x][0])
Dfs(s[x][0]), rt[x] = rt[s[x][0]];
if (s[x][1])
Dfs(s[x][1]), Merge(rt[x], rt[s[x][1]], 1ll * c[p[x]] * 796898467 % kM);
// cout << x << ":\n";
// for (int i = 1; i <= n; i++)
// cout << i << ' ' << Query(rt[x], i) << '\n';
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, x; i <= n; i++)
cin >> x, s[x][1] = s[x][0], s[x][0] = i;
for (int i = 1; i <= n; i++)
cin >> p[i], c[i] = p[i];
sort(c + 1, c + n + 1);
for (int i = 1; i <= n; i++)
p[i] = lower_bound(c + 1, c + n + 1, p[i]) - c;
Dfs(1);
for (int i = 1, res = 0; i <= n; i++) {
int t = Query(rt[1], i);
if (!t)
continue;
res++;
ans = (ans + 1ll * res * c[i] % kM * t % kM * t % kM) % kM;
}
cout << ans;
return 0;
}
2025/10/23
昨天晚上 1 点睡,一天都没有精神,什么都思考不了/kk
D.马自立
可以将题目转化为是否存在一个子图包含给定边,并且这个子图存在欧拉回路。然后脑子短路了去看 T2 了。
如果继续思考会想到存在欧拉回路等价于每个结点的度都为偶数,那么题目就是 允许删去若干不重要的边,能否使得所有结点的度变成偶数。
考虑给定边不能动,那么把这些边断开,并赋予每个点一个初始的奇偶性。考虑剩下的一些连通块,这里有一个套路。首先建出一颗生成树,如果一个点的度数是奇数,那么把他和父亲的边断开。如果到根的时候根的度数还是奇数,那么说明不可把每个点都变成偶度数。进一步思考发现每删去一条边会导致两个点的度数奇偶性发生改变,这样 度数为奇数的点的个数 的奇偶性不会发生改变。注意到对于不合法的点的个数为偶数时,一定存在一种方案使得所有点都能调整过来,这个方案类似一个传递到父亲的过程。
所以直接用并查集维护每一个连通块的不合法的点的奇偶性即可
Code
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using pii = pair<int, int>;
const int kN = 5e5 + 1;
int T, n, m, fa[kN], sum[kN];
vector<pii> p;
int Find(int x) {
return fa[x] == x ? fa[x] : fa[x] = Find(fa[x]);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("festival.in", "r", stdin);
freopen("festival.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> T;
while (T--) {
cin >> n >> m;
fill(sum, sum + n + 1, -1), iota(fa + 1, fa + n + 1, 1);
for (int i = 1, x, y, z; i <= m; i++) {
cin >> x >> y >> z;
sum[x] = max(sum[x], 0), sum[y] = max(sum[y], 0);
if (z)
sum[x] ^= 1, sum[y] ^= 1;
else
p.push_back({x, y});
}
for (auto it : p) {
int x = Find(it.first), y = Find(it.second);
if (x == y)
continue;
fa[y] = x, sum[x] ^= sum[y];
}
int ans = 1;
for (int i = 1; i <= n; i++)
if (i == fa[i])
ans &= sum[i] <= 0;
cout << (ans ? "Yes\n" : "No\n");
for (int i = 1; i <= n; i++)
p.clear();
}
return 0;
}
2025/10/24
P2824 [HEOI2016/TJOI2016] 排序
和之前的 P2839 [国家集训队] middle 做法差不多,都是二分答案然后将序列转化为 01 序列更方便 check。这种题竟然可以用二分答案,感觉很巧妙
Code
#include <iostream>
using namespace std;
using pii = pair<int, int>;
const int kN = 1e5 + 1, kI = -1;
int n, m, x, a[kN], b[kN];
struct Query {
int x, l, r;
} q[kN];
struct Tr {
int tag, cnt;
} tr[kN << 2];
void Func(int x, int l, int r, int k) {
tr[x].cnt = (r - l + 1) * k, tr[x].tag = k;
}
void Pushdown(int x, int l, int r) {
int m = (l + r) / 2;
if (tr[x].tag != kI)
Func(x * 2, l, m, tr[x].tag), Func(x * 2 + 1, m + 1, r, tr[x].tag), tr[x].tag = kI;
}
void Update(int nl, int nr, int k, int x = 1, int l = 1, int r = n) {
if (nl > nr)
return;
if (nl <= l && r <= nr)
return Func(x, l, r, k);
Pushdown(x, l, r);
int m = (l + r) / 2;
if (nl <= m)
Update(nl, nr, k, x * 2, l, m);
if (nr > m)
Update(nl, nr, k, x * 2 + 1, m + 1, r);
tr[x].cnt = tr[x * 2].cnt + tr[x * 2 + 1].cnt;
}
int Query(int nl, int nr, int x = 1, int l = 1, int r = n) {
if (nl <= l && r <= nr)
return tr[x].cnt;
Pushdown(x, l, r);
int m = (l + r) / 2, res = 0;
if (nl <= m)
res = Query(nl, nr, x * 2, l, m);
if (nr > m)
res += Query(nl, nr, x * 2 + 1, m + 1, r);
return res;
}
void Build(int x = 1, int l = 1, int r = n) {
tr[x].tag = kI;
if (l == r)
return tr[x].cnt = b[l], void();
int m = (l + r) / 2;
Build(x * 2, l, m), Build(x * 2 + 1, m + 1, r);
tr[x].cnt = tr[x * 2].cnt + tr[x * 2 + 1].cnt;
}
bool Check(int t) {
for (int i = 1; i <= n; i++)
b[i] = t <= a[i];
Build();
for (int i = 1; i <= m; i++) {
int t = Query(q[i].l, q[i].r);
if (q[i].x)
Update(q[i].l, q[i].l + t - 1, 1), Update(q[i].l + t, q[i].r, 0);
else
Update(q[i].r - t + 1, q[i].r, 1), Update(q[i].l, q[i].r - t, 0);
}
return Query(x, x);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= m; i++)
cin >> q[i].x >> q[i].l >> q[i].r;
cin >> x;
int l = 1, r = n;
for (int m = (l + r + 1) / 2; l < r; m = (l + r + 1) / 2) {
if (Check(m))
l = m;
else
r = m - 1;
}
cout << l;
return 0;
}
Troubles
这么简单的题卡了我好久。
考虑把产生答案的 \((a_i, b_j)\) 放到二维平面上,很明显发现所有的这种点围成的矩形会包含所有 \((a_i, b_i)\),并且之间没有任何的偏序关系。所以当 \(b_j\) 递减的时候, \(a_i\) 一定递增,不然会产生偏序关系,答案一定不优。所以考虑按照 \(b_i\) 从大到小排序,此时只需要把 \(a_i\) 单调递增的点拿出来即可。
然后很明显的发现肯定是一段连续的区间在同一个矩形中,考虑设 \(f_i\) 表示考虑到第 \(i\) 个点,最优答案是多少。因为 \(b_i\) 和 \(a_i\) 都是单调的,所以区间左端点的 \(b_i\) 最大,右端点的 \(a_i\) 最大,有转移 \(f_i = \min(f_{j - 1} + a_i * b_j)\)。这个东西可以用单调队列优化也可以用李超树,我想着练习一下李超树所以写的李超树
要注意值域是 \(10^6\) 不是 \(10^5\)
Code
#include <algorithm>
#include <iostream>
using namespace std;
using pii = pair<int, int>;
using ll = long long;
const int kN = 3e6 + 1, kV = 1e6;
const ll kI = 1e18;
int n;
pii a[kN];
ll ans, f[kN];
struct Line {
ll k, b = kI;
ll Calc(int x) { return k * x + b; }
} tr[kN * 4];
bool Cmp(Line a, Line b, int x) {
return a.Calc(x) < b.Calc(x);
}
void Insert(Line v, int x = 1, int l = 1, int r = kV) {
int m = (l + r) / 2;
Cmp(v, tr[x], m) && (swap(v, tr[x]), 0);
if (Cmp(v, tr[x], l))
Insert(v, x * 2, l, m);
else if (Cmp(v, tr[x], r))
Insert(v, x * 2 + 1, m + 1, r);
}
ll Query(int p, int x = 1, int l = 1, int r = kV) {
if (l == r)
return tr[x].Calc(p);
int m = (l + r) / 2;
ll res = p <= m ? Query(p, x * 2, l, m) : Query(p, x * 2 + 1, m + 1, r);
return min(res, tr[x].Calc(p));
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i].first >> a[i].second;
sort(a + 1, a + n + 1, [](pii x, pii y) { return x.second > y.second || (x.second == y.second && x.first > y.first); });
for (int i = 1, s = 0; i <= n; i++) {
if (a[i].first <= s) {
f[i] = f[i - 1];
continue;
}
Insert({a[i].second, f[i - 1]}), ans = f[i] = Query(a[i].first);
s = a[i].first;
}
cout << ans;
return 0;
}
2025/10/25
P4198 楼房重建
pushup 可以用递归维护的线段树,非常巧妙。
这题需要注意的是 pushup 的时候右边的子树记录的答案不等于整个子树中右子树产生的贡献,而是需要整个子树的答案减去左边子树产生的贡献。略微卡精度
Code
#include <iostream>
using namespace std;
const int kN = 1e5 + 1;
const double eps = 1e-10;
int n, m;
struct Tr {
double k;
int cnt;
} tr[kN * 4];
int Pushup(double k, int x, int l, int r) {
if (l == r)
return tr[x].k - k > eps && tr[x].cnt;
int m = (l + r) / 2;
if (k - tr[x * 2].k > eps)
return Pushup(k, x * 2 + 1, m + 1, r);
return Pushup(k, x * 2, l, m) + tr[x].cnt - tr[x * 2].cnt;
}
void Update(int p, double k, int x = 1, int l = 1, int r = n) {
if (l == r)
return tr[x].cnt = 1, tr[x].k = k, void();
int m = (l + r) / 2;
if (p <= m)
Update(p, k, x * 2, l, m);
else
Update(p, k, x * 2 + 1, m + 1, r);
tr[x].cnt = tr[x * 2].cnt + Pushup(tr[x * 2].k, x * 2 + 1, m + 1, r), tr[x].k = max(tr[x * 2].k, tr[x * 2 + 1].k);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int x, y; m--;) {
cin >> x >> y;
Update(x, 1.0 * y / x);
cout << tr[1].cnt << '\n';
}
return 0;
}
H - Distinct Integers
主要是想到对于每个右端点,答案为 \(i - \max pre_j\),其中 \(pre_i\) 表示在 \(i\) 之前出现的最晚的 \(a_i\) 的位置
那么原问题转化为求前缀 \(max\) 的和,和上一题一样的做法即可
Code
#include <iostream>
#include <set>
using namespace std;
using ll = long long;
const int kN = 5e5 + 1;
int n, m, a[kN];
set<int> st[kN];
struct Tr {
int mx;
ll sum;
} tr[kN * 4];
ll Pushup(int k, int x, int l, int r) {
if (l == r)
return max(tr[x].mx, k);
int m = (l + r) / 2;
if (k >= tr[x * 2].mx)
return Pushup(k, x * 2 + 1, m + 1, r) + 1ll * (m - l + 1) * k;
return Pushup(k, x * 2, l, m) + tr[x].sum - tr[x * 2].sum;
}
void Update(int p, int k, int x = 1, int l = 1, int r = n) {
if (l == r)
return tr[x].mx = tr[x].sum = k, void();
int m = (l + r) / 2;
if (p <= m)
Update(p, k, x * 2, l, m);
else
Update(p, k, x * 2 + 1, m + 1, r);
tr[x].sum = tr[x * 2].sum + Pushup(tr[x * 2].mx, x * 2 + 1, m + 1, r), tr[x].mx = max(tr[x * 2].mx, tr[x * 2 + 1].mx);
}
ll Query(int k, int nl, int nr, int x = 1, int l = 1, int r = n) {
if (nl <= l && r <= nr)
return Pushup(k, x, l, r);
int m = (l + r) / 2;
ll res = 0;
if (nl <= m)
res = Query(k, nl, nr, x * 2, l, m);
if (nr > m)
res += Query(max(k, tr[x * 2].mx), nl, nr, x * 2 + 1, m + 1, r);
return res;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("in", "r", stdin);
freopen("out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i < kN; i++)
st[i].insert(0);
for (int i = 1; i <= n; i++) {
cin >> a[i], a[i]++;
Update(i, *st[a[i]].rbegin()), st[a[i]].insert(i);
}
for (int op, x, y; m--;) {
cin >> op >> x >> y, x++, y++;
if (!op) {
auto it = st[a[x]].find(x);
if (next(it) != st[a[x]].end())
Update(*next(it), *prev(it));
st[a[x]].erase(x), a[x] = y;
it = st[y].lower_bound(x);
if (it != st[y].end())
Update(*it, x);
Update(x, *prev(it));
st[y].insert(x);
} else
cout << 1ll * (x + y - 1) * (y - x) / 2 - Query(x - 1, x, y - 1) << '\n';
}
return 0;
}

浙公网安备 33010602011771号