[数据结构] [算法] 专题狂写

或曰,有学长两天授吾以十专题,吾顿感日月之紧迫,以专题竟不能以吾之所有,遂成此文,以记之

语文确实没学好

本文章汇编于:[算法] 2-SAT简记[算法] 一些分治[数据结构] 划分树[数据结构] 珂朵莉树 等以及一些复习内容

本文可能涵盖多个知识点,故每个的讲解可能比较简略,仅供参考

2-SAT

2SAT 用于求解一个变量只有两种情况的适应性问题(就是有没有解以及输出一种方案);

其实可以类比二分图最大匹配(但其实两者的差别还是很大的);

算法流程

对于每一个变量,我们都有两种情况,truefalse

而题目中给我们的,是形如 {A=true/falseB=true/false} 的若干个二元组,问是否能够全部满足;

如题:Luogu P4782 【模板】2-SAT

考虑对于{A=pB=q}, 我们可以有如下的伪代码:

if (A == p) B 可以为 q 或 !q
if (A == !p) B 只能为 q
if (B == q) A 可以为 p 或 !p
if (B == !q) A 只能为 p

我们不难发现,对于第二种和第四种情况,我们是可以确定 AB 的;

因此,我们可以依据这个性质,进行加边操作:

ApBq 这条边代表当 Ap 时,B 只能为 q

这样建好图后,我们要判断无解;

引理1:ApA!p 在同一强联通分量中,即无解;

显然;

求法:Tarjan
对于有解的情况,题目要求输出一种方案,有以下引理:

引理2: 对于A 的两种情况 ApA!p,若选择两者拓扑序较大的( Tarjan 遍历顺序较小的)作为答案,则一定可以满足要求;

这里的“满足要求”指的是满足要求的多种情况中的一种情况;

其实稍微思考一下,不难发现拓扑序较大的节点需要满足的条件较少,所以更容易满足要求;

至于证明,我来浅浅的证明一下:

证:

考虑采用反证法;

f(Ap) 表示缩点后 Ap 所在强连通分量的拓扑序;

设有:

f(Ap)<f(A!p)              (1)                           f(B!q)<f(Bq)              (2)

且存在单向边:

X=(BqAp)

由题意,我们可以得知有一条单向边:

Y=(A!pB!q)

此时我们可以发现,原来的二元组为:{ApB!q}

所以此时的合法方案有:

A=p,B=!q

A=p,B=q

A=!p,B=!q

三种;

不难发现,这三种情况涵盖了选出的两个变量的拓扑序都是小的和一大一小两种情况,唯独没有两个都是大的这种情况;

X,易得:

f(Ap)>f(Bq)              (3)

Y,易得:

f(A!p<B!q)              (4)

联立 (1)(3)(4),得:

f(B!q)>f(Bq)

(2) 矛盾,所以可以得出,在这种情况下,选出的两个变量的拓扑序都是小的和一大一小两种情况不合法(最极限的情况),而我们又可以判断出题目中所给出的情况有解,所以选择两者拓扑序较大的一定可以满足要求;

引理2得证;

证毕;

Luogu P5782 [POI2001] 和平委员会

把每个人的搭档看做其反面即可;

最后判断一下搭档是否在同一个强连通分量里即可;

输出方案和上一题相同;

以下分治内容同步更新于:一些分治

点分治

静态点分治

Luogu P3806 【模板】点分治 1

考虑此题,暴力非常好想,对于每个询问,枚举起点及终点即可;

时间复杂度:Θ(mn2)peppapig都不能忍受;

引入点分治:

对于两点 u,v 间的路径,我们可以分为两种:

  1. 跨越根节点;

  2. 不跨越根节点;

对于第一类路径,不妨设一点 x 到根节点的路径长度为 dis[x] ,则第一类路径长可以表示为:dis[u]+dis[v]

对于第二类路径,其 u,v 一定是在同一棵子树内的,所以我们可以对这棵子树单独求解,再求一遍第一类路径,以得到答案;

这样很明显要用到分治;

所以我们就有了思路:对于每一次询问,对其子树单独求解,最后得到答案;

但是,时间复杂度仍然不能保证。考虑一条链,这样我们需要对 Θ(n) 个子树分别求解,时间复杂度为 Θ(n),不能接受;

所以,我们有了一种新的方法:找树的重心

树的重心,即最大的子树大小不超过 n2 的根节点;

每次进行分治时,我们找子树的重心作为根节点进行分治,这样时间复杂度就变为了:整体点分治 Θ(nlogn)

对于证明(我不会),可以参考一篇dalao的证明

其实可以感性理解一下,我们每次找重心时,都会把现在树的大小除以 2,这不就相当于二分吗,再加上我们每次找重心时都会把当前子树的节点遍历一遍,所以再乘个 n,就是 Θ(nlogn)

大体思路已经清楚,现在看代码实现:

对于找重心的实现,设 maxp[x] 代表以 x 为根的树的子树中的最大的的大小,显然,重心的 maxp 是最小的;

所以我们就可以对现有子树进行 dfs,每次回溯时更新 maxp 即可,这样我们就找到了重心;

找重心的代码:

void get_rt(int x, int fa) {
	siz[x] = 1;
	maxp[x] = 0;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == fa || vis[u]) continue;
		get_rt(u, x);
		siz[x] += siz[u];
		maxp[x] = max(maxp[x], siz[u]);
	}
	maxp[x] = max(maxp[x], sum - siz[x]);
	if (maxp[x] < maxp[rt]) rt = x;
}

其实对于大部分的点分治题目来说,所需要的一共有三个函数:

  1. solve 函数:用于构建分治框架;

  2. calc 函数:用于计算当前答案;

  3. getrt 函数:用于找重心;

对于第一种函数,各个题目基本都是相同的,对于本题大致如下:

void solve(int x) {
	vis[x] = true;
	judge[0] = 1;
	calc(x);
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		sum = siz[u];
		rt = 0;
		maxp[0] = 0x3f3f3f3f;
		get_rt(u, 0);
		solve(rt);
	}
}

对于第二种函数,随题目的不同而不同,对于本题大致如下:

void get_dis(int x, int fa) {
	if (dis[x] > 1e7) return; //防炸数组;
	rem[++rem[0]] = dis[x];
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == fa || vis[u]) continue;
		dis[u] = dis[x] + e[i].w;
		get_dis(u, x);
	}
}
void calc(int x) {
	int o = 0;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		rem[0] = 0;
		dis[u] = e[i].w;
		get_dis(u, x);
		for (int j = rem[0]; j >= 1; j--) {
			for (int k = 1; k <= m; k++) {
				if (ask[k] >= rem[j]) {
					if (ask[k] - rem[j] > 1e7) continue; //防炸数组;
					test[k] |= judge[ask[k] - rem[j]];
				}
			}
		}
		for (int j = rem[0]; j >= 1; j--) {
			s[++o] = rem[j];
			judge[rem[j]] = true;
		}
	}
	for (int i = 1; i <= o; i++) {
		judge[s[i]] = false;
	}
}

如上,我们对于本题,在线做法不能忍受,所以要离线,judge[i] 表示在这棵子树所遍历到的节点中有没有到重心长度为 i 的路径,dis[x] 表示当前所便历的子树的到根的距离;

这样,本题的做法就明朗了;

代码:

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
int n, m;
struct sss{
	int t, ne, w;
}e[1000005];
int h[1000005], cnt;
int sum, rt;
int ask[1000005];
int maxp[1000005], siz[1000005];
bool vis[1000005];
bool judge[1000005], test[1000005];
int rem[1000005], dis[1000005];
int s[1000005];
void add(int u, int v, int ww) {
	e[++cnt].t = v;
	e[cnt].w = ww;
	e[cnt].ne = h[u];
	h[u] = cnt;
}
void get_rt(int x, int fa) {
	siz[x] = 1;
	maxp[x] = 0;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == fa || vis[u]) continue;
		get_rt(u, x);
		siz[x] += siz[u];
		maxp[x] = max(maxp[x], siz[u]);
	}
	maxp[x] = max(maxp[x], sum - siz[x]);
	if (maxp[x] < maxp[rt]) rt = x;
}
void get_dis(int x, int fa) {
	if (dis[x] > 1e7) return; //防炸数组;
	rem[++rem[0]] = dis[x];
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == fa || vis[u]) continue;
		dis[u] = dis[x] + e[i].w;
		get_dis(u, x);
	}
}
void calc(int x) {
	int o = 0;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		rem[0] = 0;
		dis[u] = e[i].w;
		get_dis(u, x);
		for (int j = rem[0]; j >= 1; j--) {
			for (int k = 1; k <= m; k++) {
				if (ask[k] >= rem[j]) {
					if (ask[k] - rem[j] > 1e7) continue; //防炸数组;
					test[k] |= judge[ask[k] - rem[j]];
				}
			}
		}
		for (int j = rem[0]; j >= 1; j--) {
			s[++o] = rem[j];
			judge[rem[j]] = true;
		}
	}
	for (int i = 1; i <= o; i++) {
		judge[s[i]] = false;
	}
}
void solve(int x) {
	vis[x] = true;
	judge[0] = 1;
	calc(x);
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		sum = siz[u];
		rt = 0;
		maxp[0] = 0x3f3f3f3f;
		get_rt(u, 0);
		solve(rt);
	}
}
int main() {
	cin >> n >> m;
	int u, v, w;
	for (int i = 1; i < n; i++) {
		cin >> u >> v >> w;
		add(u, v, w);
		add(v, u, w);
	}
	for (int i = 1; i <= m; i++) {
		cin >> ask[i];
	}
	maxp[rt] = 0x3f3f3f3f;
	sum = n;
	get_rt(1, 0);
	solve(rt);
	for (int i = 1; i <= m; i++) {
		if (test[i]) {
			cout << "AYE" << endl;
		} else {
			cout << "NAY" << endl;
		}
	}
	return 0;
}

还有一些题,这里大体整一下思路;

  1. Luogu P4206 [NOI2005] 聪聪与可可

板子题,记一下 mod 3 意义下余数分别为 1 2 0 的个数,合并时统计即可;

  1. Luogu P4149 [IOI2011] Race

板子题,开个二元组记录一下权值和边数即可;

  1. Luogu P4178 Tree

板子题,和第一题类似,只不过开个树状数组记录一下前缀和,然后就解决了;

  1. Luogu P3714 [BJOI2017] 树的难题

其实思路不难,但细节太多了。。。

首先,路径还是能拆分成两类:经过根的和不经过根的;

所以可以点分治;

首先将每个点的儿子按大小排序,因为这样我们就可以比较方便的处理到根的路径颜色相同的子树们;

然后进行点分治,我们开两个线段树,把与当前路径颜色相同的放进一个线段树,不同的放进一个线段树,然后正常跑就行;

注意线段树的清空,可以直接在根节点上打懒标记;

然后就是一些细节,不说了,可以看代码;

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
int n, m, l, r;
int c[500005];
int rt, sum;
struct sss{
	int t, ne, w;
}e[1000005];
int h[1000005], cnt;
void add(int u, int v, int ww) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	e[cnt].w = ww;
	h[u] = cnt;
}
vector<pair<int, int> > v[200005];
struct sas{
	int dis, sum;
}dis[200005], rem[200005], po[200005];
int maxp[1000005], siz[1000005], dep[1000005];
bool vis[1000005];
int ans;
namespace seg{
	inline int ls(int x) {
		return x << 1;
	}
	inline int rs(int x) {
		return x << 1 | 1;
	}
	struct asa{
		int l, r, ma, lz;
	}tr[2][900005];
	inline void push_up(int s, int id) {
		tr[s][id].ma = max(tr[s][ls(id)].ma, tr[s][rs(id)].ma);
	}
	inline void push_down(int s, int id) {
		if(tr[s][id].lz != 0) {
			tr[s][ls(id)].lz = tr[s][id].lz;
			tr[s][rs(id)].lz = tr[s][id].lz;
			tr[s][ls(id)].ma = tr[s][id].lz;
			tr[s][rs(id)].ma = tr[s][id].lz;
			tr[s][id].lz = 0;
		}
	}
	void bt(int s, int id, int l, int r) {
		tr[s][id].l = l;
		tr[s][id].r = r;
		if (l == r) {
			tr[s][id].ma = -0x3f3f3f3f;
			tr[s][id].lz = 0;
			return;
		}
		int mid = (l + r) >> 1;
		bt(s, ls(id), l, mid);
		bt(s, rs(id), mid + 1, r);
		push_up(s, id);
	}
	inline void clear(int s) {
		tr[s][1].lz = -0x3f3f3f3f;
		tr[s][1].ma = -0x3f3f3f3f;
	}
	int ask(int s, int id, int l, int r) {
		if (tr[s][id].l >= l && tr[s][id].r <= r) {
			return tr[s][id].ma;
		}
		push_down(s, id);
		int mid = (tr[s][id].l + tr[s][id].r) >> 1;
		if (r <= mid) return ask(s, ls(id), l, r);
		else if (l > mid) return ask(s, rs(id), l, r);
		else return max(ask(s, ls(id), l, mid), ask(s, rs(id), mid + 1, r));
	}
	void add(int s, int id, int pos, int d) {
		if (tr[s][id].l == tr[s][id].r) {
			tr[s][id].ma = max(tr[s][id].ma, d);
			tr[s][id].lz = 0;
			return;
		}
		push_down(s, id);
		int mid = (tr[s][id].l + tr[s][id].r) >> 1;
		if (pos <= mid) add(s, ls(id), pos, d);
		else add(s, rs(id), pos, d);
		push_up(s, id);
	}
}
void get_rt(int x, int f) {
	siz[x] = 1;
	maxp[x] = 0;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == f || vis[u]) continue;
		get_rt(u, x);
		siz[x] += siz[u];
		maxp[x] = max(maxp[x], siz[u]);
	}
	maxp[x] = max(maxp[x], sum - siz[x]);
	if (maxp[rt] > maxp[x]) rt = x;
}
void get_dis(int x, int f, int pre) {
	dep[x] = dep[f] + 1;
	if (dep[x] > r) return;
	dis[x].dis = dep[x];
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u] || u == f) continue;
		dis[u].sum = dis[x].sum; //注意继承的问题;
		if (e[i].w != pre && e[i].w) dis[u].sum += c[e[i].w]; //注意判断;
		get_dis(u, x, e[i].w);
	}
}
void dfs(int x, int f) {
	if (dis[x].dis == 0) return;
	rem[++rem[0].sum] = sas{dis[x].dis, dis[x].sum};
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == f || vis[u]) continue;
		dfs(u, x);
	}
}
void calc(int x) {
	int color = 0;
	int o = 0;
	dep[x] = 0;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		if (color == 0) {
			color = e[i].w;
		} else if (color != e[i].w) {
			color = e[i].w;
			seg::clear(1);
			for (int j = 1; j <= o; j++) {
				seg::add(0, 1, po[j].dis, po[j].sum);
			}
			o = 0;
		}
		rem[0].sum = 0;
		dis[u].sum = c[e[i].w];
		get_dis(u, x, e[i].w);
		dfs(u, x);
		for (int j = 1; j <= rem[0].sum; j++) {
			if (rem[j].dis > r) continue;
			if (rem[j].dis >= l && rem[j].dis <= r) {
				ans = max(ans, rem[j].sum);
			}
			if (rem[j].dis == r) continue;
			if (rem[j].dis == 0) continue;
			int aa = seg::ask(0, 1, max(0, l - rem[j].dis), r - rem[j].dis);
			int bb = seg::ask(1, 1, max(0, l - rem[j].dis), r - rem[j].dis);
			bb -= c[e[i].w];
			ans = max(ans, max(rem[j].sum + aa, rem[j].sum + bb));
		}
		for (int j = 1; j <= rem[0].sum; j++) {
			if (rem[j].dis == 0) continue;
			o++;
			po[o].dis = rem[j].dis;
			po[o].sum = rem[j].sum;
		}
		for (int j = 1; j <= rem[0].sum; j++) {
			if (rem[j].dis == 0) continue;
			seg::add(1, 1, rem[j].dis, rem[j].sum);
		}
	}
	seg::clear(0);
	seg::clear(1);
}
void solve(int x) {
	vis[x] = true;
	calc(x);
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		rt = 0;
		maxp[rt] = 0x3f3f3f3f;
		sum = siz[u];
		get_rt(u, 0);
		solve(rt);
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m >> l >> r;
	for (int i = 1; i <= m; i++) {
		cin >> c[i];
	}
	int x, y, w;
	for (int i = 1; i <= n - 1; i++) {
		cin >> x >> y >> w;
		v[x].push_back({w, y});
		v[y].push_back({w, x});
	}
	for (int i = 1; i <= n; i++) {
		sort(v[i].begin(), v[i].end());
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j < v[i].size(); j++) {
			add(i, v[i][j].second, v[i][j].first);
		}
	}
	seg::bt(0, 1, 0, n);
	seg::bt(1, 1, 0, n);
	ans = -0x3f3f3f3f;
	rt = 0;
	maxp[rt] = 0x3f3f3f3f;
	sum = n;
	get_rt(1, 0);
	solve(rt);
	cout << ans;
	return 0;
}

貌似题解有单调队列的优秀做法,但我不会,有兴趣的可以去看看;

可能是我目光短浅,感觉点分治这玩意就是模板 + 数据结构维护,没啥的;

当然前提是得看出来是点分治。。。

动态点分治,点分树

一般我们遇到的问题不单单只是查询,还有修改,如果此时每个修改都进行一边点分治的话,时间复杂度是不允许的,这时候,我们就需要用到点分树;

Luogu P6329 【模板】点分树 | 震波

如题,加了一个单点修改的操作;

既然我们每次都要找重心,那么不妨将子树的重心与当前考虑的树的重心连边,这样我们就得到了一棵点分树;

这样做有什么优点呢?不难发现,这棵树高是 log(n) 级别的,也就是说,我们可以在上面暴力跳父亲;

这样,我们只需要记录点分树上的父子关系,每次询问时,从询问的点(设为 x )出发一级一级的跳父亲并统计答案即可;

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cmath>
using namespace std;
int n, m;
int a[100005];
inline int lowbit(int x) {
	return x & (-x);
}
struct sss{
	int t, ne;
}e[500005];
int h[500005], cnt;
void add(int u, int v) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	h[u] = cnt;
}
int dep[100005], f[100005][35];
int fa[100005], maxp[100005], siz[100005];
int dfn[100005];
int dcnt;
int sum, rt;
bool vis[100005];
vector<int> tr[2][100005];
void init_dfs(int x, int ff) {
	dep[x] = dep[ff] + 1;
	dfn[x] = ++dcnt;
	f[x][0] = ff;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (u == ff) continue;
		init_dfs(u, x);
	}
}
void get_rt(int x, int ff) {
	maxp[x] = 0;
	siz[x] = 1;
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u] || u == ff) continue;
		get_rt(u, x);
		siz[x] += siz[u];
		maxp[x] = max(maxp[x], siz[u]);
	}
	maxp[x] = max(maxp[x], sum - siz[x]);
	if (maxp[x] < maxp[rt]) rt = x;
}
void dfs(int x) {
	siz[x] = sum + 1;
	vis[x] = true;
	tr[0][x].resize(siz[x] + 1);
	tr[1][x].resize(siz[x] + 1);
	for (int i = h[x]; i; i = e[i].ne) {
		int u = e[i].t;
		if (vis[u]) continue;
		sum = siz[u];
		rt = 0;
		maxp[rt] = 0x3f3f3f3f;
		get_rt(u, 0);
		fa[rt] = x;
		dfs(rt);
	}
}
int mi(int x, int y) {
	return dfn[x] < dfn[y] ? x : y;
}
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 d = __lg(y - x + 1);
	// return mi(f[x][d], f[y - (1 << d) + 1][d]);
	if (dep[x] < dep[y]) swap(x, y);
	for (int i = 30; i >= 0; i--) {
		if (dep[f[x][i]] >= dep[y]) x = f[x][i];
	}
	if (x == y) return x;
	for (int i = 30; i >= 0; i--) {
		if (f[x][i] != f[y][i]) {
			x = f[x][i];
			y = f[y][i];
		}
	}
	return f[x][0];
}
int get_dis(int x, int y) {
	return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}
void add_tr(int s, int x, int dis, int d) {
	dis++;
	for (int i = dis; i <= siz[x]; i += lowbit(i)) tr[s][x][i] += d;
}
void def(int x, int d) {
	for (int i = x; i; i = fa[i]) {
		add_tr(0, i, get_dis(i, x), d);

	}
	for (int i = x; fa[i]; i = fa[i]) {
		add_tr(1, i, get_dis(fa[i], x), d);
	}
}
int ask(int s, int x, int d) {
	int ans = 0;
	d++;
	d = min(d, siz[x]);
	for (int i = d; i; i -= lowbit(i)) {
		ans += tr[s][x][i];
	}
	return ans;
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int x, y;
	for (int i = 1; i <= n - 1; i++) {
		cin >> x >> y;
		add(x, y);
		add(y, x);
	}
	init_dfs(1, 0);
	for (int j = 1; j <= 33; j++) {
		for (int i = 1; i <= n; i++) {
			f[i][j] = f[f[i][j - 1]][j - 1];
		}
	}
	sum = n;
	rt = 0;
	maxp[rt] = 0x3f3f3f3f;
	get_rt(1, 0);
	dfs(rt); // establish a dian fen tree;
	for (int i = 1; i <= n; i++) {
		def(i, a[i]);
	}
	int ans = 0;
	int s;
	for (int i = 1; i <= m; i++) {
		cin >> s >> x >> y;
		x ^= ans;
		y ^= ans;
		if (s == 1) {
			def(x, y - a[x]);
			a[x] = y;
		}
		if (s == 0) {
			ans = 0;
			ans += ask(0, x, y);
			for (int i = x; fa[i]; i = fa[i]) {
				int dis = get_dis(x, fa[i]);
				if (y >= dis) {
					ans += ask(0, fa[i], y - dis) - ask(1, i, y - dis);
				}
			}
			cout << ans << endl;
		}
	}
	return 0;
}

普通分治

其实没啥,每次只计算跨越分治中心的区间的贡献,剩下的递归到左右两边进行分治;

时间复杂度:分治树高度为 Θ(logn),乘上其他操作的复杂度即可;

例题一:现在有一个 n 阶排列 a,计算:

i=1nj=inmin(ai,ai+1,...,aj)

其中 n200000

题意简述:找一个给定的序列的所有子区间的最小值的和;

可以线性做,对于每一个 ai,记录其向左和向右第一个小于它的值,计算一下即可;

这里讲一下分治的做法;

其实对于这种求所有区间中符合条件的区间的题目,一般都可以分治做;

考虑跨过分治中心的区间,设分治中心为 mid, 我们可以从分治中心向左维护出对于任意一个左端点 l,区间 [l,mid] 的最小值并存放在一个数组 b 中;

处理完上述步骤后,我们开始从分治中心向右遍历右端点,每遍历到一个右端点 r,我们发现它的所有跨过分治中心的区间的最小值有两种情况:

  1. 是区间 [mid,r] 中的值;

  2. mid 左边的值;

对于第一种情况,我们每次遍历时维护一个最小值 mi 即可;

对于第二种情况,暴力做法肯定不行,那怎么办呢?

挖掘一下性质,我们发现 b 数组中的值是非严格单调递减的(因为最小值只能不变或者更小,不会变大);

所以,我们可以每次遍历右端点时用二分查找找出 b 数组中第一个大于 mi 的位置,然后小于等于它的最小值不变,大于它的最小值变为 mi

为了避免手写二分(懒,不想写),我们可以将 b 数组 reverse 一下,然后用 upper bound 即可;

当然,还要维护一个前缀和(注意是 reverse 后的);

时间复杂度:分治 + 遍历 Θ(nlogn),二分查找 Θ(logn),总的 Θ(nlog2n) (时间复杂度确实劣了一些);

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
long long n;
long long a[500005];
long long b[500005], sum[500005];
long long c[500005];
long long ans;
void solve(long long l, long long r) {
	if (l >= r) return;
	if (r - l + 1 == 2) {
		ans += min(a[l], a[r]);
		return;
	}
	long long mid = (l + r) >> 1;
	long long o = 0;
	for (int i = 0; i <= mid - l; i++) b[i] = 0x3f3f3f3f;
	for (long long i = mid - 1; i >= l; i--) {
		o++;
		b[o] = min(b[o - 1], a[i]);
	}
	long long cnt = 0;
	for (long long i = o; i >= 1; i--) {
		c[++cnt] = b[i];
	}
	for (long long i = 0; i <= cnt; i++) {
		sum[i] = 0;
	}
	for (long long i = 1; i <= cnt; i++) {
		sum[i] = sum[i - 1] + c[i];
	}
	long long mi = a[mid];
	for (long long j = mid + 1; j <= r; j++) {
		mi = min(mi, a[j]);
		long long pos = upper_bound(c + 1, c + 1 + cnt, mi) - c;
		if (pos <= cnt) ans += (cnt - pos + 1) * mi;
		ans += sum[pos - 1];
	}
	solve(l, mid);
	solve(mid, r);
}
int main() {
	cin >> n;
	for (long long i = 1; i <= n; i++) {
		cin >> a[i];
	}
	solve(1, n);
	for (long long i = 1; i <= n; i++) ans += a[i];
	cout << ans;
	return 0;
}

其实很多分治的套路是维护前缀和 + 发现性质,做的时候可以注意一下;

例题二: Luogu P4062 [Code+#1] Yazid 的新生舞会

这题貌似题解中的主流做法是用数据结构维护高阶前缀和,这里讲一下分治做法;

还是求所有区间中符合条件的区间,可以考虑分治;

找区间中的绝对众数,我们可以借鉴一下摩尔投票法,设现在我们考虑的众数为 x,将不是 x 的数看为 1,是 x 的数看为 1,最后判断一下整个区间的和与 0 的关系即可;

首先,对于一个区间 [l,r],其绝对众数是 [l,k] 的绝对众数或 [k+1,r] 的绝对众数,其中 lkr

所以可以令 k=mid,然后分治求解;

分治时,还是先从分治中心向左找符合条件的绝对众数以及区间和所出现的次数,向右遍历时统计一下区间和是否 >0 即可;

由于我们要遍历绝对众数,而绝对众数最多变化 Θ(logn) 次(相当于每次砍一半才能更新一次绝对众数),所以总的时间复杂度为 Θ(nlog2n)

写的比较粗略,可以参考一下原题解区的题解;

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
int n, ddd;
int a[1000005];
long long ans;
int pos[1000005], vis[1000005], num[1000005], cnt[1000005];
void solve(int l, int r) {
	if (l == r) {
		ans++;
		return;
	}
	int mid = (l + r) >> 1;
	num[0] = 0;
	for (int i = mid; i >= l; i--) {
		if (++cnt[a[i]] > (mid - i + 1) / 2) {
			if (!pos[a[i]]) {
				pos[a[i]] = ++num[0];
				num[pos[a[i]]] = a[i];
			}
		}
	}
	for (int i = mid + 1; i <= r; i++) {
		if (++cnt[a[i]] > (i - mid) / 2) {
			if (!pos[a[i]]) {
				pos[a[i]] = ++num[0];
				num[pos[a[i]]] = a[i];
			}
		}
	}
	for (int i = l; i <= r; i++) {
		pos[a[i]] = 0;
		cnt[a[i]] = 0;
	}
	for (int i = 1; i <= num[0]; i++) {
		int sum = r - l + 1;
		int ma = sum;
		int mi = sum;
		cnt[sum] = 1;
		for (int j = l; j < mid; j++) {
			if (a[j] == num[i]) {
				sum++;
			} else {
				sum--;
			}
			ma = max(ma, sum);
			mi = min(mi, sum);
			cnt[sum]++;
		}
		if (a[mid] == num[i]) {
			sum++;
		} else {
			sum--;
		}
		for (int j = mi; j <= ma; j++) {
			cnt[j] += cnt[j - 1];
		}
		for (int j = mid + 1; j <= r; j++) {
			if (a[j] == num[i]) {
				sum++;
			} else {
				sum--;
			}
			ans += cnt[min(sum - 1, ma)];
		}
		for (int j = mi; j <= ma; j++) {
			cnt[j] = 0;
		}
	}
	solve(l, mid);
	solve(mid + 1, r);
}
int main() {
	cin >> n >> ddd;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	solve(1, n);
	cout << ans;
	return 0;
}

猫树分治

实话说,我就做过一个关于这个的题,感觉和普通分治没有什么区别;

直接粘我以前写的题解了(出处):

Luogu P6240 好吃的题目

暴力一:每次跑一边 DP

暴力二:使用背包的合并操作,时间复杂度 Θ(n2) ?;

正解:猫树分治;

这玩意听着这么像数据结构,其实就是一个套路;

好像它的发明者受到了线段树分治的启发?

和普通的分治没什么区别,难的是想到分治(所以才给它起了个名字嘛);

每次只计算跨过分治中心的区间,首先预处理出从分治中心向左和向右的每个点到终点这段区间的所有 200 个最优值,然后进行合并,注意要将不跨过分治中心的区间筛选出来,分别放在左右两边,然后继续递归;

所以我们需要四个指针,两个记录现在处理的序列上的左右端点,另外两个记录现在处理的问题的区间(这里的 “区间” 并不绝对,只要是没处理的,都能出现在这一段区间),然后正常递归即可;

时间复杂度:T(200nlogn)

点击查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n, m;
int h[500005], w[500005];
struct sss{
	int l, r, t;
}b[500005];
int ans[500005], p[500005], s[500005], ncnt;
int f[50005][205];
void solve(int l, int r, int L, int R) {
	if (L > R) return;
	int mid = (l + r) >> 1;
	int Mid = L - 1;
	for (int i = 0; i <= 200; i++) f[mid][i] = 0;
	for (int i = mid + 1; i <= r; i++) {
		for (int j = 0; j < h[i]; j++) f[i][j] = f[i - 1][j];
		for (int j = h[i]; j <= 200; j++) {
			f[i][j] = max(f[i - 1][j], f[i - 1][j - h[i]] + w[i]);
		}
	}
	for (int i = h[mid]; i <= 200; i++) f[mid][i] = w[mid];
	for (int i = mid - 1; i >= l; i--) {
		for (int j = 0; j < h[i]; j++) f[i][j] = f[i + 1][j];
		for (int j = h[i]; j <= 200; j++) {
			f[i][j] = max(f[i + 1][j], f[i + 1][j - h[i]] + w[i]);
		}
	}
	ncnt = 0;
	int u = 0;
	for (int i = L; i <= R; i++) {
		u = p[i];
		if (b[u].r <= mid) p[++Mid] = u;
		else if (mid < b[u].l) s[++ncnt] = u;
		else {
			int ret = 0;
			for (int i = 0; i <= b[u].t; i++) {
				ret = max(ret, f[b[u].l][i] + f[b[u].r][b[u].t - i]);
			}
			ans[u] = ret;
		}
	}
	for (int i = 1; i <= ncnt; i++) {
		p[Mid + i] = s[i];
	}
	R = ncnt + Mid;
	solve(l, mid, L, Mid);
	solve(mid + 1, r, Mid + 1, R);
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for (register int i = 1; i <= n; i++) {
		cin >> h[i];
	}
	for (register int i = 1; i <= n; i++) {
		cin >> w[i];
	}
	for (register int i = 1; i <= m; i++) {
		cin >> b[i].l >> b[i].r >> b[i].t;
		if (b[i].l == b[i].r) {
			if (b[i].t >= h[b[i].l]) ans[i] = w[b[i].l];
		} else {
			p[++ncnt] = i;
		}
	}
	solve(1, n, 1, ncnt);
	for (int i = 1; i <= m; i++) {
		cout << ans[i] << endl;
	}
	return 0;
}

线段树分治

在区间上的操作,线段树好像都能干,并且它长得就很能分治,所以用它也并不奇怪;

例题:Luogu P5787 二分图 /【模板】线段树分治

看见这种在时间线上的题,一般好像可以用线段树分治来做;

以前好像还有一道,但是忘了;

首先判断二分图,我们使用可撤销的扩展域并查集,具体可以看看原题解区;

具体的,我们对于这一条时间线开一个线段树,每个节点开一个动态数组存这个点所管辖的时间段内所加边的下标,最后从根开始 dfs 一下即可;

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <stack>
using namespace std;
int n, m, k;
stack<pair<int, pair<int, int> > > s;
int fa[500005];
int u[500005], t[500005];
int siz[500005];
int find(int x) {
	return (x == fa[x]) ? x : find(fa[x]);
}
namespace seg{
	inline int ls(int x) {
		return x << 1;
	}
	inline int rs(int x) {
		return x << 1 | 1;
	}
	struct sss{
		int l, r;
		vector<int> v;
	}tr[500005];
	void bt(int id, int l, int r) {
		tr[id].l = l;
		tr[id].r = r;
		if (l == r) {
			return;
		}
		int mid = (l + r) >> 1;
		bt(ls(id), l, mid);
		bt(rs(id), mid + 1, r);
	}
	void add(int id, int l, int r, int d) {
		if (tr[id].l >= l && tr[id].r <= r) {
			tr[id].v.push_back(d);
			return;
		}
		int mid = (tr[id].l + tr[id].r) >> 1;
		if (l <= mid) add(ls(id), l, r, d);
		if (r > mid) add(rs(id), l, r, d);
	}
}
void merge(int x, int y) {
	if (x == y) return;
	if (siz[x] > siz[y]) swap(x, y);
	s.push({y, {siz[x], x}});
	fa[x] = y;
	siz[y] += siz[x];
}
void dfs(int id) {
	bool vis = true;
	int o = s.size();
	for (int i = 0; i < seg::tr[id].v.size(); i++) {
		int x = seg::tr[id].v[i];
		int uu = find(u[x]);
		int tt = find(t[x]);
		if (uu == tt) {
			for (int j = seg::tr[id].l; j <= seg::tr[id].r; j++) cout << "No" << endl;
			vis = false;
			break;
		}
		merge(find(u[x] + n), tt);
		merge(find(t[x] + n), uu);
	}
	if (vis) {
		if (seg::tr[id].l == seg::tr[id].r) {
			cout << "Yes" << endl;
		} else {
			dfs(seg::ls(id));
			dfs(seg::rs(id));
		}
	}
	while(s.size() > o) {
		siz[s.top().first] -= s.top().second.first;
		fa[s.top().second.second] = s.top().second.second;
		s.pop();
	}
}
int main() {
	cin >> n >> m >> k;
	seg::bt(1, 1, k);
	int l, r;
	for (int i = 1; i <= m; i++) {
		cin >> u[i] >> t[i] >> l >> r;
		if (l != r) {
			seg::add(1, l + 1, r, i);
		}
	}
	for (int i = 1; i <= 2 * n; i++) {
		fa[i] = i;
		siz[i] = 1;
	}
	dfs(1);
	return 0;
}

CDQ分治

CDQ的题,一般都能用线段树做,一个不行,那就树套树。 ---学长-Houraisan-Kaguya

确实,CDQ好像就是模拟了一下线段树的递归操作,但说实话,它确实比树套树好写;

应用范围

CDQ分治通常用来解决高维偏序问题;

一个 n 维偏序问题,是形如 a1b1 a2b2 ... anbn 的条件,让你找出满足条件的个数;

这时候,通常解法是树套树套树套。。。,每层树维护一个偏序关系,最后查询一下即可;

但毕竟大多数人不想写这样的数据结构,因为常数大,空间大,而且还难调。。。

所以,CDQ分治被发明出来了;

具体实现

来一道例题:Luogu P3810 【模板】三维偏序(陌上花开)

这是三维偏序;

首先,我们以 a 为第一关键字, b 为第二关键字, c 为第三关键字排序,这样我们其实就把它转换成了一个二维偏序问题了;

当然我们不能用正常的二位偏序的方法去做,因为现在排完序的顺序不能改变了,那应该怎么做呢?

考虑分治,因为我们发现,当我们把一段区间分成左右两个子区间时,左子区间的某些值可以对右子区间的某些值做出贡献;

这就是CDQ分治的思想;

具体地,我们分治时将左子区间和右子区间分别b 为第一关键字, c 为第二关键字排序,因为左子区间的 a 值一定是不严格小于右子区间的 a 值的,所以这样处理以后我们就可以拿两个指针顺着扫了;

这样一来,我们就把问题转成了一维偏序问题了,所以对于 c,我们只需开一个树状数组记录一下前缀和即可得到答案;

我们用指针 i 扫右子区间,用指针 j 扫左子区间,每次判断两个 c 的大小关系并把 j 所对应的 c 加入树状数组,最后直接统计答案即可;

注意最后要清空树状数组;

这样就做完了;

时间复杂度:T(n)=2T(n2)+Θ(nlogn)=Θ(nlog2n) ,空间复杂度:Θ(nlogn)

可以发现,和树套树的复杂度基本一样,但常数少很多;

又可以发现,对于一个 a 维偏序问题,CDQ分治是可以嵌套的,时间复杂度一般为 Θ(nloga1n) (但并不绝对,视情况而定);

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, k, m;
inline int lowbit(int x)  {
	return x & (-x);
}
struct sss{
	int a, b, c, ans, w;
}a[500005], b[500005];
int tr[500005];
int ans[500005];
void add(int pos, int d) {
	for (int i = pos; i <= k; i += lowbit(i)) tr[i] += d;
}
int ask(int pos) {
	int ans = 0;
	for (int i = pos; i; i -= lowbit(i)) ans += tr[i];
	return ans;
}
bool cmpa(sss x, sss y) {
	if (x.a == y.a) {
		if (x.b == y.b) {
			return x.c < y.c;
		} else {
			return x.b < y.b;
		}
	} else {
		return x.a < y.a;
	}
}
bool cmpb(sss x, sss y) {
	if (x.b == y.b) {
		return x.c < y.c;
	} else {
		return x.b < y.b;
	}
}
void cdq(int l, int r) {
	if (l == r) return;
	int mid = (l + r) >> 1;
	cdq(l, mid);
	cdq(mid + 1, r);
	sort(b + l, b + mid + 1, cmpb);
	sort(b + mid + 1, b + r + 1, cmpb);
	int i = mid + 1;
	int j = l;
	while(i <= r) {
		while(j <= mid && b[j].b <= b[i].b) {
			add(b[j].c, b[j].w);
			j++;
		}
		b[i].ans += ask(b[i].c);
		i++;
	}
	for (int i = l; i < j; i++) {
		add(b[i].c, -b[i].w);
	}
}
int main() {
	cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		cin >> a[i].a >> a[i].b >> a[i].c;
	}
	sort(a + 1, a + 1 + n, cmpa);
	int c = 0;
	for (int i = 1; i <= n; i++) {
		c++;
		if (a[i].a != a[i + 1].a || a[i].b != a[i + 1].b || a[i].c != a[i + 1].c) {
			b[++m] = a[i];
			b[m].w = c;
			c = 0;
		}
	}
	cdq(1, m);
	for (int i = 1; i <= m; i++) {
		ans[b[i].ans + b[i].w - 1] += b[i].w;
	}
	for (int i = 0; i < n; i++) {
		cout << ans[i] << endl;
	}
	return 0;
}

例题

  1. Luogu P4390 [BalkanOI2007] Mokia 摩基亚

把时间 t 看作第一个限制条件,x,y 看作第二个和第三个,每次只有都小于等于的修改操作才能产生贡献;

同时利用一下二维差分,把一次查询看成四个小查询,但要注意可能会减成 0,这样会导致树状数组死循环,所以所有输入的横纵坐标,以及 n 值都 +1 即可;

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int s, w, x, y, xx, yy;
inline int lowbit(int x) {
	return x & (-x);
}
struct sss{
	int t, x, y, w;
	bool s;
}e[500005];
int n;
bool cmpx(sss x, sss y) {
	if (x.x == y.x) return x.y < y.y;
	else return x.x < y.x;
}
bool cmp(sss x, sss y) {
	return x.t < y.t;
}
int tr[3000005];
void add(int pos, int d) {
	for (int i = pos; i <= w; i += lowbit(i)) tr[i] += d;
}
int ask(int pos) {
	int ans = 0;
	for (int i = pos; i; i -= lowbit(i)) ans += tr[i];
	return ans;
}
void cdq(int l, int r) {
	if (l == r) return;
	int mid = (l + r) >> 1;
	cdq(l, mid);
	cdq(mid + 1, r);
	sort(e + l, e + mid + 1, cmpx);
	sort(e + mid + 1, e + r + 1, cmpx);
	int i = mid + 1;
	int j = l;
	while(i <= r) {
		while(e[i].x >= e[j].x && j <= mid) {
			if (e[j].s == 0) add(e[j].y, e[j].w);
			j++;
		}
		if (e[i].s == 1) {
			e[i].w += ask(e[i].y);
		}
		i++;
	}
	for (int i = l; i < j; i++) {
		if (e[i].s == 0) add(e[i].y, -e[i].w);
	}
}
int main() {
	while(cin >> s) {
		if (s == 3) break;
		if (s == 0) {
			cin >> w;
			w++;
		}
		if (s == 1) {
			cin >> x >> y >> xx;
			x++;
			y++;
			e[++n] = {n, x, y, xx, 0};
		}
		if (s == 2) {
			cin >> x >> y >> xx >> yy;
			x++;
			y++;
			xx++;
			yy++;
			e[++n] = {n, xx, yy, 0, 1};
			e[++n] = {n, x - 1, y - 1, 0, 1};
			e[++n] = {n, xx, y - 1, 0, 1};
			e[++n] = {n, x - 1, yy, 0, 1};
		}
	}
	cdq(1, n);
	sort(e + 1, e + 1 + n, cmp);
	int i = 1;
	while(i <= n) {
		if (e[i].s == 1) {
			cout << e[i].w + e[i + 1].w - e[i + 2].w - e[i + 3].w << endl;
			i += 4;
		} else {
			i++;
		}
	}
	return 0;
}
  1. Luogu P4169 [Violet] 天使玩偶/SJY摆棋子

对于每一个询问 (x,y),我们要求:

min(|xxx|+|yyy|)

其中,(xx,yy) 代表已经有的一个点;

依据以往的套路,我们想着要去掉绝对值,于是对于每一个 (x,y),我们将现在有的 (xx,yy) 分成四种,为在 (x,y) 的左下方,左上方,右上方,右下方,分别用 o=1,o=2,o=3,o=4 表示;

那么我们要求的就变成了:

min{o=1  x+y(xx+yy)(xxx,yyy)o=2  xy(xxyy)(xxx,yyy)o=3  xy(xxyy)(xxx,yyy)o=4  yx(yyxx)(xxx,yyy)

当然,只有时间在 询问 (x,y) 之前的修改才有贡献;

那么这就是四个三维偏序问题,用四次CDQ求解即可;

当然,你也可以进行一些变换,使其变成三个相同条件(但也是要用四次CDQ);

注意每次CDQ前都要按时间重新排一次序

时间复杂度:Θ((n+m)log(n+m)logma),其中 ma 是输入横纵坐标的最大值;

但常数很大,注意卡常;

加了一点树状数组的剪枝

注意树状数组中 0 的情况!

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
#define FI(n) FastIO::read(n)
#define FO(n) FastIO::write(n)
#define Flush FastIO::Fflush()
namespace FastIO {
    const int SIZE=1<<16;
    char buf[SIZE],obuf[SIZE],str[60];
    int bi=SIZE,bn=SIZE,opt;
    inline int read(register char *s) {
        while(bn){
            for(;bi<bn&&buf[bi]<=' ';bi=-~bi);
            if(bi<bn)break;
            bn=fread(buf,1,SIZE,stdin);
            bi&=0;
        }
        register int sn=0;
        while(bn){
            for(;bi<bn&&buf[bi]>' ';bi=-~bi)s[sn++]=buf[bi];
            if(bi<bn)break;
            bn=fread(buf,1,SIZE,stdin);
            bi&=0;
        }
        s[sn]&=0;
        return sn;
    }
    inline bool read(register int &x){
        int n=read(str),bf=0;
        if(!n)return 0;
        register int i=0;
        (str[i]=='-')&&(bf=1,i=-~i);
		(str[i]=='+')&&(i=-~i);
        for(x=0;i<n;i=-~i)x=(x<<3)+(x<<1)+(str[i]^48);
        bf&&(x=~x+1);
        return 1;
    }
    inline bool read(register long long &x) {
        int n=read(str),bf=1;
        if(!n)return 0;
        register int i=0;
        (str[i]=='-')&&(bf=-1,i=-~i);
        for(x=0;i<n;i=-~i)x=(x<<3)+(x<<1)+(str[i]^48);
        (bf<0)&&(x=~x+1);
        return 1;
    }
    inline void write(register int x) {
        if(!x)obuf[opt++]='0';
        else{
            (x<0)&&(obuf[opt++]='-',x=~x+1);
            register int sn=0;
            while(x)str[sn++]=x%10+'0',x/=10;
            for(register int i=sn-1;i>=0;i=~-i)obuf[opt++]=str[i];
        }
        (opt>=(SIZE>>1))&&(fwrite(obuf,1,opt,stdout),opt&=0);
    }
    inline void write(register long long x) {
        if(!x)obuf[opt++]='0';
        else{
            (x<0)&&(obuf[opt++]='-',x=~x+1);
            register int sn=0;
            while(x)str[sn++]=x%10+'0',x/=10;
            for(register int i=sn-1;i>=0;i=~-i)obuf[opt++]=str[i];
        }
        (opt>=(SIZE>>1))&&(fwrite(obuf,1,opt,stdout),opt&=0);
    }
    inline void write(register unsigned long long x){
        if(!x)obuf[opt++]='0';
        else{
            register int sn=0;
            while(x)str[sn++]=x%10+'0',x/=10;
            for(register int i=sn-1;i>=0;i=~-i)obuf[opt++]=str[i];
        }
        (opt>=(SIZE>>1))&&(fwrite(obuf,1,opt,stdout),opt&=0);
    }
    inline void write(register char x) {
        obuf[opt++]=x;
        (opt>=(SIZE>>1))&&(fwrite(obuf,1,opt,stdout),opt&=0);
    }
    inline void Fflush(){
        opt&&fwrite(obuf,1,opt,stdout);
        opt&=0;
    }
};
int n, m;
int ma;
namespace BIT{
	int tr[2000005];
	inline int lowbit(int x) {
		return x & (-x);
	}
	inline void clear() {
		for (int i = 1; i <= ma + 1; i++) tr[i] = -0x3f3f3f3f;
	}
	void add(int s, int pos, int d) {
		pos++;
		if (s == 1) for (register int i = pos; i <= ma + 1; i += lowbit(i)) {
			if (tr[i] >= d && d != -0x3f3f3f3f) return; 
			else tr[i] = (d == -0x3f3f3f3f ? d : max(tr[i], d));
		} else {
			for (register int i = pos; i; i -= lowbit(i)){
				if (tr[i] >= d && d != -0x3f3f3f3f) return;
				else tr[i] = (d == -0x3f3f3f3f ? d : max(tr[i], d));
			}
		}
	}
	int ask(int s, int d) {
		d++;
		int ans = -0x3f3f3f3f;
		if (s == 1) for (register int i = d; i; i -= lowbit(i)) ans = max(ans, tr[i]);
		else for (register int i = d; i <= ma + 1; i += lowbit(i)) ans = max(ans, tr[i]);
		return ans;
	}
}
using namespace BIT;
int tim, cnt;
struct sss{
	int t, x, y, s, ans;
}e[5000005], t[5000005];
bool cmpx(sss x, sss y) {
	if (x.x == y.x) return x.y < y.y;
	else return x.x < y.x;
}
bool cmpxx(sss x, sss y) {
	if (x.x == y.x) return x.y > y.y;
	else return x.x > y.x;
}
bool cmp(sss x, sss y) {
	return x.t < y.t;
}
void cdq(int o, int l, int r) {
	if (l == r) return;
	int mid = (l + r) >> 1;
	cdq(o, l, mid);
	cdq(o, mid + 1, r);
	int i = mid + 1;
	int j = l;
	while(i <= r) {
		if (o <= 2) {
			while(j <= mid && e[j].x <= e[i].x) {
				if (e[j].s == 1) {
					if (o == 1) {
						add(1, e[j].y, e[j].x + e[j].y);
					} else if (o == 2) {
						add(2, e[j].y, e[j].x - e[j].y);
					}
				}
				j++;
			}
		} else {
			while(j <= mid && e[j].x >= e[i].x) {
				if (e[j].s == 1) {
					if (o == 3) {
						add(2, e[j].y, -(e[j].x + e[j].y));
					} else if (o == 4) {
						add(1, e[j].y, e[j].y - e[j].x);
					}
				}
				j++;
			}
		}
		if (e[i].s == 2) {
			if (o == 1) {
				e[i].ans = min(e[i].ans, e[i].x + e[i].y - ask(1, e[i].y));
			} else if (o == 2) {
				e[i].ans = min(e[i].ans, e[i].x - e[i].y - ask(2, e[i].y));
			} else if (o == 3) {
				e[i].ans = min(e[i].ans, -e[i].x - e[i].y - ask(2, e[i].y));
			} else if (o == 4) {
				e[i].ans = min(e[i].ans, e[i].y - e[i].x - ask(1, e[i].y));
			}
		}
		i++;
	}
	for (register int i = l; i < j; i++) {
		if (o == 1 || o == 4) add(1, e[i].y, -0x3f3f3f3f);
		else add(2, e[i].y, -0x3f3f3f3f);
	}
	i = mid + 1;
	j = l;
	int k = l - 1;
	if (o <= 2) {
		while(i <= r) {
			while(j <= mid && cmpx(e[j], e[i])) {
				t[++k] = e[j];
				j++;
			}
			t[++k] = e[i];
			i++;
		}
	} else {
		while(i <= r) {
			while(j <= mid && cmpxx(e[j], e[i])) {
				t[++k] = e[j];
				j++;
			}
			t[++k] = e[i];
			i++;
		}
	}
	while(i <= r) {
		t[++k] = e[i];
		i++;
	}
	while(j <= mid) {
		t[++k] = e[j];
		j++;
	}
	for (register int i = l; i <= r; i++) e[i] = t[i];
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	FI(n);
	FI(m);
	int x, y;
	for (register int i = 1; i <= n; i++) {
		tim++;
		FI(x);
		FI(y);
		e[++cnt] = {tim, x, y, 1, 0x3f3f3f3f};
		ma = max(ma, y);
	}
	int ss;
	for (register int i = 1; i <= m; i++) {
		FI(ss);
		FI(x);
		FI(y);
		ma = max(ma, y);
		tim++;
		e[++cnt] = {tim, x, y, ss, 0x3f3f3f3f};
	}
	clear();
	cdq(1, 1, cnt);
	sort(e + 1, e + 1 + cnt, cmp);
	clear();
	cdq(2, 1, cnt);
	sort(e + 1, e + 1 + cnt, cmp);
	clear();
	cdq(3, 1, cnt);
	sort(e + 1, e + 1 + cnt, cmp);
	clear();
	cdq(4, 1, cnt);
	sort(e + 1, e + 1 + cnt, cmp);
	for (register int i = 1; i <= cnt; i++) {
		if (e[i].s == 2) {
			FO(e[i].ans);
			FO('\n');
		}
	}
	Flush;
	return 0;
}
  1. Luogu P3157 [CQOI2011] 动态逆序对

之前在CSDN上看见一篇博客说划分树能解决动态逆序对,但他既没给讲解,也没给实现,我也没找到用划分树解决的题解,貌似不行吧(毕竟划分树是静态的。。。);

考虑能产生贡献的一对点对 (i,j) 需要满足的条件为:

ti<tj,posi<posj,vali>valj

或:

ti<tj,posi>posj,vali<valj

此时 ij 产生贡献;

我们将初始序列看作全 0 的,初始值看作插入,删除操作就是删除,那么前者作正贡献,后者作负贡献;

跑两遍三维偏序即可,时间复杂度:Θ(nlog2n)

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, m;
struct sss{
	long long t, pos, val, s, id;
}e[500005], t[500005];
long long pos[500005];
long long ans[500005];
long long cnt, tim;
namespace BIT{
	long long tr[500005];
	inline long long lowbit(long long x) {
		return x & (-x);
	}
	void add(long long p, long long d) {
		for (long long i = p; i <= n; i += lowbit(i)) tr[i] += d;
	}
	long long ask(long long p) {
		long long ans = 0;
		for (long long i = p; i; i -= lowbit(i)) ans += tr[i];
		return ans;
	}
}
using namespace BIT;
void cdq1(long long l, long long r) {
	if (l == r) return;
	long long mid = (l + r) >> 1;
	cdq1(l, mid);
	cdq1(mid + 1, r);
	long long i = mid + 1;
	long long j = l;
	while(i <= r) {
		while(j <= mid && e[j].pos <= e[i].pos) {
			add(e[j].val, e[j].s);
			j++;
		}
		ans[e[i].id] += e[i].s * (ask(n) - ask(e[i].val));
		i++;
	}
	for (long long i = l; i < j; i++) {
		add(e[i].val, -e[i].s);
	}
	i = mid + 1;
	j = l;
	long long k = l - 1;
	while(i <= r) {
		while(j <= mid && e[j].pos <= e[i].pos) {
			t[++k] = e[j];
			j++;
		}
		t[++k] = e[i];
		i++;
	}
	while(i <= r) {
		t[++k] = e[i];
		i++;
	}
	while(j <= mid) {
		t[++k] = e[j];
		j++;
	}
	for (long long i = l; i <= r; i++) {
		e[i] = t[i];
	}
}
void cdq2(long long l, long long r) {
	if (l == r) return;
	long long mid = (l + r) >> 1;
	cdq2(l, mid);
	cdq2(mid + 1, r);
	long long i = mid + 1;
	long long j = l;
	while(i <= r) {
		while(j <= mid && e[j].pos >= e[i].pos) {
			add(e[j].val, e[j].s);
			j++;
		}
		ans[e[i].id] += e[i].s * ask(e[i].val - 1);
		i++;
	}
	for (long long i = l; i < j; i++) {
		add(e[i].val, -e[i].s);
	}
	i = mid + 1;
	j = l;
	long long k = l - 1;
	while(i <= r) {
		while(j <= mid && e[j].pos >= e[i].pos) {
			t[++k] = e[j];
			j++;
		}
		t[++k] = e[i];
		i++;
	}
	while(i <= r) {
		t[++k] = e[i];
		i++;
	}
	while(j <= mid) {
		t[++k] = e[j];
		j++;
	}
	for (long long i = l; i <= r; i++) {
		e[i] = t[i];
	}
}
inline bool cmp(sss x, sss y) {
	return x.t < y.t;
}
int main() {
	cin >> n >> m;
	long long x;
	for (long long i = 1; i <= n; i++) {
		cin >> x;
		pos[x] = i;
		e[++cnt] = {++tim, i, x, 1, 0};
	}
	for (long long i = 1; i <= m; i++) {
		cin >> x;
		e[++cnt] = {++tim, pos[x], x, -1, i};
	}
	cdq1(1, cnt);
	sort(e + 1, e + 1 + cnt, cmp);
	cdq2(1, cnt);
	long long an = 0;
	for (long long i = 0; i < m; i++) {
		an += ans[i];
		cout << an << endl;
	}
	return 0;
}
  1. Luogu P4093 [HEOI2016/TJOI2016] 序列

注意:变换序列可以是原序列

这道题有一个新的套路:用CDQ搞DP;

我们设 mai 表示 i 这个位置能有的最大值, mii 表示能有的最小值, ai 表示原值,fi 表示以 i 结尾的最长长度,那么有状态转移方程:

fi=maxj[0,i)fj+1

其中要满足:

aimaj

ajmii

(因为只能有一个变的,且要所有可能的序列都满足,所以使条件最苛刻即可);

我们把转移顺序看作第一维,和剩下两维构成了一个三维偏序问题,所以用CDQ搞它即可;

注意这种题和普通的CDQ不同,我们分治时必须要把左边区间全部更新完在更新右边区间,并且每次回溯时要按转移顺序重新排序,因为回溯的时候还要继续递归;

这里应该就不能用归并排序了,因为没有统一的 cmp,所以老老实实的用 sort 吧(常数大了点,不过这道题没有关系);

具体细节看代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n, m;
struct sss{
	int a, mi, ma, id;
}e[500005];
inline bool cmp1(sss x, sss y) {
	return x.a < y.a;
}
inline bool cmp2(sss x, sss y) {
	return x.ma < y.ma;
}
inline bool cmp3(sss x, sss y) {
	return x.id < y.id;
}
int f[500005];
int ans;
namespace BIT{
	int tr[500005];
	inline int lowbit(int x) {
		return x & (-x);
	}
	void add(int pos, int d) {
		for (int i = pos; i <= 100000; i += lowbit(i)) {
			if (d == 0) tr[i] = d;
			else tr[i] = max(tr[i], d);
		}
	}
	int ask(int pos) {
		int an = 0;
		for (int i = pos; i; i -= lowbit(i)) {
			an = max(an, tr[i]);
		}
		return an;
	}
}
using namespace BIT;
void cdq(int l, int r) {
	if (l == r) {
		f[l] = max(f[l], 1);
		return;
	}
	int mid = (l + r) >> 1;
	cdq(l, mid);
	sort(e + l, e + mid + 1, cmp2);
	sort(e + mid + 1, e + r + 1, cmp1);
	int i = mid + 1;
	int j = l;
	while(i <= r) {
		while(j <= mid && e[i].a >= e[j].ma) {
			add(e[j].a, f[e[j].id]);
			j++;
		}
		f[e[i].id] = max(f[e[i].id], ask(e[i].mi) + 1);
		i++;
	}
	for (int i = l; i < j; i++) {
		add(e[i].a, 0);
	}
	sort(e + l, e + r + 1, cmp3); //注意排序;
	cdq(mid + 1, r); //先把左区间处理完再处理右区间;
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> e[i].a;
		e[i].mi = e[i].a;
		e[i].ma = e[i].a;
		e[i].id = i;
	}
	int x, y;
	for (int i = 1; i <= m; i++) {
		cin >> x >> y;
		e[x].mi = min(e[x].mi, y);
		e[x].ma = max(e[x].ma, y);
	}
	cdq(1, n);
	for (int i = 1; i <= n; i++) ans = max(ans, f[i]);
	cout << ans;
	return 0;
}

划分树

介绍

划分树,一种数据结构,和线段树很像,常用来解决求区间第 k 小的问题,不支持修改,时间复杂度:建树 Θ(nlogn) + 单次查询 Θ(logn),空间复杂度 Θ(nlogn),在这种问题及其扩展问题上具有优良的性能,但其它问题就凸显出其局限性;

思想

划分树主体思想是快排 + 线段树,可以说把它俩揉一块就成了划分树;

类似线段树,划分树也是每次将区间分成左右两个子区间,这样就方便了我们递归求解;

划分树,顾名思义,就是把原序列不断划分的一种数据结构,对于其需要解决的求区间第 k 小的问题,它的解决方法是:对于每个树上的节点,存储其管辖范围内(不妨设为 [L,R])所有的元素,这些元素的特点是:顺序和原数组的输入顺序相同,但这些元素都出现在,且只能出现在原数组排好序后的 [L,R] 中(就是第 L 小到第 R 小);

这样就保证了不会更改原序列的位置,从而方便查询;

具体实现

需要维护的东西:

设当前节点所管辖的范围是 [L,R]mid=L+R2

  1. tr[lev][i] :用于存储树的结构,第一维是级数(也就是现在递归到的层数),从 0 开始,第二维保存了当前层的元素(和上面说的一样),特殊的,当层数为 0 时,保存的是原序列(未排序的);

  2. tole[lev][i] :表示在 [L,i] 这段区间中,被分到左子区间的数有多少(这是为了查询而维护的)。可以发现,这本质上是一个 DP 数组,可以用 DP 的思想去维护一下;

  3. a[i] :表示原数组排好序的数组;

以这道题为例:POJ 2761 Feed the dogs

也可以从 Luogu 上找到(链接 from hzoi_ShadowLuogu P1533 可怜的狗狗

但貌似Luogu的题解区里很少有划分树的做法呢

求区间第 k 小;

首先,进行输入与排序;

cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> tr[0][i];
		a[i] = tr[0][i];
	}
	sort(a + 1, a + 1 + n);

然后是建树操作;

对于建树,根据前面的思想,我们要让左子区间的所有数非严格小于右子区间的所有数,显然我们要让小于中位数的数去左区间,大于中位数的数去右区间,对于中位数本身,我们根据左子区间的区间长度以及现有的数的个数分类讨论;

怎样找中位数呢?

别忘了我们有一个排好序的数组 a,那么中位数其实就是 a[mid]

有了这些,建树就好办了;

对于具体过程,我们可以从左到右扫一遍整个区间,如有小于中位数的,下放到左子区间,同时更新 tole 数组(tole[lev][i]++) ,大于中位数的,下放到右子区间,等于中位数的,判断一下左子区间还有没有位置,如有,则放到左子区间,否则放到右子区间;

最后如果到叶子节点,回溯即可;

完事,时间复杂度 T(n)=2T(n2)+Θ(n)=Θ(nlogn) (貌似是这么分析,今天刚跟学长学的。。。)

建树部分的代码:

void bt(int lev, int l, int r) {
	if (l == r) return;
	int mid = (l + r) >> 1;
	int midl = mid - l + 1; //代表现在左子区间还有多少空位置能放数,初始化为左子区间长度;
	for (int i = l; i <= r; i++) {
		if (tr[lev][i] < a[mid]) midl--;
	}
	int subl = l;
	int subr = mid + 1;
	for (int i = l; i <= r; i++) {
		if (i == l) tole[lev][i] = 0;
		else tole[lev][i] = tole[lev][i - 1]; //继承上一步的结果;
		if (tr[lev][i] < a[mid] || tr[lev][i] == a[mid] && midl > 0) {
			tr[lev + 1][subl++] = tr[lev][i]; //给左子区间;
			tole[lev][i]++;
			if (tr[lev][i] == a[mid]) midl--;
		} else {
			tr[lev + 1][subr++] = tr[lev][i]; //给右子区间;
		}
	}
	bt(lev + 1, l, mid);
	bt(lev + 1, mid + 1, r);
}

然后是查询操作;

设查询的区间为 [l,r]

对于查询,采用递归,如果 k 在左子区间,则递归左子区间,否则递归右子区间,最后到叶子节点返回答案;

这时候,我们维护的 tole 数组就派上用场了;

具体地,在当前节点时,我们要判断下一步到底是递归左边还是右边,那么我们就判断 tole[lev][r]tole[lev][l1] (其实就是 [l,r] 中被分到左子区间的元素个数,不妨记为 tolef)与 k 的大小关系,若后者比前者大,则递归右区间,否则递归左区间;

明确了递归区间后,我们要考虑询问区间和 k 的值是否会改变;

  1. 递归到了左子区间;

[L,l1] 中放到左子区间的数的个数为 lef,其值为 tole[lev][l1],那么现在我们的询问区间就变成了:

左端点:为 L+lef

右端点:为 L+lef+tolef1

k 不变,直接递归即可;

  1. 递归到了右子区间;

leftolef 的定义不变,那么现在我们的询问区间就变成了:

左端点:为 mid+1 + [L,l1] 中进入右子区间的数的个数;

[L,l1] 中进入右子区间的数的个数为这段区间的长度 - 进入左子区间的数的个数,即为 l1L+1lef=lLlef

所以左端点即为:mid+lLlef+1

右端点:为 mid+1 + [L,r] 中进入右子区间的数的个数 - 1

[L,r] 中进入右子区间的数的个数为这段区间的长度 - 进入左子区间的数的个数,即为 rL+1leftolef

所以右端点即为:mid+rLleftolef+1

此时 k 变成了 ktolef,然后递归即可;

这样,查询就完事了;

int ask(int lev, int l, int r, int L, int R, int k) { //定义和上面写的一样;
	if (l == r) return tr[lev][l];
	int mid = (L + R) >> 1;
	int lef, tolef;
	if (l == L) {
		lef = 0;
		tolef = tole[lev][r];
	} else {
		lef = tole[lev][l - 1];
		tolef = tole[lev][r] - lef;
	}
	if (k <= tolef) {
		int newl = L + lef;
		int newr = L + lef + tolef - 1;
		return ask(lev + 1, newl, newr, L, mid, k);
	} else {
		int newl = mid + l - L - lef + 1;
		int newr = mid + r - L + 1 - lef - tolef;
		return ask(lev + 1, newl, newr, mid + 1, R, k - tolef);
	}
}

解决这道题的代码:

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, m;
int tr[35][200005];
int tole[35][200005];
int a[200005];
namespace divtr{
	void bt(int lev, int l, int r) {
		if (l == r) return;
		int mid = (l + r) >> 1;
		int midl = mid - l + 1;
		for (int i = l; i <= r; i++) {
			if (tr[lev][i] < a[mid]) midl--;
		}
		int subl = l;
		int subr = mid + 1;
		for (int i = l; i <= r; i++) {
			if (i == l) tole[lev][i] = 0;
			else tole[lev][i] = tole[lev][i - 1];
			if (tr[lev][i] < a[mid] || tr[lev][i] == a[mid] && midl > 0) {
				tr[lev + 1][subl++] = tr[lev][i];
				tole[lev][i]++;
				if (tr[lev][i] == a[mid]) midl--;
			} else {
				tr[lev + 1][subr++] = tr[lev][i];
			}
		}
		bt(lev + 1, l, mid);
		bt(lev + 1, mid + 1, r);
	}
	int ask(int lev, int l, int r, int L, int R, int k) {
		if (l == r) return tr[lev][l];
		int mid = (L + R) >> 1;
		int lef, tolef;
		if (l == L) {
			lef = 0;
			tolef = tole[lev][r];
		} else {
			lef = tole[lev][l - 1];
			tolef = tole[lev][r] - lef;
		}
		if (k <= tolef) {
			int newl = L + lef;
			int newr = L + lef + tolef - 1;
			return ask(lev + 1, newl, newr, L, mid, k);
		} else {
			int newl = mid + l - L - lef + 1;
			int newr = mid + r - L + 1 - lef - tolef;
			return ask(lev + 1, newl, newr, mid + 1, R, k - tolef);
		}
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> tr[0][i];
		a[i] = tr[0][i];
	}
	sort(a + 1, a + 1 + n);
	divtr::bt(0, 1, n);
	int l, r, k;
	for (int i = 1; i <= m; i++) {
		cin >> l >> r >> k;
		cout << divtr::ask(0, l, r, 1, n, k) << endl;
	}
	return 0;
}

感觉这东西貌似没啥用,但还是学了。。。

珂朵莉树

介绍

珂朵莉树,学名珂朵莉树,又学名老司机树( ODT ),常用来解决“区间推平”(把区间改为某一个数)等的操作,只适用于随机数据,可以定向卡掉;

同机房dalao说:

如果只有区间推平的操作,就不用保证数据随机。 ——int_R

其实这玩意就是暴力,没啥可说的,分块都比不上她暴力;

但人家毕竟是,所以对于一些题还是有用的;

对于复杂度证明,详见https://www.luogu.com.cn/blog/blaze/solution-cf896c

实现

只要你会 C++ STL 里的 set,搞定她完全不是问题;

当然,你还需要一些基本的指针操作;

主要思想:维护一段序列的所有连续的子区间(这里的“连续”指的是值连续);

对于 set 里的每一个元素,其为三元组,记录 l,r,v,分别代表本段连续区间的左端点,右端点,值;

当然,set 中的元素是按 l 升序排列的;

声明:

struct sss{
	long long l, r;
	mutable long long v;
	bool operator <(const sss &A) const {
		return l < A.l;
	}
};

注:mutable 其实是和 const 相对应的,使用 mutable 可以使变量 v 在一个常量结构体下仍然可以改变;

mutable 具体需要在区间加等操作(见下文的的操作三)中用;

操作一 spilt

分裂操作,即指定一个 pos,将整个序列分成两部分;

显然,最多只会对一个子区间进行操作;

借助 set 中的 lower bound 函数,我们可以快速查找出 pos 在哪个子区间里,此时会有这么几种情况:

  1. 此区间的 l=pos

直接返回此区间即可;

  1. 此区间的上一个区间的 r<pos

我们可以发现,此时 pos 太大了,已经超出了区间长度,所以直接返回迭代器即可;

  1. pos 在此区间的上一个区间中;

把此区间的上一个区间拆分成 l,pos1,vpos,r,v 两个区间即可;

一个技巧:setinsert 函数有一个返回值,是 pair 类型的,first 返回插入元素的迭代器,second 返回这个元素是否被插入(因为 set 不可重);

set<sss>::iterator spilt(long long pos) {
	set<sss>::iterator it = s.lower_bound(sss{pos, 0, 0});
	if (it != s.end() && it -> l == pos) return it;
	it--;
	if (it -> r < pos) return s.end();
	long long l = it -> l;
	long long r = it -> r;
	long long v = it -> v;
	s.erase(it);
	s.insert(sss{l, pos - 1, v});
	return s.insert(sss{pos, r, v}).first;
}

注:it>l 等价于 it.l

操作二 assign

区间推平操作,设要修改的区间为 [l,r],我们可以先 spilt(r+1),再 spilt(l),(顺序不能颠倒,不然会 RE),最后删除这两步操作所对应的迭代器之间的所有元素(子区间),并插入要修改的元素(区间)即可;

void assign(long long l, long long r, long long x) {
	set<sss>::iterator rit = spilt(r + 1), lit = spilt(l);
	s.erase(lit, rit);
	s.insert(sss{l, r, x});
}

注:s.erase(a,b) 指的是删除 [a,b) 之间的元素;

操作三 其它暴力操作

几乎所有区间上的操作,只要你会暴力,就能用她做;

对于一段要操作的区间 [l,r],直接暴力遍历其中所有的元素,在进行操作即可;

例题:CodeForces 896C

对于第三个操作,发现用线段树不太好做,又发现数据随机,所以直接珂朵莉树搞它就行;

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <set>
#include <vector>
using namespace std;
long long n, m, seed, vmax;
long long a[1000005];
struct sss{
	long long l, r;
	mutable long long v;
	bool operator <(const sss &A) const {
		return l < A.l;
	}
};
set<sss> s;
set<sss>::iterator spilt(long long pos) {
	set<sss>::iterator it = s.lower_bound(sss{pos, 0, 0});
	if (it != s.end() && it -> l == pos) return it;
	it--;
	if (it -> r < pos) return s.end();
	long long l = it -> l;
	long long r = it -> r;
	long long v = it -> v;
	s.erase(it);
	s.insert(sss{l, pos - 1, v});
	return s.insert(sss{pos, r, v}).first;
}
void assign(long long l, long long r, long long x) {
	set<sss>::iterator rit = spilt(r + 1), lit = spilt(l);
	s.erase(lit, rit);
	s.insert(sss{l, r, x});
}
long long rnd() {
	long long ret = seed;
	seed = (seed * 7 + 13) % 1000000007;
	return ret;
}
void add(long long l, long long r, long long d) {
	set<sss>::iterator rit = spilt(r + 1), lit = spilt(l);
	for (set<sss>::iterator it = lit; it != rit; it++) {
		it -> v += d; //mutable用处
	}
}
vector<pair<long long, long long> > v;
long long ask_kth(long long l, long long r, long long k) {
	set<sss>::iterator rit = spilt(r + 1), lit = spilt(l);
	v.clear();
	for (set<sss>::iterator it = lit; it != rit; it++) {
		v.push_back({it -> v, it -> r - it -> l + 1});
	}
	sort(v.begin(), v.end());
	long long o = 0;
	while(1) {
		k -= v[o].second;
		if (k <= 0) return v[o].first;
		o++;
	}
}
long long ksm(long long a, long long b, long long c) {
	long long ans = 1;
	while(b) {
		if (b & 1) ans = ans * a % c;
		a = a * a % c;
		b >>= 1;
	}
	return ans;
}
long long mo(long long l, long long r, long long x, long long y) {
	set<sss>::iterator rit = spilt(r + 1), lit = spilt(l);
	long long ans = 0;
	for (set<sss>::iterator it = lit; it != rit; it++) {
		ans = (ans + ksm(it -> v % y, x, y) * (it -> r - it -> l + 1) % y) % y;
	}
	return ans;
}
int main() {
	cin >> n >> m >> seed >> vmax;
	for (long long i = 1; i <= n; i++) {
		a[i] = (rnd() % vmax) + 1;
		s.insert(sss{i, i, a[i]});
	}
    for (long long i = 1; i <= m; ++i) {
        long long op, l, r, x, y;
        op = (rnd() % 4) + 1;
        l = (rnd() % n) + 1;
        r = (rnd() % n) + 1;
        if (l > r) swap(l, r);
        if (op == 3) {
            x = (rnd() % (r - l + 1)) + 1;
        } else {
            x = (rnd() % vmax) + 1;
        }
        if (op == 4) {
            y = (rnd() % vmax) + 1;
        }
        if (op == 1) {
        	add(l, r, x);
        } else if (op == 2) {
			assign(l, r, x);
        } else if (op == 3) {
        	cout << ask_kth(l, r, x) << endl;
        } else if (op == 4) {
        	cout << mo(l, r, x, y) << endl;
        }
    }
	return 0;
}

并查集

没想到吧我来写并查集了。。。

普通的不在赘述,这里主要有两个变种;

扩展域并查集

适用范围:能够形成信息闭环,同时又有几个互相矛盾的信息;

一般会用扩展域并查集,即多开几倍空间来处理信息;

例题:Luogu P2024 [NOI2001] 食物链

比较套路的一道题;

考虑开三倍空间,对于 xy 的信息,我们合并 (x,y+n) (x+n,y+2n) (x+2n,y),这样就可以了;

发现这样做的好处是满足环的要求;

然后考虑如何判断假话,正难则反,对于同类的情况,相当于 x 不吃 y,且 y 不吃 x。对于 xy 的情况,相当于 xy 不是同类,且 y 不吃 x

这样就做完了;

如果需要维护的量变多了的话,我们直接多开几倍空间即可;

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;
int n, k;
int fa[500005];
int find(int x) {
	if (x != fa[x]) fa[x] = find(fa[x]);
	return fa[x];
}
int ans;
int main() {
// 	freopen("in.in", "r", stdin);
// 	freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> k;
	for (int i = 1; i <= 3 * n; i++) fa[i] = i;
	int s, x, y;
	for (int i = 1; i <= k; i++) {
		cin >> s >> x >> y;
		if (x > n || y > n || (x == y && s == 2)) {
			ans++;
			continue;
		}
		if (s == 1) {
			if (x == y) continue;
			if (find(x) == find(y + n) || find(y) == find(x + n)) {
				ans++;
				continue;
			}
			fa[find(x)] = find(y);
			fa[find(x + n)] = find(y + n);
			fa[find(x + 2 * n)] = find(y + 2 * n);
		}
		if (s == 2) {
			if (find(x) == find(y) || find(y) == find(x + n)) {
				ans++;
				continue;
			}
			fa[find(x)] = find(y + n);
			fa[find(x + n)] = find(y + 2 * n);
			fa[find(x + 2 * n)] = find(y);
		}
	}
	cout << ans;
	return 0;
}

边带权并查集

在每次 find 操作时直接更新它到根的信息;

例题:P1196 [NOI2002] 银河英雄传说

维护每个点到这个连通块的根的距离,连通块可以并查集实现,维护距离可以用边带权实现;

对于后者,具体地,我们记一个 sumx 表示以 x 为根的连通块的 siz, 记 disx 表示 x 到这个连通块根的距离, 在 find 函数回溯的时候更新距离为 disx+=disfax,然后合并的时候更新 disx=sumy 即可;

时间复杂度:Θ(nlogn)

Vjudge 专题

挺多挺杂,一块写了;

DP

  1. CF510D Fox And Jumping

小清新DP,清新在是用 map 写的;

可以整出一个状态 fi 表示最大公约数为 i 时的最小花费,转移有 fgcd(i,j)=min(fi+fj,fgcd(i,j))

如果直接枚举,需要枚举值域,但是我们发现最多状态数不超过 Θ(29×n) 个(因为九个质数相乘已经超过值域),所以可以开一个 map 记录一下所有状态数,然后直接遍历map即可;

遍历 map 可以类比遍历 set,开一个迭代器,类型是 pair,第一个是key,第二个是value,然后转移即可;

时间复杂度:Θ(29×n2),这个套路还是挺有用的,至少模拟赛的较高档的暴力出过;

点击查看代码
#include <iostream>
#include <cstdio>
#include <map>
#include <algorithm>
using namespace std;
int n;
map<int, int> f;
int a[305], b[305];
int main() {
	// freopen("in.in", "r", stdin);
	// freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	for (int i = 1; i <= n; i++) {
		cin >> b[i];
	}
	for (int i = 1; i <= n; i++) {
		if (!f[a[i]]) f[a[i]] = b[i];
		else f[a[i]] = min(f[a[i]], b[i]);
		for (map<int, int>::iterator it = f.begin(); it != f.end(); it++) {
			if (!f[__gcd(a[i], it -> first)]) f[__gcd(a[i], it -> first)] = b[i] + it -> second;
			else f[__gcd(a[i], it -> first)] = min(f[__gcd(a[i], it -> first)], b[i] + it -> second);
		}
	}
	if (!f[1]) cout << -1;
	else cout << f[1];
	return 0;
}
  1. CF459E Pashmak and Graph

这个题一看可能没有什么思路,但是我们考虑一下题目中给的限制,如果我们将边按边权从小到大排序,那么这个题目求的就是联通的最长上升子序列;

朴素实现是 Θ(n2) 的,但是我们发现,对于一条边,我们只关心它左端点或是右端点的最大值,所以用一个数组存一下即可,时间复杂度:Θ(n)

还需要注意的一点是我们要求严格单调递增的序列,所以要将相同边权的先处理完再更新最大值;

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, m;
struct sss{
	int x, y, w;
}e[500005];
bool cmpw(sss x, sss y) {
	return x.w < y.w;
}
int f[500005];
int ma[500005];
int main() {
	// freopen("in.in", "r", stdin);
	// freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		cin >> e[i].x >> e[i].y >> e[i].w;
	}
	sort(e + 1, e + 1 + m, cmpw);
	int ans = 0;
	int ls = e[1].w;
	int l = 1;
	for (int i = 1; i <= m; i++) {
		if (e[i].w != ls) {
			for (int j = l; j < i; j++) {
				ma[e[j].y] = max(ma[e[j].y], f[j]);
			}
			l = i;
			ls = e[i].w;
		}
		f[i] = ma[e[i].x] + 1;
		ans = max(ans, f[i]);
	}
	cout << ans;
	return 0;
}
  1. CF810E Find a car

这个题比较好,考虑寻找一些规律;

然后就找不出来规律,然后就看了题解

引理ai,j=(i1)(j1)+1

这个打打表吧;

那么这样我们发现,最后求的答案可以用数位DP解决;

考虑设 fx,0/1,0/1,0/1 表示现在考虑到了第 x 位,i,j,ij 是否被限制的方案数;

直接转移即可;

考虑如何求最终的答案,我们用一下二维差分,将要求的问题转化成四个子问题,分别求解即可;

时间复杂度:Θ(qlogmax(x2,y2,k))

点击查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const long long mod = 1e9 + 7;
int t;
long long x1, y1, x2, y2, k;
long long f[35][2][2][2], g[35][2][2][2];
long long p[55];
long long ans;
int x[35], xcnt, y[35], ycnt, z[35], zcnt, ma;
pair<long long, long long> dfs(int now, bool lix, bool liy, bool liz) {
	if (now > ma) return {1, 0};
	if (f[now][lix][liy][liz]) return {f[now][lix][liy][liz], g[now][lix][liy][liz]};
	int resx = (lix ? x[now] : 1);
	int resy = (liy ? y[now] : 1);
	int resz = (liz ? z[now] : 1);
	for (int i = 0; i <= resx; i++) {
		for (int j = 0; j <= resy; j++) {
			int o = (i ^ j);
			if (o <= resz) {
				pair<long long, long long> ret = dfs(now + 1, (lix && (i == x[now])), (liy && (j == y[now])), (liz && (o == z[now])));
				g[now][lix][liy][liz] = (g[now][lix][liy][liz] + ret.second % mod + ret.first * p[ma - now] * o % mod) % mod;
				f[now][lix][liy][liz] = (f[now][lix][liy][liz] + ret.first) % mod;
			}
		}
	}
	return {f[now][lix][liy][liz], g[now][lix][liy][liz]};
}
long long w(long long xx, long long yy, long long zz) {
	memset(f, 0, sizeof(f));
	memset(g, 0, sizeof(g));
	memset(x, 0, sizeof(x));
	memset(y, 0, sizeof(y));
	memset(z, 0, sizeof(z));
	xcnt = 0, ycnt = 0, zcnt = 0;
	while(xx) {
		x[++xcnt] = (xx & 1);
		xx >>= 1;
	}
	while(yy) {
		y[++ycnt] = (yy & 1);
		yy >>= 1;
	}
	while(zz) {
		z[++zcnt] = (zz & 1);
		zz >>= 1;
	}
	xcnt = max(1, xcnt), ycnt = max(1, ycnt), zcnt = max(1, zcnt);
	ma = max({xcnt, ycnt, zcnt});
	reverse(x + 1, x + 1 + ma);
	reverse(y + 1, y + 1 + ma);
	reverse(z + 1, z + 1 + ma);
	pair<long long, long long> now = dfs(1, 1, 1, 1);
	long long ret = (now.first % mod + now.second) % mod;
	return ret;
}
int main() {
	// freopen("in.in", "r", stdin);
	// freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> t;
	p[0] = 1;
	for (int i = 1; i <= 45; i++) {
		p[i] = p[i - 1] * 2 % mod;
	}
	while(t--) {
		cin >> x1 >> y1 >> x2 >> y2 >> k;
		x1--; y1--; x2--; y2--; k--;
		ans = w(x2, y2, k);
		if (x1 && y1) {
			ans = (ans + w(x1 - 1, y1 - 1, k)) % mod;
		}
		if (x1) {
			ans = (ans - w(x1 - 1, y2, k) % mod + mod) % mod;
		}
		if (y1) {
			ans = (ans - w(x2, y1 - 1, k) % mod + mod) % mod;
		}
		cout << (long long)ans << '\n';
	}
	return 0;
}
  1. Luogu P4099 [HEOI2013] SAO

发现只有 n1 个限制,那么我们将这些限制连边,得到的图将是一个

那么在树上做一些东西就会变得简单一些;

考虑树形DP,设 fx,j 表示 x 的子树中,x拓扑序排名j 的方案数,注意这个状态设计,比较典型;

那么我们用树形背包的思维来转移它,依次顺序考虑每一棵子树;

我们首先将所有 fx,j 以前的状态都复制进一个数组 g 中,并将 fx 清空;

那么现在我们考虑转移 fx,j,设 u 为现在所考虑的子树,那么有转移:

fx,j=k=1sizugi×fu,k×K

前两项没有问题,对于这个 K,它其实是我们为了满足合并后的拓扑序而来的一个方案数;

考虑如何求 K

对于 x,我们的拓扑序从 i 变为了 j,那么我们要在前面插入子树序列中的 ji 个数,方案数为 Cj1ji,插在后面的方案数为 Csizx+sizujsizui,所以 K=Cj1ji×Csizx+sizujsizui

然后考虑怎样满足题目中的限制,那么我们发现,K 是不变的,所以我们限制 k 的取值就行了;

最后上一个前缀和优化达到 Θ(n2)

点击查看代码
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const long long mod = 1e9 + 7;
int t;
int n;
struct sss{
	int t, ne, w;
}e[500005];
int h[500005], cnt;
void add(int u, int v, int ww) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	h[u] = cnt;
	e[cnt].w = ww;
}
long long f[1005][1005], siz[1005], fac[1005], fav[1005], g[1005];
long long ksm(long long a, long long b) {
	long long ans = 1;
	while(b) {
		if (b & 1) ans = ans * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return ans;
}
long long C(long long a, long long b) {
	if (b < 0) return 0;
	if (a < b) return 0;
	return fac[a] * fav[b] % mod * fav[a - b] % mod;
}
void dfs(int x, int fa) {
	siz[x] = 1;
	f[x][1] = 1;
	for (int o = h[x]; o; o = e[o].ne) {
		int u = e[o].t;
		if (u == fa) continue;
		dfs(u, x);
		for (int i = 1; i <= siz[x]; i++) {
			g[i] = f[x][i];
			f[x][i] = 0;
		}
		if (e[o].w) {
			for (int j = 1; j <= siz[x]; j++) {
				for (int i = j; i <= siz[u] + j - 1; i++) {
					f[x][i] = (f[x][i] + C(i - 1, i - j) * C(siz[x] + siz[u] - i, siz[x] - j) % mod * g[j] % mod * (f[u][siz[u]] - f[u][i - j] + mod) % mod) % mod;
				}
			}
		} else {
			for (int j = 1; j <= siz[x]; j++) {
				for (int i = j + 1; i <= siz[u] + j; i++) {
					f[x][i] = (f[x][i] + C(i - 1, i - j) * C(siz[x] + siz[u] - i, siz[x] - j) % mod * g[j] % mod * f[u][i - j] % mod) % mod;
				}
			}
		}
 		siz[x] += siz[u];
	}
	for (int i = 1; i <= siz[x]; i++) {
		f[x][i] = (f[x][i] + f[x][i - 1]) % mod;
	}
}
int main() {
	// freopen("in.in", "r", stdin);
	// freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> t;
	int x, y;
	char s;
	fac[0] = 1;
	fav[0] = 1;
	for (int i = 1; i <= 1000; i++) {
		fac[i] = fac[i - 1] * i % mod;
		fav[i] = ksm(fac[i], mod - 2);
	}
	while(t--) {
		cin >> n;
		for (int i = 1; i <= n - 1; i++) {
			cin >> x;
			cin >> s;
			cin >> y;
			x++;
			y++;
			add(x, y, (s == '<'));
			add(y, x, (s == '>'));
		}
		dfs(1, 0);
		cout << f[1][n] << '\n';
		memset(f, 0, sizeof(f));
		cnt = 0;
		for (int i = 1; i <= n; i++) {
			h[i] = 0;
		}
	}
	return 0;
}
  1. Luogu P4516 [JSOI2018] 潜入行动

也是依据这个题好好复习了一下树上背包;

树上背包转移时要依次顺序考虑每棵子树的贡献;

考虑设 fx,j,0/1,0/1 表示考虑 x 的子树,用了 j 个机器, x 用没用, x 有没有被监听的方案数,转移直接分讨,注意分讨时从贡献的角度考虑,这里不再细说;

对于时间复杂度,是 Θ(nk) 的,感性理解一下就是只会有最多 Θ(nk)Θ(k2) 的合并,所以是 Θ(nk) 的;

点击查看代码
#include <iostream>
#include <cstdio>
const long long mod = 1e9 + 7;
using namespace std;
int n, k;
int f[100005][105][2][2], g[105][2][2];
struct sss{
	int t, ne;
}e[500005];
int h[500005], cnt;
void add(int u, int v) {
	e[++cnt].t = v;
	e[cnt].ne = h[u];
	h[u] = cnt;
}
int siz[500005];
void dfs(int x, int fa) {
	siz[x] = 1;
	f[x][0][0][0] = f[x][1][1][0] = 1;
	for (int o = h[x]; o; o = e[o].ne) {
		int u = e[o].t;
		if (u == fa) continue;
		dfs(u, x);
		for (int i = 0; i <= k; i++) {
			for (int j = 0; j <= 1; j++) {
				for (int y = 0; y <= 1; y++) {
					g[i][j][y] = f[x][i][j][y];
					f[x][i][j][y] = 0;
				}
			}
		}
		for (int i = 0; i <= min(siz[x], k); i++) {
			for (int j = 0; j + i <= k && j <= siz[u]; j++) {
				f[x][i + j][0][0] = (f[x][i + j][0][0] + 1ll * f[u][j][0][1] * g[i][0][0] % mod) % mod;
				f[x][i + j][0][1] = (f[x][i + j][0][1] + ((1ll * g[i][0][1] * (f[u][j][0][1] + f[u][j][1][1]) % mod) % mod + 1ll * g[i][0][0] * f[u][j][1][1] % mod) % mod) % mod;
				f[x][i + j][1][0] = (f[x][i + j][1][0] + (1ll * g[i][1][0] * (f[u][j][0][0] + f[u][j][0][1]) % mod) % mod) % mod;
				long long sum = (f[u][j][1][0] + f[u][j][1][1]) % mod;
				long long su = (((f[u][j][1][0] + f[u][j][1][1]) % mod + f[u][j][0][1]) % mod + f[u][j][0][0]) % mod;
				f[x][i + j][1][1] = (f[x][i + j][1][1] + (1ll * g[i][1][0] * sum % mod + 1ll * g[i][1][1] * su % mod) % mod) % mod;
			}
		}
		siz[x] += siz[u];
	}
}
int main() {
	// freopen("in.in", "r", stdin);
	// freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> k;
	int x, y;
	for (int i = 1; i <= n - 1; i++) {
		cin >> x >> y;
		add(x, y);
		add(y, x);
	}
	dfs(1, 0);
	cout << (f[1][k][0][1] + f[1][k][1][1]) % mod;
	return 0;
}
  1. CF505C Mr. Kitayuta, the Treasure Hunter

一道比较新颖的DP题;

考虑朴素DP,设 fi,j 表示到第 i 位,上一次跳跃的距离为 j,转移和状态数是 Θ(V2) 的,考虑优化;

发现第二维有很多冗余状态,考虑怎样优化这个东西;

发现每次只会有三种改变方式,于是可以更改这个状态为 fi,j 表示到第 i 位,和 d 差了 j 的答案,发现这样做 j 不会很大,最多 300 左右,证明考虑 d=1,然后一直向前加 2,3,4,...,我们发现这样做一共最多只有 x 次,其中 x(x1)2=30000,差不多 300

于是我们这样做就行了,转移考虑三个方向,时间复杂度:Θ(mV),其中 m 差不多 300

点击查看代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n, d;
int f[30005][675], a[30005];
int main() {
// 	freopen("in.in", "r", stdin);
// 	freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> d;
	int x;
	for (int i = 1; i <= n; i++) {
		cin >> x;
		a[x]++;
	}
	memset(f, 0xcf, sizeof(f));
	f[d][350] = a[d];
	for (int i = d + 1; i <= x; i++) {
		for (int j = -320; j <= 320; j++) {
			if (i - (d + j) >= d && d + j > 0) {
				f[i][j + 350] = max({f[i - (d + j)][(j - 1) + 350], f[i - (d + j)][j + 350], f[i - (d + j)][j + 1 + 350]}) + a[i];
			}
		}
	}
	int ans = 0;
	for (int i = d; i <= x; i++) {
		for (int j = -320; j <= 320; j++) {
			ans = max(ans, f[i][j + 350]);
		}
	}
	cout << ans;
	return 0;
}
  1. CF1866D Digital Wallet

随机跳题总是给我跳DP,质量还挺高,挺好

考虑这题咋做,首先想用贪心,然后用 set 贪了半天贪不出来,所以改成DP;

fi 表示区间左端点为 i 时的最大值,那么我们考虑一列数的存活区间,可以发现,对于一列 i,它的存活区间为 [ik+1,i],所以我们考虑这一列每个点的贡献时只需从这个范围内考虑即可;

然后就是转移,我们考虑选不选这个数,那么我们直接从 fi1 转移过来,状态转移方程为 fi=max(fi,fi1+a)

因为每个数只能选一次,所以我们倒序转移,每次确保从上一个状态转移过来即可;

时间复杂度: Θ(nmk)

点击查看代码
#include <iostream>
#include <cstdio>
using namespace std;
int n, m, k;
long long a[15][100005], f[100005];
int main() {
// 	freopen("in.in", "r", stdin);
// 	freopen("out.out", "w", stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			cin >> a[i][j];
		}
	}
	for (int j = 1; j <= m; j++) {
		for (int i = 1; i <= n; i++) {
			for (int x = j; x >= max(1, j - k + 1); x--) {
				f[x] = max(f[x], f[x - 1] + a[i][j]);
			}
		}
	}
	cout << f[m - k + 1];
	return 0;
}

To be continued...

posted @   Peppa_Even_Pig  阅读(40)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示