Algorithm Review 8 分治

分治

主定理

  • 即 Master Theorem,可用于推导由分治法得到的递推关系式的时间复杂度,设
    T ( n ) = a T ( n b ) + f ( n ) T(n) = aT(\frac{n}{b}) + f(n) T(n)=aT(bn)+f(n)
  • 其中 a , b a,b a,b 为常数, a ≥ 1 , b > 1 a \ge 1, b > 1 a1,b>1 f ( n ) f(n) f(n) 为递推以外进行的计算工作,只能为一般的多项式。
  • 考虑递归到底层时底层的总时间复杂度为 Θ ( a log ⁡ b n ) = Θ ( n log ⁡ b a ) \Theta (a^{\log_bn}) = \Theta(n^{\log_ba}) Θ(alogbn)=Θ(nlogba),讨论 n log ⁡ b a n^{\log_ba} nlogba f ( n ) f(n) f(n) 的关系,则有以下结果:
    • f ( n ) = O ( n log ⁡ b a − ε ) , ε > 0 f(n) = \mathcal O(n^{\log_b{a -\varepsilon}}),\varepsilon>0 f(n)=O(nlogbaε),ε>0,则 T ( n ) = Θ ( n log ⁡ b a ) T(n) = \Theta(n^{\log_ba}) T(n)=Θ(nlogba)
    • f ( n ) = Θ ( n log ⁡ b a log ⁡ k n ) f(n) = \Theta(n^{\log_ba}\log^kn) f(n)=Θ(nlogbalogkn),则 T ( n ) = Θ ( n log ⁡ b a log ⁡ k + 1 n ) T(n) = \Theta (n^{\log_ba}\log^{k + 1}n) T(n)=Θ(nlogbalogk+1n)
    • f ( n ) = Ω ( n log ⁡ b a + ε ) , ε > 0 f(n) = \Omega(n^{\log_b a + \varepsilon}),\varepsilon>0 f(n)=Ω(nlogba+ε),ε>0,且对于某个常数 c < 1 c < 1 c<1 和所有充分大的 n n n a f ( n b ) ≤ c f ( n ) af(\frac{n}{b})\le cf(n) af(bn)cf(n)(正则条件),那么 T ( n ) = Θ ( f ( n ) ) T(n) = \Theta (f(n)) T(n)=Θ(f(n))

部分复杂度分析实例

  • 考虑一种分治乘法:
    • 设相乘的数分别为 a , b a,b a,b,在十进制下有 n n n 位( n n n 为偶数),设 a , b a,b a,b 的前 n 2 \frac{n}{2} 2n 位分别为 a 1 , b 1 a_1,b_1 a1,b1,后 n 2 \frac{n}{2} 2n 位分别为 a 2 , b 2 a_2, b_2 a2,b2
    • a b = ( 1 0 n 2 a 1 + a 2 ) ( 1 0 n 2 b 1 + b 2 ) = 1 0 n a 1 b 1 + 1 0 n 2 ( a 1 b 2 + a 2 b 1 ) + a 2 b 2 ab = (10^{\frac{n}{2}}a_1 + a_2)(10^{\frac{n}{2}} b_1 + b_2) = 10^na_1b_1 + 10^{\frac{n}{2}}(a_1b_2 + a_2b_1) + a_2b_2 ab=(102na1+a2)(102nb1+b2)=10na1b1+102n(a1b2+a2b1)+a2b2,设
      m 1 = a 1 b 1 m 2 = a 2 b 2 m 3 = ( a 1 + a 2 ) ( b 1 + b 2 ) a 1 b 2 + a 2 b 1 = m 3 − m 1 − m 2 \begin{aligned}m_1 &= a_1b_1 \\m_2 &= a_2b_2 \\m_3 &= (a_1 + a_2)(b_1 + b_2)\\a_1b_2 + a_2b_1 &= m _3 - m_1 - m_2\\\end{aligned} m1m2m3a1b2+a2b1=a1b1=a2b2=(a1+a2)(b1+b2)=m3m1m2则只需做 3 次乘法,有 T ( n ) = 3 T ( n 2 ) + n T(n) = 3T(\frac{n}{2}) + n T(n)=3T(2n)+n,应用主定理 T ( n ) = Θ ( n log ⁡ 2 3 ) T(n) = \Theta (n^{\log_23}) T(n)=Θ(nlog23)
    • Strassen算法 用类似的方法可设计出分治矩阵乘法,将 2 × 2 2 \times 2 2×2 矩阵乘法的 8 次乘法运算降至 7 次,从而将时间复杂降至 T ( n ) = 7 T ( n 2 ) + 18 ( n 2 ) 2 T(n) = 7T(\frac{n}{2}) + 18(\frac{n}{2})^2 T(n)=7T(2n)+18(2n)2,从而求得 T ( n ) = Θ ( n log ⁡ 2 7 ) T(n) = \Theta(n^{\log_27}) T(n)=Θ(nlog27)
  • 已知 T ( n ) = 2 T ( n ) + log ⁡ n T(n) = 2T(\sqrt n) + \log n T(n)=2T(n )+logn,求 T ( n ) T(n) T(n)
    • k = log ⁡ n k = \log n k=logn,则 T ( 2 k ) = 2 T ( 2 k 2 ) + k T(2^k) = 2T(2^{\frac{k}{2}}) + k T(2k)=2T(22k)+k
    • 再设 S ( k ) = T ( 2 k ) S(k) = T(2^k) S(k)=T(2k),则 S ( k ) = 2 S ( k 2 ) + k S(k) = 2S(\frac{k}{2}) + k S(k)=2S(2k)+k,则 S ( k ) = Θ ( k log ⁡ k ) S(k) = \Theta(k \log k) S(k)=Θ(klogk)
    • T ( n ) = Θ ( log ⁡ n log ⁡ log ⁡ n ) T(n) = \Theta(\log n \log \log n) T(n)=Θ(lognloglogn)

平面最近点对

  • 将所有点按 x x x 坐标排序,分治时二路归并实现按 y y y 坐标排序。
  • 设当前区间为 [ l , r ] [l,r] [l,r] ,取中点 m i d mid mid,其 x x x x m i d x_{mid} xmid,设分治得到 [ l , m i d ] [l,mid] [l,mid] [ m i d + 1 , r ] [mid + 1,r] [mid+1,r] 内最近点对距离的最小值为 δ \delta δ,我们只需取出所有满足 ∣ x i − x m i d ∣ < δ |x_i - x_{mid}| < \delta xixmid<δ 的点 i i i 组成一个点集,并检查点集内部比每个点 y y y 坐标小且相差不超过 δ \delta δ 的点更新 δ \delta δ 即可。
  • 运用鸽巢原理可以证明,对于点集内的每个点,每次检查的点数为 O ( 1 ) \mathcal O(1) O(1),总时间复杂度 Θ ( n log ⁡ n ) \mathcal \Theta(n \log n) Θ(nlogn)
#include 

template <class T>
inline void read(T &res)
{
	char ch; bool flag = false; res = 0;
	while (ch = getchar(), !isdigit(ch) && ch != '-');
	ch == '-' ? flag = true : res = ch ^ 48;
	while (ch = getchar(), isdigit(ch))
		res = res * 10 + ch - 48;
	flag ? res = -res : 0;
}

typedef long long ll;
typedef long double ld;
const ld Maxn = 4e18;
const int N = 2e5 + 5;
int n, tn;

template <class T>
inline T Min(T x, T y) {return x < y ? x : y;}
template <class T>
inline void CkMin(T &x, T y) {x > y ? x = y : 0;} 
template <class T>
inline T Abs(T x) {return x < 0 ? -x : x;}

struct point
{
	int x, y;
	
	point() {}
	point(int X, int Y):
		x(X), y(Y) {}
		
	inline ld dist()
	{
		return sqrt((ld)x * x + (ld)y * y); 
	}
	
	inline void scan()
	{
		read(x);
		read(y);
	}
	
	inline point operator - (const point &a) const
	{
		return point(x - a.x, y - a.y);
	}
}p[N], t[N];

inline bool cmpx(const point &a, const point &b)
{
	return a.x < b.x || a.x == b.x && a.y < b.y;
}

inline bool cmpy(const point &a, const point &b)
{
	return a.y < b.y || a.y == b.y && a.x < b.x;
}

inline ld solve(int l, int r)
{
	if (l == r)
		return Maxn;
	int mid = l + r >> 1, xmid = p[mid].x;
	ld dist = Min(solve(l, mid), solve(mid + 1, r));
	std::inplace_merge(p + l, p + mid + 1, p + r + 1, cmpy);
	tn = 0;
	for (int i = l; i <= r; ++i)
		if (Abs(p[i].x - xmid) < dist)
		{
			for (int j = tn; j >= 1 && p[i].y - t[j].y < dist; --j)
				CkMin(dist, (p[i] - t[j]).dist());
			t[++tn] = p[i];
		}
	return dist;
}

int main()
{
	read(n);
	for (int i = 1; i <= n; ++i)
		p[i].scan();
	std::sort(p + 1, p + n + 1, cmpx);
	printf("%.4lf\n", (double)solve(1, n));
}

求序列第 k 小数

  • STL \text{STL} STLnth_element 函数实现原理,可用类似快速排序的 Partition 函数求解该问题。
  • 设总时间复杂度为 T ( n ) T(n) T(n),假设我们严格随机地选取 p i v o t pivot pivot,有递推式:
    T ( n ) = 1 n ∑ i = 1 n − 1 T ( i ) + n ⇔ n T ( n ) = ∑ i = 1 n − 1 T ( i ) + n 2 T(n) = \frac{1}{n}\sum \limits_{i = 1}^{n - 1}T(i) + n \Leftrightarrow nT(n) = \sum \limits_{i = 1}^{n - 1}T(i) + n^2 T(n)=n1i=1n1T(i)+nnT(n)=i=1n1T(i)+n2
  • 另取 ( n − 1 ) T ( n − 1 ) = ∑ i = 1 n − 2 T ( i ) + ( n − 1 ) 2 (n - 1)T(n - 1) = \sum \limits_{i = 1}^{n - 2}T(i) + (n - 1)^2 (n1)T(n1)=i=1n2T(i)+(n1)2,错位相消可得:
    T ( n ) = T ( n − 1 ) + 2 − 1 n ⇒ T ( n ) = O ( n ) T(n) = T(n - 1) + 2 - \frac{1}{n} \Rightarrow T(n) = \mathcal O(n) T(n)=T(n1)+2n1T(n)=O(n)
  • 该算法最坏情况下复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2),但概率极低,可忽略不计。
inline int Rand(int l, int r)
{
	return 1ll * rand() * rand() % (r - l) + l;
}

inline int Partition(int *a, int l, int r)
{
	std::swap(a[l], a[Rand(l, r)]);
	int pivot = a[l];
	while (l < r)
	{
		while (l < r && a[r] >= pivot)
			--r;
		a[l] = a[r];
		while (l < r && a[l] <= pivot)
			++l;
		a[r] = a[l];
	}
	a[l] = pivot;
	return l;
}

inline void nthElement(int *a, int l, int r, int k)
{
	if (l == r)
		return ;
	int pos = Partition(a, l, r);
	int cnt = pos - l + 1;
	if (k == cnt)
		return ;
	else if (k > cnt)
		nthElement(a, pos + 1, r, k - cnt);
	else 
		nthElement(a, l, pos - 1, k);
}

网格图最短路

  • 给定一个总结点数为 S S S 的网格图,边权均为正, q q q 次询问两点间的最短距离,考虑一种分治做法:
    • 设当前分治区域的顶点数为 S S S,取较长边的中线作划分,显然中线上的顶点数至多为 S \sqrt {S} S ,分别以中线上所有顶点为起点做单源最短路。
    • 对于跨过中线的询问,暴力枚举其最短路径跨过中线的结点即可求解。
    • 对于未跨过中线的询问,递归两个子区域求解。
    • 设做单源最短路的总时间复杂度为 T ( S ) T(S) T(S),由主定理 T ( S ) = 2 T ( S 2 ) + O ( S S log ⁡ S ) T(S) = 2T(\frac{S}{2}) + \mathcal O(S \sqrt S \log S) T(S)=2T(2S)+O(SS logS),故 T ( S ) = O ( S S log ⁡ S ) T(S) = \mathcal O(S \sqrt S \log S) T(S)=O(SS logS),总时间复杂度 O ( ( S log ⁡ S + q ) S ) \mathcal O((S \log S + q)\sqrt S) O((SlogS+q)S )
  • 把每层预处理的结果存储下来,就能支持在线操作。
  • 该分治方法可以推广到任意具有可分治性质且多次询问起终点最短路的图中。

CDQ分治

  • 设当前分治区间为 [ l , r ] [l,r] [l,r],取中点 m i d mid mid,执行如下步骤:
    • 递归 [ l , m i d ] [l,mid] [l,mid]
    • 处理 [ l , m i d ] [l,mid] [l,mid] [ m i d + 1 , r ] [mid + 1,r] [mid+1,r] 的影响。
    • 递归 [ m i d + 1 , r ] [mid + 1,r] [mid+1,r]
  • 在保证正确性的情况下,上述步骤具体执行顺序可视情况而定。

典例 [Violet]天使玩偶

题目大意

  • 动态加点,每次询问距离某点曼哈顿距离最小的点,允许离线。

解法

  • 先考虑计算每个点左下角的最近点,其余情况对坐标做变换后处理方式相同。
  • 即求时间顺序在该询问之前且横纵坐标均不大于该点的点中横纵坐标之和的最大值,显然这是一个三维偏序问题,可用 CDQ \text{CDQ} CDQ 分治解决。

线段树分治

  • 考虑下面这一问题:
    • 无向图 G 1 G_1 G1 n n n 个点 m m m 条边构成, G i ( 1 < i ≤ k ) G_i(1Gi(1<ik) G p i ( p i < i ) G_{p_i}(p_i < i) Gpi(pi<i) 增加一条边/删去一条边生成。
    • 求将 G 1 , G 2 , … , G k G_1,G_2,\dots,G_k G1,G2,,Gk 分组,使得任意两张任意两点的连通性相同的图被分在同一组。
    • 1 ≤ n , m , k ≤ 1 0 5 1 \le n,m,k \le 10^5 1n,m,k105
  • 首先我们简化任意两张图连通性的判断,定义无向图 G = ( V , E ) G = (V,E) G=(V,E) 连通性的哈希函数为:
    H ( G ) = ( ∑ G 的连通分量 G ′ = ( V ′ , E ′ ) min ⁡ x ∈ V ′ { x } ∑ x ∈ V ′ B x ) m o d    P H(G) = \left(\sum\limits_{G的连通分量G'=(V',E')}\min\limits_{x\in V'}\{x\}\sum\limits_{x\in V'}B^{x} \right)\mod P H(G)= G的连通分量G=(V,E)xVmin{x}xVBx modP
  • 其中 B , P B,P B,P 为任取的质数,我们只需将哈希值相同的图分为一组即可。
  • 显然 p i → i p_i \to i pii 构成一个有根树结构,若该树退化成 k k k 个点的链,则是经典的线段树分治。
  • 把链上每个点看做是一个时间点,则我们可以得到每条边的出现时间的区间,以时间为域建线段树,任意一个区间都可以被拆分成线段树上的 O ( log ⁡ k ) \mathcal O(\log k) O(logk) 个区间,我们将这些边挂在这些区间上并 DFS \text{DFS} DFS 线段树,用按秩合并的并查集即可实现可撤销地维护图的连通性和其哈希值,到达叶子结点时即可得到当前时间点对应的哈希值。
  • 对于一般的树结构,我们只需要 DFS \text{DFS} DFS 整棵树,将从父结点向子结点走和从子结点向父结点走视作相反的操作,即可转化成链上的问题。
  • 注意线段树叶结点的回溯。
  • 类似的思想也可以拓广到其它二叉树的数据结构上,如 Trie 等。
  • 可撤销并查集可以通过用栈记录指针和其原始值来实现,具体代码如下。
struct traceback
{
	int *addr, val;
	traceback() {}
	traceback(int *Addr, int Val):
		addr(Addr), val(Val) {}
}stk[L];

inline void Change(int &x, int v)
{
	stk[++top] = traceback(&x, x);
	x = v;
}

inline void Back(int _top)
{
	while (top != _top)
	{
		*(stk[top].addr) = stk[top].val;
		--top;
	}
}

点分治

  • 待补充,以下为统计树上距离小于 K K K 的点对数的模板。
#include 
 
template <class T>
inline void read(T &res)
{
	char ch; bool flag = false; res = 0;
	while (ch = getchar(), !isdigit(ch) && ch != '-');
	ch == '-' ? flag = true : res = ch ^ 48;
	while (ch = getchar(), isdigit(ch))
		res = res * 10 + ch - 48; 
	flag ? res = -res : 0;
}

template <class T>
inline void put(T x)
{
	if (x > 9)
		put(x / 10);
	putchar(x % 10 + 48);
}

typedef long long ll;
const int Maxn = 0x3f3f3f3f;
const int N = 4e4 + 5;
const int N2 = 8e4 + 5; 
int n, K, m, tot_sze, rt_sze, rt, ans;
int a[N], sze[N];
bool vis[N]; ll dis[N];

struct arc
{
	int to, cst; 
	arc *nxt;
}p[N2], *adj[N], *P = p;

template <class T>
inline void CkMax(T &x, T y) {if (x < y) x = y;}

inline void linkArc(int x, int y, int z)
{
	(++P)->nxt = adj[x]; adj[x] = P; P->to = y; P->cst = z;
	(++P)->nxt = adj[y]; adj[y] = P; P->to = x; P->cst = z;
}

inline void findSze(int x, int Fa)
{
	sze[x] = 1;
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (y == Fa || vis[y]) 
			continue;
		findSze(y, x);
		sze[x] += sze[y];
	}
}

inline void findG(int x, int Fa)
{
	int cnt = 0;
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (y == Fa || vis[y]) 
			continue;
		findG(y, x);
		CkMax(cnt, sze[y]);
	}
	CkMax(cnt, tot_sze - sze[x]);
	if (cnt < rt_sze)
		rt_sze = cnt, rt = x;
}

inline void findDis(int x, int Fa)
{
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (y == Fa || vis[y]) 
			continue;
		dis[y] = dis[x] + e->cst;
		if (dis[y] <= K) 
			a[++m] = dis[y];
		findDis(y, x);
	}
}

inline int calc(int x, int len)
{
	a[m = 1] = dis[x] = len;
	findDis(x, 0);
	
	std::sort(a + 1, a + m + 1);
	int r = m, res = 0;
	for (int i = 1; i < r; ++i)
	{
		while (r > i && a[r] + a[i] > K) --r;
		if (r > i) res += r - i;
	}
	return res;
}

inline void solve(int x)
{
	rt_sze = Maxn;
	findG(x, 0);
	vis[x = rt] = true;

	findSze(x, 0);
	ans += calc(x, 0);
	for (arc *e = adj[x]; e; e = e->nxt)
	{
		int y = e->to;
		if (vis[y]) 
			continue;
		ans -= calc(y, e->cst); 
		tot_sze = sze[y];
		solve(y);
	}	
}
 
int main()
{
	read(n);
	for (int i = 1, x, y, z; i < n; ++i)
	{
		read(x); read(y); read(z);
		linkArc(x, y, z);
	}
	read(K);
	
	findSze(1, 0);
	tot_sze = n;
	solve(1); 
	
	put(ans), putchar('\n'); 
	return 0;
}

你可能感兴趣的:(学习笔记,算法,分治)