DS 练习
P9058 [Ynoi2004] rpmtdq
树上支配对模板题。
P10834 [COTS 2023] 题 Zadatak
首先,不难发现由于原来正方形的边长为偶数,所以所有的正方形都可以划分成四个对称的边长为 \(\frac {a_i} 2\) 的小正方形。
由下图可知:

第 \(i\) 个 \(L\) 形颜色完全一样,且大小为 \(2\times i - 1\)。
原问题转化为 \(n\) 个长度为 \(a_i\) 的序列初始全为 \(1\),每次选两个序列异或合并,代价是两个序列按位与为 \(1\) 的位置对应的代价。
差分操作后,转化为有 \(n\) 个序列初始 \(a_n\) 为 \(1\),每次选两个序列异或合并,代价是两个序列的后缀异或和的按位与为 \(1\) 的位置对应的代价。
可以用线段树合并维护。
线段树维护当前区间的异或和,为 \(0\) 的位置的总代价,为 \(1\) 的位置的总代价。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("P10834_4.in", "r", stdin);
freopen("1.out", "w", stdout);
}
using ll = long long;
const int kN = 1e5 + 7;
int n, a[kN], rt[kN << 1], tot;
ll ans;
struct SGT{
struct node{
int x, ls, rs;
ll s[2];
}tree[kN * 21];
ll Get(int l, int r){
return 1ll * r * r - 1ll * (l - 1) * (l - 1);
}
void pushup(int x, int l, int r){
int mid = l + r >> 1;
tree[x].x = tree[tree[x].ls].x ^ tree[tree[x].rs].x;
tree[x].s[0] = (tree[x].rs ? tree[tree[x].rs].s[0] : Get(mid + 1, r)) + (tree[x].ls ? tree[tree[x].ls].s[tree[tree[x].rs].x] : (!tree[tree[x].rs].x ? Get(l, mid) : 0ll));
tree[x].s[1] = (tree[x].rs ? tree[tree[x].rs].s[1] : 0ll) + (tree[x].ls ? tree[tree[x].ls].s[tree[tree[x].rs].x ^ 1] : (tree[tree[x].rs].x ? Get(l, mid) : 0ll));
return ;
}
void update(int &x, int l, int r, int pos){
if(!x) x = ++tot;
if(l == r){
tree[x].x = 1;
tree[x].s[0] = 0, tree[x].s[1] = 2ll * pos - 1;
return ;
}
int mid = l + r >> 1;
if(pos <= mid) update(tree[x].ls, l, mid, pos);
else update(tree[x].rs, mid + 1, r, pos);
pushup(x, l, r);
return ;
}
void merge(int &x, int xa, int xb, int taga, int tagb, int l, int r){
if(!xa && !xb){
if(taga && tagb) ans += Get(l, r);
return ;
}
if(!xa){
if(taga) ans += tree[xb].s[tagb ^ 1];
x = xb;
return ;
}
if(!xb){
if(tagb) ans += tree[xa].s[taga ^ 1];
x = xa;
return ;
}
if(l == r){
if((tree[xa].x ^ taga) & (tree[xb].x ^ tagb)) ans += 2ll * l - 1;
x = xa;
tree[x].x ^= tree[xb].x;
tree[x].s[tree[x].x] = 2ll * l - 1;
tree[x].s[tree[x].x ^ 1] = 0;
return ;
}
x = xa;
int mid = l + r >> 1;
merge(tree[x].ls, tree[xa].ls, tree[xb].ls, taga ^ tree[tree[xa].rs].x, tagb ^ tree[tree[xb].rs].x, l, mid);
merge(tree[x].rs, tree[xa].rs, tree[xb].rs, taga, tagb, mid + 1, r);
pushup(x, l, r);
}
}sgt;
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
a[i] /= 2;
sgt.update(rt[i], 1, 1e6, a[i]);
}
int m = n - 1;
while(m--){
int x, y;
cin >> x >> y;
ans = 0;
sgt.merge(rt[++n], rt[x], rt[y], 0, 0, 1, 1e6);
cout << ans * 4ll << '\n';
}
return 0;
}
P14061 【MX-X21-T6】[IAMOI R5] 希望有羽毛和翅膀
挖掘性质:只会选长度为 \(k\) 或 \(k + 1\) 的子串,答案至多为 \(\frac n k\)。
发现讨论 \(k\) 有点复杂,使用 manacher 算法后,只需考虑回文中心即可,且对于一个回文中心,所有的回文串长度固定为奇数或偶数,所以不需要讨论 \(k\) 和 \(k + 1\)。
由于答案至多为 \(\frac n k\),考虑根号分治。
设 \(nxt\) 为下一个符合题意的回文中心。
当 \(k < B\) 时,考虑套用弹飞绵羊的 \(trick\),设 \(jump_i\) 表示从 \(i\) 跳出当前块跳到的位置,\(f_i\) 表示此过程中跳跃的次数。这样预处理时空复杂度为 \(\Theta(nB)\) ,询问的复杂度为 \(\Theta(q\times \frac n B)\)。
但是预处理的空间复杂度 \(\Theta(nB)\) 很不牛,发现问题是对于所有 \(k<B\) ,每个位置都存储了其对应的 \(nxt\)。如果每个位置都只存储了同一种 \(k\),那空间复杂度就变成线性的了。所以可以想到把询问离线,将 \(k\) 从小到大排序,每次遇到新的 \(k\),再把原来位置的 \(nxt\) 给覆盖,这样预处理的空间复杂度就做到 \(\Theta(n)\) 了。
当 \(k \ge B\) 时,由于答案很小,可以直接暴力跳 \(nxt\),问题在于如何快速求出 \(nxt\)。这个 \(nxt\) 由于量级较大,是无法预处理的。
记 \(p_i\) 表示以 \(i\) 为回文中心的最长回文半径,那么 \(nxt_i\) 实际上表示 \(i\) 右边第一个满足 \(p_j \ge k\) 的 \(j\)。这可以 st 表加二分做到 \(\Theta(q\times \frac n B \times log n)\)。考虑如何优化。
有一种思考方式是按 \(p\) 的值域从小到大扫描线,原问题转化为删除一个位置,查询一个位置的后继,可以用序列并查集维护。具体为将 \(i\) 和 \(i+1\) 合并,查询 \(i\) 所在的祖先。这样在均摊 \(\Theta(1)\) 的时间内可以查找到 \(nxt\),因为每次询问最多跳 \(\frac n B\) 次,所以时间复杂度为 \(\Theta((n+q)\times \frac n B)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
freopen("1.out", "w", stdout);
}
const int kN = 4e5 + 7;
int B, p[kN], n, Q, ans[kN];
struct node{
int l, r, k, id;
}q1[kN], q2[kN];
char s[kN];
string tmp;
static inline int To(int pos, int k) {
return pos + k + (((pos ^ k) & 1) ^ 1);
}
namespace Sol1{
int m, t, jump[kN], f[kN], pos[kN], L[kN], R[kN], nxt[kN], nxtm[kN];
void Ins(int l, int r, int k, int id){
q1[++m] = {l, r, k, id};
}
void main(){
t = n / B;
for(int i = 1; i <= t; i++) L[i] = R[i - 1] + 1, R[i] = i * B;
if(R[t] < n){
++t; L[t] = R[t - 1] + 1; R[t] = n;
}
for(int i = 1; i <= t; i++){
for(int j = L[i]; j <= R[i]; j++){
pos[j] = i;
}
}
sort(q1 + 1, q1 + m + 1, [&](node x, node y){return x.k < y.k;});
int lst = 0;
for(int i = 1; i <= m; i++){
int k = q1[i].k;
if(lst != k){
nxt[n + 1] = jump[n + 1] = n + 1;
for(int i = n, j = n + 1; i >= 1; i--){
if(p[i] >= k) j = i;
nxtm[i] = j;
if(p[i] >= k){
f[i] = 1;
if(To(i, k) + k <= n) jump[i] = nxt[i] = nxtm[To(i, k) + k];
else jump[i] = nxt[i] = n + 1;
if(jump[i] <= R[pos[i]]) f[i] += f[jump[i]], jump[i] = jump[jump[i]];
}
}
lst = k;
}
int l = q1[i].l, r = q1[i].r, id = q1[i].id;
if(l + k > r) continue;
int x = nxtm[l + k], res = 0;
while(To(jump[x], k) <= r) res += f[x], x = jump[x];
while(To(nxt[x], k) <= r) res++, x = nxt[x];
if(To(x, k) <= r) res++;
ans[id] = res;
}
return ;
}
}
namespace Sol2{
int m;
void Ins(int l, int r, int k, int id){
q2[++m] = {l, r, k, id};
}
struct DSU{
int fa[kN];
void init(){
for(int i = 1; i <= n + 1; i++) fa[i] = i;
}
int find(int x){
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void Del(int x){
fa[x] = find(x + 1);
}
}dsu;
int tp[kN];
void main(){
for(int i = 1; i <= n; i++) tp[i] = i;
sort(tp + 1, tp + n + 1, [&](int x, int y){return p[x] < p[y];});
sort(q2 + 1, q2 + m + 1, [&](node x, node y){return x.k < y.k;});
int lst = 0;
dsu.init();
for(int i = 1, j = 1; i <= m; i++){
int l = q2[i].l, r = q2[i].r, k = q2[i].k, id = q2[i].id;
while(j <= n && p[tp[j]] < k) dsu.Del(tp[j]), ++j;
int now, cnt = 0;
if(l + k <= n) now = dsu.find(l + k);
else continue;
while(To(now, k) <= r){
++cnt;
if(To(now, k) + k <= r) now = dsu.find(To(now, k) + k);
else break;
}
ans[id] = cnt;
}
return ;
}
}
void init(){
cin >> n >> Q;
cin >> tmp;
n = 0;
s[++n] = '#';
for(int i = 0; i < tmp.size(); i++){
s[++n] = tmp[i];
s[++n] = '#';
}
B = 700;
int j = 0, r = 0;
for(int i = 1; i <= n; i++){
if(r >= i) p[i] = min(p[2 * j - i], r - i + 1);
else p[i] = 1;
while(i + p[i] <= n && s[i + p[i]] == s[i - p[i]]) ++p[i];
if(i + p[i] - 1 > r){
r = i + p[i] - 1;
j = i;
}
}
for(int i = 1; i <= n; i++) p[i]--;
for(int i = 1; i <= Q; i++){
int l, r, k;
cin >> l >> r >> k;
l = 2 * l - 1, r = 2 * r + 1;
if(k < B) Sol1::Ins(l, r, k, i);
else Sol2::Ins(l, r, k, i);
}
return ;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
init();
Sol1::main();
Sol2::main();
for(int i = 1; i <= Q; i++) cout << ans[i] << '\n';
return 0;
}
P7230 [COCI 2015/2016 #3] NEKAMELEONI
此题可以加强到与 \(k\) 无关的做法,设 \(f_i\) 表示以 \(i\) 为左端点时最小的有端点。
发现修改一个位置的值很难维护 \(f_i\),但是删除一个数是比较容易维护的。设删除了一个位置 \(p\),记 \(l\),\(r\) 为上一个和下一个与 \(a_p\) 相等的下标,那么 \(\forall i \in (l,p]\),\(f_i=max(f_i,r)\)。这可以用吉司机线段树维护,当然容易发现 \(f_i\) 是单调不降的,所以可以线段树二分加区间赋值解决。
于是我们得到了最后的做法,一开始把所有的数包括修改后的数加入原序列,然后对删除操作线段树分治即可。时间复杂度 \(\Theta(q\log q \log n)\)
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kN = 1e5 + 7;
const int INF = 2e9;
int f[kN], n, k, m, ok[kN], a[kN], ans[kN];
multiset<int> s[kN];
vector<pii> q[kN << 2];
struct Stack{
int x;
int mx, mn;
int tag;
}stk[kN * 20 * 20];
int top;
struct SGT{
struct node{
int mx, mn, tag;
}tree[kN * 4];
void pushup(int x){
stk[++top] = {x, tree[x].mx, tree[x].mn, tree[x].tag};
tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
tree[x].mn = min(tree[x << 1].mn, tree[x << 1 | 1].mn);
return ;
}
void maketag(int x, int l, int r, int v){
stk[++top] = {x, tree[x].mx, tree[x].mn, tree[x].tag};
tree[x].mx = v;
tree[x].mn = v - r + 1;
tree[x].tag = v;
return ;
}
void pushdown(int x, int l, int r){
if(tree[x].tag){
int mid = l + r >> 1;
stk[++top] = {x, tree[x].mx, tree[x].mn, tree[x].tag};
maketag(x << 1, l, mid, tree[x].tag);
maketag(x << 1 | 1, mid + 1, r, tree[x].tag);
tree[x].tag = 0;
}
return ;
}
void update(int x, int l, int r, int L, int R, int v){
if(L <= l && r <= R){
maketag(x, l, r, v);
return ;
} pushdown(x, l, r);
int mid = (l + r) / 2;
if(L <= mid) update(x << 1, l, mid, L, R, v);
if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
pushup(x);
return ;
}
int query(int x, int l, int r, int k){
if(l == r){
if(tree[x].mx >= k) return l;
return n + 1;
} pushdown(x, l, r);
int mid = l + r >> 1;
if(tree[x << 1].mx < k) return query(x << 1 | 1, mid + 1, r, k);
return query(x << 1, l, mid, k);
}
}sgt;
void Upd(int x, int l, int r, int L, int R, pii v){
if(L > R) return ;
if(L <= l && r <= R){
q[x].push_back(v);
return ;
}
int mid = l + r >> 1;
if(L <= mid) Upd(x << 1, l, mid, L, R, v);
if(mid < R) Upd(x << 1 | 1, mid + 1, r, L, R, v);
return ;
}
void Add(int x, int y){
s[y].erase(s[y].lower_bound(x));
auto it = s[y].lower_bound(x);
if(*it == x) return ;
int l = *prev(it) + 1, r = *it, v;
l = max(l, 1), v = r;
r = min(r, sgt.query(1, 1, n, v) - 1);
if(l > r) return ;
sgt.update(1, 1, n, l, r, v);
return ;
}
void Sol(int x, int l, int r){
int ltop = top;
for(auto &[i, j] : q[x]) Add(i, j);
if(l == r){
ans[l] = sgt.tree[1].mn;
if(ans[l] > n) ans[l] = -1;
}
else{
int mid = l + r >> 1;
Sol(x << 1, l, mid);
Sol(x << 1 | 1, mid + 1, r);
}
while(top > ltop){
Stack tmp = stk[top];
sgt.tree[tmp.x].mx = tmp.mx;
sgt.tree[tmp.x].mn = tmp.mn;
sgt.tree[tmp.x].tag = tmp.tag;
top--;
}
for(auto &[i, j] : q[x]) s[j].insert(i);
return ;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> k >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];
s[a[i]].insert(i);
}
for(int i = 1; i <= m; i++){
int op, p, v;
cin >> op;
if(op == 1){
cin >> p >> v;
s[v].insert(p);
Upd(1, 1, m, 1, i - 1, mp(p, v));
Upd(1, 1, m, i, m, mp(p, a[p]));
a[p] = v;
}
else ok[i] = 1;
}
for(int i = 1; i <= k; i++) s[i].insert(-INF), s[i].insert(INF);
for(int i = 1; i <= k; i++) f[1] = max(f[1], *s[i].lower_bound(1)), sgt.update(1, 1, n, 1, 1, f[1]);
for(int i = 2; i <= n; i++){
f[i] = f[i - 1];
for(int j = 1; j <= k; j++){
f[i] = max(f[i], *s[j].lower_bound(i));
}
sgt.update(1, 1, n, i, i, f[i]);
}
Sol(1, 1, m);
for(int i = 1; i <= m; i++){
if(ok[i]) cout << ans[i] << '\n';
}
return 0;
}
P16458 [UOI 2026] mex plus
像这种二选一问题可以考虑图论建模,转化成边的定向的问题。假设有边 \(x\to y\),表示 \(x\) 放入 \(A\) 序列,\(y\) 放入 \(B\) 序列。那么最后答案转化为编号最小的入度为 \(0\) 的点和编号最小的出度为 \(0\) 的点的编号之和。对于每个连通块单独考虑,最后取 min 即可。
如果当前联通块有环,环上的结点入度出度都不为 \(0\)。度数为 \(1\) 的叶子肯定会影响答案,我们不妨把这些结点放入同一个序列中,这样对答案取 min 的影响最小。假设这个序列为 \(A\),那所有叶子结点都有边指向父结点,接着只需每个结点依次指向父结点,那剩下的结点入度和出度都不为 \(0\),不会影响答案。
如果当前连通块无环,和有环联通块做法类似。唯一有问题的是到根结点 \(rt\),\(rt\) 会被放入 \(B\) 序列影响最后的答案,如何解决 \(?\) 令编号最大的结点为根即可。
最后 \(mex(A)\) 就是所有联通块中度数 \(\le 1\) 的结点的编号最小值,\(mex(B)\) 就是所有无环联通块中编号最大值的最小值。
剩下的用线段树分治维护动态图即可,注意可撤销并查集不能路径压缩,要维护 \(height\) 保证期望树高为 \(\log n\),否则复杂度容易退化。
时间复杂度为 \(\Theta(q \log q \log n)\)。
下面我的代码 \(A\),\(B\) 序列表示的含义相反。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("4.in", "r", stdin);
freopen("1.out", "w", stdout);
}
const int kN = 2e5 + 7;
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
map<pii, vector<int> > ma;
int n, Q, tim[kN << 1], m, ans[kN], M;
vector<int> lsh;
struct Query{
int op, x, y;
}q[kN << 1];
vector<Query> tree[kN << 2];
struct Heap{
priority_queue<int, vector<int>, greater<int> > que1, que2;
void Ins(int x){
que1.push(x);
}
void Del(int x){
que2.push(x);
}
int query(){
while(!que2.empty() && que1.top() == que2.top()){
que1.pop(), que2.pop();
}
assert(que1.size());
return que1.top();
}
}A, B;
struct Stack{
int x, y;
int fx, fy;
int hx, hy;
int mxx, mxy;
}stk[kN * 40];
int top, deg[kN << 3];
struct DSU{
int fa[kN << 3], siz[kN << 3], ecnt[kN << 3], mx[kN << 3], height[kN << 3];
void init(){
for(int i = 0; i <= M; i++) fa[i] = i, siz[i] = 1, ecnt[i] = 0, height[i] = 1, mx[i] = i;
}
int find(int x){
return fa[x] == x ? fa[x] : find(fa[x]);
}
void merge(int x, int y){
int fx = find(x), fy = find(y);
if(fx == fy){
stk[++top] = {x, y, fx, fy, height[fx], height[fy], mx[fx], mx[fy]};
if(ecnt[fx] < siz[fx]) A.Del(mx[fx]);
ecnt[fx]++;
if(ecnt[fx] < siz[fx]) A.Ins(mx[fx]);
return ;
}
if(height[fx] > height[fy]) swap(fx, fy), swap(x, y);
stk[++top] = {x, y, fx, fy, height[fx], height[fy], mx[fx], mx[fy]};
if(ecnt[fx] < siz[fx]) A.Del(mx[fx]);
if(ecnt[fy] < siz[fy]) A.Del(mx[fy]);
fa[fx] = fy, siz[fy] += siz[fx], ecnt[fy] += ecnt[fx] + 1, height[fy] += (height[fx] == height[fy]), mx[fy] = max(mx[fy], mx[fx]);
if(ecnt[fy] < siz[fy]) A.Ins(mx[fy]);
return ;
}
void Imerge(int x, int y, int fx, int fy, int hx, int hy, int mxx, int mxy){
if(fx == fy){
if(ecnt[fx] < siz[fx]) A.Del(mx[fx]);
ecnt[fx]--;
if(ecnt[fx] < siz[fx]) A.Ins(mx[fx]);
return ;
}
if(ecnt[fy] < siz[fy]) A.Del(mx[fy]);
fa[fx] = fx;
height[fy] = hy;
siz[fy] -= siz[fx];
ecnt[fy] -= (ecnt[fx] + 1);
mx[fy] = mxy;
if(ecnt[fx] < siz[fx]) A.Ins(mx[fx]);
if(ecnt[fy] < siz[fy]) A.Ins(mx[fy]);
}
}dsu;
void Upd(int x, int l, int r, int L, int R, Query v){
if(L <= l && r <= R){
tree[x].push_back(v);
return ;
}
int mid = (l + r) / 2;
if(L <= mid) Upd(x << 1, l, mid, L, R, v);
if(mid < R) Upd(x << 1 | 1, mid + 1, r, L, R, v);
return ;
}
void Add(int x, int y){
if(deg[x] == 1) B.Del(x);
++deg[x];
if(deg[y] == 1) B.Del(y);
++deg[y];
dsu.merge(x, y);
}
void Withdraw(int x, int y, int fx, int fy, int hx, int hy, int mxx, int mxy){
if(deg[x] == 2) B.Ins(x);
--deg[x];
if(deg[y] == 2) B.Ins(y);
--deg[y];
dsu.Imerge(x, y, fx, fy, hx, hy, mxx, mxy);
}
void Solve(int x, int l, int r){
int lsttop = top;
for(auto i : tree[x]) Add(i.x, i. y);
if(l == r){
ans[l] = A.query() + B.query();
}
if(l != r){
int mid = l + r >> 1;
Solve(x << 1, l, mid);
Solve(x << 1 | 1, mid + 1, r);
}
while(top > lsttop){
int x = stk[top].x, y = stk[top].y;
int fx = stk[top].fx, fy = stk[top].fy;
int hx = stk[top].hx, hy = stk[top].hy;
int mxx = stk[top].mxx, mxy = stk[top].mxy;
Withdraw(x, y, fx, fy, hx, hy, mxx, mxy);
top--;
}
return ;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i++){
cin >> q[i].x >> q[i].y;
q[i].op = 1;
}
for(int i = 1; i <= Q; i++){
char op;
cin >> op >> q[n + i].x >> q[n + i].y;
if(op == '+') q[n + i].op = 1;
else q[n + i].op = -1;
}
for(int i = 1; i <= n + Q; i++){
if(q[i].x > 2 * (n + Q)) lsh.push_back(q[i].x);
if(q[i].y > 2 * (n + Q)) lsh.push_back(q[i].y);
}
sort(lsh.begin(), lsh.end());
auto lshcnt = unique(lsh.begin(), lsh.end());
lsh.erase(lshcnt, lsh.end());
int tmp = lshcnt - lsh.begin();
for(int i = 1; i <= n + Q; i++){
if(q[i].x > 2 * (n + Q)) q[i].x = 2 * (n + Q) + lower_bound(lsh.begin(), lsh.end(), q[i].x) - lsh.begin() + 1;
if(q[i].y > 2 * (n + Q)) q[i].y = 2 * (n + Q) + lower_bound(lsh.begin(), lsh.end(), q[i].y) - lsh.begin() + 1;
}
M = 2 * (n + Q) + tmp;
for(int i = 0; i <= M; i++) A.Ins(i), B.Ins(i);
for(int i = 1; i <= n + Q; i++) tim[i] = -1;
for(int i = 1; i <= n; i++){
ma[mp(q[i].x, q[i].y)].push_back(i);
tim[i] = 0;
}
for(int i = n + 1; i <= n + Q; i++){
if(q[i].op == 1){
ma[mp(q[i].x, q[i].y)].push_back(i);
tim[i] = i - n;
}
else{
int j = ma[mp(q[i].x, q[i].y)].back();
ma[mp(q[i].x, q[i].y)].pop_back();
Upd(1, 0, Q, tim[j], i - n - 1, q[j]);
tim[j] = -1;
}
}
for(int i = 1; i <= n + Q; i++){
if(tim[i] != -1){
Upd(1, 0, Q, tim[i], Q, q[i]);
tim[i] = -1;
}
}
dsu.init();
Solve(1, 0, Q);
for(int i = 0; i <= Q; i++) cout << ans[i] << '\n';
return 0;
}
P7560 [JOISC 2021] フードコート (Day1)
学习了 这篇文章 的整体二分 \(\Theta(q\log n)\) 做法,本质上就是将区间修改、单点查询问题转化成先把询问和修改按下标排序,然后差分将内部转化成线性。
这道题思考了很久都觉得无法二分,因为既有加也有减。实际上我们可以忽略 离开事件,把它看作 白嫖事件 中 \(B\) 的增量即可,这一部分可以先预处理。这样就有单调性,可以二分了。
有个小细节,如果有无解的情况,可以插入一个 \(Q+1\) 的策略,使其强制有解,这样写很方便,只需要最后输出答案的时候判断即可。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("21.in", "r", stdin);
freopen("1.out", "w", stdout);
}
using ll = long long;
const ll INF = 1e18;
const int kN = 3e5 + 7;
int n, m, Q, cnt;
struct BIT{
ll tree[kN];
void update(int x, ll v){
for(int i = x; i <= n; i += i & -i) tree[i] += v;
}
ll query(int x){
ll res = 0;
for(int i = x; i; i -= i & -i) res += tree[i];
return res;
}
}bit;
struct SGT{
struct node{
ll mx;
ll taga, tagb;
}tree[kN << 2];
void pushup(int x){
tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
return ;
}
void maketag(int x, ll v1, ll v2){
tree[x].mx += v1, tree[x].taga += v1;
if(tree[x].tagb > -INF) tree[x].tagb += v1;
tree[x].mx = max(tree[x].mx, v2);
tree[x].tagb = max(tree[x].tagb, v2);
}
void pushdown(int x){
maketag(x << 1, tree[x].taga, tree[x].tagb);
maketag(x << 1 | 1, tree[x].taga, tree[x].tagb);
tree[x].taga = 0, tree[x].tagb = -INF;
}
void build(int x, int l, int r){
tree[x].mx = tree[x].taga = 0, tree[x].tagb = -INF;
if(l == r) return ;
int mid = l + r >> 1;
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
pushup(x);
return ;
}
void update(int x, int l, int r, int L, int R, ll v1, ll v2){
if(L <= l && r <= R){
maketag(x, v1, v2);
return ;
} pushdown(x);
int mid = l + r >> 1;
if(L <= mid) update(x << 1, l, mid, L, R, v1, v2);
if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v1, v2);
pushup(x);
return ;
}
ll Qmax(int x, int l, int r, int pos){
if(l == r) return tree[x].mx;
pushdown(x);
int mid = l + r >> 1;
if(pos <= mid) return Qmax(x << 1, l, mid, pos);
else return Qmax(x << 1 | 1, mid + 1, r, pos);
}
}sgt;
struct Query{
int op, x, id;
ll v;
}q[kN << 2], p[kN << 2];
int col[kN], ans[kN];
void Sol(int l, int r, int L, int R){
int ql = L - 1, qr = R + 1;
int mid = l + r >> 1;
ll sum = 0;
for(int i = L; i <= R; i++){
if(q[i].op == 1){
if(q[i].id <= mid) sum += q[i].v, p[++ql] = q[i];
else p[--qr] = q[i];
}
else{
if(q[i].v <= sum) p[++ql] = q[i];
else q[i].v -= sum, p[--qr] = q[i];
}
}
for(int i = L; i <= ql; i++) q[i] = p[i];
for(int i = ql + 1; i <= R; i++) q[i] = p[R - (i - ql) + 1];
if(l == r){
for(int i = L; i <= R; i++){
if(q[i].op == 3) ans[q[i].id] = l;
}
return ;
}
Sol(l, mid, L, ql);
Sol(mid + 1, r, qr, R);
return ;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> Q;
sgt.build(1, 1, n);
for(int i = 1; i <= Q; i++){
int op, l, r, c, k, a;
ll b;
cin >> op;
if(op == 1){
cin >> l >> r >> c >> k;
q[++cnt] = {op, l, i, k};
q[++cnt] = {op, r + 1, i, -k};
col[i] = c;
bit.update(l, k); bit.update(r + 1, -k);
sgt.update(1, 1, n, l, r, k, -INF);
}
else if(op == 2){
cin >> l >> r >> k;
sgt.update(1, 1, n, l, r, -k, 0);
}
else{
cin >> a >> b;
q[++cnt] = {op, a, i, bit.query(a) - sgt.Qmax(1, 1, n, a) + b};
}
}
q[++cnt] = {1, 1, Q + 1, INF};
sort(q + 1, q + cnt + 1, [&](Query x, Query y){return x.x == y.x ? x.op < y.op : x.x < y.x;});
Sol(1, Q + 1, 1, cnt);
for(int i = 1; i <= Q; i++){
if(ans[i]){
if(ans[i] < i) cout << col[ans[i]] << '\n';
else cout << 0 << '\n';
}
}
return 0;
}
P9340 [JOIST 2023] 旅行 / Tourism
答案为虚树的边数 \(+1\),如何求虚树的边数?
经典结论:虚树的边数 $\times 2 = $ 关键点按 \(dfs\) 序排序后,相邻两点的距离之和。发现删除操作可以直接用链表维护,加入操作必须要带 \(\log\);又因为区间询问无修改,可以离线。所以直接考虑回滚莫队,只删除不增加。复杂度 \(\Theta(n\sqrt n)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
const int kN = 1e5 + 7;
int n, m, Q, c[kN], Log2[kN], ans[kN], B, pos[kN], L[kN], R[kN], t, idx, dfn[kN], id[kN], st[kN][21], dep[kN];
vector<int> ve[kN];
struct Query{
int x, y, id;
}q[kN];
void dfs(int x, int fa){
dfn[x] = ++idx, id[idx] = x;
st[dfn[x]][0] = dfn[fa];
dep[x] = dep[fa] + 1;
for(int j : ve[x]){
if(j == fa) continue;
dfs(j, x);
}
return ;
}
int lca(int x, int y){
if(x == y) return x;
x = dfn[x], y = dfn[y];
if(x > y) swap(x, y);
++x;
int k = Log2[y - x + 1];
return id[min(st[x][k], st[y - (1 << k) + 1][k])];
}
int dis(int x, int y){
if(x == 0 || y == 0) return 0;
return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
int pre[kN], nxt[kN], cnt[kN], res;
void Del(int x){
x = dfn[c[x]];
--cnt[x];
if(cnt[x] == 0){
res -= dis(id[pre[x]], id[x]);
res -= dis(id[nxt[x]], id[x]);
res += dis(id[pre[x]], id[nxt[x]]);
pre[nxt[x]] = pre[x];
nxt[pre[x]] = nxt[x];
}
return ;
}
void Add(int x){
x = dfn[c[x]];
if(cnt[x] == 0){
res -= dis(id[pre[x]], id[nxt[x]]);
res += dis(id[pre[x]], id[x]);
res += dis(id[nxt[x]], id[x]);
pre[nxt[x]] = x;
nxt[pre[x]] = x;
}
++cnt[x];
return ;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m >> Q;
for(int i = 2; i <= n; i++) Log2[i] = Log2[i >> 1] + 1;
B = sqrt(m);
t = m / B;
for(int i = 1; i <= t; i++) L[i] = R[i - 1] + 1, R[i] = i * B;
if(R[t] < m){
++t;
L[t] = R[t - 1] + 1, R[t] = m;
}
for(int i = 1; i <= t; i++){
for(int j = L[i]; j <= R[i]; j++) pos[j] = i;
}
for(int i = 1; i < n; i++){
int u, v; cin >> u >> v;
ve[u].push_back(v), ve[v].push_back(u);
}
dfs(1, 0);
for(int j = 1; j <= 20; j++){
for(int i = 1; i + (1 << j) - 1 <= n; i++) st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
for(int i = 1; i <= m; i++) cin >> c[i];
for(int i = 1; i <= Q; i++){
cin >> q[i].x >> q[i].y, q[i].id = i;
}
sort(q + 1, q + Q + 1, [&](Query x, Query y){return pos[x.x] != pos[y.x] ? x.x < y.x : x.y > y.y;});
int p = 0;
for(int i = 1; i <= t; i++){
for(int j = 1; j <= n; j++) cnt[j] = pre[j] = nxt[j] = 0;
for(int j = L[i]; j <= m; j++) ++cnt[dfn[c[j]]];
int lst = 0;
for(int j = 1; j <= n; j++){
pre[j] = lst;
if(cnt[j]) lst = j;
}
for(int j = 1; j <= n; j++){
if(!pre[j]) pre[j] = lst;
}
lst = 0;
for(int j = n; j >= 1; j--){
nxt[j] = lst;
if(cnt[j]) lst = j;
}
for(int j = 1; j <= n; j++){
if(!nxt[j]) nxt[j] = lst;
}
res = 0;
for(int j = 1; j <= n; j++){
if(cnt[j]) res += dis(id[pre[j]], id[j]);
}
int l = L[i], r = m;
while(p < Q && pos[q[p + 1].x] == i){
++p;
while(r > q[p].y) Del(r--);
while(l < q[p].x) Del(l++);
ans[q[p].id] = res / 2 + 1;
while(l > L[i]) Add(--l);
}
}
for(int i = 1; i <= Q; i++) cout << ans[i] << '\n';
return 0;
}
P9596 [JOI Open 2018] 冒泡排序 2 / Bubble Sort 2
设 \(f_i\) 表示 \([1,i]\) 内大于 \(a_i\) 的数的个数,\(g_i\) 为 \([1,i]\) 内 \(\le a_i\) 的数的个数,有 \(f_i=i-g_i\)。
直接用树套树维护是 \(\Theta(n \log^2 n)\) 的。
有冒泡排序的性质可知 \(ans=\max_{i=1}^n{f_i}\)。
最 关键的一步 来了,注意到当 \(g_i\) 表示 \([1,n]\) 内 \(\le a_i\) 的数的个数时,\(ans\) 保持不变。
所以我们用权值线段树维护全局 \(\le v\) 的个数,全局 \(f_i\) 的最大值即可。
复杂度 \(\Theta(n \log n)\)。
很多这种用树套树维护的题,可以发现有一维的限制是多余的,从而转化为一维限制。
当然这题还有很多代码细节值得学习。
如有 \(a_i\) 相同的值,我们视左边的小于右边的。
思考一下,如果不这么做的话,那么每次删除 \(a_i\) 时,我们还需要用 \(\text{set}\) 来维护值为 \(a_i\) 的 \(f_i\) 的最大值,复杂度又退化到 \(\Theta(n \log^2 n)\) 了。
而如果这么做的话,每次可以直接删除 \(a_i\) 的影响,且由于答案是取 \(\max\),\(a_i\) 相同的,只有可能最右边的 \(a_i\) 成为答案,所以最终不会影响答案。
如何处理,令所有的 \(a_i=n\times a_i+i\) 即可。
将所有的 \(a_i\) 和修改的值离散化后在插入线段树,避免动态开点的内存过大。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const ll INF = 1e18;
const int kN = 5e5 + 7;
int n, Q, b[kN], c[kN];
ll a[kN];
struct Query{
int x;
ll v;
}q[kN];
vector<ll> lsh;
int cnt;
struct SGT{
struct node{
int s, mx, tag;
}tree[kN << 3];
void pushup(int x){
tree[x].s = tree[x << 1].s + tree[x << 1 | 1].s;
tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
return ;
}
void maketag(int x, int v){
tree[x].mx += v;
tree[x].tag += v;
return ;
}
void pushdown(int x){
maketag(x << 1, tree[x].tag);
maketag(x << 1 | 1, tree[x].tag);
tree[x].tag = 0;
return ;
}
void Ins(int x, int l, int r, int pos, int v, int v2){
if(l == r){
tree[x].s += v;
tree[x].mx = v2;
return ;
} pushdown(x);
int mid = l + r >> 1;
if(pos <= mid) Ins(x << 1, l, mid, pos, v, v2);
else Ins(x << 1 | 1, mid + 1, r, pos, v, v2);
pushup(x);
return ;
}
void Del(int x, int l, int r, int pos){
if(l == r){
tree[x].s = 0, tree[x].mx = 0;
return ;
} pushdown(x);
int mid = l + r >> 1;
if(pos <= mid) Del(x << 1, l, mid, pos);
else Del(x << 1 | 1, mid + 1, r, pos);
pushup(x);
return ;
}
int query(int x, int l, int r, int L, int R){
if(L <= l && r <= R) return tree[x].s;
pushdown(x);
int mid = l + r >> 1, ans = 0;
if(L <= mid) ans += query(x << 1, l, mid, L, R);
if(mid < R) ans += query(x << 1 | 1, mid + 1, r, L, R);
pushup(x);
return ans;
}
void update(int x, int l, int r, int L, int R, int v){
if(L <= l && r <= R){
maketag(x, v);
return ;
} pushdown(x);
int mid = l + r >> 1;
if(L <= mid) update(x << 1, l, mid, L, R, v);
if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
pushup(x);
return ;
}
}sgt;
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i++){
cin >> a[i];
a[i] = 1ll * n * a[i] + 1ll * i;
b[i] = i;
lsh.push_back(a[i]);
}
sort(b + 1, b + n + 1, [&](int x, int y){return a[x] < a[y];});
for(int i = 1; i <= n; i++) c[b[i]] = i;
for(int i = 1; i <= Q; i++){
cin >> q[i].x >> q[i].v;
++q[i].x;
q[i].v = 1ll * n * q[i].v + 1ll * q[i].x;
lsh.push_back(q[i].v);
}
sort(lsh.begin(), lsh.end());
auto it = unique(lsh.begin(), lsh.end());
lsh.erase(it, lsh.end());
cnt = lsh.size();
for(int i = 1; i <= n; i++) a[i] = lower_bound(lsh.begin(), lsh.end(), a[i]) - lsh.begin() + 1;
for(int i = 1; i <= Q; i++) q[i].v = lower_bound(lsh.begin(), lsh.end(), q[i].v) - lsh.begin() + 1;
for(int i = 1; i <= 4 * cnt; i++) sgt.tree[i].mx = -Q;
for(int i = 1; i <= n; i++) sgt.Ins(1, 1, cnt, a[i], 1, i - c[i]);
for(int i = 1; i <= Q; i++){
sgt.update(1, 1, cnt, a[q[i].x], cnt, 1);
sgt.update(1, 1, cnt, q[i].v, cnt, -1);
sgt.Del(1, 1, cnt, a[q[i].x]);
a[q[i].x] = q[i].v;
int val = sgt.query(1, 1, cnt, 1, a[q[i].x]) + 1;
sgt.Ins(1, 1, cnt, a[q[i].x], 1, q[i].x - val);
cout << sgt.tree[1].mx << '\n';
}
return 0;
}
P14311 【MX-S8-T4】平衡三元组
性质 \(1\):设 \(y\) 为 \(a_i\),则 \(x\) 是 \([l,i)\) 的最大值,\(z\) 是 \((i,r]\) 的最大值。
性质 \(2\):由性质 \(1\) 可得:\(x\) 和 \(z\) 中必有一个为 \([l,r]\) 的最大值,否则不合法。
性质 \(2\) 我没有想出来。
我们钦定 \(x\) 为 \([l,r]\) 的最大值,下标记为 \(a\)。我们找到 \((a,r]\) 的最大值下标 \(a_0\)。
有以下几种情况:
当 \(y \in(a,a_0)\) 时,三元组 \((a,y,a_0)\) 都合法,取 \(y\) 的最大值加入答案。
当 \(y = a_0\) 时,令 \(a_1\) 为 \((a_0,r]\) 的最大值的下标,若三元组 \((a, a_0, a_1)\) 合法,则加入答案。
当 \(y \in(a_0,r)\) 时,若三元组 \((a, y, a_1)\) 合法,则此情况必定不比三元组 \((a, a_0, a_1)\) 更优。否则我们需要继续分讨 \(y \in(a_0,r)\) 的情况。注意到此情况与第一种情况有相似的结构,于是我们可以递归处理。
复杂度为什么是正确的呢?
若继续递归,满足:
- \(2\times A_{a_0}>A_a+A_{a_1}\),移项得:\(A_{a_1}<2\times A_{a_0}-A_a\);
- \(2\times A_{a_1}>A_a+A_{a_2}\),移项得:\(A_{a_2}<2\times A_{a_1}-A_a\);
与第一式联立,得:\(A_{a_2}<4\times A_{a_0}-3\times A_a\);
归纳一下 \(A_{a_i}<2^i \times A_{a_0} - (2^i-1) \times A_a\)。
即 \(A_{a_i}<A_a - 2^i \times( A_a - A_{a_0})\)
若 \(A_a \ne A_{a_0}\) 则递归 \(\log V\) 次;
若 \(A_a=A_{a_0}\) 则从 \(A_{a_1}\) 开始归纳;
若 \(A_a=A_{a_0}=A_{a_1}\) 则 \((A_a,A_{a_0},A_{a_1})\) 就为最终答案,和我们刚才讨论的第二种情况一致。
因为有区间修改、区间求 \(\max\),用线段树维护,时间复杂度 \(\Theta(q\log n \log v)\)
归纳法还是很重要的,需要掌握。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 1e6 + 7;
const ll INF = 1e17;
#define pli pair<ll, int>
#define mp(a, b) make_pair(a, b)
int n, Q;
ll a[kN], mx1, mx2;
struct SGT{
struct node{
pli mx;
ll tag;
}tree[kN << 2];
void pushup(int x){
tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
return ;
}
void maketag(int x, ll v){
tree[x].mx.first += v;
tree[x].tag += v;
return ;
}
void pushdown(int x){
if(tree[x].tag){
maketag(x << 1, tree[x].tag);
maketag(x << 1 | 1, tree[x].tag);
tree[x].tag = 0;
}
return ;
}
void build(int x, int l, int r){
tree[x].mx = mp(-INF, 0);
tree[x].tag = 0;
if(l == r){
tree[x].mx = mp(a[l], l);
return ;
}
int mid = l + r >> 1;
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
pushup(x);
return ;
}
void update(int x, int l, int r, int L, int R, ll v){
if(L > R) return ;
if(L <= l && r <= R){
maketag(x, v);
return ;
} pushdown(x);
int mid = l + r >> 1;
if(L <= mid) update(x << 1, l, mid, L, R, v);
if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
pushup(x);
return ;
}
pli query(int x, int l, int r, int L, int R){
if(L > R) return mp(-INF, 0);
if(L <= l && r <= R) return tree[x].mx;
pushdown(x);
int mid = l + r >> 1;
pli ans = mp(-INF, 0);
if(L <= mid) ans = max(ans, query(x << 1, l, mid, L, R));
if(mid < R) ans = max(ans, query(x << 1 | 1, mid + 1, r, L, R));
pushup(x);
return ans;
}
}sgt;
void dfs1(int x, ll v, int l, int r){
if(l >= r) return ;
pli p = sgt.query(1, 1, n, l + 1, r);
mx1 = max(mx1, v + p.first + sgt.query(1, 1, n, l + 1, p.second - 1).first);
int y = p.second;
pli q = sgt.query(1, 1, n, y + 1, r);
if(p.first * 2ll <= v + q.first) mx1 = max(mx1, v + p.first + q.first);
else dfs1(x, v, p.second, r);
return ;
}
void dfs2(int x, ll v, int l, int r){
if(l >= r) return ;
pli p = sgt.query(1, 1, n, l, r - 1);
mx2 = max(mx2, sgt.query(1, 1, n, p.second + 1, r - 1).first + p.first + v);
int y = p.second;
pli q = sgt.query(1, 1, n, l, y - 1);
if(p.first * 2ll <= v + q.first) mx2 = max(mx2, q.first + p.first + v);
else dfs2(x, v, l, p.second);
return ;
}
static inline void Out(int l, int r){
mx1 = -INF, mx2 = -INF;
pli p = sgt.query(1, 1, n, l, r);
dfs1(p.second, p.first, p.second, r);
dfs2(p.second, p.first, l, p.second);
if(max(mx1, mx2) <= -10000000000) cout << "No\n";
else cout << max(mx1, mx2) << '\n';
return ;
}
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i++) cin >> a[i];
sgt.build(1, 1, n);
for(int i = 1; i <= Q; i++){
int o, l, r; ll v;
cin >> o;
if(o == 1){
cin >> l >> r;
Out(l, r);
}
else{
cin >> l >> r >> v;
sgt.update(1, 1, n, l, r, v);
}
}
return 0;
}
P15455 [JOI 2026 SemiFinal] 新的桥梁 / New Bridge
发现题目给定的操作本质上就是 \(\text{prim}\) 求 \(\text{MST}\) 的过程。
由于题目保证了边权各不相同,所以原图的 \(\text{MST}\) 唯一。
\(D_{s,i}\) 是从 \(s\) 开始 \(\text{prim}\) 到 \(i\) 被加入 \(s\) 所在的点集之前所执行的操作次数。
这 等价于 \(\text{MST}\) 中点 \(j\to s\) 的路径上的最大边权小于 \(i\to s\) 这条路径上的路径上的最大边权 \(<w\) 的数量减一。(因为不考虑 \(j=s\))
我们看到最大边权 \(<w\) 可以想到 \(\text{Kruskal}\) 重构树,但事实上,我们并不需要真的把重构树建出来。我们把 \(\text{MST}\) 先拉出来,模拟 \(\text{Kruskal}\) 合并的过程,按边权从小到大枚举 \(i\to s\) 的最大边权,算这条边对跨两侧点对的贡献。
\(\text{MST}\) 默认以 \(1\) 为根。
记 \(Sub_u\) 为 \(\text{MST}\) 中 \(u\) 的子树,\(siz_u\) 为 \(u\) 这棵子树的大小,\(A\) 为合并前 \(u\) 所在的连通块,\(B\) 为合并前 \(v\) 所在的连通块。
考虑这条边为 \((u,v,w)\),\(s\) 和 \(i\) 分别位于 \(Sub_u\) 和 \(Sub_v\) 中,
以下假设 \(\text{MST}\) 中,\(fa_u=v\)。
当 \(s\in Sub(u)\) 时,\(i\in Sub(v)\)。\(s\) 可以为 \(Sub(u)\) 中的任意一个点,由于 \(\text{Kruskal}\) 的过程保证了边权从小到大合并到连通块中,所以 \(j\in A\),给 \(B\) 中所有点答案加上 \(|A|\times siz_u\)。
当 \(s\in Sub(v)\) 时,同理可得,给 \(A\) 中所有点答案加上 \(|B|\times (n-siz_u)\)。
如果 \(fa_v=u\) 讨论类似。
发现只需维护连通块的加法 \(tag\)。由于合并后原来连通块的 \(tag\) 与新加入连通块的点的 \(tag\) 不一致,考虑启发式合并暴力删除点以及维护每个点的 \(tag\) 修正量即可。
时间复杂度 \(\Theta(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 3e5 + 7;
int n, m, Q, f[kN], siz[kN];
struct edge{
int u, v, w;
}e[kN << 1];
struct DSU{
int fa[kN];
void init(){
for(int i = 1; i <= n; i++) fa[i] = i;
return ;
}
int find(int x){
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
}dsu;
vector<int> ve[kN], g[kN];
void dfs(int x, int fa){
siz[x] = 1, f[x] = fa;
for(int j : ve[x]){
if(j == fa) continue;
dfs(j, x);
siz[x] += siz[j];
}
return ;
}
ll sub[kN], add[kN];
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m >> Q;
for(int i = 1; i <= m; i++){
cin >> e[i].u >> e[i].v >> e[i].w;
}
dsu.init();
sort(e + 1, e + m + 1, [&](edge x, edge y){return x.w < y.w;});
for(int i = 1; i <= m; i++){
int u = dsu.find(e[i].u), v = dsu.find(e[i].v);
if(u != v) dsu.fa[u] = v, ve[e[i].u].push_back(e[i].v), ve[e[i].v].push_back(e[i].u);
}
dfs(1, 0);
dsu.init();
for(int i = 1; i <= n; i++) g[i].push_back(i);
for(int i = 1; i <= m; i++){
int u = dsu.find(e[i].u), v = dsu.find(e[i].v);
if(u != v){
if(f[e[i].v] == e[i].u){
add[u] += 1ll * g[v].size() * siz[e[i].v];
add[v] += 1ll * g[u].size() * (n - siz[e[i].v]);
}
else{
add[u] += 1ll * g[v].size() * (n - siz[e[i].u]);
add[v] += 1ll * g[u].size() * siz[e[i].u];
}
if(g[u].size() < g[v].size()) swap(u, v);
while(g[v].size()){
sub[g[v].back()] += add[v] - add[u];
g[u].push_back(g[v].back());
g[v].pop_back();
}
dsu.fa[v] = u;
}
}
for(int i = 1; i <= Q; i++){
int x; cin >> x;
cout << sub[x] + add[dsu.find(x)] << '\n';
}
return 0;
}
P13693 [CEOI 2025] Equal Mex
本题中 \(\text{Mex}\) 表示最小未出现的正整数。
考虑 \(\Theta(nq)\) 的暴力怎么写?
假设询问区间的 \(\text{Mex}\) 为 \(M\),那么这个区间内不出现 \(M\),且 \(1,\cdots, M-1\) 全部出现。由于每一个划分段的 \(\text{Mex}\) 要相等,大胆猜测每段的 \(\text{Mex}\) 都为 \(M\),这很好证明。
然后我们可以发现最大的划分数量 \(ans\) 其实就是答案,因为 \(k\in [1,ans]\) 都是可以取到的。
如何让划分数量最大,每段尽可能小即可,最后剩余的补到末尾一段即可。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kM = 4e5 + 3;
bool ma[kM];
vector<int> ans;
vector<int> solve(int n, vector<int> &a, int Q, vector<pii> &q){
for(int i = 0; i < Q; i++) q[i].first--, q[i].second--;
for(int k = 0; k < Q; k++){
int l = q[k].first, r = q[k].second;
for(int i = 0; i < n; i++) ma[a[i]] = 0;
for(int i = l; i <= r; i++) ma[a[i]] = 1;
int m = 1;
while(ma[m]) ++m;
int cnt = 0, t = 0;
for(int i = l; i <= r; i++) ma[a[i]] = 0;
for(int i = l; i <= r; i++){
if(!ma[a[i]] && a[i] < m){
++t;
ma[a[i]] = 1;
}
if(t == m - 1){
++cnt;
t = 0;
for(int j = 1; j < m; j++) ma[j] = 0;
}
}
ans.push_back(cnt);
}
return ans;
}
思考特殊性值 \(v_i\le 2\) 怎么做?
当 \(i\in [l,r]\),\(a_i\) 全部相同时,\(ans=r-l+1\)。
否则,我们思考上面的暴力能不能优化。
本质上,我们要求每一段都包含 \(1\) 和 \(2\),并且使这个段的长度最小,我们可以把这个过程看作一个不断向后跳的过程。
可以用倍增优化,记 \(jump_{i,0}\) 表示最小的 \(j\) 满足 \(i<j\) 且 \([i,j]\) 内既出现了 \(1\) 也出现了 \(2\),这里用二分求解。
时间复杂度 \(\Theta((n+q)\log n)\)。
每次询问直接倍增跳即可。
int s[kN][3], jump[kN][21];
vector<int> ans;
vector<int> solve(int n, vector<int> &a, int Q, vector<pii> &q){
s[0][1] = (a[0] == 1);
s[0][2] = (a[0] == 2);
for(int i = 1; i < n; i++){
s[i][1] = s[i - 1][1], s[i][2] = s[i - 1][2];
s[i][a[i]]++;
}
for(int i = 0; i <= n; i++)
for(int j = 0; j <= 20; j++)
jump[i][j] = INF;
for(int i = 0; i < n; i++){
int l = i, r = n - 1, mid, p = n;
while(l <= r){
mid = l + r >> 1;
if(s[mid][1] - (i ? s[i - 1][1] : 0) > 0 && s[mid][2] - (i ? s[i - 1][2] : 0) > 0){
r = mid - 1, p = mid;
}
else l = mid + 1;
}
if(p != n) jump[i][0] = p + 1;
}
for(int j = 1; j <= 20; j++){
for(int i = 0; i <= n; i++){
if(jump[i][j - 1] != INF) jump[i][j] = jump[jump[i][j - 1]][j - 1];
}
}
for(int i = 0; i < Q; i++){
int l = q[i].first, r = q[i].second;
l--; r--;
if(s[r][1] - (l ? s[l - 1][1] : 0) == 0 || s[r][2] - (l ? s[l - 1][2] : 0) == 0) ans.push_back(r - l + 1);
else{
int res = 0;
for(int j = 20; j >= 0; j--){
if(l < n && jump[l][j] <= r + 1) res += (1 << j), l = jump[l][j];
}
ans.push_back(res);
}
}
return ans;
}
考虑正解,我们知道极小 \(\text{Mex}\) 区间不超过 \(2\times n\) 个。证明:点击这里
科普:如何求区间 \(\text{Mex}\)?
用主席树维护前 \([1,i]\) 个数中,值域 \(j\in [1,V]\) 内的数字 \(j\) 出现的最大的位置的最小值。
查询区间 \([l,r]\) 时,在主席树上二分,若 \(tree[tree[x].l].mn<l\),则答案在左半区间;否则,答案在右半区间。
如何求出极小 \(\text{Mex}\) 区间?
一般我们有两种做法。
法一:假设当前我们已经找到了 \(\text{Mex}=k\) 的极小区间 \([l,r]\),现在我们要将其拓展,每次找到左边第一个等于 \(k\) 的 \(a_i\) 和右边第一个等于 \(k\) 的 \(a_j\),这可以用 \(\text{set}\) 维护。然后我们把这个区间拓展到 \([i,r]\) 和 \([l,j]\)。
有两个要注意的地方:
-
区间 \([i,r]\) 和 \([l,j]\) 的 \(\text{Mex}\) 不一定等于 \(k+1\),所以我们要用上文讲到的求区间 \(\text{Mex}\) 的方法。
-
刚拓展到 \(\text{Mex}=k\) 的区间,我们要把不符合极小 \(\text{Mex}\) 区间定义的去掉。极小 \(\text{Mex}\) 区间可以有交,但是不能包含。
法二:考虑从极小 \(\text{Mex}\) 区间数量的证明过程出发。对于极小 \(\text{Mex}\) 区间 \([l,r]\),我们钦定 \(a_l\) 为 \([l,r]\) 中的最大值。按 \(l\) 从大到小扫描线,维护 \(p_i\) 表示数字 \(i\) 出现的最靠左的位置。\(l\) 对应的 \(\text{Mex}\) 区间的右端点为 \(\max(p_1,\cdots,p_{a_l-1})\),\(\text{Mex}=a_l+1\)。对于 \(a_r\) 为 \([l,r]\) 中的最大值,同理。
显然法一的码量小于法二,故我们采用法一。
对于每一组询问 \([l,r]\),我们先找到区间 \([l,r]\) 的 \(\text{Mex}\),记为 \(m\)。我们把所有 \(\text{Mex}=m\) 的极小 \(\text{Mex}\) 区间拉出。沿用特殊性质的做法,我们倍增跳即可。
时间复杂度 \(\Theta(n\log V+q \log n)\)
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("33.in", "r", stdin);
freopen("1.out", "w", stdout);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
#define fi first
#define se second
const int INF = 1e8;
const int kN = 6e5 + 3;
const int kV = 4e5 + 3;
struct SGT{
int tot = 0;
struct node{
int l, r, mn;
}tree[kN * 20];
void pushup(int x){
tree[x].mn = min(tree[tree[x].l].mn, tree[tree[x].r].mn);
return ;
}
void update(int &x, int lstx, int l, int r, int pos, int v){
x = ++tot;
tree[x] = tree[lstx];
if(l == r){
tree[x].mn = v;
return ;
}
int mid = l + r >> 1;
if(pos <= mid) update(tree[x].l, tree[lstx].l, l, mid, pos, v);
else update(tree[x].r, tree[lstx].r, mid + 1, r, pos, v);
pushup(x);
return ;
}
int query(int x, int l, int r, int v){
if(l == r) return l;
int mid = l + r >> 1;
if(tree[tree[x].l].mn < v) return query(tree[x].l, l, mid, v);
else return query(tree[x].r, mid + 1, r, v);
}
}sgt;
struct Query{
int l, r, id;
};
set<int> s[kV]; // 维护左边右边第一个值为 v 的下标
vector<pii> Mex[kV];
vector<Query> qry[kN];
vector<int> ans;
int rt[kN], a[kN];
int jump[kN][21], Nxt[kN], as[kN];
vector<int> solve(int n, vector<int> &v, int Q, vector<pii> &q){
for(int i = 1; i <= n; i++) a[i] = v[i - 1];
for(int i = 1; i <= n; i++){
if(a[i] == 1) Mex[2].push_back({i, i});
else Mex[1].push_back({i, i});
s[a[i]].insert(i);
sgt.update(rt[i], rt[i - 1], 1, 400001, a[i], i);
}
if(Mex[1].size()) sort(Mex[1].begin(), Mex[1].end(), [&](pii x, pii y){return (x.first == y.first) ? x.second < y.second : x.first > y.first;});
for(int i = 2; i <= 400001; i++){
sort(Mex[i].begin(), Mex[i].end(), [&](pii x, pii y){return (x.first == y.first) ? x.second < y.second : x.first > y.first;});
vector<pii> tmp;
int mn = n + 1;
for(auto [l, r] : Mex[i]){
if(r < mn) mn = r, tmp.push_back(mp(l, r));
}
swap(Mex[i], tmp);
for(auto [l, r] : Mex[i]){
auto it = s[i].lower_bound(l);
int nl = 0, nr = 0;
if(it != s[i].begin()) --it, nl = *it;
it = s[i].upper_bound(r);
if(it != s[i].end()) nr = *it;
if(nl) Mex[sgt.query(rt[r], 1, 400001, nl)].push_back(mp(nl, r));
if(nr) Mex[sgt.query(rt[nr], 1, 400001, l)].push_back(mp(l, nr));
}
}
for(int i = 0; i < Q; i++){
int l = q[i].fi, r = q[i].se;
int v = sgt.query(rt[r], 1, 400001, l);
qry[v].push_back({l, r, i});
}
for(int i = 1; i <= 400001; i++){
int j = -1;
for(int k = 0; k < Mex[i].size(); k++){
int l = Mex[i][k].fi, r = Mex[i][k].se;
jump[l][0] = r + 1;
while(j + 1 < k && Mex[i][j + 1].fi > r) ++j;
if(j != -1) Nxt[r + 1] = Mex[i][j].fi;
for(int t = 1; t <= 20; t++) jump[l][t] = jump[Nxt[jump[l][t - 1]]][t - 1];
}
sort(qry[i].begin(), qry[i].end(), [&](Query x, Query y){return x.l > y.l;});
j = -1;
for(auto [l, r, id] : qry[i]){
int res = 0;
while(j + 1 < Mex[i].size() && Mex[i][j + 1].fi >= l) ++j;
if(j == -1) continue;
l = Mex[i][j].fi;
for(int t = 20; t >= 0; t--){
if(jump[l][t] && jump[l][t] <= r + 1) l = Nxt[jump[l][t]], res += 1 << t;
}
as[id] = res;
}
for(auto [l, r] : Mex[i]){
for(int t = 0; t <= 20; t++) jump[l][t] = 0;
Nxt[r + 1] = 0;
}
}
for(int i = 0; i < Q; i++) ans.push_back(as[i]);
return ans;
}
int n, Q;
vector<int> A;
vector<pii> q;
signed main(){
File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> Q;
for(int i = 1, x; i <= n; i++) cin >> x, A.push_back(x);
for(int i = 1, l, r; i <= Q; i++){
cin >> l >> r;
q.push_back(mp(l, r));
}
vector<int> tmp = solve(n, A, Q, q);
for(int i = 0; i < Q; i++) cout << tmp[i] << '\n';
return 0;
}
P5311 [Ynoi2011] 成都七中
连通块问题应该要想到点分治。设 \(s_{x,[l,r]}\) 表示点 \(x\) 通过点 \(j\in [l, r]\) 能到达的点的集合,显然有 \(y\in S\),\(s_{x,[l,r]}=s_{y,[l,r]}\)。
考虑当前分治重心为 \(rt\),若 \(rt\in s_{x,[l,r]}\),则 \(s_{x,[l,r]}=s_{rt,[l,r]}\)。若 \(rt\notin s_{x,[l,r]}\),\(s_{x,[l,r]}\) 全部位于 \(rt\) 的一棵子树中,所以点分治的正确性是对的。
记 \(mn_u\) 和 \(mx_u\) 表示 \(u\) 到 \(rt\) 路径上编号最小和最大的点。对于询问 \((l,r,x)\),若 \(\exists u\) 满足 \(l\le mn_u \le mx_u \le r\),则 \(a_u\) 可以被算入答案。
说句题外话,推导到这,我们发现原询问的 \(x\) 为干扰变量。
如何高效计算答案?
将 \([mu_u,mx_u]\) 视作一个区间,若 \([mu_u,mx_u]\subseteq [l,r]\),\(a_u\) 会被算入答案,这是经典的离线扫描线的数颜色问题。
将贡献和询问区间按左端点从大到小排序,记录 \(lst_i\) 表示颜色 \(i\) 所在区间的最小的右端点。
用树状数组维护区间内的颜色数量,每次更新 \(lst_i\) 时,删除原来位置的贡献,加入新位置的贡献,这样保证了每个颜色最多被算入一次答案。
每次查询就查前缀 \([1,r]\) 的和即可。
时间复杂度 \(\Theta(n\log^2 n+q\log n)\)。
题外话,这题还可以用 \(\text{Kruskal}\) 重构树来解决。
具体而言,发现限制为最小的点的编号大于等于 \(l\),最大的点的编号小于等于 \(r\),于是我们可以建一棵最大生成树和一棵最小生成树的 \(\text{Kruskal}\) 重构树,边权视作两节点编号的 \(\max\) 或 \(\min\),于是问题转化为了求两棵树中的子树的叶子节点颜色并。记点 \(i\) 在第一棵子树上的 \(dfs\) 序为 \(dfs1_i\),在第一棵树上的限制为 \(l1_i \le dfs1_i \le r1_i\),在第二棵子树上的 \(dfs\) 序为 \(dfs2_i\),限制为 \(l2_i \le dfs2_i \le r2_i\)。将 \((dfs1_i,dfs2_i)\) 视作一个点,于是问题又转化为了 二维数颜色问题。
于是发现做法假了。
好像又没假。

#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
const int kN = 1e5 + 7;
int a[kN], rt, n, m, siz[kN], ban[kN], ans[kN], del[kN], mn[kN], mx[kN], lst[kN], N;
vector<int> ve[kN];
struct Query{
int l, r, id, o;
};
vector<Query> q[kN], opt;
struct BIT{
int tree[kN];
void update(int x, int v){
for(int i = x; i <= N; i += i & -i) tree[i] += v;
return ;
}
int query(int x){
int res = 0;
for(int i = x; i; i -= i & -i) res += tree[i];
return res;
}
}bit;
void dfs(int x, int fa){
siz[x] = 1;
for(int j : ve[x]){
if(j == fa || del[j]) continue;
dfs(j, x);
siz[x] += siz[j];
}
return ;
}
void find_rt(int x, int fa){
int mx = 0;
for(int j : ve[x]){
if(j == fa || del[j]) continue;
find_rt(j, x);
mx = max(mx, siz[j]);
}
mx = max(mx, n - siz[x]);
if(mx <= n / 2){
rt = x;
}
return ;
}
void dfs2(int x, int fa){
opt.push_back({mn[x], mx[x], a[x], 1});
for(auto j : q[x]){
if(!ban[j.id] && j.l <= mn[x] && mx[x] <= j.r){
ban[j.id] = 1;
opt.push_back(j);
}
}
for(int j : ve[x]){
if(j == fa || del[j]) continue;
mn[j] = min(j, mn[x]);
mx[j] = max(j, mx[x]);
dfs2(j, x);
}
return ;
}
void solve(int x){
del[x] = 1;
mn[x] = mx[x] = x;
dfs2(x, 0);
sort(opt.begin(), opt.end(), [&](Query x, Query y){return x.l != y.l ? x.l > y.l : x.o > y.o;});
for(auto j : opt){
if(j.o == 1){
if(j.r < lst[j.id]){
bit.update(lst[j.id], -1);
lst[j.id] = j.r;
bit.update(lst[j.id], 1);
}
}
else ans[j.id] = bit.query(j.r);
}
for(auto j : opt) if(j.o) bit.update(lst[j.id], -1), lst[j.id] = N + 1;
opt.clear();
for(int j : ve[x]){
if(del[j]) continue;
n = siz[j];
rt = 0;
find_rt(j, 0);
dfs(rt, 0);
solve(rt);
}
return ;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m; N = n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= 100000; i++) lst[i] = N + 1;
for(int i = 1, u, v; i < n; i++){
cin >> u >> v; ve[u].push_back(v), ve[v].push_back(u);
}
for(int i = 1, l, r, x; i <= m; i++){
cin >> l >> r >> x;
q[x].push_back({l, r, i, 0});
}
dfs(1, 0);
find_rt(1, 0);
solve(rt);
for(int i = 1; i <= m; i++) cout << ans[i] << '\n';
return 0;
}
P11118 [ROI 2024] 无人机比赛 (Day 2)
好题。
令 \(d_i=s_i-s_{i-1}\),发现若 \(d_i\le d_{i-1}\),则某一轮的结果和上一轮一模一样,所以只需要保留 \(d_i\) 的严格递增序列,长度 \(m\le \sqrt{2\times n}\),令 \(c_i\) 表示每一段在原序列中有多少个门。
对于第 \(i\) 个无人机到达第 \(k\) 个门时的贡献等于 \((\sum_{j=1}^n[t_j\times d_m\ge t_i \times d_k])-m\),因为只要还没有到达终点的无人机会回到存档点。
总答案 \(ans=-n\times m + \sum_{i=1}^n\sum_{j=1}^n\sum_{k=1}^m[t_j\times d_m\ge t_i \times d_k]\)。
问题来了,如何处理 \([t_j\times d_m\ge t_i \times d_k]\)?
首先,移项得:\(\frac{t_j\times d_m}{d_k}\ge t_i\)。
记 \(t_i\) 的排名为 \(rk_i\),\(rk\) 的逆变换为 \(id_i\)。
发现 \(\frac{t_j\times d_m}{d_k}\) 只与 \(j\) 和 \(k\) 有关,令 \(R_{j,k}\) 表示有多少个 \(t_i\le \frac{t_j\times d_m}{d_k}\),这可以预处理。
\([t_j\times d_m\ge t_i \times d_k]\) 被转化成 \([R_{j,k}\ge rk_i]\)。
重新写一遍答案的式子 \(ans=-n\times m + \sum_{i=1}^n\sum_{j=1}^n\sum_{k=1}^m[R_{j,k}\ge rk_i]\)。
现在我们要做的就是维护答案的增量。
分两种情况:
-
第 \(x+1\) 个无人机使其他无人机返回存档点的次数 \(-m+\sum_{j=1}^{x+1}\sum_{k=1}^m [R_{j,k}\ge rk_{x+1}]\times c_k\)
发现有 \(\Theta(n)\) 次查询 \(\ge rk_{x+1}\),\(\Theta(n\sqrt n)\) 次插入 \(R_{x+1,k}\),根号平衡一下。
-
其他无人机使第 \(x+1\) 个无人机的返回存档点的次数 \(\sum_{i=1}^x\sum_{k=1}^m[R_{x+1,k}\ge rk_i]\times c_k\)。
类似地,发现有 \(\Theta(n)\) 次插入 \(rk_{x+1}\),\(\Theta(n\sqrt n)\) 次查询 \(\le R_{x+1,k}\),根号平衡一下。
总复杂度 \(\Theta(n\sqrt n)\)。
感觉这题难点在于把答案的形式写出来,并对它化简。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("52.in", "r", stdin);
}
using ll = long long;
#define pll pair<ll, ll>
#define mp(a, b) make_pair(a, b)
const int kN = 2e5 + 7;
int n, m, rk[kN], id[kN], r[kN][550];
ll ans, t[kN], s[kN], d[kN], c[kN];
int cnt, pos[kN], L[kN], R[kN];
struct DS1{
ll sum[403], val[kN];
void update(int x, ll v){
val[x] += v;
sum[pos[x]] += v;
}
ll query(int x){
ll ans = 0;
for(int i = x; i <= R[pos[x]]; i++) ans += val[i];
for(int i = pos[x] + 1; i <= cnt; i++) ans += sum[i];
return ans;
}
}ds1;
struct DS2{
ll sum[403], val[kN];
void update(int x, ll v){
for(int i = x; i <= R[pos[x]]; i++) val[i] += v;
for(int i = pos[x]; i <= cnt; i++) sum[i] += v;
return ;
}
ll query(int x){
return sum[pos[x] - 1] + val[x];
}
}ds2;
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
int B = sqrt(n);
cnt = n / B;
for(int i = 1; i <= cnt; i++){
L[i] = R[i - 1] + 1, R[i] = i * B;
}
if(R[cnt] < n){
L[++cnt] = R[cnt - 1] + 1;
R[cnt] = n;
}
for(int i = 1; i <= cnt; i++){
for(int j = L[i]; j <= R[i]; j++) pos[j] = i;
}
for(int i = 1; i <= n; i++) cin >> t[i];
for(int i = 1; i <= m; i++) cin >> s[i];
for(int i = m; i >= 1; i--) s[i] = s[i] - s[i - 1];
int M = 0;
for(int i = 1; i <= m; i++){
d[++M] = s[i];
c[M] = 1;
while(i + 1 <= m && s[i + 1] <= d[M]) ++i, c[M]++;
}
swap(m, M);
for(int i = 1; i <= n; i++) id[i] = i;
sort(id + 1, id + n + 1, [&](int x, int y){return (t[x] == t[y]) ? x < y : t[x] < t[y];});
for(int i = 1; i <= n; i++) rk[id[i]] = i;
for(int k = 1; k <= m; k++){
for(int j = 1, i = 0; j <= n; j++){
while(i + 1 <= n && mp(t[id[j]] * d[m], id[j]) >= mp(t[id[i + 1]] * d[k], id[i + 1])) ++i;
r[id[j]][k] = i;
}
}
for(int x = 0; x < n; x++){
for(int k = 1; k <= m; k++) ds1.update(r[x + 1][k], c[k]);
ans += ds1.query(rk[x + 1]);
for(int k = 1; k <= m; k++) ans += ds2.query(r[x + 1][k]) * c[k];
ds2.update(rk[x + 1], 1);
cout << ans - 1ll * (x + 1) * M << '\n';
}
return 0;
}
P11947 [KTSC 2025] 可爱区间 / maxsum
想要区间 \([l,r]\) 是可爱的,我们肯定会让 \(i\in [l,r]\) 填 \(B_i\),\(i\notin [l,r]\) 填 \(A_i\)。
下标先都平移一位。
考虑对 \([l,r]\) 有哪些限制?
记 \(s1_i\) 为 \(a\) 的前缀和,\(s2_i\) 为 \(b\) 的前缀和。
-
不存在与 \([l,r]\) 不相交的区间的 \(A_i\) 之和 \(\ge s2_r-s2_{l-1}\)。这个限制可以强化到 \(A\) 中的最大子段和 \(\le s2_r-s2_{l-1}\)。
-
区间 \([l,r]\) 内不存在一段 真后缀 的和 \(\le 0\),即 \(s2_r\) 是 \(i\in [l,r]\) 中 \(s2_i\) 中的最大值。
-
区间 \([l,r]\) 内不存在一段 真前缀 的和 \(\le 0\),即 \(s2_{l-1}\) 是 \(i\in [l-1,r-1]\) 中 \(s2_i\) 的最小值。
-
区间 \([1,l-1]\) 内所有 后缀 的最大的 \(A\) 之和(可以为空)为 \(0\),区间 \([r+1,n]\) 内所有 前缀 的最大的 \(A\) 之和(可以为空)为 \(0\)。
考虑扫描线,对 \(r\) 从小到大扫描。
对于限制二,我们可以预处理出每个 \(r\) 所对应的最大的 \(l-1\)。(为什么是 \(l-1\),我们发现上面的四个限制基本上都与 \(l-1\) 有关)。
对于限制三,我们想到维护一个单调不降的单调栈取存合法的 \(l-1\)。
对于限制一,可以发现 \(l - 1\) 是一段前缀,在单调栈上二分即可。
对于限制四,预处理出 \(pre_i\) 和 \(suf_i\) 分别表示 \([1,i]\) 和 \([i,n]\) 的最大后缀/前缀和即可,只有在 \(pre_{i}=0\) 的时候入栈,在 \(suf_{i+1}=0\) 的时候算答案。
对于答案差分一下,变成右端点在 \([1,r2]\) 内,左端点在 \([l1,r1]\) 内的答案,这可以用单点修改,区间历史和的线段树来维护。
总复杂度 \(\Theta(n\log n)\)。
本题难点在于想到单调栈。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 250007;
int qcnt, n, Q;
ll a[kN], b[kN], pre[kN], suf[kN], mxA, s1[kN], s2[kN];
int stk[kN], top, lim[kN];
struct SGT{
struct node{
ll s, s2, tag;
}tree[kN << 2];
void pushup(int x){
tree[x].s = tree[x << 1].s + tree[x << 1 | 1].s;
tree[x].s2 = tree[x << 1].s2 + tree[x << 1 | 1].s2;
return ;
}
void maketag(int x, ll v){
tree[x].s2 += tree[x].s * v;
tree[x].tag += v;
return ;
}
void pushdown(int x){
maketag(x << 1, tree[x].tag);
maketag(x << 1 | 1, tree[x].tag);
tree[x].tag = 0;
return ;
}
void update(int x, int l, int r, int pos, ll v){
if(l == r){
tree[x].s += v;
return ;
} pushdown(x);
int mid = l + r >> 1;
if(pos <= mid) update(x << 1, l, mid, pos, v);
else update(x << 1 | 1, mid + 1, r, pos, v);
pushup(x);
return ;
}
void update2(int x, int l, int r, int L, int R){
if(L <= l && r <= R){
maketag(x, 1ll);
return ;
} pushdown(x);
int mid = l + r >> 1;
if(L <= mid) update2(x << 1, l, mid, L, R);
if(mid < R) update2(x << 1 | 1, mid + 1, r, L, R);
pushup(x);
return ;
}
ll query(int x, int l, int r, int L, int R){
if(L <= l && r <= R) return tree[x].s2;
pushdown(x);
int mid = l + r >> 1; ll res = 0;
if(L <= mid) res += query(x << 1, l, mid, L, R);
if(mid < R) res += query(x << 1 | 1, mid + 1, r, L, R);
pushup(x);
return res;
}
}sgt;
ll ans[kN];
struct Query{
int l, r, id;
ll op;
};
vector<Query> opt[kN];
vector<ll> maxsum(vector<int> A, vector<int> B, vector<int> L1, vector<int> R1, vector<int> L2, vector<int> R2){
n = A.size(), Q = L1.size();
for(int i = 1; i <= n; i++) a[i] = A[i - 1], b[i] = B[i - 1];
for(int i = 1; i <= n; i++) s1[i] = s1[i - 1] + a[i], s2[i] = s2[i - 1] + b[i];
for(int i = 1; i <= Q; i++){
L1[i - 1]++, L2[i - 1]++, R1[i - 1]++, R2[i - 1]++;
opt[R2[i - 1]].push_back({L1[i - 1] - 1, R1[i - 1] - 1, i, 1ll});
opt[L2[i - 1] - 1].push_back({L1[i - 1] - 1, R1[i - 1] - 1, i, -1ll});
}
for(int i = 1; i <= n; i++) pre[i] = max(pre[i - 1] + a[i], 0ll);
for(int i = n; i >= 1; i--) suf[i] = max(suf[i + 1] + a[i], 0ll);
for(int i = 1; i <= n; i++){
while(top && s2[stk[top]] <= s2[i]) top--;
lim[i] = stk[top]; stk[++top] = i;
} top = 0;
mxA = -1e18;
for(int i = 1; i <= n; i++) mxA = max(mxA, pre[i - 1] + a[i]);
stk[++top] = 0;
sgt.update(1, 0, n, 0, 1ll);
for(int i = 1; i <= n; i++){
if(suf[i + 1] == 0){
int l = 1, r = top, mid, Ed = 0, Fi = top + 1;
while(l <= r){
mid = l + r >> 1;
if(s2[i] - s2[stk[mid]] >= mxA){
l = mid + 1;
Ed = mid;
}
else r = mid - 1;
}
if(lim[i] <= stk[Ed] && Ed) sgt.update2(1, 0, n, lim[i], stk[Ed]);
}
for(auto j : opt[i]){
ans[j.id] += j.op * sgt.query(1, 0, n, j.l, j.r);
}
while(top && s2[stk[top]] > s2[i]){
sgt.update(1, 0, n, stk[top], -1ll);
top--;
}
if(pre[i] == 0) sgt.update(1, 0, n, i, 1ll), stk[++top] = i;
}
vector<ll> tmp;
for(int i = 1; i <= Q; i++) tmp.push_back(ans[i]);
return tmp;
}
signed main(){
File();
vector<ll> A, B;
vector<int> L1, R1, L2, R2;
cin >> n >> Q;
for(int i = 1; i <= n; i++){
ll x, y; cin >> x >> y;
A.push_back(x), B.push_back(y);
}
for(int i = 1; i <= Q; i++){
int x, y, xx, yy;
cin >> x >> y >> xx >> yy;
L1.push_back(x), R1.push_back(y);
L2.push_back(xx), R2.push_back(yy);
}
vector<ll> tmp = maxsum(A, B, L1, R1, L2, R2);
for(auto i : tmp) cout << i << ' ';
cout << '\n';
return 0;
}
P11945 [KTSC 2025] 军事基地 / safezone
看到这道题,有个显然的想法是扫描线。
我们扫描 \(x\) 轴,维护 \(y\) 轴上的点所属的集合,对于一个矩形,在左端点时加入 \([l,r]\),和 \([l,r]\) 所代表的集合内的点连边,然后在矩形右端点的时候删除,这样做,复杂度显然爆炸。
考虑给纵轴上的每个点设一个代表元,表示当前这个点属于哪个矩形,那么每次向这个点所代表的矩形合并,合并可以用并查集维护。
再仔细思考,发现上面的操作本质上是给 \([l,r]\) 的点打上颜色,可以想到用 \(\text{ODT}\) 来维护。
那接下来我们有两个问题:
-
如何保证 \(\text{ODT}\) 的复杂度?
-
如何保证高效的删除?
先考虑第二个问题。
由于 \(\text{ODT}\) 无法支持删除,我们可以想到对于区间 \([l,r]\) 内的每个颜色段,如果当前颜色段内还有点,那么这个代表元肯定是存在的。
这个只需要用线段树维护区间加和区间查即可。
再考虑第一个问题。
每次访问完 \([l,r]\) 内的所有颜色段后,将 \([l,r]\) 改为一个整体的颜色段,这样是对的。
简单证明:每次询问一个区间的数量约等于删除区间的数量,每次操作至多加入 \(\Theta(1)\) 个区间,可看作询问颜色段的数量约等于操作的数量,所以复杂度为 \(\Theta(n\log ^2 n)\) ,不要忘记乘上线段树的复杂度。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("13.in", "r", stdin);
return ;
}
const int kN = 5e5 + 7;
vector<int> A, B, C, D, ans;
int n, cx, cy;
using ll = long long;
struct SGT{
struct node{
ll s, tag;
}tree[kN << 3];
void pushup(int x){
tree[x].s = tree[x << 1].s + tree[x << 1 | 1].s;
return ;
}
void maketag(int x, int l, int r, ll v){
tree[x].s += 1ll * (r - l + 1) * v;
tree[x].tag += 1ll * v;
return ;
}
void pushdown(int x, int l, int r){
int mid = l + r >> 1;
maketag(x << 1, l, mid, tree[x].tag);
maketag(x << 1 | 1, mid + 1, r, tree[x].tag);
tree[x].tag = 0;
return ;
}
void build(int x, int l, int r){
tree[x].s = tree[x].tag = 0;
if(l == r) return ;
int mid = l + r >> 1;
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
pushup(x);
return ;
}
void update(int x, int l, int r, int L, int R, ll v){
if(L <= l && r <= R){
maketag(x, l, r, v);
return ;
} pushdown(x, l, r);
int mid = l + r >> 1;
if(L <= mid) update(x << 1, l, mid, L, R, v);
if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
pushup(x);
return ;
}
ll query(int x, int l, int r, int L, int R){
if(L <= l && r <= R) return tree[x].s;
pushdown(x, l, r);
int mid = l + r >> 1;
ll ans = 0;
if(L <= mid) ans += query(x << 1, l, mid, L, R);
if(mid < R) ans += query(x << 1 | 1, mid + 1, r, L, R);
pushup(x);
return ans;
}
}sgt;
struct DSU{
int fa[kN], siz[kN];
void init(){
for(int i = 1; i <= n; i++) siz[i] = 1, fa[i] = i;
return ;
}
int find(int x){
if(fa[x] != x) fa[x] = find(fa[x]);
return fa[x];
}
void merge(int x, int y){
x = find(x), y = find(y);
if(x != y){
if(siz[x] > siz[y]) swap(x, y);
fa[x] = y, siz[y] += siz[x];
}
return ;
}
}dsu;
struct node{
int l, r, id;
bool friend operator < (node a, node b){
return a.l < b.l;
}
};
struct ODT{
set<node> s;
void insert(node x){
s.insert(x);
return ;
}
void split(int l, int r){
auto it = --s.upper_bound({l, l, -1});
if((*it).l != l){
node tmp = *it;
s.erase(it);
s.insert({tmp.l, l - 1, tmp.id});
s.insert({l, tmp.r, tmp.id});
}
it = --s.upper_bound({r, r, -1});
if((*it).r != r){
node tmp = *it;
s.erase(it);
s.insert({tmp.l, r, tmp.id});
s.insert({r + 1, tmp.r, tmp.id});
}
return ;
}
void assign(node x){
int l = x.l, r = x.r, v = x.id + 1;
split(l, r);
auto itr = s.upper_bound({r, r, -1});
auto itl = s.lower_bound({l, l, -1});
for(auto it = itl; it != itr; ++it){
if(sgt.query(1, 1, cy, (*it).l, (*it).r) > 0){
dsu.merge(v, (*it).id);
}
}
s.erase(itl, itr);
s.insert({l, r, v});
return ;
}
}odt;
int lshx[kN << 1], lshy[kN << 1], vis[kN];
vector<node> Add[kN << 1], Del[kN << 1];
vector<int> find_union(int _n, vector<int> A, vector<int> B, vector<int> C, vector<int> D){
cx = 0, cy = 0; n = _n;
for(int i = 0; i < n; i++) lshx[++cx] = A[i], lshx[++cx] = C[i];
for(int i = 0; i < n; i++) lshy[++cy] = B[i], lshy[++cy] = D[i];
sort(lshx + 1, lshx + cx + 1);
sort(lshy + 1, lshy + cy + 1);
cx = unique(lshx + 1, lshx + cx + 1) - (lshx + 1);
cy = unique(lshy + 1, lshy + cy + 1) - (lshy + 1);
for(int i = 0; i < n; i++){
A[i] = lower_bound(lshx + 1, lshx + cx + 1, A[i]) - lshx;
C[i] = lower_bound(lshx + 1, lshx + cx + 1, C[i]) - lshx;
B[i] = lower_bound(lshy + 1, lshy + cy + 1, B[i]) - lshy;
D[i] = lower_bound(lshy + 1, lshy + cy + 1, D[i]) - lshy;
Add[A[i]].push_back({B[i], D[i], i});
Del[C[i] + 1].push_back({B[i], D[i], i});
}
dsu.init();
odt.insert({1, cy, 0});
sgt.build(1, 1, cy);
for(int i = 1; i <= cx; i++){
for(auto j : Del[i]) sgt.update(1, 1, cy, j.l, j.r, -1);
for(auto j : Add[i]){
odt.assign(j);
sgt.update(1, 1, cy, j.l, j.r, 1);
}
}
int res = 0;
for(int i = 1; i <= n; i++){
int x = dsu.find(i);
if(!vis[x]){
vis[x] = ++res;
}
ans.push_back(vis[x] - 1);
}
return ans;
}
P6881 [JOI 2020 Final] 火灾 / Fire
这道题怎么说呢,感觉有很多没怎么见过的 \(trick\)。
首先有个基本的想法,以序列下标为 \(x\) 轴,以时间为 \(y\) 轴,我们就能把每个时刻的序列状态给表示出来。

对于每个相同的值,其图形都可以看作一个平行四边形,这是最标准的图形。
接下来我们需要确定这个平行四边形的四个顶点。
记 \(l_i\) 为 \(i\) 左边第一个 \(>a_i\) 的数,\(r_i\) 为 \(i\) 右边第一个 \(>a_i\) 的数,那么四个顶点分别为 \((i,0),(i,i-l_i-1),(r_i-1,r_i-i-1),(r_i-1,r_i-l_i-2)\)。
这可以用单调栈求出。
当然这里有 \(\text{corner case}\),就是左边或右边没有 \(>a_i\) 的元素,这种情况就是普通的等腰直角三角形,且有一条直角边为 \(x\) 轴。为了方便统一处理,我们可以将其补全成一个平行四边形,只是多余的部分我们不算贡献即可。
我们考虑平行四边形怎么算贡献。有一个经典的做法是拆成三个三角形,如图:

看成一个大三角形减去两个小三角形。
而对于每一个三角形,我们知道它是一个等要直角三角形,且有一条直角边在 \(x\) 轴上和它的上顶点。
对于询问,相当于是一条平行于 \(x\) 轴的线段,我们差分一下,变成平行于 \(x\) 轴的射线。
现在我们需要知道对于一条射线,它与三角形的交线长度。
假设射线的端点为 \((x_0,y_0)\),三角形的上顶点为 \((x,y)\),我们可以分以下两种情况讨论。
-
\((x_0,y_0)\) 在三角形内,如图:
![]()
我们通过几何关系能够得到交线长度为 \(y-(x-x_0)-y_0+1\),整理一下为 \((x_0-y_0)-(x-y)+1\)。
当 \(x_0\le x,y_0\le y,x_0-y_0\ge x-y\) 时满足 \((x_0,y_0)\) 在三角形内,发现 \(y_0\le y\) 这个条件时多余的,于是只剩下 \(x_0\le x,x_0-y_0\ge x-y\)。
-
\((x_0,y_0)\) 在三角形外,如图:

我们通过几何关系能够得到交线长度为 \(y-y_0+1\)。
于是我们可以对 \(x\) 轴从小到大扫描线,用两个数据结构来维护两种不同的贡献,动态维护两种贡献。发现一种需要维护单点加、前缀查,另一种需要维护单点加、后缀查,这都可以用树状数组维护,复杂度 \(\Theta(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
const int kN = 2e5 + 7;
int n, Q, stk[kN], l[kN], r[kN], s[kN], top, cntp, cntq;
ll ans[kN];
struct BIT1{
ll tree1[kN * 3], tree2[kN * 3];
void update(int x, int v){
x--;
ll v2 = 1ll * v * x;
for(int i = x + n + 1; i <= 3 * n; i += i & -i) tree1[i] += v2, tree2[i] += 1ll * v;
return ;
}
ll query(int x){
ll res = 0;
for(int i = x + n + 1; i; i -= i & -i) res += tree2[i] * 1ll * x, res -= tree1[i];
return res;
}
}ds1;
struct BIT2{
ll tree1[kN * 3], tree2[kN * 3];
void update(int x, int v){
++x;
ll v2 = 1ll * v * x;
for(int i = x + n + 1; i; i -= i & -i) tree1[i] += v2, tree2[i] += 1ll * v;
return ;
}
ll query(int x){
ll res = 0;
for(int i = x + n + 1; i <= 3 * n; i += i & -i) res -= tree2[i] * 1ll * x, res += tree1[i];
return res;
}
}ds2;
struct Add{
int x, y, a;
}p[kN * 3];
struct Query{
int x, y, id;
}q[kN << 1];
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> Q;
for(int i = 1; i <= n; i++) cin >> s[i];
for(int i = 1; i <= n; i++){
while(top && s[stk[top]] < s[i]) top--;
if(top) l[i] = stk[top];
else l[i] = i - n - 1;
stk[++top] = i;
} top = 0;
for(int i = n; i >= 1; i--){
while(top && s[stk[top]] <= s[i]) top--;
if(top) r[i] = stk[top];
else r[i] = n + 1;
stk[++top] = i;
} top = 0;
for(int i = 1; i <= n; i++){
p[++cntp] = {i - 1, i - l[i] - 2, -s[i]};
p[++cntp] = {r[i] - 1, r[i] - i - 2, -s[i]};
p[++cntp] = {r[i] - 1, r[i] - l[i] - 2, s[i]};
}
sort(p + 1, p + cntp + 1, [&](Add x, Add y){return x.x < y.x;});
for(int i = 1; i <= cntp; i++) ds1.update(p[i].x - p[i].y, p[i].a);
for(int i = 1; i <= Q; i++){
int t, l, r;
cin >> t >> l >> r;
q[++cntq] = {l - 1, t, -i};
q[++cntq] = {r, t, i};
}
sort(q + 1, q + cntq + 1, [&](Query x, Query y){return x.x < y.x;});
for(int i = 1, j = 0; i <= cntq; i++){
while(j < cntp && p[j + 1].x < q[i].x){
++j;
ds1.update(p[j].x - p[j].y, -p[j].a);
ds2.update(p[j].y, p[j].a);
}
int id = abs(q[i].id), v = (q[i].id > 0 ? 1 : -1);
ans[id] += 1ll * v * (ds1.query(q[i].x - q[i].y) + ds2.query(q[i].y));
}
for(int i = 1; i <= Q; i++) cout << ans[i] << '\n';
return 0;
}
P10656 [ROI 2017] 学习轨迹 (Day 2)
好题啊。
暴力的话枚举 \(l_1,r_1\),维护序列 \(b\) 的最大子段和,发现这难以优化。
这种题有一个性质:两个序列的带权中点必定有一个会被选。
考虑反证法:假如两个带权中点都没选,则每个序列选出来的权值小于该序列权值和的一半,还不如直接全选权值和最大的那个序列。
我们钦定选序列 \(a\) 的带权中点,如果选 \(b\) 序列的话,\(\text{swap}\) 一下再做一遍即可。
由于我们确定了会选序列 \(a\) 中的带权中点 \(A\),所以我们可以给 \(b\) 序列分段,每段单独考虑。
有一个不错的想法是扫描 \(r_2\)。发现每次新加入一个元素,在序列 \(a\) 中可以选择的区间可能会变小。假设新加入的元素的颜色在序列 \(a\) 中出现的位置为 \(x\)。
若 \(x\le A\),记 \(lt_i=x\),否则记 \(rt_i=x\)。对于 \(b\) 中的一段区间 \([l,r]\),\(a\) 中选择的区间为 \((\max_{i=l}^r lt_i,\min_{i=l}^r rt_i)\),可以用单调栈维护 \(lt_i\) 和 \(rt_i\) 的极值,用线段树维护左端点的答案即可。
时间复杂度 \(\Theta(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("1.in", "r", stdin);
}
using ll = long long;
#define pli pair<ll, int>
#define mp(a, b) make_pair(a, b)
#define fi first
#define se second
const int kN = 5e5 + 7;
int n, m, c[kN], d[kN], delta, pos[kN << 1];
const ll INF = 1e18;
ll a[kN], b[kN], ans[5];
int stkl[kN], stkr[kN], tpl, tpr, lt[kN], rt[kN];
struct SGT{
struct node{
pli s;
ll tg;
}tree[kN << 2];
void pushup(int x){
tree[x].s = max(tree[x << 1].s, tree[x << 1 | 1].s);
return ;
}
void maketag(int x, ll v){
tree[x].s.fi += v;
tree[x].tg += v;
return ;
}
void pushdown(int x){
maketag(x << 1, tree[x].tg);
maketag(x << 1 | 1, tree[x].tg);
tree[x].tg = 0;
return ;
}
void build(int x, int l, int r){
tree[x].tg = 0;
if(l == r){
tree[x].s = mp(-INF, l);
return ;
}
int mid = l + r >> 1;
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
pushup(x);
return ;
}
void update(int x, int l, int r, int L, int R, ll v){
if(L <= l && r <= R){
maketag(x, v);
return ;
} pushdown(x);
int mid = l + r >> 1;
if(L <= mid) update(x << 1, l, mid, L, R, v);
if(mid < R) update(x << 1 | 1, mid + 1, r, L, R, v);
pushup(x);
return ;
}
}sgt;
void solve(int *c, int *d, ll *a, ll *b, int n, int m, int op){
int z = 0;
for(int i = 1; i <= n; i++){
if(a[i] * 2ll > a[n]){
z = i;
break;
}
}
for(int i = 1; i <= m; i++){
if(d[i] == c[z]){
if(i > 1) solve(c, d, a, b, n, i - 1, op);
if(i < m) delta += i, solve(c, d + i, a, b + i, n, m - i, op), delta -= i;
return ;
}
}
memset(pos, 0, sizeof(pos));
for(int i = 1; i <= n; i++) pos[c[i]] = i;
sgt.build(1, 1, m);
tpl = tpr = 0;
for(int i = 1; i <= m; i++){
sgt.update(1, 1, m, i, i, INF);
sgt.update(1, 1, m, 1, i, b[i] - b[i - 1]);
int x = pos[d[i]];
lt[i] = 0, rt[i] = n;
if(x && x < z) lt[i] = x;
if(x && x > z) rt[i] = x - 1;
while(tpl && lt[stkl[tpl]] <= lt[i]){
sgt.update(1, 1, m, stkl[tpl - 1] + 1, stkl[tpl], a[lt[stkl[tpl]]]);
tpl--;
}
while(tpr && rt[stkr[tpr]] >= rt[i]){
sgt.update(1, 1, m, stkr[tpr - 1] + 1, stkr[tpr], -a[rt[stkr[tpr]]]);
tpr--;
}
sgt.update(1, 1, m, stkl[tpl] + 1, i, -a[lt[i]]);
sgt.update(1, 1, m, stkr[tpr] + 1, i, a[rt[i]]);
stkl[++tpl] = i, stkr[++tpr] = i;
auto [u, v] = sgt.tree[1].s;
if(u > ans[0]){
ans[0] = u;
int l1, r1, l2, r2;
l2 = v, r2 = i;
l1 = lt[*lower_bound(stkl + 1, stkl + tpl + 1, v)] + 1;
r1 = rt[*lower_bound(stkr + 1, stkr + tpr + 1, v)];
l2 += delta, r2 += delta;
if(op) swap(l1, l2), swap(r1, r2);
ans[1] = l1, ans[2] = r1;
ans[3] = l2, ans[4] = r2;
if(op) swap(l1, l2), swap(r1, r2);
}
}
return ;
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> c[i];
for(int i = 1; i <= n; i++) cin >> a[i], a[i] += a[i - 1];
for(int i = 1; i <= m; i++) cin >> d[i];
for(int i = 1; i <= m; i++) cin >> b[i], b[i] += b[i - 1];
if(a[n] >= b[m]){;
ans[0] = a[n], ans[1] = 1, ans[2] = n, ans[3] = ans[4] = 0;
}
else{
ans[0] = b[m], ans[1] = ans[2] = 0, ans[3] = 1, ans[4] = m;
}
solve(c, d, a, b, n, m, 0);
solve(d, c, b, a, m, n, 1);
cout << ans[0] << '\n';
cout << ans[1] << ' ' << ans[2] << '\n';
cout << ans[3] << ' ' << ans[4] << '\n';
return 0;
}
P13842 篱莘龙
考虑特殊性质,发现形如 \([b_i,a_i]\) 的线段至多选取一个,形如\([a_i,b_i]\) 的线段两两必须相交。
考虑既有 \([b_i,a_i]\) 的线段,又有 \([a_i,b_i]\) 的线段怎么处理。
首先,我们可以发现如果先选择了一条 \([b_i,a_i]\) 的线段,再选其他的 \([a_j,b_j]\) 的线段,其他的线段都要满足 \([b_i,a_i]\subseteq [a_j,b_j]\)。
最关键的一步来了,如果对于线段 \([b_i,a_i]\),存在其他的线段 \([a_j,b_j]\) 不满足 \([b_i,a_i]\subseteq [a_j,b_j]\),我们称这个线段 \([b_i,a_i]\) 是无用的。
因为后者的限制严格弱于前者。
接下来考虑如何实现。把所有的 \([a_i,b_i]\) 线段的两个端点插入 set 中。
假设当前我们加入的是 \([a_i,b_i]\) 的线段,相当于我们把原来的区间 \([l,r]\) 拆成了 \([l,mid]\) 和 \([mid,r]\)。我们把 \([a_i,b_i]\) 处的贡献加 \(1\),同时需要把跨 \(mid\) 的区间 \([b_i,a_i]\) 的贡献删除,由于至多加入 \(O(n)\) 条线段,所以我们删除也只会删 \(O(n)\) 条线段。考虑在每个 \([b_i,a_i]\) 线段的右端点 \(a_i\) 维护 \(b_i\) 的值,然后我们每次暴力查询右端点在 \([mid,r]\) 中的最小的 \(b_i\),如果 \(b_i\le mid\),我们就删除这个线段的贡献。
假设当前新加的是一个 \([b_i,a_i]\) 线段,我们可以简单的判断 set 中是否存在端点在 \([b_i,a_i]\) 之间,如果有的话,就不插入。否则插入并在 \(a_i\) 处加 \(1\),并在 \(a_i\) 处更新 \(b_i\)。
我们用线段树实现上面即可。
发现交上去会被卡常,我们把 set 换成 BIT 即可,具体操作为 BIT 维护 0-1 序列,然后树状数组倍增查询最靠前的前缀和为定值 \(S\) 的位置。
复杂度 \(\Theta(n \log n)\)。
#include <bits/stdc++.h>
using namespace std;
void File(){
freopen("nailong.in", "r", stdin);
freopen("nailong.out", "w", stdout);
}
#define pii pair<int, int>
#define mp(a, b) make_pair(a, b)
const int kN = 1e6 + 7;
int n, a[kN], b[kN];
struct BIT{
int tree[kN << 1];
void update(int x, int v){
for(int i = x; i <= 2 * n; i += i & -i) tree[i] += v;
return ;
}
int query(int x){
int res = 0;
for(int i = x; i; i -= i & -i) res += tree[i];
return res;
}
int find(int s){
int x = 0;
for(int j = 20; j >= 0; j--){
if(x + (1 << j) <= 2 * n && tree[x + (1 << j)] < s){
s -= tree[x + (1 << j)];
x += (1 << j);
}
}
return x + 1;
}
}bit;
struct SGT{
struct node{
int tag, mx;
pii mn;
}tree[kN << 3];
void pushup(int x){
tree[x].mn = min(tree[x << 1].mn, tree[x << 1 | 1].mn);
tree[x].mx = max(tree[x << 1].mx, tree[x << 1 | 1].mx);
return ;
}
void build(int x, int l, int r){
tree[x].mn = mp(2 * n + 1, 0);
if(l == r) return ;
int mid = l + r >> 1;
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
return ;
}
void maketag(int x, int l, int r, int v){
tree[x].tag += v;
tree[x].mx += v;
return ;
}
void pushdown(int x, int l, int r){
if(tree[x].tag){
int mid = l + r >> 1;
maketag(x << 1, l, mid, tree[x].tag);
maketag(x << 1 | 1, mid + 1, r, tree[x].tag);
tree[x].tag = 0;
}
return ;
}
void add(int x, int l, int r, int L, int R, int v){
if(L <= l && r <= R){
maketag(x, l, r, v);
return ;
}
pushdown(x, l, r);
int mid = l + r >> 1;
if(L <= mid) add(x << 1, l, mid, L, R, v);
if(mid < R) add(x << 1 | 1, mid + 1,r , L, R, v);
pushup(x);
return ;
}
void upd(int x, int l, int r, int pos, pii v){
if(l == r){
tree[x].mn = v;
return ;
}
pushdown(x, l, r);
int mid = l + r >> 1;
if(pos <= mid) upd(x << 1, l, mid, pos, v);
else upd(x << 1 | 1, mid + 1, r, pos, v);
pushup(x);
return ;
}
pii qry(int x, int l, int r, int L, int R){
if(L <= l && r <= R) return tree[x].mn;
pushdown(x, l, r);
int mid = l + r >> 1;
pii ans = mp(2 * n + 1, 0);
if(L <= mid) ans = min(ans, qry(x << 1, l, mid, L, R));
if(mid < R) ans = min(ans, qry(x << 1 | 1, mid + 1, r, L, R));
pushup(x);
return ans;
}
}sgt;
void Ins(int x){
int r = bit.find(bit.query(x) + 1) - 1;
r = min(r, 2 * n);
bit.update(x, 1);
if(x > r) return ;
while(1){
pii v = sgt.qry(1, 1, 2 * n, x, r);
if(v.first > x) break;
sgt.add(1, 1, 2 * n, a[v.second], a[v.second], -1);
sgt.upd(1, 1, 2 * n, a[v.second], mp(2 * n + 1, 0));
}
return ;
}
signed main(){
// File();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i] >> b[i];
sgt.build(1, 1, 2 * n);
for(int k = 1; k <= n; k++){
if(a[k] < b[k]){
Ins(a[k]);
Ins(b[k]);
sgt.add(1, 1, 2 * n, a[k], b[k], 1);
}
else{
int it = bit.find(bit.query(b[k]) + 1);
if(it > a[k]){
sgt.add(1, 1, 2 * n, a[k], a[k], 1);
sgt.upd(1, 1, 2 * n, a[k], mp(b[k], k));
}
}
cout << sgt.tree[1].mx << '\n';
}
return 0;
}


浙公网安备 33010602011771号