算法总结之分治法

算法总结之分治法

  • 什么是分治法
  • 分治法的典型问题求解
    • 寻找旋转排序数组中的最小值
    • 二叉树距离最大值
    • 二叉树局部最小
    • 凸边形划分方式
    • 显著逆序数对

什么是分治法

分治,分治,分而治之。这句话应该是分治法的核心所在。

当给定一个问题之后,首先是观察最简单的实例。如果能够解决最简单的实例,那接下来思考能不能将大的实例分解为小的实例?能否将小实例组合形成大的实例的解?如果两个问题的答案都是能的话,那么可以说大的实例能够规约形成小的实例。通过连续执行归约操作,就能将原给定实例逐步归约成最简单的实例;然后再反方向操作,就能将最简单实例的解逐步“组合”成原给定实 例的解。

那么,如何判断能否将大的实例分解成小的实例呢?如果能的话,又该如何分解呢?我们可以通过观察问题形式化描述中“输入”部分的关键数据结构来获得一些线索:一个字符串的一部分仍然是字符串、一个集合的一部分仍然是集合、一棵树去除根节点之后会形成若干子树,以及一个图的一部分是一个子图。

进一步地,如何判断能否将小实例的解“组合”成大实例的解呢?如果能的话,又该如何组合呢?我们可以通过观察问题形式化描述中“输出”部分的形式和约束条件来获得一些线索。

因此,要想准确地判断规模较大的实例能够归约成规模较小的实例,我们既要考虑问题描述中的“输入”部分,也要考虑“输出”部分。如果能够归约的话,我们就称这个问题具有递归结构,并可以设计递归算法进行求解。而基于规约思想的算法中,最典型、最基础的就是分治法。

分治法的典型问题求解

寻找旋转排序数组中的最小值

假设按照升序排序的数组在预先未知的某个点上进行了旋转。( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
假设数组中不存在重复元素。

示例1
输入: [3,4,5,1,2]
输出: 1

示例2
输入: [4,5,6,7,0,1,2]
输出: 0

这题最初始的想法就是遍历数组将最小值储存,然后挨个比较更新最小值,这个方法肯定不是最优解,因为输入的数组是有规律的。仔细观察会发现,将数组划分之后,会存在一个子数组是严格升序,另外一个数组同样是一个旋转排序数组,最小值必定会出现在旋转排序数组中。因此可以将问题逐步递归划分,直至子数组大小为1。

class Solution { 
	public int findMin(int[] nums) { 
		return func(nums, 0, nums.length - 1); 
	} 
	public static int func(int[] nums, int l, int r) { 
		if (l == r) return nums[l];
		int mid = l + (r - l) / 2; 
		if (nums[mid] > nums[r]) 
			return func(nums, mid + 1, r); 
		else 
			return func(nums, l, mid); 
		} 
	}

时间复杂度 l o g ( n ) log(n) log(n),空间复杂度为 O ( 1 ) O(1) O(1)

二叉树距离最大值

给定一个二叉树,假设边的长度是1。求解二叉树中任意两节点间距离的最大值

示例
输入
    5
   / \
  3   6
 / \   \
2   4   7
输出:4

对于树的问题,大多数时候都是可以也只能用分治法来解决,这道问题也不例外。由于输入的树的形状不确定,节点间最大距离有三种情况:两棵子树的高度之和、左子树距离最大值、右子树距离最大值,二叉树距离最大值为这三个距离的最大值 m a x D i s t a n c e = M a x ( d e p t h l e f t + d e p t h r i g h t ) , m a x D i s t a n c e l e f t , m a x D i s t a n c e r i g h t maxDistance = Max{(depthleft + depthright), maxDistanceleft , maxDistanceright} maxDistance=Max(depthleft+depthright),maxDistanceleft,maxDistanceright

class Solution {
    public int dis;
    public int diameterOfBinaryTree(TreeNode root) {
        maxDepth(root);
        return dis;
    }
    public int maxDepth(TreeNode root) {
        if (root == null)
            return 0;
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        dis = Math.max(dis, left + right);
        return Math.max(left, right) + 1;
    }
}

需要访问到所有的节点且只访问一次,算法的时间复杂度为 O ( n ) O(n) O(n)

二叉树局部最小

给定一棵完全二叉树,定义若树中的一个点v的value,比它相连的其他节点的value都小,则将这个节点的值作为局部最小值。

示例
输入
    5
   / \
  3   6
 / \   \
2   4   7
输出:2

这也是一道与树相关的题。很显然需要分治法一层层向下寻找,对于根结点来说,有四种情况:
1.根节点比左右孩子节点都小,则返回根节点;
2.根节点比左孩子小,比右孩子大;
3.根节点比右孩子小,比左孩子大;
4.根节点比左右孩子都大。当我们选择一条value逐渐下降的路径,到达节点v,若v的左右孩子都比其大,则返回v。否则直到找到这样的v,或到达叶子节点,由于是沿着value下降的方向,所以叶子节点是满足条件的。

int TreeNode* FindLocalMin(TreeNode* root){
    if(!root->left && !root->right) return root->val;
    if(root->left->val > root->val && root->right->val > root->val)
        return root->val;
    if(root->left->val > root->val && root->right->val < root->val)
        return FindLocalMin(root->right);
    if(root->left->val < root->val && root->right->Val > root->val)
        return FindLocalMin(root->left);
    if(root->left->val < root->val && root->right->val < root->val)
        return FindLocalMinroot->left;//we can choose either left or right
}

由于每次都舍弃了一个子树,且输入为完全二叉树,因此算法的时间复杂度为 O ( l o g n ) O(log n) O(logn)

凸边形划分方式

求解凸多边形的划分方式

输入:4
输出:2

这也是一道典型的分治法求解的题。假设凸 n n n边形的各个顶点分别为 P 1 , P 2 , . . . , P n P_1,P_2,...,P_n P1,P2,...,Pn,将一条边作为基边,这里假设将 P 1 P n P_1P_n P1Pn作为基边,那么三角形的另外一个顶点必须在 P 2 , . . . , P n P_2,...,P_n P2,...,Pn中选取,假设以 P 1 P n P_1P_n P1Pn为基边的三角形另一个顶点为 P k ( 1 < k < n ) P_k(1Pk(1<k<n),那么这个三角形将凸多边形分成了一个三角形和另外两个凸多边形,两个凸多边形的顶点分别为 P 1 , . . . , P k P_1,...,P_k P1,...,Pk P k , . . . . , P n P_k,....,P_n Pk,....,Pn,即分别为凸k边形和凸n-k+1边形。将大的凸n边形分解之后,得到的划分方式数为两个小凸多边形的划分方式乘积,由于有 n − 1 n-1 n1种选三角形顶点的方式,最后得到的表达式为 F ( n ) = F ( n − 1 ) F ( 2 ) + F ( n − 2 ) F ( 3 ) + . . . . . . . + F ( 2 ) F ( n − 1 ) F(n)=F(n-1)F(2)+F(n-2)F(3)+.......+F(2)F(n-1) F(n)=F(n1)F(2)+F(n2)F(3)+.......+F(2)F(n1)

int totalCount(int n){
    vector dp(n, 0); //store the result we have computed
    						//before to reduce the complexity
    return helper(dp, n);
}
int helper(vector& dp, int n){
    if (n<2) return 0;
    if(n==2 || n==3) {
        dp[n]=1;
        return 1;
    }
    if(dp[n] != 0) result dp[n];
    int result = 0;
    for(int i=2;i

如果采用普通的迭代算法,时间复杂度为 O ( n 2 ) O(n^2) O(n2),当存储之前计算过的结果后,时间复杂度降低为 O ( n ) O(n) O(n),但相应的空间复杂度增加至 O ( n ) O(n) O(n)

显著逆序数对

给定序列 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an,如果 i < j ii<j,且有 a [ i ] > 3 a [ j ] a[i]>3a[j] a[i]>3a[j],那么称 a [ i ] , a [ j ] a[i],a[j] a[i],a[j]为显著逆序对,寻找给定数组中显著逆序对的个数。

输入:[5,1,2]
输出:1

此题是归并排序的一个变形题,由于归并排序本身就是利用的分治法,因此寻找逆序对的核心思想也是利用分治法。将序列划分为两个子序列,前一半序列中序号均大于后一半的序列,此时前一半序列中的任意值 x i x_i xi与后一半序列中的任意值 x j x_j xj存在关系 x i > 3 ∗ x j x_i>3*x_j xi>3xj,那么这就找到了一组逆序对。在比较的同时排序,会使得数组有序,可以进一步降低比较的时间复杂度。

int mergeSort(vector& nums, int left, int right){
    int mid = left+(right-left)/2, l = left;
    int r = mid, count = 0, flag = left;
    while(l (long)3*nums[r]) count += mid - l;
            else{
                for(;flag (long)3*nums[r]){
                        count += mid- flag;
                        break;
                    }
                }
            }
            r++;
        }
    }
    for(int i=left;i& nums, int left,int right){
    if(right-left<2) return 0;
    int mid = left+(right-left)/2;
    return countInversion(nums, left, mid) + countInversion(nums, mid, right) + mergeSort(nums, left, right);
}

算法的时间复杂度与归并排序的时间复杂度相同 O ( n l o g n ) O(nlogn) O(nlogn),用了一个暂时的数组存储子序列排序完后的序列,空间复杂度为 O ( n ) O(n) O(n)

你可能感兴趣的:(算法学习)