【CDQ分治总结】CDQ分治 | 偏序 | N


CDQ分治题型总结



概述


CDQ分治是一种通过降维来解决高维偏序问题的分治算法,可避免使用某些复杂数据结构。

算法考虑的关键在于:将两个答案计算完毕的区间合并时,左段区间将会对右段区间造成怎样的影响。




范例


例1:(二维偏序)【BHOJ 14】求逆序对 (其实第一维就是数组下标,已经有序了)

非CDQ解法:

  • 权值树状数组 / 权值线段树:把数离散化,每读入一个数就查询在当前数之前的、且比当前数大的数的个数,然后累加进 s u m sum sum,最后输出 s u m sum sum 即可。
  • 平衡树:更简单了,边插入边累加 i − r a n k i - rank irank,最后输出 s u m sum sum 即可。

CDQ解法:

二元组设定:设二元组 &lt; a , b &gt; &lt;a, b&gt; <a,b> 表示数组第 a a a 个元素的值是 b b b,那求逆序对就是问总共存在多少对 &lt; a i , b i &gt; 、 &lt; a j , b j &gt; &lt;a_i, b_i&gt;、&lt;a_j, b_j&gt; <ai,bi><aj,bj> 使得 a i &lt; a j a_i &lt; a_j ai<aj(在它前面)并且 b i &gt; b j b_i &gt; b_j bi>bj(比它大)。

降维预处理:按 a a a 把整个二元组数组排升序(其实就是按照原本数组的顺序来…这一步实际不用操作)
由此可以保证每次分治开始前,左段任意一项的 a a a 都小于右段任意一项的 a a a (1)

“分治”目的:让分治完毕的区间按 b b b 升序(按 a a a 可能就无序了)。
由于上次分治达成目的,所以现在拿到的左右两段内部都是按 b b b 升序的(2)
为了自洽,这次分治需要将两段按 b b b 升序进行归并(3)

“分治”过程

  • (1)(2)(3),每次分治过程中产生的新的答案数就可求了。记左右段名字分别为 L 、 R L、R LR,长度分别为 l e n l 、 l e n r len_l、len_r lenllenr
  • ∃ i ∈ [ 0 , l e n l ) 、 j ∈ [ 0 , l e n r )    s . t .    L [ i ] . b &gt; R [ j ] . b \exist i \in [0, len_l)、j\in [0, len_r)\ \ s.t.\ \ L[i].b &gt; R[j].b i[0,lenl)j[0,lenr)  s.t.  L[i].b>R[j].b
    • (2) L [ i   . . .   l e n l ) . b L[i\ ...\ len_l).b L[i ... lenl).b &gt; R [ j ] . b &gt;R[j].b >R[j].b
    • (1) L [ i   . . .   l e n l ) . a L[i\ ...\ len_l).a L[i ... lenl).a &lt; R [ j ] . a &lt;R[j].a <R[j].a
  • 因此统计所有满足条件的 i 、 j i、j ij,就得到了这次归并过程产生的新的答案数
  • 为了自洽还需要做 (3),因此我们需要在归并的同时找满足条件的 i、j。这显然是易实现的,因为按 b b b 升序归并的过程恰能不重不漏地遇见每一个满足条件的 i、j。

代码:

权值树状数组:

#include <stdio.h>
#include <string.h>
#include <algorithm>


constexpr int MN(1e6+7);


#define lowbit(x) (x)&-(x)
int a[MN];
class Bitree
{
private:

	int n;
	int a[MN];

public:

	inline void init(const int n)
	{
		this->n = n;
		memset(a, 0, sizeof(*a) * (n+1));
	}

	void add(int i)
	{
		while (i <= n)
			++a[i], i += lowbit(i);
	}

	int sum(int i)
	{
		int s = 0;
		while (i > 0)
			s += a[i], i -= lowbit(i);
		return s;
	}
} bt;
	



struct Node
{
	int val;
	int id;
	inline bool operator <(const Node &o) const
	{
		return val < o.val;
	}
} t[MN];
int p[MN];



int main()
{
	int n;
	while (~scanf("%d", &n))
	{
		for (int i=1; i<=n; t[i].id=i, ++i)
			scanf("%d", &t[i].val);

		// discretization
		std::sort(t+1, t+n+1);
		int tot = 0;
		p[t[1].id] = ++tot;
		for (int i=2; i<=n; ++i)
			p[t[i].id] = t[i].val == t[i-1].val ? tot : ++tot;

		// dynamic counting
		unsigned ans = 0;
		bt.init(n);
		for (int i=1; i<=n; ++i)
		{
			bt.add(p[i]);
			ans += i - bt.sum(p[i]);
		}
		printf("%u\n", ans);
	}
}

平衡树(以 SBT 为例):

#include <stdio.h>
#include <string.h>

#define LL long long
#define _BS 1048576
char _bf[_BS];
char *__p1=_bf,*__p2=_bf;
#define _IO char *_p1=__p1,*_p2=__p2;
#define _OI __p1=_p1,__p2=_p2;

#ifdef _KEVIN
#define GC getchar()
#else
#define GC (_p1==_p2&&(_p2=(_p1=_bf)+fread(_bf,1,_BS,stdin),_p1==_p2)?EOF:*_p1++)
#endif
#define PC putchar
#define _Q_(x) {register char _c=GC,_v=1;for(x=0;_c<48||_c>57;_c=GC)if(_c==45)_v=-1;
#define _H_(x) for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=GC);if(_v==-1)x=-x;}
#define sc(x) _Q_(x)_H_(x)
#define se(x) _Q_(x)else if(_c==EOF)return 0;_H_(x)

template<class T>
void UPRT(const T _){if(_>=10)UPRT(_/10);PC(_%10+48);}

constexpr int MN(1e6+7);

template <typename vint, typename xint>
class SBT
{
private:

	xint root, tot;
	struct SBN
	{
		xint li, ri, sz;
		vint v;
	} t[MN];

	inline void l_r(xint &i)
	{
		const xint ri = t[i].ri;
		t[i].ri = t[ri].li, t[ri].li = i, t[ri].sz = t[i].sz;
		t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
		i = ri;
	}

	inline void r_r(xint &i)
	{
		const xint li = t[i].li;
		t[i].li = t[li].ri, t[li].ri = i, t[li].sz = t[i].sz;
		t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
		i = li;
	}

	void mt(xint &i, const bool rr)
	{
		if (rr)
		{
			if (t[t[t[i].ri].ri].sz > t[t[i].li].sz)
				l_r(i);
			else if (t[t[t[i].ri].li].sz > t[t[i].li].sz)
				r_r(t[i].ri), l_r(i);
			else
				return;
		}
		else
		{
			if (t[t[t[i].li].li].sz > t[t[i].ri].sz)
				r_r(i);
			else if (t[t[t[i].li].ri].sz > t[t[i].ri].sz)
				l_r(t[i].li), r_r(i);
			else
				return;
		}

		mt(t[i].li, 0), mt(t[i].ri, 1), mt(i, 0), mt(i, 1);
	}

	void _insert(xint &i, const vint v)
	{
		if (i)
		{
			++t[i].sz;
			if (v < t[i].v)
				_insert(t[i].li, v), mt(i, 0);
			else
				_insert(t[i].ri, v), mt(i, 1);
		}
		else
		{
			i = ++tot;
			t[i].v = v;
			t[i].sz = 1;
		}
	}

	xint _lower_rk(xint i, const vint v) const
	{
		xint rk = 1;
		while (i)
		{
			if (t[i].v >= v)
				i = t[i].li;
			else
				rk += t[t[i].li].sz + 1, i = t[i].ri;
		}

		return rk;
	}

	xint _upper_rk(xint i, const vint v) const
	{
		xint rk = 0;
    	while (i)
    	{
    		if (t[i].v > v)
    			i = t[i].li;
    		else
    			rk += t[t[i].li].sz + 1, i = t[i].ri;
		}

		return rk;
	}

public:

	inline void init(const int n)
	{
		memset(t, 0, sizeof(*t) * (n+2));
		root = tot = 0;
	}

	inline void insert(const vint v)
	{
		_insert(this->root, v);
	}

	inline xint lower_rk(const vint v) const
	{
		return _lower_rk(this->root, v);
	}

	inline xint upper_rk(const vint v) const
	{
		return _upper_rk(this->root, v);
	}
};

SBT<int, int> sbt;

int main()
{
	_IO

	
	int n, tp;
	LL sum;
	while (1)
	{
		se(n)
		sbt.init(n), sum = 0;

		for (int i=1; i<=n; ++i)
		{
			sc(tp)
			sbt.insert(tp);
			sum += i - sbt.upper_rk(tp);
		}

		UPRT(sum), PC(10);
	}

	return 0;
}


CDQ:

#include <stdio.h>
#include <string.h>

constexpr int MN(1e6);

int a[MN], t[MN];
unsigned tot;

void merge(int *L, int len_l, int len_r)
{
	int *R = a + len_l;
	int top = 0, i = 0, j = 0;

	while (i<len_l && j<len_r)
	{
		if (L[i] > R[j])
		{
			t[top++] = R[j++];
			tot += len_l - i;	// a[i ... len_l-1] 共len_l-1-i+1 == len_l-i个数都比b[j]大,逆序对数+=len_l-i
		}
		else
			t[top++] = L[i++];
	}
	while (i < len_l)
		t[top++] = L[i++];
	while (j < len_r)
		t[top++] = R[j++];

	memcpy(L, t, sizeof(*L) * top);
}

void cdq(int *a, int n)
{
	if (n <= 1)
		return;

	int mid = n >> 1;
	cdq(a, mid);
	cdq(a+mid, n-mid);
	merge(a, mid, n-mid);
}


int main()
{
	int n;
	while (~scanf("%d", &n))
	{
		tot = 0;
		for (int i=0; i<n; ++i)
			scanf("%d", a+i);

		cdq(a, n);

		printf("%u\n", tot);
	}

	return 0;
}


好吧上面这个问题好像还不足以体现CDQ的高明之处,现在我们稍微加大难度,看一下三维偏序问题:(大名鼎鼎的板子题陌上花开~)


例2:(三维偏序)【P3810】【模板】三维偏序(陌上花开)

非CDQ解法:第一维排序、第二维树状数组、第三维平衡树

树状数组套平衡树嘛…

先说一下平衡树和树状数组各自的任务:

  • 先记所有元素中最大的 b b b 值是 m a x b max_b maxb
  • 再记当前处理到第 n o w now now 个元素,其 a ,   b ,   c a,\ b,\ c a, b, c 值分别记为 a n o w ,   b n o w ,   c n o w a_{now},\ b_{now},\ c_{now} anow, bnow, cnow
  • 平衡树:共有 m a x b max_b maxb 棵。每棵树对应一个 b b b 值,记为 t r e e . b tree.b tree.b,这棵树里面存放着 [ 0 ,   n o w ] [0,\ now] [0, now] 这些元素中 b b b 值等于 t r e e . b tree.b tree.b 的元素 c c c 值。
  • 树状数组:维护 m a x b max_b maxb 这么多棵平衡树的“前缀和”。比如处理到第 n o w now now 个元素的时候,我们所查询的“前缀和”的定义是: t r e e . b ∈ [ 0 , b n o w ] tree.b \in [0, b_{now}] tree.b[0,bnow] 的这么多棵树的 c n o w c_{now} cnow r a n k rank rank 之和。

所以如果我们按 a a a 非降序遍历,遍历到 n o w now now 的时候可以就保证前面的 a a a 值都满足 ≤ \le 条件。此时再查询区间 [ 0 , b n o w ] [0, b_{now}] [0,bnow] 内的 “前缀和”,就又同时满足了 b 、 c b、c bc 值的 ≤ \le 条件,就可以得到 n o w now now 这个元素对应的答案了。


实际写代码的时候要注意两个地方:

  • 1.最外层按照 a a a 的遍历应是一批一批地进行的。也就是说要一次性插入一批 a a a 相等的元素,然后再挨个查询“前缀和”并记录答案。
  • 2.注意这里是小于等于,所以 r a n k rank rank 实际上应该是一个被称为 u p p e r _ r a n k upper\_rank upper_rank 的东西。(并列的时候按最后一个的排名算)

然后会发现树套树这货码量又大常数又大(q^q)!所以偏序问题还是CDQ大法好啊!



CDQ解法:

二元组设定:设二元组 &lt; a , b &gt; &lt;a, b&gt; <a,b> 表示数组第 a a a 个元素的值是 b b b,那求逆序对就是问总共存在多少对 &lt; a i , b i &gt; 、 &lt; a j , b j &gt; &lt;a_i, b_i&gt;、&lt;a_j, b_j&gt; <ai,bi><aj,bj> 使得 a i &lt; a j a_i &lt; a_j ai<aj(在它前面)并且 b i &gt; b j b_i &gt; b_j bi>bj(比它大)。

降维预处理:按 a a a 把整个二元组数组排升序(其实就是按照原本数组的顺序来…这一步实际不用操作)
由此可以保证每次分治开始前,左段任意一项的 a a a 都小于右段任意一项的 a a a (1)

“ 分治 ” 目的:让分治完毕的区间按 b b b 升序(按 a a a 可能就无序了)。
由于上次分治达成目的,所以现在拿到的左右两段内部都是按 b b b 升序的(2)
为了自洽,这次分治需要将两段按 b b b 升序进行归并(3)

“ 分治 ” 过程

  • (1)(2)(3),每次分治过程中产生的新的答案数就可求了。记左右段名字分别为 L 、 R L、R LR,长度分别为 l e n l 、 l e n r len_l、len_r lenllenr
  • ∃ i ∈ [ 0 , l e n l ) 、 j ∈ [ 0 , l e n r )    s . t .    L [ i ] . b &gt; R [ j ] . b \exist i \in [0, len_l)、j\in [0, len_r)\ \ s.t.\ \ L[i].b &gt; R[j].b i[0,lenl)j[0,lenr)  s.t.  L[i].b>R[j].b
    • (2) L [ i   . . .   l e n l ) . b L[i\ ...\ len_l).b L[i ... lenl).b &gt; R [ j ] . b &gt;R[j].b >R[j].b
    • (1) L [ i   . . .   l e n l ) . a L[i\ ...\ len_l).a L[i ... lenl).a &lt; R [ j ] . a &lt;R[j].a <R[j].a
  • 因此统计所有满足条件的 i 、 j i、j ij,就得到了这次归并过程产生的新的答案数
  • 为了自洽还需要做 (3),因此我们需要在归并的同时找满足条件的 i、j。这显然是易实现的,因为按 b b b 升序归并的过程恰能不重不漏地遇见每一个满足条件的 i、j。

代码:

树状数组套平衡树(以 SBT 为例):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define LL long long
#define _BS 1048576
char _bf[_BS];
char *__p1=_bf,*__p2=_bf;
#define _IO char *_p1=__p1,*_p2=__p2;
#define _OI __p1=_p1,__p2=_p2;

#ifdef _KEVIN
#define GC getchar()
#else
#define GC (_p1==_p2&&(_p2=(_p1=_bf)+fread(_bf,1,_BS,stdin),_p1==_p2)?EOF:*_p1++)
#endif
#define PC putchar
#define sc(x) {register char _c=GC,_v=1;for(x=0;_c<48||_c>57;_c=GC)if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=GC);if(_v==-1)x=-x;}

template<class T>
void UPRT(const T _){if(_>=10)UPRT(_/10);PC(_%10+48);}

constexpr int MN(1e5+7), MV(2e5+7);

template <typename vint, typename xint = int>
class SBT
{
public:

    xint root[MV], tot;
    struct SBN
    {
        xint li, ri, sz;
        vint v;
    } t[30 * MV];

public:

    inline void l_r(xint &i)
    {
        const xint ri = t[i].ri;
        t[i].ri = t[ri].li, t[ri].li = i, t[ri].sz = t[i].sz;
        t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
        i = ri;
    }

    inline void r_r(xint &i)
    {
        const xint li = t[i].li;
        t[i].li = t[li].ri, t[li].ri = i, t[li].sz = t[i].sz;
        t[i].sz = t[t[i].li].sz + t[t[i].ri].sz + 1;
        i = li;
    }

    void mt(xint &i, const bool rr)
    {
        if (rr)
        {
            if (t[t[t[i].ri].ri].sz > t[t[i].li].sz)
                l_r(i);
            else if (t[t[t[i].ri].li].sz > t[t[i].li].sz)
                r_r(t[i].ri), l_r(i);
            else
                return;
        }
        else
        {
            if (t[t[t[i].li].li].sz > t[t[i].ri].sz)
                r_r(i);
            else if (t[t[t[i].li].ri].sz > t[t[i].ri].sz)
                l_r(t[i].li), r_r(i);
            else
                return;
        }

        mt(t[i].li, 0), mt(t[i].ri, 1), mt(i, 0), mt(i, 1);
    }

    void insert(xint &i, const vint v)
    {
        if (i)
        {
            ++t[i].sz;
            if (v < t[i].v)
                insert(t[i].li, v), mt(i, 0);
            else
                insert(t[i].ri, v), mt(i, 1);
        }
        else
        {
            i = ++tot;
            t[i].v = v;
            t[i].sz = 1;
        }
    }

    xint lower_rk(xint i, const vint v) const
	{
		xint rk = 1;
		while (i)
		{
			if (t[i].v >= v)
				i = t[i].li;
			else
				rk += t[t[i].li].sz + 1, i = t[i].ri;
		}

		return rk;
	}

	xint upper_rk(xint i, const vint v) const
	{
		xint rk = 0;
    	while (i)
    	{
    		if (t[i].v > v)
    			i = t[i].li;
    		else
    			rk += t[t[i].li].sz + 1, i = t[i].ri;
		}

		return rk;
	}
};

SBT<int> sbt;


int k;
inline void add(int x, const int v)
{
	while (x <= k)
		sbt.insert(sbt.root[x], v), x += x&-x;
}

inline int sum(int x, const int v)
{
	int sum = 0;
	while (x)
		sum += sbt.upper_rk(sbt.root[x], v), x -= x&-x;

	return sum;
}

struct Node
{
	int a, b, c;
	inline bool operator <(const Node &o) const
	{
		return a<o.a;
	}
} a[MN];

bool vis[MN];
int ans[MN], f[MN];

int main()
{
    _IO

    int n;
    sc(n)sc(k)
    for (int i=0 ;i<n; ++i)
    {
    	sc(a[i].a)
    	sc(a[i].b)
    	sc(a[i].c)
	}

	std::sort(a, a+n);

	for (int i=0; i<n; ++i)
	{
		if (!vis[i])
			for (int j=i; a[j].a == a[i].a && j<n; ++j)
				add(a[j].b, a[j].c), vis[j] = true;
		f[i] = sum(a[i].b, a[i].c);
	}

	for (int i=0; i<n; ++i)
		++ans[f[i]];

	for (int i=1; i<=n; ++i)
		UPRT(ans[i]), PC(10);

    return 0;
}


CDQ:

#include <stdio.h>
#include <string.h>

constexpr int MN(1e6);

int a[MN], t[MN];
unsigned tot;

void merge(int *L, int len_l, int len_r)
{
	int *R = a + len_l;
	int top = 0, i = 0, j = 0;

	while (i<len_l && j<len_r)
	{
		if (L[i] > R[j])
		{
			t[top++] = R[j++];
			tot += len_l - i;	// a[i ... len_l-1] 共len_l-1-i+1 == len_l-i个数都比b[j]大,逆序对数+=len_l-i
		}
		else
			t[top++] = L[i++];
	}
	while (i < len_l)
		t[top++] = L[i++];
	while (j < len_r)
		t[top++] = R[j++];

	memcpy(L, t, sizeof(*L) * top);
}

void cdq(int *a, int n)
{
	if (n <= 1)
		return;

	int mid = n >> 1;
	cdq(a, mid);
	cdq(a+mid, n-mid);
	merge(a, mid, n-mid);
}


int main()
{
	int n;
	while (~scanf("%d", &n))
	{
		tot = 0;
		for (int i=0; i<n; ++i)
			scanf("%d", a+i);

		cdq(a, n);

		printf("%u\n", tot);
	}

	return 0;
}


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值