不得不说
本来标题想写分治,但是想了想发现自己分治能说的不多,主要的内容就是 C D Q CDQ CDQ分治.便取了这个标题.
预备知识
- 关于什么是分治
分治,字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。在计算机科学中,分治法就是运用分治思想的一种很重要的算法。分治法是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)等等。 - 一般步骤
- 划分步:把输入的问题划分为k个子问题,并尽量使这 k k k个子问题的规模大致相同。
- 治理步:当问题的规模大于某个预定的阈值 n 0 n_0 n0时,治理步由 k k k个递归调用组成。
- 组合步:组合步把各个子问题的解组合起来,它对分治算法的实际性能至关重要,算法的有效性很大地依赖于组合步的实现。
- 时间复杂度
- 直观估计
- 分治由以上三部分构成,整体时间复杂度则由这三部分的时间复杂度之和构成.
- 由于递归,最终的子问题变得极为简单,以至于其时间复杂度在整个分治策略上的比重微乎其微.
- 直观估计
- 经典例题
- 归并排序,快排等
- 求逆序对等
经典例题
A
t
c
o
d
e
r
A
−
C
o
l
o
r
f
u
l
S
u
b
s
e
q
u
e
n
c
e
Atcoder\\ A\ -\ Colorful\ Subsequence
AtcoderA − Colorful Subsequence
https://atcoder.jp/contests/agc031/tasks/agc031_a
- 题目简析:
- 问多少种子序列,子序列中的字母不同.
- 列如 b a a baa baa,包括: b , a , a , b,a,a, b,a,a,两个不同位置 a a a的 b a ba ba,总计 5 5 5个, b a a baa baa排除是因为 a a a是重复的.
- 解法
- 先将每个字母的个数统计下来,然后分治计算,一个字母的时候,答案是该字母出现的次数.
- 只有两个字母的时候,如 a b ab ab,包含的排列有 a , b , a b a,b,ab a,b,ab,相当于 ′ a 的 个 数 , b 的 个 数 , a 和 b 组 合 个 数 ′ 的 加 和 'a的个数,b的个数,a和b组合个数'的加和 ′a的个数,b的个数,a和b组合个数′的加和,而 a 和 b 组 合 个 数 , 则 是 a 的 个 数 × b 的 个 数 a和b组合个数,则是a的个数\times b的个数 a和b组合个数,则是a的个数×b的个数
- 同理可得, a n s ans ans即为 a n s L + a n s R + a n s L × a n s R ansL+ansR+ansL\times ansR ansL+ansR+ansL×ansR.
#include<iostream>
#include<algorithm>
#include<string>
#include<queue>
#include<stdio.h>
#include<stdlib.h>
#ifndef null
#define null -1
#endif
using namespace std;
const int MAXN = 2e5 + 10;
const int INF = 1e5 + 10;
const int MOD = 1e9 + 7;
typedef long long ll;
char s[INF];
int num[27];
string s1="0";
ll solve(int L, int R)
{
if (L == R)
return num[s1[L] - 'a'];
int mid = (L + R) >> 1;
ll nL = solve(L, mid), nR = solve(mid + 1, R);
return (nL%MOD + nR%MOD + (nL%MOD * nR%MOD)%MOD)%MOD;
}
int main()
{
int n;
cin >> n >> s;
for (int i = 0; i < n; i++) {
if (!num[s[i] - 'a'])
s1 = s1 + s[i];
num[s[i] - 'a']++;
}
cout << solve(1, s1.length() - 1)<<endl;
return 0;
}
CDQ分治
前面絮絮叨叨的简单介绍了下分治,想必各位对分治有了一定认识.
下面是重头戏:
C
D
Q
CDQ
CDQ分治.
这个算法,是由陈丹琦大牛在论文中提出的%%%.
首先,我们需要知道一些事情:
- 优势在于可以顶替复杂的高级数据结构,而且常数比较小
- 缺点在于必须离线操作
用来解决什么问题呢?
- 首先,分治问题2333
- 分治后的答案,不仅单单考虑子问题 { L , m i d } \{L,mid\} {L,mid}和子问题 { m i d + 1 , R } \{mid+1,R\} {mid+1,R}.
- 还需要考虑子问题 { L , m i d } \{L,mid\} {L,mid}对子问题 { m i d + 1 , R } \{mid+1,R\} {mid+1,R}的影响 / / /联系产生的答案.
列如:
- 二维偏序问题
- 给定一个二元组 { x , y } \{x,y\} {x,y},要求问有多少对 { x i , y i } , { x j , y j } \{x_i,y_i\},\{x_j,y_j\} {xi,yi},{xj,yj}满足 x i > x j & & y i > y j x_i>x_j\&\&y_i>y_j xi>xj&&yi>yj
- 解法为:
- 先将二元组按照 x x x的大小排列.
- 分治后,我们分别知道 { L , m i d } \{L,mid\} {L,mid}区间和 { m i d + 1 , R } \{mid+1,R\} {mid+1,R}区间内的解
- 再计算跨过 m i d mid mid的两对点,对 { L , m i d } \{L,mid\} {L,mid}和 { m i d + 1 , R } \{mid+1,R\} {mid+1,R}中的二元组按照 y y y的大小排序
- 由于先前分组便已经对 x x x进行排序,所以,只需要二分便可以求得左区间相对于右区间的点的个数.
- 三维偏序问题
- 和二维偏序问题类似,但有一定不同
- 给定一个三元组 { x , y , z } \{x,y,z\} {x,y,z},要求问有多少对 { x i , y i , z i } , { x j , y j , z j } \{x_i,y_i,z_i\},\{x_j,y_j,z_j\} {xi,yi,zi},{xj,yj,zj}满足 x i > x j & & y i > y j & & z i > z j x_i>x_j\&\&y_i>y_j\&\&z_i>z_j xi>xj&&yi>yj&&zi>zj
- 解法为:
- 先将三元组按照 x x x的大小排列.
- 分治后,我们分别知道 { L , m i d } \{L,mid\} {L,mid}区间和 { m i d + 1 , R } \{mid+1,R\} {mid+1,R}区间内的解
- 再计算跨过 m i d mid mid的两对点,对 { L , m i d } \{L,mid\} {L,mid}和 { m i d + 1 , R } \{mid+1,R\} {mid+1,R}中的三元组按照 y y y的大小排序
- 由于先前分组便已经对 x x x进行排序,所以,只需要二分便可以求得满足 y y y条件的点.
- 再建立一个权值树状数组 / / /线段树,再将上面符合的 x , y , z {x,y,z} x,y,z对应中满足不等式的 z z z的点求出.
例题
伪代码
void cdq(int left,int right)
{
if(left==right)
return ;
int mid=(left+right)>>1;
cdq(left,mid),cdq(mid+1,right);
sort(a+l,a+mid+1,cmp);
sort(a+mid+1,a+right+1,cmp);
/**
*处理左区间对于右区间影响的代码
*/