分治算法总结(未完结)

Posted raiuny

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分治算法总结(未完结)相关的知识,希望对你有一定的参考价值。

分治算法总结,CDQ分治,树状数组,三维偏序问题

分治思想:将一个问题分解成若干个结构与原问题相同的子问题。(划分子问题 + 后处理)

一、经典问题


1. 最大子段和

思路: 计算左边的最大子段和、右边的最大子段和以及跨越中界的最大子段和,然后求最大值。
每次划分左右相当于将问题二分,最多划分logn次,跨越中界求最大子段和时间复杂度是O(n),因此可得:
总体时间复杂度为O(nlogn)

int a[N];
int solve(int L, int R) 
	if (L == R) return a[L];
	int mid = (L + R) >> 1;
	int L_ans = solve(L, mid), R_ans = solve(mid+1, R);
	int maxsuf = -INF, maxpre = -INF, suf = 0, pre = 0;
	for (int i = mid; i >= L; --i) 
	 suf += a[i]; // 求最大后缀和
	 maxsuf = max(maxsuf, suf);

	for (int i = mid + 1; i <= R; ++i) 
	 pre += a[i];
	 maxpre = max(maxpre, pre); //求最大前缀和

	 return max(max(L_ans, R_ans), maxsuf + maxpre);

优化思路:划分后的每层都有重复计算部分,可以把结果用一个数组保存下来 >> DP

2. 归并排序

将一个乱序的数组分为左右两个部分,问题二分后,只需要执行将两个数组合并这一步操作, 使用双指针可实现O(n)的合并。

int a[N], tmp[N];
void merge_sort(int l, int r) 
	if (l >= r) return;
	int mid = (l + r) >> 1;
	merge_sort(l, mid);
	merge_sort(mid+1, r);
	int i = l , j = mid + 1, k = l;
	while(i <= mid && j <= r) 
		if (a[i] < a[j]) tmp[k++] = a[i++];
		else tmp[k++] = a[j++];
	
	while(i <= mid) tmp[k++] = a[i++];
	while(j <= r) tmp[k++] = a[j++];
	for (int i = l; i <= r; ++i) a[i] = tmp[i];



二、CDQ分治


CDQ分治:适用于偏序问题的离线算法。
主要思想:在分治问题中,将问题分成两个子问题,将左区间答案、右区间答案、右区间对左区间的贡献或者左区间对右区间的贡献进行合并后得到总问题的答案。
可处理的问题:求两种属性或者多种属性的元素对满足某种特定关系的数目(例如:下标满足一种特征,值满足一种特征)

2.1 求逆序对数

思路:分为左右两个区间,左边区间的下标都是小于右边区间的下标,(下标满足顺序关系),左右区间排好序后,归并时判断是否是逆序对即可
归并过程:
i < j, a[i] <= a[j] 则 左区间元素并入,左区间i号元素对右区间逆序对贡献为0
i < j, a[i] > a[j] 则 右区间元素并入,右区间j号元素对左区间的逆序对贡献为mid-i+1。
最后计算总逆序对贡献

int a[N], tmp[N];
ll CDQ(int l, int r) 
	if (l >= r) return 0;
	int mid = (l+r) >> 1;
	ll ans = CDQ(l ,mid) + CDQ(mid+1, r);
	int i = 1, j = mid + 1, k = l;
	while (i <= mid && j <= r) 
	if (a[i] <= a[j]) tmp[k++] = a[i++];
	else 
			tmp[k++] = a[j++];
			ans += mid - i + 1;
		
	
	while(i <= mid) tmp[k++] = a[i++]
	while(j <= mid) tmp[k++] = a[j++];
	for (int i = l; i <= r; ++i)
		a[i] = tmp[i];
	return ans;


可用树状数组求逆序对数

树状数组是用于维护前缀和的数据结构,有update和query两个操作,这两个操作的复杂度均为O(logn),这个问题中可以在值域上建数,类似于桶,每个桶里装这个元素的个数。
树状数组详细讲解可参考:leetcode: 有序数组、归并排序、树状数组和线段树的实现及注释

class Solution 
public:
    int reversePairs(vector<int>& nums) 
        int n = nums.size();
        vector<int> v(nums);
        sort(v.begin(), v.end());
        v.erase(unique(v.begin(), v.end()), v.end()); // 去重
        int m = v.size(); // 去重后的长度
        BIT tree(m);
        int ans = 0;
        for (int i = 0; i < n; i++) 
            nums[i] = lower_bound(v.begin(), v.end(), nums[i]) - v.begin() + 1;
         //离散化,按大小顺序给每个值编号,不影响最后的结果
        for (int i = 0; i < n; i++) 
            ans += i - tree.query(nums[i]); // i - query表示大于nums[i]的个数,即逆序个数
            tree.update(nums[i], 1);
        
        return ans;
    

	class BIT  // 树状数组 BIT (Binary Indexed Tree)查询,修改时间复杂度均为O(logn)
	public:
		BIT(int n)
			this->n = n;
			c.resize(n+1);
		
		int lowbit(int x) 
			return x & -x;
		
		void update(int x, int v)  // 在位置x处增加v
		for (int i = x; i < n+1; i += lowbit(i)) c[i] += v;
		
		int query(int x) 
			int res = 0;
			//求前缀和, 这个问题中是求小于等于x的数目,因此上面的i - query(x)就是前i个中大于x的数目,即逆序对数
			for (int i = x; i > 0; i -= lowbit(i))
				res += c[i];
			return res;
		
	private:
		int n;
		vector<int> c;
	;
;

可用线段树代替树状数组来求逆序对数:

线段树是一种二叉树,用于解决区间查询问题,例如区间最小值、区间最大值、区间和等问题。线段树的每一个节点表示一个区间,包括区间的左右端点和一些其他信息,例如区间的最小值、最大值、和等。线段树的根节点表示整个区间,每个节点的左儿子表示左区间,右儿子表示右区间,对于需要查询的区间,通过递归地访问线段树的节点,可以快速计算出区间的最小值、最大值、和等。

类似于树状数组,只是会比树状数组更大,依然查询区间和,用来求每个元素对求逆序对数的贡献,相加后得到总的逆序对数。


class Solution 
public:
    int reversePairs(vector<int>& nums) 
        int n = nums.size();
        vector<int> v(nums);
        sort(v.begin(), v.end());
        v.erase(unique(v.begin(), v.end()), v.end());
        int m = v.size();
        SegmentTree st(m);
        int ans = 0;
        for (int i = 0; i < n; i++) 
            nums[i] = lower_bound(v.begin(), v.end(), nums[i]) - v.begin() + 1;
         //离散化
        for (int i = 0; i < n; i++) 
            ans += st.calcSum(nums[i]+1, m); // 计算之前比它大的元素,逆序
            st.add(nums[i], 1);
        
        return ans;
    

class SegmentTree 
public:
	SegmentTree(int n) 
		this->n = n;
	    this->tree.resize(2*n+1);
	
	void add(int i, int v)  // 从叶子节点一直到根节点沿路+v,更新区间和
		i += n;
		while(i > 0) 
            tree[i] += v;
            i = i >> 1;
        
    
	int calcSum(int l, int r) 
		l += n, r += n; // 从叶子节点开始计算
		int sum = 0;
		while(l <= r) 
			if(l&1) sum += tree[l++]; // 右子节点,不能直接除以2,加上这个单节点后要跨一步
			if(!(r&1)) sum += tree[r--]; // 左子节点,不能直接除以2,加上这个单节点要跨一步
			l = l >> 1;
			r = r >> 1;
		
	return sum;
	
private:
    int n;
    vector<int> tree;
;
;

2.2 三维偏序问题

问题描述:给定n个元素,每个元素有三个属性a、b、c,最大属性值为k,f(i)表示对于每个i (1 ≤ i ≤ n ) ,有多少个j满足j≠i且a_j ≤ a_i, b_j ≤ b_i, c_j ≤ c_i,求对于f的每一个可能取值,有多少个i满足要求。
思路:用CDQ嵌套CDQ方式或者CDQ嵌套树状数组的方式解决、

待续

以上是关于分治算法总结(未完结)的主要内容,如果未能解决你的问题,请参考以下文章

高精总结(未完结)

快读(快速读入)有多快(未完结)

算法(未完成)

乱序版 ● 剑指offer每日算法题打卡题解——分治算法(题号17,14)

leetcode算法题基础(四十八) 分治法总结

使用分治算法在未排序的数组中查找最大和