[BZOJ3295] [Cqoi2011]动态逆序对 (树套树)or(CDQ分治)

题意:N个数的排列,M次操作,每次求当前的逆序对数量并删掉一个数。


<一>先说一下cdq分治做法。(5960kb,1.4s)

网上很多题解,我都看不懂(其实很多人的程序几乎是一样的,就改了一下变量名),然后就自己硬着头皮想了这道题,基本是独立做出来的,做出来之后竟然1A,简直愉快。不过我太辣鸡了想了半天才发现这本质上是一个三维偏序,分别是时间,下标,数值,记为(t,x,y)。

我们可以把删除的过程倒过来,当做插入来做,时间t表示这个数是第几个插入的,显然给出的删除的点的t值依次是N,N-1,N-2...(越先删除的视为越后插入的)注意不在询问范围内的点的t值可以任意设置,并且显然没有哪两个点有相同的t或x或y值,这使得问题好考虑得多了。我们求的就是按顺序插入每一个数时,这个数左边比它大的、右边比它小的分别有多少个。形式化地,对一个点(t0,x0,y0),求出满足ty0的点的个数记为lda[t0],满足tx0,y>y0的点的个数记为rxiao[t0]。

我想了一会儿,觉得最外层按x排比较科学,内部对t进行划分排序(相当于快排,将t值<=mid的划分到左边,同时对于划分到同一侧的点要保证原来的相对顺序不变),对y用树状数组来维护。每个节点[L,R]划分出来是这样的:

[BZOJ3295] [Cqoi2011]动态逆序对 (树套树)or(CDQ分治)_第1张图片

要找[L,mid]对[mid+1,R]的贡献:

先考虑对lda的贡献。枚举t∈[mid+1,R]的点(t0,x0,y0),区间内的点由于已经按时间划分好了,所以不需要考虑ty0的点,由于两边的x值各自保持单调(如图),所以可以像树状数组求逆序对一样,将[L,mid]区间内的点的y值在树状数组上增加1,然后求[mid+1,R]的每个y值在树状数组上的前缀和即可。由于l1和l2都是单增的,这一操作复杂度为nlogn。

再考虑对rxiao的贡献:类似地,找出[L,mid]中x值大于[mid+1,R]中的x值的即可。

注意一层分治并不能找出[mid+1,R]中所有值的lda和rxiao,但整个分治一定会不遗漏不重复地覆盖每个点的决策区间。每层复杂度nlogn,总共logn层,总复杂度nlog^2n。貌似有人说cdq分治可以做到nlogn?我觉得不太科学,毕竟三维,不可能把某一维直接吃掉吧。。

cdq分治做这种题真是优秀,空间复杂度仅为O(n),时间复杂度也不逊于高级数据结构,并且分治(对半分)以及树状数组的常数都是极小的,基本是严格logn。

#include
#include
#include
#include
using namespace std;
const int MAXN = 100010;
#define rep(i,a,b) for(int i=a;i<=b;++i)
#define erp(i,a,b) for(int i=a;i>=b;--i)
#define clr(a) memset(a,0,sizeof a)
#define LL long long

void get(int &r) {
	char c; r=0;
	do c=getchar(); while (c<'0'||c>'9');
	do r=r*10+c-'0', c=getchar(); while (c>='0'&&c<='9');
}

int N, M;
namespace BIT
{
	int c[MAXN], r;
	void add(int i, int x)
	{
		for (; i<=N; i+=i&-i) c[i] += x;
	}
	int sum(int i)
	{
		r = 0;
		for (; i>0; i-=i&-i) r += c[i];
		return r;
	}
};

struct dot
{
	int t, x, y; // every t, x, y is unique
} a[MAXN], np[MAXN];

int rxiao[MAXN], lda[MAXN];
LL ans[MAXN];
void cdq(int L, int R)
{
	using namespace BIT;
	if (L >= R) return;
	int mid = (L+R)>>1;

	int l1 = L, l2 = mid+1;
	rep(i, L, R)
	{
		if (a[i].t <= mid) np[l1++] = a[i];
		else np[l2++] = a[i];
	}
	rep(i, L, R) a[i] = np[i];

	l1 = L;
	rep(i, mid+1, R) // 先找坐标更小,值更大的
	{
		for (; l1<=mid && np[l1].x=L && np[l1].x>np[i].x; --l1) add(np[l1].y, 1);
		rxiao[ np[i].t ] += sum(np[i].y - 1);
	}
	rep(i, l1+1, mid) add(np[i].y, -1);

	cdq(L, mid), cdq(mid+1, R);
}

int pos[MAXN];
int main()
{
	get(N), get(M);
	rep(i, 1, N) get(a[i].y), a[i].x = i, pos[a[i].y] = i;
	int t, tmr = N;
	rep(i, 1, M) get(t), a[pos[t]].t = tmr--;
	rep(i, 1, N) if (!a[i].t) a[i].t = tmr--;
	cdq(1, N);
	rep(i, 1, N) ans[i] = ans[i-1] + rxiao[i] + lda[i];
	erp(i, N, N-M+1) cout << ans[i] << '\n';
	return 0;
}


由于这篇blog的重点是cdq分治,高级数据结构的做法是以前写的,但这道题确实是个好题,我还是简单说一下。


<二>树状数组套主席树。(103MB,2.3s)

非常裸,主席树(按权值建)表示前面一段区间中的每个值的数量,方便求lda和rxiao(上面提过这两个的含义)。因为需要修改,要用树状数组来绑定前缀和。由于主席树的强大,删点和插点两种顺序都能做。

注意树状数组套主席树的空间复杂度是O((n+m)log^2n)的,理论上128M开不下。有个优化,一开始不保存在主席树里面,用树状数组来保存,然后每次删除转换为增加一个点。这样空间复杂度是O(mlog^2),由于maxm<=maxn/2,可以节约不少。还有,建议用数组来保存,因为OJ上指针的内存开销是整数的两倍。

时间当然常数很小了,由于线段树是走单路且无tag,是严格logn的,树状数组更小了。

#include
#include
#include
#include
using namespace std;
#define LL long long
const int MAXN = 100010;

void get(int &r) {
	char c; r=0;
	do c=getchar(); while (c<'0'||c>'9');
	do r=r*10+c-'0', c=getchar(); while (c>='0'&&c<='9');
}

int N, M, n;
int a[MAXN], b[MAXN];
LL ans;

namespace pre
{
	int bt[MAXN];
	void update(int x, int d) {
		for (; x<=N; x+=x&-x)
			bt[x] += d;
	}
	int gsum(int x) {
		int res = 0;
		for (; x>0; x-=x&-x)
			res += bt[x];
		return res;
	}
};

int lda[MAXN], rxiao[MAXN];
struct Node {
	int cnt;
	Node*lch, *rch;
};
struct SegTree
{
	Node nil, *NIL;
	Node nds[MAXN*85], *ncnt;
	Node *rt[MAXN];
	Node *c[50];
	int cn;
	SegTree () {
		NIL = &nil;
		rt[0] = nil.lch = nil.rch = NIL;
		ncnt = nds;
	}
	void ins(Node*&x, Node*p, int v, int dt, int l, int r)
	{
		if (!p) p = NIL;
		x = ++ncnt;
		x->cnt = p->cnt + dt;
		x->lch = p->lch;
		x->rch = p->rch;
		if (l == r) return;
		int mid = (l+r)>>1;
		if (v <= mid) ins(x->lch, p->lch, v, dt, l, mid);
		else ins(x->rch, p->rch, v, dt, mid+1, r);
	}
	void load(int pos)
	{
		for (cn = 0; pos; pos -= pos&-pos)
			c[++cn] = rt[pos];
	}
	int calc_greater(Node*x, int v, int l=1, int r=N)
	{
		if (l == r) return 0;
		int mid = (l+r) >> 1, res = 0;
		if (v > mid) {
			for (int i = 1; i<=cn; ++i) c[i] = c[i]->rch;
			return calc_greater(x->rch, v, mid+1, r);
		}
		for (int i = 1; i<=cn; ++i)
			res += c[i]->rch->cnt, c[i] = c[i]->lch;
		return res + calc_greater(x->lch, v, l, mid);
	}
	int calc_less(Node*x, int v, int l=1, int r=N)
	{
		if (l == r) return 0;
		int mid = (l+r) >> 1, res = 0;
		if (v <= mid) {
			for (int i = 1; i<=cn; ++i) c[i] = c[i]->lch;
			return calc_less(x->lch, v, l, mid);
		}
		for (int i = 1; i<=cn; ++i)
			res += c[i]->lch->cnt, c[i] = c[i]->rch;
		return res + calc_less(x->rch, v, mid+1, r);
	}
	void update(int i, int v, int dt)
	{
		for (; i<=N; i += i&-i)
			ins(rt[i], rt[i], v, dt, 1, N);
	}
	LL erase(int v)
	{
		int p = b[v], cnt;
		load(N);
		cnt = calc_less(rt[N], v);
		load(p);
		cnt -= calc_less(rt[p], v);
		load(p-1);
		cnt += calc_greater(rt[p-1], v);
		update(p, v, 1);
		return ans = ans - (lda[p] + rxiao[p] - cnt);
	}
} sg;

int ba[MAXN];

int main()
{
	get(N), get(M);
	for (int i = 1; i<=N; ++i)
		get(a[i]), b[a[i]] = i;
	for (int i = 1; i<=N; ++i)
	{
		sg.rt[i] = sg.NIL;
		sg.rt[i]->lch = sg.rt[i]->rch = sg.NIL;
		lda[i] = (i-1) - pre::gsum(a[i]);
		pre::update(a[i], 1);
		ans += lda[i];
	}
	memset(pre::bt, 0, sizeof pre::bt);
	for (int i = N; i>=1; --i)
	{
		rxiao[i] = pre::gsum(a[i]);
		pre::update(a[i], 1);
	}
	cout << ans << '\n';
	for (int i = 1, x; i


<三>线段树套平衡树(treap)。(43MB,5.1s)

其实我觉得线段树套平衡树比树状数组套主席树好想一些。lda和rxiao本质上就是求一个区间当中有多少数大于/小于一个给定的数,这是线段树套平衡树的基本功能。

空间复杂度是O(nlogn),这是比较能够接受的。理论时间复杂度是O(nlog^2n),但是这个常数很大。线段树多路查询的复杂度很难证明,我大致可以确信线段树一次多路查询极端上届不超过log^2n,实践表现均摊一般为4logn。treap就更不敢保证了。。

其实写树状数组套AVL会快很多。。

#include
#include
#include
#include
#include
using namespace std;

#define idx(l,r) ((l + r) | (l != r))
#define LL long long
const int MAXN = 100010;
int N, M;

int a[MAXN], pos[MAXN];
LL ans;
int bt[MAXN];
void update(int x, int d) {
	for (; x<=N; x+=x&-x)
		bt[x] += d;
}
int qsum(int x) {
	int res = 0;
	for (; x>0; x-=x&-x)
		res += bt[x];
	return res;
}
int lda[MAXN], rxiao[MAXN];

struct Node {
	int rn, v, sz;
	Node*ch[2];
} nil, *NIL = &nil;
struct Treap
{
	Node nd[MAXN * 20], *ncnt;
	Node *rt[MAXN * 2];
	Treap () {
		ncnt = nd;
		nil.ch[0] = nil.ch[1] = NIL;
		for (int i = 0; isz = x->ch[0]->sz + x->ch[1]->sz + 1;
	}
	inline void rotate(Node*&x, bool d) //d=0左旋
	{
		Node*y = x->ch[!d];
		x->ch[!d] = y->ch[d];
		y->ch[d] = x;
		pushup(x), pushup(x = y);
	}
	void ins(Node*&x, int v)
	{
		if (x == NIL) {
			x = ++ncnt;
			x->ch[0] = x->ch[1] = NIL;
			x->v = v, x->rn = rand();
		}
		else {
			bool d = v > x->v;
			ins(x->ch[d], v);
			if (x->rn < x->ch[d]->rn) rotate(x, !d);
		}
		pushup(x);
	}
	int qmore(Node*x, int v)
	{
		if (x==NIL) return 0;
		if (v > x->v) return qmore(x->ch[1], v);
		if (v == x->v) return x->ch[1]->sz + 1;
		return x->ch[1]->sz + 1 + qmore(x->ch[0], v);
	}
	int qless(Node*x, int v)
	{
		if (x==NIL) return 0;
		if (v < x->v) return qless(x->ch[0], v);
		if (v == x->v) return x->ch[0]->sz + 1;
		return x->ch[0]->sz + 1 + qless(x->ch[1], v);
	}
} tp;

int qmore(int v, int l, int r, int L=1, int R=N)
{
	if (R < l || L > r) return 0;
	if (L>=l && R<=r) return tp.qmore(tp.rt[idx(L, R)], v);
	int mid = (L + R) >> 1;
	return qmore(v, l, r, L, mid) + qmore(v, l, r, mid+1, R);
}

int qless(int v, int l, int r, int L=1, int R=N)
{
	if (R < l || L > r) return 0;
	if (L>=l && R<=r) return tp.qless(tp.rt[idx(L, R)], v);
	int mid = (L + R) >> 1;
	return qless(v, l, r, L, mid) + qless(v, l, r, mid+1, R);
}

void ins(int i, int v, int L=1, int R=N)
{
	tp.ins(tp.rt[idx(L, R)], v);
	if (L == R) return;
	int mid = (L + R) >> 1;
	if (i <= mid) ins(i, v, L, mid);
	else ins(i, v, mid+1, R);
}

int main()
{
	srand(45251);
	scanf("%d%d", &N, &M);
	for (int i = 1; i<=N; ++i)
	{
		scanf("%d", a+i), pos[a[i]] = i;
		lda[i] = i-1-qsum(a[i]), update(a[i], 1);
		ans += lda[i];
	}
	memset(bt, 0, sizeof bt);
	for (int i = N; i; --i)
	{
		rxiao[i] = qsum(a[i]);
		update(a[i], 1);
	}
	cout << ans << '\n';
	for (int t, i = 1; i



你可能感兴趣的:(题解,cdq分治,平衡树)