2025.7.18 数据结构
拜谢高一零基础 Ag 学长 fsfdgdg!\bx\bx
P11993 [JOIST 2025] 迁移计划 / Migration Plan
Hint:对每个深度开一棵线段树,操作一就是对应两棵线段树的合并.
想到线段树合并基本上就做完了. 由于一定是深度大的合并到深度小的,我们只是合并了信息而并没有改变树形结构,所以单点查询时实际上查询的是对应深度线段树上的子树和. 因为只有子树的贡献可能合并到根节点.
于是先跑一遍 dfs 求出 的 dfn 序,把子树查询转化为区间查询,然后就做完了.
P9020 [USACO23JAN] Mana Collection P
Hint:贪心地考虑发现最终贡献只与最晚到达某个法力池的时间有关,拆贡献,考虑优化方法.
比较厉害的一个题.
李超线段树前置
线段树上维护若干一次函数在线段树每个区间中点处的最值(有点像凸壳),支持直线(线段)插入. 直线插入时间复杂度 \(O(\log n)\),是在线段树上搜索的复杂度. 线段插入时间复杂度 \(O(\log^2 n)\),因为要拆分成 \(O(\log n)\) 个区间分别递归.
考虑加入一条直线 \(f(x)\),设线段树上每个区间维护的线段并起来为 \(g(x)\). 首先考察整个区间中点 \(mid\) 哪个函数更优,分类讨论:
- 若 \(f(x)\) 更优,将其与这一段的 \(g(x)\) 交换.
此后进行一次判断,若区间 \(l=r\) 不再能二分就结束递归,否则继续判断:
- 若左端点 \(f(x)\) 更优,说明左区间存在可能更新的 \(g(x)\),所以递归到左儿子继续更新.
- 若右端点 \(f(x)\) 更优,说明右区间存在可能更新的 \(g(x)\),所以递归到右儿子继续更新.
上述算法用于维护一条直线的插入. 如果插入一条线段,则需要像线段树区间修改操作那样分出每个区间分别进行上述操作.
核心代码:
struct lct{
int tr[maxn];
inline ld f(int x, int id) {return (ld)l[id].k * x + l[id].b;}
inline bool eq(ld x, ld y) {return x - y <= eps && y - x <= eps;}
inline bool cmp(int u, int v, int x) {//u 是否比 v 优
ld a = f(x, u), b = f(x, v);
return a > b || (eq(a, b) && u < v);
}
inline void upd(int u, int l, int r, int p) {
int mid = l + r >> 1;
if(cmp(p, tr[u], mid)) swap(tr[u], p);//递归修改
if(l == r) return;
if(cmp(p, tr[u], l)) upd(ls, l, mid, p);
if(cmp(p, tr[u], r)) upd(rs, mid + 1, r, p);
return;
}
inline void add(int u, int l, int r, int ql, int qr, int p) {
if(ql <= l && r <= qr) return upd(u, l, r, p), void(0);
int mid = l + r >> 1;
if(ql <= mid) add(ls, l, mid, ql, qr, p);
if(mid < qr) add(rs, mid + 1, r, ql, qr, p);
return;
}
inline int ask(int u, int l, int r, int p) {
if(l == r) return tr[u];
int mid = l + r >> 1, res = tr[u], tmp;
if(p <= mid) tmp = ask(ls, l, mid, p);
else tmp = ask(rs, mid + 1, r, p);
return (cmp(tmp, res, p) ? tmp : res);
}
} t;
设最晚到达各个法力池的时间 \(x_1<x_2<\cdots<x_k\),可以列出总法力的式子:
其实这样并不好做,因为每个法力池都可以选择停留与否. 贪心地考虑,按时间轴倒着跑不停留一定是最优的,因为停的时间延后会损失开始一些法力池的贡献. 所以我们用每两个法力池之间花费的时间 \(t_i\)(\(i\) 走到 \(i+1\) 的时间)来表示上述式子.
发现这个式子可以看作一条直线,可以状压 DP 求出斜率和截距.
设 \(sum_s\) 表示当前选的法力池一秒产生的法力,\(f_{s,i}\) 表示当前所在法力池以及选择法力池的状态所对应的最大法力. 对于 \(\sum\limits_{j=i}^{k-1}t_j\),由于我们不确定路径的具体形态,所以考虑 Floyd 先把所有最短路求出来. 枚举当前选了哪些法力池的状态 \(s\),可以列出转移:
于是可以把每条直线插入李超线段树,询问时查询 \(s_i\) 处的取值即可. 总时间复杂度 \(O(n^3+n^22^n+q\log\max\{s_i\})\).
实现细节比较多,调的时候要耐心.
P12829 「DLESS-2」XOR and Inversion
Hint:考虑逆序对和正序对在什么条件下会相互转化,合并询问,用分治实现.
考虑逆序对的本质其实是值和位置两维上的偏序关系. 对于两个数 \(p,q\),若只对值进行异或 \(x\) 操作,那么逆序对/正序对发生相互转化当且仅当 \(p,q\) 的值在二进制下从高位到低位第一个不相同的位,\(x\) 在这一位为 \(1\);若只对位置进行异或 \(x'\) 操作,那么逆序对/正序对发生相互转化当且仅当 \(p,q\) 的位置在二进制下从高位到低位第一个不相同的位,\(x'\) 在这一位为 \(1\). 那么显然所有操作之间都可以合并.
二维偏序可以考虑对位置维分治,统计值域答案. 再考虑如何刻画二进制位下第一个不同的位置,不妨枚举第一个不同的二进制位,统计前缀个数扔到桶里面. 记 \(cnt_{i,j,0/1}\) 表示位置维第一个不同位置是 \(i\),值域维第一个不同位置是 \(j\),正序对/逆序对的个数. 值 \(p_i\) 在位置第 \(d\) 层对地 \(j\) 位的贡献就可以计算了.
还有以下细节:
- 分治统计前半段对后半段的贡献,后半段当前位置是 \(1\) 的更大,统计到顺序对,反之统计到逆序对.
- 桶要开成动态的,我使用的是 vector 中的 resize 来重设.
- 可能需要一定的卡常.
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define R register
#define getcha() (S==T&&(T=(S=fsr)+fread(fsr,1,1<<15,stdin),S==T)?EOF:*S++)
char fsr[1<<21],*S=fsr,*T=fsr;
inline int read(){
int r(0),w(1);char ch;
while(ch=getcha(),ch>=58 || ch<=47)w=(ch=='-'?-1:1);r=(r<<3)+(r<<1)+ch-48;
while(ch=getcha(),ch<=57 && ch>=48)r=(r<<3)+(r<<1)+ch-48;
return r*w;
}
inline void write(ll x)
{
if(x<0)
putchar('-'),x=-x;
if(x>9)
write(x/10);
putchar(x%10+'0');
return;
}
const int maxn = 2e1, max2n = (1 << 20);
int TT, n, q, p[max2n];
ll cnt[maxn][maxn][2];
vector< vector<ll> > t;
inline void calc(int d, int l, int r) {
if(l == r) return;
int mid = l + r >> 1; calc(d - 1, l, mid), calc(d - 1, mid + 1, r);
for(R int i = l; i <= mid; i++) for(R int j = 0; j < n; j++) t[j][p[i] >> j]++;
for(R int i = mid + 1; i <= r; i++) for(R int j = 0; j < n; j++) cnt[d][j][((p[i] >> j) & 1) ^ 1] += t[j][(p[i] >> j) ^ 1];
for(R int i = l; i <= mid; i++) for(R int j = 0; j < n; j++) t[j][p[i] >> j] = 0;
return;
}
inline void solve() {
vector< vector<ll> >().swap(t);//释放空间
n = read(), q = read(); for(R int i = 0; i < n; i++) for(R int j = 0; j < n; j++) cnt[i][j][0] = cnt[i][j][1] = 0;
for(R int i = 0; i < (1 << n); i++) p[i] = read();
t.resize(n); for(R int i = 0; i < n; i++) t[i].resize(1 << (n - i));
calc(n - 1, 0, (1 << n) - 1); int a = 0, b = 0;
while(q--){
ll res = 0; int op, x; op = read(), x = read();
if(op == 1) b ^= x; else a ^= x;
for(R int i = 0; i < n; i++) for(R int j = 0; j < n; j++) res += cnt[i][j][((a >> i) & 1) ^ ((b >> j) & 1) ^ 1];
write(res), puts("");
}
return;
}
int main() {
// ios :: sync_with_stdio(false); cout.tie(0);
TT = read();
while(TT--) solve();
return 0;
}
忠告:不建议使用 vector 开桶,常数过大. 我在 c++20 + 超快读快写的条件下才极限卡过.


浙公网安备 33010602011771号