2023.7.6 数据结构专题二

T1 [ZJOI2006] 书架

luogu link

说一个比较邪门但是能过的做法
首先一切的一切需要把平衡树板子那些操作全打出来 都要用
然后我们把每个数的位置作为它的权值插入平衡树
并且用 \(maxn\)\(minn\) 记录当前所有位置的最大/小值
再开两个数组双向维护这个权值对应的编号以及这个编号目前的权值
对于 Top 操作 我们先把编号对应的权值删掉
然后把 \(minn - 1\) 并作为这个编号新的权值
Bottom 同理
然后 Insert 操作直接查询编号对应权值的前驱/后继 并把它们的信息交换
Ask 操作直接查编号对应权值的排名
Query 操作查排名然后映射回编号

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 3e5 + 0721;
int loc[N], id[N];
int minn, maxn;
int n, m;

struct tree {
	struct node {
		int l, r; 
		int val, siz, dat;
	} T[N];
	int tot = 0, root = 0;
	
	inline void maintain(int k) {
		T[k].siz = T[T[k].l].siz + T[T[k].r].siz + 1;
	}
	
	int newnode(int val) {
		T[++tot].val = val;
		T[tot].siz = 1;
		T[tot].dat = rand();
		return tot;
	}
	
	void split(int k, int key, int &x, int &y) { 
		if (k == 0) {
			x = y = 0;
			return;
		}
		if (T[k].val <= key) { 
			x = k;
			split(T[k].r, key, T[x].r, y); 
		} else { 
			y = k;
			split(T[k].l, key, x, T[y].l);
		}
		maintain(k);
	}
	
	int merge(int x, int y) { 
		if (x == 0 || y == 0) return x + y;
		if (T[x].dat > T[y].dat) {
			T[x].r = merge(T[x].r, y); 
			maintain(x);
			return x;
		}
		else {
			T[y].l = merge(x, T[y].l);
			maintain(y);
			return y;
		}
	}
	
	void insert(int key) { 
		int x, y;
		split(root, key - 1, x, y);
		int id = newnode(key);
		root = merge(merge(x, id), y);
	}
	
	void remove(int key) {
		int x, y, z;
		split(root, key - 1, x, y);
		split(y, key, y, z); 
		if (y) y = merge(T[y].l, T[y].r); 
		root = merge(merge(x, y), z);
	}
	
	int rank(int key) { 
		int x, y, ans;
		split(root, key - 1, x, y); 
		ans = T[x].siz + 1;
		root = merge(x, y); 
		return ans; 
	}
	
	int kth(int rnk) {
		int p = root;
		while (p) {
			if (T[T[p].l].siz + 1 == rnk) break;
			else if (T[T[p].l].siz + 1 > rnk) p = T[p].l;
			else {
				rnk -= T[T[p].l].siz + 1;
				p = T[p].r; 
			} 
		}
		return T[p].val;
	}
	
	int get_pre(int key) { 
		int x, y, p;
		split(root, key - 1, x, y); 
		p = x;
		while (T[p].r != 0) p = T[p].r; 
		root = merge(x, y);
		return T[p].val; 
	}
	
	int get_nxt(int key) {
		int x, y, p;
		split(root, key, x, y);
		p = y;
		while (T[p].l != 0) p = T[p].l;
		root = merge(x, y);
		return T[p].val; 
	}
} treap;

void Top(int x) {
	int pos = loc[x];
	treap.remove(pos);
	loc[x] = --minn;
	id[minn + 100000] = x;
	treap.insert(minn);
}

void Bottom(int x) {
	int pos = loc[x];
	treap.remove(pos);
	loc[x] = ++maxn;
	id[maxn + 100000] = x;
	treap.insert(maxn);
}

void Insert1(int x) {
	int pos = loc[x];
	int pre = treap.get_pre(pos);
	int xx = id[pre + 100000];
	swap(loc[x], loc[xx]);
	swap(id[pre + 100000], id[pos + 100000]);
}

void Insert2(int x) {
	int pos = loc[x];
	int nxt = treap.get_nxt(pos);
	int xx = id[nxt + 100000];
	swap(loc[x], loc[xx]);
	swap(id[nxt + 100000], id[pos + 100000]);
}

int Ask(int x) {
	int pos = loc[x];
	int ans = treap.rank(pos);
	return ans - 1;
}

int Query(int x) {
	int pos = treap.kth(x);
	return id[pos + 100000];
}

int main() {
	srand(time(0));
	
	scanf("%d%d", &n, &m);
	minn = 1, maxn = n;
	for (int i = 1; i <= n; ++i) {
		int x;
		scanf("%d", &x);
		id[i + 100000] = x;
		loc[x] = i;
		treap.insert(i);
	}
	
	while (m--) {
		string opt;
		int s, num;
		cin >> opt;
		if (opt[0] == 'T') {
			scanf("%d", &s);
			Top(s);
		} 
		else if (opt[0] == 'B') {
			scanf("%d", &s);
			Bottom(s);
		} 
		else if (opt[0] == 'I') {
			scanf("%d%d", &s, &num);
			if (num == -1) Insert1(s);
			else if (num == 1) Insert2(s);
		} 
		else if (opt[0] == 'A') {
			scanf("%d", &s);
			printf("%d\n", Ask(s));
		} 
		else {
			scanf("%d", &s);
			printf("%d\n", Query(s));
		}
	}

	return 0;
}

T2 千山鸟飞绝

luogu link

首先对于每个点 有鸟飞来 有鸟飞走 还要查某些鸟的信息 对每个点都开一棵平衡树
然后考虑每棵平衡树要维护目前大小 目前最大值 历史最大大小 历史最大值
又因为鸟的最大值不能是自己 所以还需要维护次大值和历史次大值

并且这题需要和懒标记结合 因为当这个鸟飞来的时候 我维护的历史那一车东西对它都不管用
所以我们把维护的历史那一车东西都送进懒标记 然后把历史那一车东西清零
然后再拿这个鸟的信息来更新状态
飞走的时候只需要更新当前状态 历史状态没影响
懒标记只有当最后查鸟的信息的时候再下传


T3 T-Shirts

luogu link

首先很显然的要把所有T恤按质量从大到小排序
考虑每个人一个一个算肯定会T
那我们考虑每一件T恤对人的影响
假如它的价格是 \(k\) 显然手里钱大于等于 \(k\) 的都会被收割一波
但是肯定不能暴力统计大于等于 \(k\) 的人数的数量
我们考虑用平衡树维护
对于 \(\left[0, k\right)\) 这些人 因为买不起所以没有影响
对于 \(\ge 2k\) 的这些人也没有影响 因为排序还是没变 打个懒标记即可
问题是剩下的人
我们选择把他们一个一个揪出来删掉然后减去 \(k\) 再插回去
因为每次暴力操作手里的钱是减半的 所以暴力操作的次数是 \(log\) 级别的

点击查看代码
/*
考虑平衡树上每一个节点除了基础的那些还要维护什么
显然要维护对应的id和购买次数
所以懒标记要维护减掉的钱数和购买次数
这样每次合并和分裂的时候都要pushdown
对于每个操作 分别按k-1和2k-1裂x,y,z
然后把z打上懒标记 把x与z合并
那么对于y 直接遍历(注意pushdown)然后插入回去
*/
#include <bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;

inline int read() {
	int xr = 0, F = 1;
	char cr;
	while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
	while (cr >= '0' && cr <= '9')
		xr = (xr << 1) + (xr << 3) + (cr ^ 48), cr = getchar();
	return xr * F;
}

void write(int x) {
	int wtop = 0;
	char ws[51];
	if (x < 0) x = -x, putchar('-');
	do {
		ws[++wtop] = x % 10 + '0';
		x /= 10;
	} while (x);
	for (int i = wtop; i; --i) putchar(ws[i]);
}

namespace steven24 {
	
const int N = 2e5 + 0721;	
int n, m;
int ans[N];

struct clothes {
	int q, c;
	friend bool operator<(clothes x, clothes y) {
		if (x.q != y.q) return x.q > y.q;
		else return x.c < y.c;
	}
} a[N];
	
struct Binary_tree {

int ls[N], rs[N];
int val[N], id[N], c[N], data[N];
int minu[N], lc[N];
int rt, tot;

int newnode(int vval, int iid) {
	val[++tot] = vval;
	id[tot] = iid;
	data[tot] = rand();
	c[tot] = 0;
	return tot;
}

void modify(int k, int v, bool opt) { //1表示对价值减少 0表示对答案增加
	if (opt) {
		val[k] -= v;
		minu[k] += v;
	} else {
		c[k] += v;
		lc[k] += v;
	}
}

void pushdown(int k) {
	if (minu[k]) {
		if (ls[k]) modify(ls[k], minu[k], 1);
		if (rs[k]) modify(rs[k], minu[k], 1);
		minu[k] = 0;
	}
	if (lc[k]) {
		if (ls[k]) modify(ls[k], lc[k], 0);
		if (rs[k]) modify(rs[k], lc[k], 0);
		lc[k] = 0;
	}
}

void split(int k, int key, int &x, int &y) {
//	cout << k << " " << ls[k] << " " << rs[k] << "\n";
	if (!k) return x = y = 0, void();
	pushdown(k);
	if (val[k] <= key) {
		x = k;
		split(rs[k], key, rs[x], y);
	} else {
		y = k;
		split(ls[k], key, x, ls[y]);
	}
}

int merge(int x, int y) {
	if (!x || !y) return x + y;
	pushdown(x), pushdown(y);
	if (data[x] >= data[y]) {
		rs[x] = merge(rs[x], y);
		return x;
	} else {
		ls[y] = merge(x, ls[y]);
		return y;
	}
}

void insert(int vval, int iid) {
	int x, y, z;
	split(rt, vval - 1, x, z);
	y = newnode(vval, iid);
	rt = merge(merge(x, y), z);
}

void insert_again(int k, int key) {
	int x, y;
	split(rt, key, x, y);
	rt = merge(merge(x, k), y);
}

void dfs(int x, int k) {
	val[x] -= k;
	++c[x];
	pushdown(x);  //记得要下传
	if (ls[x]) dfs(ls[x], k);
	if (rs[x]) dfs(rs[x], k);
	rs[x] = ls[x] = 0; //重新插入要还原左右儿子
	insert_again(x, val[x]);
}

void work(int k) {
//	cout << "work";
	int x, y, z;
	split(rt, k - 1, x, y);
	split(y, (k << 1) - 1, y, z);
	modify(z, 1, 0);
	modify(z, k, 1);
	rt = merge(x, z);
	if (y) dfs(y, k);
}
	
} fhq;

void dfs(int x) {
	ans[fhq.id[x]] = fhq.c[x];
	fhq.pushdown(x);
	if (fhq.ls[x]) dfs(fhq.ls[x]);
	if (fhq.rs[x]) dfs(fhq.rs[x]);
}

void main() {
	n = read();
	for (int i = 1; i <= n; ++i) a[i].c = read(), a[i].q = read();
	sort(a + 1, a + 1 + n);
	m = read();
	for (int i = 1; i <= m; ++i) {
		int key = read();
		fhq.insert(key, i);
	}
	
	
	for (int i = 1; i <= n; ++i) {
		fhq.work(a[i].c);
	}
	
	dfs(fhq.rt);
	
	for (int i = 1; i <= m; ++i) write(ans[i]), putchar(' ');
	putchar('\n');
}
	
}

signed main() {
	steven24::main();
	return 0;
}
/*
3
7 5
3 5
4 3
2
13 14 
*/


T4 [TJOI2011] 树的序

luogu link

首先发现如果把数列的位置作为堆的权值 那么按题意插入之后一定是一个笛卡尔树
又因为我们要输出字典序最小的 显然这个笛卡尔树我们要先从左子树输出
所以输出它的前序遍历即可

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 0721;
int a[N], stk[N], ls[N], rs[N];
int n, top;

void print(int x) {
	if (x) {
		printf("%d ", x);
		print(ls[x]);
		print(rs[x]);
	}
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) {
		int x;
		scanf("%d", &x);
		a[x] = i;
	}
	
	for (int i = 1; i <= n; ++i) {
		int k = top;
		while (k > 0 && a[stk[k]] > a[i]) --k;
		if (k) rs[stk[k]] = i;
		if (k < top) ls[i] = stk[k + 1];
		stk[++k] = i;
		top = k;
	}
	
	print(stk[1]);

	return 0;
}

T5 Subsequence

luogu link

首先考虑将这个柿子化简

\(\sum\limits_{i = 1}^m (m \cdot a_{b_i}) - 2 * \sum\limits_{i = 1}^m \sum\limits_{j = i + 1}^m f(b_i, b_j) - \sum\limits_{i = 1}^ma_{b_i}\)

等于 \(\sum\limits_{i = 1}^m ((m - 1) \cdot a_{b_i}) - 2 * \sum\limits_{i = 1}^m \sum\limits_{j = i + 1}^m f(b_i, b_j)\)

我们发现每个 \(b_i\) 都出现了 \(m - 1\) 次(不信就拿 12 23 13 自己推一下)
所以就可以继续化为 \(\sum\limits_{i = 1}^m \sum\limits_{j = i + 1}^m a_{b_i} + a_{b_j} - 2 * f(b_i, b_j)\)
这个柿子非常的像 \(LCA\) 那玩意
并且 \(f(b_i, b_j)\) 相当于是一个区间内的最小值 发现可以建笛卡尔树 因为它满足连续的区间是一棵完整的子树 并且根节点键值最小

考虑 DP 设 \(f_{i, j}\) 表示在第 \(i\) 个节点的子树内选了 \(j\) 个节点的最大价值
转移时对于一边 \(i\) 的左子树内一边在 \(i\) 的右子树内的点对 他们的最小值就是 \(val_i\)
考虑第 \(i\) 个点选 / 不选
不选 有 \(f_{i, j + k} = f_{ls, j} + f_{rs, k} - 2 \times val_i \times j \times k\)

若选 则集合内剩余 \(m - 1\) 个元素都会和 \(i\) 产生 \(val_i\) 的贡献
然后对于子树内部选择的点 它自己就是最小值
所以有 \(f_{i, j + k + 1} = f_{ls, j} + f_{rs, k} - 2 \times val_i \times j \times k + (m - 1) \times val_i - 2 \times (i + j) \times val_i\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;

inline int read() {
    int xr = 0, F = 1;
    char cr;
    while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
    while (cr >= '0' && cr <= '9')
        xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
    return xr * F;
}

void write(ll x) {
    char ws[51];
    int wt = 0;
    if (x < 0) putchar('-'), x = -x;
    do {
        ws[++wt] = x % 10 + '0';
        x /= 10;
    } while (x);
    for (int i = wt; i; --i) putchar(ws[i]);
}

namespace steven24 {

const int N = 5210;
int a[N];
int ls[N], rs[N];
ll f[N][N];
int siz[N];
bool vis[N];
int stk[N], top;
int n, m, rt;

void dfs(int x) {
    siz[x] = 1;
    if (ls[x]) dfs(ls[x]);
    if (rs[x]) dfs(rs[x]);
    siz[x] += siz[ls[x]] + siz[rs[x]];

    for (int i = 0; i <= siz[ls[x]]; ++i) {
        for (int j = 0; j <= siz[rs[x]]; ++j) {
            f[x][i + j] = max(f[x][i + j], f[ls[x]][i] + f[rs[x]][j] - i * j * a[x] * 2ll);
            f[x][i + j + 1] = max(f[x][i + j + 1], f[ls[x]][i] + f[rs[x]][j] - i * j * a[x] * 2ll + 1ll * (m - 1) * a[x] - 2ll * (i + j) * a[x]);
        }
    }

    // for (int i = 0; i <= m; ++i) if (f[x][i]) cout << x << " " << i << " " << f[x][i] << "\n";
    // cout << "\n";
}

void main() {
    n = read(), m = read();
    for (int i = 1; i <= n; ++i) a[i] = read();
    for (int i = 1; i <= n; ++i) {
		int k = top;
		while (k > 0 && a[stk[k]] > a[i]) --k;
		if (k) rs[stk[k]] = i;
		if (k < top) ls[i] = stk[k + 1];
		stk[++k] = i;
		top = k;
	}
    for (int i = 1; i <= n; ++i) vis[ls[i]] = vis[rs[i]] = 1;
    for (int i = 1; i <= n; ++i) {
    	if (!vis[i]) {
    		dfs(i);
    		write(f[i][m]), putchar('\n');
		} 
	}
}

}

signed main() {
    steven24::main();
    return 0;
}

T6 Hossam and Range Minimum Query

luogu link

查询区间内出现次数 还是出现次数为奇数的 考虑主席树二分
那么我们就需要维护一些奇数特有的性质 发现异或操作奇数次就不为 \(0\) 否则为 \(0\)
所以我们在每个节点上维护区间异或和即可
但是因为要维护异或和 出个 \(1 2 3\) 啥的就直接挂掉了
所以给每个点随机一个很大的权值即可


T7 [CTSC2018] 混合果汁

luogu link

虽然我还是觉得叫混合果汁儿更顺口

首先不难想到二分答案 关键在于 \(check\)
首先很显然的贪心思路是尽可能地多选价值最小的
暴力统计是不行的 我们考虑用主席树维护这个东西
我们以价格为下标 记录每个节点的果汁总升数和总钱数
然后在主席树上二分就可以了

二分的时候要注意还要额外判整棵树上的总升数合法 否则可能出现要求升数乘以 100000 仍小于 g 但实际不满足的情况

点击查看代码
/*
主席树的每个节点维护果汁总数和全选上的价格
查询时主席树二分果汁总数
如果l == r的时候l * 数量 
*/
#include <bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;

inline int read() {
    int xr = 0, F = 1;
    char cr;
    while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
    while (cr >= '0' && cr <= '9')
        xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
    return xr * F;
}

void write(ll x) {
    char ws[51];
    int wt = 0;
    if (x < 0) putchar('-'), x = -x;
    do {
        ws[++wt] = x % 10 + '0';
        x /= 10;
    } while (x);
    for (int i = wt; i; --i) putchar(ws[i]);
}

namespace steven24 {
	
const int N = 1e5 + 0721;
const int M = 3e6 + 0721;
const int maxn = 1e5;

int T[N];
int n, m;

struct node {
	int d, c, l;
	friend bool operator<(node x, node y) {
		return x.d > y.d;
	}
} a[N];
	
struct segment_tree {
	
#define mid (l + r >> 1)	
	
int sum[M];
ll cost[M];
int ls[M], rs[M], tot;

void pushup(int k) {
	sum[k] = sum[ls[k]] + sum[rs[k]];
	cost[k] = cost[ls[k]] + cost[rs[k]];
}

int build(int l, int r) {
	int k = ++tot;
	if (l == r) {
		return k;
	}
	ls[k] = build(l, mid);
	rs[k] = build(mid + 1, r);
	return k;
}	

int insert(int pre, int l, int r, int loc, int val) {
	int k = ++tot;
	ls[k] = ls[pre], rs[k] = rs[pre];
	if (l == r) {
		sum[k] = sum[pre] + val;
		cost[k] = 1ll * sum[k] * l;
		return k;
	}
	if (loc <= mid) ls[k] = insert(ls[pre], l, mid, loc, val);
	else rs[k] = insert(rs[pre], mid + 1, r, loc, val);
	pushup(k);
	return k;
}

ll query(int k, int l, int r, int val) {
	if (l == r) {
		return 1ll * val * l;
	}
	if (val <= sum[ls[k]]) return query(ls[k], l, mid, val);
	else return cost[ls[k]] + query(rs[k], mid + 1, r, val - sum[ls[k]]);
}
	
} seg;

ll binary_search(ll g, ll L) {
	int l = 1, r = maxn;
	int ans = -1;
	while (l <= r) {
		if (seg.query(T[mid], 1, maxn, L) <= g && seg.sum[T[mid]] >= L) {
			ans = mid;
			l = mid + 1;
		} else
			r = mid - 1;
	}
	return ans;
}
	
void main() {
	n = read(), m = read();
	for (int i = 1; i <= n; ++i) a[i].d = read(), a[i].c = read(), a[i].l = read();
	
	sort(a + 1, a + 1 + n);
	T[maxn + 1] = seg.build(1, maxn);
	
	int prs = 1;
	int lst = maxn + 1;
	
	for (int i = maxn; i; --i) {
		if (prs > n || a[prs].d < i) T[i] = T[i + 1];
		while (prs <= n && a[prs].d == i) {
			T[i] = seg.insert(T[lst], 1, maxn, a[prs].c, a[prs].l);
			++prs;
			lst = i;
		}
	}
	
	while (m--) {
		int g = read(), l = read();
		write(binary_search(g, l)), putchar('\n');
	}
} 

}

signed main() {
    steven24::main();
    return 0;
}
/*
3 4
1 3 5
2 1 3
3 2 5
6 3
5 3
10 10
20 10
*/

T8 最大异或和

luogu link

首先想到把异或和转化为两个前缀异或和异或
然后我们想到建立一个可持久化 \(01trie\) 树 每次贪心地选取与当前节点不一样的走
那如何判呢? 我们直接维护每个版本中节点出现的次数 然后用两个版本相减 看有没有剩下的即可


T9 MinimizOR

luogu link

有了上一题的经验 我们可以建可持久化 \(01trie\) 然后用两个版本相减看这个区间内这个节点出现次数
因为我们尽量的要选两个 \(0\)
对于一个节点 如果它有 \(\ge 2\)\(0\) 那我们走过去就好了
如果一个 \(0\) 都没有 我们只能走 \(1\) 那边
问题在于只有一个 \(0\) 的情况
这种情况下我们选择把这个数揪出来 然后强制把这一位取反再插回去 然后我们走 \(1\)
当然做完这个询问要再把这个数删掉


T10 [✗✓OI R1] 天动万象

luogu link

因为我不玩原 所以不会很正常
所以先咕了((

算了还是写一下吧
首先我们发现这样一个性质:每次操作之后叶节点就没了
还有另外一个性质:对于一条链 每次操作就相当于把这个链上所有点的权值向根移动一位
那么我们考虑树链剖分
那么这样的话 每次操作就是首先把所有链都向根节点平移 但是我们如何处理一个点的非重儿子呢
我们考虑把一个点的非重儿子都连到与父亲相连的另一条链上 那么实际上每次操作就是把他们往父亲平移然后把每条链上往根节点平移一下
呃啊 但是还是十分的不好维护
我们考虑能不能用 \(dfs\) 序来直接维护这个平移的操作
因为每次操作每条链都有一个叶节点被空出来 那么假如说让它是由它 \(dfs\)\(+1\) 的一个点平移过来呢?
那么假如说我们不是把每个非重儿子挂在父亲相连的链上 而是把它们挂在一条全是虚点的链上 然后把这条链连到父亲上
那就完美满足上面的性质了
然后我们还要每次把虚点的值都送给那个父亲节点
然后就和树剖差不多了 因为虚点的编号肯定是前一条链末尾节点的编号 \(-1\)
所以我们维护每一条链末尾结点就行了
并且显然我们还要维护对于每个节点链顶的编号
然后问题就变成了区间平移 单点修改(把虚点值给父亲节点)和查询区间最大值(操作1)
用平衡树实现即可
偷一张图大家感性理解一下:
image

posted @ 2023-07-06 20:56  Steven24  阅读(35)  评论(0)    收藏  举报